Skip to content

feat(config-deps): support optionalDependencies with platform filtering#11725

Merged
zkochan merged 16 commits into
mainfrom
config-deps-optional-deps
May 18, 2026
Merged

feat(config-deps): support optionalDependencies with platform filtering#11725
zkochan merged 16 commits into
mainfrom
config-deps-optional-deps

Conversation

@zkochan

@zkochan zkochan commented May 18, 2026

Copy link
Copy Markdown
Member

Summary

Extends configDependencies to resolve and install one level of optionalDependencies, with os / cpu / libc platform filtering applied at install time. Closes the prerequisite called out in #11723: this is what makes the esbuild/swc-style platform-binary pattern viable for config dependencies (e.g. shipping pacquet as a config dep with native binaries via optionalDependencies).

What lands

  • Resolution (resolveOptionalSubdeps.ts, wired into resolveConfigDeps and resolveAndInstallConfigDeps): after each top-level config dep resolves, walks one level of optionalDependencies, resolves each, and records them in the env lockfile with os/cpu/libc preserved. The parent's snapshot gets optionalDependencies: { … }. All variants are recorded regardless of host platform, so the env lockfile stays portable across machines.
  • Install (installConfigDeps.ts): after the parent is installed into its GVS leaf, fetches each platform-compatible subdep into its own GVS leaf and creates a sibling symlink inside the parent leaf's node_modules/. Node's realpath-based resolution then makes require('pkg-platform-arch') from inside the parent resolve correctly. Stale siblings are pruned, so platform changes between runs produce a clean layout.
  • GVS hash (new calcGlobalVirtualStorePathWithSubdeps in graph-hasher): the parent's GVS leaf hash now folds in the optional subdeps' full pkg ids. Without this, changing a subdep version while keeping the parent pinned would land in the same leaf and silently overwrite the sibling symlinks. The leaf function keeps its original "no children" contract; the new function is a separate entry point that pacquet can mirror cleanly.
  • Re-install detection: the "skip if already installed" check compares the existing .pnpm-config/{name} symlink's realpath against the expected GVS leaf, not the package.json's name/version. With subdep versions now feeding the leaf hash, name/version alone isn't sufficient. The check only short-circuits the parent's re-import and re-symlink — installOptionalSubdeps always runs so platform-specific siblings get pruned and relinked when the host's effective platform changes (Rosetta x64 ↔ arm64, etc.).
  • Exact versions only: subdep specifiers must be valid semver exact versions (e.g. "1.2.3"). Ranges ("^1.0.0") and tags ("latest") are rejected up-front with a CONFIG_DEP_OPTIONAL_NOT_EXACT error. With the parent pinned by integrity, the subdep's resolved version mustn't drift between machines.
  • Error handling: optional-subdep resolution failures are logged via skippedOptionalDependencyLogger with reason: 'resolution_failure' (same shape as installing/deps-resolver) and the install continues — except for ERR_PNPM_TRUST_DOWNGRADE, which is a security signal that must still abort the install.

Scope

Only one level deep. Transitive dependencies and lifecycle scripts remain unsupported — pacquet doesn't need them yet, and they carry meaningful security and complexity tradeoffs that deserve a separate discussion.

The env lockfile schema needs no changes: LockfilePackageInfo already carries os/cpu/libc, and LockfilePackageSnapshot.optionalDependencies already exists for recording the parent→child edge.

Known limitation

If a workspace already had a resolved config dep in the env lockfile (snapshots[pkgKey] = {}) before this PR, optional subdeps won't be retroactively discovered on subsequent installs. Workaround: pnpm update <pkg> (or remove + re-add). In practice no published package today relies on optionalDependencies in a config dep — they couldn't, since the feature didn't exist — so the practical exposure is narrow. See the inline review thread for the design rationale.

Test plan

  • resolveConfigDeps records all platform variants with os/cpu/libc preserved (incl. libc for musl variants).
  • Config dep with no optionalDependencies still produces an empty snapshot.
  • installConfigDeps symlinks the platform-matching subdep and skips non-matching ones (covers both os and cpu mismatches).
  • Sibling subdep is reachable via createRequire(parent).resolve('subdepName').
  • Changing only an optional subdep version re-installs the parent into a new GVS leaf and re-symlinks .pnpm-config/{name}.
  • Removing a sibling symlink and re-running install restores it (skip-check passes but subdep step still runs).
  • Range/tag specifiers in optionalDependencies are rejected with a clear error.
  • calcLeafGlobalVirtualStorePath unit test covers the leaf-only contract.
  • calcGlobalVirtualStorePathWithSubdeps unit tests: equals leaf path when subdeps is empty; differs on subdep version change, addition, and key-order independence.
  • Existing installConfigDeps and resolveConfigDeps tests still pass.
  • Docs updated for configDependencies in pnpm.io — separate PR to follow.
  • pacquet-side parity tracked separately (pacquet doesn't currently implement config deps).

Written by an agent (Claude Code, claude-opus-4-7).

zkochan added 2 commits May 18, 2026 18:41
Config dependencies can now declare one level of optionalDependencies
with os/cpu/libc constraints, like the esbuild/swc platform-binary
pattern. The env lockfile records all variants so it stays portable
across machines; the installer picks the matching binary at install
time and symlinks it next to the parent in the global virtual store,
making `require('pkg-platform-arch')` resolvable from the parent.

Refs #11723.
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Config dependencies now record and install one level of optionalDependencies (with platform selectors), include optional-subdep fingerprints in global virtual store leaf paths, and symlink platform-matching optional subdeps into the parent package's node_modules; env lockfile snapshots and packages preserve platform fields.

Changes

Optional sub-dependencies for config dependencies

Layer / File(s) Summary
Type contracts for normalized config dependencies and subdependencies
installing/env-installer/src/parseIntegrity.ts
Introduces NormalizedConfigDep with optional optionalSubdeps and NormalizedSubdep with optional os/cpu/libc selectors and resolution metadata.
Optional sub-dependency resolution from npm
installing/env-installer/src/resolveOptionalSubdeps.ts
New module resolves optional sub-dependencies in parallel via npm resolver, ignores failures per optional semantics, enforces exact-version specs, validates integrity, writes platform-aware package entries to envLockfile.packages, and returns resolved versions or undefined.
Integration of optional subdeps resolution into lockfile building
installing/env-installer/src/resolveConfigDeps.ts, installing/env-installer/src/resolveAndInstallConfigDeps.ts
Calls resolveOptionalSubdeps during package resolution and stores returned optionalDependencies under envLockfile.snapshots[pkgKey] when present; otherwise writes an empty snapshot.
Optional subdependency installation with platform filtering
installing/env-installer/src/installConfigDeps.ts
Reads normalized optionalSubdeps from lockfile, builds optionalSubdepIds for leaf hashing, filters them using packageIsInstallable (os/cpu/libc), prunes unexpected sibling entries, imports missing packages into the global virtual store, and symlinks installable subdeps into the parent node_modules.
Global virtual store path hashing includes subdep fingerprints
deps/graph-hasher/src/index.ts, deps/graph-hasher/test/calcLeafGlobalVirtualStorePath.test.ts
calcLeafGlobalVirtualStorePath accepts a deps map and includes child fingerprints in the hash; tests assert sensitivity to version changes, adding subdeps, and order-independence.
Resolution and installation test coverage
installing/env-installer/test/installConfigDeps.ts, installing/env-installer/test/resolveConfigDeps.test.ts
Tests confirm platform-matching optional subdeps are recorded and installed and that non-matching os/cpu selectors are skipped; includes Node resolution checks from within the parent package and a negative test for non-exact specs.
Dependencies, configuration, and changeset documentation
installing/env-installer/package.json, installing/env-installer/tsconfig.json, .changeset/config-deps-optional-subdeps.md
Adds @pnpm/config.package-is-installable workspace dependency, semver and @types/semver, a TypeScript project reference, and documents the behavioral change and env lockfile portability for platform variants.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through lockfiles, fields in tow,

Picking os and cpu where binaries grow,
Symlinks snug so require will find,
Optional kin, filtered and aligned—
A little rabbit's build, gentle and slow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding support for optionalDependencies in config dependencies with platform filtering, which is the primary feature across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch config-deps-optional-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.408 ± 0.081 2.272 2.553 1.04 ± 0.05
pacquet@main 2.326 ± 0.082 2.214 2.491 1.00
pnpm 4.575 ± 0.058 4.475 4.670 1.97 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.40785820742,
      "stddev": 0.08109477601930336,
      "median": 2.43291746692,
      "user": 2.70840038,
      "system": 3.5976249599999996,
      "min": 2.27240797442,
      "max": 2.5534866704200003,
      "times": [
        2.4518136384200004,
        2.42842805142,
        2.3805501984200004,
        2.4374068824200004,
        2.44359051742,
        2.3067358234200004,
        2.4473001614200003,
        2.5534866704200003,
        2.27240797442,
        2.35686215642
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.3258925136200004,
      "stddev": 0.08219160337914824,
      "median": 2.3054538089200003,
      "user": 2.72482528,
      "system": 3.5923663599999998,
      "min": 2.21392083542,
      "max": 2.4910551774200003,
      "times": [
        2.4910551774200003,
        2.34641323242,
        2.2567211294200002,
        2.28727316842,
        2.4154430434200003,
        2.2734125404200003,
        2.32363444942,
        2.28365259142,
        2.3673989684200003,
        2.21392083542
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.5749634424200005,
      "stddev": 0.05798504929164784,
      "median": 4.569345843420001,
      "user": 7.75433458,
      "system": 4.0448131599999995,
      "min": 4.47485003342,
      "max": 4.6696376624200004,
      "times": [
        4.6696376624200004,
        4.62612048042,
        4.63022459342,
        4.54573454242,
        4.51090527642,
        4.55986586542,
        4.59360428342,
        4.47485003342,
        4.57126204842,
        4.56742963842
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 693.4 ± 26.9 674.6 766.7 1.00
pacquet@main 699.4 ± 18.2 678.9 735.1 1.01 ± 0.05
pnpm 2522.4 ± 68.3 2435.4 2669.3 3.64 ± 0.17
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.69340720314,
      "stddev": 0.02692371569169816,
      "median": 0.6861074705400001,
      "user": 0.37953686,
      "system": 1.58866364,
      "min": 0.67462891804,
      "max": 0.76669713504,
      "times": [
        0.76669713504,
        0.68978102104,
        0.68464088304,
        0.69374573904,
        0.69965718904,
        0.68108896404,
        0.67462891804,
        0.68114218404,
        0.67511594004,
        0.68757405804
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.69935460494,
      "stddev": 0.018217219309568287,
      "median": 0.6927037220400001,
      "user": 0.38269755999999994,
      "system": 1.5856835400000002,
      "min": 0.67886477304,
      "max": 0.73505714304,
      "times": [
        0.7200539890400001,
        0.70972795204,
        0.68755837604,
        0.70723234804,
        0.68831624004,
        0.68314010904,
        0.68650391504,
        0.73505714304,
        0.69709120404,
        0.67886477304
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.5224226513400003,
      "stddev": 0.06826096537497021,
      "median": 2.5198053760400003,
      "user": 2.9647700599999998,
      "system": 2.22259024,
      "min": 2.4354255580400004,
      "max": 2.66925170504,
      "times": [
        2.5205556600400003,
        2.48923982904,
        2.56103161104,
        2.66925170504,
        2.4478524200400003,
        2.5190550920400003,
        2.4354255580400004,
        2.55815343604,
        2.4722048500400002,
        2.55145635204
      ]
    }
  ]
}

zkochan added 3 commits May 18, 2026 20:02
… subdep siblings

For scoped config dependencies, `path.dirname(pkgDirInGlobalVirtualStore)`
returned the scope subdirectory (e.g. `<gvs>/<relPath>/node_modules/@scope`)
instead of the actual `node_modules` root. The subsequent `readModulesDir`
then returned unscoped names (`['foo']` instead of `['@scope/foo']`), which
failed the expected-siblings membership check and caused the parent itself
to be rimraf'd as an unexpected sibling. Compute the path from `relPath`
directly so the node_modules root is correct for both scoped and unscoped
parents.
@zkochan zkochan marked this pull request as ready for review May 18, 2026 19:05
Copilot AI review requested due to automatic review settings May 18, 2026 19:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
installing/env-installer/test/resolveConfigDeps.test.ts (1)

79-90: ⚡ Quick win

Assert libc preservation explicitly in env lockfile packages.

The test currently proves os/cpu for one variant, but not libc. Since this feature relies on os/cpu/libc selectors, add a strict assertion for a musl/glibc package entry.

Suggested assertion tightening
-  expect(envLockfile!.packages['`@pnpm.e2e/only-linux-x64-musl`@1.0.0']).toBeDefined()
+  expect(envLockfile!.packages['`@pnpm.e2e/only-linux-x64-musl`@1.0.0']).toStrictEqual({
+    resolution: {
+      integrity: getIntegrity('`@pnpm.e2e/only-linux-x64-musl`', '1.0.0'),
+    },
+    os: ['linux'],
+    cpu: ['x64'],
+    libc: ['musl'],
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@installing/env-installer/test/resolveConfigDeps.test.ts` around lines 79 -
90, The test must explicitly assert preservation of the libc selector for the
linux-musl package: locate the envLockfile usage and the packages entry for
'`@pnpm.e2e/only-linux-x64-musl`@1.0.0' (the existing expect on
envLockfile!.packages['`@pnpm.e2e/only-linux-x64-musl`@1.0.0']), and tighten the
assertion toStrictEqual an object that includes resolution.integrity (use
getIntegrity('`@pnpm.e2e/only-linux-x64-musl`','1.0.0')) plus os: ['linux'], cpu:
['x64'], and libc: ['musl'] so the test verifies libc is preserved.
installing/env-installer/test/installConfigDeps.ts (1)

127-160: ⚡ Quick win

Add a non-os mismatch case for platform filtering.

This negative test only proves os filtering. A regression in cpu/libc handling could still pass unnoticed. Please add at least one cpu mismatch case (and a Linux-gated libc mismatch case if feasible).

Suggested test addition
+test('optional subdep that does not match the current cpu is skipped', async () => {
+  prepareEmpty()
+  const { storeController, storeDir } = createTempStore()
+
+  const parentName = '`@pnpm.e2e/foo`'
+  const parentVersion = '100.0.0'
+  const subdepName = '`@pnpm.e2e/bar`'
+  const subdepVersion = '100.0.0'
+
+  const lockfile = createEnvLockfile()
+  const parentKey = `${parentName}@${parentVersion}`
+  lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion }
+  lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } }
+  lockfile.snapshots[parentKey] = {
+    optionalDependencies: { [subdepName]: subdepVersion },
+  }
+  lockfile.packages[`${subdepName}@${subdepVersion}`] = {
+    resolution: { integrity: getIntegrity(subdepName, subdepVersion) },
+    os: [process.platform],
+    cpu: ['this-cpu-does-not-exist'],
+  }
+
+  await installConfigDeps(lockfile, {
+    registries: { default: registry },
+    rootDir: process.cwd(),
+    store: storeController,
+    storeDir,
+  })
+
+  const parentRealPath = fs.realpathSync(`node_modules/.pnpm-config/${parentName}`)
+  const requireFromParent = createRequire(path.join(parentRealPath, 'package.json'))
+  expect(() => requireFromParent.resolve(`${subdepName}/package.json`)).toThrow(/Cannot find/)
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@installing/env-installer/test/installConfigDeps.ts` around lines 127 - 160,
Add a second negative case to the test 'optional subdep that does not match the
current platform is skipped' that exercises platform filtering beyond 'os':
create another subdependency entry (e.g. subdepCpuName/subdepCpuVersion) in
lockfile.packages with a cpu mismatch (set cpu: ['this-cpu-does-not-exist']) and
include it in lockfile.snapshots optionalDependencies like the existing
os-mismatch package, then assert that resolving that subdependency from the
parent also throws; additionally, if process.platform === 'linux' add a third
package with a libc mismatch (e.g. libc: ['this-libc-does-not-exist']) and
assert it is skipped as well so cpu and Linux-gated libc filtering are covered
(use the same patterns/keys as the existing subdep entries and the same
requireFromParent.resolve check).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@installing/env-installer/test/installConfigDeps.ts`:
- Around line 127-160: Add a second negative case to the test 'optional subdep
that does not match the current platform is skipped' that exercises platform
filtering beyond 'os': create another subdependency entry (e.g.
subdepCpuName/subdepCpuVersion) in lockfile.packages with a cpu mismatch (set
cpu: ['this-cpu-does-not-exist']) and include it in lockfile.snapshots
optionalDependencies like the existing os-mismatch package, then assert that
resolving that subdependency from the parent also throws; additionally, if
process.platform === 'linux' add a third package with a libc mismatch (e.g.
libc: ['this-libc-does-not-exist']) and assert it is skipped as well so cpu and
Linux-gated libc filtering are covered (use the same patterns/keys as the
existing subdep entries and the same requireFromParent.resolve check).

In `@installing/env-installer/test/resolveConfigDeps.test.ts`:
- Around line 79-90: The test must explicitly assert preservation of the libc
selector for the linux-musl package: locate the envLockfile usage and the
packages entry for '`@pnpm.e2e/only-linux-x64-musl`@1.0.0' (the existing expect on
envLockfile!.packages['`@pnpm.e2e/only-linux-x64-musl`@1.0.0']), and tighten the
assertion toStrictEqual an object that includes resolution.integrity (use
getIntegrity('`@pnpm.e2e/only-linux-x64-musl`','1.0.0')) plus os: ['linux'], cpu:
['x64'], and libc: ['musl'] so the test verifies libc is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d9d5e40-be82-4689-a052-37820150f8d3

📥 Commits

Reviewing files that changed from the base of the PR and between bfa861f and 267cb6b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • .changeset/config-deps-optional-subdeps.md
  • installing/env-installer/package.json
  • installing/env-installer/src/installConfigDeps.ts
  • installing/env-installer/src/parseIntegrity.ts
  • installing/env-installer/src/resolveAndInstallConfigDeps.ts
  • installing/env-installer/src/resolveConfigDeps.ts
  • installing/env-installer/src/resolveOptionalSubdeps.ts
  • installing/env-installer/test/installConfigDeps.ts
  • installing/env-installer/test/resolveConfigDeps.test.ts
  • installing/env-installer/tsconfig.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (rely on hoisting), and use a single options object for functions with more than two or three arguments
Sort imports in three groups: standard libraries, external dependencies (alphabetically), then relative imports
Write code that explains itself through clear naming and types — do not write comments that merely restate what the code already says; use comments only for non-obvious reasons, hidden invariants, or workarounds

Files:

  • installing/env-installer/src/resolveConfigDeps.ts
  • installing/env-installer/src/parseIntegrity.ts
  • installing/env-installer/src/resolveOptionalSubdeps.ts
  • installing/env-installer/test/installConfigDeps.ts
  • installing/env-installer/src/resolveAndInstallConfigDeps.ts
  • installing/env-installer/test/resolveConfigDeps.test.ts
  • installing/env-installer/src/installConfigDeps.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use util.types.isNativeError() instead of instanceof Error when type-checking errors in Jest tests, as instanceof checks can fail across VM realms

Files:

  • installing/env-installer/test/resolveConfigDeps.test.ts
🧠 Learnings (2)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.

Applied to files:

  • installing/env-installer/package.json
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/env-installer/src/resolveConfigDeps.ts
  • installing/env-installer/src/parseIntegrity.ts
  • installing/env-installer/src/resolveOptionalSubdeps.ts
  • installing/env-installer/test/installConfigDeps.ts
  • installing/env-installer/src/resolveAndInstallConfigDeps.ts
  • installing/env-installer/test/resolveConfigDeps.test.ts
  • installing/env-installer/src/installConfigDeps.ts
🔇 Additional comments (18)
installing/env-installer/package.json (1)

36-36: LGTM!

installing/env-installer/tsconfig.json (1)

15-17: LGTM!

.changeset/config-deps-optional-subdeps.md (1)

1-8: LGTM!

installing/env-installer/src/parseIntegrity.ts (2)

3-22: LGTM!


24-39: LGTM!

installing/env-installer/src/resolveOptionalSubdeps.ts (3)

1-17: LGTM!


18-72: LGTM!


74-80: LGTM!

installing/env-installer/test/installConfigDeps.ts (1)

2-3: LGTM!

Also applies to: 85-125

installing/env-installer/test/resolveConfigDeps.test.ts (1)

100-116: LGTM!

installing/env-installer/src/resolveConfigDeps.ts (1)

18-18: LGTM!

Also applies to: 72-78

installing/env-installer/src/resolveAndInstallConfigDeps.ts (1)

19-19: LGTM!

Also applies to: 135-141

installing/env-installer/src/installConfigDeps.ts (6)

78-89: LGTM!


165-176: LGTM!


181-219: LGTM!


246-269: LGTM!


240-244: 🏗️ Heavy lift

The scoped package handling in this code is correct and does not present a concern.

readModulesDir returns full scoped package names (e.g., @esbuild/linux-x64), not just scope directories. The implementation in fs/read-modules-dir/src/index.ts (line 31) constructs full names when a scope is encountered: const pkgName = scope ? ${scope}/${dir.name as string} : dir.name. This means expectedSiblings and existingSiblings contain the same data structure—full scoped names—making the filtering logic at lines 242–244 work as intended.

			> Likely an incorrect or invalid review comment.

231-238: 💤 Low value

No issues found. The packageIsInstallable function returns boolean | null per its type signature. When optional: true is set, it returns false for unsupported packages and true for supported ones—never null or throws. The === true comparison is correct and necessary to filter for truly installable packages.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for resolving and installing one level of optionalDependencies for configDependencies, recording all platform variants in the env lockfile while filtering by os/cpu/libc at install time so platform-specific “binary companion” packages (esbuild/swc style) work for config deps.

Changes:

  • Resolve and record a config dependency’s one-level optionalDependencies into the env lockfile (preserving os/cpu/libc metadata).
  • Install-time platform filtering + sibling symlinking of compatible optional subdeps in the config dep’s GVS leaf to enable Node resolution from inside the parent.
  • Add/adjust tests and wire in the new helper module + dependency.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pnpm-lock.yaml Adds workspace dependency needed by env-installer.
installing/env-installer/tsconfig.json Adds project reference for config/package-is-installable.
installing/env-installer/test/resolveConfigDeps.test.ts Adds coverage for recording optional subdeps + platform fields and keeping empty snapshots when absent.
installing/env-installer/test/installConfigDeps.ts Adds coverage for install-time filtering and sibling symlink resolution behavior.
installing/env-installer/src/resolveOptionalSubdeps.ts New resolver helper to capture one-level optional subdeps into the env lockfile.
installing/env-installer/src/resolveConfigDeps.ts Wires optional-subdep resolution into config-deps resolution flow.
installing/env-installer/src/resolveAndInstallConfigDeps.ts Wires optional-subdep resolution into “resolve missing config deps then install” flow.
installing/env-installer/src/parseIntegrity.ts Extends normalized config dep shape to include normalized optional subdeps.
installing/env-installer/src/installConfigDeps.ts Installs compatible optional subdeps into their own GVS leaf + symlinks them next to the parent; prunes stale siblings.
installing/env-installer/package.json Adds dependency on @pnpm/config.package-is-installable.
.changeset/config-deps-optional-subdeps.md Declares minor bumps + documents new behavior at a high level.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread installing/env-installer/src/resolveAndInstallConfigDeps.ts
@zkochan zkochan added the area: config dependencies Changes related to configDependencies. label May 18, 2026
@zkochan

zkochan commented May 18, 2026

Copy link
Copy Markdown
Member Author

Addressed the two CodeRabbit nitpicks in bf8935a:

  • test/resolveConfigDeps.test.ts now asserts libc preservation via a strict toStrictEqual on the @pnpm.e2e/only-linux-x64-musl@1.0.0 packages entry.
  • test/installConfigDeps.ts adds a cpu-mismatch negative test so a regression in CPU (or any non-os) filtering would be caught.

Written by an agent (Claude Code, claude-opus-4-7).

…hash

calcLeafGlobalVirtualStorePath hashed with deps:{}, so a config dep with
optionalDependencies always landed in the same GVS leaf regardless of
which subdep versions the lockfile pointed at. Changing a subdep version
while keeping the parent pinned would silently reuse the existing leaf
and overwrite its sibling symlinks. Thread the optional subdeps' full
pkg ids into the hash so the leaf identity matches the lockfile content.

The leaf hash for a subdep itself stays empty-children (one level deep,
matching the config-dep surface).
Copilot AI review requested due to automatic review settings May 18, 2026 19:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread installing/env-installer/src/installConfigDeps.ts Outdated
zkochan added 2 commits May 18, 2026 21:49
Range and tag specifiers (^1.0.0, ~1.0.0, latest, etc.) would let the
resolved subdep version drift between machines even when the parent is
pinned by integrity — defeating the lockfile's reproducibility promise.
Reject anything that isn't a valid semver exact version up-front with a
clear PnpmError.
…anged

The skip-if-already-installed check compared the existing package.json's
name/version against the lockfile's. With optional subdeps in play, those
can be unchanged while the expected GVS leaf has a different hash (subdep
version change, platform change), so the early return left .pnpm-config
pointing at the stale leaf with the wrong sibling symlinks. Compare the
existing symlink's realpath against the expected leaf instead.
Copilot AI review requested due to automatic review settings May 18, 2026 19:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread installing/env-installer/src/installConfigDeps.ts Outdated
zkochan added 2 commits May 18, 2026 22:04
…rent re-link

The leaf hash captures parent + subdep identities from the lockfile but not
the host's process.arch / process.platform selection, so a host switch
between runs (Rosetta x64 vs arm64 on macOS, or any change to the effective
platform) keeps the parent symlink target valid while leaving stale sibling
links inside the leaf. Run installOptionalSubdeps unconditionally so the
prune+relink loop fires; the realpath check now only short-circuits the
parent's re-import and re-symlink.
…tale-state repair test

The previous test mutated `lockfile.packages[*].os` between two runs to
simulate a platform change. The mechanics there were hard to reason
about and CI failed in a way that didn't match the production fix's
expected behavior (likely a fixture/order interaction with two distinct
subdeps). Replace it with a test that directly exercises the property
under test: install once, manually `rimraf` the sibling symlink, install
again with the same lockfile, assert the symlink is restored. This
covers the "skip-check passes but installOptionalSubdeps must still
run" path without the cross-package complexity.
Copilot AI review requested due to automatic review settings May 18, 2026 20:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread installing/env-installer/src/resolveOptionalSubdeps.ts Outdated
Comment thread installing/env-installer/src/resolveOptionalSubdeps.ts Outdated
zkochan added 2 commits May 18, 2026 23:01
…l-subdep resolution failures

Mirror the optional-dep error handling from installing/deps-resolver:
trust-downgrade errors are a security signal that must abort the
install even when the failing dep is optional, while other resolution
failures get logged via skippedOptionalDependencyLogger with reason
'resolution_failure' so users can diagnose missing platform binaries.
…n function

calcLeafGlobalVirtualStorePath reverts to its original "leaf, no
children" semantics. The one-level-with-subdeps computation that
the env-installer needs now lives in a dedicated
calcGlobalVirtualStorePathWithSubdeps so each function has a single
clear contract — and pacquet can mirror them independently when
it ports config-dep support.
Copilot AI review requested due to automatic review settings May 18, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread installing/env-installer/src/installConfigDeps.ts
Comment thread installing/env-installer/src/resolveOptionalSubdeps.ts Outdated
zkochan added 2 commits May 18, 2026 23:31
…eps; fix log version

Two follow-ups from PR review:

- Switch from packageIsInstallable to checkPackage in the optional-subdep
  platform filter. packageIsInstallable emits installCheckLogger.warn on
  every incompatible variant, which would fire on every install for every
  non-matching platform binary (the env lockfile records all variants for
  portability). Emit our own skipped-optional-dependency debug log instead.
- For resolution_failure logs in resolveOptionalSubdeps, set package.version
  to the requested bareSpecifier instead of undefined so consumers that
  format \`name@version\` don't render \`name@undefined\`. Matches the
  fallback used in installing/deps-resolver.
Copilot AI review requested due to automatic review settings May 18, 2026 22:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +303 to +304
const realPath = await fs.promises.realpath(linkPath)
return realPath === expectedTarget

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0a4d552. symlinkPointsTo now realpaths both sides before comparing. Handles the storeDir-is-itself-a-symlink case (e.g. ~/Library/pnpm/store → versioned dir) and the case-insensitive-filesystem case (macOS APFS, Windows NTFS) where two strings can disagree about casing while referring to the same inode. If either path doesn't exist yet, realpath throws and the helper returns false — falling through to the install path is the correct behavior in that case.


Written by an agent (Claude Code, claude-opus-4-7).

@zkochan zkochan merged commit c8d8fde into main May 18, 2026
12 of 13 checks passed
@zkochan zkochan deleted the config-deps-optional-deps branch May 18, 2026 23:29
KSXGitHub pushed a commit that referenced this pull request May 19, 2026
Pulls in:
- cddf522 feat(pacquet): port lockfile verification (minimumReleaseAge
  + trustPolicy) (#11729) — adds `default_cache_dir`, which was
  written against the pre-DI-refactor shape and uses the closure-based
  `HomeDir: FnOnce(...)` parameter plus a module-level `NoEnv` struct
  this branch's refactor retired. Followup commit ports it to the
  trait-based DI seam the PR introduces.
- c8d8fde feat(config-deps): support optionalDependencies with
  platform filtering (#11725) — does not touch files this branch
  edits.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants