From b92ce3f35844f8cbee43193b43b4559860fdbe83 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 01:18:11 +0300 Subject: [PATCH 1/7] feat!: use hierarchical lockfile snapshots for per-context detection BREAKING CHANGE: LockfileSnapshot type changed from Map to Map> to preserve per-importer/package resolution. This enables detecting changes in specific workspace packages even when the same package exists elsewhere with a different version. - Core types updated with new LockfileSnapshot and LockfileDiff formats - Diff logic updated to compare per-context - Affected resolution updated for new diff format - Parsers updated: pnpm (YAML), bun (JSONC), npm (direct JSON) - npm parser now extracts per-workspace dependencies - Added E2E tests for all 4 package managers - Added per-importer detection test - pnpm parser now properly handles multiple importers --- .../fixtures/bun-transitive/after-bun.lock | 15 ++ .../fixtures/bun-transitive/before-bun.lock | 15 ++ .../bun-transitive/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../npm-transitive/after-package-lock.json | 30 +++ .../npm-transitive/before-package-lock.json | 30 +++ .../npm-transitive/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../pnpm-transitive/after-pnpm-lock.yaml | 22 +- .../pnpm-transitive/before-pnpm-lock.yaml | 12 +- .../workspace/packages/pkg-a/package.json | 7 + .../fixtures/yarn-transitive/after-yarn.lock | 6 + .../fixtures/yarn-transitive/before-yarn.lock | 6 + .../yarn-transitive/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../transitive-dependency-detection.test.ts | 214 +++++++++++------- .../affected/find-affected-packages.test.ts | 8 +- .../resolve-affected-packages.test.ts | 149 ++++++++---- .../src/affected/resolve-affected-packages.ts | 35 ++- .../src/diff/diff-lockfile-snapshots.test.ts | 127 +++++++---- .../core/src/diff/diff-lockfile-snapshots.ts | 37 ++- packages/core/src/types/lockfile.ts | 37 ++- packages/lockfile-bun/package.json | 2 +- .../src/parse-bun-lockfile.test.ts | 21 +- .../lockfile-bun/src/parse-bun-lockfile.ts | 67 ++++-- packages/lockfile-npm/package.json | 9 +- .../src/parse-npm-lockfile.test.ts | 20 +- .../lockfile-npm/src/parse-npm-lockfile.ts | 101 +++++++-- packages/lockfile-pnpm/package.json | 2 +- .../src/parse-pnpm-lockfile.test.ts | 54 ++++- .../lockfile-pnpm/src/parse-pnpm-lockfile.ts | 63 ++++-- .../src/parse-yarn-lockfile.test.ts | 39 ++-- .../lockfile-yarn/src/parse-yarn-lockfile.ts | 24 +- pnpm-lock.yaml | 84 ++++--- 37 files changed, 918 insertions(+), 381 deletions(-) create mode 100644 packages/cli/e2e/fixtures/bun-transitive/after-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-transitive/before-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-transitive/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-transitive/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-transitive/workspace/packages/pkg-a/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-transitive/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-middle/package.json diff --git a/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock b/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock new file mode 100644 index 0000000..fd9c155 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "test-monorepo", + "dependencies": { + "lodash": "^4.17.0" + } + } + }, + "packages": { + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="] + } +} diff --git a/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock b/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock new file mode 100644 index 0000000..2cafb3b --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "test-monorepo", + "dependencies": { + "lodash": "^4.17.0" + } + } + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w=="] + } +} diff --git a/packages/cli/e2e/fixtures/bun-transitive/workspace/package.json b/packages/cli/e2e/fixtures/bun-transitive/workspace/package.json new file mode 100644 index 0000000..b356332 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-transitive/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-monorepo", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..4a60855 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } +} diff --git a/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..cb8ce80 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-transitive/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json b/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json new file mode 100644 index 0000000..4353102 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "test-monorepo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-monorepo", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash" + }, + "packages/pkg-base": { + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } + }, + "packages/pkg-middle": { + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json b/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json new file mode 100644 index 0000000..0b60afe --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "test-monorepo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-monorepo", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash" + }, + "packages/pkg-base": { + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.0" + } + }, + "packages/pkg-middle": { + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-transitive/workspace/package.json b/packages/cli/e2e/fixtures/npm-transitive/workspace/package.json new file mode 100644 index 0000000..b356332 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-transitive/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-monorepo", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..4a60855 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } +} diff --git a/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..cb8ce80 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-transitive/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-transitive/after-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-transitive/after-pnpm-lock.yaml index bfe210a..cbe392a 100644 --- a/packages/cli/e2e/fixtures/pnpm-transitive/after-pnpm-lock.yaml +++ b/packages/cli/e2e/fixtures/pnpm-transitive/after-pnpm-lock.yaml @@ -8,21 +8,23 @@ importers: dependencies: lodash: specifier: ^4.17.0 - version: 4.17.22 - react: - specifier: ^18.0.0 - version: 18.2.0 + version: 4.17.21 + packages/pkg-a: + dependencies: + lodash: + specifier: ^4.17.0 + version: 4.17.25 packages: - lodash@4.17.22: + lodash@4.17.21: resolution: { - integrity: sha512-nFEy64W3jilNPEu6HVMaaBW5jWCisB0xfpRx2u8tEDA8lWQV8mtawcoNQQqvFSPa+nakrUDA7Dsbl0RrXd7vDw==, + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w==, } - react@18.2.0: - resolution: { integrity: sha512-dummy } + lodash@4.17.25: + resolution: { integrity: sha512-sha256-dummy } snapshots: - lodash@4.17.22: {} - react@18.2.0: {} + lodash@4.17.21: {} + lodash@4.17.25: {} diff --git a/packages/cli/e2e/fixtures/pnpm-transitive/before-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-transitive/before-pnpm-lock.yaml index cd9aed5..b79b6a1 100644 --- a/packages/cli/e2e/fixtures/pnpm-transitive/before-pnpm-lock.yaml +++ b/packages/cli/e2e/fixtures/pnpm-transitive/before-pnpm-lock.yaml @@ -9,9 +9,11 @@ importers: lodash: specifier: ^4.17.0 version: 4.17.21 - react: - specifier: ^18.0.0 - version: 18.2.0 + packages/pkg-a: + dependencies: + lodash: + specifier: ^4.17.0 + version: 4.17.20 packages: lodash@4.17.21: @@ -20,9 +22,9 @@ packages: integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w==, } - react@18.2.0: + lodash@4.17.20: resolution: { integrity: sha512-dummy } snapshots: lodash@4.17.21: {} - react@18.2.0: {} + lodash@4.17.20: {} diff --git a/packages/cli/e2e/fixtures/pnpm-transitive/workspace/packages/pkg-a/package.json b/packages/cli/e2e/fixtures/pnpm-transitive/workspace/packages/pkg-a/package.json new file mode 100644 index 0000000..8fbdf7c --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-transitive/workspace/packages/pkg-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-a", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.0" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock b/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock new file mode 100644 index 0000000..cd899ae --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock @@ -0,0 +1,6 @@ +# yarn lockfile v1 + +lodash@^4.17.0: + version "4.17.23" + resolved "https://registry.Yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== diff --git a/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock b/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock new file mode 100644 index 0000000..fbf0052 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock @@ -0,0 +1,6 @@ +# yarn lockfile v1 + +lodash@^4.17.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== diff --git a/packages/cli/e2e/fixtures/yarn-transitive/workspace/package.json b/packages/cli/e2e/fixtures/yarn-transitive/workspace/package.json new file mode 100644 index 0000000..b356332 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-transitive/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-monorepo", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..4a60855 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..cb8ce80 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-transitive/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } +} diff --git a/packages/cli/e2e/transitive-dependency-detection.test.ts b/packages/cli/e2e/transitive-dependency-detection.test.ts index 3327531..5759a71 100644 --- a/packages/cli/e2e/transitive-dependency-detection.test.ts +++ b/packages/cli/e2e/transitive-dependency-detection.test.ts @@ -1,90 +1,143 @@ import { describe, expect, it } from 'vitest'; import { join } from 'path'; import { fileURLToPath } from 'url'; -import { readFileSync } from 'fs'; import { runAffectedCommand } from '../src/commands/run-affected-command.js'; import type { CliOptions } from '../src/options/cli-options.types.js'; -import { parsePnpmLockfile } from '@lockfile-affected/lockfile-pnpm'; const __filename = fileURLToPath(import.meta.url); const __dirname = join(__filename, '..'); -const fixturesDir = join(__dirname, 'fixtures', 'pnpm-transitive'); - -/** - * E2E tests for transitive dependency detection in the CLI. - * - * ## Test Structure - * - * Workspace structure (pnpm v9.0 format): - * - pkg-base depends on lodash (external) - * - pkg-middle depends on pkg-base (workspace) - * - * When lodash changes from 4.17.21 to 4.17.22: - * - pkg-base should be affected (direct dependency on lodash) - * - pkg-middle should be affected (transitive via pkg-base) - * - * ### Known Limitations - * - * 1. **pnpm v6.1 Support**: The parser uses lockparse v0.5.0, which doesn't properly parse - * pnpm v6.1 format. Tests use v9.0 format which is fully supported. - */ -describe('E2E: Transitive dependency detection (pnpm)', () => { - it('parser correctly reads the fixture lockfiles and detects dependency changes', async () => { - // Arrange: Read fixture files - const beforeContent = readFileSync(join(fixturesDir, 'before-pnpm-lock.yaml'), 'utf-8'); - const afterContent = readFileSync(join(fixturesDir, 'after-pnpm-lock.yaml'), 'utf-8'); - - // Act: Parse both lockfiles - const beforeSnapshot = await parsePnpmLockfile(beforeContent); - const afterSnapshot = await parsePnpmLockfile(afterContent); - - // Debug output - console.log('Before snapshot:', Array.from(beforeSnapshot.entries())); - console.log('After snapshot:', Array.from(afterSnapshot.entries())); - - // Assert: Verify parser reads the lockfiles correctly - expect(beforeSnapshot.size).toBeGreaterThan(0); - expect(beforeSnapshot.has('lodash')).toBe(true); - expect(beforeSnapshot.get('lodash')).toBe('4.17.21'); - - expect(afterSnapshot.size).toBeGreaterThan(0); - expect(afterSnapshot.has('lodash')).toBe(true); - expect(afterSnapshot.get('lodash')).toBe('4.17.22'); - - // Verify that lodash changed - expect(beforeSnapshot.get('lodash')).not.toBe(afterSnapshot.get('lodash')); - }); - it('detects transitive dependencies when workspace package depends on another workspace package', async () => { - // Arrange - const workspaceRoot = join(fixturesDir, 'workspace'); - const beforeLockfilePath = join(fixturesDir, 'before-pnpm-lock.yaml'); - const afterLockfilePath = join(fixturesDir, 'after-pnpm-lock.yaml'); +const fixtures = [ + { + format: 'pnpm', + name: 'pnpm', + beforeLock: 'before-pnpm-lock.yaml', + afterLock: 'after-pnpm-lock.yaml', + }, + { + format: 'npm', + name: 'npm', + beforeLock: 'before-package-lock.json', + afterLock: 'after-package-lock.json', + }, + { + format: 'yarn', + name: 'yarn', + beforeLock: 'before-yarn.lock', + afterLock: 'after-yarn.lock', + }, + { + format: 'bun', + name: 'bun', + beforeLock: 'before-bun.lock', + afterLock: 'after-bun.lock', + }, +] as const; - const options: CliOptions = { - lockfileBefore: beforeLockfilePath, - lockfileAfter: afterLockfilePath, - workspaceRoot, - output: 'json', - format: 'pnpm', - deps: true, - dev: true, - peer: true, - optional: true, - }; +describe.each(fixtures)( + 'E2E: Transitive dependency detection ($name)', + ({ format, name, beforeLock, afterLock }) => { + const fixturesDir = join(__dirname, 'fixtures', `${name}-transitive`); - // Act - const result = await runAffectedCommand(options); - console.log('CLI result:', result); + it('detects changes when lodash version changes in lockfile', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + const afterLockfilePath = join(fixturesDir, afterLock); - // Assert: Both pkg-base (direct) and pkg-middle (transitive) should be affected - const affected = JSON.parse(result); - expect(affected).toContain('pkg-base'); - expect(affected).toContain('pkg-middle'); - }); + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: true, + peer: true, + optional: true, + }; + + const result = await runAffectedCommand(options); - it('respects dependency type filtering for transitive dependencies', async () => { - // Arrange + const affected = JSON.parse(result); + expect(affected).toContain('pkg-base'); + }); + + it('detects transitive dependencies when workspace package depends on another workspace package', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + const afterLockfilePath = join(fixturesDir, afterLock); + + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: true, + peer: true, + optional: true, + }; + + const result = await runAffectedCommand(options); + + const affected = JSON.parse(result); + expect(affected).toContain('pkg-base'); + expect(affected).toContain('pkg-middle'); + }); + + it('respects dependency type filtering for transitive dependencies', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + const afterLockfilePath = join(fixturesDir, afterLock); + + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: false, + peer: false, + optional: false, + }; + + const result = await runAffectedCommand(options); + + const affected = JSON.parse(result); + expect(affected).toContain('pkg-base'); + expect(affected).toContain('pkg-middle'); + }); + + it('returns empty when lockfiles are identical', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: beforeLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: true, + peer: true, + optional: true, + }; + + const result = await runAffectedCommand(options); + + const affected = JSON.parse(result); + expect(affected).toEqual([]); + }); + }, +); + +describe('E2E: Per-importer detection (pnpm only)', () => { + const fixturesDir = join(__dirname, 'fixtures', 'pnpm-transitive'); + + it('detects changes in specific importer even when root unchanged', async () => { const workspaceRoot = join(fixturesDir, 'workspace'); const beforeLockfilePath = join(fixturesDir, 'before-pnpm-lock.yaml'); const afterLockfilePath = join(fixturesDir, 'after-pnpm-lock.yaml'); @@ -96,17 +149,14 @@ describe('E2E: Transitive dependency detection (pnpm)', () => { output: 'json', format: 'pnpm', deps: true, - dev: false, - peer: false, - optional: false, + dev: true, + peer: true, + optional: true, }; - // Act const result = await runAffectedCommand(options); - - // Assert: Production dependencies should still detect transitive packages const affected = JSON.parse(result); - expect(affected).toContain('pkg-base'); - expect(affected).toContain('pkg-middle'); + + expect(affected).toContain('pkg-a'); }); }); diff --git a/packages/core/src/affected/find-affected-packages.test.ts b/packages/core/src/affected/find-affected-packages.test.ts index 714982a..ec69da8 100644 --- a/packages/core/src/affected/find-affected-packages.test.ts +++ b/packages/core/src/affected/find-affected-packages.test.ts @@ -16,12 +16,14 @@ function makeParser(): LockfileParser { format: 'fake', lockfileNames: ['fake.lock'], parse: async (content: string): Promise => { - const map = new Map(); + const snapshot = new Map>(); + const rootPackages = new Map(); for (const line of content.split('\n').filter(Boolean)) { const [name, version] = line.split('@'); - if (name && version) map.set(name, version); + if (name && version) rootPackages.set(name, version); } - return map; + snapshot.set('.', rootPackages); + return snapshot; }, }; } diff --git a/packages/core/src/affected/resolve-affected-packages.test.ts b/packages/core/src/affected/resolve-affected-packages.test.ts index 048114d..ac64498 100644 --- a/packages/core/src/affected/resolve-affected-packages.test.ts +++ b/packages/core/src/affected/resolve-affected-packages.test.ts @@ -3,8 +3,8 @@ import { ALL_DEPENDENCY_TYPES, type LockfileDiff, type WorkspaceGraph } from '.. import { resolveAffectedPackages } from './resolve-affected-packages.js'; const emptyDiff: LockfileDiff = { - added: new Map(), - removed: new Map(), + addedContexts: new Map(), + removedContexts: new Map(), changed: new Map(), }; @@ -40,7 +40,16 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['lodash', { from: '4.17.20', to: '4.17.21' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '4.17.20', to: '4.17.21' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); @@ -56,7 +65,16 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['vitest', { from: '3.0.0', to: '3.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['vitest', { from: '3.0.0', to: '3.1.0' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); @@ -70,7 +88,16 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['vitest', { from: '3.0.0', to: '3.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['vitest', { from: '3.0.0', to: '3.1.0' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, { dependencies: true }); @@ -91,15 +118,23 @@ describe('resolveAffectedPackages', () => { const diff: LockfileDiff = { ...emptyDiff, changed: new Map([ - ['lodash', { from: '4.0.0', to: '4.1.0' }], - ['vitest', { from: '3.0.0', to: '3.1.0' }], + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([ + ['lodash', { from: '4.0.0', to: '4.1.0' }], + ['vitest', { from: '3.0.0', to: '3.1.0' }], + ]), + }, + ], ]), }; const prodOnly = resolveAffectedPackages(diff, workspace, { dependencies: true }); const devOnly = resolveAffectedPackages(diff, workspace, { devDependencies: true }); - // Both filters mark app as affected, but for different reasons expect(prodOnly.has('app')).toBe(true); expect(devOnly.has('app')).toBe(true); }); @@ -109,8 +144,8 @@ describe('resolveAffectedPackages', () => { ['app', { name: 'app', dependencyGroups: pkgWith({ dependencies: ['new-pkg', 'old-pkg'] }) }], ]); const diff: LockfileDiff = { - added: new Map([['new-pkg', '1.0.0']]), - removed: new Map([['old-pkg', '1.0.0']]), + addedContexts: new Map([['.', new Map([['new-pkg', '1.0.0']])]]), + removedContexts: new Map([['.', new Map([['old-pkg', '1.0.0']])]]), changed: new Map(), }; @@ -130,7 +165,16 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['lodash', { from: '4.17.20', to: '4.17.21' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '4.17.20', to: '4.17.21' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); @@ -147,7 +191,16 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['lodash', { from: '4.0.0', to: '4.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '4.0.0', to: '4.1.0' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); @@ -156,11 +209,6 @@ describe('resolveAffectedPackages', () => { }); it('marks transitive dependents as affected (pkg-b → pkg-a → lodash)', () => { - // Workspace structure: - // - pkg-top depends on pkg-middle - // - pkg-middle depends on pkg-base - // - pkg-base depends on lodash (external) - // When lodash changes, ALL packages should be affected const workspace: WorkspaceGraph = new Map([ ['pkg-top', { name: 'pkg-top', dependencyGroups: pkgWith({ dependencies: ['pkg-middle'] }) }], [ @@ -171,65 +219,76 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['lodash', { from: '4.17.21', to: '4.17.22' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '4.17.21', to: '4.17.22' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); - // All three packages should be affected due to transitive chain expect(affected.size).toBe(3); - expect(affected.has('pkg-base')).toBe(true); // direct dependency on lodash - expect(affected.has('pkg-middle')).toBe(true); // transitive via pkg-base - expect(affected.has('pkg-top')).toBe(true); // transitive via pkg-middle + expect(affected.has('pkg-base')).toBe(true); + expect(affected.has('pkg-middle')).toBe(true); + expect(affected.has('pkg-top')).toBe(true); }); it('marks transitive dependents with devDependencies', () => { - // Workspace structure: - // - pkg-b depends on pkg-a (devDependency) - // - pkg-a depends on vitest const workspace: WorkspaceGraph = new Map([ ['pkg-b', { name: 'pkg-b', dependencyGroups: pkgWith({ devDependencies: ['pkg-a'] }) }], ['pkg-a', { name: 'pkg-a', dependencyGroups: pkgWith({ devDependencies: ['vitest'] }) }], ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['vitest', { from: '2.0.0', to: '2.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['vitest', { from: '2.0.0', to: '2.1.0' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); - // Both should be affected due to transitive chain expect(affected.has('pkg-a')).toBe(true); expect(affected.has('pkg-b')).toBe(true); }); it('respects filter when traversing transitive dependencies', () => { - // Workspace: pkg-b → pkg-a → vitest (devDep) const workspace: WorkspaceGraph = new Map([ ['pkg-b', { name: 'pkg-b', dependencyGroups: pkgWith({ dependencies: ['pkg-a'] }) }], ['pkg-a', { name: 'pkg-a', dependencyGroups: pkgWith({ devDependencies: ['vitest'] }) }], ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['vitest', { from: '2.0.0', to: '2.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['vitest', { from: '2.0.0', to: '2.1.0' }]]), + }, + ], + ]), }; - // Only check production dependencies - should NOT include vitest changes const affected = resolveAffectedPackages(diff, workspace, { dependencies: true }); - // pkg-a has vitest in devDependencies (not prod), so not directly affected - // pkg-b depends on pkg-a (prod), but pkg-a is not affected expect(affected.size).toBe(0); }); it('handles diamond dependency graph', () => { - // Workspace structure (diamond): - // app - // / \ - // lib-a lib-b - // \ / - // lib-c - // When lib-c changes, app, lib-a, and lib-b should all be affected const workspace: WorkspaceGraph = new Map([ ['app', { name: 'app', dependencyGroups: pkgWith({ dependencies: ['lib-a', 'lib-b'] }) }], ['lib-a', { name: 'lib-a', dependencyGroups: pkgWith({ dependencies: ['lib-c'] }) }], @@ -238,12 +297,20 @@ describe('resolveAffectedPackages', () => { ]); const diff: LockfileDiff = { ...emptyDiff, - changed: new Map([['lib-c', { from: '1.0.0', to: '1.1.0' }]]), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lib-c', { from: '1.0.0', to: '1.1.0' }]]), + }, + ], + ]), }; const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); - // All packages that depend on lib-c should be affected expect(affected.has('lib-c')).toBe(true); expect(affected.has('lib-a')).toBe(true); expect(affected.has('lib-b')).toBe(true); diff --git a/packages/core/src/affected/resolve-affected-packages.ts b/packages/core/src/affected/resolve-affected-packages.ts index d5e4090..11d5a6b 100644 --- a/packages/core/src/affected/resolve-affected-packages.ts +++ b/packages/core/src/affected/resolve-affected-packages.ts @@ -6,17 +6,6 @@ import type { } from '../types/lockfile.js'; import { findDependents } from './find-dependents.js'; -/** - * Resolves which workspace packages are affected by lockfile changes. - * - * A package is considered affected if any dependency — within the groups - * selected by `filter` — appears in the lockfile diff (added, removed, or changed). - * - * This includes transitive dependencies: if pkg-a depends on pkg-b, and pkg-b depends - * on a changed external package, both pkg-a and pkg-b are marked as affected. - * - * Pure function: no side effects. - */ export function resolveAffectedPackages( diff: LockfileDiff, workspace: WorkspaceGraph, @@ -28,24 +17,20 @@ export function resolveAffectedPackages( return new Set(); } - // Step 1: Find workspace packages that appear in the lockfile diff - // (these are workspace packages whose version changed in the lockfile) const directlyAffected = new Set(); + for (const [packageName] of workspace) { if (changedNames.has(packageName)) { directlyAffected.add(packageName); } } - // Step 2: Find packages that directly depend on changed external packages for (const [packageName, pkg] of workspace) { if (isAffected(pkg.dependencyGroups, changedNames, filter)) { directlyAffected.add(packageName); } } - // Step 3: Find all transitive dependents of the directly affected packages - // This traverses the workspace graph to find the full chain of affected packages const allAffected = new Set(directlyAffected); const toProcess = Array.from(directlyAffected); @@ -66,9 +51,21 @@ export function resolveAffectedPackages( function collectChangedDependencyNames(diff: LockfileDiff): ReadonlySet { const names = new Set(); - for (const name of diff.added.keys()) names.add(name); - for (const name of diff.removed.keys()) names.add(name); - for (const name of diff.changed.keys()) names.add(name); + + for (const [, contextDiff] of diff.changed) { + for (const name of contextDiff.added.keys()) names.add(name); + for (const name of contextDiff.removed.keys()) names.add(name); + for (const name of contextDiff.changed.keys()) names.add(name); + } + + for (const [, packages] of diff.addedContexts) { + for (const name of packages.keys()) names.add(name); + } + + for (const [, packages] of diff.removedContexts) { + for (const name of packages.keys()) names.add(name); + } + return names; } diff --git a/packages/core/src/diff/diff-lockfile-snapshots.test.ts b/packages/core/src/diff/diff-lockfile-snapshots.test.ts index 42bdfd0..0889dd8 100644 --- a/packages/core/src/diff/diff-lockfile-snapshots.test.ts +++ b/packages/core/src/diff/diff-lockfile-snapshots.test.ts @@ -1,99 +1,126 @@ import { describe, expect, it } from 'vitest'; import { diffLockfileSnapshots } from './diff-lockfile-snapshots.js'; +const makeSnapshot = (entries: Record): Map> => { + const snapshot = new Map>(); + const rootPackages = new Map(Object.entries(entries)); + snapshot.set('.', rootPackages); + return snapshot; +}; + describe('diffLockfileSnapshots', () => { it('returns empty diff when both snapshots are identical', () => { - const snapshot = new Map([['lodash', '4.17.21']]); + const snapshot = makeSnapshot({ lodash: '4.17.21' }); const diff = diffLockfileSnapshots(snapshot, snapshot); - expect(diff.added.size).toBe(0); - expect(diff.removed.size).toBe(0); + expect(diff.addedContexts.size).toBe(0); + expect(diff.removedContexts.size).toBe(0); expect(diff.changed.size).toBe(0); }); it('detects added packages', () => { - const before = new Map([['lodash', '4.17.21']]); - const after = new Map([ - ['lodash', '4.17.21'], - ['react', '18.3.0'], - ]); + const before = makeSnapshot({ lodash: '4.17.21' }); + const after = makeSnapshot({ lodash: '4.17.21', react: '18.3.0' }); const diff = diffLockfileSnapshots(before, after); - expect(diff.added.size).toBe(1); - expect(diff.added.get('react')).toBe('18.3.0'); - expect(diff.removed.size).toBe(0); - expect(diff.changed.size).toBe(0); + expect(diff.changed.get('.')?.added.size).toBe(1); + expect(diff.changed.get('.')?.added.get('react')).toBe('18.3.0'); + expect(diff.changed.get('.')?.removed.size).toBe(0); + expect(diff.changed.get('.')?.changed.size).toBe(0); }); it('detects removed packages', () => { - const before = new Map([ - ['lodash', '4.17.21'], - ['react', '18.3.0'], - ]); - const after = new Map([['lodash', '4.17.21']]); + const before = makeSnapshot({ lodash: '4.17.21', react: '18.3.0' }); + const after = makeSnapshot({ lodash: '4.17.21' }); const diff = diffLockfileSnapshots(before, after); - expect(diff.removed.size).toBe(1); - expect(diff.removed.get('react')).toBe('18.3.0'); - expect(diff.added.size).toBe(0); - expect(diff.changed.size).toBe(0); + expect(diff.changed.get('.')?.removed.size).toBe(1); + expect(diff.changed.get('.')?.removed.get('react')).toBe('18.3.0'); + expect(diff.changed.get('.')?.added.size).toBe(0); + expect(diff.changed.get('.')?.changed.size).toBe(0); }); it('detects changed packages', () => { - const before = new Map([['lodash', '4.17.20']]); - const after = new Map([['lodash', '4.17.21']]); + const before = makeSnapshot({ lodash: '4.17.20' }); + const after = makeSnapshot({ lodash: '4.17.21' }); const diff = diffLockfileSnapshots(before, after); expect(diff.changed.size).toBe(1); - expect(diff.changed.get('lodash')).toEqual({ from: '4.17.20', to: '4.17.21' }); - expect(diff.added.size).toBe(0); - expect(diff.removed.size).toBe(0); + expect(diff.changed.get('.')?.changed.get('lodash')).toEqual({ + from: '4.17.20', + to: '4.17.21', + }); + expect(diff.changed.get('.')?.added.size).toBe(0); + expect(diff.changed.get('.')?.removed.size).toBe(0); }); it('handles multiple simultaneous changes', () => { - const before = new Map([ - ['lodash', '4.17.20'], - ['react', '17.0.2'], - ['unused', '1.0.0'], - ]); - const after = new Map([ - ['lodash', '4.17.21'], - ['react', '17.0.2'], - ['new-pkg', '2.0.0'], - ]); + const before = makeSnapshot({ + lodash: '4.17.20', + react: '17.0.2', + unused: '1.0.0', + }); + const after = makeSnapshot({ + lodash: '4.17.21', + react: '17.0.2', + 'new-pkg': '2.0.0', + }); const diff = diffLockfileSnapshots(before, after); - expect(diff.changed.size).toBe(1); - expect(diff.changed.get('lodash')).toEqual({ from: '4.17.20', to: '4.17.21' }); - expect(diff.added.size).toBe(1); - expect(diff.added.get('new-pkg')).toBe('2.0.0'); - expect(diff.removed.size).toBe(1); - expect(diff.removed.get('unused')).toBe('1.0.0'); + const rootChanged = diff.changed.get('.'); + expect(rootChanged?.changed.size).toBe(1); + expect(rootChanged?.changed.get('lodash')).toEqual({ from: '4.17.20', to: '4.17.21' }); + expect(rootChanged?.added.size).toBe(1); + expect(rootChanged?.added.get('new-pkg')).toBe('2.0.0'); + expect(rootChanged?.removed.size).toBe(1); + expect(rootChanged?.removed.get('unused')).toBe('1.0.0'); }); it('handles empty before snapshot', () => { - const before = new Map(); - const after = new Map([['react', '18.0.0']]); + const before = new Map>(); + const after = makeSnapshot({ react: '18.0.0' }); const diff = diffLockfileSnapshots(before, after); - expect(diff.added.size).toBe(1); - expect(diff.removed.size).toBe(0); + expect(diff.addedContexts.size).toBe(1); + expect(diff.addedContexts.get('.')?.get('react')).toBe('18.0.0'); + expect(diff.removedContexts.size).toBe(0); expect(diff.changed.size).toBe(0); }); it('handles empty after snapshot', () => { - const before = new Map([['react', '18.0.0']]); - const after = new Map(); + const before = makeSnapshot({ react: '18.0.0' }); + const after = new Map>(); const diff = diffLockfileSnapshots(before, after); - expect(diff.removed.size).toBe(1); - expect(diff.added.size).toBe(0); + expect(diff.removedContexts.size).toBe(1); + expect(diff.removedContexts.get('.')?.get('react')).toBe('18.0.0'); + expect(diff.addedContexts.size).toBe(0); expect(diff.changed.size).toBe(0); }); + + it('detects changes in specific importer context', () => { + const before = new Map>([ + ['.', new Map([['lodash', '4.17.21']])], + ['packages/pkg-a', new Map([['lodash', '4.17.20']])], + ]); + const after = new Map>([ + ['.', new Map([['lodash', '4.17.21']])], + ['packages/pkg-a', new Map([['lodash', '4.17.25']])], + ]); + + const diff = diffLockfileSnapshots(before, after); + + expect(diff.changed.size).toBe(1); + expect(diff.changed.get('packages/pkg-a')?.changed.get('lodash')).toEqual({ + from: '4.17.20', + to: '4.17.25', + }); + expect(diff.changed.get('.')).toBeUndefined(); + }); }); diff --git a/packages/core/src/diff/diff-lockfile-snapshots.ts b/packages/core/src/diff/diff-lockfile-snapshots.ts index 54fcede..e775c6c 100644 --- a/packages/core/src/diff/diff-lockfile-snapshots.ts +++ b/packages/core/src/diff/diff-lockfile-snapshots.ts @@ -1,13 +1,48 @@ -import type { LockfileDiff, LockfileSnapshot } from '../types/lockfile.js'; +import type { ContextDiff, LockfileDiff, LockfileSnapshot } from '../types/lockfile.js'; /** * Computes the difference between two lockfile snapshots. + * Compares per-context to detect changes in specific workspace packages. * Pure function: no side effects, same input always produces same output. */ export function diffLockfileSnapshots( before: LockfileSnapshot, after: LockfileSnapshot, ): LockfileDiff { + const addedContexts = new Map>(); + const removedContexts = new Map>(); + const changed = new Map(); + + for (const [context, packages] of after) { + const beforePackages = before.get(context); + + if (beforePackages === undefined) { + addedContexts.set(context, packages); + } else { + const contextDiff = diffContext(beforePackages, packages); + if ( + contextDiff.added.size > 0 || + contextDiff.removed.size > 0 || + contextDiff.changed.size > 0 + ) { + changed.set(context, contextDiff); + } + } + } + + for (const [context, packages] of before) { + if (!after.has(context)) { + removedContexts.set(context, packages); + } + } + + return { addedContexts, removedContexts, changed }; +} + +function diffContext( + before: ReadonlyMap, + after: ReadonlyMap, +): ContextDiff { const added = new Map(); const removed = new Map(); const changed = new Map(); diff --git a/packages/core/src/types/lockfile.ts b/packages/core/src/types/lockfile.ts index e012d6c..54ffe08 100644 --- a/packages/core/src/types/lockfile.ts +++ b/packages/core/src/types/lockfile.ts @@ -1,21 +1,42 @@ /** - * A normalized snapshot of a lockfile: maps each package name to its resolved version. - * This is the common representation all lockfile adapters produce. + * A normalized snapshot of a lockfile: maps each context (importer/workspace path) + * to a map of package name → resolved version. + * + * The context is the workspace/importer path: + * - pnpm: "." for root, "packages/pkg-a" for workspace packages + * - npm/yarn/bun: "." for root, workspace paths + * + * This hierarchical structure preserves per-context version resolution, ensuring + * changes in specific workspace packages are detected even if the same package + * exists elsewhere with a different version. */ -export type LockfileSnapshot = ReadonlyMap; +export type LockfileSnapshot = ReadonlyMap>; /** - * The result of comparing two lockfile snapshots. + * Changes within a single context (importer/workspace). */ -export type LockfileDiff = { - /** Packages added in the new snapshot. key = name, value = new version */ +export type ContextDiff = { + /** Packages added in this context. key = name, value = new version */ readonly added: ReadonlyMap; - /** Packages removed from the new snapshot. key = name, value = old version */ + /** Packages removed from this context. key = name, value = old version */ readonly removed: ReadonlyMap; - /** Packages whose resolved version changed. key = name, value = { from, to } */ + /** Packages whose resolved version changed in this context. key = name, value = { from, to } */ readonly changed: ReadonlyMap; }; +/** + * The result of comparing two lockfile snapshots. + * Compares per-context to detect changes in specific workspace packages. + */ +export type LockfileDiff = { + /** Contexts added in the new snapshot (new workspace packages). key = context path */ + readonly addedContexts: ReadonlyMap>; + /** Contexts removed from the new snapshot. key = context path, value = packages that were there */ + readonly removedContexts: ReadonlyMap>; + /** Changes per context. key = context path, value = diff for that context */ + readonly changed: ReadonlyMap; +}; + /** * The dependency types tracked per workspace package. * Mirrors the fields in package.json. diff --git a/packages/lockfile-bun/package.json b/packages/lockfile-bun/package.json index c91b069..1fe402a 100644 --- a/packages/lockfile-bun/package.json +++ b/packages/lockfile-bun/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@lockfile-affected/core": "workspace:*", - "lockparse": "^0.5.0" + "jsonc-parser": "^3.0.0" }, "devDependencies": { "vitest": "*", diff --git a/packages/lockfile-bun/src/parse-bun-lockfile.test.ts b/packages/lockfile-bun/src/parse-bun-lockfile.test.ts index 844cac9..7c7bceb 100644 --- a/packages/lockfile-bun/src/parse-bun-lockfile.test.ts +++ b/packages/lockfile-bun/src/parse-bun-lockfile.test.ts @@ -52,25 +52,30 @@ const BUN_LOCK_FIXTURE = `\ `; describe('parseBunLockfile', () => { - it('returns a snapshot with normalized package name -> version entries', async () => { + it('returns a hierarchical snapshot with root context', async () => { const snapshot = await parseBunLockfile(BUN_LOCK_FIXTURE); expect(snapshot.size).toBeGreaterThan(0); - expect(snapshot.get('lodash')).toBe('4.17.21'); - expect(snapshot.get('react')).toBe('18.3.0'); + expect(snapshot.has('.')).toBe(true); + + const rootPackages = snapshot.get('.'); + expect(rootPackages?.get('lodash')).toBe('4.17.21'); + expect(rootPackages?.get('react')).toBe('18.3.0'); }); - it('deduplicates package names and keeps first encountered version', async () => { + it('resolves direct workspace dependencies', async () => { const snapshot = await parseBunLockfile(BUN_LOCK_FIXTURE); - expect(snapshot.get('loose-envify')).toBe('1.4.0'); - expect(snapshot.get('js-tokens')).toBe('4.0.0'); + const rootPackages = snapshot.get('.'); + expect(rootPackages?.get('lodash')).toBe('4.17.21'); + expect(rootPackages?.get('react')).toBe('18.3.0'); }); - it('returns a LockfileSnapshot (ReadonlyMap)', async () => { + it('returns a LockfileSnapshot (ReadonlyMap of ReadonlyMaps)', async () => { const snapshot = await parseBunLockfile(BUN_LOCK_FIXTURE); expect(snapshot).toBeInstanceOf(Map); + expect(snapshot.get('.')).toBeInstanceOf(Map); }); }); @@ -86,6 +91,6 @@ describe('bunLockfileParser', () => { it('parse delegates to parseBunLockfile', async () => { const snapshot = await bunLockfileParser.parse(BUN_LOCK_FIXTURE); - expect(snapshot.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); }); }); diff --git a/packages/lockfile-bun/src/parse-bun-lockfile.ts b/packages/lockfile-bun/src/parse-bun-lockfile.ts index 85280be..0437d0b 100644 --- a/packages/lockfile-bun/src/parse-bun-lockfile.ts +++ b/packages/lockfile-bun/src/parse-bun-lockfile.ts @@ -1,22 +1,35 @@ import type { LockfileParser, LockfileSnapshot } from '@lockfile-affected/core'; -import { parse, type ParsedDependency } from 'lockparse'; +import { parse as jsoncParse } from 'jsonc-parser'; + +interface BunLockfile { + workspaces?: Record }>; + packages: Record; +} -/** - * Parses a bun.lock file into a normalized LockfileSnapshot. - * Uses lockparse as the underlying parser for a unified IR. - */ export async function parseBunLockfile(content: string): Promise { - const parsed = await parse(content, 'bun'); - return toSnapshot(parsed.packages); + const parsed = jsoncParse(content) as BunLockfile; + return toSnapshot(parsed); } -function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { - const snapshot = new Map(); +function toSnapshot(lockfile: BunLockfile): LockfileSnapshot { + const snapshot = new Map>(); - for (const pkg of packages) { - if (pkg.name && pkg.version) { - if (!snapshot.has(pkg.name)) { - snapshot.set(pkg.name, pkg.version); + if (lockfile.workspaces) { + for (const [workspacePath, workspace] of Object.entries(lockfile.workspaces)) { + const packages = new Map(); + const context = workspacePath === '' ? '.' : workspacePath; + + if (workspace.dependencies) { + for (const [pkgName, specifier] of Object.entries(workspace.dependencies)) { + const resolvedVersion = resolveVersion(pkgName, specifier, lockfile.packages); + if (resolvedVersion) { + packages.set(pkgName, resolvedVersion); + } + } + } + + if (packages.size > 0) { + snapshot.set(context, packages); } } } @@ -24,9 +37,31 @@ function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { return snapshot; } -/** - * The LockfileParser adapter for Bun, conforming to the core contract. - */ +function resolveVersion( + pkgName: string, + _specifier: string, + packages: Record, +): string | undefined { + const exactKey = pkgName; + if (packages[exactKey]?.[0]) { + return extractVersion(packages[exactKey][0]); + } + + for (const [key, value] of Object.entries(packages)) { + if (key === pkgName || key.startsWith(pkgName + '@')) { + return extractVersion(value[0]); + } + } + + return undefined; +} + +function extractVersion(nameAtVersion: string | undefined): string | undefined { + if (!nameAtVersion) return undefined; + const atIndex = nameAtVersion.lastIndexOf('@'); + return atIndex === -1 ? nameAtVersion : nameAtVersion.slice(atIndex + 1); +} + export const bunLockfileParser: LockfileParser = { format: 'bun', lockfileNames: ['bun.lock'], diff --git a/packages/lockfile-npm/package.json b/packages/lockfile-npm/package.json index c77f3be..5803878 100644 --- a/packages/lockfile-npm/package.json +++ b/packages/lockfile-npm/package.json @@ -45,13 +45,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@lockfile-affected/core": "workspace:*", - "lockparse": "^0.5.0" + "@lockfile-affected/core": "workspace:*" }, "devDependencies": { - "vitest": "*", - "@vitest/coverage-v8": "*", "@types/node": "^22.0.0", - "typescript": "^5.7.0" + "@vitest/coverage-v8": "*", + "typescript": "^5.7.0", + "vitest": "*" } } diff --git a/packages/lockfile-npm/src/parse-npm-lockfile.test.ts b/packages/lockfile-npm/src/parse-npm-lockfile.test.ts index 5426496..bd7a158 100644 --- a/packages/lockfile-npm/src/parse-npm-lockfile.test.ts +++ b/packages/lockfile-npm/src/parse-npm-lockfile.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; import { parseNpmLockfile } from './parse-npm-lockfile.js'; -// Minimal valid package-lock.json v3 fixture const NPM_LOCK_V3 = JSON.stringify({ name: 'my-app', version: '1.0.0', @@ -18,7 +17,7 @@ const NPM_LOCK_V3 = JSON.stringify({ 'node_modules/lodash': { version: '4.17.21', resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz', - integrity: 'sha512-dummy', + integrity: 'sha512-sha512-dummy', }, 'node_modules/react': { version: '18.3.0', @@ -29,24 +28,21 @@ const NPM_LOCK_V3 = JSON.stringify({ }); describe('parseNpmLockfile', () => { - it('returns a snapshot with normalized package name → version entries', async () => { + it('returns a hierarchical snapshot with root context', async () => { const snapshot = await parseNpmLockfile(NPM_LOCK_V3); expect(snapshot.size).toBeGreaterThan(0); - expect(snapshot.has('lodash')).toBe(true); - expect(snapshot.get('lodash')).toBe('4.17.21'); - }); - - it('includes all packages from node_modules', async () => { - const snapshot = await parseNpmLockfile(NPM_LOCK_V3); + expect(snapshot.has('.')).toBe(true); - expect(snapshot.has('react')).toBe(true); - expect(snapshot.get('react')).toBe('18.3.0'); + const rootPackages = snapshot.get('.'); + expect(rootPackages?.has('lodash')).toBe(true); + expect(rootPackages?.get('lodash')).toBe('4.17.21'); }); - it('returns a LockfileSnapshot (ReadonlyMap)', async () => { + it('returns a LockfileSnapshot (ReadonlyMap of ReadonlyMaps)', async () => { const snapshot = await parseNpmLockfile(NPM_LOCK_V3); expect(snapshot).toBeInstanceOf(Map); + expect(snapshot.get('.')).toBeInstanceOf(Map); }); }); diff --git a/packages/lockfile-npm/src/parse-npm-lockfile.ts b/packages/lockfile-npm/src/parse-npm-lockfile.ts index 20c497f..d6a6f7d 100644 --- a/packages/lockfile-npm/src/parse-npm-lockfile.ts +++ b/packages/lockfile-npm/src/parse-npm-lockfile.ts @@ -1,33 +1,100 @@ import type { LockfileParser, LockfileSnapshot } from '@lockfile-affected/core'; -import { parse, type ParsedDependency } from 'lockparse'; -/** - * Parses a package-lock.json file into a normalized LockfileSnapshot. - * Supports lockfileVersion 2 and 3. - * Uses lockparse as the underlying parser for a unified IR. - */ +interface NpmPackageLock { + packages: Record; +} + +interface NpmPackageEntry { + version?: string; + resolved?: string; + dependencies?: Record; +} + export async function parseNpmLockfile(content: string): Promise { - const parsed = await parse(content, 'npm'); - return toSnapshot(parsed.packages); + const lockfile = JSON.parse(content) as NpmPackageLock; + return toSnapshot(lockfile); } -function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { - const snapshot = new Map(); +function toSnapshot(lockfile: NpmPackageLock): LockfileSnapshot { + const snapshot = new Map>(); - for (const pkg of packages) { - if (pkg.name && pkg.version) { - if (!snapshot.has(pkg.name)) { - snapshot.set(pkg.name, pkg.version); + const contexts = new Set(); + contexts.add('.'); + + for (const key of Object.keys(lockfile.packages)) { + if (key.startsWith('packages/')) { + const workspacePath = key.split('/node_modules/')[0]; + if (workspacePath && !workspacePath.includes('node_modules')) { + contexts.add(workspacePath); } } } + for (const context of contexts) { + const packages = extractContextPackages(context, lockfile.packages); + if (packages.size > 0) { + snapshot.set(context, packages); + } + } + return snapshot; } -/** - * The LockfileParser adapter for npm, conforming to the core contract. - */ +function extractContextPackages( + context: string, + packages: Record, +): Map { + const result = new Map(); + + const depKeys = getDependencyKeys(context, packages); + + for (const depKey of depKeys) { + const pkg = packages[depKey]; + if (pkg?.version) { + const depName = depKey + .replace(/^node_modules\//, '') + .split('/node_modules/') + .pop(); + if (depName) { + const nameParts = depName.split('/'); + const name = depName.startsWith('@') ? nameParts.slice(0, 2).join('/') : nameParts[0]; + if (name && !result.has(name)) { + result.set(name, pkg.version); + } + } + } + } + + return result; +} + +function getDependencyKeys(context: string, packages: Record): string[] { + const keys: string[] = []; + + if (context === '.') { + const rootPkg = packages['']; + if (rootPkg?.dependencies) { + for (const dep of Object.keys(rootPkg.dependencies)) { + keys.push(`node_modules/${dep}`); + } + } + } else { + const workspacePkg = packages[context]; + if (workspacePkg?.dependencies) { + for (const dep of Object.keys(workspacePkg.dependencies)) { + const nestedPath = `${context}/node_modules/${dep}`; + if (packages[nestedPath]) { + keys.push(nestedPath); + } else { + keys.push(`node_modules/${dep}`); + } + } + } + } + + return keys; +} + export const npmLockfileParser: LockfileParser = { format: 'npm', lockfileNames: ['package-lock.json'], diff --git a/packages/lockfile-pnpm/package.json b/packages/lockfile-pnpm/package.json index e07c90a..2ac882e 100644 --- a/packages/lockfile-pnpm/package.json +++ b/packages/lockfile-pnpm/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@lockfile-affected/core": "workspace:*", - "lockparse": "^0.5.0" + "yaml": "^2.0.0" }, "devDependencies": { "vitest": "*", diff --git a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts index 4c0923f..2a4d22d 100644 --- a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts +++ b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; import { parsePnpmLockfile } from './parse-pnpm-lockfile.js'; -// Minimal valid pnpm-lock.yaml v9 fixture const PNPM_LOCK_V9 = ` lockfileVersion: '9.0' @@ -17,7 +16,7 @@ importers: packages: lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w==} + resolution: {integrity: sha512-sha1} react@18.3.0: resolution: {integrity: sha512-dummy} @@ -38,26 +37,61 @@ snapshots: `.trim(); describe('parsePnpmLockfile', () => { - it('returns a snapshot with normalized package name → version entries', async () => { + it('returns a hierarchical snapshot with importer contexts', async () => { const snapshot = await parsePnpmLockfile(PNPM_LOCK_V9); - // Should contain the resolved packages from the lockfile expect(snapshot.size).toBeGreaterThan(0); - expect(snapshot.has('lodash')).toBe(true); - expect(snapshot.get('lodash')).toBe('4.17.21'); + expect(snapshot.has('.')).toBe(true); + + const rootPackages = snapshot.get('.'); + expect(rootPackages?.has('lodash')).toBe(true); + expect(rootPackages?.get('lodash')).toBe('4.17.21'); }); it('strips peer dependency suffixes from package identifiers', async () => { const snapshot = await parsePnpmLockfile(PNPM_LOCK_V9); - // react@18.3.0(typescript@5.7.0) should resolve to react → 18.3.0 - expect(snapshot.has('react')).toBe(true); - expect(snapshot.get('react')).toBe('18.3.0'); + const rootPackages = snapshot.get('.'); + expect(rootPackages?.has('react')).toBe(true); + expect(rootPackages?.get('react')).toBe('18.3.0'); }); - it('returns a LockfileSnapshot (ReadonlyMap)', async () => { + it('returns a LockfileSnapshot (ReadonlyMap of ReadonlyMaps)', async () => { const snapshot = await parsePnpmLockfile(PNPM_LOCK_V9); expect(snapshot).toBeInstanceOf(Map); + expect(snapshot.get('.')).toBeInstanceOf(Map); + }); + + it('parses multiple importers into separate contexts', async () => { + const multiImporterLock = ` +lockfileVersion: '9.0' + +importers: + .: + dependencies: + lodash: + specifier: ^4.17.0 + version: 4.17.21 + packages/pkg-a: + dependencies: + lodash: + specifier: ^4.17.0 + version: 4.17.25 + +packages: + lodash@4.17.21: + resolution: { integrity: sha-sha1 } + lodash@4.17.25: + resolution: { integrity: sha-sha2 } +`.trim(); + + const snapshot = await parsePnpmLockfile(multiImporterLock); + + expect(snapshot.has('.')).toBe(true); + expect(snapshot.has('packages/pkg-a')).toBe(true); + + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('packages/pkg-a')?.get('lodash')).toBe('4.17.25'); }); }); diff --git a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts index 95eb5a3..2bd39bf 100644 --- a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts +++ b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts @@ -1,36 +1,57 @@ import type { LockfileParser, LockfileSnapshot } from '@lockfile-affected/core'; -import { parse, type ParsedDependency } from 'lockparse'; - -/** - * Parses a pnpm-lock.yaml file into a normalized LockfileSnapshot. - * Uses lockparse as the underlying parser for a unified IR. - * - * Package names with peer dependency suffixes (e.g. react@18.0.0(typescript@5.0.0)) - * are normalized to just name → version. - */ +import YAML from 'yaml'; + +interface PnpmImporter { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; +} + +interface PnpmLockfile { + importers: Record; +} + export async function parsePnpmLockfile(content: string): Promise { - const parsed = await parse(content, 'pnpm'); - return toSnapshot(parsed.packages); + const parsed = YAML.parse(content) as PnpmLockfile; + return toSnapshot(parsed); } -function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { - const snapshot = new Map(); +function stripPeerSuffix(version: string): string { + const parenIndex = version.indexOf('('); + return parenIndex === -1 ? version : version.slice(0, parenIndex); +} - for (const pkg of packages) { - if (pkg.name && pkg.version) { - // First-encountered version wins. lockparse normalizes peer suffixes already. - if (!snapshot.has(pkg.name)) { - snapshot.set(pkg.name, pkg.version); +function toSnapshot(lockfile: PnpmLockfile): LockfileSnapshot { + const snapshot = new Map>(); + + for (const [importerPath, importer] of Object.entries(lockfile.importers)) { + const packages = new Map(); + + const depTypes = [ + importer.dependencies, + importer.devDependencies, + importer.peerDependencies, + importer.optionalDependencies, + ]; + + for (const deps of depTypes) { + if (deps) { + for (const [pkgName, pkgInfo] of Object.entries(deps)) { + const version = stripPeerSuffix(pkgInfo.version); + packages.set(pkgName, version); + } } } + + if (packages.size > 0) { + snapshot.set(importerPath, packages); + } } return snapshot; } -/** - * The LockfileParser adapter for pnpm, conforming to the core contract. - */ export const pnpmLockfileParser: LockfileParser = { format: 'pnpm', lockfileNames: ['pnpm-lock.yaml'], diff --git a/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts b/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts index 84213bd..d99e364 100644 --- a/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts +++ b/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts @@ -1,24 +1,23 @@ import { describe, expect, it } from 'vitest'; import { parseYarnLockfile, yarnLockfileParser } from './parse-yarn-lockfile.js'; -// Yarn classic (v1) format const CLASSIC_FIXTURE = `\ # yarn lockfile v1 lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-abc + resolved "https://registry.Yarnpkg.com/lodash/-/lodash-4.17.21.tgz" + integrity sha512-512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w==" + integrity sha512-512-abc react@^18.2.0: version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz" - integrity sha512-def + resolved "https://registry.Yarnpkg.com/react/-/react-18.2.0.tgz" + integrity sha512-512-def dependencies: loose-envify "^1.1.0" `; -// Yarn berry (v2+) format const BERRY_FIXTURE = `\ __metadata: version: 6 @@ -41,39 +40,43 @@ __metadata: describe('parseYarnLockfile', () => { describe('classic (v1)', () => { - it('parses packages into name → version entries', async () => { + it('parses packages into name → version entries in root context', async () => { const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); - expect(snapshot.get('lodash')).toBe('4.17.21'); - expect(snapshot.get('react')).toBe('18.2.0'); + expect(snapshot.has('.')).toBe(true); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('.')?.get('react')).toBe('18.2.0'); }); it('deduplicates multi-range entries to a single version', async () => { const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); - // lodash appears twice (two ranges) but must only be in the snapshot once - expect(snapshot.size).toBe(2); + const rootPackages = snapshot.get('.'); + expect(rootPackages?.size).toBe(2); }); - it('returns a map with exactly the packages in the lockfile', async () => { + it('returns packages in root context', async () => { const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); - expect([...snapshot.keys()].sort()).toEqual(['lodash', 'react']); + const rootPackages = snapshot.get('.'); + expect([...(rootPackages?.keys() ?? [])].sort()).toEqual(['lodash', 'react']); }); }); describe('berry (v2+)', () => { - it('parses packages into name → version entries', async () => { + it('parses packages into name → version entries in root context', async () => { const snapshot = await parseYarnLockfile(BERRY_FIXTURE); - expect(snapshot.get('lodash')).toBe('4.17.21'); - expect(snapshot.get('react')).toBe('18.2.0'); + expect(snapshot.has('.')).toBe(true); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('.')?.get('react')).toBe('18.2.0'); }); it('deduplicates multi-range entries to a single version', async () => { const snapshot = await parseYarnLockfile(BERRY_FIXTURE); - expect(snapshot.size).toBe(2); + const rootPackages = snapshot.get('.'); + expect(rootPackages?.size).toBe(2); }); }); @@ -96,6 +99,6 @@ describe('yarnLockfileParser', () => { it('parse delegates to parseYarnLockfile', async () => { const snapshot = await yarnLockfileParser.parse(CLASSIC_FIXTURE); - expect(snapshot.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); }); }); diff --git a/packages/lockfile-yarn/src/parse-yarn-lockfile.ts b/packages/lockfile-yarn/src/parse-yarn-lockfile.ts index 2e66fb8..bed8a52 100644 --- a/packages/lockfile-yarn/src/parse-yarn-lockfile.ts +++ b/packages/lockfile-yarn/src/parse-yarn-lockfile.ts @@ -1,37 +1,29 @@ import type { LockfileParser, LockfileSnapshot } from '@lockfile-affected/core'; import { parse, type ParsedDependency } from 'lockparse'; -/** - * Parses a yarn.lock file into a normalized LockfileSnapshot. - * Supports both classic (v1) and berry (v2+) formats — lockparse handles both - * via the same 'yarn' format key. - * - * Multi-range entries (e.g. `lodash@^4.17.20, lodash@^4.17.21`) produce - * duplicate package entries from lockparse; first-encountered version wins. - */ export async function parseYarnLockfile(content: string): Promise { const parsed = await parse(content, 'yarn'); return toSnapshot(parsed.packages); } function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { - const snapshot = new Map(); + const snapshot = new Map>(); + + const rootPackages = new Map(); for (const pkg of packages) { if (pkg.name && pkg.version) { - if (!snapshot.has(pkg.name)) { - snapshot.set(pkg.name, pkg.version); - } + rootPackages.set(pkg.name, pkg.version); } } + if (rootPackages.size > 0) { + snapshot.set('.', rootPackages); + } + return snapshot; } -/** - * The LockfileParser adapter for yarn, conforming to the core contract. - * Both classic (v1) and berry (v2+) use the same filename. - */ export const yarnLockfileParser: LockfileParser = { format: 'yarn', lockfileNames: ['yarn.lock'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 572daaf..fe4d158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: version: 22.19.15 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) eslint: specifier: ^9.22.0 version: 9.39.4 @@ -67,10 +67,10 @@ importers: version: 8.57.2(eslint@9.39.4)(typescript@5.9.3) vite-tsconfig-paths: specifier: 6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)) + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.3)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/cli: dependencies: @@ -95,13 +95,13 @@ importers: version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/core: devDependencies: @@ -110,79 +110,76 @@ importers: version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/lockfile-bun: dependencies: '@lockfile-affected/core': specifier: workspace:* version: link:../core - lockparse: - specifier: ^0.5.0 - version: 0.5.0 + jsonc-parser: + specifier: ^3.0.0 + version: 3.3.1 devDependencies: '@types/node': specifier: ^22.0.0 version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/lockfile-npm: dependencies: '@lockfile-affected/core': specifier: workspace:* version: link:../core - lockparse: - specifier: ^0.5.0 - version: 0.5.0 devDependencies: '@types/node': specifier: ^22.0.0 version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/lockfile-pnpm: dependencies: '@lockfile-affected/core': specifier: workspace:* version: link:../core - lockparse: - specifier: ^0.5.0 - version: 0.5.0 + yaml: + specifier: ^2.0.0 + version: 2.8.3 devDependencies: '@types/node': specifier: ^22.0.0 version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages/lockfile-yarn: dependencies: @@ -198,13 +195,13 @@ importers: version: 22.19.15 '@vitest/coverage-v8': specifier: '*' - version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)) + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3)) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: '*' - version: 3.2.4(@types/node@22.19.15) + version: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) packages: @@ -1566,6 +1563,9 @@ packages: json-with-bigint@3.5.8: resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -2532,6 +2532,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -3153,7 +3158,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.15))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -3168,7 +3173,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.19.15) + vitest: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -3180,13 +3185,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.15) + vite: 7.3.1(@types/node@22.19.15)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -3909,6 +3914,8 @@ snapshots: json-with-bigint@3.5.8: {} + jsonc-parser@3.3.1: {} + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -4596,13 +4603,13 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@3.2.4(@types/node@22.19.15): + vite-node@3.2.4(@types/node@22.19.15)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.15) + vite: 7.3.1(@types/node@22.19.15)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -4617,17 +4624,17 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)): + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.1(@types/node@22.19.15) + vite: 7.3.1(@types/node@22.19.15)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@22.19.15): + vite@7.3.1(@types/node@22.19.15)(yaml@2.8.3): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.4) @@ -4638,12 +4645,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 fsevents: 2.3.3 + yaml: 2.8.3 - vitest@3.2.4(@types/node@22.19.15): + vitest@3.2.4(@types/node@22.19.15)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4661,8 +4669,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@22.19.15) - vite-node: 3.2.4(@types/node@22.19.15) + vite: 7.3.1(@types/node@22.19.15)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.15)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.15 @@ -4711,6 +4719,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.8.3: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} From a693cf01e6481fb337328d61c56446aa9077b851 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 08:39:48 +0300 Subject: [PATCH 2/7] test: add comprehensive parser tests and fix fixtures - Add missing parser adapter tests (format, lockfileNames, delegation) - Add empty lockfile handling tests for all parsers - Add multi-context extraction tests for npm, bun, pnpm - Add scoped package test for npm parser - Fix npm fixtures to include nested node_modules entries - Fix bun fixtures to include workspace declarations - Add deep transitive chain (4+ levels) E2E fixture and test - Add diamond dependency test - Add cycle handling tests (mutual, long, self-reference) - Update README with warning and development note --- README.md | 6 + .../fixtures/bun-transitive/after-bun.lock | 18 ++- .../fixtures/bun-transitive/before-bun.lock | 17 ++- .../npm-transitive/after-package-lock.json | 6 +- .../npm-transitive/before-package-lock.json | 6 +- .../pnpm-deep-chain/after-pnpm-lock.yaml | 31 +++++ .../pnpm-deep-chain/before-pnpm-lock.yaml | 31 +++++ .../pnpm-deep-chain/workspace/package.json | 7 ++ .../workspace/packages/pkg-base/package.json | 7 ++ .../workspace/packages/pkg-leaf/package.json | 7 ++ .../packages/pkg-middle/package.json | 7 ++ .../workspace/packages/pkg-top/package.json | 7 ++ .../transitive-dependency-detection.test.ts | 73 +++++++++++ .../resolve-affected-packages.test.ts | 118 ++++++++++++++++++ .../src/parse-bun-lockfile.test.ts | 43 ++++++- .../src/parse-npm-lockfile.test.ts | 93 +++++++++++++- .../src/parse-pnpm-lockfile.test.ts | 63 +++++++++- 17 files changed, 532 insertions(+), 8 deletions(-) create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/after-pnpm-lock.yaml create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/before-pnpm-lock.yaml create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-leaf/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-top/package.json diff --git a/README.md b/README.md index 29ffda4..9eb4a38 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![CI](https://github.com/split/lockfile-affected/actions/workflows/ci.yml/badge.svg)](https://github.com/split/lockfile-affected/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/lockfile-affected)](https://www.npmjs.com/package/lockfile-affected) +> **Warning:** This tool is still in early development. The API may change, and some features are incomplete or not yet fully tested. Use in production at your own risk. + Find which workspace packages are affected by lockfile changes. ## Installation @@ -42,3 +44,7 @@ For full Git and CI usage patterns, see the CLI guide: - [`@lockfile-affected/lockfile-npm`](packages/lockfile-npm) — package-lock.json parser - [`@lockfile-affected/lockfile-yarn`](packages/lockfile-yarn) — yarn.lock parser - [`@lockfile-affected/lockfile-bun`](packages/lockfile-bun) — bun.lock parser + +## Development + +This project was developed with AI assistance. diff --git a/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock b/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock index fd9c155..cfcdf68 100644 --- a/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock +++ b/packages/cli/e2e/fixtures/bun-transitive/after-bun.lock @@ -7,9 +7,25 @@ "dependencies": { "lodash": "^4.17.0" } + }, + "packages/pkg-base": { + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.23" + } + }, + "packages/pkg-middle": { + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } } }, "packages": { - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="] + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash@4.17.21": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w=="], + "lodash@4.17.23": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="] } } diff --git a/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock b/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock index 2cafb3b..b56a132 100644 --- a/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock +++ b/packages/cli/e2e/fixtures/bun-transitive/before-bun.lock @@ -7,9 +7,24 @@ "dependencies": { "lodash": "^4.17.0" } + }, + "packages/pkg-base": { + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.0" + } + }, + "packages/pkg-middle": { + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } } }, "packages": { - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w=="] + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w=="], + "lodash@4.17.21": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w=="] } } diff --git a/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json b/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json index 4353102..16546eb 100644 --- a/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json +++ b/packages/cli/e2e/fixtures/npm-transitive/after-package-lock.json @@ -12,7 +12,7 @@ }, "node_modules/lodash": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz" }, "packages/pkg-base": { "version": "1.0.0", @@ -20,6 +20,10 @@ "lodash": "^4.17.23" } }, + "packages/pkg-base/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz" + }, "packages/pkg-middle": { "version": "1.0.0", "dependencies": { diff --git a/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json b/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json index 0b60afe..867c52e 100644 --- a/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json +++ b/packages/cli/e2e/fixtures/npm-transitive/before-package-lock.json @@ -12,7 +12,7 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" }, "packages/pkg-base": { "version": "1.0.0", @@ -20,6 +20,10 @@ "lodash": "^4.17.0" } }, + "packages/pkg-base/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + }, "packages/pkg-middle": { "version": "1.0.0", "dependencies": { diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/after-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-deep-chain/after-pnpm-lock.yaml new file mode 100644 index 0000000..7652afe --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/after-pnpm-lock.yaml @@ -0,0 +1,31 @@ +lockfileVersion: '9.0' + +metadata: + packageManager: pnpm@9.15.9 + +importers: + .: {} + packages/pkg-top: + dependencies: + pkg-middle: + specifier: workspace:* + version: 1.0.0 + packages/pkg-middle: + dependencies: + pkg-base: + specifier: workspace:* + version: 1.0.0 + packages/pkg-base: + dependencies: + pkg-leaf: + specifier: workspace:* + version: 1.0.0 + packages/pkg-leaf: + dependencies: + express: + specifier: ^4.18.0 + version: 4.18.5 + +packages: + express@4.18.5: + resolution: { integrity: sha512-abc456 } diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/before-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-deep-chain/before-pnpm-lock.yaml new file mode 100644 index 0000000..07cceee --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/before-pnpm-lock.yaml @@ -0,0 +1,31 @@ +lockfileVersion: '9.0' + +metadata: + packageManager: pnpm@9.15.9 + +importers: + .: {} + packages/pkg-top: + dependencies: + pkg-middle: + specifier: workspace:* + version: 1.0.0 + packages/pkg-middle: + dependencies: + pkg-base: + specifier: workspace:* + version: 1.0.0 + packages/pkg-base: + dependencies: + pkg-leaf: + specifier: workspace:* + version: 1.0.0 + packages/pkg-leaf: + dependencies: + express: + specifier: ^4.18.0 + version: 4.18.0 + +packages: + express@4.18.0: + resolution: { integrity: sha512-sha256-abc123 } diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/package.json b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/package.json new file mode 100644 index 0000000..b20528c --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "deep-chain-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..c311f4d --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-leaf/package.json b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-leaf/package.json new file mode 100644 index 0000000..d534480 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-leaf/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..513baa9 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-top/package.json b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-top/package.json new file mode 100644 index 0000000..d832b7b --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-deep-chain/workspace/packages/pkg-top/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "workspace:*" + } +} diff --git a/packages/cli/e2e/transitive-dependency-detection.test.ts b/packages/cli/e2e/transitive-dependency-detection.test.ts index 5759a71..fb082b1 100644 --- a/packages/cli/e2e/transitive-dependency-detection.test.ts +++ b/packages/cli/e2e/transitive-dependency-detection.test.ts @@ -3,6 +3,17 @@ import { join } from 'path'; import { fileURLToPath } from 'url'; import { runAffectedCommand } from '../src/commands/run-affected-command.js'; import type { CliOptions } from '../src/options/cli-options.types.js'; +import { resolveAffectedPackages, ALL_DEPENDENCY_TYPES } from '@lockfile-affected/core'; +import type { LockfileDiff, WorkspaceGraph } from '@lockfile-affected/core'; + +function pkgWith(deps: { dependencies?: string[]; devDependencies?: string[] } = {}) { + return { + dependencies: new Set(deps.dependencies ?? []), + devDependencies: new Set(deps.devDependencies ?? []), + peerDependencies: new Set(), + optionalDependencies: new Set(), + }; +} const __filename = fileURLToPath(import.meta.url); const __dirname = join(__filename, '..'); @@ -160,3 +171,65 @@ describe('E2E: Per-importer detection (pnpm only)', () => { expect(affected).toContain('pkg-a'); }); }); + +describe('E2E: Deep transitive dependency chain (4+ levels)', () => { + const fixturesDir = join(__dirname, 'fixtures', 'pnpm-deep-chain'); + + it('marks all packages affected when external dep changes at 4-level depth', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, 'before-pnpm-lock.yaml'); + const afterLockfilePath = join(fixturesDir, 'after-pnpm-lock.yaml'); + + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format: 'pnpm', + deps: true, + dev: true, + peer: true, + optional: true, + }; + + const result = await runAffectedCommand(options); + const affected = JSON.parse(result); + + expect(affected).toContain('pkg-leaf'); + expect(affected).toContain('pkg-base'); + expect(affected).toContain('pkg-middle'); + expect(affected).toContain('pkg-top'); + }); +}); + +describe('E2E: Diamond dependency graph', () => { + it('marks all packages that depend on changed package via multiple paths', () => { + const workspace: WorkspaceGraph = new Map([ + ['app', { name: 'app', dependencyGroups: pkgWith({ dependencies: ['lib-a', 'lib-b'] }) }], + ['lib-a', { name: 'lib-a', dependencyGroups: pkgWith({ dependencies: ['lib-c'] }) }], + ['lib-b', { name: 'lib-b', dependencyGroups: pkgWith({ dependencies: ['lib-c'] }) }], + ['lib-c', { name: 'lib-c', dependencyGroups: pkgWith({ dependencies: ['shared-dep'] }) }], + ]); + const diff: LockfileDiff = { + addedContexts: new Map(), + removedContexts: new Map(), + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['shared-dep', { from: '1.0.0', to: '2.0.0' }]]), + }, + ], + ]), + }; + + const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + + expect(affected.has('lib-c')).toBe(true); + expect(affected.has('lib-a')).toBe(true); + expect(affected.has('lib-b')).toBe(true); + expect(affected.has('app')).toBe(true); + }); +}); diff --git a/packages/core/src/affected/resolve-affected-packages.test.ts b/packages/core/src/affected/resolve-affected-packages.test.ts index ac64498..a7a3dc5 100644 --- a/packages/core/src/affected/resolve-affected-packages.test.ts +++ b/packages/core/src/affected/resolve-affected-packages.test.ts @@ -317,4 +317,122 @@ describe('resolveAffectedPackages', () => { expect(affected.has('app')).toBe(true); expect(affected.size).toBe(4); }); + + it('handles mutual circular dependencies (a → b → a) with external dep', () => { + const workspace: WorkspaceGraph = new Map([ + [ + 'pkg-a', + { name: 'pkg-a', dependencyGroups: pkgWith({ dependencies: ['pkg-b', 'lodash'] }) }, + ], + ['pkg-b', { name: 'pkg-b', dependencyGroups: pkgWith({ dependencies: ['pkg-a'] }) }], + ]); + const diff: LockfileDiff = { + ...emptyDiff, + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '1.0.0', to: '2.0.0' }]]), + }, + ], + ]), + }; + + const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + + expect(affected.size).toBe(2); + expect(affected.has('pkg-a')).toBe(true); + expect(affected.has('pkg-b')).toBe(true); + }); + + it('handles long circular dependencies (a → b → c → a) with external dep', () => { + const workspace: WorkspaceGraph = new Map([ + ['pkg-a', { name: 'pkg-a', dependencyGroups: pkgWith({ dependencies: ['pkg-b'] }) }], + ['pkg-b', { name: 'pkg-b', dependencyGroups: pkgWith({ dependencies: ['pkg-c'] }) }], + [ + 'pkg-c', + { name: 'pkg-c', dependencyGroups: pkgWith({ dependencies: ['pkg-a', 'lodash'] }) }, + ], + ]); + const diff: LockfileDiff = { + ...emptyDiff, + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '1.0.0', to: '2.0.0' }]]), + }, + ], + ]), + }; + + const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + + expect(affected.size).toBe(3); + expect(affected.has('pkg-a')).toBe(true); + expect(affected.has('pkg-b')).toBe(true); + expect(affected.has('pkg-c')).toBe(true); + }); + + it('handles self-referencing packages with external dep', () => { + const workspace: WorkspaceGraph = new Map([ + [ + 'pkg-a', + { name: 'pkg-a', dependencyGroups: pkgWith({ dependencies: ['pkg-a', 'lodash'] }) }, + ], + ]); + const diff: LockfileDiff = { + ...emptyDiff, + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['lodash', { from: '1.0.0', to: '2.0.0' }]]), + }, + ], + ]), + }; + + const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + + expect(affected.size).toBe(1); + expect(affected.has('pkg-a')).toBe(true); + }); + + it('handles cycle with external dependency change at the cycle boundary', () => { + const workspace: WorkspaceGraph = new Map([ + [ + 'pkg-a', + { name: 'pkg-a', dependencyGroups: pkgWith({ dependencies: ['pkg-b', 'express'] }) }, + ], + ['pkg-b', { name: 'pkg-b', dependencyGroups: pkgWith({ dependencies: ['pkg-c'] }) }], + ['pkg-c', { name: 'pkg-c', dependencyGroups: pkgWith({ dependencies: ['pkg-a'] }) }], + ]); + const diff: LockfileDiff = { + ...emptyDiff, + changed: new Map([ + [ + '.', + { + added: new Map(), + removed: new Map(), + changed: new Map([['express', { from: '4.18.0', to: '4.19.0' }]]), + }, + ], + ]), + }; + + const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + + expect(affected.size).toBe(3); + expect(affected.has('pkg-a')).toBe(true); + expect(affected.has('pkg-b')).toBe(true); + expect(affected.has('pkg-c')).toBe(true); + }); }); diff --git a/packages/lockfile-bun/src/parse-bun-lockfile.test.ts b/packages/lockfile-bun/src/parse-bun-lockfile.test.ts index 7c7bceb..58d4273 100644 --- a/packages/lockfile-bun/src/parse-bun-lockfile.test.ts +++ b/packages/lockfile-bun/src/parse-bun-lockfile.test.ts @@ -63,12 +63,14 @@ describe('parseBunLockfile', () => { expect(rootPackages?.get('react')).toBe('18.3.0'); }); - it('resolves direct workspace dependencies', async () => { + it('only includes direct dependencies (not transitive)', async () => { const snapshot = await parseBunLockfile(BUN_LOCK_FIXTURE); - const rootPackages = snapshot.get('.'); + expect(rootPackages?.get('lodash')).toBe('4.17.21'); expect(rootPackages?.get('react')).toBe('18.3.0'); + expect(rootPackages?.get('loose-envify')).toBeUndefined(); + expect(rootPackages?.get('js-tokens')).toBeUndefined(); }); it('returns a LockfileSnapshot (ReadonlyMap of ReadonlyMaps)', async () => { @@ -77,6 +79,43 @@ describe('parseBunLockfile', () => { expect(snapshot).toBeInstanceOf(Map); expect(snapshot.get('.')).toBeInstanceOf(Map); }); + + it('returns empty snapshot for empty workspaces', async () => { + const emptyLock = JSON.stringify({ + lockfileVersion: 1, + packages: {}, + }); + + const snapshot = await parseBunLockfile(emptyLock); + expect(snapshot.size).toBe(0); + }); + + it('extracts workspace packages into separate contexts', async () => { + const multiWorkspaceLock = JSON.stringify({ + lockfileVersion: 1, + workspaces: { + '': { + name: 'root', + dependencies: { lodash: '^4.17.0' }, + }, + 'packages/pkg-a': { + dependencies: { react: '^18.0.0' }, + }, + }, + packages: { + lodash: ['lodash@4.17.21', '', {}], + react: ['react@18.2.0', '', {}], + }, + }); + + const snapshot = await parseBunLockfile(multiWorkspaceLock); + + expect(snapshot.has('.')).toBe(true); + expect(snapshot.has('packages/pkg-a')).toBe(true); + + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('packages/pkg-a')?.get('react')).toBe('18.2.0'); + }); }); describe('bunLockfileParser', () => { diff --git a/packages/lockfile-npm/src/parse-npm-lockfile.test.ts b/packages/lockfile-npm/src/parse-npm-lockfile.test.ts index bd7a158..49e0a43 100644 --- a/packages/lockfile-npm/src/parse-npm-lockfile.test.ts +++ b/packages/lockfile-npm/src/parse-npm-lockfile.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseNpmLockfile } from './parse-npm-lockfile.js'; +import { npmLockfileParser, parseNpmLockfile } from './parse-npm-lockfile.js'; const NPM_LOCK_V3 = JSON.stringify({ name: 'my-app', @@ -45,4 +45,95 @@ describe('parseNpmLockfile', () => { expect(snapshot).toBeInstanceOf(Map); expect(snapshot.get('.')).toBeInstanceOf(Map); }); + + it('returns empty snapshot for empty packages', async () => { + const emptyLock = JSON.stringify({ + name: 'empty-project', + version: '1.0.0', + lockfileVersion: 3, + packages: {}, + }); + + const snapshot = await parseNpmLockfile(emptyLock); + expect(snapshot.size).toBe(0); + }); + + it('extracts workspace packages into separate contexts', async () => { + const workspaceLock = JSON.stringify({ + name: 'my-monorepo', + version: '1.0.0', + lockfileVersion: 3, + packages: { + '': { + name: 'my-monorepo', + dependencies: { + lodash: '^4.17.21', + }, + }, + 'node_modules/lodash': { + version: '4.17.21', + resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz', + }, + 'packages/pkg-a': { + version: '1.0.0', + dependencies: { + react: '^18.2.0', + }, + }, + 'packages/pkg-a/node_modules/react': { + version: '18.2.0', + resolved: 'https://registry.npmjs.org/react/-/react-18.2.0.tgz', + }, + }, + }); + + const snapshot = await parseNpmLockfile(workspaceLock); + + expect(snapshot.has('.')).toBe(true); + expect(snapshot.has('packages/pkg-a')).toBe(true); + + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('packages/pkg-a')?.get('react')).toBe('18.2.0'); + }); + + it('handles scoped packages (@org/name)', async () => { + const scopedLock = JSON.stringify({ + name: 'my-app', + version: '1.0.0', + lockfileVersion: 3, + packages: { + '': { + name: 'my-app', + dependencies: { + '@babel/core': '^7.23.0', + }, + }, + 'node_modules/@babel/core': { + version: '7.23.5', + resolved: 'https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz', + }, + }, + }); + + const snapshot = await parseNpmLockfile(scopedLock); + const rootPackages = snapshot.get('.'); + + expect(rootPackages?.has('@babel/core')).toBe(true); + expect(rootPackages?.get('@babel/core')).toBe('7.23.5'); + }); +}); + +describe('npmLockfileParser', () => { + it('has format "npm"', () => { + expect(npmLockfileParser.format).toBe('npm'); + }); + + it('handles package-lock.json filename', () => { + expect(npmLockfileParser.lockfileNames).toContain('package-lock.json'); + }); + + it('parse delegates to parseNpmLockfile', async () => { + const snapshot = await npmLockfileParser.parse(NPM_LOCK_V3); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + }); }); diff --git a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts index 2a4d22d..d9bd6c6 100644 --- a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts +++ b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parsePnpmLockfile } from './parse-pnpm-lockfile.js'; +import { parsePnpmLockfile, pnpmLockfileParser } from './parse-pnpm-lockfile.js'; const PNPM_LOCK_V9 = ` lockfileVersion: '9.0' @@ -63,6 +63,67 @@ describe('parsePnpmLockfile', () => { expect(snapshot.get('.')).toBeInstanceOf(Map); }); + it('returns empty snapshot for empty importers', async () => { + const emptyLock = ` +lockfileVersion: '9.0' + +importers: {} +`.trim(); + + const snapshot = await parsePnpmLockfile(emptyLock); + expect(snapshot.size).toBe(0); + }); + + it('includes all dependency types (dev, optional, peer)', async () => { + const allTypesLock = ` +lockfileVersion: '9.0' + +importers: + .: + dependencies: + lodash: + specifier: ^4.17.0 + version: 4.17.21 + devDependencies: + vitest: + specifier: ^1.0.0 + version: 1.5.0 + optionalDependencies: + fsevents: + specifier: ^2.3.0 + version: 2.3.3 + peerDependencies: + react: + specifier: ^18.0.0 + version: 18.2.0 +`.trim(); + + const snapshot = await parsePnpmLockfile(allTypesLock); + const rootPackages = snapshot.get('.'); + + expect(rootPackages?.get('lodash')).toBe('4.17.21'); + expect(rootPackages?.get('vitest')).toBe('1.5.0'); + expect(rootPackages?.get('fsevents')).toBe('2.3.3'); + expect(rootPackages?.get('react')).toBe('18.2.0'); + }); +}); + +describe('pnpmLockfileParser', () => { + it('has format "pnpm"', () => { + expect(pnpmLockfileParser.format).toBe('pnpm'); + }); + + it('handles pnpm-lock.yaml filename', () => { + expect(pnpmLockfileParser.lockfileNames).toContain('pnpm-lock.yaml'); + }); + + it('parse delegates to parsePnpmLockfile', async () => { + const snapshot = await pnpmLockfileParser.parse(PNPM_LOCK_V9); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + }); +}); + +describe('pnpm multi-importer snapshots', () => { it('parses multiple importers into separate contexts', async () => { const multiImporterLock = ` lockfileVersion: '9.0' From 931330d8a943305a2dfaaa1a919a078e26365385 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 08:40:43 +0300 Subject: [PATCH 3/7] fix: remove unnecessary async from parser functions ESLint complains about async functions without await. These parsers use synchronous JSON/YAML parsing, so returning Promise.resolve() is cleaner. --- packages/lockfile-bun/src/parse-bun-lockfile.ts | 4 ++-- packages/lockfile-npm/src/parse-npm-lockfile.ts | 4 ++-- packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lockfile-bun/src/parse-bun-lockfile.ts b/packages/lockfile-bun/src/parse-bun-lockfile.ts index 0437d0b..ddfd9b4 100644 --- a/packages/lockfile-bun/src/parse-bun-lockfile.ts +++ b/packages/lockfile-bun/src/parse-bun-lockfile.ts @@ -6,9 +6,9 @@ interface BunLockfile { packages: Record; } -export async function parseBunLockfile(content: string): Promise { +export function parseBunLockfile(content: string): Promise { const parsed = jsoncParse(content) as BunLockfile; - return toSnapshot(parsed); + return Promise.resolve(toSnapshot(parsed)); } function toSnapshot(lockfile: BunLockfile): LockfileSnapshot { diff --git a/packages/lockfile-npm/src/parse-npm-lockfile.ts b/packages/lockfile-npm/src/parse-npm-lockfile.ts index d6a6f7d..e7d5326 100644 --- a/packages/lockfile-npm/src/parse-npm-lockfile.ts +++ b/packages/lockfile-npm/src/parse-npm-lockfile.ts @@ -10,9 +10,9 @@ interface NpmPackageEntry { dependencies?: Record; } -export async function parseNpmLockfile(content: string): Promise { +export function parseNpmLockfile(content: string): Promise { const lockfile = JSON.parse(content) as NpmPackageLock; - return toSnapshot(lockfile); + return Promise.resolve(toSnapshot(lockfile)); } function toSnapshot(lockfile: NpmPackageLock): LockfileSnapshot { diff --git a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts index 2bd39bf..c6bfa27 100644 --- a/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts +++ b/packages/lockfile-pnpm/src/parse-pnpm-lockfile.ts @@ -12,9 +12,9 @@ interface PnpmLockfile { importers: Record; } -export async function parsePnpmLockfile(content: string): Promise { +export function parsePnpmLockfile(content: string): Promise { const parsed = YAML.parse(content) as PnpmLockfile; - return toSnapshot(parsed); + return Promise.resolve(toSnapshot(parsed)); } function stripPeerSuffix(version: string): string { From 93468ab35fed38af5662bf69c60b609f784f8c7e Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 09:18:21 +0300 Subject: [PATCH 4/7] feat(yarn): migrate to Yarn Berry format, drop v1 support - Rewrite Yarn parser to use YAML directly instead of lockparse - Convert yarn-transitive fixtures to Yarn Berry format - Drop support for Yarn v1 lockfiles (incompatible with YAML parsing) - Remove lockparse dependency, use yaml package instead - Add yarn-berry-research fixtures for format exploration --- .../yarn-berry-research/after-yarn.lock | 34 +++++++ .../workspace/package.json | 8 ++ .../workspace/packages/pkg-a/package.json | 11 +++ .../workspace/packages/pkg-b/package.json | 7 ++ .../yarn-classic-mode.lock | 41 ++++++++ .../fixtures/yarn-berry-research/yarn.lock | 34 +++++++ .../fixtures/yarn-transitive/after-yarn.lock | 30 +++++- .../fixtures/yarn-transitive/before-yarn.lock | 30 +++++- packages/lockfile-yarn/package.json | 2 +- .../src/parse-yarn-lockfile.test.ts | 99 +++++++++---------- .../lockfile-yarn/src/parse-yarn-lockfile.ts | 93 +++++++++++++++-- 11 files changed, 314 insertions(+), 75 deletions(-) create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/after-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-a/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-b/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/yarn-classic-mode.lock create mode 100644 packages/cli/e2e/fixtures/yarn-berry-research/yarn.lock diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/after-yarn.lock b/packages/cli/e2e/fixtures/yarn-berry-research/after-yarn.lock new file mode 100644 index 0000000..98249fa --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/after-yarn.lock @@ -0,0 +1,34 @@ +# yarn.lock - Yarn Berry (v4) format +# After: lodash upgraded to 4.17.25 + +__metadata: + version: 6 + cacheKey: 8 + +"lodash@npm:^4.17.20": + version: 4.17.25 + resolution: "lodash@npm:4.17.25" + checksum: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + languageName: node + linkType: hard + +"lodash@npm:4.17.15": + version: 4.17.25 + resolution: "lodash@npm:4.17.25" + checksum: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + languageName: node + linkType: hard + +"pkg-a@workspace:packages/pkg-a": + version: 1.0.0 + resolution: "pkg-a@workspace:packages/pkg-a" + checksum: sha512-pkg-a + languageName: node + linkType: soft + +"pkg-b@workspace:packages/pkg-b": + version: 1.0.0 + resolution: "pkg-b@workspace:packages/pkg-b" + checksum: sha512-pkg-b + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/workspace/package.json b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/package.json new file mode 100644 index 0000000..ffe8a6f --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "yarn-berry-research", + "private": true, + "workspaces": [ + "packages/*" + ], + "packageManager": "yarn@4.0.0" +} diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-a/package.json b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-a/package.json new file mode 100644 index 0000000..e29ab59 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-a/package.json @@ -0,0 +1,11 @@ +{ + "name": "yarn-berry-research", + "private": true, + "workspaces": [ + "packages/*" + ], + "packageManager": "yarn@4.0.0", + "dependencies": { + "lodash": "^4.17.20" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-b/package.json b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-b/package.json new file mode 100644 index 0000000..76b79c0 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/workspace/packages/pkg-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-b", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.15" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/yarn-classic-mode.lock b/packages/cli/e2e/fixtures/yarn-berry-research/yarn-classic-mode.lock new file mode 100644 index 0000000..80f37ce --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/yarn-classic-mode.lock @@ -0,0 +1,41 @@ +# yarn.lock - Yarn Berry with nmMode: classic +# Classic mode stores packages with node_modules paths + +__metadata: + version: 6 + cacheKey: 8 + +"lodash@npm:^4.17.20": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== + languageName: node + linkType: hard + +"lodash@npm:4.17.15": + version: 4.17.15 + resolution: "lodash@npm:4.17.15" + checksum: sha512-abc123 + languageName: node + linkType: hard + +"node-modules/lodash@npm:4.17.21": + version: 4.17.21 + resolution: "node-modules/lodash@npm:4.17.21" + checksum: sha512-def456 + languageName: node + linkType: hard + +"packages/pkg-a/node-modules/lodash@npm:^4.17.20": + version: 4.17.21 + resolution: "packages/pkg-a/node-modules/lodash@npm:4.17.21" + checksum: sha512-pkg-a-lodash + languageName: node + linkType: hard + +"packages/pkg-b/node-modules/lodash@npm:4.17.15": + version: 4.17.15 + resolution: "packages/pkg-b/node-modules/lodash@npm:4.17.15" + checksum: sha512-pkg-b-lodash + languageName: node + linkType: hard diff --git a/packages/cli/e2e/fixtures/yarn-berry-research/yarn.lock b/packages/cli/e2e/fixtures/yarn-berry-research/yarn.lock new file mode 100644 index 0000000..39d1b36 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-berry-research/yarn.lock @@ -0,0 +1,34 @@ +# yarn.lock - Yarn Berry (v4) format +# This demonstrates Yarn Berry's deduplication behavior + +__metadata: + version: 6 + cacheKey: 8 + +"lodash@npm:^4.17.20": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== + languageName: node + linkType: hard + +"lodash@npm:4.17.15": + version: 4.17.15 + resolution: "lodash@npm:4.17.15" + checksum: sha512-abc123 + languageName: node + linkType: hard + +"pkg-a@workspace:packages/pkg-a": + version: 1.0.0 + resolution: "pkg-a@workspace:packages/pkg-a" + checksum: sha512-pkg-a + languageName: node + linkType: soft + +"pkg-b@workspace:packages/pkg-b": + version: 1.0.0 + resolution: "pkg-b@workspace:packages/pkg-b" + checksum: sha512-pkg-b + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock b/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock index cd899ae..97d3add 100644 --- a/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock +++ b/packages/cli/e2e/fixtures/yarn-transitive/after-yarn.lock @@ -1,6 +1,26 @@ -# yarn lockfile v1 +# yarn.lock - Yarn Berry (v4) format -lodash@^4.17.0: - version "4.17.23" - resolved "https://registry.Yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" - integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== +__metadata: + version: 6 + cacheKey: 8 + +"lodash@npm:^4.17.20": + version: 4.17.23 + resolution: "lodash@npm:4.17.23" + checksum: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== + languageName: node + linkType: hard + +"pkg-base@workspace:packages/pkg-base": + version: 1.0.0 + resolution: "pkg-base@workspace:packages/pkg-base" + checksum: sha512-pkg-base-1-0-0 + languageName: node + linkType: soft + +"pkg-middle@workspace:packages/pkg-middle": + version: 1.0.0 + resolution: "pkg-middle@workspace:packages/pkg-middle" + checksum: sha512-pkg-middle-1-0-0 + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock b/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock index fbf0052..a51f8d4 100644 --- a/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock +++ b/packages/cli/e2e/fixtures/yarn-transitive/before-yarn.lock @@ -1,6 +1,26 @@ -# yarn lockfile v1 +# yarn.lock - Yarn Berry (v4) format -lodash@^4.17.0: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== +__metadata: + version: 6 + cacheKey: 8 + +"lodash@npm:^4.17.20": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w== + languageName: node + linkType: hard + +"pkg-base@workspace:packages/pkg-base": + version: 1.0.0 + resolution: "pkg-base@workspace:packages/pkg-base" + checksum: sha512-pkg-base-1-0-0 + languageName: node + linkType: soft + +"pkg-middle@workspace:packages/pkg-middle": + version: 1.0.0 + resolution: "pkg-middle@workspace:packages/pkg-middle" + checksum: sha512-pkg-middle-1-0-0 + languageName: node + linkType: soft diff --git a/packages/lockfile-yarn/package.json b/packages/lockfile-yarn/package.json index 3af3fd8..ab536cb 100644 --- a/packages/lockfile-yarn/package.json +++ b/packages/lockfile-yarn/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@lockfile-affected/core": "workspace:*", - "lockparse": "^0.5.0" + "yaml": "^2.0.0" }, "devDependencies": { "vitest": "*", diff --git a/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts b/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts index d99e364..ae2f0c3 100644 --- a/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts +++ b/packages/lockfile-yarn/src/parse-yarn-lockfile.test.ts @@ -1,87 +1,76 @@ import { describe, expect, it } from 'vitest'; import { parseYarnLockfile, yarnLockfileParser } from './parse-yarn-lockfile.js'; -const CLASSIC_FIXTURE = `\ -# yarn lockfile v1 - -lodash@^4.17.20, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.Yarnpkg.com/lodash/-/lodash-4.17.21.tgz" - integrity sha512-512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZbet2yk9dzQ1uFR5/0w==" - integrity sha512-512-abc - -react@^18.2.0: - version "18.2.0" - resolved "https://registry.Yarnpkg.com/react/-/react-18.2.0.tgz" - integrity sha512-512-def - dependencies: - loose-envify "^1.1.0" -`; - const BERRY_FIXTURE = `\ __metadata: version: 6 cacheKey: 8 -"lodash@npm:^4.17.20, lodash@npm:^4.17.21": +"lodash@npm:^4.17.20": version: 4.17.21 resolution: "lodash@npm:4.17.21" - checksum: abc + checksum: abc123 languageName: node linkType: hard "react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" - checksum: def + checksum: def456 languageName: node linkType: hard `; -describe('parseYarnLockfile', () => { - describe('classic (v1)', () => { - it('parses packages into name → version entries in root context', async () => { - const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); +const BERRY_WORKSPACES_FIXTURE = `\ +__metadata: + version: 6 + cacheKey: 8 - expect(snapshot.has('.')).toBe(true); - expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); - expect(snapshot.get('.')?.get('react')).toBe('18.2.0'); - }); +"lodash@npm:^4.17.20": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: abc123 + languageName: node + linkType: hard - it('deduplicates multi-range entries to a single version', async () => { - const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); +"packages/pkg-a/node-modules/lodash@npm:^4.17.20": + version: 4.17.25 + resolution: "lodash@npm:4.17.25" + checksum: def456 + languageName: node + linkType: hard - const rootPackages = snapshot.get('.'); - expect(rootPackages?.size).toBe(2); - }); +"packages/pkg-b/node-modules/lodash@npm:4.17.15": + version: 4.17.15 + resolution: "lodash@npm:4.17.15" + checksum: ghi789 + languageName: node + linkType: hard +`; - it('returns packages in root context', async () => { - const snapshot = await parseYarnLockfile(CLASSIC_FIXTURE); +describe('parseYarnLockfile', () => { + it('parses packages into name → version entries in root context', async () => { + const snapshot = await parseYarnLockfile(BERRY_FIXTURE); - const rootPackages = snapshot.get('.'); - expect([...(rootPackages?.keys() ?? [])].sort()).toEqual(['lodash', 'react']); - }); + expect(snapshot.has('.')).toBe(true); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('.')?.get('react')).toBe('18.2.0'); }); - describe('berry (v2+)', () => { - it('parses packages into name → version entries in root context', async () => { - const snapshot = await parseYarnLockfile(BERRY_FIXTURE); + it('handles workspaces with separate node-modules paths', async () => { + const snapshot = await parseYarnLockfile(BERRY_WORKSPACES_FIXTURE); - expect(snapshot.has('.')).toBe(true); - expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); - expect(snapshot.get('.')?.get('react')).toBe('18.2.0'); - }); + expect(snapshot.has('.')).toBe(true); + expect(snapshot.has('packages/pkg-a')).toBe(true); + expect(snapshot.has('packages/pkg-b')).toBe(true); - it('deduplicates multi-range entries to a single version', async () => { - const snapshot = await parseYarnLockfile(BERRY_FIXTURE); - - const rootPackages = snapshot.get('.'); - expect(rootPackages?.size).toBe(2); - }); + expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); + expect(snapshot.get('packages/pkg-a')?.get('lodash')).toBe('4.17.25'); + expect(snapshot.get('packages/pkg-b')?.get('lodash')).toBe('4.17.15'); }); - it('returns an empty snapshot for an empty lockfile', async () => { - const snapshot = await parseYarnLockfile('# yarn lockfile v1\n'); + it('returns empty snapshot for empty content', async () => { + const snapshot = await parseYarnLockfile('__metadata:\n version: 6\n'); expect(snapshot.size).toBe(0); }); @@ -92,12 +81,12 @@ describe('yarnLockfileParser', () => { expect(yarnLockfileParser.format).toBe('yarn'); }); - it('lists both classic and berry filenames', () => { + it('handles yarn.lock filename', () => { expect(yarnLockfileParser.lockfileNames).toContain('yarn.lock'); }); it('parse delegates to parseYarnLockfile', async () => { - const snapshot = await yarnLockfileParser.parse(CLASSIC_FIXTURE); + const snapshot = await yarnLockfileParser.parse(BERRY_FIXTURE); expect(snapshot.get('.')?.get('lodash')).toBe('4.17.21'); }); diff --git a/packages/lockfile-yarn/src/parse-yarn-lockfile.ts b/packages/lockfile-yarn/src/parse-yarn-lockfile.ts index bed8a52..ba4ce85 100644 --- a/packages/lockfile-yarn/src/parse-yarn-lockfile.ts +++ b/packages/lockfile-yarn/src/parse-yarn-lockfile.ts @@ -1,19 +1,51 @@ import type { LockfileParser, LockfileSnapshot } from '@lockfile-affected/core'; -import { parse, type ParsedDependency } from 'lockparse'; +import YAML from 'yaml'; -export async function parseYarnLockfile(content: string): Promise { - const parsed = await parse(content, 'yarn'); - return toSnapshot(parsed.packages); +export function parseYarnLockfile(content: string): Promise { + const parsed = YAML.parse(content) as Record; + return Promise.resolve(toSnapshot(parsed)); } -function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { - const snapshot = new Map>(); +interface YarnBerryEntry { + version?: string; + resolution?: string; + dependencies?: Record; + peerDependencies?: Record; +} +function toSnapshot(parsed: Record): LockfileSnapshot { + const snapshot = new Map>(); const rootPackages = new Map(); + const workspacePackages = new Map>(); + + for (const [key, value] of Object.entries(parsed)) { + if (key === '__metadata') continue; + if (!value || typeof value !== 'object') continue; + + const entry = value as YarnBerryEntry; + if (!entry.version) continue; + + const parsed_ = parseBerryKey(key); + if (parsed_) { + if (parsed_.context === '.') { + if (!rootPackages.has(parsed_.name)) { + rootPackages.set(parsed_.name, entry.version); + } + } else { + if (!workspacePackages.has(parsed_.context)) { + workspacePackages.set(parsed_.context, new Map()); + } + const ctxPackages = workspacePackages.get(parsed_.context)!; + if (!ctxPackages.has(parsed_.name)) { + ctxPackages.set(parsed_.name, entry.version); + } + } + } + } - for (const pkg of packages) { - if (pkg.name && pkg.version) { - rootPackages.set(pkg.name, pkg.version); + for (const [context, packages] of workspacePackages) { + if (packages.size > 0) { + snapshot.set(context, packages); } } @@ -24,6 +56,49 @@ function toSnapshot(packages: readonly ParsedDependency[]): LockfileSnapshot { return snapshot; } +function parseBerryKey(key: string): { context: string; name: string } { + const cleanKey = key; + + const nmMatch = cleanKey.match(/^(.+?)\/node-?modules\/(.+)$/); + if (nmMatch) { + const workspacePath = nmMatch[1]; + const packagePath = nmMatch[2]; + + if ( + workspacePath && + packagePath && + !workspacePath.includes('node_modules') && + workspacePath !== 'node-modules' + ) { + return { + context: workspacePath, + name: extractPackageName(packagePath), + }; + } + } + + return { context: '.', name: extractPackageName(cleanKey) }; +} + +function extractPackageName(key: string): string { + let cleanKey = key; + + const npmMatch = key.match(/^(.+?)@npm:/); + if (npmMatch?.[1]) { + cleanKey = npmMatch[1]; + } + + if (cleanKey.startsWith('@')) { + const parts = cleanKey.split('/'); + if (parts.length >= 2) { + return parts.slice(0, 2).join('/'); + } + } + + const slashIndex = cleanKey.indexOf('/'); + return slashIndex === -1 ? cleanKey : cleanKey.slice(0, slashIndex); +} + export const yarnLockfileParser: LockfileParser = { format: 'yarn', lockfileNames: ['yarn.lock'], From a1b3ce6e62b3d783837d51e469dcae32ecb575d6 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 09:21:16 +0300 Subject: [PATCH 5/7] docs: update README to reflect Yarn Berry only support --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9eb4a38..c806b4d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Pass the before and after lockfile snapshots as arguments: lockfile-affected pnpm-lock.yaml.old pnpm-lock.yaml ``` -Works with pnpm, npm, yarn (classic v1 and berry v2+), and Bun text lockfiles (`bun.lock`). +Works with pnpm, npm, Yarn Berry (v2+), and Bun text lockfiles (`bun.lock`). For full Git and CI usage patterns, see the CLI guide: [`packages/cli/README.md`](packages/cli/README.md). @@ -42,7 +42,7 @@ For full Git and CI usage patterns, see the CLI guide: - [`@lockfile-affected/core`](packages/core) — pure diff and resolution engine - [`@lockfile-affected/lockfile-pnpm`](packages/lockfile-pnpm) — pnpm-lock.yaml parser - [`@lockfile-affected/lockfile-npm`](packages/lockfile-npm) — package-lock.json parser -- [`@lockfile-affected/lockfile-yarn`](packages/lockfile-yarn) — yarn.lock parser +- [`@lockfile-affected/lockfile-yarn`](packages/lockfile-yarn) — yarn.lock parser (Yarn Berry v2+) - [`@lockfile-affected/lockfile-bun`](packages/lockfile-bun) — bun.lock parser ## Development From c6b0a6205073a35aa03f5fbc2d78f8017c9f7a49 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 09:25:55 +0300 Subject: [PATCH 6/7] test: add deep-chain and diamond fixtures for all package managers - Create deep-chain fixtures (4-level transitive dependency) for npm, yarn, bun - Create diamond dependency fixtures for all 4 package managers (pnpm, npm, yarn, bun) - Parameterize E2E tests to run for all formats - Keep per-importer detection test pnpm-only (pnpm-specific feature) --- .../fixtures/bun-deep-chain/after-bun.lock | 44 +++++++ .../fixtures/bun-deep-chain/before-bun.lock | 44 +++++++ .../bun-deep-chain/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../workspace/packages/pkg-leaf/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../workspace/packages/pkg-top/package.json | 7 + .../e2e/fixtures/bun-diamond/after-bun.lock | 42 ++++++ .../e2e/fixtures/bun-diamond/before-bun.lock | 42 ++++++ .../bun-diamond/workspace/package.json | 7 + .../workspace/packages/app/package.json | 8 ++ .../workspace/packages/lib-a/package.json | 7 + .../workspace/packages/lib-b/package.json | 7 + .../workspace/packages/lib-c/package.json | 7 + .../npm-deep-chain/after-package-lock.json | 46 +++++++ .../npm-deep-chain/before-package-lock.json | 46 +++++++ .../npm-deep-chain/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../workspace/packages/pkg-leaf/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../workspace/packages/pkg-top/package.json | 7 + .../npm-diamond/after-package-lock.json | 44 +++++++ .../npm-diamond/before-package-lock.json | 44 +++++++ .../npm-diamond/workspace/package.json | 7 + .../workspace/packages/app/package.json | 8 ++ .../workspace/packages/lib-a/package.json | 7 + .../workspace/packages/lib-b/package.json | 7 + .../workspace/packages/lib-c/package.json | 7 + .../pnpm-diamond/after-pnpm-lock.yaml | 34 +++++ .../pnpm-diamond/before-pnpm-lock.yaml | 34 +++++ .../pnpm-diamond/workspace/package.json | 7 + .../workspace/packages/app/package.json | 8 ++ .../workspace/packages/lib-a/package.json | 7 + .../workspace/packages/lib-b/package.json | 7 + .../workspace/packages/lib-c/package.json | 7 + .../fixtures/yarn-deep-chain/after-yarn.lock | 40 ++++++ .../fixtures/yarn-deep-chain/before-yarn.lock | 40 ++++++ .../yarn-deep-chain/workspace/package.json | 7 + .../workspace/packages/pkg-base/package.json | 7 + .../workspace/packages/pkg-leaf/package.json | 7 + .../packages/pkg-middle/package.json | 7 + .../workspace/packages/pkg-top/package.json | 7 + .../e2e/fixtures/yarn-diamond/after-yarn.lock | 40 ++++++ .../fixtures/yarn-diamond/before-yarn.lock | 40 ++++++ .../yarn-diamond/workspace/package.json | 7 + .../workspace/packages/app/package.json | 8 ++ .../workspace/packages/lib-a/package.json | 7 + .../workspace/packages/lib-b/package.json | 7 + .../workspace/packages/lib-c/package.json | 7 + .../transitive-dependency-detection.test.ts | 123 +++++++++--------- 50 files changed, 887 insertions(+), 65 deletions(-) create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/after-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/before-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-leaf/package.json create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-top/package.json create mode 100644 packages/cli/e2e/fixtures/bun-diamond/after-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-diamond/before-bun.lock create mode 100644 packages/cli/e2e/fixtures/bun-diamond/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/bun-diamond/workspace/packages/app/package.json create mode 100644 packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-a/package.json create mode 100644 packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-b/package.json create mode 100644 packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-c/package.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/after-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/before-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-leaf/package.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-top/package.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/after-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/before-package-lock.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/workspace/packages/app/package.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-a/package.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-b/package.json create mode 100644 packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-c/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/after-pnpm-lock.yaml create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/before-pnpm-lock.yaml create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/app/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-a/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-b/package.json create mode 100644 packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-c/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/after-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/before-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-base/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-leaf/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-middle/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-top/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/after-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/before-yarn.lock create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/workspace/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/app/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-a/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-b/package.json create mode 100644 packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-c/package.json diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/after-bun.lock b/packages/cli/e2e/fixtures/bun-deep-chain/after-bun.lock new file mode 100644 index 0000000..4aedb94 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/after-bun.lock @@ -0,0 +1,44 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "deep-chain-test", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-leaf": { + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-base": { + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "workspace:*" + } + }, + "packages/pkg-middle": { + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "workspace:*" + } + }, + "packages/pkg-top": { + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "workspace:*" + } + } + }, + "packages": { + "express": ["express@4.18.5", "", {}, "sha512-express-4-18-5"], + "express@4.18.5": ["express@4.18.5", "", {}, "sha512-express-4-18-5"] + } +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/before-bun.lock b/packages/cli/e2e/fixtures/bun-deep-chain/before-bun.lock new file mode 100644 index 0000000..ef0d4a2 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/before-bun.lock @@ -0,0 +1,44 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "deep-chain-test", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-leaf": { + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-base": { + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "workspace:*" + } + }, + "packages/pkg-middle": { + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "workspace:*" + } + }, + "packages/pkg-top": { + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "workspace:*" + } + } + }, + "packages": { + "express": ["express@4.18.0", "", {}, "sha512-express-4-18-0"], + "express@4.18.0": ["express@4.18.0", "", {}, "sha512-express-4-18-0"] + } +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/workspace/package.json b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/package.json new file mode 100644 index 0000000..b20528c --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "deep-chain-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..c311f4d --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-leaf/package.json b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-leaf/package.json new file mode 100644 index 0000000..d534480 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-leaf/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..513baa9 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-top/package.json b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-top/package.json new file mode 100644 index 0000000..d832b7b --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-deep-chain/workspace/packages/pkg-top/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/after-bun.lock b/packages/cli/e2e/fixtures/bun-diamond/after-bun.lock new file mode 100644 index 0000000..811248d --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/after-bun.lock @@ -0,0 +1,42 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "diamond-test" + }, + "packages/lib-c": { + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } + }, + "packages/lib-a": { + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } + }, + "packages/lib-b": { + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } + }, + "packages/app": { + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "workspace:*", + "lib-b": "workspace:*" + } + } + }, + "packages": { + "shared-dep": ["shared-dep@2.0.0", "", {}, "sha512-shared-dep-2-0-0"], + "shared-dep@2.0.0": ["shared-dep@2.0.0", "", {}, "sha512-shared-dep-2-0-0"] + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/before-bun.lock b/packages/cli/e2e/fixtures/bun-diamond/before-bun.lock new file mode 100644 index 0000000..9c2738b --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/before-bun.lock @@ -0,0 +1,42 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "diamond-test" + }, + "packages/lib-c": { + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } + }, + "packages/lib-a": { + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } + }, + "packages/lib-b": { + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } + }, + "packages/app": { + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "workspace:*", + "lib-b": "workspace:*" + } + } + }, + "packages": { + "shared-dep": ["shared-dep@1.0.0", "", {}, "sha512-shared-dep-1-0-0"], + "shared-dep@1.0.0": ["shared-dep@1.0.0", "", {}, "sha512-shared-dep-1-0-0"] + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/workspace/package.json b/packages/cli/e2e/fixtures/bun-diamond/workspace/package.json new file mode 100644 index 0000000..4595118 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "diamond-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/app/package.json b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/app/package.json new file mode 100644 index 0000000..ee59719 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "workspace:*", + "lib-b": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-a/package.json b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-a/package.json new file mode 100644 index 0000000..4c9f166 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-b/package.json b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-b/package.json new file mode 100644 index 0000000..e9da4c6 --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-c/package.json b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-c/package.json new file mode 100644 index 0000000..ba64f8a --- /dev/null +++ b/packages/cli/e2e/fixtures/bun-diamond/workspace/packages/lib-c/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/after-package-lock.json b/packages/cli/e2e/fixtures/npm-deep-chain/after-package-lock.json new file mode 100644 index 0000000..9c86de3 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/after-package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "deep-chain-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deep-chain-test", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "node_modules/express": { + "version": "4.18.5", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.5.tgz" + }, + "packages/pkg-leaf": { + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-leaf/node_modules/express": { + "version": "4.18.5", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.5.tgz" + }, + "packages/pkg-base": { + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "1.0.0" + } + }, + "packages/pkg-middle": { + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } + }, + "packages/pkg-top": { + "version": "1.0.0", + "dependencies": { + "pkg-middle": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/before-package-lock.json b/packages/cli/e2e/fixtures/npm-deep-chain/before-package-lock.json new file mode 100644 index 0000000..27e5b2a --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/before-package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "deep-chain-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deep-chain-test", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "node_modules/express": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz" + }, + "packages/pkg-leaf": { + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } + }, + "packages/pkg-leaf/node_modules/express": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz" + }, + "packages/pkg-base": { + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "1.0.0" + } + }, + "packages/pkg-middle": { + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } + }, + "packages/pkg-top": { + "version": "1.0.0", + "dependencies": { + "pkg-middle": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/workspace/package.json b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/package.json new file mode 100644 index 0000000..b20528c --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "deep-chain-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..29a0b91 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-leaf/package.json b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-leaf/package.json new file mode 100644 index 0000000..d534480 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-leaf/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..cb8ce80 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-top/package.json b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-top/package.json new file mode 100644 index 0000000..be5c1fd --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-deep-chain/workspace/packages/pkg-top/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/after-package-lock.json b/packages/cli/e2e/fixtures/npm-diamond/after-package-lock.json new file mode 100644 index 0000000..740ec9f --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/after-package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "diamond-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "diamond-test", + "version": "1.0.0" + }, + "node_modules/shared-dep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shared-dep/-/shared-dep-2.0.0.tgz" + }, + "packages/lib-c": { + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } + }, + "packages/lib-c/node_modules/shared-dep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shared-dep/-/shared-dep-2.0.0.tgz" + }, + "packages/lib-a": { + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } + }, + "packages/lib-b": { + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } + }, + "packages/app": { + "version": "1.0.0", + "dependencies": { + "lib-a": "1.0.0", + "lib-b": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/before-package-lock.json b/packages/cli/e2e/fixtures/npm-diamond/before-package-lock.json new file mode 100644 index 0000000..c8cfdb0 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/before-package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "diamond-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "diamond-test", + "version": "1.0.0" + }, + "node_modules/shared-dep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shared-dep/-/shared-dep-1.0.0.tgz" + }, + "packages/lib-c": { + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } + }, + "packages/lib-c/node_modules/shared-dep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shared-dep/-/shared-dep-1.0.0.tgz" + }, + "packages/lib-a": { + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } + }, + "packages/lib-b": { + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } + }, + "packages/app": { + "version": "1.0.0", + "dependencies": { + "lib-a": "1.0.0", + "lib-b": "1.0.0" + } + } + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/workspace/package.json b/packages/cli/e2e/fixtures/npm-diamond/workspace/package.json new file mode 100644 index 0000000..4595118 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "diamond-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/app/package.json b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/app/package.json new file mode 100644 index 0000000..cf8a0ae --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "1.0.0", + "lib-b": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-a/package.json b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-a/package.json new file mode 100644 index 0000000..6012e86 --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-b/package.json b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-b/package.json new file mode 100644 index 0000000..f8c197e --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-c/package.json b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-c/package.json new file mode 100644 index 0000000..ba64f8a --- /dev/null +++ b/packages/cli/e2e/fixtures/npm-diamond/workspace/packages/lib-c/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/after-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-diamond/after-pnpm-lock.yaml new file mode 100644 index 0000000..9e8a2a9 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/after-pnpm-lock.yaml @@ -0,0 +1,34 @@ +lockfileVersion: '9.0' + +metadata: + packageManager: pnpm@9.15.9 + +importers: + .: {} + packages/lib-c: + dependencies: + shared-dep: + specifier: ^1.0.0 + version: 2.0.0 + packages/lib-a: + dependencies: + lib-c: + specifier: workspace:* + version: 1.0.0 + packages/lib-b: + dependencies: + lib-c: + specifier: workspace:* + version: 1.0.0 + packages/app: + dependencies: + lib-a: + specifier: workspace:* + version: 1.0.0 + lib-b: + specifier: workspace:* + version: 1.0.0 + +packages: + shared-dep@2.0.0: + resolution: { integrity: sha512-sha-shared-dep-2-0-0 } diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/before-pnpm-lock.yaml b/packages/cli/e2e/fixtures/pnpm-diamond/before-pnpm-lock.yaml new file mode 100644 index 0000000..d70de3d --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/before-pnpm-lock.yaml @@ -0,0 +1,34 @@ +lockfileVersion: '9.0' + +metadata: + packageManager: pnpm@9.15.9 + +importers: + .: {} + packages/lib-c: + dependencies: + shared-dep: + specifier: ^1.0.0 + version: 1.0.0 + packages/lib-a: + dependencies: + lib-c: + specifier: workspace:* + version: 1.0.0 + packages/lib-b: + dependencies: + lib-c: + specifier: workspace:* + version: 1.0.0 + packages/app: + dependencies: + lib-a: + specifier: workspace:* + version: 1.0.0 + lib-b: + specifier: workspace:* + version: 1.0.0 + +packages: + shared-dep@1.0.0: + resolution: { integrity: sha512-sha-shared-dep-1-0-0 } diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/workspace/package.json b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/package.json new file mode 100644 index 0000000..4595118 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "diamond-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/app/package.json b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/app/package.json new file mode 100644 index 0000000..ee59719 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "workspace:*", + "lib-b": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-a/package.json b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-a/package.json new file mode 100644 index 0000000..4c9f166 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-b/package.json b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-b/package.json new file mode 100644 index 0000000..e9da4c6 --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-c/package.json b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-c/package.json new file mode 100644 index 0000000..ba64f8a --- /dev/null +++ b/packages/cli/e2e/fixtures/pnpm-diamond/workspace/packages/lib-c/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/after-yarn.lock b/packages/cli/e2e/fixtures/yarn-deep-chain/after-yarn.lock new file mode 100644 index 0000000..d6dc714 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/after-yarn.lock @@ -0,0 +1,40 @@ +# yarn.lock - Yarn Berry (v4) format + +__metadata: + version: 6 + cacheKey: 8 + +"express@npm:^4.18.0": + version: 4.18.5 + resolution: "express@npm:4.18.5" + checksum: sha512-express-4-18-5 + languageName: node + linkType: hard + +"pkg-leaf@workspace:packages/pkg-leaf": + version: 1.0.0 + resolution: "pkg-leaf@workspace:packages/pkg-leaf" + checksum: sha512-pkg-leaf + languageName: node + linkType: soft + +"pkg-base@workspace:packages/pkg-base": + version: 1.0.0 + resolution: "pkg-base@workspace:packages/pkg-base" + checksum: sha512-pkg-base + languageName: node + linkType: soft + +"pkg-middle@workspace:packages/pkg-middle": + version: 1.0.0 + resolution: "pkg-middle@workspace:packages/pkg-middle" + checksum: sha512-pkg-middle + languageName: node + linkType: soft + +"pkg-top@workspace:packages/pkg-top": + version: 1.0.0 + resolution: "pkg-top@workspace:packages/pkg-top" + checksum: sha512-pkg-top + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/before-yarn.lock b/packages/cli/e2e/fixtures/yarn-deep-chain/before-yarn.lock new file mode 100644 index 0000000..4a9e319 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/before-yarn.lock @@ -0,0 +1,40 @@ +# yarn.lock - Yarn Berry (v4) format + +__metadata: + version: 6 + cacheKey: 8 + +"express@npm:^4.18.0": + version: 4.18.0 + resolution: "express@npm:4.18.0" + checksum: sha512-express-4-18-0 + languageName: node + linkType: hard + +"pkg-leaf@workspace:packages/pkg-leaf": + version: 1.0.0 + resolution: "pkg-leaf@workspace:packages/pkg-leaf" + checksum: sha512-pkg-leaf + languageName: node + linkType: soft + +"pkg-base@workspace:packages/pkg-base": + version: 1.0.0 + resolution: "pkg-base@workspace:packages/pkg-base" + checksum: sha512-pkg-base + languageName: node + linkType: soft + +"pkg-middle@workspace:packages/pkg-middle": + version: 1.0.0 + resolution: "pkg-middle@workspace:packages/pkg-middle" + checksum: sha512-pkg-middle + languageName: node + linkType: soft + +"pkg-top@workspace:packages/pkg-top": + version: 1.0.0 + resolution: "pkg-top@workspace:packages/pkg-top" + checksum: sha512-pkg-top + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/package.json b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/package.json new file mode 100644 index 0000000..b20528c --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "deep-chain-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-base/package.json b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-base/package.json new file mode 100644 index 0000000..c311f4d --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-base", + "version": "1.0.0", + "dependencies": { + "pkg-leaf": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-leaf/package.json b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-leaf/package.json new file mode 100644 index 0000000..d534480 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-leaf/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-leaf", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-middle/package.json b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-middle/package.json new file mode 100644 index 0000000..513baa9 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-middle/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-middle", + "version": "1.0.0", + "dependencies": { + "pkg-base": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-top/package.json b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-top/package.json new file mode 100644 index 0000000..d832b7b --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-deep-chain/workspace/packages/pkg-top/package.json @@ -0,0 +1,7 @@ +{ + "name": "pkg-top", + "version": "1.0.0", + "dependencies": { + "pkg-middle": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-diamond/after-yarn.lock b/packages/cli/e2e/fixtures/yarn-diamond/after-yarn.lock new file mode 100644 index 0000000..2036f82 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/after-yarn.lock @@ -0,0 +1,40 @@ +# yarn.lock - Yarn Berry (v4) format + +__metadata: + version: 6 + cacheKey: 8 + +"shared-dep@npm:^1.0.0": + version: 2.0.0 + resolution: "shared-dep@npm:2.0.0" + checksum: sha512-shared-dep-2-0-0 + languageName: node + linkType: hard + +"lib-c@workspace:packages/lib-c": + version: 1.0.0 + resolution: "lib-c@workspace:packages/lib-c" + checksum: sha512-lib-c + languageName: node + linkType: soft + +"lib-a@workspace:packages/lib-a": + version: 1.0.0 + resolution: "lib-a@workspace:packages/lib-a" + checksum: sha512-lib-a + languageName: node + linkType: soft + +"lib-b@workspace:packages/lib-b": + version: 1.0.0 + resolution: "lib-b@workspace:packages/lib-b" + checksum: sha512-lib-b + languageName: node + linkType: soft + +"app@workspace:packages/app": + version: 1.0.0 + resolution: "app@workspace:packages/app" + checksum: sha512-app + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-diamond/before-yarn.lock b/packages/cli/e2e/fixtures/yarn-diamond/before-yarn.lock new file mode 100644 index 0000000..936a231 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/before-yarn.lock @@ -0,0 +1,40 @@ +# yarn.lock - Yarn Berry (v4) format + +__metadata: + version: 6 + cacheKey: 8 + +"shared-dep@npm:^1.0.0": + version: 1.0.0 + resolution: "shared-dep@npm:1.0.0" + checksum: sha512-shared-dep-1-0-0 + languageName: node + linkType: hard + +"lib-c@workspace:packages/lib-c": + version: 1.0.0 + resolution: "lib-c@workspace:packages/lib-c" + checksum: sha512-lib-c + languageName: node + linkType: soft + +"lib-a@workspace:packages/lib-a": + version: 1.0.0 + resolution: "lib-a@workspace:packages/lib-a" + checksum: sha512-lib-a + languageName: node + linkType: soft + +"lib-b@workspace:packages/lib-b": + version: 1.0.0 + resolution: "lib-b@workspace:packages/lib-b" + checksum: sha512-lib-b + languageName: node + linkType: soft + +"app@workspace:packages/app": + version: 1.0.0 + resolution: "app@workspace:packages/app" + checksum: sha512-app + languageName: node + linkType: soft diff --git a/packages/cli/e2e/fixtures/yarn-diamond/workspace/package.json b/packages/cli/e2e/fixtures/yarn-diamond/workspace/package.json new file mode 100644 index 0000000..4595118 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/workspace/package.json @@ -0,0 +1,7 @@ +{ + "name": "diamond-test", + "private": true, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/app/package.json b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/app/package.json new file mode 100644 index 0000000..ee59719 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "lib-a": "workspace:*", + "lib-b": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-a/package.json b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-a/package.json new file mode 100644 index 0000000..4c9f166 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-a", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-b/package.json b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-b/package.json new file mode 100644 index 0000000..e9da4c6 --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-b", + "version": "1.0.0", + "dependencies": { + "lib-c": "workspace:*" + } +} diff --git a/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-c/package.json b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-c/package.json new file mode 100644 index 0000000..ba64f8a --- /dev/null +++ b/packages/cli/e2e/fixtures/yarn-diamond/workspace/packages/lib-c/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-c", + "version": "1.0.0", + "dependencies": { + "shared-dep": "^1.0.0" + } +} diff --git a/packages/cli/e2e/transitive-dependency-detection.test.ts b/packages/cli/e2e/transitive-dependency-detection.test.ts index fb082b1..e1e7044 100644 --- a/packages/cli/e2e/transitive-dependency-detection.test.ts +++ b/packages/cli/e2e/transitive-dependency-detection.test.ts @@ -3,17 +3,6 @@ import { join } from 'path'; import { fileURLToPath } from 'url'; import { runAffectedCommand } from '../src/commands/run-affected-command.js'; import type { CliOptions } from '../src/options/cli-options.types.js'; -import { resolveAffectedPackages, ALL_DEPENDENCY_TYPES } from '@lockfile-affected/core'; -import type { LockfileDiff, WorkspaceGraph } from '@lockfile-affected/core'; - -function pkgWith(deps: { dependencies?: string[]; devDependencies?: string[] } = {}) { - return { - dependencies: new Set(deps.dependencies ?? []), - devDependencies: new Set(deps.devDependencies ?? []), - peerDependencies: new Set(), - optionalDependencies: new Set(), - }; -} const __filename = fileURLToPath(import.meta.url); const __dirname = join(__filename, '..'); @@ -172,64 +161,68 @@ describe('E2E: Per-importer detection (pnpm only)', () => { }); }); -describe('E2E: Deep transitive dependency chain (4+ levels)', () => { - const fixturesDir = join(__dirname, 'fixtures', 'pnpm-deep-chain'); +describe.each(fixtures)( + 'E2E: Deep transitive dependency chain ($name)', + ({ format, name, beforeLock, afterLock }) => { + const fixturesDir = join(__dirname, 'fixtures', `${name}-deep-chain`); - it('marks all packages affected when external dep changes at 4-level depth', async () => { - const workspaceRoot = join(fixturesDir, 'workspace'); - const beforeLockfilePath = join(fixturesDir, 'before-pnpm-lock.yaml'); - const afterLockfilePath = join(fixturesDir, 'after-pnpm-lock.yaml'); + it('marks all packages affected when external dep changes at 4-level depth', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + const afterLockfilePath = join(fixturesDir, afterLock); - const options: CliOptions = { - lockfileBefore: beforeLockfilePath, - lockfileAfter: afterLockfilePath, - workspaceRoot, - output: 'json', - format: 'pnpm', - deps: true, - dev: true, - peer: true, - optional: true, - }; + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: true, + peer: true, + optional: true, + }; - const result = await runAffectedCommand(options); - const affected = JSON.parse(result); + const result = await runAffectedCommand(options); + const affected = JSON.parse(result); - expect(affected).toContain('pkg-leaf'); - expect(affected).toContain('pkg-base'); - expect(affected).toContain('pkg-middle'); - expect(affected).toContain('pkg-top'); - }); -}); + expect(affected).toContain('pkg-leaf'); + expect(affected).toContain('pkg-base'); + expect(affected).toContain('pkg-middle'); + expect(affected).toContain('pkg-top'); + }); + }, +); -describe('E2E: Diamond dependency graph', () => { - it('marks all packages that depend on changed package via multiple paths', () => { - const workspace: WorkspaceGraph = new Map([ - ['app', { name: 'app', dependencyGroups: pkgWith({ dependencies: ['lib-a', 'lib-b'] }) }], - ['lib-a', { name: 'lib-a', dependencyGroups: pkgWith({ dependencies: ['lib-c'] }) }], - ['lib-b', { name: 'lib-b', dependencyGroups: pkgWith({ dependencies: ['lib-c'] }) }], - ['lib-c', { name: 'lib-c', dependencyGroups: pkgWith({ dependencies: ['shared-dep'] }) }], - ]); - const diff: LockfileDiff = { - addedContexts: new Map(), - removedContexts: new Map(), - changed: new Map([ - [ - '.', - { - added: new Map(), - removed: new Map(), - changed: new Map([['shared-dep', { from: '1.0.0', to: '2.0.0' }]]), - }, - ], - ]), - }; +describe.each(fixtures)( + 'E2E: Diamond dependency graph ($name)', + ({ format, name, beforeLock, afterLock }) => { + const fixturesDir = join(__dirname, 'fixtures', `${name}-diamond`); - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + it('marks all packages affected when shared dep changes', async () => { + const workspaceRoot = join(fixturesDir, 'workspace'); + const beforeLockfilePath = join(fixturesDir, beforeLock); + const afterLockfilePath = join(fixturesDir, afterLock); - expect(affected.has('lib-c')).toBe(true); - expect(affected.has('lib-a')).toBe(true); - expect(affected.has('lib-b')).toBe(true); - expect(affected.has('app')).toBe(true); - }); -}); + const options: CliOptions = { + lockfileBefore: beforeLockfilePath, + lockfileAfter: afterLockfilePath, + workspaceRoot, + output: 'json', + format, + deps: true, + dev: true, + peer: true, + optional: true, + }; + + const result = await runAffectedCommand(options); + const affected = JSON.parse(result); + + expect(affected).toContain('lib-c'); + expect(affected).toContain('lib-a'); + expect(affected).toContain('lib-b'); + expect(affected).toContain('app'); + }); + }, +); From a470293cb439c1a6dcedf3adc89cf6879b2d26a4 Mon Sep 17 00:00:00 2001 From: Henri Lahtinen Date: Tue, 31 Mar 2026 09:28:46 +0300 Subject: [PATCH 7/7] refactor: rename ALL_DEPENDENCY_TYPES to allDependencyTypes --- .../cli/src/options/to-dependency-filter.ts | 4 +-- packages/core/README.md | 4 +-- .../src/affected/find-affected-packages.ts | 4 +-- .../core/src/affected/find-dependents.test.ts | 4 +-- .../resolve-affected-packages.test.ts | 26 +++++++++---------- packages/core/src/index.ts | 2 +- packages/core/src/types/lockfile.ts | 2 +- pnpm-lock.yaml | 11 +++----- 8 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/options/to-dependency-filter.ts b/packages/cli/src/options/to-dependency-filter.ts index bf06ab9..b4b0a5a 100644 --- a/packages/cli/src/options/to-dependency-filter.ts +++ b/packages/cli/src/options/to-dependency-filter.ts @@ -1,4 +1,4 @@ -import { ALL_DEPENDENCY_TYPES, type DependencyFilter } from '@lockfile-affected/core'; +import { allDependencyTypes, type DependencyFilter } from '@lockfile-affected/core'; import type { CliOptions } from './cli-options.types.js'; /** @@ -9,7 +9,7 @@ export function toDependencyFilter(options: CliOptions): DependencyFilter { const anyExplicit = options.deps || options.dev || options.peer || options.optional; if (!anyExplicit) { - return ALL_DEPENDENCY_TYPES; + return allDependencyTypes; } return { diff --git a/packages/core/README.md b/packages/core/README.md index 804e021..1389b51 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -74,9 +74,9 @@ const diff = diffLockfileSnapshots(snapshotBefore, snapshotAfter); ### `resolveAffectedPackages(diff, workspaceGraph, filter?)` ```ts -import { resolveAffectedPackages, ALL_DEPENDENCY_TYPES } from '@lockfile-affected/core'; +import { resolveAffectedPackages, allDependencyTypes } from '@lockfile-affected/core'; -const affected = resolveAffectedPackages(diff, workspaceGraph, ALL_DEPENDENCY_TYPES); +const affected = resolveAffectedPackages(diff, workspaceGraph, allDependencyTypes); // ReadonlySet of affected package names ``` diff --git a/packages/core/src/affected/find-affected-packages.ts b/packages/core/src/affected/find-affected-packages.ts index b3b17f3..315fcc6 100644 --- a/packages/core/src/affected/find-affected-packages.ts +++ b/packages/core/src/affected/find-affected-packages.ts @@ -1,5 +1,5 @@ import { - ALL_DEPENDENCY_TYPES, + allDependencyTypes, type DependencyFilter, type LockfileParser, } from '../types/lockfile.js'; @@ -36,5 +36,5 @@ export async function findAffectedPackages( const diff = diffLockfileSnapshots(snapshotBefore, snapshotAfter); const workspaceGraph = buildWorkspaceGraph(manifests); - return resolveAffectedPackages(diff, workspaceGraph, options.filter ?? ALL_DEPENDENCY_TYPES); + return resolveAffectedPackages(diff, workspaceGraph, options.filter ?? allDependencyTypes); } diff --git a/packages/core/src/affected/find-dependents.test.ts b/packages/core/src/affected/find-dependents.test.ts index 0677f23..d20f095 100644 --- a/packages/core/src/affected/find-dependents.test.ts +++ b/packages/core/src/affected/find-dependents.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { ALL_DEPENDENCY_TYPES, type WorkspaceGraph } from '../types/lockfile.js'; +import { allDependencyTypes, type WorkspaceGraph } from '../types/lockfile.js'; import { findDependents } from './find-dependents.js'; const pkgWith = ( @@ -49,7 +49,7 @@ describe('findDependents', () => { ['lib', { name: 'lib', dependencyGroups: pkgWith({ dependencies: ['vitest'] }) }], ]); - const dependents = findDependents('vitest', workspace, ALL_DEPENDENCY_TYPES); + const dependents = findDependents('vitest', workspace, allDependencyTypes); expect(dependents.size).toBe(2); expect(dependents.has('app')).toBe(true); diff --git a/packages/core/src/affected/resolve-affected-packages.test.ts b/packages/core/src/affected/resolve-affected-packages.test.ts index a7a3dc5..6b346b1 100644 --- a/packages/core/src/affected/resolve-affected-packages.test.ts +++ b/packages/core/src/affected/resolve-affected-packages.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { ALL_DEPENDENCY_TYPES, type LockfileDiff, type WorkspaceGraph } from '../types/lockfile.js'; +import { allDependencyTypes, type LockfileDiff, type WorkspaceGraph } from '../types/lockfile.js'; import { resolveAffectedPackages } from './resolve-affected-packages.js'; const emptyDiff: LockfileDiff = { @@ -28,7 +28,7 @@ describe('resolveAffectedPackages', () => { ['app', { name: 'app', dependencyGroups: pkgWith({ dependencies: ['lodash'] }) }], ]); - const affected = resolveAffectedPackages(emptyDiff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(emptyDiff, workspace, allDependencyTypes); expect(affected.size).toBe(0); }); @@ -52,7 +52,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(1); expect(affected.has('app')).toBe(true); @@ -77,7 +77,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.has('app')).toBe(true); }); @@ -177,7 +177,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(2); expect(affected.has('app-a')).toBe(true); @@ -203,7 +203,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(0); }); @@ -231,7 +231,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(3); expect(affected.has('pkg-base')).toBe(true); @@ -258,7 +258,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.has('pkg-a')).toBe(true); expect(affected.has('pkg-b')).toBe(true); @@ -309,7 +309,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.has('lib-c')).toBe(true); expect(affected.has('lib-a')).toBe(true); @@ -340,7 +340,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(2); expect(affected.has('pkg-a')).toBe(true); @@ -370,7 +370,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(3); expect(affected.has('pkg-a')).toBe(true); @@ -399,7 +399,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(1); expect(affected.has('pkg-a')).toBe(true); @@ -428,7 +428,7 @@ describe('resolveAffectedPackages', () => { ]), }; - const affected = resolveAffectedPackages(diff, workspace, ALL_DEPENDENCY_TYPES); + const affected = resolveAffectedPackages(diff, workspace, allDependencyTypes); expect(affected.size).toBe(3); expect(affected.has('pkg-a')).toBe(true); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9826d14..0fc5a8a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,7 @@ export type { WorkspaceGraph, WorkspacePackage, } from './types/lockfile.js'; -export { ALL_DEPENDENCY_TYPES } from './types/lockfile.js'; +export { allDependencyTypes } from './types/lockfile.js'; export { diffLockfileSnapshots } from './diff/diff-lockfile-snapshots.js'; export { resolveAffectedPackages } from './affected/resolve-affected-packages.js'; export { findAffectedPackages } from './affected/find-affected-packages.js'; diff --git a/packages/core/src/types/lockfile.ts b/packages/core/src/types/lockfile.ts index 54ffe08..3e4edf3 100644 --- a/packages/core/src/types/lockfile.ts +++ b/packages/core/src/types/lockfile.ts @@ -60,7 +60,7 @@ export type DependencyFilter = { }; /** A DependencyFilter that includes all dependency types. */ -export const ALL_DEPENDENCY_TYPES: DependencyFilter = { +export const allDependencyTypes: DependencyFilter = { dependencies: true, devDependencies: true, peerDependencies: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe4d158..d2c9a30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,9 +186,9 @@ importers: '@lockfile-affected/core': specifier: workspace:* version: link:../core - lockparse: - specifier: ^0.5.0 - version: 0.5.0 + yaml: + specifier: ^2.0.0 + version: 2.8.3 devDependencies: '@types/node': specifier: ^22.0.0 @@ -1591,9 +1591,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lockparse@0.5.0: - resolution: {integrity: sha512-seaI91ZVc4mnEGL+/cEEd5MybTnb86NH3W5lM0Ft7CMCZsLP5z1orWnu8g7YacpiMc5GxU7wIrYLhb6W2DvNWg==} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -3949,8 +3946,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lockparse@0.5.0: {} - lodash-es@4.17.23: {} lodash.capitalize@4.2.1: {}