npmpackage-lock.json

How does npm and package-lock.json behave, with respect to different operating systems/CPU architectures?


My understanding is that:

  1. There are certain npm packages that are intended only for use on certain operating systems.

    For example fsevents is only for macOS - it's for listening to file changes in a directory tree.

    The os and cpu properties of package.json can be used to mark said packages this way.

  2. The purpose of a package-lock.json is to have the exact same dependencies be installed on each run of npm i - this way you don't have 100 different developers all working with 100 different versions of dependencies and constant 'it works on my machine' type problems.

How does this work as it relates to an application that might be being built by some users on macOS and others on Windows?

  1. What happens if a dependency is required and but the host operating system does not match? Does npm just error?
    Does that mean that all OS-specific packages should always be marked as optional (or else the consuming package itself be marked as OS specific).

    I note that Vite for example marks fsevents as an optional dependency.

    Or does the package get installed, and it's up to the consuming code to detect OS/CPU architecture and use/not use it?

  2. What happens to the package-lock.json when two different operating systems install the dependencies?

    I assume that you wouldn't have a situation where the package-lock.json keeps changing between different OS installation runs.


Solution

  • How npm handles OS-specific dependencies (lockfiles & optional deps)

    Historically (npm v6 and older), this was a major pain point known as "Lockfile Thrashing," where a Windows user would install, commit the lockfile, and break the build for macOS users.

    However, in modern npm (v7+), this is handled via Universal Lockfiles and Optional Dependencies.

    Here is the breakdown of how it works:

    1. The "Universal" Lockfile
      You asked: What happens to the package-lock.json when two different operating systems install the dependencies?
      In modern npm, the package-lock.json is a superset blueprint. It describes the dependency tree for every possible operating system, not just the one currently running the install.

      • On macOS: npm reads the lockfile, sees fsevents (or other OS-specific binaries), notes that it matches the current OS, and downloads the tarball to node_modules.
      • On Windows: npm reads the lockfile, sees fsevents, checks the os metadata, sees it is macOS-only, and skips the download.
        Crucially: The Windows npm client does not remove the fsevents entry from package-lock.json. It leaves the metadata there so that the lockfile remains consistent for the rest of the team.
    2. optionalDependencies is key
      You asked: Does that mean that all OS-specific packages should always be marked as optional?
      Yes. If a package relies on native bindings or specific OS features (like fsevents), it should be listed in optionalDependencies.

      • If a package is in standard dependencies and the OS check fails, npm may throw EBADPLATFORM and stop the install.
      • If it is in optionalDependencies, npm attempts to install it. If the os or cpu check fails (or compilation fails), npm ignores the error and continues the installation process.
    3. Runtime Detection
      You asked: Does the package get installed, and it's up to the consuming code to detect it?
      If the OS check fails, the package folder is not created in node_modules. The code is physically missing from the disk. Therefore, the consuming library must handle this at runtime. A common pattern (used by libraries like chokidar) looks like this:

    let fsevents;
    try {
      fsevents = require('fsevents');
    } catch (error) {
      // Dependency is missing (likely on Windows/Linux)
      // Fallback to standard fs.watch
    }
    

    Summary