Skip to content

Commit a95f9ca

Browse files
committed
test: cover ls/explain/linked-strategy and no-file paths for .npm-extension
1 parent 64a1ca8 commit a95f9ca

3 files changed

Lines changed: 148 additions & 1 deletion

File tree

tap-snapshots/test/lib/commands/ls.js.test.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ test-npm-ls@1.0.0 {CWD}/prefix
315315
\`-- dog@2.0.0
316316
`
317317

318+
exports[`test/lib/commands/ls.js TAP ls .npm-extension dep > human output annotates the transformed node 1`] = `
319+
test-npm-extension@1.0.0 {CWD}/prefix
320+
\`-- foo@1.0.0 .npm-extension: dependencies.bar
321+
\`-- bar@1.0.0
322+
`
323+
318324
exports[`test/lib/commands/ls.js TAP ls broken resolved field > should NOT print git refs in output tree 1`] = `
319325
npm-broken-resolved-field-test@1.0.0 {CWD}/prefix
320326
\`-- a@1.0.1

test/lib/commands/ls.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,49 @@ t.test('ls', async t => {
351351
t.match(applied, { selector: 'foo@1', dependencies: ['bar'] }, 'json output includes provenance')
352352
})
353353

354+
const npmExtensionPrefix = {
355+
'package.json': JSON.stringify({
356+
name: 'test-npm-extension',
357+
version: '1.0.0',
358+
dependencies: { foo: '^1.0.0' },
359+
}),
360+
node_modules: {
361+
'.package-lock.json': JSON.stringify({
362+
packages: {
363+
'node_modules/foo': {
364+
version: '1.0.0',
365+
dependencies: { bar: '^1.0.0' },
366+
npmExtensionApplied: { extensionPoint: 'transformManifest', dependencies: ['bar'] },
367+
},
368+
'node_modules/bar': { version: '1.0.0' },
369+
},
370+
}),
371+
foo: {
372+
'package.json': JSON.stringify({ name: 'foo', version: '1.0.0', dependencies: { bar: '^1.0.0' } }),
373+
},
374+
bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }) },
375+
},
376+
}
377+
378+
t.test('.npm-extension dep', async t => {
379+
const { npm, result, ls } = await mockLs(t, { config: {}, prefixDir: npmExtensionPrefix })
380+
touchHiddenPackageLock(npm.prefix)
381+
await ls.exec([])
382+
t.matchSnapshot(cleanCwd(result()), 'human output annotates the transformed node')
383+
})
384+
385+
t.test('.npm-extension dep --json', async t => {
386+
const { npm, result, ls } = await mockLs(t, {
387+
config: { json: true },
388+
prefixDir: npmExtensionPrefix,
389+
})
390+
touchHiddenPackageLock(npm.prefix)
391+
await ls.exec([])
392+
const applied = JSON.parse(result()).dependencies.foo.npmExtensionApplied
393+
t.match(applied, { extensionPoint: 'transformManifest', dependencies: ['bar'] },
394+
'json output includes provenance')
395+
})
396+
354397
t.test('with filter arg', async t => {
355398
const config = {
356399
color: 'always',

workspaces/arborist/test/arborist/reify-npm-extension.js

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ const createRegistry = (t) => new MockRegistry({
1414
})
1515

1616
// foo@1.0.0 does not declare bar; both are served as installable tarballs from source dirs.
17-
const register = async (t, dir, { withBar = true } = {}) => {
17+
const register = async (t, dir, { withBar = true, withBaz = false } = {}) => {
1818
const registry = createRegistry(t)
1919
const fooManifest = registry.manifest({ name: 'foo', packuments: [{ version: '1.0.0' }] })
2020
await registry.package({ manifest: fooManifest, tarballs: { '1.0.0': join(dir, 'src/foo') } })
2121
if (withBar) {
2222
const barManifest = registry.manifest({ name: 'bar', packuments: [{ version: '1.2.3' }] })
2323
await registry.package({ manifest: barManifest, tarballs: { '1.2.3': join(dir, 'src/bar') } })
2424
}
25+
if (withBaz) {
26+
const bazManifest = registry.manifest({ name: 'baz', packuments: [{ version: '3.0.0' }] })
27+
await registry.package({ manifest: bazManifest, tarballs: { '3.0.0': join(dir, 'src/baz') } })
28+
}
2529
}
2630

2731
// a transformManifest that adds bar to foo
@@ -83,13 +87,68 @@ t.test('lockfile records hash, provenance, effective deps, and version 4', async
8387
t.strictSame(fooEntry.dependencies, { bar: '^1.0.0' }, 'foo entry carries the effective dependency metadata')
8488
})
8589

90+
t.test('explain annotates the transform-created edge', async t => {
91+
const dir = await setup(t)
92+
const tree = await newArb(dir).reify()
93+
const foo = [...tree.inventory.values()].find(n => n.name === 'foo' && !n.isLink)
94+
const explanation = foo.edgesOut.get('bar').explain()
95+
t.strictSame(explanation.npmExtension, { extensionPoint: 'transformManifest', field: 'dependencies' },
96+
'edge explanation carries the transform provenance')
97+
})
98+
99+
t.test('explain annotates an edge created in a non-first field', async t => {
100+
// adds bar to optionalDependencies, so the edge explanation loop skips `dependencies` before matching
101+
const dir = await setup(t, {
102+
extension: `module.exports = {
103+
transformManifest (pkg) {
104+
if (pkg.name === 'foo') {
105+
pkg.optionalDependencies = { ...pkg.optionalDependencies, bar: '^1.0.0' }
106+
}
107+
return pkg
108+
},
109+
}
110+
`,
111+
})
112+
const tree = await newArb(dir).reify()
113+
const foo = [...tree.inventory.values()].find(n => n.name === 'foo' && !n.isLink)
114+
const explanation = foo.edgesOut.get('bar').explain()
115+
t.strictSame(explanation.npmExtension, { extensionPoint: 'transformManifest', field: 'optionalDependencies' },
116+
'edge explanation reports the optionalDependencies field')
117+
})
118+
86119
t.test('does not rewrite the installed dependency package.json', async t => {
87120
const dir = await setup(t)
88121
await newArb(dir).reify()
89122
const installed = JSON.parse(fs.readFileSync(join(dir, 'node_modules/foo/package.json'), 'utf8'))
90123
t.notOk(installed.dependencies, 'the on-disk foo/package.json is not given a bar dependency')
91124
})
92125

126+
t.test('composes with packageExtensions on the same package', async t => {
127+
// .npm-extension adds bar to foo (runs first); packageExtensions adds baz to foo (runs on the transform output)
128+
const dir = t.testdir({
129+
'package.json': JSON.stringify({
130+
name: 'root',
131+
dependencies: { foo: '1.0.0' },
132+
packageExtensions: { 'foo@1': { dependencies: { baz: '^3.0.0' } } },
133+
}),
134+
'.npm-extension.cjs': addBar,
135+
src: {
136+
foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) },
137+
bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.2.3' }) },
138+
baz: { 'package.json': JSON.stringify({ name: 'baz', version: '3.0.0' }) },
139+
},
140+
})
141+
await register(t, dir, { withBaz: true })
142+
const tree = await newArb(dir).reify()
143+
const foo = [...tree.inventory.values()].find(n => n.name === 'foo' && !n.isLink)
144+
t.ok(foo.edgesOut.get('bar')?.to, 'transform-created bar edge resolved')
145+
t.ok(foo.edgesOut.get('baz')?.to, 'packageExtensions-created baz edge resolved')
146+
t.same(foo.npmExtensionApplied, { extensionPoint: 'transformManifest', dependencies: ['bar'] },
147+
'transform provenance recorded')
148+
t.same(foo.packageExtensionsApplied, { selector: 'foo@1', dependencies: ['baz'] },
149+
'packageExtensions provenance recorded')
150+
})
151+
93152
t.test('composes with overrides during reify', async t => {
94153
const dir = await setup(t, { overrides: { bar: '1.2.3' } })
95154
const tree = await newArb(dir).reify()
@@ -120,6 +179,45 @@ t.test('ignore-extension disables the transform and records no state', async t =
120179
t.notOk(lock.packages['node_modules/foo'].dependencies, 'foo has no extension-added dependency')
121180
})
122181

182+
t.test('a project with no .npm-extension installs normally and records no state', async t => {
183+
const dir = t.testdir({
184+
'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' } }),
185+
src: { foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) } },
186+
})
187+
await register(t, dir, { withBar: false })
188+
await newArb(dir).reify()
189+
const lock = readLock(dir)
190+
t.notOk(lock.packages[''].npmExtensionHash, 'no extension hash recorded')
191+
t.notOk(lock.packages['node_modules/foo'].dependencies, 'foo unchanged')
192+
})
193+
194+
t.test('provenance round-trips under install-strategy=linked', async t => {
195+
const dir = await setup(t)
196+
await newArb(dir, { installStrategy: 'linked' }).reify()
197+
// a second linked reify rescans the store and links, re-deriving provenance on both
198+
const tree = await newArb(dir, { installStrategy: 'linked' }).reify()
199+
const foo = [...tree.inventory.values()].find(n => n.name === 'foo')
200+
t.ok(foo.npmExtensionApplied || foo.target?.npmExtensionApplied, 'provenance present on the linked node or its target')
201+
})
202+
203+
t.test('loadActual re-derives provenance only for transformed installed deps', async t => {
204+
// a filesystem-scanned tree: foo is the transform target, qux is an unrelated installed dep
205+
const dir = t.testdir({
206+
'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '^1.0.0', qux: '^1.0.0' } }),
207+
'.npm-extension.cjs': addBar,
208+
node_modules: {
209+
foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) },
210+
qux: { 'package.json': JSON.stringify({ name: 'qux', version: '1.0.0' }) },
211+
},
212+
})
213+
const actual = await newArb(dir).loadActual()
214+
const foo = [...actual.inventory.values()].find(n => n.name === 'foo' && !n.isLink)
215+
const qux = [...actual.inventory.values()].find(n => n.name === 'qux' && !n.isLink)
216+
t.strictSame(foo.npmExtensionApplied, { extensionPoint: 'transformManifest', dependencies: ['bar'] },
217+
'foo carries provenance from the re-derived transform')
218+
t.equal(qux.npmExtensionApplied, null, 'qux, untouched by the transform, carries no provenance')
219+
})
220+
123221
t.test('provenance round-trips through the lockfile', async t => {
124222
const dir = await setup(t)
125223
await newArb(dir).reify()

0 commit comments

Comments
 (0)