Node.js Package Management

Overview

IVPM provides first-class support for Node.js packages, managing both npm registry packages and source packages (linked via npm link) within a project-local Node.js environment.

Note

The Node handler performs the work described on this page. For handler-level details (activation conditions, phase ordering, how it fits into the update pipeline), see Node Handler (node) in Package Handlers.

Node Environment Structure

IVPM creates a project-local Node.js environment in packages/node/ on demand: it is only created when at least one dependency (direct or transitive) is a Node.js package. Projects that contain no Node packages never create this directory, keeping the workspace lean.

When a Node environment is created it is placed at:

packages/
└── node/
    ├── package.json         # Generated by IVPM — do not edit manually
    ├── package-lock.json    # Committed for reproducible builds
    ├── node_modules/        # Installed packages (git-ignored)
    ├── export.envrc         # direnv activation snippet
    └── .nvmrc               # Node version pin (when version: is set)

Package Manager Selection

IVPM supports three Node.js package managers:

  1. npm — default, requires Node.js to be installed

  2. pnpm — faster, disk-efficient alternative

  3. yarn — alternative package manager

Specify the manager in ivpm.yaml:

package:
  name: my-project
  with:
    node:
      manager: pnpm   # npm (default) | pnpm | yarn

Configuring the Node Handler

A project can permanently configure the Node handler by adding a with.node section inside package: in ivpm.yaml.

package:
  name: my-project

  with:
    node:
      manager: npm      # npm (default) | pnpm | yarn
      version: "20"     # Node version to pin (.nvmrc)
      env: true         # Write packages.envrc entry (default: true)

  dep-sets:
    - name: default-dev
      deps:
        - name: lodash
          src: npm

manager key

Selects the package manager for all node install / node link operations.

Value

Behaviour

npm (default)

Use npm install --prefix packages/node

pnpm

Use pnpm install --dir packages/node

yarn

Use yarn install --cwd packages/node

version key

When set, writes a .nvmrc file to packages/node/ containing the specified version string. Tools such as nvm use and volta read this file to select the correct Node runtime automatically.

with:
  node:
    version: "20"    # writes packages/node/.nvmrc

env key

Controls whether the handler writes an activation entry into packages/packages.envrc. Defaults to true. Set to false to suppress environment modification (useful for CI or projects that manage their own PATH).

with:
  node:
    env: false    # Don't patch packages.envrc

npm Registry Packages (src: npm)

Install a package from the npm registry by specifying src: npm.

deps:
  # Latest version
  - name: lodash
    src: npm

  # Specific version
  - name: react
    src: npm
    version: "^18.0.0"

  # Dev-only dependency
  - name: jest
    src: npm
    dev: true

  # Optional dependency
  - name: fsevents
    src: npm
    optional: true

Attributes:

version

npm semver range (e.g., ^18.0.0, >=1.0.0 <2.0.0). Defaults to * (latest).

dev

When true, the package is placed in devDependencies in the generated packages/node/package.json. Defaults to false.

optional

When true, marks the package as optional. Defaults to false.

Install behavior: All npm packages are installed into packages/node/node_modules/ by running the configured package manager.

Importing deps from an Existing package.json (src: package.json)

If your project already has a package.json (e.g., in the repo root or in a sub-package), you can import all its dependencies into the IVPM-managed environment without duplicating them:

deps:
  - name: root-pkg-json
    src: package.json
    url: file://${PROJECT_ROOT}/package.json

IVPM reads the dependencies and devDependencies sections of the referenced file and synthesises PackageNpm entries for each one. An explicit src: npm entry in the same dep-set always overrides a same-named entry from src: package.json (explicit entries win).

Attributes:

url

Path to the package.json file. Supports:

  • file:// prefix for absolute paths

  • ${VAR} environment variable substitution

Source Package Linking (type: node)

Source packages (git, dir) that provide a Node.js library can be linked into the managed environment using npm link so that other packages in the project can require() them directly:

deps:
  # Git-hosted TypeScript library
  - name: my-ts-lib
    url: https://github.com/org/my-ts-lib.git
    type: node

  # Link but treat as dev-only
  - name: shared-utils
    url: https://github.com/org/shared-utils.git
    type: { node: { dev: true, link: true } }

  # Source present but do NOT link (just track)
  - name: build-tool
    url: https://github.com/org/build-tool.git
    type: { node: { link: false } }

Per-package options (``type: { node: { … } }``):

dev

Treat the linked package as a dev dependency. Defaults to false.

link

When true (default), runs npm link <path> for the package after install. Set to false to track the package without linking.

Auto-detection: IVPM automatically applies type: node (with link: true) to any source package (git/dir/http/gh-rls) that contains a package.json but has no explicit type: set. This mirrors the Python handler’s pyproject.toml auto-detection.

Generated packages/node/package.json

IVPM synthesises a packages/node/package.json from all collected npm packages. The file is written during ivpm update and should be committed to your repository (together with package-lock.json) for reproducible builds.

Example generated file:

{
  "name": "ivpm-node-env",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "lodash": "^4.17.21",
    "axios": "^1.0.0"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "typescript": "^5.0.0"
  }
}

Note

Do not edit packages/node/package.json manually — IVPM regenerates it on every ivpm update run. Put all dependency declarations in ivpm.yaml.

Hash-based skip: IVPM stores a SHA-256 hash of the generated package.json in packages/ivpm.json. On subsequent runs, if the hash matches and node_modules/ already exists, the expensive npm install step is skipped automatically, making repeated ivpm update calls fast.

Environment Activation

Manual activation

Without direnv, source the export file manually:

$ source packages/node/export.envrc

Windows

On Windows, IVPM additionally writes:

  • packages/node/activate_node.bat — adds node_modules/.bin to %PATH%

  • packages/node/activate_node.ps1 — PowerShell equivalent

  • packages/packages_activate.bat / .ps1 — top-level wrappers

:: In a .bat script or CMD session
call packages\packages_activate.bat

:: In PowerShell
. packages\packages_activate.ps1

Node Version Management

When version: is configured, IVPM writes a .nvmrc file:

with:
  node:
    version: "20"
# .nvmrc written to packages/node/.nvmrc
$ cat packages/node/.nvmrc
20

# nvm picks up the version automatically
$ cd packages/node && nvm use
Now using node v20.x.x

# volta also reads .nvmrc
$ volta pin node@$(cat packages/node/.nvmrc)

To ensure the correct Node.js version is active before running IVPM:

$ nvm use 20 && ivpm update

Lockfile Recommendations

Commit packages/node/package-lock.json (or pnpm-lock.yaml / yarn.lock for other managers) to version control. This ensures every developer and CI system installs the exact same package versions.

IVPM will warn if package-lock.json is present but git-ignored:

WARNING: packages/node/package-lock.json is gitignored.
Consider committing it for reproducible builds.

Add to .gitignore (node_modules only, not the lockfile):

packages/node/node_modules/

Complete Examples

Example 1: Front-end Tooling

package:
  name: my-webapp
  default-dep-set: default-dev

  with:
    node:
      manager: npm
      version: "20"

  dep-sets:
    - name: default-dev
      deps:
        # Runtime
        - name: react
          src: npm
          version: "^18.0.0"
        - name: react-dom
          src: npm
          version: "^18.0.0"

        # Dev tools
        - name: typescript
          src: npm
          version: "^5.0.0"
          dev: true
        - name: jest
          src: npm
          version: "^29.0.0"
          dev: true
        - name: "@types/react"
          src: npm
          version: "^18.0.0"
          dev: true

Usage:

$ ivpm update
$ tsc --noEmit          # type-check (tsc on PATH via direnv)
$ jest                  # run tests
$ node -e "require('react')"  # verify install

Example 2: Mixed Python + Node Project

package:
  name: fullstack-app
  default-dep-set: default-dev

  with:
    python:
      venv: uv
    node:
      manager: npm

  dep-sets:
    - name: default-dev
      deps:
        # Python backend
        - name: fastapi
          src: pypi
        - name: uvicorn
          src: pypi

        # Node frontend build
        - name: webpack
          src: npm
          dev: true
        - name: babel-loader
          src: npm
          dev: true

        # Shared TypeScript library (source)
        - name: shared-types
          url: https://github.com/org/shared-types.git
          type: node

Usage:

$ ivpm update
$ uvicorn app:main &         # Python backend
$ webpack --config webpack.config.js   # Node build

Example 3: Importing from Existing package.json

package:
  name: existing-project
  default-dep-set: default-dev

  dep-sets:
    - name: default-dev
      deps:
        # Import all deps from the repo's root package.json
        - name: root-deps
          src: package.json
          url: file://${PROJECT_ROOT}/package.json

        # Override one specific version
        - name: lodash
          src: npm
          version: "^4.17.21"   # Overrides whatever is in package.json

Best Practices

  1. Commit lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock) for reproducible builds; never commit node_modules/.

  2. Pin Node.js version using with.node.version and .nvmrc for consistent environments.

  3. Separate dev deps with dev: true to keep production installs lean.

  4. Use auto-detection — source packages with package.json are detected automatically; only add type: node when you need to change options.

  5. Let IVPM own packages/node/package.json — declare all deps in ivpm.yaml and never edit the generated file directly.

  6. Use pnpm for monorepos with many shared sub-packages; it handles symlink-based deduplication better than npm.

See Also