Skip to content

fix(detect) handle truncated pnpm lockfiles and empty yarn.lock#17

Merged
indexzero merged 3 commits intomainfrom
fix/lockfile-detection-edge-cases
Mar 15, 2026
Merged

fix(detect) handle truncated pnpm lockfiles and empty yarn.lock#17
indexzero merged 3 commits intomainfrom
fix/lockfile-detection-edge-cases

Conversation

@indexzero
Copy link
Owner

@indexzero indexzero commented Mar 15, 2026

What

tryParsePnpm in src/detect.js now parses only the first 20 lines of YAML (the header) instead of the full file, so detection succeeds even when the file body is truncated. A new parsePnpmYaml() helper in src/parsers/pnpm/index.js wraps yaml.load with progressive tail-trimming — if parsing fails, it removes up to 20 trailing lines one at a time until the YAML is valid. FlatlockSet.#parseAll in src/set.js and fromPnpmLock both use this helper instead of raw yaml.load.

tryParseYarnClassic in src/detect.js no longer requires at least one package entry. Empty yarn.lock files (header-only, zero packages) are now accepted as yarn-classic when the # yarn lockfile v1 comment appears at column 0 within the first 5 lines.

Why

Truncated pnpm lockfiles

pnpm v5/v6 lockfiles use inline YAML flow collections for resolution metadata (resolution: {integrity: sha512-...}). When a lockfile is truncated mid-entry — common when files are copied from browser downloads, partial clones, or disk corruption — the unclosed { causes js-yaml to throw unexpected end of the stream within a flow collection. This error propagated through three code paths:

  1. tryParsePnpm in detection — caught by try/catch, returned false, causing "Unable to detect lockfile type"
  2. yaml.load(content) in FlatlockSet.#parseAll — unhandled, threw to the user
  3. yaml.load(input) in fromPnpmLock — same

The fix splits the concern: detection only needs the header (where lockfileVersion lives), so it parses the first 20 lines. The parser needs the full file but tolerates truncation by trimming trailing lines until yaml.load succeeds. The null guard on pkg.resolution at line 280 handles the boundary entry that parsed as a key with null value after trimming.

Empty yarn.lock

yarn init and various CI setups produce yarn.lock files containing only the standard header comment and no package entries. The previous detection required Object.keys(result.object).length > 0, which rejected these files. Simply removing the check would cause false positives — the @yarnpkg/lockfile parser is lenient enough to return { type: 'success', object: {} } for arbitrary indented text (e.g., the spoofing test fixture). The fix allows empty results only when /^# yarn lockfile v1/m matches within the first 5 lines, which is where real yarn.lock files place the header.

Risk Assessment

Low risk. Detection changes are additive — previously-detected lockfiles take the same code paths. The pnpm tail-trimming only activates when yaml.load throws (previously a hard failure). The yarn header check is strictly more permissive than the old hasEntries requirement, gated by a narrow regex on the first 5 lines. All 156 existing tests pass, including the spoofing resistance suite.

indexzero and others added 3 commits March 15, 2026 00:07
Truncated pnpm lockfiles (incomplete YAML flow collections) caused
js-yaml to throw during both detection and parsing. Detection now
parses only the YAML header, and a new parsePnpmYaml helper
progressively trims trailing lines until parsing succeeds.

Empty yarn.lock files (header-only, no packages) were rejected by
detection because it required at least one package entry. Now allows
empty results when the yarn lockfile v1 header is present in the
first 5 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix biome lint: use optional chaining for pkg?.resolution, sort imports
in set.js, remove trailing commas in test arrays.

Add tests for truncated pnpm v6 lockfile (synthetic, cut mid-flow-
collection) and empty yarn.lock (header-only). Verify detection and
parsing both succeed for each case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tsc requires explicit casts since yaml.load returns unknown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@indexzero indexzero merged commit ca60ae1 into main Mar 15, 2026
5 checks passed
@indexzero indexzero deleted the fix/lockfile-detection-edge-cases branch March 16, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant