@@ -182,7 +182,7 @@ t.test('lockfile-only node with hasInstallScript=true emits a sentinel', async t
182182
183183t . test ( 'sentinel is not emitted when scripts are already enumerated' , async t => {
184184 // If `hasInstallScript: true` coexists with a real `scripts` map, we
185- // surface the real names — the sentinel must not overwrite them.
185+ // surface the real names, and the sentinel must not overwrite them.
186186 const getInstallScripts = mockGetInstallScripts ( t )
187187 const node = {
188188 resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
@@ -206,3 +206,178 @@ t.test('sentinel is not emitted when hasInstallScript is absent', async t => {
206206 }
207207 t . strictSame ( await getInstallScripts ( node ) , { } )
208208} )
209+
210+ t . test ( 'lockfile-only node hydrates real scripts from package.json on disk' , async t => {
211+ // The common lockfile-driven case (`npm ci`, a repeat `npm install`):
212+ // `node.package.scripts` is empty but the package is unpacked on disk.
213+ // We must read the installed package.json and surface the real script
214+ // body instead of the sentinel.
215+ const getInstallScripts = mockGetInstallScripts ( t )
216+ const path = t . testdir ( {
217+ 'package.json' : JSON . stringify ( {
218+ name : 'pkg' ,
219+ version : '1.0.0' ,
220+ scripts : { postinstall : 'node -e "console.log(1)"' } ,
221+ } ) ,
222+ } )
223+ const lockfileNode = {
224+ resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
225+ path,
226+ isRegistryDependency : true ,
227+ hasInstallScript : true ,
228+ package : { name : 'pkg' , version : '1.0.0' } ,
229+ }
230+ t . strictSame (
231+ await getInstallScripts ( lockfileNode ) ,
232+ { postinstall : 'node -e "console.log(1)"' }
233+ )
234+ } )
235+
236+ t . test ( 'lockfile-only node hydrates an explicit install script from disk' , async t => {
237+ // A native package such as fsevents that ships a prebuilt binary (no
238+ // binding.gyp, so synthetic gyp detection finds nothing) but declares an
239+ // explicit `install: node-gyp rebuild`. The disk read must recover it
240+ // rather than emitting the sentinel.
241+ const getInstallScripts = mockGetInstallScripts ( t )
242+ const path = t . testdir ( {
243+ 'package.json' : JSON . stringify ( {
244+ name : 'fsevents' ,
245+ version : '2.3.3' ,
246+ scripts : { install : 'node-gyp rebuild' } ,
247+ } ) ,
248+ } )
249+ const lockfileNode = {
250+ resolved : 'https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz' ,
251+ path,
252+ isRegistryDependency : true ,
253+ hasInstallScript : true ,
254+ package : { name : 'fsevents' , version : '2.3.3' } ,
255+ }
256+ t . strictSame (
257+ await getInstallScripts ( lockfileNode ) ,
258+ { install : 'node-gyp rebuild' }
259+ )
260+ } )
261+
262+ t . test ( 'lockfile-only node hydrates a preinstall script from disk' , async t => {
263+ // Cover the disk `preinstall` recovery path.
264+ const getInstallScripts = mockGetInstallScripts ( t )
265+ const path = t . testdir ( {
266+ 'package.json' : JSON . stringify ( {
267+ name : 'pkg' ,
268+ version : '1.0.0' ,
269+ scripts : { preinstall : 'node pre.js' } ,
270+ } ) ,
271+ } )
272+ const lockfileNode = {
273+ resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
274+ path,
275+ isRegistryDependency : true ,
276+ hasInstallScript : true ,
277+ package : { name : 'pkg' , version : '1.0.0' } ,
278+ }
279+ t . strictSame (
280+ await getInstallScripts ( lockfileNode ) ,
281+ { preinstall : 'node pre.js' }
282+ )
283+ } )
284+
285+ t . test ( 'lockfile-only non-registry node hydrates prepare from disk' , async t => {
286+ // A git/file dependency installed from a lockfile: `prepare` runs for
287+ // non-registry sources, so the disk read must recover it.
288+ const getInstallScripts = mockGetInstallScripts ( t )
289+ const path = t . testdir ( {
290+ 'package.json' : JSON . stringify ( {
291+ name : 'pkg' ,
292+ version : '1.0.0' ,
293+ scripts : { prepare : 'node build.js' } ,
294+ } ) ,
295+ } )
296+ const lockfileNode = {
297+ resolved : 'git+ssh://git@github.com/foo/bar.git#abc' ,
298+ path,
299+ isRegistryDependency : false ,
300+ hasInstallScript : true ,
301+ package : { name : 'pkg' , version : '1.0.0' } ,
302+ }
303+ t . strictSame (
304+ await getInstallScripts ( lockfileNode ) ,
305+ { prepare : 'node build.js' }
306+ )
307+ } )
308+
309+ t . test ( 'hydrated prepare is ignored for registry deps' , async t => {
310+ // Hydration reuses the same registry/non-registry boundary as the
311+ // in-memory path: a registry dep whose on-disk package.json only has a
312+ // `prepare` script falls through to the sentinel, since `prepare` never
313+ // runs for registry installs.
314+ const getInstallScripts = mockGetInstallScripts ( t )
315+ const path = t . testdir ( {
316+ 'package.json' : JSON . stringify ( {
317+ name : 'pkg' ,
318+ version : '1.0.0' ,
319+ scripts : { prepare : 'build' } ,
320+ } ) ,
321+ } )
322+ const lockfileNode = {
323+ resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
324+ path,
325+ isRegistryDependency : true ,
326+ hasInstallScript : true ,
327+ package : { name : 'pkg' , version : '1.0.0' } ,
328+ }
329+ t . strictSame (
330+ await getInstallScripts ( lockfileNode ) ,
331+ { install : '(install scripts present)' }
332+ )
333+ } )
334+
335+ t . test ( 'falls back to sentinel when on-disk package.json has no install scripts' , async t => {
336+ // The lockfile flagged install scripts but the on-disk package.json has
337+ // none we recognise (e.g. only lifecycle scripts like `test`). Keep the
338+ // sentinel so the advisory still surfaces that something was flagged.
339+ const getInstallScripts = mockGetInstallScripts ( t )
340+ const path = t . testdir ( {
341+ 'package.json' : JSON . stringify ( {
342+ name : 'pkg' ,
343+ version : '1.0.0' ,
344+ scripts : { test : 'tap' } ,
345+ } ) ,
346+ } )
347+ const lockfileNode = {
348+ resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
349+ path,
350+ isRegistryDependency : true ,
351+ hasInstallScript : true ,
352+ package : { name : 'pkg' , version : '1.0.0' } ,
353+ }
354+ t . strictSame (
355+ await getInstallScripts ( lockfileNode ) ,
356+ { install : '(install scripts present)' }
357+ )
358+ } )
359+
360+ t . test ( 'disk hydration does not mutate node.package' , async t => {
361+ // The enumeration helper is read-only: recovering scripts from disk must
362+ // not write them back onto the node (unlike Builder#addToBuildSet, which
363+ // owns the node and hydrates it deliberately).
364+ const getInstallScripts = mockGetInstallScripts ( t )
365+ const path = t . testdir ( {
366+ 'package.json' : JSON . stringify ( {
367+ name : 'pkg' ,
368+ version : '1.0.0' ,
369+ scripts : { postinstall : 'echo hi' } ,
370+ } ) ,
371+ } )
372+ const pkg = { name : 'pkg' , version : '1.0.0' }
373+ const lockfileNode = {
374+ resolved : 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz' ,
375+ path,
376+ isRegistryDependency : true ,
377+ hasInstallScript : true ,
378+ package : pkg ,
379+ }
380+ await getInstallScripts ( lockfileNode )
381+ t . strictSame ( pkg , { name : 'pkg' , version : '1.0.0' } , 'node.package untouched' )
382+ t . notOk ( pkg . scripts , 'no scripts written back onto node.package' )
383+ } )
0 commit comments