NPM lockfiles do not handle transitive dependencies correctly - node.js

On my current project we have dependencies listed with the exact version (e. g. babel-core: '6.5.2') in our package.json file.
But after the recent project build (which includes running npm install on the build machine) some node modules were updated and our artifact got broken.
The research has shown that although we have the exact versions mentioned in package.json file, the required module might have dependency on itself through its dependency, and both of them are using the caret dependencies. In our case it appeared to be
babel-core: '6.5.2' depending on
babel-register: '^6.16.0' depending on
babel-core: '^6.9.0' resolved as babel-core: '6.26.0' (because of the caret range rules)
And the specified babel-core version breaks our builds.
We've tried creating package-lock.json files in order to prevent these transitive dependencies to be resolved the way they were resolved.
But, as the documentation on package-lock files states:
Whenever you run npm install, npm generates or updates your package lock
And thus, all the packages depending on babel-core with caret version are resolved with incorrect version of a module.
After taking a closer look at package-lock.json file, I don't see it's worth changing it because of
the snowball effect - changing one dependency requires to change all the others, depending on it (and this is recursive process)
the need of verifying both the existence of the version you're about to change to and its hash (for the integrity field)
The question is: how do I install the exact versions of all the modules, mentioned in my package.json file, preventing NPM to resolve it again with a different version?

Try using npm shrinkwrap, it is used for transitive lock.

Related

Do I also need to ignore the package-lock.json file when I ignore the node_modules folder from git? [duplicate]

