From 32f9456160fd44b2be993591d58f21f493a7c2ad Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Sat, 11 Jul 2026 00:04:22 +0900 Subject: [PATCH 1/2] test(snapshots): remove legacy snapshot infrastructure Drop the obsolete runner, migration tooling, scripts, dependencies, and configuration now that CLI snapshots run exclusively through the PTY suite. --- .gitignore | 3 - .typos.toml | 1 - .vscode/settings.json | 2 +- package.json | 2 +- packages/cli/package.json | 3 - packages/tools/package.json | 8 - .../__snapshots__/utils.spec.ts.snap | 138 --- .../src/__tests__/migrate-snap-tests.spec.ts | 136 --- packages/tools/src/__tests__/utils.spec.ts | 441 ---------- packages/tools/src/index.ts | 10 +- packages/tools/src/migrate-snap-tests.ts | 682 --------------- packages/tools/src/snap-test.ts | 784 ------------------ packages/tools/src/utils.ts | 311 ------- pnpm-lock.yaml | 140 +--- pnpm-workspace.yaml | 2 - tsconfig.json | 2 - vite.config.ts | 11 - 17 files changed, 4 insertions(+), 2672 deletions(-) delete mode 100644 packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap delete mode 100644 packages/tools/src/__tests__/migrate-snap-tests.spec.ts delete mode 100644 packages/tools/src/__tests__/utils.spec.ts delete mode 100644 packages/tools/src/migrate-snap-tests.ts delete mode 100755 packages/tools/src/snap-test.ts delete mode 100644 packages/tools/src/utils.ts diff --git a/.gitignore b/.gitignore index d559645c9e..1defa1dc72 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,3 @@ rolldown-vite vite /crates/vite_global_cli/vp .void/ - -# Generated by `tool migrate-snap-tests`; batch PRs summarize it in the description instead -crates/vite_cli_snapshots/tests/cli_snapshots/MIGRATION-REPORT.md diff --git a/.typos.toml b/.typos.toml index cbbddf7b71..7fd8ba8179 100644 --- a/.typos.toml +++ b/.typos.toml @@ -13,7 +13,6 @@ PnP = "PnP" [files] extend-exclude = [ - "**/snap-tests/**/snap.txt", "crates/fspy_detours_sys/detours", "crates/fspy_detours_sys/src/generated_bindings.rs", "packages/cli/src/oxfmt-config.ts", diff --git a/.vscode/settings.json b/.vscode/settings.json index c18ac042f5..e3d1f0ffbb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,5 @@ "typescript.reportStyleChecksAsWarnings": false, "typescript.updateImportsOnFileMove.enabled": "always", "typescript.experimental.useTsgo": true, - "vitest.configSearchPatternExclude": "{**/node_modules/**,**/vendor/**,**/.*/**,**/*.d.ts,{**/packages/cli/snap-tests*/**,**/crates/vite_cli_snapshots/tests/cli_snapshots/**}}" + "vitest.configSearchPatternExclude": "{**/node_modules/**,**/vendor/**,**/.*/**,**/*.d.ts,**/crates/vite_cli_snapshots/tests/cli_snapshots/**}" } diff --git a/package.json b/package.json index 6c4bc317c5..a93ff0dadb 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "local-registry:kill": "node packages/tools/src/local-npm-registry.ts --kill", "tsgo": "tsgo -b tsconfig.json", "lint": "vp lint --type-aware --type-check --threads 4", - "test": "vp test run && pnpm -r snap-test", + "test": "vp test run", "snapshot-test": "just snapshot-test", "fmt": "vp fmt", "test:unit": "vp test run", diff --git a/packages/cli/package.json b/packages/cli/package.json index 4c637d01bd..03df99b11d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -355,9 +355,6 @@ "build": "oxnode -C dev ./build.ts", "build-ts": "oxnode -C dev ./build.ts --skip-native", "build-native": "oxnode -C dev ./build.ts --skip-ts", - "snap-test": "pnpm snap-test-local && pnpm snap-test-global", - "snap-test-local": "tool snap-test", - "snap-test-global": "tool snap-test --dir snap-tests-global --bin-dir ~/.vite-plus/bin", "publish-native": "node ./publish-native-addons.ts", "test": "vitest run" }, diff --git a/packages/tools/package.json b/packages/tools/package.json index a7751d531e..57fa69644b 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -7,18 +7,10 @@ "tool": "./src/bin.js" }, "type": "module", - "scripts": { - "snap-test": "tool snap-test" - }, - "dependencies": { - "@yarnpkg/fslib": "catalog:", - "@yarnpkg/shell": "catalog:" - }, "devDependencies": { "@oxc-node/cli": "catalog:", "@oxc-node/core": "catalog:", "@types/semver": "catalog:", - "minimatch": "catalog:", "semver": "catalog:", "vitest": "catalog:", "yaml": "catalog:" diff --git a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap deleted file mode 100644 index 29545b8696..0000000000 --- a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap +++ /dev/null @@ -1,138 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`replaceUnstableOutput() > replace date 1`] = ` -"Start at -" -`; - -exports[`replaceUnstableOutput() > replace full datetime (YYYY-MM-DD HH:MM:SS) 1`] = ` -"Installed: - Created: - Updated: " -`; - -exports[`replaceUnstableOutput() > replace hash values 1`] = ` -"npm notice shasum: -npm notice integrity: sha512- -"shasum": "", -"integrity": "sha512-"," -`; - -exports[`replaceUnstableOutput() > replace ignore npm audited packages log 1`] = ` -"removed 1 package in ms -up to date in ms -added 1 package in ms -added 3 packages in ms -Done in ms" -`; - -exports[`replaceUnstableOutput() > replace ignore npm notice access token expired or revoked warning log 1`] = ` -"line 1 -line 2 -line 3" -`; - -exports[`replaceUnstableOutput() > replace ignore npm registry domain 1`] = ` -"https://registry./testnpm2 -https://registry./debug -https://registry./testnpm2/-/testnpm2-1.0.0.tgz -"resolved": "https://registry./testnpm2/-/testnpm2-1.0.0.tgz", -"resolved": "https://registry./testnpm2/-/testnpm2-1.0.0.tgz"," -`; - -exports[`replaceUnstableOutput() > replace ignore npm warn exec The following package was not found and will be installed: cowsay@ warning log 1`] = `"hello world"`; - -exports[`replaceUnstableOutput() > replace ignore pnpm request warning log 1`] = ` -"Foo bar -Packages:" -`; - -exports[`replaceUnstableOutput() > replace ignore tarball download average speed warning log 1`] = `"Progress: resolved"`; - -exports[`replaceUnstableOutput() > replace pnpm progress plus markers with 1`] = ` -"Scope: all workspace projects -Packages: + -+ -Progress: resolved , reused , downloaded , added , done" -`; - -exports[`replaceUnstableOutput() > replace pnpm registry request error warning log 1`] = `"Progress: resolved"`; - -exports[`replaceUnstableOutput() > replace tsdown output 1`] = ` -"ℹ tsdown v powered by rolldown v -ℹ entry: src/index.ts -ℹ Build start -ℹ dist/index.js kB │ gzip: kB -ℹ 1 files, total: kB -✔ Build complete in ms" -`; - -exports[`replaceUnstableOutput() > replace unstable cwd 1`] = `"/foo.txt"`; - -exports[`replaceUnstableOutput() > replace unstable pnpm install output 1`] = ` -"Scope: all workspace projects -Packages: + -+ -Progress: resolved , reused , downloaded , added , done - -devDependencies: -+ vite-plus -+ vitest " -`; - -exports[`replaceUnstableOutput() > replace unstable pnpm install output 2`] = ` -"Scope: all workspace projects -Lockfile is up to date, resolution step is skipped -Already up to date - -╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Ignored build scripts: esbuild. │ -│ Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. │ -│ │ -╰────────────────────────────────────────────────────────────────────────────────────────────╯ - -Done in ms using pnpm v" -`; - -exports[`replaceUnstableOutput() > replace unstable semver version 1`] = ` -"foo v - v - v - - - -tsdown/ -vitest/ -foo/v -foo@ -bar@v" -`; - -exports[`replaceUnstableOutput() > replace unstable tmpdir with realpath 1`] = ` -"/foo.txt -/../other/bar.txt" -`; - -exports[`replaceUnstableOutput() > replace unstable vite-plus hash version 1`] = ` -""vite-plus": "^0.0.0-" -"vite-plus-core": "^0.0.0-"" -`; - -exports[`replaceUnstableOutput() > replace vite-plus home paths 1`] = ` -"/js_runtime/node/v/bin/node -/packages/cowsay/lib/node_modules/cowsay/./cli.js - -/bin" -`; - -exports[`replaceUnstableOutput() > replace yarn YN0000: └ Completed with duration to empty string 1`] = ` -"➤ YN0000: └ Completed -➤ YN0000: └ Completed -➤ YN0000: └ Completed" -`; - -exports[`replaceUnstableOutput() > replace yarn YN0013 1`] = ` -"➤ YN0000: ┌ Fetch step -➤ YN0000: └ Completed" -`; diff --git a/packages/tools/src/__tests__/migrate-snap-tests.spec.ts b/packages/tools/src/__tests__/migrate-snap-tests.spec.ts deleted file mode 100644 index 5db3cfbcf4..0000000000 --- a/packages/tools/src/__tests__/migrate-snap-tests.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - fixtureName, - translateCommand, - type NewStep, - type TranslationContext, -} from '../migrate-snap-tests.ts'; - -function ctx(): TranslationContext { - return { todos: [], notes: [], localRegistry: false, needsFreshRuntime: false }; -} - -function argvs(steps: NewStep[]): string[][] { - return steps.map((s) => s.argv); -} - -function isTodo(steps: NewStep[]): boolean { - return steps.length === 1 && steps[0].comment?.startsWith('TODO(migrate)') === true; -} - -describe('fixtureName', () => { - it('normalizes every invalid identifier character', () => { - expect(fixtureName('migration-not-supported-npm8.2')).toBe('migration_not_supported_npm8_2'); - expect(fixtureName('check-pass')).toBe('check_pass'); - }); -}); - -describe('translateCommand', () => { - it('drops comment-only commands with a report note', () => { - const context = ctx(); - expect(translateCommand('# tests below assert the cache state', context)).toEqual([]); - expect(context.notes).toHaveLength(1); - expect(context.todos).toHaveLength(0); - }); - - it('maps test expressions to stat-file asserts, keeping exit semantics', () => { - expect(argvs(translateCommand('test ! -f .nvmrc', ctx()))).toEqual([ - ['vpt', 'stat-file', '.nvmrc', '--assert-not', 'file'], - ]); - expect(argvs(translateCommand('test -d dist', ctx()))).toEqual([ - ['vpt', 'stat-file', 'dist', '--assert', 'dir'], - ]); - expect(argvs(translateCommand('test -e marker', ctx()))).toEqual([ - ['vpt', 'stat-file', 'marker', '--assert-not', 'missing'], - ]); - }); - - it('keeps guard chains short-circuiting via the failing assert', () => { - // `test -f marker && vp run build`: the guard step fails on a missing - // marker and the line-boundary flow skips the guarded command. - const steps = translateCommand('test -f marker && vp run build', ctx()); - expect(argvs(steps)).toEqual([ - ['vpt', 'stat-file', 'marker', '--assert', 'file'], - ['vp', 'run', 'build'], - ]); - expect(steps[0].continueOnFailure).toBeUndefined(); - expect(steps[1].continueOnFailure).toBe(true); - }); - - it('passes octal and +x chmod through, TODOs other symbolic modes', () => { - expect(argvs(translateCommand('chmod 755 hook.mjs', ctx()))).toEqual([ - ['vpt', 'chmod', '755', 'hook.mjs'], - ]); - expect(argvs(translateCommand('chmod +x hook.mjs', ctx()))).toEqual([ - ['vpt', 'chmod', '+x', 'hook.mjs'], - ]); - expect(isTodo(translateCommand('chmod u+rw hook.mjs', ctx()))).toBe(true); - }); - - it('TODOs glob arguments for shell-less vpt file verbs', () => { - expect(isTodo(translateCommand('rm -rf *.tgz', ctx()))).toBe(true); - expect(isTodo(translateCommand('cat dist/*.js', ctx()))).toBe(true); - }); - - it('appends the newline echo would have written to redirected files', () => { - const steps = translateCommand('echo hello > out.txt', ctx()); - expect(argvs(steps)).toEqual([['vpt', 'write-file', 'out.txt', 'hello\n']]); - }); - - it('keeps printf redirects exact, TODOs escape sequences', () => { - expect(argvs(translateCommand("printf 'plain text' > out.txt", ctx()))).toEqual([ - ['vpt', 'write-file', 'out.txt', 'plain text'], - ]); - expect(isTodo(translateCommand("printf 'a\\nb' > out.txt", ctx()))).toBe(true); - }); - - it('accepts dot-path json-edit and TODOs legacy expression syntax', () => { - expect( - argvs(translateCommand("json-edit package.json scripts.build 'vp build'", ctx())), - ).toEqual([['vpt', 'json-edit', 'package.json', 'scripts.build', 'vp build']]); - expect(isTodo(translateCommand("json-edit package.json '_.dependencies = {}'", ctx()))).toBe( - true, - ); - }); - - it('TODOs env assignments that need shell expansion', () => { - expect( - isTodo(translateCommand('NPM_CONFIG_PREFIX=$(pwd)/prefix npm install -g x', ctx())), - ).toBe(true); - expect(isTodo(translateCommand('PATH=$PATH vp check', ctx()))).toBe(true); - }); - - it('marks only the line-final step continue-on-failure', () => { - // Legacy lines were independent; && within a line short-circuited. - const steps = translateCommand('vp add x && cat package.json', ctx()); - expect(steps).toHaveLength(2); - expect(steps[0].continueOnFailure).toBeUndefined(); - expect(steps[1].continueOnFailure).toBe(true); - }); - - it('TODOs ls flags that list-dir does not replicate', () => { - expect(isTodo(translateCommand('ls -la node_modules', ctx()))).toBe(true); - }); - - it('flags runtime-provisioning commands for seed-runtime = false', () => { - const context = ctx(); - translateCommand('vp env install 22', context); - expect(context.needsFreshRuntime).toBe(true); - const plain = ctx(); - translateCommand('vp env list', plain); - expect(plain.needsFreshRuntime).toBe(false); - }); - - it('turns leading cd chains into step cwd', () => { - const steps = translateCommand('cd packages/web && vp run build', ctx()); - expect(steps).toHaveLength(1); - expect(steps[0].argv).toEqual(['vp', 'run', 'build']); - expect(steps[0].cwd).toBe('packages/web'); - }); - - it('TODOs cd forms it cannot represent', () => { - expect(isTodo(translateCommand('cd /tmp && vp check', ctx()))).toBe(true); - expect(isTodo(translateCommand('cd $DIR && vp check', ctx()))).toBe(true); - }); -}); diff --git a/packages/tools/src/__tests__/utils.spec.ts b/packages/tools/src/__tests__/utils.spec.ts deleted file mode 100644 index f5cb16b8f4..0000000000 --- a/packages/tools/src/__tests__/utils.spec.ts +++ /dev/null @@ -1,441 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import fs from 'node:fs'; -import { homedir, tmpdir } from 'node:os'; -import path from 'node:path'; - -import { describe, expect, test } from 'vitest'; - -import { isPassThroughEnv, replaceUnstableOutput } from '../utils.ts'; - -describe('replaceUnstableOutput()', () => { - test('strip ANSI escape sequences', () => { - const output = '\u001b[1m\u001b[2mnote:\u001b[0m\u001b[0m yarn@2+ uses upgrade-interactive'; - expect(replaceUnstableOutput(output)).toBe('note: yarn@2+ uses upgrade-interactive'); - }); - - test('normalize CRLF line endings', () => { - const output = 'line 1\r\nline 2\r\nline 3\r'; - expect(replaceUnstableOutput(output)).toBe('line 1\nline 2\nline 3'); - }); - - test('strip clack spinner frames', () => { - const output = '│\n◒ Preparing local Git repository...\n◇ Prepared local Git repository\n'; - expect(replaceUnstableOutput(output)).toBe('│\n◇ Prepared local Git repository\n'); - // a frame at end-of-output without a trailing newline is stripped too - expect(replaceUnstableOutput('text\n◐ Working...')).toBe('text\n'); - }); - - test('replace unstable semver version', () => { - const output = ` -foo v1.0.0 - v1.0.0-beta.1 - v1.0.0-beta.1+build.1 - 1.0.0 - 1.0.0-beta.1 - 1.0.0-beta.1+build.1 -tsdown/0.15.1 -vitest/3.2.4 -foo/v100.1.1000 -foo@1.0.0 -bar@v1.0.0 - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace devEngines.packageManager pinned versions', () => { - // prerelease identifiers with hyphens and build metadata are normalized too - for (const version of ['11.5.1', '11.5.1-rc-1', '11.5.1+sha.abc']) { - const json = [ - '{', - ' "devEngines": {', - ' "packageManager": {', - ' "name": "pnpm",', - ` "version": "${version}",`, - ' "onFail": "download"', - ' }', - ' }', - '}', - ].join('\n'); - expect(replaceUnstableOutput(json)).toContain('"version": ""'); - } - }); - - test('replace vitest-family JSON version pins', () => { - // these track the bundled VITEST_VERSION and bump on every daily upgrade-deps run, - // so masking keeps catalog / devDependency snaps stable across bumps - const output = [ - ' "catalog": {', - ' "vite": "npm:@voidzero-dev/vite-plus-core@latest",', - ' "vitest": "4.1.9",', - ' "vite-plus": "latest"', - ' },', - ' "devDependencies": {', - ' "@vitest/browser-playwright": "4.1.9",', - ' "@vitest/browser-webdriverio": "4.1.9-beta.2"', - ' }', - ].join('\n'); - const result = replaceUnstableOutput(output); - expect(result).toContain('"vitest": ""'); - expect(result).toContain('"@vitest/browser-playwright": ""'); - expect(result).toContain('"@vitest/browser-webdriverio": ""'); - // range specs and the vite-plus alias must stay untouched - expect(result).toContain('"vite": "npm:@voidzero-dev/vite-plus-core@latest"'); - expect(result).toContain('"vite-plus": "latest"'); - }); - - test('keeps user-owned @vitest/coverage-* provider pins visible', () => { - // coverage providers are project-installed peers vite-plus never pins; the - // runtime guard fail-fasts on version skew, so the exact version must stay - // visible in snapshots rather than collapse to - const output = [ - ' "@vitest/coverage-v8": "4.1.8",', - ' "@vitest/coverage-istanbul": "4.1.8"', - ].join('\n'); - expect(replaceUnstableOutput(output)).toBe(output); - }); - - test('keeps non-exact vitest specs (catalog: / ranges) unmasked', () => { - const output = [' "vitest": "catalog:",', ' "vitest": "^4.0.0"'].join('\n'); - expect(replaceUnstableOutput(output)).toBe(output); - }); - - test('keeps unsupported pre-4 vitest pins visible (migration-refusal echoes)', () => { - // The migrator refuses to manage vitest < 4, so a pre-4 JSON pin is always a - // user's OWN untouched package.json echoed by an unsupported-version fixture. - // Masking it would hide a regression that rewrote the dependency before failing. - expect(replaceUnstableOutput(' "vitest": "3.2.4"')).toBe(' "vitest": "3.2.4"'); - expect(replaceUnstableOutput(' "@vitest/spy": "3.0.0"')).toBe(' "@vitest/spy": "3.0.0"'); - // future double-digit majors still mask - expect(replaceUnstableOutput(' "vitest": "10.0.1"')).toBe(' "vitest": ""'); - }); - - test('replace date', () => { - const output = ` -Start at 15:01:23 -15:01:23 - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace full datetime (YYYY-MM-DD HH:MM:SS)', () => { - const output = ` - Installed: 2026-02-04 15:30:45 - Created: 2024-01-15 10:30:00 - Updated: 1999-12-31 23:59:59 - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace parenthesized thread counts', () => { - const output = ` -pass: All 3 files are correctly formatted (88ms, 2 threads) -pass: Found no warnings or lint errors in 1 file (ms, 16 threads) - `; - expect(replaceUnstableOutput(output.trim())).toBe( - [ - 'pass: All 3 files are correctly formatted (ms, threads)', - 'pass: Found no warnings or lint errors in 1 file (ms, threads)', - ].join('\n'), - ); - }); - - test('replace unstable pnpm install output', () => { - const outputs = [ - ` -Scope: all 6 workspace projects -Packages: +312 -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Progress: resolved 1, reused 0, downloaded 0, added 0 -Progress: resolved 316, reused 316, downloaded 0, added 315 -WARN  Skip adding vite to the default catalog because it already exists as npm:vite-plus. Please use \`pnpm update\` to update the catalogs. -WARN  Skip adding vitest to the default catalog because it already exists as beta. Please use \`pnpm update\` to update the catalogs. -Progress: resolved 316, reused 316, downloaded 0, added 316, done - -devDependencies: -+ vite-plus 0.0.0-8a4f4936e0eca32dd57e1a503c2b09745953344d -+ vitest 3.2.4 - `, - ` -Scope: all 2 workspace projects -Lockfile is up to date, resolution step is skipped -Already up to date - -╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Ignored build scripts: esbuild. │ -│ Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. │ -│ │ -╰────────────────────────────────────────────────────────────────────────────────────────────╯ - -Done in 171ms using pnpm v10.16.1 - `, - ]; - for (const output of outputs) { - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - } - }); - - test('strip pnpm supply-chain verification status line', () => { - const output = ` -> vp add testnpm2 -D -✓ Lockfile passes supply-chain policies (verified 123ms ago) -Packages: +1 - `; - expect(replaceUnstableOutput(output.trim())).not.toContain('supply-chain policies'); - }); - - test.skipIf(process.platform === 'win32')('replace unstable cwd', () => { - const cwd = tmpdir(); - const output = path.join(cwd, 'foo.txt'); - expect(replaceUnstableOutput(output.trim(), cwd)).toMatchSnapshot(); - }); - - test.skipIf(process.platform === 'win32')('replace unstable tmpdir with realpath', () => { - const tmp = fs.realpathSync(tmpdir()); - const cwd = path.join(tmp, `vite-plus-unittest-${randomUUID()}`); - const output = `${path.join(cwd, 'foo.txt')}\n${path.join(cwd, '../other/bar.txt')}`; - expect(replaceUnstableOutput(output.trim(), cwd)).toMatchSnapshot(); - }); - - describe.skipIf(process.platform !== 'win32')('Windows cwd replacement', () => { - test('mixed-separator cwd matches all-backslash output', () => { - const cwd = - 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp/vite-plus-test-abc/command-staged-broken-config'; - const output = - 'failed to load config from C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\vite-plus-test-abc\\command-staged-broken-config\\vite.config.ts'; - expect(replaceUnstableOutput(output, cwd)).toBe( - 'failed to load config from /vite.config.ts', - ); - }); - - test('mixed-separator cwd matches all-forward-slash output', () => { - const cwd = - 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp/vite-plus-test-abc/vite-plugins-async-test'; - const output = - ' RUN C:/Users/RUNNER~1/AppData/Local/Temp/vite-plus-test-abc/vite-plugins-async-test\n'; - expect(replaceUnstableOutput(output, cwd)).toBe(' RUN \n'); - }); - - test('all-backslash cwd matches all-backslash output', () => { - const cwd = 'C:\\Users\\runner\\project'; - const output = 'error in C:\\Users\\runner\\project\\src\\main.ts'; - expect(replaceUnstableOutput(output, cwd)).toBe('error in /src/main.ts'); - }); - - test('cwd at end of string without trailing separator', () => { - const cwd = 'C:\\Users\\runner\\project'; - const output = 'path is C:\\Users\\runner\\project'; - expect(replaceUnstableOutput(output, cwd)).toBe('path is '); - }); - - test('parent directory replacement with backslash paths', () => { - const cwd = 'C:\\Users\\RUNNER~1\\Temp/vite-plus-test/my-test'; - const output = 'found C:\\Users\\RUNNER~1\\Temp\\vite-plus-test\\other\\file.ts'; - expect(replaceUnstableOutput(output, cwd)).toBe('found /../other/file.ts'); - }); - }); - - test('replace tsdown output', () => { - const output = ` -ℹ tsdown v0.15.1 powered by rolldown v0.15.1 -ℹ entry: src/index.ts -ℹ Build start -ℹ dist/index.js 0.15 kB │ gzip: 0.12 kB -ℹ 1 files, total: 0.15 kB -✔ Build complete in 100ms - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace yarn YN0013', () => { - const output = ` -➤ YN0000: ┌ Fetch step -➤ YN0013: │ A package was added to the project (+ 0.7 KiB). -➤ YN0000: └ Completed - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace yarn YN0000: └ Completed with duration to empty string', () => { - const output = ` -➤ YN0000: └ Completed in 100ms -➤ YN0000: └ Completed in 100ms 200ms -➤ YN0000: └ Completed - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace ignore pnpm request warning log', () => { - const output = ` -Foo bar - WARN  Request took ms: https://registry.npmjs.org/testnpm2 -Packages: - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace ignore npm audited packages log', () => { - const output = ` -removed 1 package, and audited 3 packages in 700ms -up to date, audited 4 packages in 11ms -added 1 package, and audited 3 packages in 700ms -added 3 packages, and audited 4 packages in 100ms - -found 0 vulnerabilities -Done in 1000ms - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace ignore npm registry domain', () => { - const output = ` -https://registry.npmjs.org/testnpm2 -https://registry.yarnpkg.com/debug -https://registry.yarnpkg.com/testnpm2/-/testnpm2-1.0.0.tgz -"resolved": "https://registry.yarnpkg.com/testnpm2/-/testnpm2-1.0.0.tgz", -"resolved": "https://registry.npmjs.org/testnpm2/-/testnpm2-1.0.0.tgz", - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace pnpm registry request error warning log', () => { - const output = ` - WARN  GET https://registry.npmjs.org/test-vite-plus-install error (ECONNRESET). Will retry in 10 seconds. 2 retries left. -[WARN] GET https://registry.npmjs.org/testnpm2 error (ECONNRESET). Will retry in 10 seconds. 2 retries left. -Progress: resolved -`; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace ignore tarball download average speed warning log', () => { - const output = ` - WARN  Tarball download average speed 29 KiB/s (size 56 KiB) is below 50 KiB/s: https://registry.npmjs.org/qs/-/qs-6.14.0.tgz (GET) - WARN  Tarball download average speed 34 KiB/s (size 347 KiB) is below 50 KiB/s: https://registry.npmjs.org/undici/-/undici-7.16.0.tgz (GET) -Progress: resolved -`; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace hash values', () => { - const output = ` -npm notice shasum: 65c35f9599054722ecde040abd4a19682a723cdc -npm notice integrity: sha512-qugLL42iCblSD[...]Gfk6HJodp2ZOQ== -"shasum": "65c35f9599054722ecde040abd4a19682a723cdc", -"integrity": "sha512-qugLL42iCblSDO0Vwic9xYkKYNtf+MwPW4cQSppKbGtQ/xswl1gXyu/DF5b7I/WbsVi02DJIHGfk6HJodp2ZOQ==", - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace ignore npm notice access token expired or revoked warning log', () => { - const output = ` -line 1 -npm notice Access token expired or revoked. Please try logging in again. -npm notice Access token expired or revoked. Please try logging in again. -line 2 -npm notice Access token expired or revoked. Please try logging in again. -line 3 - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace unstable vite-plus hash version', () => { - const output = ` -"vite-plus": "^0.0.0-aa9f90fe23216b8ad85b0ba4fc1bccb0614afaf0" -"vite-plus-core": "^0.0.0-43b91ac4e4bc63ba78dee8a813806bdbaa7a4378" - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test.skipIf(process.platform === 'win32')('replace vite-plus home paths', () => { - const home = homedir(); - const output = [ - `${home}/.vite-plus/js_runtime/node/v20.18.0/bin/node`, - `${home}/.vite-plus/packages/cowsay#123e4567-e89b-42d3-a456-426614174000/lib/node_modules/cowsay/./cli.js`, - `${home}/.vite-plus`, - `${home}/.vite-plus/bin`, - ].join('\n'); - expect(replaceUnstableOutput(output)).toMatchSnapshot(); - }); - - test('replace ignore npm warn exec The following package was not found and will be installed: cowsay@ warning log', () => { - const output = ` -npm warn exec The following package was not found and will be installed: cowsay@ -npm warn exec The following package was not found and will be installed: cowsay@1.6.0 -hello world - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('replace pnpm progress plus markers with ', () => { - const output = ` -Scope: all 6 workspace projects -Packages: +312 -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Progress: resolved 316, reused 316, downloaded 0, added 316, done - `; - expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); - }); - - test('preserve trailing plus in text like Vite+', () => { - const output = 'Migrate an existing project to Vite+'; - expect(replaceUnstableOutput(output)).toBe('Migrate an existing project to Vite+'); - }); - - test('replace bun build hash after the v banner', () => { - // bun prints lines like `bun pm trust v1.3.11 (af24e281)` where the - // parenthesized hex is a build-commit hash that drifts per release. - // After the leading semver is normalized to `v`, the trailing - // hash should also be normalized to `()` so snaps stay stable. - const output = [ - 'bun pm trust v1.3.11 (af24e281)', - 'bun add v1.3.11 (af24e281)', - 'bun install v1.3.0 (deadbeef)', - 'bun outdated v1.2.21 (0123abcd)', - ].join('\n'); - expect(replaceUnstableOutput(output)).toBe( - [ - 'bun pm trust v ()', - 'bun add v ()', - 'bun install v ()', - 'bun outdated v ()', - ].join('\n'), - ); - }); - - test('only redact hash that follows the v placeholder', () => { - // The redaction is intentionally anchored to the `v` placeholder - // produced by the prior semver normalization step. Unrelated parenthesized - // hex (e.g. SHA fragments in log lines) must not be touched. - const output = [ - 'commit (af24e281) was deployed', - 'changeset id: (cafebabe)', - 'foo v1.0.0 (notahex!)', - ].join('\n'); - expect(replaceUnstableOutput(output)).toBe( - [ - 'commit (af24e281) was deployed', - 'changeset id: (cafebabe)', - 'foo v (notahex!)', - ].join('\n'), - ); - }); - - test('do not redact uppercase hex (bun emits lowercase)', () => { - // Bun's build hashes are lowercase. The regex is case-sensitive to avoid - // accidentally redacting unrelated uppercase parenthesized text. - const output = 'bun pm trust v1.3.11 (AF24E281)'; - expect(replaceUnstableOutput(output)).toBe('bun pm trust v (AF24E281)'); - }); -}); - -describe('isPassThroughEnv()', () => { - test('should return true if env is pass-through', () => { - expect(isPassThroughEnv('NPM_AUTH_TOKEN')).toBe(true); - expect(isPassThroughEnv('PATH')).toBe(true); - }); - - test('should return false if env is not pass-through', () => { - expect(isPassThroughEnv('NODE_ENV')).toBe(false); - expect(isPassThroughEnv('API_URL')).toBe(false); - }); -}); diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 107e825469..28fdec4905 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,14 +1,6 @@ const subcommand = process.argv[2]; switch (subcommand) { - case 'snap-test': - const { snapTest } = await import('./snap-test.ts'); - await snapTest(); - break; - case 'migrate-snap-tests': - const { migrateSnapTests } = await import('./migrate-snap-tests.ts'); - migrateSnapTests(); - break; case 'replace-file-content': const { replaceFileContent } = await import('./replace-file-content.ts'); replaceFileContent(); @@ -48,7 +40,7 @@ switch (subcommand) { default: console.error(`Unknown subcommand: ${subcommand}`); console.error( - 'Available subcommands: snap-test, migrate-snap-tests, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite, local-npm-registry', + 'Available subcommands: replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite, local-npm-registry', ); process.exit(1); } diff --git a/packages/tools/src/migrate-snap-tests.ts b/packages/tools/src/migrate-snap-tests.ts deleted file mode 100644 index 7a78f7c797..0000000000 --- a/packages/tools/src/migrate-snap-tests.ts +++ /dev/null @@ -1,682 +0,0 @@ -/** - * One-click migration of old snap-test cases (steps.json + fixture files) to - * the new PTY snapshot suite (crates/vite_cli_snapshots fixtures with - * snapshots.toml), following the mapping in rfcs/interactive-snapshot-tests.md. - * - * Usage: - * tool migrate-snap-tests packages/cli/snap-tests --vp local [name-filter] [--keep-old] - * tool migrate-snap-tests packages/cli/snap-tests-global --vp global [name-filter] [--keep-old] - * - * Successfully converted case directories are removed from the old tree, so - * every case lives in exactly one tree (git has the history; --keep-old - * defers the removal). Old snap.txt files are not converted; record new - * baselines afterwards with `UPDATE_SNAPSHOTS=1 just snapshot-test ` - * and review them against the deleted snap.txt in `git diff`. - */ -import fs from 'node:fs'; -import path from 'node:path'; -import { parseArgs } from 'node:util'; - -// The legacy steps.json schema comes straight from the old runner, so the -// migrator can never drift from what actually ran. -import type { Steps as OldSteps } from './snap-test.ts'; - -interface NewStep { - argv: string[]; - comment?: string; - cwd?: string; - envs?: [string, string][]; - continueOnFailure?: boolean; - timeout?: number; - snapshot?: boolean; -} - -/** New-runner fixture/case names allow only `[A-Za-z0-9_]`. */ -function fixtureName(caseName: string): string { - return caseName.replaceAll(/[^A-Za-z0-9_]/g, '_'); -} - -interface CaseReport { - name: string; - notes: string[]; - todos: string[]; - /** True when nothing was written (e.g. target fixture already exists). */ - skipped?: boolean; -} - -const OS_MAP: Record = { - win32: 'windows', - darwin: 'macos', - linux: 'linux', -}; - -/** - * True at each index outside single/double quotes: the one quote-state - * scanner behind splitOnAndAnd and extractComment, so quoting rules cannot - * drift between them. - */ -function unquotedMask(line: string): boolean[] { - const mask: boolean[] = []; - let quote: string | null = null; - for (const ch of line) { - if (quote) { - mask.push(false); - if (ch === quote) { - quote = null; - } - } else if (ch === "'" || ch === '"') { - quote = ch; - mask.push(false); - } else { - mask.push(true); - } - } - return mask; -} - -/** Splits a shell line on a top-level `&&`, respecting quotes. */ -function splitOnAndAnd(line: string): string[] { - const mask = unquotedMask(line); - const parts: string[] = []; - let start = 0; - for (let i = 0; i < line.length - 1; i++) { - if (mask[i] && line[i] === '&' && line[i + 1] === '&') { - parts.push(line.slice(start, i)); - start = i + 2; - i++; - } - } - parts.push(line.slice(start)); - return parts.map((p) => p.trim()).filter((p) => p.length > 0); -} - -/** Tokenizes a simple shell command (no operators), handling quotes. */ -function tokenize(command: string): string[] | null { - const tokens: string[] = []; - let current = ''; - let hasCurrent = false; - let i = 0; - while (i < command.length) { - const ch = command[i]; - if (ch === ' ' || ch === '\t') { - if (hasCurrent) { - tokens.push(current); - current = ''; - hasCurrent = false; - } - i++; - continue; - } - if (ch === "'" || ch === '"') { - const end = command.indexOf(ch, i + 1); - if (end === -1) { - return null; - } - current += command.slice(i + 1, end); - hasCurrent = true; - i = end + 1; - continue; - } - current += ch; - hasCurrent = true; - i++; - } - if (hasCurrent) { - tokens.push(current); - } - return tokens; -} - -/** Extracts a trailing ` # comment` (outside quotes) from a command line. */ -function extractComment(line: string): { command: string; comment?: string } { - const trimmed = line.trimStart(); - if (trimmed.startsWith('#')) { - // Comment-only entries are documentation, not commands. - return { command: '', comment: trimmed.slice(1).trim() }; - } - const mask = unquotedMask(line); - for (let i = 1; i < line.length; i++) { - if (mask[i] && line[i] === '#' && (line[i - 1] === ' ' || line[i - 1] === '\t')) { - return { command: line.slice(0, i).trim(), comment: line.slice(i + 1).trim() }; - } - } - return { command: line.trim() }; -} - -// Legacy commands that can be represented directly without shell semantics. -const PASSTHROUGH_PROGRAMS = new Set([ - 'vp', - 'vpr', - 'vpx', - 'vpt', - 'oxfmt', - 'oxlint', - 'node', - 'git', - 'npm', - 'npx', - 'pnpm', - 'yarn', - 'bun', - 'corepack', -]); - -const COREUTILS_MAP: Record = { - cat: 'print-file', - ls: 'list-dir', - touch: 'touch-file', -}; - -/** Programs whose name and args map to the identically-named vpt subcommand. */ -const VPT_VERBATIM = new Set(['mkdir', 'rm', 'cp']); - -/** New steps run without a shell, so glob patterns would be passed literally. */ -function hasGlob(args: string[]): boolean { - return args.some((a) => /[*?[\]]/.test(a)); -} - -interface TranslationContext { - todos: string[]; - notes: string[]; - localRegistry: boolean; - /** Set when a step provisions/removes managed runtimes (`vp env install`). */ - needsFreshRuntime: boolean; -} - -/** Records a hand-conversion TODO and returns the placeholder step for it. */ -function makeTodo(ctx: TranslationContext, reason: string, command: string): NewStep { - ctx.todos.push(`${reason}: \`${command}\``); - return { - argv: ['vpt', 'print', 'TODO(migrate)'], - comment: `TODO(migrate) ${reason}: ${command}`, - }; -} - -/** - * Translates one simple (operator-free) shell command into a step, or returns - * a TODO step preserving the raw text for hand conversion. - */ -function translateSimple(command: string, ctx: TranslationContext): NewStep | null { - const todo = (reason: string): NewStep => makeTodo(ctx, reason, command); - - // `echo/printf ... > file` (single `>`, not append) becomes an explicit - // write-file; every other operator or redirect form needs hand conversion. - // `echo` appends the newline it would have written; `printf` is exact but - // only when the content carries no escape/format sequences. - const redirect = command.includes('>>') - ? null - : command.match(/^(echo|printf)\s+(.+?)\s*>\s*(\S+)$/); - if (redirect) { - const contentTokens = tokenize(redirect[2]); - if (contentTokens) { - const content = contentTokens.join(' '); - if (redirect[1] === 'echo') { - return { argv: ['vpt', 'write-file', redirect[3], `${content}\n`] }; - } - if (!/[\\%]/.test(content)) { - return { argv: ['vpt', 'write-file', redirect[3], content] }; - } - return todo('printf escape sequences need hand conversion'); - } - } - if (/[|;`]|\$\(|<|>>/.test(command)) { - return todo('shell operators need hand conversion'); - } - if (command.includes('>')) { - return todo('redirect needs hand conversion'); - } - - const tokens = tokenize(command); - if (!tokens || tokens.length === 0) { - return todo('unparsable command'); - } - - // Leading VAR=value assignments become step envs. Values needing shell - // expansion ($(pwd), $PATH, backticks) cannot be represented statically. - const envs: [string, string][] = []; - while (tokens.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) { - const [key, ...rest] = tokens.shift()!.split('='); - const value = rest.join('='); - if (/[$`]/.test(value)) { - return todo(`env value for ${key} needs shell expansion`); - } - envs.push([key, value]); - } - if (tokens.length === 0) { - return todo('env-only command'); - } - - // `node $SNAP_LOCAL_REGISTRY -- ` wrapper: unwrap and flag the case. - if (tokens[0] === 'node' && tokens[1] === '$SNAP_LOCAL_REGISTRY') { - ctx.localRegistry = true; - const sep = tokens.indexOf('--'); - const inner = tokens.slice(sep === -1 ? 2 : sep + 1).join(' '); - const innerStep = translateSimple(inner, ctx); - if (innerStep && envs.length > 0) { - innerStep.envs = [...envs, ...(innerStep.envs ?? [])]; - } - return innerStep; - } - - if (tokens.some((t) => t.includes('$'))) { - return todo('shell variable expansion needs hand conversion'); - } - - const program = tokens[0]; - const args = tokens.slice(1); - - let step: NewStep | null = null; - if (PASSTHROUGH_PROGRAMS.has(program)) { - // Runtime-provisioning commands must start from an empty VP_HOME, so - // the case opts out of seed-runtime (emitted at the case level). - if ( - program === 'vp' && - args[0] === 'env' && - /^(install|i|uninstall|uni)$/.test(args[1] ?? '') - ) { - ctx.needsFreshRuntime = true; - } - step = { argv: tokens }; - } else if (program in COREUTILS_MAP || VPT_VERBATIM.has(program) || program === 'chmod') { - if (hasGlob(args)) { - return todo('glob expansion needs hand conversion'); - } - if (program === 'chmod') { - // vpt chmod accepts an octal mode or the common `+x` form only. - if (args.length === 2 && /^([0-7]{3,4}|\+x)$/.test(args[0])) { - step = { argv: ['vpt', 'chmod', ...args] }; - } else { - return todo('unsupported chmod invocation'); - } - } else if (program in COREUTILS_MAP) { - // Flags change coreutils semantics in ways the vpt counterpart does - // not replicate (ls -a, touch -c, cat -n, ...): faithful or flagged, - // never silently stripped. - if (args.some((a) => a.startsWith('-'))) { - return todo(`${program} flags need hand conversion`); - } - step = { argv: ['vpt', COREUTILS_MAP[program], ...args] }; - } else { - step = { argv: ['vpt', program, ...args] }; - } - } else if (program === 'json-edit') { - // The legacy repo helper also accepted assignment expressions - // (`json-edit pkg.json '_.dependencies = {}'`); vpt json-edit is - // strictly ` `. - if (args.length === 3 && !args[1].includes('=') && !args[1].includes(' ')) { - step = { argv: ['vpt', 'json-edit', ...args] }; - } else { - return todo('legacy json-edit expression needs hand conversion'); - } - } else if (program === 'echo') { - step = { argv: ['vpt', 'print', args.join(' ')] }; - } else if (program === 'test') { - // `test -f x` style existence checks map to stat-file, which prints an - // explicit file/dir/missing line AND fails on mismatch via --assert, so - // both the recorded assertion and the shell exit semantics survive - // (guards like `test -f x && cmd` short-circuit through the runner's - // line-boundary failure flow). - const paths = args.filter((a) => a !== '!' && !a.startsWith('-')); - const flags = args.filter((a) => a.startsWith('-')); - const negated = args.includes('!'); - if ( - paths.length > 0 && - flags.length === 1 && - args.every((a) => a === '!' || /^-[fde]$/.test(a) || !a.startsWith('-')) - ) { - const assertArgs = - flags[0] === '-e' - ? [negated ? '--assert' : '--assert-not', 'missing'] - : [negated ? '--assert-not' : '--assert', flags[0] === '-d' ? 'dir' : 'file']; - step = { argv: ['vpt', 'stat-file', ...paths, ...assertArgs] }; - } else { - return todo('unsupported test expression'); - } - } else if (program === 'true') { - ctx.notes.push(`dropped no-op step: \`${command}\``); - return null; - } else { - return todo(`program \`${program}\` is not allowed as a step`); - } - - if (step && envs.length > 0) { - step.envs = envs; - } - return step; -} - -/** Translates one old command line into zero or more new steps. */ -function translateCommand(raw: string, ctx: TranslationContext): NewStep[] { - const { command, comment } = extractComment(raw); - if (command.length === 0) { - if (comment) { - ctx.notes.push(`dropped comment-only command: \`# ${comment}\``); - } - return []; - } - if (command.includes('||')) { - return [makeTodo(ctx, '`||` chain needs hand conversion', command)]; - } - const steps: NewStep[] = []; - const parts = splitOnAndAnd(command); - - // Special-case `test -f x && echo ...`: the stat-file line already asserts - // existence, the echo added no information. - if (parts.length === 2 && /^test\s/.test(parts[0]) && /^echo\s/.test(parts[1])) { - const step = translateSimple(parts[0], ctx); - if (step) { - if (comment) { - step.comment = comment; - } - step.continueOnFailure = true; - ctx.notes.push(`folded \`&& echo\` into stat-file assertion: \`${command}\``); - return [step]; - } - } - - // `cd && ...` scopes the rest of the chain to that directory (each - // legacy command line started fresh at the fixture root, so the cwd never - // leaks across lines). - let cwd: string | undefined; - for (const part of parts) { - if (/^cd(\s|$)/.test(part)) { - const cdTokens = tokenize(part); - const dir = cdTokens?.length === 2 ? cdTokens[1] : null; - if (!dir || dir.startsWith('/') || /[$`]/.test(dir)) { - return [makeTodo(ctx, '`cd` form needs hand conversion', command)]; - } - cwd = cwd === undefined ? dir : `${cwd}/${dir}`; - continue; - } - const step = translateSimple(part, ctx); - if (step) { - if (cwd !== undefined) { - step.cwd = cwd; - } - steps.push(step); - } - } - if (comment && steps.length > 0) { - steps[0].comment = steps[0].comment ? `${comment}; ${steps[0].comment}` : comment; - } - // Legacy command LINES were independent (a failure did not stop the next - // line), while `&&` within a line short-circuited. The runner stops on - // failure by default, so only the line-final step opts back out; chain- - // internal failures still stop, exactly like the shell did. - if (steps.length > 0) { - steps[steps.length - 1].continueOnFailure = true; - } - return steps; -} - -function tomlString(value: string): string { - return JSON.stringify(value); -} - -function tomlKey(key: string): string { - return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key); -} - -function emitStep(step: NewStep): string { - const isSimple = - step.comment === undefined && - step.cwd === undefined && - step.envs === undefined && - step.continueOnFailure !== true && - step.timeout === undefined && - step.snapshot === undefined; - const argv = `[${step.argv.map(tomlString).join(', ')}]`; - if (isSimple) { - return ` ${argv},`; - } - const fields = [`argv = ${argv}`]; - if (step.cwd !== undefined) { - fields.push(`cwd = ${tomlString(step.cwd)}`); - } - if (step.comment !== undefined) { - fields.push(`comment = ${tomlString(step.comment)}`); - } - if (step.envs !== undefined) { - const envs = step.envs.map(([k, v]) => `[${tomlString(k)}, ${tomlString(v)}]`).join(', '); - fields.push(`envs = [${envs}]`); - } - if (step.timeout !== undefined) { - fields.push(`timeout = ${step.timeout}`); - } - if (step.snapshot !== undefined) { - fields.push(`snapshot = ${String(step.snapshot)}`); - } - if (step.continueOnFailure === true) { - fields.push('continue-on-failure = true'); - } - return ` { ${fields.join(', ')} },`; -} - -function migrateCase( - caseDir: string, - caseName: string, - flavor: string, - outDir: string, -): CaseReport { - const report: CaseReport = { name: caseName, notes: [], todos: [] }; - - const newName = fixtureName(caseName); - // Never clobber an existing fixture: the same case name can exist in both - // legacy trees (local and global), and merging those is a hand decision - // (usually a second [[case]] or a vp = ["local", "global"] matrix). - const targetDir = path.join(outDir, newName); - if (fs.existsSync(targetDir)) { - report.todos.push( - `target fixture \`${path.basename(targetDir)}\` already exists; case skipped, merge it by hand`, - ); - report.skipped = true; - return report; - } - - const old: OldSteps = JSON.parse(fs.readFileSync(path.join(caseDir, 'steps.json'), 'utf8')); - const ctx: TranslationContext = { - todos: report.todos, - notes: report.notes, - localRegistry: false, - needsFreshRuntime: false, - }; - - if (newName !== caseName) { - report.notes.push(`renamed to \`${newName}\` (identifier rule)`); - } - - const lines: string[] = [ - '[[case]]', - `name = ${tomlString(newName)}`, - `vp = ${tomlString(flavor)}`, - ]; - - if (old.ignoredPlatforms && old.ignoredPlatforms.length > 0) { - const filters = old.ignoredPlatforms.map((filter) => { - if (typeof filter === 'string') { - const os = OS_MAP[filter]; - if (!os) { - report.todos.push(`unknown ignoredPlatforms value: ${filter}`); - } - return tomlString(os ?? filter); - } - const os = OS_MAP[filter.os] ?? filter.os; - const libc = filter.libc ? `, libc = ${tomlString(filter.libc)}` : ''; - return `{ os = ${tomlString(os)}${libc} }`; - }); - lines.push(`skip-platforms = [${filters.join(', ')}]`); - } - - if (old.env && Object.keys(old.env).length > 0) { - const sets = Object.entries(old.env).filter(([, v]) => v !== ''); - const unsets = Object.entries(old.env) - .filter(([, v]) => v === '') - .map(([k]) => k); - if (sets.length > 0) { - const table = sets.map(([k, v]) => `${tomlKey(k)} = ${tomlString(v)}`).join(', '); - lines.push(`env = { ${table} }`); - } - if (unsets.length > 0) { - lines.push(`unset-env = [${unsets.map(tomlString).join(', ')}]`); - report.notes.push(`empty-string env entries became unset-env: ${unsets.join(', ')}`); - } - } - - if (old.serial) { - report.notes.push('dropped `serial: true` (per-case VP_HOME isolation replaces it)'); - } - if (old.linkCheckoutPackages) { - report.todos.push('`linkCheckoutPackages` is not supported by the new suite yet'); - } - - // Translate EVERYTHING (steps and after-cleanup) before emitting the - // ctx-derived case flags below, so a flag-triggering command in `after` - // is observed too. - const stepLines: string[] = []; - for (const entry of old.commands) { - const raw = typeof entry === 'string' ? entry : entry.command; - for (const step of translateCommand(raw, ctx)) { - if (typeof entry !== 'string') { - step.timeout = entry.timeout; - if (entry.ignoreOutput === true) { - step.snapshot = false; - } - } - stepLines.push(emitStep(step)); - } - } - const afterLines: string[] = []; - for (const raw of old.after ?? []) { - for (const step of translateCommand(raw, ctx)) { - afterLines.push(emitStep(step)); - } - } - - if (ctx.needsFreshRuntime) { - lines.push('seed-runtime = false'); - report.notes.push( - 'runtime-provisioning case: generated with `seed-runtime = false` so it starts from an empty VP_HOME', - ); - } - if (old.localVitePlusPackages || ctx.localRegistry) { - lines.push('local-registry = true'); - // The runner has no local-registry support yet; keep the generated case - // out of default runs so a migrated batch stays green meanwhile. - lines.push('ignore = true'); - report.todos.push( - '`local-registry` cases are not supported by the new suite yet (generated with `ignore = true`)', - ); - } - lines.push('steps = [', ...stepLines, ']'); - if (afterLines.length > 0) { - lines.push('after = [', ...afterLines, ']'); - } - - // Write the fixture: everything except steps.json and snap.txt carries over. - fs.mkdirSync(targetDir, { recursive: true }); - // Only the ROOT metadata files are omitted; a project file that happens - // to be named snap.txt or steps.json in a subdirectory carries over. - const rootMetadata = new Set([ - path.resolve(caseDir, 'steps.json'), - path.resolve(caseDir, 'snap.txt'), - ]); - fs.cpSync(caseDir, targetDir, { - recursive: true, - filter: (src) => !rootMetadata.has(path.resolve(src)), - }); - fs.writeFileSync(path.join(targetDir, 'snapshots.toml'), `${lines.join('\n')}\n`); - return report; -} - -export function migrateSnapTests(): void { - const { values, positionals } = parseArgs({ - args: process.argv.slice(3), - options: { - vp: { type: 'string' }, - out: { type: 'string', default: 'crates/vite_cli_snapshots/tests/cli_snapshots/fixtures' }, - 'keep-old': { type: 'boolean', default: false }, - }, - allowPositionals: true, - }); - const flavor = values.vp; - const outDir = values.out; - const keepOld = values['keep-old']; - const [oldDir, nameFilter] = positionals; - if (!oldDir || (flavor !== 'local' && flavor !== 'global')) { - console.error( - 'Usage: tool migrate-snap-tests --vp [name-filter] [--out ] [--keep-old]', - ); - process.exit(1); - } - - const caseDirs = fs - .readdirSync(oldDir, { withFileTypes: true }) - .filter((e) => e.isDirectory() && !e.name.startsWith('.')) - .map((e) => e.name) - .filter((name) => (nameFilter ? name.includes(nameFilter) : true)) - .toSorted(); - - const reports: CaseReport[] = []; - for (const name of caseDirs) { - const caseDir = path.join(oldDir, name); - const report = migrateCase(caseDir, name, flavor, outDir); - reports.push(report); - // Only cleanly converted cases leave the legacy tree: TODO placeholders - // are not coverage, so those cases keep their old dir until the hand - // conversion lands. - if (!keepOld && !report.skipped) { - if (report.todos.length === 0) { - fs.rmSync(caseDir, { recursive: true, force: true }); - } else { - report.notes.push('old case dir kept until the TODOs are hand-converted'); - } - } - } - - const reportLines: string[] = [ - '# Snap-test migration report', - '', - `Source: \`${oldDir}\` (flavor: ${flavor}), ${reports.length} case(s).`, - '', - 'Record baselines with `UPDATE_SNAPSHOTS=1 just snapshot-test `', - 'and review each new snapshot against the deleted snap.txt in `git diff`.', - ...(keepOld - ? ['The old case directories were kept (--keep-old); delete them in the same PR.'] - : ['The old case directories were removed (recover with `git checkout -- `).']), - '', - ]; - let todoCount = 0; - for (const report of reports) { - reportLines.push(`## ${report.name}`); - if (report.todos.length === 0 && report.notes.length === 0) { - reportLines.push('', 'auto-migrated cleanly', ''); - continue; - } - reportLines.push(''); - for (const todo of report.todos) { - reportLines.push(`- TODO: ${todo}`); - todoCount++; - } - for (const note of report.notes) { - reportLines.push(`- note: ${note}`); - } - reportLines.push(''); - } - // The report lives next to the fixtures dir, not inside it: everything - // inside `fixtures/` is treated as a fixture by the runner. - const reportPath = path.join(outDir, '..', 'MIGRATION-REPORT.md'); - fs.writeFileSync(reportPath, reportLines.join('\n')); - const migrated = reports.filter((r) => !r.skipped).length; - const skipped = reports.length - migrated; - const removed = keepOld ? 0 : reports.filter((r) => !r.skipped && r.todos.length === 0).length; - console.log( - `Migrated ${migrated} case(s) to ${outDir}${ - removed > 0 ? ` and removed ${removed} cleanly converted old case dir(s)` : '' - }${skipped > 0 ? `, skipped ${skipped}` : ''}; ${todoCount} TODO(s) need hand conversion.`, - ); - console.log(`Report: ${reportPath}`); -} - -// Exported for unit tests only. -export { fixtureName, translateCommand }; -export type { NewStep, TranslationContext }; diff --git a/packages/tools/src/snap-test.ts b/packages/tools/src/snap-test.ts deleted file mode 100755 index 691083ea54..0000000000 --- a/packages/tools/src/snap-test.ts +++ /dev/null @@ -1,784 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import fs from 'node:fs'; -import fsPromises from 'node:fs/promises'; -import { cpus, homedir, tmpdir } from 'node:os'; -import path from 'node:path'; -import { setTimeout } from 'node:timers/promises'; -import { debuglog, parseArgs } from 'node:util'; - -import { npath } from '@yarnpkg/fslib'; -import { execute } from '@yarnpkg/shell'; - -import { packLocalVitePlusPackages } from './pack-local-vite-plus.ts'; -import { isPassThroughEnv, replaceUnstableOutput } from './utils.js'; - -const debug = debuglog('vite-plus/snap-test'); - -let localVpPackagesPromise: Promise | undefined; - -/** - * Pack the checkout's `vite-plus` and `@voidzero-dev/vite-plus-core` once per - * run, for fixtures with `localVitePlusPackages: true`. Commands routed - * through `local-npm-registry.ts` serve these tarballs at the checkout - * version, so real package-manager installs work without the version being - * published on npm (e.g. on release branches) and exercise the actual local - * build. - * - * The destination lives inside the run's temp root rather than a fixed - * directory on purpose: a fresh pack per run (~2s) guarantees the served - * tarballs match the current checkout build (a reused fixed directory would - * silently serve stale tarballs after a rebuild unless it grew its own - * invalidation scheme), the run's exit cleanup deletes it for free, and - * concurrent snap-test processes (local + global) cannot overwrite each - * other's tarballs mid-read. - */ -function packLocalVitePlusPackagesOnce(casesDir: string, tempTmpDir: string): Promise { - localVpPackagesPromise ??= (async () => { - const destination = path.join(tempTmpDir, 'local-vite-plus-packages'); - fs.mkdirSync(destination, { recursive: true }); - const startTime = Date.now(); - await packLocalVitePlusPackages(resolveRepoRoot(casesDir), destination); - console.log('Packed local Vite+ packages in %dms', Date.now() - startTime); - return destination; - })(); - return localVpPackagesPromise; -} - -// Remove comments (starting with ' #') from command strings -// `@yarnpkg/shell` doesn't parse comments. -// This doesn't handle all edge cases (such as ' #' in quoted strings), but is good enough for our test cases. -function stripComments(command: string): string { - if (command.trim().startsWith('#')) { - return ''; - } - const commentStart = command.indexOf(' #'); - return commentStart === -1 ? command : command.slice(0, commentStart); -} - -/** - * Run tasks with limited concurrency based on CPU count. - * @param tasks Array of task functions to execute - * @param maxConcurrency Maximum number of concurrent tasks (defaults to CPU count) - */ -async function runWithConcurrencyLimit( - tasks: (() => Promise)[], - maxConcurrency = cpus().length, -): Promise { - const executing: Promise[] = []; - const errors: Error[] = []; - - for (const task of tasks) { - const promise = task() - .catch((error) => { - errors.push(error); - console.error('Task failed:', error); - }) - .finally(() => { - // oxlint-disable-next-line typescript/no-floating-promises - executing.splice(executing.indexOf(promise), 1); - }); - - executing.push(promise); - - if (executing.length >= maxConcurrency) { - await Promise.race(executing); - } - } - - await Promise.all(executing); - - if (errors.length > 0) { - throw new Error(`${errors.length} test case(s) failed. First error: ${errors[0].message}`); - } -} - -function expandHome(p: string): string { - return p.startsWith('~') ? path.join(homedir(), p.slice(1)) : p; -} - -function parseShard(value: string): { index: number; total: number } { - const match = value.match(/^(\d+)\/(\d+)$/); - if (!match) { - throw new Error( - `Invalid --shard format: "${value}". Expected format: --shard=/ (e.g., --shard=1/3)`, - ); - } - const index = Number(match[1]); - const total = Number(match[2]); - if (total < 1) { - throw new Error(`Invalid --shard total: ${total}. Must be >= 1`); - } - if (index < 1 || index > total) { - throw new Error(`Invalid --shard index: ${index}. Must be between 1 and ${total}`); - } - return { index, total }; -} - -function selectShard(items: T[], index: number, total: number): T[] { - const chunkSize = Math.ceil(items.length / total); - const start = (index - 1) * chunkSize; - return items.slice(start, start + chunkSize); -} - -const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests'; - -function resolveGlobalCliScriptsDir(casesDir: string): string { - const candidates = [ - // `packages/cli/snap-tests-global` -> `packages/cli/dist` - path.join(path.dirname(casesDir), 'dist'), - // Fallback for the common `pnpm -F vite-plus snap-test-global` cwd. - path.resolve('dist'), - ]; - - const scriptsDir = candidates.find((dir) => fs.existsSync(path.join(dir, 'bin.js'))); - if (!scriptsDir) { - throw new Error( - `Unable to find built Vite+ CLI scripts for global snap tests. Tried:\n${candidates - .map((dir) => `- ${dir}`) - .join('\n')}`, - ); - } - - return scriptsDir; -} - -function resolveRepoRoot(casesDir: string): string { - return path.resolve(path.dirname(casesDir), '..', '..'); -} - -function resolveGlobalCliBinary(binDir: string): string { - const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; - const binaryPath = path.join(path.resolve(expandHome(binDir)), binaryName); - if (!fs.existsSync(binaryPath)) { - throw new Error(`Unable to find global snap test vp binary at ${binaryPath}`); - } - - return fs.realpathSync(binaryPath); -} - -function resolveInstalledGlobalCliTargetBinary(binDir: string): string { - const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; - const binaryPath = path.join( - path.resolve(expandHome(binDir)), - '..', - 'current', - 'bin', - binaryName, - ); - if (!fs.existsSync(binaryPath)) { - throw new Error(`Unable to find installed global snap test vp binary at ${binaryPath}`); - } - - return fs.realpathSync(binaryPath); -} - -function resolveBuiltGlobalCliArtifact( - casesDir: string, - binaryName: string, - packageName: string, -): string { - const repoRoot = resolveRepoRoot(casesDir); - const targetDirs = [path.join(repoRoot, 'target')]; - if (process.env.CARGO_TARGET_DIR) { - targetDirs.unshift(process.env.CARGO_TARGET_DIR); - } - const candidates: string[] = []; - for (const targetDir of targetDirs) { - candidates.push( - path.join(targetDir, 'release', binaryName), - path.join(targetDir, 'debug', binaryName), - ); - if (!fs.existsSync(targetDir)) { - continue; - } - - for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) { - if (entry.isDirectory()) { - candidates.push( - path.join(targetDir, entry.name, 'release', binaryName), - path.join(targetDir, entry.name, 'debug', binaryName), - ); - } - } - } - const binaryPath = candidates.find((candidate) => fs.existsSync(candidate)); - if (!binaryPath) { - throw new Error( - `Unable to find built Vite+ global CLI ${binaryName} for global snap tests. Tried:\n${candidates - .map((candidate) => `- ${candidate}`) - .join('\n')}\nRun \`cargo build -p ${packageName} --release\` before snap-test-global.`, - ); - } - - return fs.realpathSync(binaryPath); -} - -function resolveBuiltGlobalCliBinary(casesDir: string): string { - const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; - return resolveBuiltGlobalCliArtifact(casesDir, binaryName, 'vite_global_cli'); -} - -function resolveBuiltGlobalCliShim(casesDir: string): string { - return resolveBuiltGlobalCliArtifact(casesDir, 'vp-shim.exe', 'vite_trampoline'); -} - -function newestMtimeMs(filePath: string): number { - const stats = fs.statSync(filePath); - if (!stats.isDirectory()) { - return stats.mtimeMs; - } - - return fs - .readdirSync(filePath) - .reduce( - (newest, entry) => Math.max(newest, newestMtimeMs(path.join(filePath, entry))), - stats.mtimeMs, - ); -} - -function fileContentsEqual(a: string, b: string): boolean { - return fs.readFileSync(a).equals(fs.readFileSync(b)); -} - -function assertGlobalCliBinaryMatchesCheckout(binDir: string, casesDir: string): void { - const repoRoot = resolveRepoRoot(casesDir); - const builtBinary = resolveBuiltGlobalCliBinary(casesDir); - const sourcePaths = [ - path.join(repoRoot, 'Cargo.toml'), - path.join(repoRoot, 'Cargo.lock'), - path.join(repoRoot, 'crates', 'vite_global_cli', 'src'), - path.join(repoRoot, 'crates', 'vite_shared', 'src'), - ]; - const shouldCheckMtime = process.env.GITHUB_ACTIONS !== 'true'; - const newestSourceMtime = shouldCheckMtime ? Math.max(...sourcePaths.map(newestMtimeMs)) : 0; - if (shouldCheckMtime && fs.statSync(builtBinary).mtimeMs + 1000 < newestSourceMtime) { - throw new Error( - `Built Vite+ global CLI binary is older than the current checkout: ${builtBinary}\n` + - 'Run `cargo build -p vite_global_cli --release` before snap-test-global.', - ); - } - - const globalBinary = resolveGlobalCliBinary(binDir); - if (process.platform !== 'win32' && fileContentsEqual(globalBinary, builtBinary)) { - return; - } - - if (process.platform === 'win32') { - const builtShim = resolveBuiltGlobalCliShim(casesDir); - const installedTargetBinary = resolveInstalledGlobalCliTargetBinary(binDir); - if ( - fileContentsEqual(globalBinary, builtShim) && - fileContentsEqual(installedTargetBinary, builtBinary) - ) { - return; - } - - throw new Error( - `Global snap tests would use stale Windows vp binaries.\n` + - `Entrypoint: ${globalBinary}\n` + - `Expected entrypoint to match the current checkout shim at ${builtShim}.\n` + - `Installed target: ${installedTargetBinary}\n` + - `Expected target to match the current checkout build at ${builtBinary}.\n` + - 'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.', - ); - } - - throw new Error( - `Global snap tests would use a stale vp binary from ${globalBinary}.\n` + - `Expected it to match the current checkout build at ${builtBinary}.\n` + - 'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.', - ); -} - -function replaceInstalledCheckoutPackages(rootDir: string, repoRoot: string): void { - const stack = [rootDir]; - const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'; - const replacements = new Map([ - ['node_modules/vite-plus', path.join(repoRoot, 'packages', 'cli')], - ['node_modules/vite', path.join(repoRoot, 'packages', 'core')], - ['node_modules/@voidzero-dev/vite-plus-core', path.join(repoRoot, 'packages', 'core')], - ]); - - while (stack.length > 0) { - const dir = stack.pop()!; - for (const [relativePackagePath, checkoutPackageDir] of replacements) { - const candidate = path.join(dir, relativePackagePath); - if (fs.existsSync(candidate) && fs.realpathSync(candidate) !== checkoutPackageDir) { - fs.rmSync(candidate, { recursive: true, force: true }); - fs.symlinkSync(checkoutPackageDir, candidate, symlinkType); - } - } - - const isNodeModulesPath = dir.split(path.sep).includes('node_modules'); - const isPnpmStorePath = dir.split(path.sep).includes('.pnpm'); - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (!entry.isDirectory() || entry.name === '.git' || entry.name === '.bin') { - continue; - } - if ( - isNodeModulesPath && - !isPnpmStorePath && - entry.name !== '.pnpm' && - entry.name !== '@voidzero-dev' - ) { - continue; - } - stack.push(path.join(dir, entry.name)); - } - } -} - -export async function snapTest() { - const { positionals, values } = parseArgs({ - allowPositionals: true, - args: process.argv.slice(3), - options: { - dir: { type: 'string' }, - 'bin-dir': { type: 'string' }, - shard: { type: 'string' }, - }, - }); - - const filter = positionals[0] ?? ''; // Optional filter to run specific test cases - const shard = values.shard ? parseShard(values.shard) : undefined; - - // Create a unique temporary directory for testing - // On macOS, `tmpdir()` is a symlink. Resolve it so that we can replace the resolved cwd in outputs. - // Remove hyphens from UUID to avoid npm's @npmcli/redact treating the path as containing - // secrets (it matches UUID patterns like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). - // Use `realpathSync.native` (libuv `uv_fs_realpath`) instead of the JS - // legacy form: on Windows the JS form can return paths with mixed - // separators (`C:\Users/.../Temp`) while the native form returns the - // canonical backslash path. The mixed form propagates downstream and - // confuses Node's ESM package walk-up — `#module-sync-enabled` subpath - // imports inside pnpm-nested deps then fail with - // `ERR_PACKAGE_IMPORT_NOT_DEFINED`. Also use `path.join` (not string - // concat with `/`) so the suffix matches. - const systemTmpDir = fs.realpathSync.native(tmpdir()); - const tempTmpDir = path.join(systemTmpDir, `vite-plus-test-${randomUUID().replaceAll('-', '')}`); - fs.mkdirSync(tempTmpDir, { recursive: true }); - // Pre-create the npm global prefix directory so tests using npm global - // operations (link, outdated -g, etc.) don't fail with ENOENT. - fs.mkdirSync(path.join(tempTmpDir, NPM_GLOBAL_PREFIX_DIR, 'lib'), { recursive: true }); - - // Clean up stale .node-version and package.json in the system temp directory. - // vite-plus walks up the directory tree to resolve Node.js versions, so leftover - // files from previous runs can cause tests to pick up unexpected version configs. - for (const staleFile of ['.node-version', 'package.json']) { - const stalePath = path.join(systemTmpDir, staleFile); - if (fs.existsSync(stalePath)) { - fs.rmSync(stalePath); - } - } - - const vitePlusHome = path.join(homedir(), '.vite-plus'); - - // Remove .previous-version so command-upgrade-rollback snap test is stable - const previousVersionPath = path.join(vitePlusHome, '.previous-version'); - if (fs.existsSync(previousVersionPath)) { - fs.rmSync(previousVersionPath); - } - - // Ensure shim mode is "managed" so snap tests use vite-plus managed Node.js - // instead of the system Node.js (equivalent to running `vp env on`). - const configPath = path.join(vitePlusHome, 'config.json'); - if (fs.existsSync(configPath)) { - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - if (config.shimMode && config.shimMode !== 'managed') { - delete config.shimMode; - fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); - } - } - - // Make dependencies available in the test cases. - // Create a real node_modules directory so we can add the CLI package itself - // alongside the symlinked dependencies (needed for `vite-plus/*` imports in - // vite.config.ts). - const tempNodeModules = path.join(tempTmpDir, 'node_modules'); - fs.mkdirSync(tempNodeModules); - const cliNodeModules = path.resolve('node_modules'); - for (const entry of fs.readdirSync(cliNodeModules)) { - fs.symlinkSync( - path.join(cliNodeModules, entry), - path.join(tempNodeModules, entry), - process.platform === 'win32' ? 'junction' : 'dir', - ); - } - // Add the CLI package itself so `vite-plus/*` subpath imports resolve - fs.symlinkSync( - path.resolve('.'), - path.join(tempNodeModules, 'vite-plus'), - process.platform === 'win32' ? 'junction' : 'dir', - ); - - // Clean up the temporary directory on exit - process.on('exit', () => { - try { - fs.rmSync(tempTmpDir, { recursive: true, force: true }); - } catch (error) { - console.error('Error cleaning up temporary directory: %s, %s', tempTmpDir, error); - } - }); - - const casesDir = path.resolve(values.dir || 'snap-tests'); - - // Every case has been migrated to the PTY snapshot suite; git does not - // track the resulting empty tree, so a fresh checkout has no dir at all. - if (!fs.existsSync(casesDir)) { - console.log(`No snap tests: ${casesDir} does not exist`); - return; - } - - // Collect valid test case names (sorted for deterministic sharding) - const validCaseNames: string[] = []; - const missingStepsJson: string[] = []; - for (const caseName of fs.readdirSync(casesDir).toSorted()) { - if (caseName.startsWith('.')) { - continue; - } - const caseDir = path.join(casesDir, caseName); - if (!fs.statSync(caseDir).isDirectory()) { - continue; - } - const stepsPath = path.join(caseDir, 'steps.json'); - if (!fs.existsSync(stepsPath)) { - missingStepsJson.push(caseName); - continue; - } - if (caseName.includes(filter)) { - validCaseNames.push(caseName); - } - } - - if (missingStepsJson.length > 0) { - throw new Error( - `${missingStepsJson.length} test case(s) missing steps.json: ${missingStepsJson.join(', ')}`, - ); - } - - // Apply sharding to select a subset of test cases - const selectedCases = shard - ? selectShard(validCaseNames, shard.index, shard.total) - : validCaseNames; - const globalCliScriptsDir = values['bin-dir'] ? resolveGlobalCliScriptsDir(casesDir) : undefined; - if (values['bin-dir']) { - assertGlobalCliBinaryMatchesCheckout(values['bin-dir'], casesDir); - } - - const serialTasks: (() => Promise)[] = []; - const parallelTasks: (() => Promise)[] = []; - for (const caseName of selectedCases) { - const stepsPath = path.join(casesDir, caseName, 'steps.json'); - const steps: Steps = JSON.parse(fs.readFileSync(stepsPath, 'utf-8')); - const task = () => - runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir'], globalCliScriptsDir); - if (steps.serial) { - serialTasks.push(task); - } else { - parallelTasks.push(task); - } - } - - const totalCount = serialTasks.length + parallelTasks.length; - if (totalCount > 0) { - const cpuCount = cpus().length; - const shardInfo = shard ? `, shard ${shard.index}/${shard.total}` : ''; - console.log( - 'Running %d test cases (%d serial + %d parallel, concurrency limit %d%s)', - totalCount, - serialTasks.length, - parallelTasks.length, - cpuCount, - shardInfo, - ); - await runWithConcurrencyLimit(serialTasks, 1); - await runWithConcurrencyLimit(parallelTasks, cpuCount); - } - process.exit(0); // Ensure exit even if there are pending timed-out steps -} - -// Command, PlatformFilter, and Steps are exported for migrate-snap-tests.ts, -// which translates this legacy steps.json schema to the PTY harness format. -export interface Command { - command: string; - /** - * If true, the stdout and stderr output of the command will be ignored. - * This is useful for commands that stdout/stderr is unstable. - */ - ignoreOutput?: boolean; - /** - * The timeout in milliseconds for the command. - * If not specified, the default timeout is 50 seconds. - */ - timeout?: number; -} - -export interface PlatformFilter { - os: string; - libc?: string; -} - -export interface Steps { - ignoredPlatforms?: (string | PlatformFilter)[]; - env?: Record; - commands: (string | Command)[]; - /** - * If true, the harness packs the checkout's `vite-plus` and - * `@voidzero-dev/vite-plus-core` (once per run) and exposes the tarball - * directory as `SNAP_LOCAL_VP_PACKAGES_DIR`. Commands wrapped with - * `node $SNAP_LOCAL_REGISTRY -- ...` then install these local packages at - * the checkout version instead of resolving it from the npm registry - * (which would fail on release branches before the version is published). - * These fixtures run real cold-cache installs, so the default command - * timeout is raised to 120s. - */ - localVitePlusPackages?: boolean; - /** - * If true, installed Vite+ packages in the test project are relinked to the - * current checkout after each successful command. - */ - linkCheckoutPackages?: boolean; - /** - * Commands to run after the test completes, regardless of success or failure. - * Useful for cleanup tasks like killing background processes. - * These commands are not included in the snap output. - */ - after?: string[]; - /** - * If true, this test case will run serially before parallel tests. - * Use for tests that modify global shared state (e.g., `vp env default`). - */ - serial?: boolean; -} - -// oxlint-disable-next-line no-underscore-dangle -let _isMusl: boolean | null = null; - -function isMusl(): boolean { - if (_isMusl === null) { - if (process.platform !== 'linux') { - _isMusl = false; - } else if (typeof process.report?.getReport === 'function') { - // Use Node.js process.report API to detect libc type: - // - glibcVersionRuntime present → glibc - // - shared objects contain "musl" → musl - const report = process.report.getReport() as Record; - if (report.header?.glibcVersionRuntime) { - _isMusl = false; - } else if (Array.isArray(report.sharedObjects)) { - _isMusl = report.sharedObjects.some( - (f: string) => f.includes('libc.musl-') || f.includes('ld-musl-'), - ); - } else { - _isMusl = false; - } - } else { - _isMusl = false; - } - } - return _isMusl; -} - -function shouldSkipPlatform(ignoredPlatforms: (string | PlatformFilter)[]): boolean { - for (const filter of ignoredPlatforms) { - if (typeof filter === 'string') { - if (filter === process.platform) { - return true; - } - } else { - if (filter.os !== process.platform) { - continue; - } - if (filter.libc === undefined) { - return true; - } - if (filter.libc === 'musl' && isMusl()) { - return true; - } - if (filter.libc === 'glibc' && !isMusl()) { - return true; - } - } - } - return false; -} - -async function runTestCase( - name: string, - tempTmpDir: string, - casesDir: string, - binDir?: string, - globalCliScriptsDir?: string, -) { - const steps: Steps = JSON.parse( - await fsPromises.readFile(`${casesDir}/${name}/steps.json`, 'utf-8'), - ); - if (steps.ignoredPlatforms !== undefined && shouldSkipPlatform(steps.ignoredPlatforms)) { - console.log('%s skipped on platform %s', name, process.platform); - return; - } - - console.log('%s started', name); - const caseTmpDir = path.join(tempTmpDir, name); - await fsPromises.cp(path.join(casesDir, name), caseTmpDir, { - recursive: true, - errorOnExist: true, - }); - - const passThroughEnvs = Object.fromEntries( - Object.entries(process.env).filter(([key]) => isPassThroughEnv(key)), - ); - const env: Record = { - ...passThroughEnvs, - // Indicate CLI is running in test mode, so that it prints more detailed outputs. - // Also disables tips for stable snapshots. - VP_CLI_TEST: '1', - // Suppress Node.js runtime warnings (e.g. MODULE_TYPELESS_PACKAGE_JSON) - // to keep snap outputs stable across Node.js versions. - NODE_NO_WARNINGS: '1', - NO_COLOR: 'true', - // set CI=true make sure snap-tests are stable on GitHub Actions - CI: 'true', - VP_HOME: path.join(homedir(), '.vite-plus'), - // Set git identity so `git commit` works on CI runners without global git config - GIT_AUTHOR_NAME: 'Test', - GIT_COMMITTER_NAME: 'Test', - GIT_AUTHOR_EMAIL: 'vite-plus-test@test.com', - GIT_COMMITTER_EMAIL: 'vite-plus-test@test.com', - // Skip `vp install` inside `vp migrate` — snap tests don't need real installs - VP_SKIP_INSTALL: '1', - // make sure npm install global packages to the temporary directory - NPM_CONFIG_PREFIX: path.join(tempTmpDir, NPM_GLOBAL_PREFIX_DIR), - // Absolute path to the local npm registry wrapper, so fixtures can route - // installs through it (`node $SNAP_LOCAL_REGISTRY -- vp ...`). - SNAP_LOCAL_REGISTRY: path.join( - resolveRepoRoot(casesDir), - 'packages', - 'tools', - 'src', - 'local-npm-registry.ts', - ), - // Global CLI snap tests execute the Rust binary from --bin-dir, but the JS - // entry should come from this checkout instead of a stale ~/.vite-plus install. - ...(globalCliScriptsDir ? { VITE_GLOBAL_CLI_JS_SCRIPTS_DIR: globalCliScriptsDir } : {}), - - // A test case can override/unset environment variables above. - // For example, VP_CLI_TEST/CI can be unset to test the real-world outputs. - ...steps.env, - }; - - if (steps.localVitePlusPackages) { - env['SNAP_LOCAL_VP_PACKAGES_DIR'] = await packLocalVitePlusPackagesOnce(casesDir, tempTmpDir); - } - - // Unset VP_NODE_VERSION to prevent `vp env use` session overrides - // from leaking into snap tests. - delete env['VP_NODE_VERSION']; - - // Unset VP_TOOL_RECURSION to prevent the shim recursion guard from - // leaking into snap tests. When `pnpm` runs the test via the `vp` shim, vp - // sets this marker before exec. Without clearing it, every npm/node command - // in the test would bypass the managed shim and fall through to the system binary. - delete env['VP_TOOL_RECURSION']; - - // Sometimes on Windows, the PATH variable is named 'Path' - if ('Path' in env && !('PATH' in env)) { - env['PATH'] = env['Path']; - delete env['Path']; - } - // The node shim prepends ~/.vite-plus/js_runtime/node/VERSION/bin/ to PATH, - // which leaks into this process. Strip internal vite-plus paths so the test - // environment simulates a clean user PATH (only the shim bin dir + system paths). - const vitePlusJsRuntime = path.join(env['VP_HOME'], 'js_runtime'); - env['PATH'] = [ - // Extend PATH to include the package's bin directory - // --bin-dir overrides the default for cases like global CLI tests - // where vp should resolve to the Rust binary instead of the Node.js script - path.resolve(expandHome(binDir || 'bin')), - ...env['PATH'].split(path.delimiter).filter((p) => !p.startsWith(vitePlusJsRuntime)), - ].join(path.delimiter); - - const newSnap: string[] = []; - - const startTime = Date.now(); - const cwd = npath.toPortablePath(caseTmpDir); - - try { - for (const command of steps.commands) { - const cmd = typeof command === 'string' ? { command } : command; - debug('running command: %o, cwd: %s, env: %o', cmd, caseTmpDir, env); - - // While `@yarnpkg/shell` supports capturing output via in-memory `Writable` streams, - // it seems not to have stable ordering of stdout/stderr chunks. - // To ensure stable ordering, we redirect outputs to a file instead. - const outputStreamPath = path.join(caseTmpDir, 'output.log'); - const outputStream = await fsPromises.open(outputStreamPath, 'w'); - - const exitCode = await Promise.race([ - execute(stripComments(cmd.command), [], { - env, - cwd, - stdin: null, - // Declared to be `Writable` but `FileHandle` works too. - // @ts-expect-error - stderr: outputStream, - // @ts-expect-error - stdout: outputStream, - glob: { - // Disable glob expansion. Pass args like '--filter=*' as-is. - isGlobPattern: () => false, - match: async () => [], - }, - }), - // Fixtures that install local Vite+ packages run real cold-cache - // installs, so their commands get a higher default ceiling. - setTimeout(cmd.timeout ?? (steps.localVitePlusPackages ? 120 : 50) * 1000), - ]); - - await outputStream.close(); - if (exitCode === 0 && globalCliScriptsDir && steps.linkCheckoutPackages) { - replaceInstalledCheckoutPackages(caseTmpDir, resolveRepoRoot(casesDir)); - } - - let output = fs.readFileSync(outputStreamPath, 'utf-8'); - - let commandLine = `> ${cmd.command}`; - if (exitCode !== 0) { - commandLine = (exitCode === undefined ? '[timeout]' : `[${exitCode}]`) + commandLine; - } else { - // only allow ignore output if the command is successful - if (cmd.ignoreOutput) { - output = ''; - } - } - newSnap.push(commandLine); - if (output.length > 0) { - newSnap.push(replaceUnstableOutput(output, caseTmpDir)); - } - if (exitCode === undefined) { - break; // Stop executing further commands on timeout - } - } - } finally { - // Run after commands for cleanup, regardless of success or failure - if (steps.after) { - for (const afterCmd of steps.after) { - debug('running after command: %s, cwd: %s', afterCmd, caseTmpDir); - try { - await execute(stripComments(afterCmd), [], { - env, - cwd, - stdin: null, - }); - } catch (error) { - debug('after command failed: %s, error: %o', afterCmd, error); - } - } - } - } - - const newSnapContent = newSnap.join('\n'); - - await fsPromises.writeFile(`${casesDir}/${name}/snap.txt`, newSnapContent); - console.log('%s finished in %dms', name, Date.now() - startTime); -} diff --git a/packages/tools/src/utils.ts b/packages/tools/src/utils.ts deleted file mode 100644 index 470dd41c33..0000000000 --- a/packages/tools/src/utils.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { homedir } from 'node:os'; -import path from 'node:path'; - -import { Minimatch } from 'minimatch'; - -const ANSI_ESCAPE_REGEX = new RegExp( - `${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])`, - 'g', -); - -export function replaceUnstableOutput(output: string, cwd?: string) { - // Normalize line endings and strip ANSI escapes so snapshots are stable - // across CI platforms and terminal capabilities. - output = output.replaceAll(ANSI_ESCAPE_REGEX, '').replaceAll(/\r\n/g, '\n').replaceAll(/\r/g, ''); - - if (cwd) { - // On Windows, cwd may have mixed separators (from template literals like `${tmp}/name`) - // while output may use all-backslash OR all-forward-slash paths depending on the tool. - // Try all three forms: all-backslash, all-forward-slash, and original mixed. - const replacePathToken = (rawPath: string, placeholder: string) => { - if (process.platform === 'win32') { - const backslash = rawPath.replaceAll('/', '\\'); - output = output.replaceAll(backslash + '\\', placeholder + '/'); - output = output.replaceAll(backslash, placeholder); - const forwardslash = rawPath.replaceAll('\\', '/'); - output = output.replaceAll(forwardslash + '/', placeholder + '/'); - output = output.replaceAll(forwardslash, placeholder); - } - output = output.replaceAll(rawPath, placeholder); - }; - replacePathToken(cwd, ''); - const parent = path.dirname(cwd); - if (parent !== '/') { - replacePathToken(parent, '/..'); - } - } - // On Windows, normalize path separators in file paths for consistent snapshots. - // Only replace backslashes that look like path separators (preceded/followed by valid path chars). - // This avoids breaking ASCII art or escape sequences. - if (process.platform === 'win32') { - // Replace backslashes in patterns like: word\word, ./path\to, src\file.ts - // Pattern: backslash between alphanumeric/dot/underscore/hyphen chars - output = output.replaceAll(/([a-zA-Z0-9._-])\\([a-zA-Z0-9._-])/g, '$1/$2'); - } - - return ( - output - // semver version - // e.g.: ` v1.0.0` -> ` ` - // e.g.: `/1.0.0` -> `/` - // The prerelease is bounded to version characters so a long - // `@0.0.0-commit.` npm alias inside JSON does not greedily swallow the - // closing quote/comma (e.g. `"npm:...core@0.0.0-commit.",`). - .replaceAll(/([@/\s]v?)\d+\.\d+\.\d+(?:-[\w.+-]*)?/g, '$1') - // vitest-family pins written as JSON values (catalog blocks, devDependencies, - // overrides/resolutions) all track the bundled VITEST_VERSION and so change on - // every daily upgrade-deps bump. The quote-preceded value is not caught by the - // generic semver rule above (which only matches `@`/`/`/whitespace prefixes), - // so mask it here to keep these snaps stable — mirroring the already-masked YAML - // `vitest: ` catalog form. e.g.: `"vitest": "4.1.9"` -> `"vitest": ""` - // - // Only major >= 4 is masked. The migrator refuses to manage vitest < 4 - // (`checkVitestVersion` requires >=4.0.0), so the only place a pre-4 pin can - // surface in a snapshot is an unsupported-version fixture echoing the user's - // OWN untouched `package.json` (e.g. migration-not-supported-vitest3 keeps - // `"vitest": "3.2.4"`). Leaving those visible keeps that snap able to prove the - // refused migration did not rewrite the dependency. Major matcher: `[4-9]` or - // any 2+ digit major (`[1-9]\d+`), so it also covers future vitest 10+. - // - // `@vitest/coverage-*` is EXCLUDED: coverage providers are user-installed peers - // that vite-plus never adds, pins, or overrides (see VITE_PLUS_OVERRIDE_PACKAGES - // in constants.ts), and `define-config.ts` fail-fasts when an installed provider - // skews from the bundled vitest. Their exact version is meaningful, so keep it - // visible — masking it would hide a stale/accidentally-rewritten user pin. - .replaceAll( - /("(?:vitest|@vitest\/(?!coverage-)[\w-]+)": ")(?:[4-9]|[1-9]\d+)\.\d+\.\d+(?:-[\w.]+)?(")/g, - '$1$2', - ) - // Vite+ and its core package are written as exact lockstep versions by - // create/migrate. Mask JSON dependency values so release bumps do not - // create unrelated snapshot churn (YAML values and npm aliases are - // already covered by the generic semver normalization above). - .replaceAll( - /("(?:vite-plus|@voidzero-dev\/vite-plus-core)": ")\d+\.\d+\.\d+(?:-[\w.]+)?(")/g, - '$1$2', - ) - // devEngines.packageManager auto-pin writes the exact resolved version - // e.g.: `"name": "pnpm",\n "version": "11.5.1"` -> `"version": ""` - // (the optional suffix covers prerelease and build metadata: -rc-1, +sha.abc) - .replaceAll( - /("name": "(?:pnpm|npm|yarn|bun)",\s*\n\s*"version": ")\d+\.\d+\.\d+(?:[-+][\w.+-]+)?(")/g, - '$1$2', - ) - // vite build banner can appear on some environments/runtimes: - // vite v - // transforming...✓ ... - // Keep snapshots stable by stripping the standalone banner line. - .replaceAll(/(?:^|\n)vite v\n(?=transforming\.\.\.)/g, '\n') - // vite-plus hash version - // e.g.: `vite-plus": "^0.0.0-aa9f90fe23216b8ad85b0ba4fc1bccb0614afaf0"` -> `vite-plus": "^0.0.0-` - .replaceAll(/0\.0\.0-\w{40}/g, '0.0.0-') - // date (YYYY-MM-DD HH:MM:SS) - .replaceAll(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, '') - // date only (YYYY-MM-DD) - .replaceAll(/\d{4}-\d{2}-\d{2}/g, '') - // time only (HH:MM:SS) - .replaceAll(/\d{2}:\d{2}:\d{2}/g, '') - // managed global package install ID - .replaceAll( - /#[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/g, - '', - ) - // duration - .replaceAll(/\d+(?:\.\d+)?(?:s|ms|µs|ns)/g, 'ms') - // parenthesized thread counts in CLI summaries - .replaceAll(/, \d+ threads\)/g, ', threads)') - // oxlint - .replaceAll(/with \d+ rules/g, 'with rules') - .replaceAll(/using \d+ threads/g, 'using threads') - // pnpm - .replaceAll(/Packages: \+\d+/g, 'Packages: +') - // only keep done - .replaceAll( - /Progress: resolved \d+, reused \d+, downloaded \d+, added \d+, done/g, - 'Progress: resolved , reused , downloaded , added , done', - ) - // ignore pnpm progress - .replaceAll(/Progress: resolved \d+, reused \d+, downloaded \d+, added \d+\n/g, '') - // ignore the pnpm supply-chain verification status line (pnpm >= 10.34.3); - // whether it appears and its "verified ... ago" timing depend on pnpm - // version and lockfile verification state, not on vp behavior - .replaceAll(/✓ Lockfile passes supply-chain policies \(verified [^)]*\)\n/g, '') - // ignore pnpm warn lines; pnpm pads `WARN` with thin spaces (U+2009) in - // TTY output and brackets it as `[WARN]` in non-interactive output - .replaceAll(/[\u2009 ]?\[?WARN\]?\s+Skip\s+adding .+?\n/g, '') - .replaceAll(/[\u2009 ]?\[?WARN\]?\s+Request\s+took .+?\n/g, '') - .replaceAll(/Scope: all \d+ workspace projects/g, 'Scope: all workspace projects') - .replaceAll(/\+{2,}\n/g, '+\n') - // ignore pnpm registry request error warning log, e.g.: - // `[WARN] GET https://registry.npmjs.org/testnpm2 error (ECONNRESET). Will retry in 10 seconds. 2 retries left.` - .replaceAll(/[\u2009 ]?\[?WARN\]?\s+GET\s+https:\/\/registry\..+?\n/g, '') - // ignore clack spinner frames (e.g. `◒ Preparing local Git repository...`), - // they appear intermittently depending on timing; the final `◇`/`◆` line stays - .replaceAll(/^[◐◓◑◒]\s[^\n]*\n?/gm, '') - // ignore bun resolution progress (appears intermittently depending on cache state) - .replaceAll(/Resolving dependencies\n/g, '') - .replaceAll(/Resolved, downloaded and extracted \[\d+\]\n/g, '') - .replaceAll(/Resolving\.\.\. /g, '') - .replaceAll(/Saved lockfile\n/g, '') - .replaceAll(/ \(v\d+\.\d+\.\d+ available\)/g, '') - // ignore yarn YN0013, because it's unstable output, only exists on CI environment - // ➤ YN0013: │ A package was added to the project (+ 0.7 KiB). - .replaceAll(/➤ YN0013:[^\n]+\n/g, '') - // ignore yarn `YN0000: └ Completed `, it's unstable output - // ➤ YN0000: └ Completed in ms ms - // ➤ YN0000: └ Completed in ms - // => - // ➤ YN0000: └ Completed - .replaceAll( - /➤ YN0000: └ Completed.* (s|ms|µs)( (s|ms|µs))?\n/g, - '➤ YN0000: └ Completed\n', - ) - // ignore npm warn - // npm warn Unknown env config "recursive". This will stop working in the next major version of npm - .replaceAll(/npm warn Unknown env config .+?\n/g, '') - // WARN  Issue while reading "/path/to/.npmrc". Failed to replace env in config: ${NPM_AUTH_TOKEN} - .replaceAll(/[\u2009 ]?\[?WARN\]?\s+Issue\s+while\s+reading .+?\n/g, '') - // ignore npm audited packages log - // "removed 1 package, and audited 3 packages in 700ms" => "removed package in ms" - // "up to date, audited 4 packages in 11ms" => "up to date in ms" - // "added 3 packages, and audited 4 packages in 100ms" => "added 3 packages in ms" - // "\nfound 0 vulnerabilities\n" => "" - .replaceAll( - /(removed \d+ package), and audited \d+ packages( in (?:s|ms|µs))\n/g, - '$1$2\n', - ) - .replaceAll(/(up to date), audited \d+ packages( in (?:s|ms|µs))\n/g, '$1$2\n') - .replaceAll( - /(added \d+ packages?), and audited \d+ packages( in (?:s|ms|µs))\n/g, - '$1$2\n', - ) - .replaceAll(/\nfound \d+ vulnerabilities\n/g, '') - // vite modules transformed count - .replaceAll(/✓ \d+ modules? transformed/g, '✓ modules transformed') - // replace size for tsdown - .replaceAll(/ \d+(\.\d+)? ([kKmMgG]?B)/g, ' $2') - // replace npm notice size: - // "npm notice 5.6kB snap.txt" - // "npm notice 619B steps.json" - .replaceAll(/ \d+(\.\d+)?([kKmMgG]?B) /g, ' $2 ') - // '"size": 821' => '"size": ' - // '"unpackedSize": 2720' => '"unpackedSize": ' - .replaceAll(/"(size|unpackedSize)": \d+/g, '"$1": ') - // ignore npm registry domain - .replaceAll(/(https?:\/\/registry\.)[^/\s]+(\/?)/g, '$1$2') - // ignore pnpm tarball download average speed warning log - .replaceAll(/[\u2009 ]?\[?WARN\]?\s+Tarball download average speed .+?\n/g, '') - // ignore npm hash values - .replaceAll(/shasum: .+?\n/g, 'shasum: \n') - .replaceAll(/integrity: ([\w-]+)-.+?\n/g, 'integrity: $1-\n') - .replaceAll(/"shasum": ".+?"/g, '"shasum": ""') - .replaceAll(/"integrity": "(\w+)-.+?"/g, '"integrity": "$1-"') - // replace homedir; e.g.: /Users/foo/Library/pnpm/global/5/node_modules/testnpm2 => /Library/pnpm/global/5/node_modules/testnpm2 - .replaceAll(homedir(), '') - .replaceAll(/\/\.vite-plus/g, '') - // replace npm log file path with timestamp - // e.g.: /.npm/_logs/T07_38_18_387Z-debug-0.log => /.npm/_logs/-debug.log - .replaceAll( - /(\/\.npm\/_logs\/)T\d{2}_\d{2}_\d{2}_\d+Z-debug-\d+\.log/g, - '$1-debug.log', - ) - // remove the newline after "Checking formatting..." - .replaceAll(`Checking formatting...\n`, 'Checking formatting...') - // remove warning @: No license field - .replaceAll(/warning .+?: No license field\n/g, '') - // remove "npm warn exec The following package was not found and will be installed: cowsay@" - .replaceAll( - /npm warn exec The following package was not found and will be installed: .+?\n/g, - '', - ) - // remove "npm notice Access token expired or revoked..." - .replaceAll(/npm notice Access token expired or revoked.+?\n/g, '') - // remove mise reshimming messages (appears when global npm packages change) - .replaceAll(/Reshimming mise.+?\n/g, '') - // remove plugin timings warnings (intermittent CI warnings) - // [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown: - // - externalize-deps (74%) - .replaceAll(/\[PLUGIN_TIMINGS\] Warning:.*?\n(?:\s+-\s+.+?\n)*/g, '') - // remove JS stack traces (lines starting with " at ") - .replaceAll(/\n\s+at .+/g, '') - // replace git stash hashes: "git stash (abc1234)" => "git stash ()" - .replaceAll(/git stash \([0-9a-f]+\)/g, 'git stash ()') - // replace bun-style build hashes after the semver banner: - // "bun pm trust v (af24e281)" => "bun pm trust v ()" - .replaceAll(/(v) \([0-9a-f]+\)/g, '$1 ()') - // normalize cat error spacing: Windows "cat:file" vs Unix "cat: file" - .replaceAll(/\bcat:(\S)/g, 'cat: $1') - ); -} - -// Exact matches for common environment variables -const DEFAULT_PASSTHROUGH_ENVS = [ - // System and shell - 'HOME', - 'USER', - 'TZ', - 'LANG', - 'SHELL', - 'PWD', - 'PATH', - // CI/CD environments - 'CI', - // Node.js specific - 'NODE_OPTIONS', - 'COREPACK_HOME', - 'NPM_CONFIG_STORE_DIR', - 'PNPM_HOME', - // Library paths - 'LD_LIBRARY_PATH', - 'DYLD_FALLBACK_LIBRARY_PATH', - 'LIBPATH', - // Terminal/display - 'COLORTERM', - 'TERM', - 'TERM_PROGRAM', - 'DISPLAY', - 'FORCE_COLOR', - 'NO_COLOR', - // Temporary directories - 'TMP', - 'TEMP', - // Vercel specific - 'VERCEL', - 'VERCEL_*', - 'NEXT_*', - 'USE_OUTPUT_FOR_EDGE_FUNCTIONS', - 'NOW_BUILDER', - // Windows specific - 'APPDATA', - 'PROGRAMDATA', - 'SYSTEMROOT', - 'SYSTEMDRIVE', - 'USERPROFILE', - 'HOMEDRIVE', - 'HOMEPATH', - 'PATHEXT', // .EXE;.BAT;... - // IDE specific (exact matches) - 'ELECTRON_RUN_AS_NODE', - 'JB_INTERPRETER', - '_JETBRAINS_TEST_RUNNER_RUN_SCOPE_TYPE', - 'JB_IDE_*', - // VSCode specific - 'VSCODE_*', - // Docker specific - 'DOCKER_*', - 'BUILDKIT_*', - 'COMPOSE_*', - // Token patterns - '*_TOKEN', - // oxc specific - 'OXLINT_*', - // Rust specific - 'RUST_*', - // Vite specific - 'VITE_*', -].map((env) => new Minimatch(env)); - -export function isPassThroughEnv(env: string) { - const upperEnv = env.toUpperCase(); - return DEFAULT_PASSTHROUGH_ENVS.some((pattern) => pattern.match(upperEnv)); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 553a110158..76ae25d8df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,12 +126,6 @@ catalogs: '@vitest/utils': specifier: 4.1.10 version: 4.1.10 - '@yarnpkg/fslib': - specifier: ^3.1.3 - version: 3.1.4 - '@yarnpkg/shell': - specifier: ^4.1.3 - version: 4.1.3 acorn-import-assertions: specifier: ^1.9.0 version: 1.9.0 @@ -686,13 +680,6 @@ importers: version: 0.22.4(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.22.4)(@tsdown/exe@0.22.4)(@typescript/native-preview@7.0.0-dev.20260605.1)(@vitejs/devtools@0.3.4(typescript@6.0.2)(vite@packages+core))(oxc-resolver@11.21.3)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.2)(unplugin-unused@0.5.6)(unrun@0.3.1) packages/tools: - dependencies: - '@yarnpkg/fslib': - specifier: 'catalog:' - version: 3.1.4 - '@yarnpkg/shell': - specifier: 'catalog:' - version: 4.1.3(typanion@3.14.0) devDependencies: '@oxc-node/cli': specifier: 'catalog:' @@ -703,9 +690,6 @@ importers: '@types/semver': specifier: 'catalog:' version: 7.7.1 - minimatch: - specifier: 'catalog:' - version: 10.2.4 semver: specifier: 'catalog:' version: 7.8.5 @@ -2845,18 +2829,6 @@ packages: '@nkzw/safe-word-list@3.1.0': resolution: {integrity: sha512-yU6NAne/aU8n4BnYwlOu451PD/FH9AVKsOsQbITxAPYg4HEcBMv7v13woPOpxAS0lC87N2TOQdBZ7OjiK+5sIA==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@octokit/app@16.1.2': resolution: {integrity: sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==} engines: {node: '>= 20'} @@ -4984,19 +4956,6 @@ packages: resolution: {integrity: sha512-C/Gsy5NAatsGUF1eT9Ks/ErR52/X4YI7MSm7BtwNOw8v2Ko+SiCA5qXts61J0A7QYwOn4gfXfBZZnzSAng6G/w==} engines: {node: '>=18.20.0'} - '@yarnpkg/fslib@3.1.4': - resolution: {integrity: sha512-Yyguw5RM+xI1Bv0RFbs1ZF5HwU+9/He4YT7yeT722yAlLfkz9IzZHO6a5yStEshxiliPn9Fdj4H54a785xpK/g==} - engines: {node: '>=18.12.0'} - - '@yarnpkg/parsers@3.0.3': - resolution: {integrity: sha512-mQZgUSgFurUtA07ceMjxrWkYz8QtDuYkvPlu0ZqncgjopQ0t6CNEo/OSealkmnagSUx8ZD5ewvezUwUuMqutQg==} - engines: {node: '>=18.12.0'} - - '@yarnpkg/shell@4.1.3': - resolution: {integrity: sha512-5igwsHbPtSAlLdmMdKqU3atXjwhtLFQXsYAG0sn1XcPb3yF8WxxtWxN6fycBoUvFyIHFz1G0KeRefnAy8n6gdw==} - engines: {node: '>=18.12.0'} - hasBin: true - '@yuku-codegen/binding-darwin-arm64@0.5.44': resolution: {integrity: sha512-mpZc7hrjl/qxcbEMS6vVLE0lO4DjiXCvrp3gYbZ58bbmsSxSGCTlOCA0lpeLXAKOZxe0ZrVoqKteaewFF2Vbuw==} cpu: [arm64] @@ -5218,9 +5177,6 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -6130,10 +6086,6 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -6159,9 +6111,6 @@ packages: resolution: {integrity: sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA==} hasBin: true - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} @@ -6655,10 +6604,6 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true @@ -6932,10 +6877,6 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -7500,9 +7441,6 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -7644,10 +7582,6 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -7710,9 +7644,6 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -8035,9 +7966,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - srvx@0.11.15: resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} engines: {node: '>=20.16.0'} @@ -10509,18 +10437,6 @@ snapshots: '@nkzw/safe-word-list@3.1.0': {} - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@octokit/app@16.1.2': dependencies: '@octokit/auth-app': 8.1.2 @@ -12261,28 +12177,6 @@ snapshots: - react-native-b4a - supports-color - '@yarnpkg/fslib@3.1.4': - dependencies: - tslib: 2.8.1 - - '@yarnpkg/parsers@3.0.3': - dependencies: - js-yaml: 3.14.2 - tslib: 2.8.1 - - '@yarnpkg/shell@4.1.3(typanion@3.14.0)': - dependencies: - '@yarnpkg/fslib': 3.1.4 - '@yarnpkg/parsers': 3.0.3 - chalk: 4.1.2 - clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.6 - fast-glob: 3.3.3 - micromatch: 4.0.8 - tslib: 2.8.1 - transitivePeerDependencies: - - typanion - '@yuku-codegen/binding-darwin-arm64@0.5.44': optional: true @@ -12444,10 +12338,6 @@ snapshots: - bare-abort-controller - react-native-b4a - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} aria-query@5.3.0: @@ -13375,14 +13265,6 @@ snapshots: fast-fifo@1.3.2: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -13407,10 +13289,6 @@ snapshots: dependencies: strnum: 2.1.1 - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - fd-package-json@2.0.0: dependencies: walk-up-path: 4.0.0 @@ -13867,11 +13745,6 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -14166,12 +14039,11 @@ snapshots: meow@13.2.0: {} - merge2@1.4.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + optional: true mime-db@1.54.0: {} @@ -14871,8 +14743,6 @@ snapshots: query-selector-shadow-dom@1.0.1: {} - queue-microtask@1.2.3: {} - radix3@1.1.2: {} randombytes@2.1.0: @@ -15023,8 +14893,6 @@ snapshots: ret@0.5.0: {} - reusify@1.1.0: {} - rfdc@1.4.1: {} rgb2hex@0.2.5: {} @@ -15110,10 +14978,6 @@ snapshots: run-applescript@7.1.0: {} - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -15406,8 +15270,6 @@ snapshots: split2@4.2.0: {} - sprintf-js@1.0.3: {} - srvx@0.11.15: {} stable-hash-x@0.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 08a3365893..619c592bdf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -55,8 +55,6 @@ catalog: '@vitest/utils': 4.1.10 '@voidzero-dev/vitepress-theme': 4.8.4 '@vueuse/core': ^14.0.0 - '@yarnpkg/fslib': ^3.1.3 - '@yarnpkg/shell': ^4.1.3 acorn: ^8.17.0 acorn-import-assertions: ^1.9.0 astring: ^1.9.0 diff --git a/tsconfig.json b/tsconfig.json index 4f4601c86f..1d4878c612 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,6 @@ // PTY snapshot fixtures are workspaces under test, not repo code "crates/vite_cli_snapshots/tests", "packages/cli/docs", - "packages/cli/snap-tests", - "packages/cli/snap-tests-global", // build.ts imports vite/packages/vite/rolldown.config.ts which doesn't use .js extensions "packages/core/build.ts", "vite", diff --git a/vite.config.ts b/vite.config.ts index 55bc6603c7..13b09fd61d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -42,9 +42,6 @@ export default defineConfig({ }, ], ignorePatterns: [ - '**/snap-tests/**', - '**/snap-tests-global/**', - '**/snap-tests-todo/**', // PTY snapshot fixtures; also excluded in test/fmt below and tsconfig.json 'crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/**', 'packages/*/binding/**', @@ -56,8 +53,6 @@ export default defineConfig({ './vite/**', './rolldown/**', '**/node_modules/**', - '**/snap-tests/**', - '**/snap-tests-global/**', // PTY snapshot fixtures; also excluded in lint/fmt here and tsconfig.json 'crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/**', // FIXME: Error: failed to prepare the command for injection: Invalid argument (os error 22) @@ -69,12 +64,6 @@ export default defineConfig({ '**/tmp/**', // PTY snapshot fixtures; also excluded in lint/test above and tsconfig.json 'crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/**', - 'packages/cli/snap-tests/check-*/**', - 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', - // JSONC fixtures intentionally keep comments and trailing commas - 'packages/cli/snap-tests/migration-preserve-editor-jsonc-comments/.vscode/**', - 'packages/cli/snap-tests-global/migration-lint-staged-ts-config', - 'packages/cli/snap-tests-global/migration-partially-installed-vite-plus/**', 'ecosystem-ci/*/**', 'packages/cli/src/run-config.ts', 'vite', From 1fe0e90162141f592a963e99f7c6da1d0fd14b40 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Sat, 11 Jul 2026 00:04:37 +0900 Subject: [PATCH 2/2] docs(snapshots): make the PTY suite the sole supported path Remove migration-era guidance and update active documentation, skills, and code comments to describe the PTY snapshot workflow only. --- .claude/skills/bump-vite-task/SKILL.md | 15 ++------- .claude/skills/release-manager/SKILL.md | 3 +- .claude/skills/sync-tsdown-cli/SKILL.md | 2 +- .../skills/verify-interactive-cli/SKILL.md | 4 +-- .../interactive-cli-tmux-driver.sh | 4 +-- AGENTS.md | 31 +++++-------------- CONTRIBUTING.md | 23 ++------------ .../vite_cli_snapshots/src/bin/vpt/chmod.rs | 2 +- .../src/bin/vpt/json_edit.rs | 2 +- .../src/bin/vpt/list_dir.rs | 4 +-- .../tests/cli_snapshots/README.md | 23 ++------------ .../tarballs/README.md | 8 ++--- .../eslint.config.mjs | 2 +- .../eslint.config.mjs | 2 +- .../tests/cli_snapshots/flavor.rs | 3 +- .../tests/cli_snapshots/main.rs | 17 +++++----- .../tests/cli_snapshots/redact.rs | 4 +-- crates/vite_command/src/ps1_shim.rs | 2 +- crates/vite_global_cli/src/js_executor.rs | 3 +- .../src/commands/approve_builds.rs | 4 +-- packages/cli/src/migration/migrator/README.md | 16 ++++------ packages/tools/README.md | 1 - packages/tools/src/local-npm-registry.ts | 10 +++--- packages/tools/src/pack-local-vite-plus.ts | 4 +-- 24 files changed, 57 insertions(+), 132 deletions(-) diff --git a/.claude/skills/bump-vite-task/SKILL.md b/.claude/skills/bump-vite-task/SKILL.md index 2637838e0e..13e272779e 100644 --- a/.claude/skills/bump-vite-task/SKILL.md +++ b/.claude/skills/bump-vite-task/SKILL.md @@ -51,21 +51,13 @@ vite-task changes often affect CLI output, which means snapshot tests need updat - **Cache behavior messages**: e.g., new summary lines about cache status - **Task output formatting**: e.g., step numbering, separator lines -**PTY snapshot suite (`crates/vite_cli_snapshots`)**, the primary suite: +**PTY snapshot suite (`crates/vite_cli_snapshots`):** - A bump can break it two ways: runner compilation (it consumes vite-task's `pty_terminal_test`, `pty_terminal_test_client`, and `snapshot_test` crates directly, so their API changes surface here; fix in the runner) and recorded CLI output. -- Unlike the legacy trees, output changes are handled locally with real assertions: `UPDATE_SNAPSHOTS=1 just snapshot-test`, then review the `.md` diffs like code. Without a built `packages/cli/dist`, run the global flavor only: `VP_SNAP_SKIP_FLAVORS=local UPDATE_SNAPSHOTS=1 just snapshot-test`. +- Update output locally with real assertions: `UPDATE_SNAPSHOTS=1 just snapshot-test`, then review the `.md` diffs like code. Without a built `packages/cli/dist`, run the global flavor only: `VP_SNAP_SKIP_FLAVORS=local UPDATE_SNAPSHOTS=1 just snapshot-test`. - Windows runs in the `CLI snapshot test (Windows)` CI job via a cross-compiled nextest archive; snapshots are OS-shared, so a Windows-only diff there means a redaction gap, not a re-record. - Reference: `crates/vite_cli_snapshots/tests/cli_snapshots/README.md`. -**Legacy snap trees** (`packages/cli/snap-tests/*/snap.txt`, `packages/cli/snap-tests-global/*/snap.txt`, being migrated; do not add cases): - -1. Push your changes and let CI run the snap tests. -2. CI will show the diff in the E2E test logs if snap tests fail. -3. Extract the diff from CI logs and apply it locally. -4. Check all three platforms (Linux, Mac, Windows) since they may have slightly different snap test coverage. -5. Watch for trailing newline issues - ensure snap files end consistently. - ### 7. Review changelog and update docs - Fetch the vite-task `CHANGELOG.md` diff between old and new commits to see what changed: @@ -98,7 +90,6 @@ After creating the PR, automatically watch CI without asking the user first. Ens - **Lint**: Clippy and format checks - **Test** (Linux, Mac, Windows): Rust unit tests - **CLI snapshot test** (Linux, Mac, Windows): the PTY snapshot suite - most likely to fail on a vite-task bump (runner compiles against vite-task crates AND asserts CLI output) -- **CLI E2E test** (Linux, Mac, Windows): legacy snap tests during the migration - **Run task**: Task runner integration tests - **Cargo Deny**: License/advisory checks (may have pre-existing failures unrelated to bump) @@ -106,5 +97,5 @@ The only **required** status check for merging is `done`, which aggregates the o ## Notes -- Building the full CLI locally (`pnpm bootstrap-cli`) requires the rolldown Node.js package to be built first, which is complex. Prefer relying on CI for snap test generation. +- Building the full CLI locally (`pnpm bootstrap-cli`) requires the rolldown Node.js package to be built first, which is complex. Prefer the global-only snapshot command above or CI when no local CLI build is available. - `Cargo.lock` is automatically updated by cargo when you change the revision in `Cargo.toml`. diff --git a/.claude/skills/release-manager/SKILL.md b/.claude/skills/release-manager/SKILL.md index 62d102923f..fc32c4dc45 100644 --- a/.claude/skills/release-manager/SKILL.md +++ b/.claude/skills/release-manager/SKILL.md @@ -251,8 +251,7 @@ Do not assume the merge brought in only the fix PR: `main` may have accumulated Known release-branch-only failure modes: -- **Binding version drift**: `CLI E2E test` fails its no-unexpected-changes guard with a diff flipping version strings in `binding/index.cjs`. Fix: step 2. -- **Unpublished-version installs**: the release branch carries version `X.Y.Z` before it exists on npm. Snap fixtures that run a real package-manager install (`vp create` / `vp migrate` followed by install) fail with `ERR_PNPM_NO_MATCHING_VERSION` or bun/yarn `failed to resolve` for `vite-plus@X.Y.Z` / `@voidzero-dev/vite-plus-core@X.Y.Z`. Fix: pin `"VP_VERSION": ""` in the fixture's `steps.json` env (see #2017); the snapshot normalizer rewrites versions to ``, so output does not change. Any new install-performing fixture must ship with this pin or it will break the next release. +- **Binding version drift**: CI's no-unexpected-changes guard reports a diff flipping version strings in `binding/index.cjs`. Fix: step 2. - **Registry flakes**: registry-bound fixtures can time out (about 50s) and look like regressions. Rerun before diagnosing, and never commit a `[timeout]` snapshot. ## 6. Merge diff --git a/.claude/skills/sync-tsdown-cli/SKILL.md b/.claude/skills/sync-tsdown-cli/SKILL.md index 1293d220f0..c3744acc82 100644 --- a/.claude/skills/sync-tsdown-cli/SKILL.md +++ b/.claude/skills/sync-tsdown-cli/SKILL.md @@ -18,4 +18,4 @@ Compare the upstream `tsdown` CLI options with `vp pack` (defined in `packages/c - `-c, --config` is intentionally commented out (vp pack uses vite.config.ts) - `--env-prefix` has a different default (`['VITE_PACK_', 'TSDOWN_']`) 6. Verify with `pnpm --filter vite-plus build-ts` and `vp pack -h` -7. If new parameters were added, add a corresponding snap test under `packages/cli/snap-tests/` to verify the new option works correctly +7. If new parameters were added, add a corresponding PTY snapshot case under `crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/` to verify the new option works correctly diff --git a/.claude/skills/verify-interactive-cli/SKILL.md b/.claude/skills/verify-interactive-cli/SKILL.md index 72cdef44c0..9adc2b5017 100644 --- a/.claude/skills/verify-interactive-cli/SKILL.md +++ b/.claude/skills/verify-interactive-cli/SKILL.md @@ -1,12 +1,12 @@ --- name: verify-interactive-cli -description: Drive and capture vp's interactive (clack) prompts in a tmux session to verify interactive UX and catch spinner-over-prompt bugs that snap tests (which run non-interactively) miss. Use when asked to test/verify/capture an interactive vp command's prompts (vp migrate, vp create, ...), reproduce a prompt-rendering bug, or show the real interactive CLI output for a PR. +description: Drive and capture vp's interactive (clack) prompts in a tmux session to manually verify interactive UX and catch spinner-over-prompt bugs. Use when asked to test/verify/capture an interactive vp command's prompts (vp migrate, vp create, ...), reproduce a prompt-rendering bug, or show the real interactive CLI output for a PR. allowed-tools: Bash, Read --- # Verify vp's interactive CLI prompts -Snap tests run `vp` non-interactively, so interactive clack prompts (hooks / agent / editor confirms, the Node-version upgrade confirm) and TTY-only rendering bugs are never exercised. This skill drives the real prompts in a TTY (via tmux), captures clean output, and can catch a spinner animating underneath an active prompt (a real, recurring UX bug class, e.g. the "Preparing migration" and "Checking Node.js version support" spinners). +The PTY snapshot suite covers scripted prompt flows. This skill complements it by driving an installed CLI in tmux, capturing clean output, and checking a prompt over several seconds for a spinner animating underneath it (a real, recurring UX bug class, e.g. the "Preparing migration" and "Checking Node.js version support" spinners). ## Prerequisites diff --git a/.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh b/.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh index 5e0d45918e..6e9484dfc3 100755 --- a/.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh +++ b/.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Drive and capture an interactive `vp` (clack) prompt flow inside tmux, to verify -# interactive UX that snap tests (which run non-interactively) never cover. +# Drive and capture an interactive `vp` (clack) prompt flow inside tmux for +# manual verification against an installed CLI. # # Usage: # interactive-cli-tmux-driver.sh "" [STOP_AT_REGEX] diff --git a/AGENTS.md b/AGENTS.md index f34d4aa1d0..95d84f28d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,17 +22,15 @@ High-signal repo map: ``` vite-plus/ -├── packages/cli/ # Published vite-plus package, JS CLI, docs, templates, legacy snap tests, NAPI binding +├── packages/cli/ # Published vite-plus package, JS CLI, docs, templates, NAPI binding │ ├── src/bin.ts # JS entrypoint and local CLI dispatch │ ├── src/resolve-test.ts # Resolves upstream Vitest for `vp test` │ ├── src/utils/agent.ts # Generated agent-file marker/update logic -│ ├── binding/ # Rust NAPI binding used by the local CLI -│ ├── snap-tests/ # Legacy local CLI output snapshots -│ └── snap-tests-global/ # Legacy global CLI output snapshots +│ └── binding/ # Rust NAPI binding used by the local CLI ├── packages/core/ # @voidzero-dev/vite-plus-core bundled Vite/Rolldown/tsdown/VitePress surfaces ├── packages/prompts/ # Prompt UI/helpers package, including snapshot milestones -├── packages/tools/ # Repo tooling, local npm registry, legacy snap migrator -├── crates/vite_cli_snapshots/ # PTY snapshot test runner + vpt helper (new CLI tests) +├── packages/tools/ # Repo tooling and local npm registry +├── crates/vite_cli_snapshots/ # PTY snapshot test runner + vpt helper (CLI tests) ├── crates/vite_command/ # Shared command execution helpers ├── crates/vite_error/ # Shared error types ├── crates/vite_global_cli/ # Standalone global vp binary and top-level command routing @@ -62,9 +60,9 @@ vite-plus/ - **Bundled toolchain surfaces**: start with `packages/core/BUNDLING.md` and `packages/cli/BUNDLING.md`. - **Generated project agent guidance**: `packages/cli/AGENTS.md` and `packages/cli/src/utils/agent.ts`; do not edit these when the task is only to improve root repo guidance. - **Product/repo docs**: root contributor docs live at the repo root and the VitePress site under `docs/` (`docs/guide/`, `docs/config/`); generated agent guidance is separate. -- **CLI output behavior**: inspect the relevant code plus `crates/vite_cli_snapshots/tests/cli_snapshots/` (PTY snapshot suite; write new cases here). Legacy `packages/cli/snap-tests/` and `packages/cli/snap-tests-global/` are migration-only. +- **CLI output behavior**: inspect the relevant code plus `crates/vite_cli_snapshots/tests/cli_snapshots/` (PTY snapshot suite; write new cases here). - **Interactive CLI testing (prompts, pickers, keystrokes)**: `crates/vite_cli_snapshots/tests/cli_snapshots/README.md` and `rfcs/interactive-snapshot-tests.md`. -- **Install-testing against the local build**: `packages/tools/src/local-npm-registry.ts` serves the packed checkout behind a real registry interface; used by install snap fixtures (`localVitePlusPackages`), ecosystem e2e (`ecosystem-ci/patch-project.ts`), and local `vp migrate`/`vp create` iteration (see `CONTRIBUTING.md`). +- **Install-testing against the local build**: `packages/tools/src/local-npm-registry.ts` serves the packed checkout behind a real registry interface; used by PTY snapshot fixtures, ecosystem e2e (`ecosystem-ci/patch-project.ts`), and local `vp migrate`/`vp create` iteration (see `CONTRIBUTING.md`). ## Command and Config Model @@ -142,18 +140,6 @@ UPDATE_SNAPSHOTS=1 just snapshot-test # record/accept snapshots Snapshot mismatches fail the run with a unified diff and write `.md.new`; recorded `.md` snapshots are reviewed like code and committed with the fixture. Steps are argv arrays (no shell); use `vpt` subcommands instead of coreutils so cases stay platform-identical. Cases declare `vp = "local" | "global" | ["local", "global"]`; local-flavor cases require a fresh `packages/cli/dist`. -### Snap tests (legacy, being migrated) - -The old trees `packages/cli/snap-tests/` and `packages/cli/snap-tests-global/` still run in CI during the migration but must not receive new cases; convert them with `node packages/tools/src/bin.js migrate-snap-tests ...` instead (see the runner README). - -```bash -pnpm -F vite-plus snap-test -pnpm -F vite-plus snap-test-local -pnpm -F vite-plus snap-test-global -``` - -Legacy snap tests regenerate `snap.txt` and can exit successfully even when output changed. Always inspect `git diff` afterward. Ensure fixture inputs are committed or created by the test setup, not accidentally supplied by ignored local files. - ## Code Conventions ### Rust @@ -178,14 +164,13 @@ Reference these files instead of duplicating rules here: ## Testing Strategy -Use the validation matrix above as the source of truth. For behavior-bearing changes, find the nearest existing tests before editing and add or update coverage in the same area. For CLI output changes, pair focused tests with snap-test diff review. For documentation-only changes, verify referenced paths, commands, and links instead of running unrelated suites. +Use the validation matrix above as the source of truth. For behavior-bearing changes, find the nearest existing tests before editing and add or update coverage in the same area. For CLI output changes, pair focused tests with PTY snapshot diff review. For documentation-only changes, verify referenced paths, commands, and links instead of running unrelated suites. ## Common Pitfalls - **Treating Vite+ as only Vite Task**: Vite Task is integrated, but this repo spans CLI, runtime, package management, bundled packages, create/migrate, docs, and upstream integration. - **Looking for local `packages/test` or `crates/vite_task`**: neither is tracked here. Check `packages/cli/BUNDLING.md` for test shims and `Cargo.toml` for Vite Task git dependency wiring. - **Confusing built-ins with scripts**: `vp test` and `vp run test` can do different things. -- **Trusting snap-test exit status alone**: always inspect snapshot diffs. ## Debugging @@ -193,7 +178,7 @@ Use the validation matrix above as the source of truth. For behavior-bearing cha - Use `vp help` and `vp --help` to inspect command surfaces. - For command routing bugs, compare the global path (`crates/vite_global_cli/`) with the local/NAPI path (`packages/cli/src/bin.ts`, `packages/cli/binding/`). - For config-loading bugs, compare the static extractor (`crates/vite_static_config/`) with the JS resolver fallback (`packages/cli/src/resolve-vite-config.ts`). -- For package-manager behavior, inspect `crates/vite_pm_cli/`, `crates/vite_install/`, and related snap tests. +- For package-manager behavior, inspect `crates/vite_pm_cli/`, `crates/vite_install/`, and related PTY snapshot cases. - For `vp check`, lint, format, or type-check behavior, validate end-to-end because bundled-tool routing can hide silent config drift. ## AI Assistant Tips diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c25373fd24..d912f09000 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,7 @@ vp upgrade --force ## Validate the local build against a real project -Unit and snap tests don't cover everything. Interactive flows in particular (prompts, pickers, scaffolding) are easiest to validate by running your work-in-progress CLI inside a real Vite+ project. +Automated tests don't cover everything. Complex flows such as prompts, pickers, and scaffolding are also worth validating by running your work-in-progress CLI inside a real Vite+ project. First, understand how `vp` picks which `vite-plus` to run: for JS-backed commands (such as `vp create`), the global `vp` binary resolves `vite-plus` from the project's `node_modules` first and only falls back to the global installation in `~/.vite-plus`. If your test project has `vite-plus` installed from npm, `pnpm bootstrap-cli` alone will not make it run your local code. @@ -118,7 +118,7 @@ pnpm local-registry --pack --serve Notes: - The served versions carry an old publish time, so `minimumReleaseAge` gates never quarantine them, and wrapped runs get throwaway Yarn Berry / bun caches (both cache registry state in ways that would otherwise leak stale local builds between runs). -- The same server backs the install snap fixtures (`localVitePlusPackages` in `steps.json`) and ecosystem e2e (`ecosystem-ci/patch-project.ts`), so a flow that works here works there too. +- The same server backs PTY snapshot cases with `local-registry = true` and ecosystem e2e (`ecosystem-ci/patch-project.ts`), so a flow that works here works there too. - `pnpm local-registry:ps` lists any registry processes still running (e.g. a `--serve` you forgot, or a wrapper that was killed mid-run); `pnpm local-registry:kill` stops them all and removes their leftover temp caches. ### Global CLI (Rust) changes @@ -155,25 +155,6 @@ UPDATE_SNAPSHOTS=1 just snapshot-test create The full case/step/interaction reference (including the `vpt` helper tool and milestone conventions for interactive tests) lives in `crates/vite_cli_snapshots/tests/cli_snapshots/README.md`; the design rationale is in `rfcs/interactive-snapshot-tests.md`. -## Running Snap Tests (legacy) - -The legacy snap trees in `packages/cli/snap-tests/` (local CLI) and `packages/cli/snap-tests-global/` (global CLI) still run in CI while they are migrated to the PTY runner (`tool migrate-snap-tests`). Do not add new cases to them. - -```bash -# Run all snap tests (local + global) -pnpm -F vite-plus snap-test - -# Run only local CLI snap tests -pnpm -F vite-plus snap-test-local -pnpm -F vite-plus snap-test-local - -# Run only global CLI snap tests -pnpm -F vite-plus snap-test-global -pnpm -F vite-plus snap-test-global -``` - -Legacy snap tests auto-generate `snap.txt` files. Check `git diff` to verify output changes are correct. - ## Verified Commits All commits in PR branches should be GitHub-verified so reviewers can confirm commit authenticity. diff --git a/crates/vite_cli_snapshots/src/bin/vpt/chmod.rs b/crates/vite_cli_snapshots/src/bin/vpt/chmod.rs index ab66f87ab7..8eac04ece0 100644 --- a/crates/vite_cli_snapshots/src/bin/vpt/chmod.rs +++ b/crates/vite_cli_snapshots/src/bin/vpt/chmod.rs @@ -1,7 +1,7 @@ /// chmod `|+x` `` /// /// Sets POSIX permission bits; `+x` adds the execute bits to the current -/// mode (the common legacy-fixture form, `chmod +x hook.mjs`). Windows +/// mode (for example, `chmod +x hook.mjs`). Windows /// treats it as a validated no-op: the mode and target are still checked, /// so a typo or a failed earlier setup step fails on every platform. pub fn run(args: &[String]) -> Result<(), Box> { diff --git a/crates/vite_cli_snapshots/src/bin/vpt/json_edit.rs b/crates/vite_cli_snapshots/src/bin/vpt/json_edit.rs index d34c41aadd..689ffe3750 100644 --- a/crates/vite_cli_snapshots/src/bin/vpt/json_edit.rs +++ b/crates/vite_cli_snapshots/src/bin/vpt/json_edit.rs @@ -2,7 +2,7 @@ /// /// Sets a (possibly nested) key in a JSON file. `value` is parsed as JSON; /// if that fails it is treated as a plain string. Intermediate objects are -/// created as needed. Replaces the old snap-test `json-edit` helper. +/// created as needed. pub fn run(args: &[String]) -> Result<(), Box> { let [file, dot_path, raw_value] = args else { return Err("Usage: vpt json-edit ".into()); diff --git a/crates/vite_cli_snapshots/src/bin/vpt/list_dir.rs b/crates/vite_cli_snapshots/src/bin/vpt/list_dir.rs index ac145584c7..0cf5d38f5e 100644 --- a/crates/vite_cli_snapshots/src/bin/vpt/list_dir.rs +++ b/crates/vite_cli_snapshots/src/bin/vpt/list_dir.rs @@ -35,8 +35,8 @@ pub fn run(args: &[String]) -> Result<(), Box> { } let dir = dir.ok_or("Usage: vpt list-dir [--ext ] [--recursive] [--all]")?; - // Like `ls `, a file target prints its own name; legacy fixtures - // use that form as an existence assertion. + // Like `ls `, a file target prints its own name so it can serve as + // an existence assertion. let path = std::path::Path::new(dir); if path.is_file() { println!("{dir}"); diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/README.md b/crates/vite_cli_snapshots/tests/cli_snapshots/README.md index be3cd51f13..1d2b9b1c80 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/README.md @@ -5,9 +5,8 @@ real pseudo-terminal backed by a vt100 emulator, so interactive flows (prompts, pickers, watch modes, ctrl-c) are first-class testable surfaces. Snapshots are Markdown files with real pass/fail semantics. -**Write new CLI tests here.** The legacy trees (`packages/cli/snap-tests/`, -`packages/cli/snap-tests-global/`) are being migrated to this suite and -must not receive new cases. Design rationale: `rfcs/interactive-snapshot-tests.md`. +**Write new CLI tests here.** Design rationale: +`rfcs/interactive-snapshot-tests.md`. ## Quick start @@ -205,21 +204,3 @@ Fixture trees are excluded from repo-wide fmt, lint, typecheck, and vitest (`vite.config.ts`, `tsconfig.json`); recorded snapshots and `snapshots.toml` are runner metadata and never appear inside the staged workspace a test runs in. - -## Migrating a legacy case - -```bash -node packages/tools/src/bin.js migrate-snap-tests packages/cli/snap-tests --vp local -UPDATE_SNAPSHOTS=1 just snapshot-test -# review each new .md against the deleted snap.txt in git diff, then commit -``` - -The migrator converts `steps.json` fields, splits `&&` chains, maps shell -built-ins to `vpt`, removes cleanly converted (TODO-free) old case -directories (`--keep-old` defers that; git history keeps the originals), -and reports anything needing hand conversion in `MIGRATION-REPORT.md` -(generated, gitignored). Cases with TODOs keep their legacy dir until the -hand conversion lands, so placeholder steps never silently replace real -coverage. A case whose target fixture already exists is skipped and -reported: the same name in both legacy trees means a hand merge, usually a -second `[[case]]` in the fixture or a `vp = ["local", "global"]` matrix. diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/README.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/README.md index 5373623899..67edb76574 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/README.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/README.md @@ -2,7 +2,7 @@ `create-1.0.0.tgz` is committed binary. The mock manifest's `integrity: "sha512-…"` field locks the bytes — regenerating without -matching the original bit-for-bit will break the snap-test. +matching the original bit-for-bit will break the snapshot case. To regenerate (e.g. after editing one of the bundled template files): @@ -12,7 +12,7 @@ mkdir -p /tmp/vp-bundled-monorepo-build/package/templates/workspace cd /tmp/vp-bundled-monorepo-build COPYFILE_DISABLE=1 tar --no-mac-metadata -czf \ - /packages/cli/snap-tests/create-org-bundled-monorepo/tarballs/create-1.0.0.tgz \ + /crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/create-1.0.0.tgz \ package/ # Then update mock-manifest.json's `integrity` field: @@ -20,7 +20,7 @@ node -e " const fs = require('node:fs'); const crypto = require('node:crypto'); const buf = fs.readFileSync( - '/packages/cli/snap-tests/create-org-bundled-monorepo/tarballs/create-1.0.0.tgz', + '/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/tarballs/create-1.0.0.tgz', ); console.log('sha512-' + crypto.createHash('sha512').update(buf).digest('base64')); " @@ -28,4 +28,4 @@ node -e " `COPYFILE_DISABLE=1` and `--no-mac-metadata` keep macOS from injecting `._*` AppleDouble files that pollute the extracted output and break the -snap-test. +snapshot case. diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_nuxt_skip/eslint.config.mjs b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_nuxt_skip/eslint.config.mjs index e08ffd33f5..e1d5fb9ced 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_nuxt_skip/eslint.config.mjs +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_nuxt_skip/eslint.config.mjs @@ -1,5 +1,5 @@ // Stand-in for the auto-generated `.nuxt/eslint.config.mjs` flow. -// We don't actually re-export from `.nuxt/` here so the snap-test +// We don't actually re-export from `.nuxt/` here so the PTY snapshot // sandbox can load this file without running `nuxt prepare` first. export default [ { diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_type_aware/eslint.config.mjs b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_type_aware/eslint.config.mjs index 948cbedafb..22519056b9 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_type_aware/eslint.config.mjs +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_type_aware/eslint.config.mjs @@ -1,6 +1,6 @@ // Flat config exercising the type-aware sniffer without importing // typescript-eslint at runtime, so `@oxlint/migrate` can load the file -// in the snap-test sandbox where no node_modules are installed. +// in the PTY snapshot sandbox where no node_modules are installed. export default [ { languageOptions: { diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/flavor.rs b/crates/vite_cli_snapshots/tests/cli_snapshots/flavor.rs index eff72c6023..416f7b159c 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/flavor.rs +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/flavor.rs @@ -103,8 +103,7 @@ fn local_cli_package_dir() -> Result { dist_entry.display(), )); } - // A stale dist silently tests old code; fail fast when sources are newer - // (the legacy runner did the same for the global binary via mtimes). + // A stale dist silently tests old code; fail fast when sources are newer. // Skipped in CI, where dist is always freshly built, and under the // override, which points at another checkout on purpose. if overridden.is_none() && std::env::var_os("GITHUB_ACTIONS").is_none() { diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/main.rs b/crates/vite_cli_snapshots/tests/cli_snapshots/main.rs index 0572c62e3a..2394bd61eb 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/main.rs +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/main.rs @@ -89,7 +89,7 @@ struct StepTable { /// visible (`\x1b[…m`) so colour/style attributes are asserted. /// - `timeout`: per-step override in ms (default `STEP_TIMEOUT`). /// - `snapshot = false`: omit the screen while the step succeeds; failures -/// always keep their output (legacy `ignoreOutput` semantics). +/// always keep their output. /// - `tty = false`: piped stdio instead of a PTY, for non-TTY assertions; /// interactions require a PTY. /// - `continue_on_failure`: on failure, execution skips past the next step @@ -349,8 +349,7 @@ struct Case { #[serde(default = "default_true", rename = "seed-runtime")] seed_runtime: bool, /// Expose the run-root node_modules as the workspace's parent-dir - /// node_modules (the legacy runner's layout), for fixtures that address - /// the linked checkout packages by path (`node + /// node_modules for fixtures that address the linked checkout packages by path (`node /// ../node_modules/vite-plus/bin/oxlint`) rather than by specifier /// through Node's upward walk. #[serde(default, rename = "link-node-modules")] @@ -1119,8 +1118,8 @@ fn run_case( }; // Installs through the local registry are slower than pure vp commands, so - // local-registry steps get the legacy 120s default (still overridable - // per step); everything else keeps the standard per-step default. + // local-registry steps get a 120s default (still overridable per step); + // everything else keeps the standard per-step default. let step_default_timeout = if case.local_registry { Duration::from_secs(120) } else { STEP_TIMEOUT }; @@ -1328,8 +1327,7 @@ fn run_case( } // `snapshot = false` suppresses the screen only on success; failures - // always keep their output for diagnosis (legacy ignoreOutput - // semantics). + // always keep their output for diagnosis. let succeeded = matches!(termination_state, TerminationState::Exited(0)); if step.snapshot || !succeeded { let mut redacted = redact_output(raw_output, &redactions, !step.formatted_snapshot); @@ -1348,9 +1346,8 @@ fn run_case( // Shell-like `&&` semantics with line boundaries: a failing step // skips the rest of its line, up to and including the next - // continue-on-failure step (the line terminator in migrated - // fixtures), and the following line resumes, exactly the legacy - // model. Hand-written cases without markers stop here entirely. + // continue-on-failure step, then the following line resumes. + // Cases without markers stop here entirely. if !succeeded && !step.continue_on_failure { match case.steps[step_index + 1..].iter().position(|s| s.continue_on_failure) { Some(offset) => { diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs b/crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs index ca05806716..f4a3cbbfd9 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs @@ -1,7 +1,7 @@ //! Normalization of captured terminal screens before they enter a snapshot. //! -//! Deliberately minimal compared to the old snap-test `replaceUnstableOutput`: -//! grid rendering already removes ANSI noise, spinner frames, and +//! Redaction stays deliberately minimal: grid rendering already removes ANSI +//! noise, spinner frames, and //! stdout/stderr interleaving, so every rule here should correspond to a real //! source of nondeterminism (paths, durations, versions, machine parallelism). diff --git a/crates/vite_command/src/ps1_shim.rs b/crates/vite_command/src/ps1_shim.rs index 59c6c3d2ad..a04cccaa44 100644 --- a/crates/vite_command/src/ps1_shim.rs +++ b/crates/vite_command/src/ps1_shim.rs @@ -190,7 +190,7 @@ mod tests { } /// `pnpm`/`npm`/`yarn` `.ps1` wrappers introspect stdin and hang - /// when stdin is piped or null (CI, snap tests, scripted invocations). + /// when stdin is piped or null (CI, snapshot tests, scripted invocations). /// In that environment the rewrite is unwanted; the spawn falls back /// to `.cmd` directly. #[test] diff --git a/crates/vite_global_cli/src/js_executor.rs b/crates/vite_global_cli/src/js_executor.rs index a4e6eee9d0..3c2c6b92ae 100644 --- a/crates/vite_global_cli/src/js_executor.rs +++ b/crates/vite_global_cli/src/js_executor.rs @@ -635,8 +635,7 @@ mod tests { EnvConfig::test_guard(EnvConfig::for_test_with_home(vp_home.path().to_path_buf())); // Pin Node 20.0.0 via `.node-version`: well below the declared floor and - // exactly the case the removed gate rejected (see the deleted - // `runtime-with-incompatible-project-node` snap test). + // exactly the case the removed gate rejected. let project = TempDir::new().unwrap(); tokio::fs::write(project.path().join(".node-version"), "20.0.0\n").await.unwrap(); let project_path = AbsolutePathBuf::new(project.path().to_path_buf()).unwrap(); diff --git a/crates/vite_install/src/commands/approve_builds.rs b/crates/vite_install/src/commands/approve_builds.rs index c2986b806e..50ddc90b8e 100644 --- a/crates/vite_install/src/commands/approve_builds.rs +++ b/crates/vite_install/src/commands/approve_builds.rs @@ -565,7 +565,7 @@ mod tests { .expect("resolves"); // Still classified as a deny (starts with !), but the displayed name // in the warning retains the second `!`. We can only assert no-op here; - // visual inspection of the warn is captured in snap-tests. + // visual inspection of the warning is captured in PTY snapshots. assert!(result.is_none()); } @@ -855,6 +855,6 @@ mod tests { }) .expect("resolves"); assert!(result.is_none()); // still a no-op - // Visual inspection of the warn text is captured in snap-tests. + // Visual inspection of the warning is captured in PTY snapshots. } } diff --git a/packages/cli/src/migration/migrator/README.md b/packages/cli/src/migration/migrator/README.md index 9829767bb6..a41391a5c0 100644 --- a/packages/cli/src/migration/migrator/README.md +++ b/packages/cli/src/migration/migrator/README.md @@ -109,15 +109,11 @@ import from the barrel. From the repo root: ```bash -vp check # format + lint + type-check -pnpm -F vite-plus exec vitest run src/migration # migration unit tests -pnpm -F vite-plus snap-test-local migration # local migration snap tests -pnpm -F vite-plus snap-test-global migration # global migration snap tests +vp check # format + lint + type-check +pnpm -F vite-plus exec vitest run src/migration # migration unit tests +just snapshot-test migration # migration CLI snapshot tests ``` -Always inspect the `git diff` of `snap-tests*/**/snap.txt` afterwards — the -runner regenerates snapshots and can exit 0 even when output changed. - -A pure reorganization must not change `tsc` results, the unit-test count, or any -`snap.txt`. A behavior change should be accompanied by the matching test/snap -updates. +Review any changed `.md` snapshots like code. A pure reorganization must not +change type-check results, the unit-test count, or snapshots. A behavior change +should be accompanied by the matching unit or snapshot test updates. diff --git a/packages/tools/README.md b/packages/tools/README.md index 90811a72c6..1f5726d254 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -2,4 +2,3 @@ - json-edit: A CLI tool to edit JSON files such as package.json in e2e tests - json-sort: A CLI tool to sort JSON keys in a file -- snap-test: run snapshot tests for CLI diff --git a/packages/tools/src/local-npm-registry.ts b/packages/tools/src/local-npm-registry.ts index 579a615c1f..a626f0c779 100644 --- a/packages/tools/src/local-npm-registry.ts +++ b/packages/tools/src/local-npm-registry.ts @@ -9,11 +9,10 @@ // Runs directly with `node` (erasable-syntax TypeScript, no loader needed). // // Used by: -// - snap tests: the harness packs the checkout once per run (see -// `localVitePlusPackages` in `snap-test.ts`) and fixtures wrap commands with -// `node $SNAP_LOCAL_REGISTRY -- vp ...`. Fixtures may also provide a +// - PTY snapshot tests: the runner packs the checkout once per run and starts +// a registry for cases with `local-registry = true`. Fixtures may provide a // `./mock-manifest.json` (keyed by URL path, e.g. `"@your-org/create"`) and -// `./tarballs/` in their case directory (the `create-org-*` cases). +// `./tarballs/` in their case directory (the `create_org_*` cases). // - ecosystem e2e: `patch-project.ts` serves the e2e tgz artifacts with // `--packages-dir` so `vp migrate` / `vp install` resolve the local build // through the standard registry code paths. @@ -162,8 +161,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..' // `--pack-to `: pack the checkout packages into and exit, without // serving. The PTY snapshot runner calls this once per run, then points each -// case's `--serve` registry at the shared dir via SNAP_LOCAL_VP_PACKAGES_DIR -// (the packed-once-per-run optimization the legacy harness did in-process). +// case's `--serve` registry at the shared dir via SNAP_LOCAL_VP_PACKAGES_DIR. if (packTo) { await packLocalVitePlusPackages(repoRoot, packTo); process.exit(0); diff --git a/packages/tools/src/pack-local-vite-plus.ts b/packages/tools/src/pack-local-vite-plus.ts index aaa9cd0b6f..642a623c98 100644 --- a/packages/tools/src/pack-local-vite-plus.ts +++ b/packages/tools/src/pack-local-vite-plus.ts @@ -1,7 +1,7 @@ // Pack the checkout's publishable Vite+ packages (vite-plus and // @voidzero-dev/vite-plus-core) so a local npm registry can serve them at the -// checkout version. Shared by the snap-test harness (once-per-run pack) and -// local-npm-registry.ts --pack (standalone dev / e2e invocations). +// checkout version. Used by local-npm-registry.ts for snapshot tests, e2e, +// and standalone development. // // Uses node builtins only and erasable TypeScript syntax, so // local-npm-registry.ts stays runnable with bare `node` from any directory.