Saturday, 18 December 2021

How can we safely avoid clashes between a local and a global npm package for command line tools?

Conflicts of a global and a local installation

I am working on the npm commandline tool and package https://github.com/ecma-make/ecmake. I ran into a strange conflict between a globally and a locally installed version of the package.

I can avoid this conflict by linking the one against the library of the other. Then there is only one instance of the library and no conflict. Now I have to think of the user, who does install the package into both places, once globally to be able to run the command without the npx prefix, once locally to have the library listed in the dev section of package.json.

How to reproduce

# prepare test fixture
mkdir ecmakeTest
cd ecmakeTest/
npm init -y

# install globally
npm install -g @ecmake/ecmake@0.3.1
npm ls -g @ecmake/ecmake

# install locally
npm install --save-dev @ecmake/ecmake@0.3.1
npm ls @ecmake/ecmake

# init ecmakeCode.js
npx ecmake --init

# run with local lib => shows the expected behaviour
npx ecmake all 

# run with global lib => NoRootTaskError
ecmake all

Origin of the conflict

The stack trace guides us to the line within the global installation: /usr/local/lib/node_modules/@ecmake/ecmake/lib/runner/reader.js:21:13.

    if (!(root instanceof Task)) {
      throw new Reader.NoRootTaskError(this.makefile);
    }

What did happen?

The object root created with the local library was checked against the class definition of the global library. They have the same name but they are different.

The global ecmake runner requires the local makefile ecmakeCode.js. This file in turn requires the Task definition of the local library.

const root = module.exports = require('@ecmake/ecmake').makeRoot();

root.default
  .described('defaults to all')
  .awaits(root.all);

[...]

How do others solve this?

Gulp and Grunt export a function, that takes the actual dependency by injection. While dependency injection is generally very smart, in this case it's not that pretty. The whole file gets wrapped. I would like to avoid this wrapping function.

See: https://gulpjs.com/docs/en/getting-started/quick-start#create-a-gulpfile

See: https://gruntjs.com/getting-started

What I already considered

The runner could first check, if there is such a conflict. In case it could delegate the arguments given on the command line to npx ecmake by creating a child process.

Alas, this would slow down the runner. At least one subprocess is required, maybe more to check the situation.

The question

Do you have a general solution to address this situation (apart from those I already named with their disadvantages)?



from How can we safely avoid clashes between a local and a global npm package for command line tools?

No comments:

Post a Comment