@@ -2,17 +2,24 @@ import fs from 'node:fs/promises'
22import os from 'node:os'
33import path from 'node:path'
44import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest'
5- import {
5+
6+ vi . mock ( 'execa' , ( ) => ( {
7+ execa : vi . fn ( ) ,
8+ } ) )
9+
10+ const { execa } = await import ( 'execa' )
11+ const {
612 getInstallCommand,
713 getRootPackageInstallStatus,
814 getRootPackageJsonPaths,
915 getWorkspaceInstallStatus,
10- } from './package-install-check.server.ts'
16+ } = await import ( './package-install-check.server.ts' )
1117
1218let tempDir : string
1319
1420beforeEach ( async ( ) => {
1521 tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'package-install-test-' ) )
22+ vi . mocked ( execa ) . mockReset ( )
1623} )
1724
1825afterEach ( async ( ) => {
@@ -36,23 +43,20 @@ async function writePackageJson(
3643 )
3744}
3845
39- async function createNodeModules (
40- dir : string ,
41- packages : Array < string > ,
42- ) : Promise < void > {
43- const nodeModulesPath = path . join ( dir , 'node_modules' )
44- await fs . mkdir ( nodeModulesPath , { recursive : true } )
45-
46- for ( const pkg of packages ) {
47- if ( pkg . startsWith ( '@' ) ) {
48- const [ scope , name ] = pkg . split ( '/' )
49- const scopePath = path . join ( nodeModulesPath , scope ! )
50- await fs . mkdir ( scopePath , { recursive : true } )
51- await fs . mkdir ( path . join ( scopePath , name ! ) , { recursive : true } )
52- } else {
53- await fs . mkdir ( path . join ( nodeModulesPath , pkg ) , { recursive : true } )
54- }
55- }
46+ function mockNpmLsResult ( {
47+ exitCode,
48+ dependencies = { } ,
49+ stdout,
50+ } : {
51+ exitCode : number
52+ dependencies ?: Record < string , { missing ?: boolean ; invalid ?: boolean } >
53+ stdout ?: string
54+ } ) {
55+ vi . mocked ( execa ) . mockResolvedValue ( {
56+ exitCode,
57+ stdout : stdout ?? JSON . stringify ( { dependencies } ) ,
58+ stderr : '' ,
59+ } as never )
5660}
5761
5862describe ( 'getInstallCommand' , ( ) => {
@@ -94,7 +98,10 @@ describe('getRootPackageInstallStatus', () => {
9498 packageManager : 'npm@10.0.0' ,
9599 dependencies : { react : '^18.0.0' } ,
96100 } )
97- await createNodeModules ( tempDir , [ 'react' ] )
101+ mockNpmLsResult ( {
102+ exitCode : 0 ,
103+ dependencies : { react : { } } ,
104+ } )
98105
99106 const status = await getRootPackageInstallStatus (
100107 path . join ( tempDir , 'package.json' ) ,
@@ -111,7 +118,10 @@ describe('getRootPackageInstallStatus', () => {
111118 packageManager : 'pnpm@8.0.0' ,
112119 dependencies : { react : '^18.0.0' } ,
113120 } )
114- await createNodeModules ( tempDir , [ 'react' ] )
121+ mockNpmLsResult ( {
122+ exitCode : 0 ,
123+ dependencies : { react : { } } ,
124+ } )
115125
116126 const status = await getRootPackageInstallStatus (
117127 path . join ( tempDir , 'package.json' ) ,
@@ -128,7 +138,10 @@ describe('getRootPackageInstallStatus', () => {
128138 packageManager : 'yarn@4.0.0' ,
129139 dependencies : { react : '^18.0.0' } ,
130140 } )
131- await createNodeModules ( tempDir , [ 'react' ] )
141+ mockNpmLsResult ( {
142+ exitCode : 0 ,
143+ dependencies : { react : { } } ,
144+ } )
132145
133146 const status = await getRootPackageInstallStatus (
134147 path . join ( tempDir , 'package.json' ) ,
@@ -145,7 +158,10 @@ describe('getRootPackageInstallStatus', () => {
145158 packageManager : 'bun@1.0.0' ,
146159 dependencies : { react : '^18.0.0' } ,
147160 } )
148- await createNodeModules ( tempDir , [ 'react' ] )
161+ mockNpmLsResult ( {
162+ exitCode : 0 ,
163+ dependencies : { react : { } } ,
164+ } )
149165
150166 const status = await getRootPackageInstallStatus (
151167 path . join ( tempDir , 'package.json' ) ,
@@ -161,7 +177,10 @@ describe('getRootPackageInstallStatus', () => {
161177 name : 'test-package' ,
162178 dependencies : { react : '^18.0.0' } ,
163179 } )
164- await createNodeModules ( tempDir , [ 'react' ] )
180+ mockNpmLsResult ( {
181+ exitCode : 0 ,
182+ dependencies : { react : { } } ,
183+ } )
165184
166185 const status = await getRootPackageInstallStatus (
167186 path . join ( tempDir , 'package.json' ) ,
@@ -176,13 +195,17 @@ describe('getRootPackageInstallStatus', () => {
176195 name : 'test-package' ,
177196 dependencies : { react : '^18.0.0' } ,
178197 } )
198+ mockNpmLsResult ( {
199+ exitCode : 1 ,
200+ stdout : '' ,
201+ } )
179202
180203 const status = await getRootPackageInstallStatus (
181204 path . join ( tempDir , 'package.json' ) ,
182205 )
183206
184207 expect ( status . dependenciesNeedInstall ) . toBe ( true )
185- expect ( status . reason ) . toBe ( 'missing-node-modules ' )
208+ expect ( status . reason ) . toBe ( 'missing-dependencies ' )
186209 expect ( status . missingDependencies ) . toEqual ( [ 'react' ] )
187210 } )
188211
@@ -191,7 +214,13 @@ describe('getRootPackageInstallStatus', () => {
191214 name : 'test-package' ,
192215 dependencies : { react : '^18.0.0' , 'react-dom' : '^18.0.0' } ,
193216 } )
194- await createNodeModules ( tempDir , [ 'react' ] )
217+ mockNpmLsResult ( {
218+ exitCode : 1 ,
219+ dependencies : {
220+ react : { } ,
221+ 'react-dom' : { missing : true } ,
222+ } ,
223+ } )
195224
196225 const status = await getRootPackageInstallStatus (
197226 path . join ( tempDir , 'package.json' ) ,
@@ -200,6 +229,11 @@ describe('getRootPackageInstallStatus', () => {
200229 expect ( status . dependenciesNeedInstall ) . toBe ( true )
201230 expect ( status . reason ) . toBe ( 'missing-dependencies' )
202231 expect ( status . missingDependencies ) . toEqual ( [ 'react-dom' ] )
232+ expect ( execa ) . toHaveBeenCalledWith (
233+ 'npm' ,
234+ [ 'ls' , '--depth=0' , '--json' , 'react' , 'react-dom' ] ,
235+ expect . objectContaining ( { cwd : tempDir , reject : false } ) ,
236+ )
203237 } )
204238
205239 test ( 'detects missing devDependencies' , async ( ) => {
@@ -208,7 +242,14 @@ describe('getRootPackageInstallStatus', () => {
208242 dependencies : { react : '^18.0.0' } ,
209243 devDependencies : { typescript : '^5.0.0' , vitest : '^1.0.0' } ,
210244 } )
211- await createNodeModules ( tempDir , [ 'react' , 'typescript' ] )
245+ mockNpmLsResult ( {
246+ exitCode : 1 ,
247+ dependencies : {
248+ react : { } ,
249+ typescript : { } ,
250+ vitest : { missing : true } ,
251+ } ,
252+ } )
212253
213254 const status = await getRootPackageInstallStatus (
214255 path . join ( tempDir , 'package.json' ) ,
@@ -224,7 +265,10 @@ describe('getRootPackageInstallStatus', () => {
224265 name : 'test-package' ,
225266 dependencies : { '@epic-web/workshop-utils' : '^1.0.0' } ,
226267 } )
227- await createNodeModules ( tempDir , [ '@epic-web/workshop-utils' ] )
268+ mockNpmLsResult ( {
269+ exitCode : 0 ,
270+ dependencies : { '@epic-web/workshop-utils' : { } } ,
271+ } )
228272
229273 const status = await getRootPackageInstallStatus (
230274 path . join ( tempDir , 'package.json' ) ,
@@ -239,7 +283,10 @@ describe('getRootPackageInstallStatus', () => {
239283 name : 'test-package' ,
240284 dependencies : { '@epic-web/workshop-utils' : '^1.0.0' } ,
241285 } )
242- await createNodeModules ( tempDir , [ ] )
286+ mockNpmLsResult ( {
287+ exitCode : 1 ,
288+ dependencies : { '@epic-web/workshop-utils' : { missing : true } } ,
289+ } )
243290
244291 const status = await getRootPackageInstallStatus (
245292 path . join ( tempDir , 'package.json' ) ,
@@ -269,7 +316,13 @@ describe('getRootPackageInstallStatus', () => {
269316 dependencies : { react : '^18.0.0' } ,
270317 optionalDependencies : { 'optional-pkg' : '^1.0.0' } ,
271318 } )
272- await createNodeModules ( tempDir , [ 'react' ] )
319+ mockNpmLsResult ( {
320+ exitCode : 1 ,
321+ dependencies : {
322+ react : { } ,
323+ 'optional-pkg' : { missing : true } ,
324+ } ,
325+ } )
273326
274327 const status = await getRootPackageInstallStatus (
275328 path . join ( tempDir , 'package.json' ) ,
@@ -301,7 +354,10 @@ describe('getRootPackageInstallStatus', () => {
301354 name : 'test-package' ,
302355 dependencies : { react : '^18.0.0' , 'react-dom' : '^18.0.0' } ,
303356 } )
304- await createNodeModules ( tempDir , [ 'react' , 'react-dom' ] )
357+ mockNpmLsResult ( {
358+ exitCode : 0 ,
359+ dependencies : { react : { } , 'react-dom' : { } } ,
360+ } )
305361
306362 const status1 = await getRootPackageInstallStatus (
307363 path . join ( tempDir , 'package.json' ) ,
@@ -316,7 +372,10 @@ describe('getRootPackageInstallStatus', () => {
316372 name : 'different-name' ,
317373 dependencies : { react : '^18.0.0' , 'react-dom' : '^18.0.0' } ,
318374 } )
319- await createNodeModules ( tempDir2 , [ 'react' , 'react-dom' ] )
375+ mockNpmLsResult ( {
376+ exitCode : 0 ,
377+ dependencies : { react : { } , 'react-dom' : { } } ,
378+ } )
320379
321380 const status2 = await getRootPackageInstallStatus (
322381 path . join ( tempDir2 , 'package.json' ) ,
@@ -411,7 +470,10 @@ describe('getWorkspaceInstallStatus', () => {
411470 packageManager : 'pnpm@8.0.0' ,
412471 dependencies : { react : '^18.0.0' } ,
413472 } )
414- await createNodeModules ( tempDir , [ 'react' ] )
473+ mockNpmLsResult ( {
474+ exitCode : 0 ,
475+ dependencies : { react : { } } ,
476+ } )
415477
416478 const status = await getWorkspaceInstallStatus ( tempDir )
417479
@@ -426,15 +488,26 @@ describe('getWorkspaceInstallStatus', () => {
426488 name : 'root-package' ,
427489 dependencies : { react : '^18.0.0' } ,
428490 } )
429- await createNodeModules ( tempDir , [ 'react' ] )
491+ vi . mocked ( execa )
492+ . mockResolvedValueOnce ( {
493+ exitCode : 0 ,
494+ stdout : JSON . stringify ( { dependencies : { react : { } } } ) ,
495+ stderr : '' ,
496+ } as never )
497+ . mockResolvedValueOnce ( {
498+ exitCode : 1 ,
499+ stdout : JSON . stringify ( {
500+ dependencies : { typescript : { missing : true } } ,
501+ } ) ,
502+ stderr : '' ,
503+ } as never )
430504
431505 const subDir = path . join ( tempDir , 'sub' )
432506 await fs . mkdir ( subDir , { recursive : true } )
433507 await writePackageJson ( subDir , {
434508 name : 'sub-package' ,
435509 dependencies : { typescript : '^5.0.0' } ,
436510 } )
437- // Don't create node_modules for sub
438511
439512 const status = await getWorkspaceInstallStatus ( tempDir )
440513
@@ -450,7 +523,17 @@ describe('getWorkspaceInstallStatus', () => {
450523 packageManager : 'yarn@4.0.0' ,
451524 dependencies : { react : '^18.0.0' } ,
452525 } )
453- await createNodeModules ( tempDir , [ 'react' ] )
526+ vi . mocked ( execa )
527+ . mockResolvedValueOnce ( {
528+ exitCode : 0 ,
529+ stdout : JSON . stringify ( { dependencies : { react : { } } } ) ,
530+ stderr : '' ,
531+ } as never )
532+ . mockResolvedValueOnce ( {
533+ exitCode : 0 ,
534+ stdout : JSON . stringify ( { dependencies : { typescript : { } } } ) ,
535+ stderr : '' ,
536+ } as never )
454537
455538 const subDir = path . join ( tempDir , 'sub' )
456539 await fs . mkdir ( subDir , { recursive : true } )
@@ -459,7 +542,6 @@ describe('getWorkspaceInstallStatus', () => {
459542 packageManager : 'bun@1.0.0' ,
460543 dependencies : { typescript : '^5.0.0' } ,
461544 } )
462- await createNodeModules ( subDir , [ 'typescript' ] )
463545
464546 const status = await getWorkspaceInstallStatus ( tempDir )
465547
0 commit comments