Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Patch files are applied verbatim by package managers, and a diff whose line
# endings changed under checkout normalization may fail to apply. Keep them
# byte-identical across platforms. (pnpm normalizes line endings before hashing
# a patch, so this does not affect the `patch_hash=` it records in the lockfile.)
*.patch -text

Large diffs are not rendered by default.

490 changes: 489 additions & 1 deletion packages/cli/src/services/check-parser/__tests__/bundler.spec.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "lockfile-pruner-fixture",
"private": true,
"dependencies": {
"@fixture/used": "workspace:*",
"@fixture/shimmed": "workspace:*",
"@fixture/absent": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixture/absent",
"version": "1.0.0",
"dependencies": {
"ee-first": "1.1.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixture/shimmed",
"version": "1.0.0",
"dependencies": {
"isarray": "2.0.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixture/used",
"version": "1.0.0",
"dependencies": {
"ms": "2.1.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
diff --git a/index.js b/index.js
index 3333333..4444444 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,4 @@
+// Patched by the lockfile-pruner fixture: unused once pruned, must be dropped.
/*!
* ee-first
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
diff --git a/index.js b/index.js
index 1111111..2222222 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,4 @@
+// Patched by the lockfile-pruner fixture: applied, must survive pruning.
/**
* Helpers.
*/

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
packages:
- packages/*
patchedDependencies:
ms@2.1.3: patches/ms@2.1.3.patch
ee-first@1.1.1: patches/ee-first@1.1.1.patch
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import { Err, Ok } from '../package-files/result.js'
import { File } from '../parser.js'

const PNPM_FIXTURE_ROOT = path.join(__dirname, 'lockfile-pruner-fixtures', 'pnpm-workspace')
// Same shape as PNPM_FIXTURE_ROOT, plus two patched dependencies: `ms` is
// consumed by the bundled member, `ee-first` only by the member the bundle
// omits. Pruning therefore leaves the `ee-first` patch applying to nothing,
// which pnpm 10+ rejects unless the install tolerates unused patches.
const PNPM_PATCHED_FIXTURE_ROOT = path.join(__dirname, 'lockfile-pruner-fixtures', 'pnpm-patched-workspace')
const NPM_FIXTURE_ROOT = path.join(__dirname, 'lockfile-pruner-fixtures', 'npm-workspace')
const BUN_FIXTURE_ROOT = path.join(__dirname, 'lockfile-pruner-fixtures', 'bun-workspace')
const YARN_FIXTURE_ROOT = path.join(__dirname, 'lockfile-pruner-fixtures', 'yarn-workspace')
Expand Down Expand Up @@ -107,7 +112,7 @@ describe('lockfile-pruner', () => {
// bundle where the `used` member is imported (real manifest), the `shimmed`
// member is declared but unimported (faux manifest), and the `absent`
// member is missing entirely.
const makeScenario = (root: string, lockfileName: string) => {
const makeScenario = (root: string, lockfileName: string, extraFiles: string[] = []) => {
const used = new Package({ name: '@fixture/used', path: path.join(root, 'packages/used'), version: '1.0.0' })
const shimmed = new Package({ name: '@fixture/shimmed', path: path.join(root, 'packages/shimmed'), version: '1.0.0' })
const absent = new Package({ name: '@fixture/absent', path: path.join(root, 'packages/absent'), version: '1.0.0' })
Expand Down Expand Up @@ -137,11 +142,22 @@ describe('lockfile-pruner', () => {
if (lockfileName === 'pnpm-lock.yaml') {
files.set(...physical('pnpm-workspace.yaml'))
}
for (const extra of extraFiles) {
files.set(...physical(extra))
}

return { workspace, files, used, shimmed, absent }
}

const makePnpmScenario = (root: string = PNPM_FIXTURE_ROOT) => makeScenario(root, 'pnpm-lock.yaml')

// The patched fixture's bundle additionally carries the patch files, exactly
// as the auto-include does for a real bundle: pnpm hashes every declared
// patch file during resolution, so an install without them cannot run at all.
const makePnpmPatchedScenario = () => makeScenario(PNPM_PATCHED_FIXTURE_ROOT, 'pnpm-lock.yaml', [
'patches/ms@2.1.3.patch',
'patches/ee-first@1.1.1.patch',
])
const makeNpmScenario = (root: string = NPM_FIXTURE_ROOT) => makeScenario(root, 'package-lock.json')
const makeBunScenario = (root: string = BUN_FIXTURE_ROOT) => makeScenario(root, 'bun.lock')
const makeYarnScenario = (root: string = YARN_FIXTURE_ROOT) => makeScenario(root, 'yarn.lock')
Expand Down Expand Up @@ -724,6 +740,31 @@ describe('lockfile-pruner', () => {
expect(result.content).not.toContain('ee-first')
}, 60_000)

it('prunes a workspace whose patch applies to nothing once pruned, with real pnpm', async () => {
const { workspace, files } = makePnpmPatchedScenario()
const result = await pruneBundledLockfile({
workspace,
packageManager: new PNpmDetector(),
files,
env: testEnv(),
})

// Without --config.allowUnusedPatches the install aborts with
// ERR_PNPM_UNUSED_PATCH, because the `ee-first` patch has nothing left to
// apply to once the member consuming it is pruned away.
expect(result.status).toEqual('pruned')
if (result.status !== 'pruned') {
return
}

// The patch that still applies keeps its marker; the one that no longer
// does loses it. Both declarations survive in the section regardless,
// because it mirrors the config rather than the dependency graph.
expect(result.content).toContain('patch_hash=8efb625dd8ccb88e78507bea1f647ed25671bcda20a8554ea02a4122021736bb')
expect(result.content).not.toContain('patch_hash=90b918fd6167721e405a502ac35adb29ec15497947e9d3b032d6da16a460b4af')
expect(result.content).toContain('ee-first@1.1.1:')
}, 60_000)

it('fails when npm silently replaces a workspace link with a registry package', async () => {
const { workspace, files } = makeNpmScenario()
// Simulate npm's registry substitution: the link entry for the used
Expand Down
Loading
Loading