feat(cli): prune the bundled lockfile to the code bundle's contents [RED-886] - #1442
Open
sorccu wants to merge 28 commits into
Open
feat(cli): prune the bundled lockfile to the code bundle's contents [RED-886]#1442sorccu wants to merge 28 commits into
sorccu wants to merge 28 commits into
Conversation
…ts [RED-886] Faux workspace-package manifests (shims for declared-but-unimported workspace members) hardcoded version 0.0.0, which breaks `workspace:^x.y.z` specifier resolution under pnpm and makes npm silently substitute a same-named registry package when a semver range does not match 0.0.0. Carry the member's real version on the Package model (populated at every construction site, including from `pnpm list --json` output) and emit it in the faux manifest, falling back to 0.0.0 only when the member declares no usable version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm records a checksum of the pnpmfile in pnpm-lock.yaml
(pnpmfileChecksum), and installing without the file makes pnpm treat the
lockfile as out of date and silently re-resolve every dependency — which is
what happens on the remote runner today for every pnpm project with a
pnpmfile, since nothing bundled it.
Bundle the workspace-root pnpmfile (pnpm workspaces only) verbatim into
Playwright code bundles, and add its contents to the dependency cache hash
as a new `pnpmfile:` record (position pinned by a new cross-language parity
fixture). Only self-contained pnpmfiles are bundled: the remote install
loads the pnpmfile before any dependencies exist and in a different
environment, so anything that loads modules beyond a small allowlist of
side-effect-free builtins, or references process/__dirname/require.resolve
and similar escape hatches, is skipped with a warning — preserving today's
behavior for such projects. The `pnpmfile` and `ignore-pnpmfile` settings
in .npmrc / pnpm-workspace.yaml are honored, and when both .pnpmfile.cjs
and .pnpmfile.mjs exist the situation is version-ambiguous (pnpm 10 loads
only the cjs, pnpm 11 only the mjs) and both are skipped.
Pnpmfiles are never parsed as check code; they are install-time
configuration, and parsing them would report constructs like optional
`try { require(...) } catch {}` blocks as missing dependencies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pm [RED-886] Expose a lockfileOnlyInstallCommand() on the PackageManager interface that regenerates the lockfile from the manifests on disk without installing anything. pnpm uses `install --lockfile-only --ignore-scripts --no-frozen-lockfile` (--no-frozen-lockfile matters because pnpm auto-enables frozen mode when CI=true); npm uses `install --package-lock-only --ignore-scripts --no-audit --no-fund`. Other package managers report the mode as unsupported so callers can skip lockfile pruning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pruneBundledLockfile() regenerates a code bundle's lockfile so it matches the bundle's actual set of manifests: the resolution-relevant bundle entries (manifests including faux shims, .npmrc, pnpm-workspace.yaml, pnpmfiles, patches, and the lockfile) are materialized into a temp dir and the package manager's lockfile-only install runs there — dependencies of workspace members that were shimmed or omitted disappear from the result. The regenerated lockfile is only accepted if it is provably a pruned copy of the original: the format version and pnpmfile checksum must be unchanged, every resolution must already exist in the original (a stale lockfile would otherwise be silently re-resolved from the registry), workspace links must stay links (npm silently substitutes registry packages for version-mismatched members), and no importer whose manifest is bundled may disappear. Anything else falls back to the original lockfile. Workspace members that bundled manifests reference as links but that have no manifest in the bundle (e.g. declared only by the workspace root) get a synthesized faux manifest, returned to the caller for registration so the bundle and the lockfile stay consistent. Supported for pnpm-lock.yaml (v6/v9) and package-lock.json (v2/v3); everything else, plus special setups like excludeLinksFromLockfile, is skipped. CHECKLY_LOCKFILE_PRUNE=0 disables pruning. Not yet wired into the bundler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ED-886] Wire the lockfile pruner into Bundler.finalize(): once the bundle's file set is final, the lockfile is regenerated against the bundle's actual manifests, so dependencies of workspace members that were shimmed or omitted no longer appear in it — previously the runner's install resolved (and tried to fetch) them, which breaks outright for private packages. Workspace members that bundled manifests reference as links but that had no manifest in the bundle get their faux manifest registered too. On any verification failure the original lockfile ships unchanged, with a warning explaining the fallback and the CHECKLY_LOCKFILE_PRUNE=0 escape hatch. The dependency cache hash now reflects the bundle's real install inputs: finalize() recomputes it with a record per synthesized (faux) manifest — whose version, unlike on-disk manifests, is load-bearing for install resolution — and a record for the pruned lockfile bytes. Payloads capture the hash through a mutable CacheHashMarker (same toJSON pattern as BundlePathMarker) because checks copy it during bundle(), before finalize() runs. Bundles with no faux manifests and no pruning keep a byte-identical hash. Cross-language parity fixtures pin the new record groups for the terraform-provider mirror. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the automatic lockfile pruning behavior in the Playwright configuration reference (supported lockfile formats, silent-skip conditions and their DEBUG namespace, warned fallbacks, and the CHECKLY_LOCKFILE_PRUNE=0 escape hatch), and describe the dependency cache key accurately: it covers the workspace's own install inputs — including members outside the bundle — plus the synthesized placeholder manifests and the pruned lockfile as additional bundle-time inputs. The caching.dependencyCache.version JSDoc now points at ComposeCacheHashInput as the single authoritative input list. CLAUDE.md lists CHECKLY_LOCKFILE_PRUNE (and the previously undocumented CHECKLY_CACHE_DIR). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Backfill also covers workspace members referenced through
peerDependencies (plugin-style monorepos previously failed pruning on
every run) and optionalDependencies, which the parser's faux-manifest
mechanism does not see; the integration fixture now exercises the
backfill registration path end to end via an optionalDependencies link.
- Skips that occur after the bundle is known to be a partial workspace
("pruning needed but unavailable": unsupported package manager or
lockfile format, excludeLinksFromLockfile, missing pnpmfile, unknown
member version) now print a one-line notice instead of being
debug-only, so users whose remote installs will still fail get a
pointer; nothing-to-do skips stay quiet.
- The pnpmfile is only bundled when the lockfile records a
pnpmfileChecksum: without one there is nothing to reproduce, and
shipping a pnpmfile alongside a lockfile that does not record its
checksum would itself make pnpm treat the lockfile as out of date.
This also silences the not-bundling warning for pnpmfiles that cannot
affect the remote install.
- The pnpmfile self-containedness analysis rejects `Function` (an eval
equivalent that could reach process via `Function('return process')`)
and no longer misreports `new.target` as `import.meta`.
- Structurally valid pnpm lockfiles at out-of-range versions are covered
by a fail-closed test, and the pnpmfile warning dedup uses object
identity (WeakSet) instead of a module-global set with a test-only
reset export.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ps [RED-886] pnpm's lockfile-only install now passes --lockfile-dir '.' so a lockfile-dir setting inherited from any config layer (project, user or global npmrc, pnpm-workspace.yaml, env) cannot redirect the subprocess's lockfile write outside the prune temp dir — potentially over the user's real lockfile. npm_config_lockfile_dir is additionally stripped from the child env as defense in depth. The unsupported-package-manager check is documented and test-pinned to run before the lockfile read, so yarn/bun workspaces always get the notable skip instead of a misleading failure warning. Optional workspace peers are deliberately still backfilled: pnpm resolves workspace: peer specs regardless of peerDependenciesMeta.optional when auto-install-peers is on, verified by a new real-pnpm test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s scope [RED-886] The checksum gate in loadWorkspacePnpmfiles is merged into the function body instead of delegating to a separate helper, and logs a debug line when it suppresses pnpmfile bundling. analyzePnpmfile's docstring now records its threat model: the analysis assumes a non-adversarial author and deliberately does not chase reflective escape hatches such as constructor-property access, because rules broad enough to catch them would false-positive on ubiquitous pnpmfile patterns like pkg.dependencies[name]. A new fixture covers the gate's wiring: a workspace with a .pnpmfile.cjs whose lockfile records no pnpmfileChecksum reports no pnpmfiles at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…RED-886] The note printed when a partial-workspace bundle ships an unpruned lockfile now points at CHECKLY_LOCKFILE_PRUNE=0, framed as a last resort for setups that cannot be pruned. Both note paths are now covered by bundler tests: an unsupported package manager prints the note, and a full-workspace bundle stays silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[RED-886] The pnpmfile bullet now states the pnpmfileChecksum precondition for bundling and lists Function among the forbidden references. The pruning bullet distinguishes the note-printing skips (a bundled lockfile over-describes a partial-workspace bundle but pruning cannot run) from the silent ones (nothing to prune, no bundled lockfile, or pruning explicitly disabled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…filter [RED-886] parseLockfilePackagesContent extracts the format dispatch from loadLockfilePackages so lockfiles that exist only in memory — such as a pruned copy of the bundled lockfile — can be parsed. The new filterTarballsByLockfile splits a planned embedded-tarball set by name@version presence in a lockfile's registry packages; since a pruned lockfile's resolutions are verified to be a subset of the original's, presence filtering is exactly equivalent to re-running spec matching. An unparseable lockfile fail-safes to keeping every tarball. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…finalize [RED-886] Embedded package tarballs (bundle.packages.embed) were downloaded and registered during per-check bundling, before the bundled lockfile was pruned — so tarballs whose only referents are dependencies of workspace members outside the bundle were downloaded and shipped even though the runner's install could never use them. For projects with large multi- version private packages this meant 100+ MB of pointless cold-cache downloads. Materialization now happens once, in Bundler.finalize(), after pruning: the planned set is filtered to what the (possibly pruned) bundled lockfile still references, only that subset is sourced (cache or download) via the new EmbeddedPackagesMaterializer.materializeTarballs, and the cache hash's embedded-package records reflect exactly the shipped set — every hash computation passes it explicitly. The memoized materialize() is gone with its per-check caller. A stderr line announces the tarball preparation, and an empty bundle never triggers downloads (every command calls finalize() unconditionally, and embed validation is skipped for projects without Playwright checks). Download failures (EmbeddedPackageError) now surface at finalize instead of during per-check bundling — same command, same error type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New workspace fixture where the imported member depends on ms@2.1.3 (embedded via a genuine registry tarball committed as a fixture — a fake package cannot survive pnpm's re-resolution of a stale lockfile) and the shimmed-only member on the fake @acme/private-utils@1.2.3. The sandbox test runs a real pnpm prune and asserts the dropped package's tarball is neither shipped nor requested (only the kept tarball is seeded, so any attempt to materialize the dropped one would fail loudly), the pruned lockfile no longer references it, and the payload cacheHash matches the kept-only embedded set exactly. parseProjectWithOptions now returns the sandbox run's stderr, which also turns two previously vacuous String(result.stderr) assertions in the symlink workspace test into real ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ED-886] The embed and caching bullets (and the bundle.packages.embed JSDoc) now state that when the bundled lockfile is pruned, the embedded set follows it — packages the pruned lockfile no longer references are neither embedded nor downloaded — with the benign explanation first (an out-of-bundle workspace member needing the package means the runner does not need it), bringing the member into the bundle as the remedy, and CHECKLY_LOCKFILE_PRUNE=0 framed as a last resort. The cold-cache registry-access note is scoped to the tarballs actually shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'Pruning the bundled lockfile...' and 'Preparing N embedded package tarball(s)...' stderr lines are not worth showing until the overall CLI output UX improves; the tarball line is now a debug log and the pruner's onRun hook is removed outright (its only remaining purpose was a debug line the pruner's own 'Running ...' debug output already covers). Warnings and the notable-skip note stay user-facing. The bundler spec's stderr spies filter out debug-library writes so the tightened empty-stderr assertions hold even with DEBUG enabled, and the full-workspace E2E test observes the prune skip through scoped DEBUG output — its previous stderr assertion would have passed vacuously once the progress line left stderr. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lockfile pruner gains tests that run a real bun install and skip themselves when bun is not on PATH. Both unit-test jobs install a pinned bun and set CHECKLY_EXPECT_BUN, which makes the suite assert that the provisioning worked instead of skipping the coverage silently. The version is pinned because the tests regenerate a committed bun.lock fixture and a future bun may serialize it differently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bun projects now get the same bundled-lockfile pruning as pnpm and npm: BunDetector gains a lockfile-only install command (bun install --lockfile-only --ignore-scripts, verified against bun 1.3.11), and the pruner parses bun.lock (JSONC, lockfileVersion 1) into its format-agnostic snapshot. Resolutions are compared as a presence set of whole tuples because pruning re-keys surviving member-scoped entries, and dependency edges resolve workspace links per edge (member-scoped packages key first, then hoisted) because a workspace member's name may also be consumed from the registry by a different importer. The binary bun.lockb format skips notably with a pointer at `bun install --save-text-lockfile`. Because bun re-roots at any ancestor package.json whose workspaces glob matches the working directory — writing the regenerated lockfile at that root, outside the temp dir and over a real file — the pruner refuses to run when its temp dir sits inside a workspace, parsing ancestor manifests with bun's own JSONC leniency and failing safe on unparseable ones. A missing package manager binary and a regenerated lockfile that bun deleted (a lockfile describing no packages) surface as notable skips rather than failures blaming the user's lockfile. Patch files are auto-included for bun as they already were for pnpm, so bun patchedDependencies survive both bundling and the temp-dir install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bundle.packages.embed now accepts bun's text bun.lock: packages parse from the lockfile tuples (registry URL at index 1, sha512 integrity at index 3), with workspace tuples excluded under the precise workspace-package reason and git/file/URL refs excluded as unfetchable. The name@ref splitting and registry-entry classification that the bun parser shares with the pnpm parser move into common helpers. The pruned-lockfile tarball filter accepts bun.lock through the same dispatch, and bun.lockb keeps failing with a --save-text-lockfile hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pruning and embedded-packages references and the bundle.packages.embed JSDoc now name bun (text bun.lock, version 1) alongside pnpm and npm, including the bunfig.toml limitations: it is not carried into the lockfile regeneration or the tarball downloads, so registry credentials must live in .npmrc (referenced through environment variables, never plaintext, since .npmrc ships with the bundle), and a lockfile bun declines to reuse re-resolves against the default registry — disclosing package names to it — before pruning rejects the result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s [RED-886] On Windows a missing executable never surfaces as a spawn ENOENT: execa spawns through cross-spawn, which wraps an unresolvable command in cmd.exe, so the child "runs" and exits non-zero with cmd.exe's not-recognized message — and the pruner reported that as a failed prune with advice to refresh a perfectly fine lockfile (seen on the Windows CI leg). The failure branch now classifies after the fact with a PATH probe and maps a missing binary to the intended notable skip. Probing only an already-failed run means a probe/spawn resolution disagreement can never block a working prune; only the reported reason is at stake. PathLookup is hardened toward parity with the spawn's own resolver: individually quoted Path entries are unquoted and PATHEXT falls back to .EXE/.CMD/.BAT/.COM when unset. Its detectPresence() sibling has a pre-existing missing-await bug that makes it vacuously succeed; that is now documented in place and tracked as RED-887 rather than fixed in passing, since fixing it changes package-manager detection outcomes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lisions [RED-886] On win32, node's spawn sorts env keys and deduplicates them case-insensitively keeping the first — uppercase sorts before lowercase, so the CI runner's ambient NPM_CONFIG_REGISTRY silently displaced the test's lowercase npm_config_registry sentinel and the kept-variable assertion read undefined. The test now drops every ambient case-variant before setting the sentinel, so exactly one casing exists regardless of the runner's environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend bundled-lockfile pruning to Yarn Berry (yarn.lock metadata versions 6/8/10; Yarn Classic stays unsupported and fails closed with a clear skip). YarnDetector gains `yarn install --mode=update-lockfile`, and the pruner grows a yarn.lock snapshot parser that treats workspace entries as importers (their dependency/peer maps as edges) and every other entry as a serialized presence set immune to descriptor re-keying. Regeneration runs under strict environment isolation: the network is disabled (a stale lockfile then fails fast instead of resolving a missing descriptor against the public registry and disclosing its name), scripts are disabled, hardened mode is turned off only for a probe-confirmed Yarn 4 (Yarn 3 rejects the unknown setting), and — because yarn honors a .yarnrc from any ancestor of the temp dir — yarnPath is neutralized (YARN_IGNORE_PATH plus a randomized rc filename) so an uncontrolled ancestor rc on a shared host cannot execute arbitrary code during the version probe. A pre-spawn version probe refuses Yarn Classic (which silently full-installs on this flag) and reports a lockfile/binary generation mismatch as an actionable skip rather than a blocked-registry failure. yarn.lock is parsed with the YAML failsafe schema so Yarn 3's unquoted numeric ranges survive as strings. `.yarn/patches` is auto-included so patch: dependencies regenerate. Committed real-yarn fixtures for both generations (4.18.0 and 3.8.7) drive end-to-end prune tests, gated on a Corepack-provisioned yarn and asserted present in CI via CHECKLY_EXPECT_YARN. This commit also fixes a pre-existing patches auto-include prefix-match bug (a sibling directory like patches-archive suppressed the include) for pnpm and bun alongside yarn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enable a Corepack-managed yarn (scoped to yarn so its pnpm shim cannot shadow the pinned pnpm) in both unit-test jobs and pre-download the two fixture-pinned Yarn Berry versions, so a corepack/registry failure surfaces as an obvious step failure rather than inside the pruner's timed subprocess. CHECKLY_EXPECT_YARN makes the suite assert yarn resolves to the fixtures' Berry version, so a lost provisioning step or a shadowing Classic yarn fails the job instead of silently skipping the real-yarn lockfile-pruner tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D-886] A prune timeout below the install floor can never succeed, but the check ran after the version probe — so under a tiny timeout the probe itself timed out first, yielding a misleading "timed out" result (and a flaky test). Move the pure check ahead of the probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend `bundle.packages.embed` to Yarn Berry. Berry lockfiles record no npm tarball integrity (their checksum hashes yarn's own cache archive, not the registry tarball), so parseYarnLockfilePackages classifies registry entries without an SRI integrity — carrying the Berry checksum as a content pin for the dependency-cache hash instead — and the materializer resolves the real tarball integrity from the registry's package metadata at finalize time. It tries the abbreviated per-version route first and falls back to the full packument when that route is absent, using the same npmrc-derived registry and credentials as the tarball download; an old-style sha1 shasum is accepted when the metadata carries no SRI. A checksum-less entry, a workspace/portal/link/patch/git ref, or an unresolved range is excluded, and Yarn Classic lockfiles are rejected with a clear message. LockfileRegistryPackage.integrity becomes optional (present for pnpm/npm/bun, absent for yarn) alongside a new lockfileChecksum; the two cache-hash call sites map planned tarballs through embeddedPackageHashInputs, which uses the integrity or, for yarn, the Berry checksum — a stable pin known at plan time — so the eager and finalize hashes stay consistent without depending on the metadata roundtrip. Trade-off: because the CLI/npm caches are keyed by integrity, yarn embeds incur a small per-package metadata request on every deploy even with a warm cache; this is documented where embed is configured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D-886] Note in the bundle.packages.embed config JSDoc and the Playwright AI-context reference that pruning and embedding now cover Yarn Berry yarn.lock (Classic excepted), that Berry integrity is resolved from registry metadata (per-version route with a packument fallback, needing registry reachability on every deploy), and that yarn credentials for embed downloads must live in .npmrc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cross-spawn wraps every cmd.exe argument in quotes, so the fake yarn.cmd saw "--version" (quoted) and never took its version branch — the probe fell through to a failed install and the test saw 'failed' instead of the Yarn Classic skip. Use %~1 to strip the quotes, matching how the real corepack yarn.cmd forwards args to node. Test-only; win32-gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: RED-886
What this does
When a workspace (monorepo) project's code bundle covers only part of the workspace, the bundled lockfile used to ship unchanged — describing dependencies of workspace members that were omitted or shipped as dependency-free placeholder manifests. The remote install would then try to fetch those dependencies, which fails outright for private packages.
This PR makes the CLI prune the bundled lockfile at finalize time: the resolution-relevant bundle entries (manifests, lockfile,
.npmrc, pnpmfiles, patches) are materialized into a temp dir and the package manager's lockfile-only install regenerates the lockfile there, so it references exactly what the bundle contains. The result is verified against the original before it ships (same lockfile version, resolution subset, workspace links stay links, bundled importers preserved) — any doubt means the original ships unchanged with a warning or note.CHECKLY_LOCKFILE_PRUNE=0disables pruning entirely.Supported:
pnpm-lock.yamlv6/v9,package-lock.jsonv2/v3, bun's textbun.lockv1 (the binarybun.lockbis not supported and skips with a pointer atbun install --save-text-lockfile), and Yarn Berryyarn.lock(metadata versions 6/8/10 — yarn 3 and 4). Yarn Classic (v1) is not supported and skips with a note.Building on that:
bundle.packages.embedfollows the pruned lockfile. Embedded-package tarballs are now materialized at finalize, after pruning, and filtered to what the pruned lockfile still references — tarballs whose only referents were pruned away are neither embedded nor downloaded. This avoids downloading potentially very large private packages that the runner could never use, instead of relying on CI caches to absorb the cost.pnpmfileChecksumand the pnpmfile is self-contained (side-effect-free builtins only); otherwise pruning skips with a note.package.jsonwhose workspaces glob matches the working directory, writing the lockfile outside the temp dir — the pruner refuses to run when its temp dir sits inside a workspace.yarn install --mode=update-lockfile), verified against yarn 4.18.0 and 3.8.7: workspace entries are the importers (their dependency/peer maps are the edges) and every other entry feeds a serialized presence set immune to descriptor re-keying; regeneration runs with the network disabled (a stale lockfile then fails fast instead of resolving a missing descriptor against the public registry and leaking its name), scripts disabled, hardened mode disabled only for a probe-confirmed yarn 4, and — because yarn honors a.yarnrc/.yarnrc.ymlfrom any ancestor of the temp dir —yarnPathneutralized (YARN_IGNORE_PATHplus a randomized rc filename) so an uncontrolled ancestor rc on a shared host cannot execute code during the version probe. A pre-spawn probe refuses Yarn Classic (which silently full-installs on this flag) and reports a lockfile/binary generation mismatch as an actionable skip.bundle.packages.embedalso works for Yarn Berry: because Berry lockfiles record no npm tarball integrity (their checksum hashes yarn's own cache, not the tarball), the CLI resolves the tarball integrity from the registry's package metadata (per-version route, packument fallback) at finalize time.Behavior changes to note
EmbeddedPackageError) now surface at finalize rather than during per-check bundling — same command, same error type, marginally later.bundle.packages.embedstarts working for bun projects (it previously errored with "unsupported lockfile").bundle.packages.embedstart working for Yarn Berry projects (both previously skipped/errored as unsupported).yarnresolves to Yarn Classic (a yarnPath-pinned setup with nopackageManagerfield) now reports a clean "not pruned" skip pointing at Corepack, instead of a full Classic install or a confusing failure.Known limitations / accepted trade-offs
~/.npmrc, pnpm env settings) participates in regeneration. Drift is caught by verification and fails closed to the unpruned lockfile.bunfig.tomlis not bundled or carried into regeneration/downloads — registry credentials must live in.npmrc(documented). A lockfile bun declines to reuse re-resolves against the default registry, disclosing package names to it, before verification rejects the result..yarnrc.yml/.yarnrcis not bundled or carried into regeneration — registry credentials for embed downloads must live in.npmrc(documented). Berry lockfiles are registry-agnostic, so pruning needs no network; the network is disabled outright so a stale lockfile fails closed without disclosing names.yarnPathrather than thepackageManagerfield land on the Classic skip — Corepack (apackageManagerfield) is required for v6/v8/v10 pruning. A cross-generation yarn/lockfile mismatch skips with an actionable message.node_modulesre-hoisting is covered at unit level only.PathLookup.detectPresencehas a pre-existing missing-awaitbug that makes executable detection vacuously succeed; it is documented in place and tracked as RED-887 rather than fixed here (fixing it changes package-manager detection outcomes).ms@2.1.3) because pnpm re-resolves all importers of a stale lockfile, so a fake kept-side package would 404.Testing
Unit + integration: 2207 tests green (150 files), including end-to-end prune fixtures for pnpm (Playwright-level, with embed filtering and no-download proofs), and unit-level real-package-manager prunes for pnpm, npm, bun and Yarn Berry (both yarn 3 and 4, from committed real fixtures). Real-bun/-yarn tests skip when the manager is not on PATH; CI provisions both (Corepack for yarn) and asserts the provisioning worked via CHECKLY_EXPECT_BUN/CHECKLY_EXPECT_YARN. The ancestor-
.yarnrccode-execution guard was verified against real yarn 1.22.22, 3.8.7 and 4.18.0.Pre-merge: a runner-side acceptance check with real credentials is still advisable, covering a pnpmfile project, an embed project, a bun project, and a Yarn Berry project (to confirm the runner installs against the embedded-package registry — the same open product question for bun and yarn).
🤖 Generated with Claude Code