npm 5 was released today and one of the new features include deterministic installs with the creation of a package-lock.json file.
Is this file supposed to be kept in source control?
I'm assuming it's similar to yarn.lock and composer.lock, both of which are supposed to be kept in source control.
Yes, package-lock.json is intended to be checked into source control. If you're using npm 5+, you may see this notice on the command line: created a lockfile as package-lock.json. You should commit this file. According to npm help package-lock.json:
package-lock.json is automatically generated for any operations where npm
modifies either the node_modules tree, or package.json. It describes the
exact tree that was generated, such that subsequent installs are able to
generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories, and serves
various purposes:
Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
Provide a facility for users to "time-travel" to previous states of node_modules without having to commit the directory itself.
To facilitate greater visibility of tree changes through readable source control diffs.
And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages.
One key detail about package-lock.json is that it cannot be published, and it
will be ignored if found in any place other than the toplevel package. It shares
a format with npm-shrinkwrap.json, which is essentially the same file, but
allows publication. This is not recommended unless deploying a CLI tool or
otherwise using the publication process for producing production packages.
If both package-lock.json and npm-shrinkwrap.json are present in the root of
a package, package-lock.json will be completely ignored.
Yes, you SHOULD:
commit the package-lock.json.
use npm ci instead of npm install when building your applications both on your CI and your local development machine
The npm ci workflow requires the existence of a package-lock.json.
A big downside of npm install command is its unexpected behavior that it may mutate the package-lock.json, whereas npm ci only uses the versions specified in the lockfile and produces an error
if the package-lock.json and package.json are out of sync
if a package-lock.json is missing.
Hence, running npm install locally, esp. in larger teams with multiple developers, may lead to lots of conflicts within the package-lock.json and developers to decide to completely delete the package-lock.json instead.
Yet there is a strong use-case for being able to trust that the project's dependencies resolve repeatably in a reliable way across different machines.
From a package-lock.json you get exactly that: a known-to-work state.
In the past, I had projects without package-lock.json / npm-shrinkwrap.json / yarn.lock files whose build would fail one day because a random dependency got a breaking update.
Those issue are hard to resolve as you sometimes have to guess what the last working version was.
If you want to add a new dependency, you still run npm install {dependency}. If you want to upgrade, use either npm update {dependency} or npm install ${dependendency}#{version} and commit the changed package-lock.json.
If an upgrade fails, you can revert to the last known working package-lock.json.
To quote npm doc:
It is highly recommended you commit the generated package lock to
source control: this will allow anyone else on your team, your
deployments, your CI/continuous integration, and anyone else who runs
npm install in your package source to get the exact same dependency
tree that you were developing on. Additionally, the diffs from these
changes are human-readable and will inform you of any changes npm has
made to your node_modules, so you can notice if any transitive
dependencies were updated, hoisted, etc.
And in regards to the difference between npm ci vs npm install:
The project must have an existing package-lock.json or npm-shrinkwrap.json.
If dependencies in the package lock do not match those in package.json, npm ci will exit with an error, instead of updating
the package lock.
npm ci can only install entire projects at a time: individual dependencies cannot be added with this command.
If a node_modules is already present, it will be automatically removed before npm ci begins its install.
It will never write to package.json or any of the package-locks: installs are essentially frozen.
Note: I posted a similar answer here
Yes, it's intended to be checked in. I want to suggest that it gets its own unique commit. We find that it adds a lot of noise to our diffs.
Yes, the best practice is to check-in (YES, CHECK-IN)
I agree that it will cause a lot of noise or conflict when seeing the diff. But the benefits are:
guarantee exact same version of every package between your dev and prod environments. This part is the most important when building in different environments at different times. You may use ^1.2.3 in your package.json, but how can you ensure each time npm install will pick up the same version in your dev machine and in the build server, especially those indirect dependency packages? Well, package-lock.json will ensure that. (With the help of npm ci which installs packages based on lock file)
it improves the installation process.
it helps with new audit feature npm audit fix.
I don't commit this file in my projects. What's the point ?
It's generated
It's the cause of a SHA1 code integrity err in gitlab with gitlab-ci.yml builds
Though it's true that I never use ^ in my package.json for libs because I had bad experiences with it.
To the people complaining about the noise when doing git diff:
git diff -- . ':(exclude)*package-lock.json' -- . ':(exclude)*yarn.lock'
What I did was use an alias:
alias gd="git diff --ignore-all-space --ignore-space-at-eol --ignore-space-change --ignore-blank-lines -- . ':(exclude)*package-lock.json' -- . ':(exclude)*yarn.lock'"
To ignore package-lock.json in diffs for the entire repository (everyone using it), you can add this to .gitattributes:
package-lock.json binary
yarn.lock binary
This will result in diffs that show "Binary files a/package-lock.json and b/package-lock.json differ whenever the package lock file was changed. Additionally, some Git services (notably GitLab, but not GitHub) will also exclude these files (no more 10k lines changed!) from the diffs when viewing online when doing this.
Yes, you can commit this file. From the npm's official docs:
package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories[.]
Yes, it's a standard practice to commit package-lock.json.
The main reason for committing package-lock.json is that everyone in the project is on the same package version.
Pros:
If you follow strict versioning and don't allow updating to major versions automatically to save yourself from backward-incompatible changes in third-party packages committing package-lock helps a lot.
If you update a particular package, it gets updated in package-lock.json and everyone using the repository gets updated to that particular version when they take the pull of your changes.
Cons:
It can make your pull requests look ugly :)
npm install won't make sure that everyone in the project is on the same package version. npm ci will help with this.
Disable package-lock.json globally
type the following in your terminal:
npm config set package-lock false
this really work for me like magic
All answers say "YES" but that also depend of the project, the doc says:
One key detail about package-lock.json is that it cannot be published, and it will be ignored if found in any place other than the toplevel package.
This mean that you don't need to publish on npm your package-lock.json for dependency but you need to use package-lock.json in your repo to lock the version of your test dependency, build dependencies…
However, If your are using lerna for managing projects with multiple packages, you should put the package.json only on the root of your repo, not in each subpackage are created with npm init. You will get something like that :
.git
lerna.json
package.json
package-lock.json <--- here
packages/a/package.json
packages/a/lib/index.js
packages/b/package.json
packages/b/lib/index.js
My use of npm is to generate minified/uglified css/js and to generate the javascript needed in pages served by a django application. In my applications, Javascript runs on the page to create animations, some times perform ajax calls, work within a VUE framework and/or work with the css. If package-lock.json has some overriding control over what is in package.json, then it may be necessary that there is one version of this file. In my experience it either does not effect what is installed by npm install, or if it does, It has not to date adversely affected the applications I deploy to my knowledge. I don't use mongodb or other such applications that are traditionally thin client.
I remove package-lock.json from repo
because npm install generates this file, and npm install is part of the deploy process on each server that runs the app. Version control of node and npm are done manually on each server, but I am careful that they are the same.
When npm install is run on the server, it changes package-lock.json,
and if there are changes to a file that is recorded by the repo on the server, the next deploy WONT allow you to pull new changes from origin. That is
you can't deploy because the pull will overwrite the changes that have been made to package-lock.json.
You can't even overwrite a locally generated package-lock.json with what is on the repo (reset hard origin master), as npm will complain when ever you issue a command if the package-lock.json does not reflect what is in node_modules due to npm install, thus breaking the deploy. Now if this indicates that slightly different versions have been installed in node_modules, once again that has never caused me problems.
If node_modules is not on your repo (and it should not be), then package-lock.json should be ignored.
If I am missing something, please correct me in the comments, but the point that versioning is taken from this file makes no sense. The file package.json has version numbers in it, and I assume this file is the one used to build packages when npm install occurs, as when I remove it, npm install complains as follows:
jason#localhost:introcart_wagtail$ rm package.json
jason#localhost:introcart_wagtail$ npm install
npm WARN saveError ENOENT: no such file or directory, open '/home/jason/webapps/introcart_devtools/introcart_wagtail/package.json'
and the build fails, however when installing node_modules or applying npm to build js/css, no complaint is made if I remove package-lock.json
jason#localhost:introcart_wagtail$ rm package-lock.json
jason#localhost:introcart_wagtail$ npm run dev
> introcart#1.0.0 dev /home/jason/webapps/introcart_devtools/introcart_wagtail
> NODE_ENV=development webpack --progress --colors --watch --mode=development
10% building 0/1 modules 1 active ...
Committing package-lock.json to the source code version control means that the project will use a specific version of dependencies that may or may not match those defined in package.json. while the dependency has a specific version without any Caret (^) and Tilde (~) as you can see, that's mean the dependency will not be updated to the most recent version. and npm install will pick up the same version as well as we need it for our current version of Angular.
Note : package-lock.json highly recommended to commit it IF I added any Caret (^) and Tilde (~) to the dependency to be updated during the CI.

