'How can we safely avoid clashes between a local and a global npm package for command line tools, that call the target file by require()?

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/[email protected]
npm ls -g @ecmake/ecmake

# install locally
npm install --save-dev @ecmake/[email protected]
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 code but they are different copies of the same code.

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);

[...]

We can verify that actually both libraries have been called by putting a logging instruction into both.

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 to global ecmake to local npx ecmake by running 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 challenge (apart from those I already named with their disadvantages)?



Solution 1:[1]

As a first part of my own answer, I do investigate, why a canonical solution is unlikely.

No canonical solution

The makefile creates a data model. The runner loads und inspects this data model and runs the instructions stored inside it. Both, the runner and the data model, need a library. There is a global npm installation and a local one. This already gives four possible combinations.

The ecmake runner provides a --base directory option modelled after the original make tool. The meaning is, "change into the named directory before doing anything". This gives a second place for a local library. We are at three times three combinations.

I will name several strategies of solutions. Combining this all, there are dozens of more or less reasonable options. This makes a canonical answer unlikely. None-the-less, the fundamental challenges of this example generally apply to many command-line tools.

Exact answers

Answers that precisely address the original questions are those strategies, that decouple the model and the runner or take care, that only one instance of the library is called.

Additional constraints

Real life doesn't answer questions that exactly. In case of ecmake the search for solutions brought up additional constraints. I want to make sure, that versions of the model and the runner match the major version, the makefile was coded against. Version conflicts at major version changes should be avoided beforehand.

Hence, the makefile should be bundled with the appropriate version in package.json. If there is only a global installation, it should try to run, but a warning of a missing local installation should be given.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 halfer