11import { randomUUID } from 'node:crypto' ;
2- import fs , { readFileSync } from 'node:fs' ;
2+ import fs from 'node:fs' ;
33import fsPromises from 'node:fs/promises' ;
4- import { open } from 'node:fs/promises' ;
54import { cpus , homedir , tmpdir } from 'node:os' ;
65import path from 'node:path' ;
76import { setTimeout } from 'node:timers/promises' ;
@@ -92,6 +91,146 @@ function selectShard<T>(items: T[], index: number, total: number): T[] {
9291
9392const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests' ;
9493
94+ function resolveGlobalCliScriptsDir ( casesDir : string ) : string {
95+ const candidates = [
96+ // `packages/cli/snap-tests-global` -> `packages/cli/dist`
97+ path . join ( path . dirname ( casesDir ) , 'dist' ) ,
98+ // Fallback for the common `pnpm -F vite-plus snap-test-global` cwd.
99+ path . resolve ( 'dist' ) ,
100+ ] ;
101+
102+ const scriptsDir = candidates . find ( ( dir ) => fs . existsSync ( path . join ( dir , 'bin.js' ) ) ) ;
103+ if ( ! scriptsDir ) {
104+ throw new Error (
105+ `Unable to find built Vite+ CLI scripts for global snap tests. Tried:\n${ candidates
106+ . map ( ( dir ) => `- ${ dir } ` )
107+ . join ( '\n' ) } `,
108+ ) ;
109+ }
110+
111+ return scriptsDir ;
112+ }
113+
114+ function resolveRepoRoot ( casesDir : string ) : string {
115+ return path . resolve ( path . dirname ( casesDir ) , '..' , '..' ) ;
116+ }
117+
118+ function resolveGlobalCliBinary ( binDir : string ) : string {
119+ const binaryName = process . platform === 'win32' ? 'vp.exe' : 'vp' ;
120+ const binaryPath = path . join ( path . resolve ( expandHome ( binDir ) ) , binaryName ) ;
121+ if ( ! fs . existsSync ( binaryPath ) ) {
122+ throw new Error ( `Unable to find global snap test vp binary at ${ binaryPath } ` ) ;
123+ }
124+
125+ return fs . realpathSync ( binaryPath ) ;
126+ }
127+
128+ function resolveBuiltGlobalCliBinary ( casesDir : string ) : string {
129+ const binaryName = process . platform === 'win32' ? 'vp.exe' : 'vp' ;
130+ const repoRoot = resolveRepoRoot ( casesDir ) ;
131+ const candidates = [
132+ path . join ( repoRoot , 'target' , 'release' , binaryName ) ,
133+ path . join ( repoRoot , 'target' , 'debug' , binaryName ) ,
134+ ] ;
135+ const binaryPath = candidates . find ( ( candidate ) => fs . existsSync ( candidate ) ) ;
136+ if ( ! binaryPath ) {
137+ throw new Error (
138+ `Unable to find built Vite+ global CLI binary for global snap tests. Tried:\n${ candidates
139+ . map ( ( candidate ) => `- ${ candidate } ` )
140+ . join ( '\n' ) } \nRun \`cargo build -p vite_global_cli --release\` before snap-test-global.`,
141+ ) ;
142+ }
143+
144+ return fs . realpathSync ( binaryPath ) ;
145+ }
146+
147+ function newestMtimeMs ( filePath : string ) : number {
148+ const stats = fs . statSync ( filePath ) ;
149+ if ( ! stats . isDirectory ( ) ) {
150+ return stats . mtimeMs ;
151+ }
152+
153+ return fs
154+ . readdirSync ( filePath )
155+ . reduce (
156+ ( newest , entry ) => Math . max ( newest , newestMtimeMs ( path . join ( filePath , entry ) ) ) ,
157+ stats . mtimeMs ,
158+ ) ;
159+ }
160+
161+ function fileContentsEqual ( a : string , b : string ) : boolean {
162+ return fs . readFileSync ( a ) . equals ( fs . readFileSync ( b ) ) ;
163+ }
164+
165+ function assertGlobalCliBinaryMatchesCheckout ( binDir : string , casesDir : string ) : void {
166+ const repoRoot = resolveRepoRoot ( casesDir ) ;
167+ const builtBinary = resolveBuiltGlobalCliBinary ( casesDir ) ;
168+ const sourcePaths = [
169+ path . join ( repoRoot , 'Cargo.toml' ) ,
170+ path . join ( repoRoot , 'Cargo.lock' ) ,
171+ path . join ( repoRoot , 'crates' , 'vite_global_cli' , 'src' ) ,
172+ path . join ( repoRoot , 'crates' , 'vite_shared' , 'src' ) ,
173+ ] ;
174+ const newestSourceMtime = Math . max ( ...sourcePaths . map ( newestMtimeMs ) ) ;
175+ if ( fs . statSync ( builtBinary ) . mtimeMs + 1000 < newestSourceMtime ) {
176+ throw new Error (
177+ `Built Vite+ global CLI binary is older than the current checkout: ${ builtBinary } \n` +
178+ 'Run `cargo build -p vite_global_cli --release` before snap-test-global.' ,
179+ ) ;
180+ }
181+
182+ const globalBinary = resolveGlobalCliBinary ( binDir ) ;
183+ if ( fileContentsEqual ( globalBinary , builtBinary ) ) {
184+ return ;
185+ }
186+
187+ throw new Error (
188+ `Global snap tests would use a stale vp binary from ${ globalBinary } .\n` +
189+ `Expected it to match the current checkout build at ${ builtBinary } .\n` +
190+ 'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.' ,
191+ ) ;
192+ }
193+
194+ function replaceInstalledCheckoutPackages ( rootDir : string , repoRoot : string ) : void {
195+ const stack = [ rootDir ] ;
196+ const symlinkType = process . platform === 'win32' ? 'junction' : 'dir' ;
197+ const replacements = new Map ( [
198+ [ 'node_modules/vite-plus' , path . join ( repoRoot , 'packages' , 'cli' ) ] ,
199+ [ 'node_modules/vite' , path . join ( repoRoot , 'packages' , 'core' ) ] ,
200+ [ 'node_modules/vitest' , path . join ( repoRoot , 'packages' , 'test' ) ] ,
201+ [ 'node_modules/@voidzero-dev/vite-plus-core' , path . join ( repoRoot , 'packages' , 'core' ) ] ,
202+ [ 'node_modules/@voidzero-dev/vite-plus-test' , path . join ( repoRoot , 'packages' , 'test' ) ] ,
203+ ] ) ;
204+
205+ while ( stack . length > 0 ) {
206+ const dir = stack . pop ( ) ! ;
207+ for ( const [ relativePackagePath , checkoutPackageDir ] of replacements ) {
208+ const candidate = path . join ( dir , relativePackagePath ) ;
209+ if ( fs . existsSync ( candidate ) && fs . realpathSync ( candidate ) !== checkoutPackageDir ) {
210+ fs . rmSync ( candidate , { recursive : true , force : true } ) ;
211+ fs . symlinkSync ( checkoutPackageDir , candidate , symlinkType ) ;
212+ }
213+ }
214+
215+ const isNodeModulesPath = dir . split ( path . sep ) . includes ( 'node_modules' ) ;
216+ const isPnpmStorePath = dir . split ( path . sep ) . includes ( '.pnpm' ) ;
217+ for ( const entry of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
218+ if ( ! entry . isDirectory ( ) || entry . name === '.git' || entry . name === '.bin' ) {
219+ continue ;
220+ }
221+ if (
222+ isNodeModulesPath &&
223+ ! isPnpmStorePath &&
224+ entry . name !== '.pnpm' &&
225+ entry . name !== '@voidzero-dev'
226+ ) {
227+ continue ;
228+ }
229+ stack . push ( path . join ( dir , entry . name ) ) ;
230+ }
231+ }
232+ }
233+
95234export async function snapTest ( ) {
96235 const { positionals, values } = parseArgs ( {
97236 allowPositionals : true ,
@@ -203,13 +342,18 @@ export async function snapTest() {
203342 const selectedCases = shard
204343 ? selectShard ( validCaseNames , shard . index , shard . total )
205344 : validCaseNames ;
345+ const globalCliScriptsDir = values [ 'bin-dir' ] ? resolveGlobalCliScriptsDir ( casesDir ) : undefined ;
346+ if ( values [ 'bin-dir' ] ) {
347+ assertGlobalCliBinaryMatchesCheckout ( values [ 'bin-dir' ] , casesDir ) ;
348+ }
206349
207350 const serialTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
208351 const parallelTasks : ( ( ) => Promise < void > ) [ ] = [ ] ;
209352 for ( const caseName of selectedCases ) {
210353 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' ] ) ;
354+ const steps : Steps = JSON . parse ( fs . readFileSync ( stepsPath , 'utf-8' ) ) ;
355+ const task = ( ) =>
356+ runTestCase ( caseName , tempTmpDir , casesDir , values [ 'bin-dir' ] , globalCliScriptsDir ) ;
213357 if ( steps . serial ) {
214358 serialTasks . push ( task ) ;
215359 } else {
@@ -258,6 +402,11 @@ interface Steps {
258402 ignoredPlatforms ?: ( string | PlatformFilter ) [ ] ;
259403 env : Record < string , string > ;
260404 commands : ( string | Command ) [ ] ;
405+ /**
406+ * If true, installed Vite+ packages in the test project are relinked to the
407+ * current checkout after each successful command.
408+ */
409+ linkCheckoutPackages ?: boolean ;
261410 /**
262411 * Commands to run after the test completes, regardless of success or failure.
263412 * Useful for cleanup tasks like killing background processes.
@@ -322,7 +471,13 @@ function shouldSkipPlatform(ignoredPlatforms: (string | PlatformFilter)[]): bool
322471 return false ;
323472}
324473
325- async function runTestCase ( name : string , tempTmpDir : string , casesDir : string , binDir ?: string ) {
474+ async function runTestCase (
475+ name : string ,
476+ tempTmpDir : string ,
477+ casesDir : string ,
478+ binDir ?: string ,
479+ globalCliScriptsDir ?: string ,
480+ ) {
326481 const steps : Steps = JSON . parse (
327482 await fsPromises . readFile ( `${ casesDir } /${ name } /steps.json` , 'utf-8' ) ,
328483 ) ;
@@ -362,6 +517,9 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
362517 VP_SKIP_INSTALL : '1' ,
363518 // make sure npm install global packages to the temporary directory
364519 NPM_CONFIG_PREFIX : path . join ( tempTmpDir , NPM_GLOBAL_PREFIX_DIR ) ,
520+ // Global CLI snap tests execute the Rust binary from --bin-dir, but the JS
521+ // entry should come from this checkout instead of a stale ~/.vite-plus install.
522+ ...( globalCliScriptsDir ? { VITE_GLOBAL_CLI_JS_SCRIPTS_DIR : globalCliScriptsDir } : { } ) ,
365523
366524 // A test case can override/unset environment variables above.
367525 // For example, VP_CLI_TEST/CI can be unset to test the real-world outputs.
@@ -409,7 +567,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
409567 // it seems not to have stable ordering of stdout/stderr chunks.
410568 // To ensure stable ordering, we redirect outputs to a file instead.
411569 const outputStreamPath = path . join ( caseTmpDir , 'output.log' ) ;
412- const outputStream = await open ( outputStreamPath , 'w' ) ;
570+ const outputStream = await fsPromises . open ( outputStreamPath , 'w' ) ;
413571
414572 const exitCode = await Promise . race ( [
415573 execute ( stripComments ( cmd . command ) , [ ] , {
@@ -431,8 +589,11 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
431589 ] ) ;
432590
433591 await outputStream . close ( ) ;
592+ if ( exitCode === 0 && globalCliScriptsDir && steps . linkCheckoutPackages ) {
593+ replaceInstalledCheckoutPackages ( caseTmpDir , resolveRepoRoot ( casesDir ) ) ;
594+ }
434595
435- let output = readFileSync ( outputStreamPath , 'utf-8' ) ;
596+ let output = fs . readFileSync ( outputStreamPath , 'utf-8' ) ;
436597
437598 let commandLine = `> ${ cmd . command } ` ;
438599 if ( exitCode !== 0 ) {
0 commit comments