confusion about npm install and npm update

I am learning about the differences between package.json and package-lock.json
I been experimenting on a package with only one dependency called chance
I first installed it via npm i chance#1.0.0 and the package.json has "chance": "^1.0.0" and package-lock.json has "version": "1.0.0".
Because I wanted to see the effect that the lock file has on the version, I went ahead and deleted package-lock.json and node_modules, I ran npm install, the version of chance stays the same in package.json, which is "chance": "^1.0.0". In the newly created lock file, the version of chance became "chance": {"version": "1.1.8",, so it updated itself.
I then deleted package-lock.json and node_modules again and ran npm update, the results seemed to be the same with the previous experiment – in package.json I have "^1.0.0" in package.json and "1.1.8" in package-lock.json
My questions are:
in either case, with "^1.0.0" in package.json and "1.1.8" in package-lock.json, which version of the dependency am I actually using in my project, I guess it is 1.1.8 right? so by merely looking at the versions in package.json is not enough to determine the exact version of the dependencies used in a project?
When does running npm install change the lock file? I know that if we delete the lock file, it will generate a new one with the newest versions in the allowable ranges from package.json. But are there any cases where npm install would change the lock file even if I didn't delete the lock file?
So, the answer is a bit complex. Essentially there are 2 things at play: The version of the package you want/need, and the version of the package that is installed.
When you are building a project, you probably don't care what specific version of a given dependency is. Most of the time you want the latest one, or the latest patch near a specific major version. The package.json is supposed to document what you, the developer, believe is required for your project to work. So, if you put in the package json "chance": "1.0.0", it would mean that only version 1.0.0 exactly is acceptable, and any other version is unacceptable. If you put "chance": "^1.0.0", it means any version compatible with 1.0.0 is acceptable. So 1.2 or 1.3 might also be fine, but 1.4 might introduce a change that breaks compatibility.
Once you decide what packages you want, by writing the package json, you run npm install. npm install can't always install exactly the versions you want. For example, imagine you want to install two packages: React v1.13 and momentJS v2.8. So you add these to your package json like this:
(Note: these version numbers and dependancies are not based on real React or Moment version numbers)
"momentJS" : "2.8",
"react" : "1.13"
then you run npm install. And you get an error: Package dependencies cannot be resolved. (or something like that). The problem is that React version 1.13 requires momentJS 2.9, but your package json specifies that you want version 2.8 exactly. You can't have both, so npm isn't able to resolve the conflict. A fix would be:
"momentJS" : "^2.8",
"react" : "1.13"
Now you are saying that you need a version of moment compatible with 2.8, and you are okay with npm adjusting that to satisfy other packages. Run npm install again and npm might install version 2.9, which satisfies both your requirement of "compatible with 1.8" and React, which wants 2.9. Now, the web app I'm currently working on has over 1,000 dependancies total, so npm absolutely needs to be able to adjust version numbers in order to get all of those packages to play nice.
Now there is often more than one way to solve a dependancy graph--more than one way to adjust all the version numbers to make every package happy. Your package lock file records what the current solution is and what actual packages are installed.
All the options for specifying package verions are here
Hope that helps!
Also: the second part of your question was "will npm change the lock file without me deleting it?" And the answer is: basically everytime you run npm install, npm changes the lock file. What npm does try to do is change the lock file as little as possible with each new install and keep most packages the same

What does 'npm i --package-lock-only' do?

What does npm i --package-lock-only do exactly? The documentation is a tad shy on examples. https://docs.npmjs.com/cli/v6/configuring-npm/package-locks
I'm curious to know if I have older packages in my local node_modules folder and no package-lock.json file, will npm i --package-lock-only generate a package-lock.json according to the version in my local node_modules folder or will it generate a package-lock.json with newer package versions that is consistent with the semver ranges in the package.json that's published in the npm registry.
It will determine versions of packages to install using package.json, and then create a package-lock.json file with its resolved versions if none exists, or overwrite an existing one.
Significantly, it does not actually install anything, which is what distinguishes it from regular npm install (or the aliased npm i).
Well, #Ben Wheeler is acurate, but there's a place to give a little background on this process. In regular situation the package-lock is meant for set a complete dependency tree of every package and it's dependencies in your application, so every developer on a different machine will have the exact same tree. This is important because the dependencies packages might be updated with time and if every developer will use different versions it could break your application. So every time you do "npm i" if you do have a package.lock.json it actually install the packages from there and not from package.json.
Sometimes when developers have a dependencies errors they tend to delete the lock file and the node_modules. which is not always the best option. Most of the time it's enough to update only the lock file to reflect the package.json with the flag --package-lock-only, and then you can do again "npm i" to install your packages. The lock file should be committed to your project repo so everyone can use it to have the same packages version.
package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories, and serves various purposes:
Describe a single representation of a dependency tree such that
teammates, deployments, and continuous integration are guaranteed to
install exactly the same dependencies.
Provide a facility for users to "time-travel" to previous states of
node_modules without having to commit the directory itself.
Facilitate greater visibility of tree changes through readable source
control diffs.
Optimize the installation process by allowing npm to skip repeated
metadata resolutions for previously-installed packages.
As of npm v7, lockfiles include enough information to gain a complete
picture of the package tree, reducing the need to read package.json
files, and allowing for significant performance improvements.

How to avoid npm install/update surprises?

How to safely npm install/update when deploying/upgrading ?
Problem 1 : npm install is a statefull operation that depends on the latest versions of dependencies in the time when the command is executed. This causes surprises when deploying since package.json file indicates ranges but not specific versions.
Problem 2 : everytime I make npm update or use ncu, I spend hours/days trying to handle incoherences between modules. Why would this happens in 2018 ?
Problem 3 : How to have package.json file that describes exactly the state of installed packages instead of ranges so that I can deploy without surprises ?
NB: I use Angular
If you use yarn or a more recent version of npm, it will generate for you a yarn.lock or package-lock.json.
This will keep exactly the version of any package when it's first installed, so further calls to yarn or npm install will fetch and install exactly those versions.
Of course you should add these lock files to your repository so anyone doing a fresh clone get the same dependencies installed.
See the npm docs: https://docs.npmjs.com/files/package-lock.json
And the yarn docs: https://yarnpkg.com/lang/en/docs/yarn-lock/
package.json file indicates ranges but not specific versions : Re-read the documentation, you can specify specific versions. See point 3 for an example.
Why would this happens in 2018 <= I/we can't speculate as to problems where you did not include any specific details, it might be a valid general gripe you have but StackOverflow is not the correct place to vent it.
Again, see the documentation. You just have to include the version number with an = sign. Example below would get only the version 5.0.0 of #angular/cdk.
"#angular/cdk": "5.0.0"
be advised that any call to npm update will update your package.json with the latest minor version, so setting a strict constraint version ex (5.0.0) will only work with npm install when no package-lock.json is present. When doing npm update, your 5.0.0 constraint will be replaced by a ^5.5.0 constraint (or whatever the next minor release is). It's fine if the packages you are using implement semver correctly, but you can have a lot of issues with breaking changes on minor release.

Why does "npm install" rewrite package-lock.json?

I just recently upgraded to npm#5. I now have a package-lock.json file with everything from package.json. I would expect that, when I run npm install that the dependency versions would be pulled from the lock file to determine what should be installed in my node_modules directory. What's strange is that it actually ends up modifying and rewriting my package-lock.json file.
For example, the lock file had typescript specified to be at version 2.1.6. Then, after the npm install command, the version was changed to 2.4.1. That seems to defeat the whole purpose of a lock file.
What am I missing? How do I get npm to actually respect my lock file?
Update 3: As other answers point out as well, the npm ci command got introduced in npm 5.7.0 as additional way to achieve fast and reproducible builds in the CI context. See the documentation and npm blog for further information.
Update 2: The issue to update and clarify the documentation is GitHub issue #18103.
Update 1: The behaviour that was described below got fixed in npm 5.4.2: the currently intended behaviour is outlined in GitHub issue #17979.
Original answer (pre-5.4.2): The behaviour of package-lock.json was changed in npm 5.1.0 as discussed in issue #16866. The behaviour that you observe is apparently intended by npm as of version 5.1.0.
That means that package.json can override package-lock.json whenever a newer version is found for a dependency in package.json. If you want to pin your dependencies effectively, you now must specify the versions without a prefix, e.g., you need to write them as 1.2.0 instead of ~1.2.0 or ^1.2.0. Then the combination of package.json and package-lock.json will yield reproducible builds. To be clear: package-lock.json alone no longer locks the root level dependencies!
Whether this design decision was good or not is arguable, there is an ongoing discussion resulting from this confusion on GitHub in issue #17979. (In my eyes it is a questionable decision; at least the name lock doesn't hold true any longer.)
One more side note: there is also a restriction for registries that don’t support immutable packages, such as when you pull packages directly from GitHub instead of npmjs.org. See this documentation of package locks for further explanation.
I've found that there will be a new version of npm 5.7.1 with the new command npm ci, that will install from package-lock.json only
The new npm ci command installs from your lock-file ONLY. If your package.json and your lock-file are out of sync then it will report an error.
It works by throwing away your node_modules and recreating it from scratch.
Beyond guaranteeing you that you'll only get what is in your lock-file it's also much faster (2x-10x!) than npm install when you don't start with a node_modules.
As you may take from the name, we expect it to be a big boon to continuous integration environments. We also expect that folks who do production deploys from git tags will see major gains.
Short Answer:
npm install honors package-lock.json only if it satisfies the requirements of package.json.
If it doesn't satisfy those requirements, packages are updated & package-lock is overwritten.
If you want the install to fail instead of overwriting package-lock when this happens, use npm ci.
Here is a scenario that might explain things (Verified with NPM 6.3.0)
You declare a dependency in package.json like:
"depA": "^1.0.0"
Then you do, npm install which will generate a package-lock.json with:
"depA": "1.0.0"
Few days later, a newer minor version of "depA" is released, say "1.1.0", then the following holds true:
npm ci # respects only package-lock.json and installs 1.0.0
npm install # also, respects the package-lock version and keeps 1.0.0 installed
# (i.e. when package-lock.json exists, it overrules package.json)
Next, you manually update your package.json to:
"depA": "^1.1.0"
Then rerun:
npm ci # will try to honor package-lock which says 1.0.0
# but that does not satisfy package.json requirement of "^1.1.0"
# so it would throw an error
npm install # installs "1.1.0" (as required by the updated package.json)
# also rewrites package-lock.json version to "1.1.0"
# (i.e. when package.json is modified, it overrules the package-lock.json)
Use the newly introduced
npm ci
npm ci promises the most benefit to large teams. Giving developers the ability to “sign off” on a package lock promotes more efficient collaboration across large teams, and the ability to install exactly what is in a lockfile has the potential to save tens if not hundreds of developer hours a month, freeing teams up to spend more time building and shipping amazing things.
Introducing npm ci for faster, more reliable builds
Use the npm ci command instead of npm install.
"ci" stands for "clean install".
It will install the project dependencies based on the package-lock.json file instead of the lenient package.json file dependencies.
It will produce identical builds to your team mates and it is also much faster.
You can read more about it in this blog post:
https://blog.npmjs.org/post/171556855892/introducing-npm-ci-for-faster-more-reliable
It appears this issue is fixed in npm v5.4.2
https://github.com/npm/npm/issues/17979
(Scroll down to the last comment in the thread)
Update
Actually fixed in 5.6.0. There was a cross platform bug in 5.4.2 that was causing the issue to still occur.
https://github.com/npm/npm/issues/18712
Update 2
See my answer here:
https://stackoverflow.com/a/53680257/1611058
npm ci is the command you should be using when installing existing projects now.
In the future, you will be able to use a --from-lock-file (or similar) flag to install only from the package-lock.json without modifying it.
This will be useful for CI, etc. environments where reproducible builds are important.
See https://github.com/npm/npm/issues/18286 for tracking of the feature.
Probably you should use something like this
npm ci
Instead of using npm install
if you don't want to change the version of your package.
According to the official documentation, both npm install and npm ci install the dependencies which are needed for the project.
The main difference is, npm install does install the packages taking packge.json as a reference. Where in the case of npm ci, it does install the packages taking package-lock.json as a reference, making sure every time the exact package is installed.
You probably have something like:
"typescript":"~2.1.6"
in your package.json which npm updates to the latest minor version, in your case being 2.4.1
Edit: Question from OP
But that doesn't explain why "npm install" would change the lock file. Isn't the lock file meant to create a reproducible build? If so,
regardless of the semver value, it should still use the same 2.1.6
version.
Answer:
This is intended to lock down your full dependency tree. Let's say typescript v2.4.1 requires widget ~v1.0.0. When you npm install it
grabs widget v1.0.0. Later on your fellow developer (or CI build)
does an npm install and gets typescript v2.4.1 but widget has been
updated to widget v1.0.1. Now your node module are out of sync. This
is what package-lock.json prevents.
Or more generally:
As an example, consider
package A:
{ "name": "A", "version": "0.1.0", "dependencies": {
"B": "<0.1.0" } }
package B:
{ "name": "B", "version": "0.0.1", "dependencies": {
"C": "<0.1.0" } }
and package C:
{ "name": "C", "version": "0.0.1" }
If these are the only versions
of A, B, and C available in the registry, then a normal npm install A
will install:
A#0.1.0 -- B#0.0.1
-- C#0.0.1
However, if B#0.0.2 is published, then a fresh npm install A will install:
A#0.1.0 -- B#0.0.2
-- C#0.0.1 assuming the new version did not modify B's dependencies. Of course, the new version of B could include a new
version of C and any number of new dependencies. If such changes are
undesirable, the author of A could specify a dependency on B#0.0.1.
However, if A's author and B's author are not the same person, there's
no way for A's author to say that he or she does not want to pull in
newly published versions of C when B hasn't changed at all.
OP Question 2: So let me see if I understand correctly. What you're
saying is that the lock file specifies the versions of the secondary
dependencies, but still relies on the fuzzy matching of package.json
to determine the top-level dependencies. Is that accurate?
Answer: No. package-lock locks the entire package tree, including the
root packages described in package.json. If typescript is locked
at 2.4.1 in your package-lock.json, it should remain that way until it is
changed. And lets say tomorrow typescript releases version 2.4.2.
If I checkout your branch and run npm install, npm will respect the
lockfile and install 2.4.1.
More on package-lock.json:
package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories, and serves various purposes:
Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
Provide a facility for users to "time-travel" to previous states of node_modules without having to commit the directory itself.
To facilitate greater visibility of tree changes through readable source control diffs.
And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages.
https://docs.npmjs.com/files/package-lock.json
EDIT: the name "lock" is a tricky one, its NPM trying to catch up with Yarn. It isn't a locked file whatsoever. package.json is a user-fixed file, that once "installed" will generate node_modules folder tree and that tree will then be written in package-lock.json. So you see, its the other way around - dependency versions will be pulled from package.json as always, and package-lock.json should be called package-tree.json
(hope this made my answer clearer, after so many downvotes)
A simplistic answer: package.json have your dependencies as usual, while package-lock.json is "an exact, and more importantly reproducible node_modules tree" (taken from npm docs itself).
As for the tricky name, its NPM trying to catch up with Yarn.
There is an open issue for this on their github page: https://github.com/npm/npm/issues/18712
This issue is most severe when developers are using different operating systems.
Npm install detects any changes made to package.json file to reflect the dependency list accordingly.
Ex. If user added or removed a new dependency, the build will download or remove the dependencies in the local computer. We can compare this to .m2 repository in java where maven keeps track of pom.xml file constantly to update the dependencies.
package-lock.json is a replica of package.json used at run-time by internal processes, only difference is package-lock.json is read-only to user.

Resources