@@ -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+
86119t . 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+
93152t . 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+
123221t . test ( 'provenance round-trips through the lockfile' , async t => {
124222 const dir = await setup ( t )
125223 await newArb ( dir ) . reify ( )
0 commit comments