@@ -66,17 +66,45 @@ function expandHome(p: string): string {
6666 return p . startsWith ( '~' ) ? path . join ( homedir ( ) , p . slice ( 1 ) ) : p ;
6767}
6868
69+ function parseShard ( value : string ) : { index : number ; total : number } {
70+ const match = value . match ( / ^ ( \d + ) \/ ( \d + ) $ / ) ;
71+ if ( ! match ) {
72+ throw new Error (
73+ `Invalid --shard format: "${ value } ". Expected format: --shard=<index>/<total> (e.g., --shard=1/3)` ,
74+ ) ;
75+ }
76+ const index = Number ( match [ 1 ] ) ;
77+ const total = Number ( match [ 2 ] ) ;
78+ if ( total < 1 ) {
79+ throw new Error ( `Invalid --shard total: ${ total } . Must be >= 1` ) ;
80+ }
81+ if ( index < 1 || index > total ) {
82+ throw new Error ( `Invalid --shard index: ${ index } . Must be between 1 and ${ total } ` ) ;
83+ }
84+ return { index, total } ;
85+ }
86+
87+ function selectShard < T > ( items : T [ ] , index : number , total : number ) : T [ ] {
88+ const chunkSize = Math . ceil ( items . length / total ) ;
89+ const start = ( index - 1 ) * chunkSize ;
90+ return items . slice ( start , start + chunkSize ) ;
91+ }
92+
93+ const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests' ;
94+
6995export async function snapTest ( ) {
7096 const { positionals, values } = parseArgs ( {
7197 allowPositionals : true ,
7298 args : process . argv . slice ( 3 ) ,
7399 options : {
74100 dir : { type : 'string' } ,
75101 'bin-dir' : { type : 'string' } ,
102+ shard : { type : 'string' } ,
76103 } ,
77104 } ) ;
78105
79106 const filter = positionals [ 0 ] ?? '' ; // Optional filter to run specific test cases
107+ const shard = values . shard ? parseShard ( values . shard ) : undefined ;
80108
81109 // Create a unique temporary directory for testing
82110 // On macOS, `tmpdir()` is a symlink. Resolve it so that we can replace the resolved cwd in outputs.
@@ -85,6 +113,9 @@ export async function snapTest() {
85113 const systemTmpDir = fs . realpathSync ( tmpdir ( ) ) ;
86114 const tempTmpDir = `${ systemTmpDir } /vite-plus-test-${ randomUUID ( ) . replaceAll ( '-' , '' ) } ` ;
87115 fs . mkdirSync ( tempTmpDir , { recursive : true } ) ;
116+ // Pre-create the npm global prefix directory so tests using npm global
117+ // operations (link, outdated -g, etc.) don't fail with ENOENT.
118+ fs . mkdirSync ( path . join ( tempTmpDir , NPM_GLOBAL_PREFIX_DIR , 'lib' ) , { recursive : true } ) ;
88119
89120 // Clean up stale .node-version and package.json in the system temp directory.
90121 // vite-plus walks up the directory tree to resolve Node.js versions, so leftover
@@ -141,10 +172,10 @@ export async function snapTest() {
141172
142173 const casesDir = path . resolve ( values . dir || 'snap-tests' ) ;
143174
144- const serialTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
145- const parallelTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
175+ // Collect valid test case names (sorted for deterministic sharding)
176+ const validCaseNames : string [ ] = [ ] ;
146177 const missingStepsJson : string [ ] = [ ] ;
147- for ( const caseName of fs . readdirSync ( casesDir ) ) {
178+ for ( const caseName of fs . readdirSync ( casesDir ) . toSorted ( ) ) {
148179 if ( caseName . startsWith ( '.' ) ) {
149180 continue ;
150181 }
@@ -158,13 +189,7 @@ export async function snapTest() {
158189 continue ;
159190 }
160191 if ( caseName . includes ( filter ) ) {
161- const steps : Steps = JSON . parse ( readFileSync ( stepsPath , 'utf-8' ) ) ;
162- const task = ( ) => runTestCase ( caseName , tempTmpDir , casesDir , values [ 'bin-dir' ] ) ;
163- if ( steps . serial ) {
164- serialTasks . push ( task ) ;
165- } else {
166- parallelTasks . push ( task ) ;
167- }
192+ validCaseNames . push ( caseName ) ;
168193 }
169194 }
170195
@@ -174,15 +199,35 @@ export async function snapTest() {
174199 ) ;
175200 }
176201
202+ // Apply sharding to select a subset of test cases
203+ const selectedCases = shard
204+ ? selectShard ( validCaseNames , shard . index , shard . total )
205+ : validCaseNames ;
206+
207+ const serialTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
208+ const parallelTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
209+ for ( const caseName of selectedCases ) {
210+ const stepsPath = path . join ( casesDir , caseName , 'steps.json' ) ;
211+ const steps : Steps = JSON . parse ( readFileSync ( stepsPath , 'utf-8' ) ) ;
212+ const task = ( ) => runTestCase ( caseName , tempTmpDir , casesDir , values [ 'bin-dir' ] ) ;
213+ if ( steps . serial ) {
214+ serialTasks . push ( task ) ;
215+ } else {
216+ parallelTasks . push ( task ) ;
217+ }
218+ }
219+
177220 const totalCount = serialTasks . length + parallelTasks . length ;
178221 if ( totalCount > 0 ) {
179222 const cpuCount = cpus ( ) . length ;
223+ const shardInfo = shard ? `, shard ${ shard . index } /${ shard . total } ` : '' ;
180224 console . log (
181- 'Running %d test cases (%d serial + %d parallel, concurrency limit %d)' ,
225+ 'Running %d test cases (%d serial + %d parallel, concurrency limit %d%s )' ,
182226 totalCount ,
183227 serialTasks . length ,
184228 parallelTasks . length ,
185229 cpuCount ,
230+ shardInfo ,
186231 ) ;
187232 await runWithConcurrencyLimit ( serialTasks , 1 ) ;
188233 await runWithConcurrencyLimit ( parallelTasks , cpuCount ) ;
@@ -316,7 +361,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
316361 // Skip `vp install` inside `vp migrate` — snap tests don't need real installs
317362 VP_SKIP_INSTALL : '1' ,
318363 // make sure npm install global packages to the temporary directory
319- NPM_CONFIG_PREFIX : path . join ( tempTmpDir , 'npm-global-lib-for-snap-tests' ) ,
364+ NPM_CONFIG_PREFIX : path . join ( tempTmpDir , NPM_GLOBAL_PREFIX_DIR ) ,
320365
321366 // A test case can override/unset environment variables above.
322367 // For example, VP_CLI_TEST/CI can be unset to test the real-world outputs.
0 commit comments