diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index 3f7d4ac1ad..e48594c9ba 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -152,13 +152,31 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise ({ + name: pkg, + pattern: new RegExp(`'${pkg.replaceAll('/', '\\/')}': ([\\d.]+(?:-[\\w.]+)?)`), + replacement: `'${pkg}': ${versions.vitest}`, + newVersion: versions.vitest, + })); const entries: PnpmWorkspaceEntry[] = [ { name: 'vitest', - pattern: /vitest-dev: npm:vitest@\^([\d.]+(?:-[\w.]+)?)/, - replacement: `vitest-dev: npm:vitest@^${versions.vitest}`, + // The `@voidzero-dev/vite-plus-test` wrapper (which used to be aliased + // here via `vitest-dev: npm:vitest@^…`) has been removed. Vitest is now + // a plain catalog entry pinned to an exact version (`vitest: x.y.z`), + // so match that shape directly. The leading newline anchor disambiguates + // from neighbouring keys like `vitepress-*` and `@vitest/browser`. + pattern: /\n {2}vitest: ([\d.]+(?:-[\w.]+)?)\n/, + replacement: `\n vitest: ${versions.vitest}\n`, newVersion: versions.vitest, }, + ...vitestBrowserEntries, { name: 'tsdown', pattern: /tsdown: \^([\d.]+(?:-[\w.]+)?)/, @@ -246,34 +264,117 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise { - const filePath = path.join(ROOT, 'packages/test/package.json'); - const pkg: PackageJson = readJsonFile(filePath); - const devDependencies = pkg.devDependencies; - if (!devDependencies) { - throw new Error('packages/test/package.json is missing devDependencies'); +// ============ Update VITEST_VERSION constant ============ +// Keeps the TypeScript source-of-truth (`packages/cli/src/utils/constants.ts`) +// in sync with the `vitest:` catalog entry in pnpm-workspace.yaml. The +// constant is consumed by both `packages/cli` and `ecosystem-ci/patch-project.ts` +// (which re-imports it), so daily upstream bumps must update it here too. +async function updateVitestVersionConstant(vitestVersion: string): Promise { + const filePath = path.join(ROOT, 'packages/cli/src/utils/constants.ts'); + const content = fs.readFileSync(filePath, 'utf8'); + const pattern = /export const VITEST_VERSION = '([\d.]+(?:-[\w.]+)?)';/; + let oldVersion: string | undefined; + const updated = content.replace(pattern, (_match: string, captured: string) => { + oldVersion = captured; + return `export const VITEST_VERSION = '${vitestVersion}';`; + }); + if (oldVersion === undefined) { + throw new Error( + `Failed to match VITEST_VERSION in ${filePath} — the pattern ${pattern} is stale, ` + + `please update it in .github/scripts/upgrade-deps.ts`, + ); } + fs.writeFileSync(filePath, updated); + recordChange('VITEST_VERSION constant', oldVersion, vitestVersion); + console.log('Updated packages/cli/src/utils/constants.ts'); +} - // Update all @vitest/* devDependencies - for (const dep of Object.keys(devDependencies)) { - if (dep.startsWith('@vitest/')) { - devDependencies[dep] = vitestVersion; +// ============ Update .github/workflows/test-vp-create.yml ============ +// The `vp create` smoke-test workflow pins every vitest-family package via the +// `VP_OVERRIDE_PACKAGES` env var so that template installs use the bundled +// version. Daily upstream bumps must rewrite those pins so the workflow does +// not drift behind the rest of the repo. +async function updateTestVpCreateWorkflow(vitestVersion: string): Promise { + const filePath = path.join(ROOT, '.github/workflows/test-vp-create.yml'); + const content = fs.readFileSync(filePath, 'utf8'); + const vitestKeys = [ + 'vitest', + '@vitest/expect', + '@vitest/runner', + '@vitest/snapshot', + '@vitest/spy', + '@vitest/utils', + '@vitest/mocker', + '@vitest/pretty-format', + '@vitest/coverage-v8', + '@vitest/coverage-istanbul', + ]; + let updated = content; + for (const key of vitestKeys) { + const pattern = new RegExp(`"${key.replaceAll('/', '\\/')}":"([\\d.]+(?:-[\\w.]+)?)"`); + let matched = false; + updated = updated.replace(pattern, (_match: string, _captured: string) => { + matched = true; + return `"${key}":"${vitestVersion}"`; + }); + if (!matched) { + throw new Error( + `Failed to match "${key}" in ${filePath} — the pattern ${pattern} is stale, ` + + `please update it in .github/scripts/upgrade-deps.ts`, + ); } } + fs.writeFileSync(filePath, updated); + console.log('Updated .github/workflows/test-vp-create.yml'); +} - // Update vitest-dev devDependency - if (devDependencies['vitest-dev']) { - devDependencies['vitest-dev'] = `^${vitestVersion}`; +// ============ Update the @vitest/mocker pnpm patch entry ============ +// `pnpm-workspace.yaml` patches `@vitest/mocker` so the static `vi.mock()` +// hoister recognizes the public `vite-plus/test` specifier. pnpm keys +// `patchedDependencies` by EXACT version (`@vitest/mocker@x.y.z`) and errors +// hard (`ERR_PNPM_PATCHED_PKG_DOES_NOT_MATCH`) when the key drifts from the +// installed version, so a daily vitest bump would otherwise break every +// auto-upgrade PR. Rewrite the key + patch-file path and rename the patch file +// to the new version. The patch file is kept version-suffixed (rather than +// switching to a name-only key) because its diff context is version-specific: +// if upstream changes `dist/chunk-hoistMocks.js`, the rename surfaces a loud +// patch-apply failure that a human must resolve — which is the desired signal. +async function updateVitestMockerPatch(vitestVersion: string): Promise { + const filePath = path.join(ROOT, 'pnpm-workspace.yaml'); + const content = fs.readFileSync(filePath, 'utf8'); + const pattern = + /'@vitest\/mocker@([\d.]+(?:-[\w.]+)?)': patches\/@vitest__mocker@[\d.]+(?:-[\w.]+)?\.patch/; + let oldVersion: string | undefined; + const updated = content.replace(pattern, (_match: string, captured: string) => { + oldVersion = captured; + return `'@vitest/mocker@${vitestVersion}': patches/@vitest__mocker@${vitestVersion}.patch`; + }); + if (oldVersion === undefined) { + throw new Error( + `Failed to match the @vitest/mocker patchedDependencies entry in ${filePath} — ` + + `the pattern ${pattern} is stale, please update it in .github/scripts/upgrade-deps.ts`, + ); } - - // Update @vitest/ui peerDependency if present - if (pkg.peerDependencies?.['@vitest/ui']) { - pkg.peerDependencies['@vitest/ui'] = vitestVersion; + if (oldVersion !== vitestVersion) { + const oldPatch = path.join(ROOT, 'patches', `@vitest__mocker@${oldVersion}.patch`); + const newPatch = path.join(ROOT, 'patches', `@vitest__mocker@${vitestVersion}.patch`); + if (!fs.existsSync(oldPatch)) { + throw new Error( + `Expected patch file ${oldPatch} to exist before renaming — ` + + `the @vitest/mocker patch may have been moved or removed.`, + ); + } + fs.renameSync(oldPatch, newPatch); + console.log(`Renamed @vitest/mocker patch ${oldVersion} -> ${vitestVersion}`); } - - fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n'); - console.log('Updated packages/test/package.json'); + // Also covers the case where the key version already matches `vitestVersion` + // but the value's patch-file suffix had drifted — `content.replace` repaired + // the line in memory and we must persist it, otherwise pnpm install can hit + // ERR_PNPM_PATCHED_PKG_DOES_NOT_MATCH. + if (updated !== content) { + fs.writeFileSync(filePath, updated); + } + recordChange('@vitest/mocker patch', oldVersion, vitestVersion); } // ============ Update packages/core/package.json ============ @@ -430,7 +531,9 @@ await updatePnpmWorkspace({ oxcParser: oxcParserVersion, oxcTransform: oxcTransformVersion, }); -await updateTestPackage(vitestVersion); +await updateVitestVersionConstant(vitestVersion); +await updateTestVpCreateWorkflow(vitestVersion); +await updateVitestMockerPatch(vitestVersion); await updateCorePackage(devtoolsVersion); writeMetaFiles(); diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 7f864fcb31..03315ec9bc 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -108,9 +108,32 @@ jobs: - name: Pack packages into tgz run: | mkdir -p tmp/tgz + # Every tgz consumer below references fixed `*-0.0.0.tgz` filenames. + # A release commit can leave `packages/{core,cli}` at a published + # version (e.g. 0.1.22), which would make `pnpm pack` emit + # `*-0.1.22.tgz` instead. Pin both to 0.0.0 so the names stay stable. + (cd packages/core && npm pkg set version=0.0.0) + (cd packages/cli && npm pkg set version=0.0.0) cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. - cd packages/test && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. + # Bun is uniquely strict about peer-dep resolution: + # 1. It checks the *resolved target's* package name and version + # against the peer range (vitest 4.1.7 declares peer + # `vite ^6 || ^7 || ^8`). + # 2. A file: override pointing at the vite-plus-core tgz fails + # both the name check (target is `@voidzero-dev/vite-plus-core`, + # not `vite`) and the version check (0.0.0 is outside `^6|^7|^8`). + # pnpm/npm/yarn don't enforce either, and using the same core tgz as + # the file: target for both `vite` and `@voidzero-dev/vite-plus-core` + # is the only configuration they install cleanly. See + # https://github.com/oven-sh/bun/issues/8406. + # + # Generate a sibling vite-7.99.0.tgz: a copy of the core tgz with + # `package.json#name` rewritten to "vite" and `version` to 7.99.0. + # ecosystem-ci/patch-project.ts points its vite override at this + # tgz only for bun-based projects (e.g. bun-vite-template); pnpm-, + # npm- and yarn-based ecosystem projects use the real core tgz. + pnpm exec tool repack-vite-tgz tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz tmp/tgz/vite-7.99.0.tgz vite 7.99.0 # Copy vp binary for e2e-test job (findVpBinary expects it in target/) cp target/${{ matrix.target }}/release/vp tmp/tgz/vp 2>/dev/null || cp target/${{ matrix.target }}/release/vp.exe tmp/tgz/vp.exe 2>/dev/null || true cp target/${{ matrix.target }}/release/vp-shim.exe tmp/tgz/vp-shim.exe 2>/dev/null || true diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml index 2b7d3f37f7..3076ee20b0 100644 --- a/.github/workflows/prepare_release.yml +++ b/.github/workflows/prepare_release.yml @@ -54,7 +54,6 @@ jobs: } update_json packages/cli/package.json update_json packages/core/package.json - update_json packages/test/package.json update_toml packages/cli/binding/Cargo.toml update_toml crates/vite_global_cli/Cargo.toml diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index e343d57855..e367da6166 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -136,5 +136,4 @@ jobs: './packages/cli/cli-npm/*' \ './packages/cli' \ './packages/core' \ - './packages/prompts' \ - './packages/test' + './packages/prompts' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1539b7291f..97bd0e0c0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,7 +161,6 @@ jobs: - name: Publish run: | pnpm publish --filter=./packages/core --tag latest --access public --no-git-checks - pnpm publish --filter=./packages/test --tag latest --access public --no-git-checks pnpm publish --filter=./packages/cli --tag latest --access public --no-git-checks - name: Create release body @@ -177,7 +176,6 @@ jobs: ### Published Packages - \`@voidzero-dev/vite-plus-core@${VERSION}\` - - \`@voidzero-dev/vite-plus-test@${VERSION}\` - \`vite-plus@${VERSION}\` ### Installation @@ -228,7 +226,6 @@ jobs: **Published Packages:** • @voidzero-dev/vite-plus-core@${{ env.VERSION }} - • @voidzero-dev/vite-plus-test@${{ env.VERSION }} • vite-plus@${{ env.VERSION }} **Install:** diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 278579a0fc..fd5f621de0 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -94,9 +94,34 @@ jobs: - name: Pack packages into tgz run: | mkdir -p tmp/tgz + # Every tgz consumer below references fixed `*-0.0.0.tgz` filenames. + # A release commit can leave `packages/{core,cli}` at a published + # version (e.g. 0.1.22), which would make `pnpm pack` emit + # `*-0.1.22.tgz` instead. Pin both to 0.0.0 so the names stay stable. + (cd packages/core && npm pkg set version=0.0.0) + (cd packages/cli && npm pkg set version=0.0.0) cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. - cd packages/test && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. + # Bun is uniquely strict about peer-dep resolution: + # 1. It checks the *resolved target's* package name and version + # against the peer range (vitest 4.1.7 declares peer + # `vite ^6 || ^7 || ^8`). + # 2. A file: override pointing at the vite-plus-core tgz fails + # both the name check (target is `@voidzero-dev/vite-plus-core`, + # not `vite`) and the version check (0.0.0 is outside `^6|^7|^8`). + # pnpm/npm/yarn don't enforce either, and using the same core tgz as + # the file: target for both `vite` and `@voidzero-dev/vite-plus-core` + # is the only configuration they install cleanly. See + # https://github.com/oven-sh/bun/issues/8406. + # + # Generate a sibling vite-7.99.0.tgz: a copy of the core tgz with + # `package.json#name` rewritten to "vite" and `version` to 7.99.0. + # Only the bun matrix entry below points its vite override at this + # alias tgz; pnpm/npm/yarn keep pointing at the real core tgz so + # pnpm's workspace resolver doesn't trip on a "vite@" + # registry lookup (the renamed tgz makes pnpm register the dep as + # vite@7.99.0 and then probe npmjs.org to validate the version). + pnpm exec tool repack-vite-tgz tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz tmp/tgz/vite-7.99.0.tgz vite 7.99.0 # Copy vp binary for test jobs cp target/x86_64-unknown-linux-gnu/release/vp tmp/tgz/vp ls -la tmp/tgz @@ -153,6 +178,13 @@ jobs: - yarn - bun env: + # Bun's strict peer check requires the `vite` override target's tgz to be + # named "vite" with a version satisfying vitest's `peer vite ^6 || ^7 || ^8`. + # The bun matrix entry uses the masquerade tgz (vite-7.99.0.tgz). pnpm/npm/yarn + # point at the real core tgz — anything else trips a registry lookup for + # vite@ when sub-package and override targets are both file: tgz aliases. + VITE_OVERRIDE_TGZ: ${{ matrix.package-manager == 'bun' && 'vite-7.99.0.tgz' || 'voidzero-dev-vite-plus-core-0.0.0.tgz' }} + VP_VERSION: 'file:${{ github.workspace }}/tmp/tgz/vite-plus-0.0.0.tgz' # Force full dependency rewriting so the library template's existing # vite-plus dep gets overridden with the local tgz VP_FORCE_MIGRATE: '1' @@ -169,22 +201,12 @@ jobs: name: vite-plus-packages path: tmp/tgz - - name: Resolve tgz paths - run: | - CLI_TGZ=$(ls "$GITHUB_WORKSPACE"/tmp/tgz/vite-plus-[0-9]*.tgz | head -n1) - CORE_TGZ=$(ls "$GITHUB_WORKSPACE"/tmp/tgz/voidzero-dev-vite-plus-core-[0-9]*.tgz | head -n1) - TEST_TGZ=$(ls "$GITHUB_WORKSPACE"/tmp/tgz/voidzero-dev-vite-plus-test-[0-9]*.tgz | head -n1) - echo "CLI_TGZ=$CLI_TGZ" >> $GITHUB_ENV - echo "VP_VERSION=file:$CLI_TGZ" >> $GITHUB_ENV - printf 'VP_OVERRIDE_PACKAGES={"vite":"file:%s","vitest":"file:%s","@voidzero-dev/vite-plus-core":"file:%s","@voidzero-dev/vite-plus-test":"file:%s"}\n' \ - "$CORE_TGZ" "$TEST_TGZ" "$CORE_TGZ" "$TEST_TGZ" >> $GITHUB_ENV - - name: Install vp CLI run: | mkdir -p target/release cp tmp/tgz/vp target/release/vp chmod +x target/release/vp - node $GITHUB_WORKSPACE/packages/tools/src/install-global-cli.ts --tgz "$CLI_TGZ" + node $GITHUB_WORKSPACE/packages/tools/src/install-global-cli.ts --tgz $GITHUB_WORKSPACE/tmp/tgz/vite-plus-0.0.0.tgz echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH - name: Verify vp installation @@ -194,6 +216,8 @@ jobs: - name: Run vp create ${{ matrix.template.name }} with ${{ matrix.package-manager }} working-directory: ${{ runner.temp }} + env: + VP_OVERRIDE_PACKAGES: '{"vite":"file:${{ github.workspace }}/tmp/tgz/${{ env.VITE_OVERRIDE_TGZ }}","@voidzero-dev/vite-plus-core":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz","vitest":"4.1.7","@vitest/expect":"4.1.7","@vitest/runner":"4.1.7","@vitest/snapshot":"4.1.7","@vitest/spy":"4.1.7","@vitest/utils":"4.1.7","@vitest/mocker":"4.1.7","@vitest/pretty-format":"4.1.7","@vitest/coverage-v8":"4.1.7","@vitest/coverage-istanbul":"4.1.7"}' run: | vp create ${{ matrix.template.create-args }} \ --no-interactive \ @@ -267,10 +291,9 @@ jobs: run: | node -e " const path = require('path'); - const expected = require('$GITHUB_WORKSPACE/packages/cli/package.json').version; const pkg = require(path.resolve('node_modules/vite-plus/package.json')); - if (pkg.version !== expected) { - console.error('Expected vite-plus@' + expected + ', got ' + pkg.version); + if (pkg.version !== '0.0.0') { + console.error('Expected vite-plus@0.0.0, got ' + pkg.version); process.exit(1); } console.log('✓ vite-plus@' + pkg.version + ' installed correctly'); diff --git a/.typos.toml b/.typos.toml index 996242eb1f..c50f7dc0e1 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,6 +4,12 @@ PUNICODE = "PUNICODE" Jod = "Jod" # Node.js v22 LTS codename flate = "flate" # flate2 crate name (gzip/deflate compression) +[default.extend-identifiers] +# Yarn Plug'n'Play — tokenizer splits `PnP` into the word `Pn`, which typos +# otherwise wants to autocorrect to `On`. Whitelist the full identifier so +# the parts aren't tokenized. +PnP = "PnP" + [files] extend-exclude = [ "**/snap-tests/**/snap.txt", diff --git a/README.md b/README.md index e9019fc152..c31da0cb13 100644 --- a/README.md +++ b/README.md @@ -189,12 +189,11 @@ If you are manually migrating a project to Vite+, install these dev dependencies npm install -D vite-plus @voidzero-dev/vite-plus-core@latest ``` -You need to add overrides to your package manager for `vite` and `vitest` so that other packages depending on Vite and Vitest will use the Vite+ versions: +You need to add overrides to your package manager for `vite` so that other packages depending on Vite (including Vitest) will use the Vite+ version: ```json "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } ``` @@ -203,15 +202,13 @@ If you are using `pnpm`, add this to your `pnpm-workspace.yaml`: ```yaml overrides: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest ``` Or, if you are using Yarn: ```json "resolutions": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } ``` diff --git a/crates/vite_global_cli/src/commands/version.rs b/crates/vite_global_cli/src/commands/version.rs index 6d420c1a0b..2723260eba 100644 --- a/crates/vite_global_cli/src/commands/version.rs +++ b/crates/vite_global_cli/src/commands/version.rs @@ -47,11 +47,7 @@ const TOOL_SPECS: [ToolSpec; 7] = [ package_name: "@voidzero-dev/vite-plus-core", bundled_version_key: Some("rolldown"), }, - ToolSpec { - display_name: "vitest", - package_name: "@voidzero-dev/vite-plus-test", - bundled_version_key: Some("vitest"), - }, + ToolSpec { display_name: "vitest", package_name: "vitest", bundled_version_key: None }, ToolSpec { display_name: "oxfmt", package_name: "oxfmt", bundled_version_key: None }, ToolSpec { display_name: "oxlint", package_name: "oxlint", bundled_version_key: None }, ToolSpec { diff --git a/crates/vite_migration/src/file_walker.rs b/crates/vite_migration/src/file_walker.rs index 9ab86a638c..83e4e48c7e 100644 --- a/crates/vite_migration/src/file_walker.rs +++ b/crates/vite_migration/src/file_walker.rs @@ -3,9 +3,12 @@ use std::path::{Path, PathBuf}; use ignore::WalkBuilder; use vite_error::Error; -// TODO: only support esm files for now -/// File extensions to process for import rewriting -const TS_JS_EXTENSIONS: &[&str] = &["ts", "tsx", "mts", "js", "jsx", "mjs"]; +/// File extensions to process for import rewriting. +/// +/// Includes CommonJS (`.cjs`/`.cts`) variants in addition to ESM, because +/// the legacy reverse-migration rules also cover `require('vite-plus/test/browser-*')` +/// calls that are most commonly found in `.cjs` / `vitest.config.cjs` files. +const TS_JS_EXTENSIONS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]; /// Result of walking TypeScript/JavaScript files #[derive(Debug)] @@ -164,7 +167,7 @@ mod tests { let result = find_ts_files(temp.path()).unwrap(); - assert_eq!(result.files.len(), 6); + assert_eq!(result.files.len(), 8); } #[test] diff --git a/crates/vite_migration/src/import_rewriter.rs b/crates/vite_migration/src/import_rewriter.rs index 175ef58491..3f0dc0e342 100644 --- a/crates/vite_migration/src/import_rewriter.rs +++ b/crates/vite_migration/src/import_rewriter.rs @@ -17,220 +17,1360 @@ use crate::{ast_grep, file_walker}; /// This rewrites: /// - `import { ... } from 'vite'` → `import { ... } from 'vite-plus'` /// - `import { ... } from 'vite/{name}'` → `import { ... } from 'vite-plus/{name}'` +/// - `require('vite')` → `require('vite-plus')` +/// - `require('vite/{name}')` → `require('vite-plus/{name}')` /// - `declare module 'vite' { ... }` → `declare module 'vite-plus' { ... }` /// - `declare module 'vite/{name}' { ... }` → `declare module 'vite-plus/{name}' { ... }` +/// +/// The `require(...)` sibling rules constrain themselves to a literal `require` +/// callee so unrelated calls like `my_require('vite')` or `require.cache['vite']` +/// are NOT touched. The `*-dynamic-import` sibling rules match dynamic +/// `import('vite')` calls (and TS type-position `typeof import('vite')`) by +/// pinning the call_expression's `function` field to the `import` token kind, +/// which excludes ordinary identifier calls. Together with the upstream +/// `import_statement` rule (covering ES `import { ... } from 'vite'`), this +/// gives full coverage of static imports/exports, dynamic imports, and +/// CommonJS requires. const REWRITE_VITE_RULES: &str = r#"--- id: rewrite-vite-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]vite['"]$ + regex: ^['"]vite['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vite-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vite-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vite-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vite-subpath-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite/.+['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite/ + by: "vite-plus/" +fix: $NEW_IMPORT +--- +id: rewrite-vite-subpath-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite/.+['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite/ + by: "vite-plus/" +fix: $NEW_IMPORT +--- +id: rewrite-vite-subpath-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite/.+['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite/ + by: "vite-plus/" +fix: $NEW_IMPORT +--- +id: rewrite-vite-subpath-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vite/.+['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite/ + by: "vite-plus/" +fix: $NEW_IMPORT +--- +id: rewrite-declare-module-vite +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['\"]vite['\"]$ + inside: + kind: module +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-declare-module-vite-subpath +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['\"]vite/.+['\"]$ + inside: + kind: module +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vite/ + by: "vite-plus/" +fix: $NEW_IMPORT +"#; + +/// ast-grep rules for rewriting vitest imports. +/// +/// This rewrites (the canonical mapping shared with the `oxlint-plugin.ts` +/// `rewriteVitePlusImportSpecifier` autofix — both implementations MUST stay +/// in sync and only produce targets that exist in the `vite-plus` package +/// `exports` map, otherwise Node fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`): +/// - `import { ... } from 'vitest'` → `import { ... } from 'vite-plus/test'` +/// - `import { ... } from 'vitest/config'` → `import { ... } from 'vite-plus'` +/// - `import { ... } from 'vitest/{name}'` → `import { ... } from 'vite-plus/test/{name}'` +/// - `import { ... } from '@vitest/browser'` → `import { ... } from 'vite-plus/test/browser'` +/// - `import { ... } from '@vitest/browser/context'` → `import { ... } from 'vite-plus/test/browser/context'` +/// - `import { ... } from '@vitest/browser/client'` → `import { ... } from 'vite-plus/test/client'` +/// - `import { ... } from '@vitest/browser/locators'` → `import { ... } from 'vite-plus/test/locators'` +/// - `import { ... } from '@vitest/browser/matchers'` → `import { ... } from 'vite-plus/test/matchers'` +/// - `import { ... } from '@vitest/browser/utils'` → `import { ... } from 'vite-plus/test/utils'` +/// +/// Note: `vite-plus` only exports `./test/browser/context` under the nested +/// `browser/` path; `client`, `locators`, `matchers` and `utils` are exposed +/// at the bare `./test/{name}` surface, so the `/browser/` segment is stripped. +/// - `import { ... } from '@vitest/browser-playwright'` → `import { ... } from 'vite-plus/test/browser-playwright'` +/// - `import { ... } from '@vitest/browser-playwright/context'` → `import { ... } from 'vite-plus/test/browser/context'` +/// - `import { ... } from '@vitest/browser-playwright/provider'` → `import { ... } from 'vite-plus/test/browser/providers/playwright'` +/// - `import { ... } from '@vitest/browser-preview'` → `import { ... } from 'vite-plus/test/browser-preview'` +/// - `import { ... } from '@vitest/browser-preview/context'` → `import { ... } from 'vite-plus/test/browser/context'` +/// - `import { ... } from '@vitest/browser-preview/provider'` → `import { ... } from 'vite-plus/test/browser/providers/preview'` +/// - `import { ... } from '@vitest/browser-webdriverio'` → `import { ... } from 'vite-plus/test/browser-webdriverio'` +/// - `import { ... } from '@vitest/browser-webdriverio/context'` → `import { ... } from 'vite-plus/test/browser/context'` +/// - `import { ... } from '@vitest/browser-webdriverio/provider'` → `import { ... } from 'vite-plus/test/browser/providers/webdriverio'` +/// +/// `declare module 'vitest' { ... }` (and the subpath/`@vitest/*` variants) are +/// intentionally NOT rewritten — the `vite-plus/test*` subpaths are thin shims +/// that `export * from 'vitest'` (and the browser provider packages), so the +/// underlying type identity is upstream. Augmenting `'vite-plus/test'` would +/// only augment the shim module and would not merge into the upstream types +/// the user actually sees through their `import { expect } from 'vite-plus/test'` +/// statements. Leaving the `declare module 'vitest' { ... }` alone keeps +/// augmentations targeting the real upstream module identity. +/// +/// `vitest/package.json` is also intentionally NOT rewritten. It is a +/// metadata-access pattern (typically used to read the vitest version) and +/// `vite-plus`'s generated exports map deliberately does not expose +/// `./test/package.json` (see `syncTestPackageExports()` in +/// `packages/cli/build.ts`, which skips the upstream `./package.json` +/// export). Rewriting it would yield `vite-plus/test/package.json`, which +/// fails at runtime with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The original +/// specifier still resolves through the transitively-installed `vitest` +/// dependency. +const REWRITE_VITEST_RULES: &str = r#"--- +id: rewrite-vitest-config-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest/config['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest/config + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-config-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest/config['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest/config + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-config-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest/config['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest/config + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-config-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest/config['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest/config + by: "vite-plus" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest + by: "vite-plus/test" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest + by: "vite-plus/test" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest + by: "vite-plus/test" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]vitest['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: vitest + by: "vite-plus/test" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser" + by: "vite-plus/test/browser" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser" + by: "vite-plus/test/browser" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser" + by: "vite-plus/test/browser" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser" + by: "vite-plus/test/browser" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-context-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/context['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-context-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/context['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-context-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-context-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-flat-subpath-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/(client|locators|matchers|utils)['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/" + by: "vite-plus/test/" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-flat-subpath-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/(client|locators|matchers|utils)['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/" + by: "vite-plus/test/" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-flat-subpath-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/(client|locators|matchers|utils)['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/" + by: "vite-plus/test/" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-flat-subpath-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser/(client|locators|matchers|utils)['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser/" + by: "vite-plus/test/" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright" + by: "vite-plus/test/browser-playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright" + by: "vite-plus/test/browser-playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright" + by: "vite-plus/test/browser-playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright" + by: "vite-plus/test/browser-playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-context-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/context['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-context-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/context['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-context-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-context-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-provider-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/provider['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/provider" + by: "vite-plus/test/browser/providers/playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-provider-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/provider['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/provider" + by: "vite-plus/test/browser/providers/playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-provider-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/provider['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/provider" + by: "vite-plus/test/browser/providers/playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-playwright-provider-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-playwright/provider['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-playwright/provider" + by: "vite-plus/test/browser/providers/playwright" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview" + by: "vite-plus/test/browser-preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview" + by: "vite-plus/test/browser-preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview" + by: "vite-plus/test/browser-preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview" + by: "vite-plus/test/browser-preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-context-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/context['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-context-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/context['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-context-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-context-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/context['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/context" + by: "vite-plus/test/browser/context" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-provider-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/provider['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/provider" + by: "vite-plus/test/browser/providers/preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-provider-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/provider['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/provider" + by: "vite-plus/test/browser/providers/preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-provider-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/provider['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/provider" + by: "vite-plus/test/browser/providers/preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-preview-provider-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-preview/provider['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-preview/provider" + by: "vite-plus/test/browser/providers/preview" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-webdriverio-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-webdriverio['"]$ + inside: + kind: import_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-webdriverio" + by: "vite-plus/test/browser-webdriverio" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-webdriverio-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-webdriverio['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-webdriverio" + by: "vite-plus/test/browser-webdriverio" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-webdriverio-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-webdriverio['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-webdriverio" + by: "vite-plus/test/browser-webdriverio" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-webdriverio-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-webdriverio['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: "@vitest/browser-webdriverio" + by: "vite-plus/test/browser-webdriverio" +fix: $NEW_IMPORT +--- +id: rewrite-vitest-browser-webdriverio-context-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]@vitest/browser-webdriverio/context['"]$ inside: kind: import_statement transform: NEW_IMPORT: replace: source: $STR - replace: vite - by: "vite-plus" + replace: "@vitest/browser-webdriverio/context" + by: "vite-plus/test/browser/context" fix: $NEW_IMPORT --- -id: rewrite-vite-subpath-import +id: rewrite-vitest-browser-webdriverio-context-export language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]vite/.+['"]$ + regex: ^['"]@vitest/browser-webdriverio/context['"]$ inside: - kind: import_statement + kind: export_statement transform: NEW_IMPORT: replace: source: $STR - replace: vite/ - by: "vite-plus/" + replace: "@vitest/browser-webdriverio/context" + by: "vite-plus/test/browser/context" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vite +id: rewrite-vitest-browser-webdriverio-context-require language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]vite['\"]$ + regex: ^['"]@vitest/browser-webdriverio/context['"]$ inside: - kind: module + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ transform: NEW_IMPORT: replace: source: $STR - replace: vite - by: "vite-plus" + replace: "@vitest/browser-webdriverio/context" + by: "vite-plus/test/browser/context" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vite-subpath +id: rewrite-vitest-browser-webdriverio-context-dynamic-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]vite/.+['\"]$ + regex: ^['"]@vitest/browser-webdriverio/context['"]$ inside: - kind: module + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import transform: NEW_IMPORT: replace: source: $STR - replace: vite/ - by: "vite-plus/" + replace: "@vitest/browser-webdriverio/context" + by: "vite-plus/test/browser/context" fix: $NEW_IMPORT -"#; - -/// ast-grep rules for rewriting vitest imports and declare module statements -/// -/// This rewrites: -/// - `import { ... } from 'vitest'` → `import { ... } from 'vite-plus/test'` -/// - `import { ... } from 'vitest/config'` → `import { ... } from 'vite-plus'` -/// - `import { ... } from 'vitest/{name}'` → `import { ... } from 'vite-plus/test/{name}'` -/// - `import { ... } from '@vitest/browser'` → `import { ... } from 'vite-plus/test/browser'` -/// - `import { ... } from '@vitest/browser/{name}'` → `import { ... } from 'vite-plus/test/browser/{name}'` -/// - `import { ... } from '@vitest/browser-playwright'` → `import { ... } from 'vite-plus/test/browser-playwright'` -/// - `import { ... } from '@vitest/browser-playwright/{name}'` → `import { ... } from 'vite-plus/test/browser-playwright/{name}'` -/// - `import { ... } from '@vitest/browser-preview'` → `import { ... } from 'vite-plus/test/browser-preview'` -/// - `import { ... } from '@vitest/browser-preview/{name}'` → `import { ... } from 'vite-plus/test/browser-preview/{name}'` -/// - `import { ... } from '@vitest/browser-webdriverio'` → `import { ... } from 'vite-plus/test/browser-webdriverio'` -/// - `import { ... } from '@vitest/browser-webdriverio/{name}'` → `import { ... } from 'vite-plus/test/browser-webdriverio/{name}'` -/// - `declare module 'vitest' { ... }` → `declare module 'vite-plus/test' { ... }` -/// - `declare module 'vitest/config' { ... }` → `declare module 'vite-plus' { ... }` -/// - `declare module 'vitest/{name}' { ... }` → `declare module 'vite-plus/test/{name}' { ... }` -/// - `declare module '@vitest/browser' { ... }` → `declare module 'vite-plus/test/browser' { ... }` -/// - `declare module '@vitest/browser/{name}' { ... }` → `declare module 'vite-plus/test/browser/{name}' { ... }` -/// - `declare module '@vitest/browser-playwright' { ... }` → `declare module 'vite-plus/test/browser-playwright' { ... }` -/// - `declare module '@vitest/browser-playwright/{name}' { ... }` → `declare module 'vite-plus/test/browser-playwright/{name}' { ... }` -/// - `declare module '@vitest/browser-preview' { ... }` → `declare module 'vite-plus/test/browser-preview' { ... }` -/// - `declare module '@vitest/browser-preview/{name}' { ... }` → `declare module 'vite-plus/test/browser-preview/{name}' { ... }` -/// - `declare module '@vitest/browser-webdriverio' { ... }` → `declare module 'vite-plus/test/browser-webdriverio' { ... }` -/// - `declare module '@vitest/browser-webdriverio/{name}' { ... }` → `declare module 'vite-plus/test/browser-webdriverio/{name}' { ... }` -const REWRITE_VITEST_RULES: &str = r#"--- -id: rewrite-vitest-config-import +--- +id: rewrite-vitest-browser-webdriverio-provider-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]vitest/config['"]$ + regex: ^['"]@vitest/browser-webdriverio/provider['"]$ inside: kind: import_statement transform: NEW_IMPORT: replace: source: $STR - replace: vitest/config - by: "vite-plus" + replace: "@vitest/browser-webdriverio/provider" + by: "vite-plus/test/browser/providers/webdriverio" fix: $NEW_IMPORT --- -id: rewrite-vitest-import +id: rewrite-vitest-browser-webdriverio-provider-export language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]vitest['"]$ + regex: ^['"]@vitest/browser-webdriverio/provider['"]$ inside: - kind: import_statement + kind: export_statement transform: NEW_IMPORT: replace: source: $STR - replace: vitest - by: "vite-plus/test" + replace: "@vitest/browser-webdriverio/provider" + by: "vite-plus/test/browser/providers/webdriverio" fix: $NEW_IMPORT --- -id: rewrite-vitest-scoped-import +id: rewrite-vitest-browser-webdriverio-provider-require language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]@vitest/(browser-playwright|browser-preview|browser-webdriverio|browser)(/.*)?['"]$ + regex: ^['"]@vitest/browser-webdriverio/provider['"]$ inside: - kind: import_statement + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ transform: NEW_IMPORT: replace: source: $STR - replace: "@vitest/" - by: "vite-plus/test/" + replace: "@vitest/browser-webdriverio/provider" + by: "vite-plus/test/browser/providers/webdriverio" fix: $NEW_IMPORT --- -id: rewrite-vitest-subpath-import +id: rewrite-vitest-browser-webdriverio-provider-dynamic-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['"]vitest/.+['"]$ + regex: ^['"]@vitest/browser-webdriverio/provider['"]$ inside: - kind: import_statement + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import transform: NEW_IMPORT: replace: source: $STR - replace: vitest/ - by: "vite-plus/test/" + replace: "@vitest/browser-webdriverio/provider" + by: "vite-plus/test/browser/providers/webdriverio" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vitest-config +id: rewrite-vitest-subpath-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]vitest/config['\"]$ + regex: ^['"]vitest/.+['"]$ + not: + regex: ^['"]vitest/package\.json['"]$ inside: - kind: module + kind: import_statement transform: NEW_IMPORT: replace: source: $STR - replace: vitest/config - by: "vite-plus" + replace: vitest/ + by: "vite-plus/test/" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vitest +id: rewrite-vitest-subpath-export language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]vitest['\"]$ + regex: ^['"]vitest/.+['"]$ + not: + regex: ^['"]vitest/package\.json['"]$ inside: - kind: module + kind: export_statement transform: NEW_IMPORT: replace: source: $STR - replace: vitest - by: "vite-plus/test" + replace: vitest/ + by: "vite-plus/test/" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vitest-scoped +id: rewrite-vitest-subpath-require language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]@vitest/(browser-playwright|browser-preview|browser-webdriverio|browser)(/.*)?['\"]$ + regex: ^['"]vitest/.+['"]$ + not: + regex: ^['"]vitest/package\.json['"]$ inside: - kind: module + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ transform: NEW_IMPORT: replace: source: $STR - replace: "@vitest/" + replace: vitest/ by: "vite-plus/test/" fix: $NEW_IMPORT --- -id: rewrite-declare-module-vitest-subpath +id: rewrite-vitest-subpath-dynamic-import language: TypeScript rule: pattern: $STR kind: string - regex: ^['\"]vitest/.+['\"]$ + regex: ^['"]vitest/.+['"]$ + not: + regex: ^['"]vitest/package\.json['"]$ inside: - kind: module + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import transform: NEW_IMPORT: replace: @@ -262,6 +1402,64 @@ transform: by: "vite-plus/pack" fix: $NEW_IMPORT --- +id: rewrite-tsdown-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown + by: "vite-plus/pack" +fix: $NEW_IMPORT +--- +id: rewrite-tsdown-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown + by: "vite-plus/pack" +fix: $NEW_IMPORT +--- +id: rewrite-tsdown-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown + by: "vite-plus/pack" +fix: $NEW_IMPORT +--- id: rewrite-declare-module-tsdown language: TypeScript rule: @@ -294,6 +1492,64 @@ transform: by: "vite-plus/pack/client" fix: $NEW_IMPORT --- +id: rewrite-tsdown-client-export +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown/client['"]$ + inside: + kind: export_statement +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown/client + by: "vite-plus/pack/client" +fix: $NEW_IMPORT +--- +id: rewrite-tsdown-client-require +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown/client['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + regex: ^require$ +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown/client + by: "vite-plus/pack/client" +fix: $NEW_IMPORT +--- +id: rewrite-tsdown-client-dynamic-import +language: TypeScript +rule: + pattern: $STR + kind: string + regex: ^['"]tsdown/client['"]$ + inside: + kind: arguments + inside: + kind: call_expression + has: + field: function + kind: import +transform: + NEW_IMPORT: + replace: + source: $STR + replace: tsdown/client + by: "vite-plus/pack/client" +fix: $NEW_IMPORT +--- id: rewrite-declare-module-tsdown-client language: TypeScript rule: @@ -341,16 +1597,56 @@ static RE_REF_VITEST_SUBPATH: LazyLock = LazyLock::new(|| { Regex::new(r#"^(\s*///\s*)"#).unwrap() }); -/// `@vitest/{pkg}[/{subpath}]` → `vite-plus/test/{pkg}[/{subpath}]` -/// Only matches packages and subpaths that vite-plus actually exports: +/// `@vitest/browser[/{subpath}]` references that map onto the *nested* +/// `vite-plus/test/browser[/{subpath}]` surface (a plain `@vitest/` → `vite-plus/test/` +/// swap is correct here): /// - `@vitest/browser` → `vite-plus/test/browser` /// - `@vitest/browser/context` → `vite-plus/test/browser/context` /// - `@vitest/browser/providers/{name}` → `vite-plus/test/browser/providers/{name}` -/// - `@vitest/browser-playwright[/{subpath}]` → `vite-plus/test/browser-playwright[/{subpath}]` -/// - `@vitest/browser-preview[/{subpath}]` → `vite-plus/test/browser-preview[/{subpath}]` -/// - `@vitest/browser-webdriverio[/{subpath}]` → `vite-plus/test/browser-webdriverio[/{subpath}]` -static RE_REF_VITEST_SCOPED: LazyLock = LazyLock::new(|| { - Regex::new(r#"^(\s*///\s*)"#).unwrap() +static RE_REF_VITEST_SCOPED_BROWSER: LazyLock = LazyLock::new(|| { + Regex::new( + r#"^(\s*///\s*)"#, + ) + .unwrap() +}); + +/// `@vitest/browser/{client,locators,matchers,utils}` references. `vite-plus` only +/// exposes these four at the *bare* `./test/{name}` surface (NOT under `./test/browser/`), +/// so the `/browser/` segment is stripped: `@vitest/browser/{name}` → `vite-plus/test/{name}`. +static RE_REF_VITEST_SCOPED_BROWSER_FLAT: LazyLock = LazyLock::new(|| { + Regex::new( + r#"^(\s*///\s*)"#, + ) + .unwrap() +}); + +/// `@vitest/browser-{provider}` (exact, no subpath) → +/// `vite-plus/test/browser-{provider}` — a plain `@vitest/` → `vite-plus/test/` swap. +static RE_REF_VITEST_SCOPED_PROVIDER: LazyLock = LazyLock::new(|| { + Regex::new( + r#"^(\s*///\s*)"#, + ) + .unwrap() +}); + +/// `@vitest/browser-{provider}/context` references. `vite-plus` projects every +/// provider's `context` onto the shared `./test/browser/context` export, so the +/// provider segment is dropped: → `vite-plus/test/browser/context`. +static RE_REF_VITEST_SCOPED_PROVIDER_CONTEXT: LazyLock = LazyLock::new(|| { + Regex::new( + r#"^(\s*///\s*)"#, + ) + .unwrap() +}); + +/// `@vitest/browser-{provider}/provider` references. `vite-plus` exposes provider +/// entry points at `./test/browser/providers/{provider}`, so the subpath is +/// rewritten accordingly: → `vite-plus/test/browser/providers/{provider}`. +static RE_REF_VITEST_SCOPED_PROVIDER_ENTRY: LazyLock = LazyLock::new(|| { + Regex::new( + r#"^(\s*///\s*)"#, + ) + .unwrap() }); /// bare `vite` → `vite-plus` @@ -510,7 +1806,47 @@ fn rewrite_reference_types(content: &mut String, skip_packages: &SkipPackages) - changed = true; continue; } - if apply_regex_replace(line, &RE_REF_VITEST_SCOPED, "${1}vite-plus/test/${2}${3}") { + // `@vitest/browser-{provider}/{context,provider}` must be matched before the + // bare provider regex so the more specific subpath rewrite wins. + if apply_regex_replace( + line, + &RE_REF_VITEST_SCOPED_PROVIDER_CONTEXT, + "${1}vite-plus/test/browser/context${3}", + ) { + changed = true; + continue; + } + if apply_regex_replace( + line, + &RE_REF_VITEST_SCOPED_PROVIDER_ENTRY, + "${1}vite-plus/test/browser/providers/${2}${3}", + ) { + changed = true; + continue; + } + if apply_regex_replace( + line, + &RE_REF_VITEST_SCOPED_PROVIDER, + "${1}vite-plus/test/${2}${3}", + ) { + changed = true; + continue; + } + // `@vitest/browser/{client,locators,matchers,utils}` strips the `/browser/` + // segment; the generic `@vitest/browser[/...]` rule keeps it. + if apply_regex_replace( + line, + &RE_REF_VITEST_SCOPED_BROWSER_FLAT, + "${1}vite-plus/test/${2}${3}", + ) { + changed = true; + continue; + } + if apply_regex_replace( + line, + &RE_REF_VITEST_SCOPED_BROWSER, + "${1}vite-plus/test/${2}${3}", + ) { changed = true; continue; } @@ -1100,6 +2436,8 @@ export default context;"# #[test] fn test_rewrite_import_content_vitest_browser_playwright_subpath() { + // `@vitest/browser-{provider}/context` maps onto the shared + // `vite-plus/test/browser/context` export (the provider segment is dropped). let vite_config = r#"import { something } from "@vitest/browser-playwright/context"; export default something;"#; @@ -1108,12 +2446,30 @@ export default something;"#; assert!(result.updated); assert_eq!( result.content, - r#"import { something } from "vite-plus/test/browser-playwright/context"; + r#"import { something } from "vite-plus/test/browser/context"; export default something;"# ); } + #[test] + fn test_rewrite_import_content_vitest_browser_playwright_provider() { + // `@vitest/browser-{provider}/provider` maps to the provider entry point + // under `vite-plus/test/browser/providers/{provider}`. + let vite_config = r#"import { playwright } from '@vitest/browser-playwright/provider'; + +export default playwright;"#; + + let result = rewrite_import_content(vite_config, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { playwright } from 'vite-plus/test/browser/providers/playwright'; + +export default playwright;"# + ); + } + #[test] fn test_rewrite_import_content_vitest_browser_preview() { let vite_config = r#"import { preview } from '@vitest/browser-preview'; @@ -1140,12 +2496,28 @@ export default something;"#; assert!(result.updated); assert_eq!( result.content, - r#"import { something } from "vite-plus/test/browser-preview/context"; + r#"import { something } from "vite-plus/test/browser/context"; export default something;"# ); } + #[test] + fn test_rewrite_import_content_vitest_browser_preview_provider() { + let vite_config = r#"import { preview } from '@vitest/browser-preview/provider'; + +export default preview;"#; + + let result = rewrite_import_content(vite_config, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { preview } from 'vite-plus/test/browser/providers/preview'; + +export default preview;"# + ); + } + #[test] fn test_rewrite_import_content_vitest_browser_webdriverio() { let vite_config = r#"import { webdriverio } from '@vitest/browser-webdriverio'; @@ -1172,12 +2544,62 @@ export default something;"#; assert!(result.updated); assert_eq!( result.content, - r#"import { something } from "vite-plus/test/browser-webdriverio/context"; + r#"import { something } from "vite-plus/test/browser/context"; export default something;"# ); } + #[test] + fn test_rewrite_import_content_vitest_browser_webdriverio_provider() { + let vite_config = r#"import { webdriverio } from '@vitest/browser-webdriverio/provider'; + +export default webdriverio;"#; + + let result = rewrite_import_content(vite_config, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { webdriverio } from 'vite-plus/test/browser/providers/webdriverio'; + +export default webdriverio;"# + ); + } + + #[test] + fn test_rewrite_import_content_vitest_browser_flat_subpaths() { + // `@vitest/browser/{client,locators,matchers,utils}` are exposed by + // vite-plus at the *bare* `./test/{name}` surface, so the `/browser/` + // segment must be stripped (the nested `./test/browser/{name}` keys + // do NOT exist in the exports map → ERR_PACKAGE_PATH_NOT_EXPORTED). + for (sub, expected) in [ + ("client", "vite-plus/test/client"), + ("locators", "vite-plus/test/locators"), + ("matchers", "vite-plus/test/matchers"), + ("utils", "vite-plus/test/utils"), + ] { + let single = format!("import x from '@vitest/browser/{sub}';"); + let result = rewrite_import_content(&single, &SkipPackages::default()).unwrap(); + assert!(result.updated, "single-quoted @vitest/browser/{sub} should be rewritten"); + assert_eq!(result.content, format!("import x from '{expected}';")); + + let double = format!("import x from \"@vitest/browser/{sub}\";"); + let result = rewrite_import_content(&double, &SkipPackages::default()).unwrap(); + assert!(result.updated, "double-quoted @vitest/browser/{sub} should be rewritten"); + assert_eq!(result.content, format!("import x from \"{expected}\";")); + } + } + + #[test] + fn test_rewrite_import_content_vitest_browser_context_kept_nested() { + // `@vitest/browser/context` keeps the nested path — `./test/browser/context` + // IS exported. + let vite_config = r#"import { context } from '@vitest/browser/context';"#; + let result = rewrite_import_content(vite_config, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"import { context } from 'vite-plus/test/browser/context';"#); + } + #[test] fn test_rewrite_import_content_vite_subpath() { let vite_config = r#"import { ModuleRunner } from 'vite/module-runner'; @@ -1468,6 +2890,9 @@ describe('app', () => { #[test] fn test_rewrite_declare_module_vitest() { + // `declare module 'vitest'` is intentionally NOT rewritten — the + // `vite-plus/test` subpath is a thin shim that re-exports vitest's + // types, so augmentations must target the upstream module identity. let content = r#"declare module 'vitest' { interface JestAssertion { toBeCustom(): void; @@ -1475,19 +2900,15 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test' { - interface JestAssertion { - toBeCustom(): void; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_config() { + // `declare module 'vitest/config'` is intentionally NOT rewritten — + // `vite-plus` re-exports `vitest/config`, so augmentations must target + // the upstream module identity to merge correctly. let content = r#"declare module 'vitest/config' { interface UserConfig { test?: TestConfig; @@ -1495,15 +2916,8 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus' { - interface UserConfig { - test?: TestConfig; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] @@ -1528,26 +2942,24 @@ describe('app', () => { #[test] fn test_rewrite_declare_module_vitest_subpath() { + // `declare module 'vitest/node'` stays — the `vite-plus/test/node` + // shim re-exports from upstream, so augmentations must target the + // upstream module identity. let content = r#"declare module 'vitest/node' { export interface VitestOptions { custom?: boolean; } -}"#; - - let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/node' { - export interface VitestOptions { - custom?: boolean; - } -}"# - ); +}"#; + + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_browser() { + // `declare module '@vitest/browser'` stays — the + // `vite-plus/test/browser` shim re-exports from upstream. let content = r#"declare module '@vitest/browser' { interface BrowserContext { custom?: boolean; @@ -1555,19 +2967,13 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser' { - interface BrowserContext { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_browser_subpath() { + // `declare module '@vitest/browser/context'` stays — shim re-exports. let content = r#"declare module '@vitest/browser/context' { export interface Context { custom?: boolean; @@ -1575,19 +2981,13 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser/context' { - export interface Context { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_browser_playwright() { + // `declare module '@vitest/browser-playwright'` stays — shim re-exports. let content = r#"declare module '@vitest/browser-playwright' { interface PlaywrightContext { custom?: boolean; @@ -1595,19 +2995,13 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-playwright' { - interface PlaywrightContext { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_browser_preview() { + // `declare module '@vitest/browser-preview'` stays — shim re-exports. let content = r#"declare module '@vitest/browser-preview' { interface PreviewContext { custom?: boolean; @@ -1615,19 +3009,13 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-preview' { - interface PreviewContext { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_declare_module_vitest_browser_webdriverio() { + // `declare module '@vitest/browser-webdriverio'` stays — shim re-exports. let content = r#"declare module '@vitest/browser-webdriverio' { interface WebDriverContext { custom?: boolean; @@ -1635,19 +3023,16 @@ describe('app', () => { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-webdriverio' { - interface WebDriverContext { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] fn test_rewrite_mixed_imports_and_declare_modules() { + // Imports are rewritten; `declare module 'vite' { ... }` is rewritten + // (vite-plus bundles vite, owning the type identity), but + // `declare module 'vitest' { ... }` stays put — the shim re-exports + // upstream and augmentations must target the upstream identity. let content = r#"import { defineConfig } from 'vite'; import { describe } from 'vitest'; @@ -1678,7 +3063,7 @@ declare module 'vite-plus' { } } -declare module 'vite-plus/test' { +declare module 'vitest' { interface JestAssertion { toBeCustom(): void; } @@ -1703,6 +3088,10 @@ export default defineConfig({});"# #[test] fn test_rewrite_multiple_declare_modules() { + // `declare module 'vite'` / `'vite/'` get rewritten (vite-plus + // bundles vite, owning the type identity), but `declare module 'vitest'` + // and `declare module '@vitest/browser'` are preserved — vite-plus + // shims re-export upstream and augmentations must target upstream. let content = r#"declare module 'vite' { interface UserConfig { custom?: boolean; @@ -1743,13 +3132,13 @@ declare module 'vite-plus/module-runner' { } } -declare module 'vite-plus/test' { +declare module 'vitest' { interface JestAssertion { toBeCustom(): void; } } -declare module 'vite-plus/test/browser' { +declare module '@vitest/browser' { interface BrowserContext { custom?: boolean; } @@ -1759,6 +3148,8 @@ declare module 'vite-plus/test/browser' { #[test] fn test_rewrite_declare_module_vitest_double_quotes() { + // Double-quoted `declare module "vitest"` is preserved for the same + // reason as the single-quoted variant — shim re-exports. let content = r#"declare module "vitest" { interface JestAssertion { toBeCustom(): void; @@ -1766,15 +3157,8 @@ declare module 'vite-plus/test/browser' { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module "vite-plus/test" { - interface JestAssertion { - toBeCustom(): void; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] @@ -1786,15 +3170,8 @@ declare module 'vite-plus/test/browser' { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-playwright/context' { - export interface Context { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] @@ -1806,15 +3183,8 @@ declare module 'vite-plus/test/browser' { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-preview/context' { - export interface Context { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] @@ -1826,15 +3196,8 @@ declare module 'vite-plus/test/browser' { }"#; let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(result.updated); - assert_eq!( - result.content, - r#"declare module 'vite-plus/test/browser-webdriverio/context' { - export interface Context { - custom?: boolean; - } -}"# - ); + assert!(!result.updated); + assert_eq!(result.content, content); } #[test] @@ -2438,14 +3801,291 @@ import { describe } from 'vite-plus/test'; export default defineConfig({});"# ); - // app: vite IS rewritten (no peerDep), vitest IS rewritten - let app_content = - fs::read_to_string(temp.path().join("packages/app/src/index.ts")).unwrap(); + // app: vite IS rewritten (no peerDep), vitest IS rewritten + let app_content = + fs::read_to_string(temp.path().join("packages/app/src/index.ts")).unwrap(); + assert_eq!( + app_content, + r#"import { defineConfig } from 'vite-plus'; +import { describe } from 'vite-plus/test'; +export default defineConfig({});"# + ); + } + + // ==================================== + // CommonJS `require(...)` Rewriting Tests + // ==================================== + // + // These tests cover the require-shape sibling rules that mirror the + // `import_statement` rewrites for `.cjs` / `.cts` and other CJS sources. + // The rules MUST only match a literal `require(...)` callee — not + // `my_require(...)`, not `require.cache[...]`, and not dynamic + // `import('...')` (the import_statement rules already cover ESM). + + #[test] + fn test_rewrite_require_vite() { + let content = r#"const { defineConfig } = require('vite');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const { defineConfig } = require('vite-plus');"#); + } + + #[test] + fn test_rewrite_require_vite_double_quotes() { + let content = r#"const { defineConfig } = require("vite");"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const { defineConfig } = require("vite-plus");"#); + } + + #[test] + fn test_rewrite_require_vite_subpath() { + let content = r#"const { ModuleRunner } = require('vite/module-runner');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const { ModuleRunner } = require('vite-plus/module-runner');"# + ); + } + + #[test] + fn test_rewrite_require_vitest() { + let content = r#"const vi = require('vitest');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const vi = require('vite-plus/test');"#); + } + + #[test] + fn test_rewrite_require_vitest_config() { + let content = r#"const { defineConfig } = require('vitest/config');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const { defineConfig } = require('vite-plus');"#); + } + + #[test] + fn test_rewrite_require_vitest_subpath() { + let content = r#"const { startVitest } = require('vitest/node');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const { startVitest } = require('vite-plus/test/node');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser() { + let content = r#"const x = require('@vitest/browser');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_context() { + let content = r#"const { context } = require('@vitest/browser/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const { context } = require('vite-plus/test/browser/context');"# + ); + } + + #[test] + fn test_rewrite_require_vitest_browser_flat_subpaths() { + // `@vitest/browser/{client,locators,matchers,utils}` get the `/browser/` + // segment stripped, mirroring the import_statement rule. + for (sub, expected) in [ + ("client", "vite-plus/test/client"), + ("locators", "vite-plus/test/locators"), + ("matchers", "vite-plus/test/matchers"), + ("utils", "vite-plus/test/utils"), + ] { + let single = format!("const x = require('@vitest/browser/{sub}');"); + let result = rewrite_import_content(&single, &SkipPackages::default()).unwrap(); + assert!(result.updated, "single-quoted require @vitest/browser/{sub} should rewrite"); + assert_eq!(result.content, format!("const x = require('{expected}');")); + + let double = format!("const x = require(\"@vitest/browser/{sub}\");"); + let result = rewrite_import_content(&double, &SkipPackages::default()).unwrap(); + assert!(result.updated, "double-quoted require @vitest/browser/{sub} should rewrite"); + assert_eq!(result.content, format!("const x = require(\"{expected}\");")); + } + } + + #[test] + fn test_rewrite_require_vitest_browser_playwright() { + let content = r#"const x = require('@vitest/browser-playwright');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser-playwright');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_playwright_context() { + let content = r#"const x = require('@vitest/browser-playwright/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser/context');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_playwright_provider() { + let content = r#"const x = require('@vitest/browser-playwright/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const x = require('vite-plus/test/browser/providers/playwright');"# + ); + } + + #[test] + fn test_rewrite_require_vitest_browser_preview() { + let content = r#"const x = require('@vitest/browser-preview');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser-preview');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_preview_context() { + let content = r#"const x = require('@vitest/browser-preview/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser/context');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_preview_provider() { + let content = r#"const x = require('@vitest/browser-preview/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const x = require('vite-plus/test/browser/providers/preview');"# + ); + } + + #[test] + fn test_rewrite_require_vitest_browser_webdriverio() { + let content = r#"const x = require('@vitest/browser-webdriverio');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser-webdriverio');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_webdriverio_context() { + let content = r#"const x = require('@vitest/browser-webdriverio/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const x = require('vite-plus/test/browser/context');"#); + } + + #[test] + fn test_rewrite_require_vitest_browser_webdriverio_provider() { + let content = r#"const x = require('@vitest/browser-webdriverio/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const x = require('vite-plus/test/browser/providers/webdriverio');"# + ); + } + + #[test] + fn test_rewrite_require_tsdown() { + let content = r#"const { defineConfig } = require('tsdown');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const { defineConfig } = require('vite-plus/pack');"#); + } + + #[test] + fn test_rewrite_require_tsdown_client() { + let content = r#"require('tsdown/client');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"require('vite-plus/pack/client');"#); + } + + #[test] + fn test_rewrite_require_mixed_in_cjs_config() { + // Mirrors a realistic `vitest.config.cjs` containing both the config + // import and direct test specifier requires. + let content = r#"const { defineConfig } = require('vitest/config'); +const vi = require('vitest'); +const { context } = require('@vitest/browser/context'); +const { defineConfig: defineViteConfig } = require('vite'); +const { build } = require('tsdown'); + +module.exports = defineConfig({});"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const { defineConfig } = require('vite-plus'); +const vi = require('vite-plus/test'); +const { context } = require('vite-plus/test/browser/context'); +const { defineConfig: defineViteConfig } = require('vite-plus'); +const { build } = require('vite-plus/pack'); + +module.exports = defineConfig({});"# + ); + } + + // -- Negative tests: require-shape rules must NOT match these -- + + #[test] + fn test_rewrite_require_does_not_match_custom_require_function() { + // `my_require('vitest')` is NOT a literal `require` callee, so the + // require-shape rules must leave it alone. (The string `'vitest'` is + // also outside an import_statement / module / require call, so the + // import-shape rules can't see it either.) + let content = r#"const x = my_require('vitest');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_require_does_not_match_require_cache_index() { + // `require.cache['vitest']` is a member access + index — there is no + // `require(...)` call here, so the require-shape rules must not match. + let content = r#"const x = require.cache['vitest'];"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_require_does_not_match_require_resolve() { + // `require.resolve('vitest')` is a method call on `require`, not a + // bare `require(...)` call — the rule constrains the callee to the + // literal identifier `require`, so this is left alone. + let content = r#"const x = require.resolve('vitest');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_require_respects_skip_vitest_peer_dependency() { + // When vitest is in peerDependencies, the require-shape rule must + // also be skipped (parity with the import-shape rule). + let content = r#"const vi = require('vitest'); +const { defineConfig } = require('vite');"#; + let skip_packages = + SkipPackages { skip_vite: false, skip_vitest: true, skip_tsdown: false }; + let result = rewrite_import_content(content, &skip_packages).unwrap(); + assert!(result.updated); + // vitest require is NOT rewritten; vite require IS rewritten. assert_eq!( - app_content, - r#"import { defineConfig } from 'vite-plus'; -import { describe } from 'vite-plus/test'; -export default defineConfig({});"# + result.content, + r#"const vi = require('vitest'); +const { defineConfig } = require('vite-plus');"# ); } @@ -2510,12 +4150,20 @@ export default defineConfig({});"# } #[test] - fn test_rewrite_reference_types_vitest_scoped_browser_matchers_not_rewritten() { - // @vitest/browser/matchers is NOT exported by vite-plus — should not be rewritten - let content = r#"/// "#; - let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); - assert!(!result.updated); - assert_eq!(result.content, content); + fn test_rewrite_reference_types_vitest_scoped_browser_flat_subpaths() { + // `@vitest/browser/{client,locators,matchers,utils}` are exposed at the + // *bare* `vite-plus/test/{name}` surface — the `/browser/` segment is stripped. + for (sub, expected) in [ + ("client", "vite-plus/test/client"), + ("locators", "vite-plus/test/locators"), + ("matchers", "vite-plus/test/matchers"), + ("utils", "vite-plus/test/utils"), + ] { + let content = format!(r#"/// "#); + let result = rewrite_import_content(&content, &SkipPackages::default()).unwrap(); + assert!(result.updated, "@vitest/browser/{sub} reference should be rewritten"); + assert_eq!(result.content, format!(r#"/// "#)); + } } #[test] @@ -2559,6 +4207,38 @@ export default defineConfig({});"# ); } + #[test] + fn test_rewrite_reference_types_vitest_scoped_provider_context() { + // `@vitest/browser-{provider}/context` references map onto the shared + // `vite-plus/test/browser/context` export (the provider segment is dropped). + for provider in ["playwright", "preview", "webdriverio"] { + let content = + format!(r#"/// "#); + let result = rewrite_import_content(&content, &SkipPackages::default()).unwrap(); + assert!(result.updated, "@vitest/browser-{provider}/context should be rewritten"); + assert_eq!( + result.content, + r#"/// "# + ); + } + } + + #[test] + fn test_rewrite_reference_types_vitest_scoped_provider_entry() { + // `@vitest/browser-{provider}/provider` references map to the provider + // entry point under `vite-plus/test/browser/providers/{provider}`. + for provider in ["playwright", "preview", "webdriverio"] { + let content = + format!(r#"/// "#); + let result = rewrite_import_content(&content, &SkipPackages::default()).unwrap(); + assert!(result.updated, "@vitest/browser-{provider}/provider should be rewritten"); + assert_eq!( + result.content, + format!(r#"/// "#) + ); + } + } + #[test] fn test_rewrite_reference_types_tsdown_client_rewritten() { // tsdown/client should be rewritten to vite-plus/pack/client @@ -2920,4 +4600,410 @@ export default defineConfig({});"# assert!(!result.updated); assert_eq!(result.content, content); } + + // ==================================== + // Re-export (`export ... from '...'`) Rewriting Tests + // ==================================== + // + // These mirror the `import_statement` rewrites for the `export_statement` + // tree-sitter kind, which covers `export { x } from 'mod'`, + // `export * from 'mod'`, `export * as ns from 'mod'`, and + // `export type { x } from 'mod'`. Without these sibling rules, projects + // that re-export from `@vitest/browser-playwright` (or the other browser + // provider packages that `rewritePackageJson()` removes from package.json) + // would silently end up with `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime. + + #[test] + fn test_rewrite_export_named_from_vite() { + let content = r#"export { defineConfig } from 'vite';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export { defineConfig } from 'vite-plus';"#); + } + + #[test] + fn test_rewrite_export_named_from_tsdown() { + let content = r#"export { defineConfig } from 'tsdown';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export { defineConfig } from 'vite-plus/pack';"#); + } + + #[test] + fn test_rewrite_export_named_from_vitest_browser_context() { + // `export { page } from '@vitest/browser/context'` — the regression + // case called out in the chatgpt-codex review. + let content = r#"export { page } from '@vitest/browser/context';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export { page } from 'vite-plus/test/browser/context';"#); + } + + #[test] + fn test_rewrite_export_star_from_vitest_browser_playwright() { + // `export * from '@vitest/browser-playwright'` — the package is dropped + // from package.json by `rewritePackageJson()`, so it MUST be rewritten + // to avoid `ERR_PACKAGE_PATH_NOT_EXPORTED` under strict pnpm/PnP. + let content = r#"export * from '@vitest/browser-playwright';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export * from 'vite-plus/test/browser-playwright';"#); + } + + #[test] + fn test_rewrite_export_star_as_ns_from_vitest() { + // `export * as ns from 'vitest'` (namespace re-export). + let content = r#"export * as vi from 'vitest';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export * as vi from 'vite-plus/test';"#); + } + + #[test] + fn test_rewrite_export_type_from_vitest_config() { + // `export type { Config } from 'vitest/config'` — type-only re-export. + // `vitest/config` collapses to bare `vite-plus` (matches the + // import-shape rule). + let content = r#"export type { Config } from 'vitest/config';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export type { Config } from 'vite-plus';"#); + } + + #[test] + fn test_rewrite_export_named_from_vitest_browser_preview() { + let content = r#"export { preview } from "@vitest/browser-preview";"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export { preview } from "vite-plus/test/browser-preview";"#); + } + + #[test] + fn test_rewrite_export_star_from_vitest_browser_webdriverio_provider() { + // `@vitest/browser-{provider}/provider` maps onto the shared + // `vite-plus/test/browser/providers/{provider}` entry point. + let content = r#"export * from '@vitest/browser-webdriverio/provider';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"export * from 'vite-plus/test/browser/providers/webdriverio';"# + ); + } + + #[test] + fn test_rewrite_export_named_from_vite_subpath() { + let content = r#"export { ModuleRunner } from 'vite/module-runner';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"export { ModuleRunner } from 'vite-plus/module-runner';"#); + } + + #[test] + fn test_rewrite_export_value_with_vitest_string_literal_not_rewritten() { + // A plain value declaration whose initializer is the string `'vitest'` + // is NOT a re-export — the string literal is outside any + // import_statement / export_statement / require call and must be left + // alone. + let content = r#"export const foo = 'vitest';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + // ========================================= + // Dynamic import() and TS import-type tests + // ========================================= + + #[test] + fn test_rewrite_dynamic_import_vite() { + let content = r#"const m = await import('vite');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vite_subpath() { + let content = r#"const m = await import("vite/module-runner");"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import("vite-plus/module-runner");"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest() { + let content = r#"const m = await import('vitest');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus/test');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_config() { + let content = r#"const m = await import('vitest/config');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_subpath() { + let content = r#"const m = await import('vitest/node');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus/test/node');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser() { + let content = r#"const provider = await import('@vitest/browser');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const provider = await import('vite-plus/test/browser');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_context() { + let content = r#"const ctx = await import('@vitest/browser/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const ctx = await import('vite-plus/test/browser/context');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_flat_subpath() { + // `@vitest/browser/{client,locators,matchers,utils}` strips the + // `/browser/` segment, matching the static-import rule. + for (sub, expected) in [ + ("client", "vite-plus/test/client"), + ("locators", "vite-plus/test/locators"), + ("matchers", "vite-plus/test/matchers"), + ("utils", "vite-plus/test/utils"), + ] { + let content = format!("const x = await import('@vitest/browser/{sub}');"); + let result = rewrite_import_content(&content, &SkipPackages::default()).unwrap(); + assert!(result.updated, "dynamic import of @vitest/browser/{sub} should be rewritten"); + assert_eq!(result.content, format!("const x = await import('{expected}');")); + } + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_playwright() { + let content = r#"const p = await import('@vitest/browser-playwright');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const p = await import('vite-plus/test/browser-playwright');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_playwright_context() { + let content = r#"const p = await import('@vitest/browser-playwright/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const p = await import('vite-plus/test/browser/context');"#); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_playwright_provider() { + let content = r#"const p = await import('@vitest/browser-playwright/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const p = await import('vite-plus/test/browser/providers/playwright');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_preview_provider() { + let content = r#"const p = await import('@vitest/browser-preview/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const p = await import('vite-plus/test/browser/providers/preview');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_vitest_browser_webdriverio_provider() { + let content = r#"const p = await import('@vitest/browser-webdriverio/provider');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"const p = await import('vite-plus/test/browser/providers/webdriverio');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_tsdown() { + let content = r#"const m = await import('tsdown');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus/pack');"#); + } + + #[test] + fn test_rewrite_dynamic_import_tsdown_client() { + let content = r#"const m = await import('tsdown/client');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"const m = await import('vite-plus/pack/client');"#); + } + + #[test] + fn test_rewrite_ts_import_type_vitest_browser_context() { + // TS type-position `typeof import('@vitest/browser/context')` is also + // a `call_expression` with the special `import` token as callee, so + // the same rule covers it. + let content = r#"type C = typeof import('@vitest/browser/context');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"type C = typeof import('vite-plus/test/browser/context');"#); + } + + #[test] + fn test_rewrite_ts_import_type_vitest_browser_playwright() { + let content = r#"type P = typeof import('@vitest/browser-playwright');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"type P = typeof import('vite-plus/test/browser-playwright');"# + ); + } + + #[test] + fn test_rewrite_dynamic_import_does_not_touch_string_literal_args() { + // A string literal that *contains* an `import('@vitest/browser')`-looking + // payload must NOT be rewritten — it is not a real dynamic import. + let content = r#"const x = "import('@vitest/browser')";"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_dynamic_import_does_not_touch_member_call_named_import() { + // A property-access call like `obj.import('@vitest/browser')` is a + // user-defined function, not a dynamic import expression. Its callee + // node-kind is `member_expression`, not `import`, so the rule must + // not match. + let content = r#"const p = obj.import('@vitest/browser');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_dynamic_import_mixed_with_static_imports() { + // Verifies a mixed file with static, dynamic, and TS-type imports. + let content = r#"import { describe } from 'vitest'; +const browser = await import('@vitest/browser'); +type C = typeof import('@vitest/browser/context'); +const provider = await import('@vitest/browser-playwright');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { describe } from 'vite-plus/test'; +const browser = await import('vite-plus/test/browser'); +type C = typeof import('vite-plus/test/browser/context'); +const provider = await import('vite-plus/test/browser-playwright');"# + ); + } + + // `vitest/package.json` is a metadata-access pattern (typically used to + // read the vitest version). Rewriting it to `vite-plus/test/package.json` + // would fail at runtime with `ERR_PACKAGE_PATH_NOT_EXPORTED` because + // `syncTestPackageExports()` deliberately skips the upstream + // `./package.json` export. The catch-all `vitest/*` subpath rules MUST + // leave it alone so the original specifier still resolves through the + // transitively-installed `vitest` dependency. + + #[test] + fn test_rewrite_import_content_vitest_package_json_static_import() { + let content = r#"import pkg from 'vitest/package.json'; + +console.log(pkg.version);"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_static_import_double_quotes() { + let content = r#"import pkg from "vitest/package.json"; + +console.log(pkg.version);"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_require() { + let content = r#"const pkg = require('vitest/package.json'); + +console.log(pkg.version);"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_dynamic_import() { + let content = r#"const pkg = await import('vitest/package.json'); + +console.log(pkg.version);"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_typeof_import() { + let content = r#"type Pkg = typeof import('vitest/package.json');"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_export_from() { + let content = r#"export { version } from 'vitest/package.json';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, content); + } + + #[test] + fn test_rewrite_import_content_vitest_config_still_rewritten() { + // Sanity check: the `package.json` exclusion must not interfere with + // the existing `vitest/config` → `vite-plus` rule (which produces a + // bare `vite-plus` target, NOT `vite-plus/test/config`). + let content = r#"import 'vitest/config';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"import 'vite-plus';"#); + } + + #[test] + fn test_rewrite_import_content_vitest_package_json_like_suffix_still_rewritten() { + // Sanity check: only the exact `vitest/package.json` subpath is + // skipped. A subpath that merely starts with `package.json` (e.g., + // `package.json.js`) MUST still be rewritten by the catch-all rule. + let content = r#"import 'vitest/package.json.js';"#; + let result = rewrite_import_content(content, &SkipPackages::default()).unwrap(); + assert!(result.updated); + assert_eq!(result.content, r#"import 'vite-plus/test/package.json.js';"#); + } } diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index d84b010cce..3ea26aa27d 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -75,8 +75,8 @@ Migrate this project to Vite+. Vite+ replaces the current split tooling around r After the migration: - Confirm `vite` imports were rewritten to `vite-plus` where needed -- Confirm `vitest` imports were rewritten to `vite-plus/test` where needed -- Remove old `vite` and `vitest` dependencies only after those rewrites are confirmed +- Confirm `vitest` imports were rewritten to `vite-plus/test` (and `@vitest/browser*` to `vite-plus/test/browser*`) where needed +- Remove old `vite`, `vitest`, and `@vitest/browser*` dependencies only after those rewrites are confirmed — `vite-plus` ships them as direct deps - Move remaining tool-specific config into the appropriate blocks in `vite.config.ts` Command mapping to keep in mind: @@ -96,20 +96,26 @@ Summarize the migration at the end and report any manual follow-up still require ### Vitest -Vitest is automatically migrated through `vp migrate`. If you are migrating manually, you have to update all the imports to `vite-plus/test` instead: +Vitest is automatically migrated through `vp migrate`. `vite-plus` re-exports upstream `vitest@4.x` under `vite-plus/test*` (including the browser-provider subpaths), so a single `vite-plus` install is enough — you no longer need to install `vitest` or any `@vitest/browser*` provider directly. If you are migrating manually, update all the imports to `vite-plus/test*` instead: ```ts // before +import { defineConfig } from 'vitest/config'; import { describe, expect, it, vi } from 'vitest'; +import { playwright } from '@vitest/browser-playwright'; const { page } = await import('@vitest/browser/context'); // after +import { defineConfig } from 'vite-plus'; import { describe, expect, it, vi } from 'vite-plus/test'; +import { playwright } from 'vite-plus/test/browser-playwright'; const { page } = await import('vite-plus/test/browser/context'); ``` +`declare module 'vitest'` / `declare module '@vitest/browser*'` augmentations are intentionally **not** rewritten — `vite-plus/test*` is a thin re-export of upstream `vitest*`, so type augmentations have to target the upstream module identity to merge correctly. Leave those `declare module` statements pointing at `'vitest'` / `'@vitest/browser*'`. + ### tsdown If your project uses a `tsdown.config.ts`, move its options into the `pack` block in `vite.config.ts`: diff --git a/docs/guide/upgrade.md b/docs/guide/upgrade.md index 9b5c48e328..e0d5131b5f 100644 --- a/docs/guide/upgrade.md +++ b/docs/guide/upgrade.md @@ -29,21 +29,20 @@ You can also use `vp add vite-plus@latest` if you want to move the dependency ex ### Updating Aliased Packages -Vite+ sets up npm aliases for its core packages during installation: +Vite+ sets up an npm alias for its core package during installation: - `vite` is aliased to `npm:@voidzero-dev/vite-plus-core@latest` -- `vitest` is aliased to `npm:@voidzero-dev/vite-plus-test@latest` -`vp update vite-plus` does not re-resolve these aliases in the lockfile. To fully upgrade, update them separately: +`vp update vite-plus` does not re-resolve this alias in the lockfile. To fully upgrade, update it separately: ```bash -vp update @voidzero-dev/vite-plus-core @voidzero-dev/vite-plus-test +vp update @voidzero-dev/vite-plus-core ``` Or update everything at once: ```bash -vp update vite-plus @voidzero-dev/vite-plus-core @voidzero-dev/vite-plus-test +vp update vite-plus @voidzero-dev/vite-plus-core ``` You can verify with `vp outdated` that no Vite+ packages remain outdated. diff --git a/ecosystem-ci/patch-project.ts b/ecosystem-ci/patch-project.ts index 133690d3fd..6af6e7a9a7 100644 --- a/ecosystem-ci/patch-project.ts +++ b/ecosystem-ci/patch-project.ts @@ -2,12 +2,10 @@ import { execSync } from 'node:child_process'; import { readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import cliPkg from '../packages/cli/package.json' with { type: 'json' }; +import { VITEST_VERSION } from '../packages/cli/src/utils/constants.ts'; import { ecosystemCiDir, tgzDir } from './paths.ts'; import repos from './repo.json' with { type: 'json' }; -const vpVersion = cliPkg.version; - const projects = Object.keys(repos); const project = process.argv[2]; @@ -21,7 +19,9 @@ const repoRoot = join(ecosystemCiDir, project); const repoConfig = repos[project as keyof typeof repos]; const directory = 'directory' in repoConfig ? repoConfig.directory : undefined; const cwd = directory ? join(repoRoot, directory) : repoRoot; -const vitePlusTgz = `file:${tgzDir}/vite-plus-${vpVersion}.tgz`; +// The e2e build job pins packages/cli to 0.0.0 before `pnpm pack`, so the +// artifact is always vite-plus-0.0.0.tgz regardless of the committed version. +const vitePlusTgz = `file:${tgzDir}/vite-plus-0.0.0.tgz`; // run vp migrate const cli = process.env.VP_CLI_BIN ?? 'vp'; @@ -51,6 +51,29 @@ if (project === 'vinext') { // vp migrate runs full dependency rewriting instead of skipping. const forceFreshMigration = 'forceFreshMigration' in repoConfig && repoConfig.forceFreshMigration; +// Bun is uniquely strict about vitest's `peer vite ^6 || ^7 || ^8` resolution +// (https://github.com/oven-sh/bun/issues/8406): it checks both the override +// target's package name and version. Point bun-based projects at the +// vite-7.99.0 alias tgz (a copy of core renamed to "vite" with a satisfying +// version); pnpm/npm/yarn must keep pointing at the real core tgz, otherwise +// they trip a registry lookup for "vite@" when a workspace +// sub-package and the override both reference the same vite-named alias. +const isBunProject = project === 'bun-vite-template'; +const viteOverrideTgz = isBunProject ? `vite-7.99.0.tgz` : `voidzero-dev-vite-plus-core-0.0.0.tgz`; + +const vitestOverrides = { + vitest: VITEST_VERSION, + '@vitest/expect': VITEST_VERSION, + '@vitest/runner': VITEST_VERSION, + '@vitest/snapshot': VITEST_VERSION, + '@vitest/spy': VITEST_VERSION, + '@vitest/utils': VITEST_VERSION, + '@vitest/mocker': VITEST_VERSION, + '@vitest/pretty-format': VITEST_VERSION, + '@vitest/coverage-v8': VITEST_VERSION, + '@vitest/coverage-istanbul': VITEST_VERSION, +}; + execSync(`${cli} migrate --no-agent --no-interactive`, { cwd, stdio: 'inherit', @@ -58,10 +81,9 @@ execSync(`${cli} migrate --no-agent --no-interactive`, { ...process.env, ...(forceFreshMigration ? { VP_FORCE_MIGRATE: '1' } : {}), VP_OVERRIDE_PACKAGES: JSON.stringify({ - vite: `file:${tgzDir}/voidzero-dev-vite-plus-core-${vpVersion}.tgz`, - vitest: `file:${tgzDir}/voidzero-dev-vite-plus-test-${vpVersion}.tgz`, - '@voidzero-dev/vite-plus-core': `file:${tgzDir}/voidzero-dev-vite-plus-core-${vpVersion}.tgz`, - '@voidzero-dev/vite-plus-test': `file:${tgzDir}/voidzero-dev-vite-plus-test-${vpVersion}.tgz`, + vite: `file:${tgzDir}/${viteOverrideTgz}`, + '@voidzero-dev/vite-plus-core': `file:${tgzDir}/voidzero-dev-vite-plus-core-0.0.0.tgz`, + ...vitestOverrides, }), VP_VERSION: vitePlusTgz, }, diff --git a/ecosystem-ci/verify-install.ts b/ecosystem-ci/verify-install.ts index 1b13c774bc..212b741122 100644 --- a/ecosystem-ci/verify-install.ts +++ b/ecosystem-ci/verify-install.ts @@ -2,11 +2,13 @@ import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; -import cliPkg from '../packages/cli/package.json' with { type: 'json' }; - const require = createRequire(`${process.cwd()}/`); -const expectedVersion = cliPkg.version; +// The ecosystem-ci pack step pins packages/cli to 0.0.0 before `pnpm pack`, so +// a correctly installed local build always reports 0.0.0 — never the published +// registry version (which `patch-project.ts` likewise references as a fixed +// `vite-plus-0.0.0.tgz`). +const expectedVersion = '0.0.0'; try { const pkgPath = require.resolve('vite-plus/package.json'); diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md index fa9ef3d792..9e97564007 100644 --- a/packages/cli/BUNDLING.md +++ b/packages/cli/BUNDLING.md @@ -1,6 +1,6 @@ # CLI Package Build Architecture -This document explains how `vite-plus` is built and how it re-exports from both the core and test packages to serve as a drop-in replacement for `vite`. +This document explains how `vite-plus` is built and how it re-exports from `@voidzero-dev/vite-plus-core` (bundled vite/rolldown/tsdown) and from upstream `vitest` to serve as a drop-in replacement for `vite`. ## Overview @@ -9,9 +9,9 @@ The CLI package uses a **4-step build process**: 1. **tsdown Build** - Bundle all CLI entry points via tsdown 2. **NAPI Binding Build** - Compile Rust code to native Node.js bindings 3. **Core Package Export Sync** - Re-export `@voidzero-dev/vite-plus-core` under `./client`, `./types/*`, etc. -4. **Test Package Export Sync** - Re-export `@voidzero-dev/vite-plus-test` under `./test/*` +4. **Test Package Export Sync** - Re-export upstream `vitest` under `./test/*` -This architecture allows users to import everything from a single package (`vite-plus`) as a drop-in replacement for `vite`, without needing to know about the separate core and test packages. +This architecture allows users to import everything from a single package (`vite-plus`) as a drop-in replacement for `vite`, without needing to know about the separate `@voidzero-dev/vite-plus-core` bundle or `vitest`. ## Build Steps @@ -24,7 +24,7 @@ Bundles all CLI entry points using tsdown (configured in `tsdown.config.ts`). Th - Public API entries: `bin`, `index`, `define-config`, `fmt`, `lint`, `pack`, `pack-bin` - Global command entries: `create`, `migrate`, `version`, `config`, `mcp`, `staged` - All third-party dependencies are inlined at build time -- Only packages that must be resolved at runtime stay external (NAPI binding, `@voidzero-dev/vite-plus-core`, `@voidzero-dev/vite-plus-test`, `oxfmt`, `oxlint`) +- Only packages that must be resolved at runtime stay external (NAPI binding, `@voidzero-dev/vite-plus-core`, `vitest`, `oxfmt`, `oxlint`) - Code splitting creates shared chunks for code used by multiple entries - DTS (`.d.ts`) files are generated for all entries @@ -90,15 +90,22 @@ export type * from '@voidzero-dev/vite-plus-core/types/importMeta.d.ts'; ### Step 4: Test Package Export Sync (`syncTestPackageExports`) -Reads the test package's exports and creates shim files that re-export everything under `./test/*`: +Reads vitest's exports plus the three `@vitest/browser-*` provider packages and creates shim files that re-export everything under `./test/*`: ```typescript -// For each test package export like "./browser-playwright" -// Creates a shim file: dist/test/browser-playwright.js -export * from '@voidzero-dev/vite-plus-test/browser-playwright'; +// For each vitest export like "./node" +// Creates a shim file: dist/test/node.js +export * from 'vitest/node'; + +// For each @vitest/browser-* provider, two shim surfaces are projected: +// dist/test/browser-playwright.js (matches old wrapper path) +// dist/test/browser/providers/playwright.js (alias path) +export * from '@vitest/browser-playwright'; ``` -**Input**: `../test/package.json` exports +Provider `.d.ts` shims are NOT bare re-exports — see the [Provider Type Identity](#why-provider-dts-shims-are-inlined) note below. + +**Input**: resolved `vitest/package.json` exports plus each `@vitest/browser-*` package's exports (all resolved via `createRequire`) **Output**: `dist/test/*.js`, `dist/test/*.d.ts`, updated `package.json` exports --- @@ -336,28 +343,47 @@ This allows TypeScript to pick up types like `import.meta.hot`, CSS module types ### Why Shim Files? -Instead of copying the actual dist files from the test package, we create thin shim files that re-export from `@voidzero-dev/vite-plus-test`. This approach: +Instead of copying vitest's dist files, we create thin shim files that re-export from `vitest`. This approach: -1. **Keeps packages in sync** - No need to rebuild CLI when test package changes +1. **Keeps packages in sync** - No need to rebuild CLI when vitest is upgraded 2. **Reduces duplication** - No file copying, just re-exports -3. **Preserves module resolution** - Node.js resolves to the actual test package +3. **Preserves module resolution** - Node.js resolves to the actual installed vitest ### Export Mapping (Test) -All test package exports are mapped under `./test/*`: +Every entry under vitest's own `exports` is shimmed under `./test/*` (wildcard exports and `./package.json` are skipped). The shim is purely a re-export — `vite-plus/test` and friends are aliases for the matching subpath of upstream `vitest`. Examples: + +| Vitest Export | CLI Package Export | +| ------------------ | -------------------------- | +| `vitest` | `vite-plus/test` | +| `vitest/browser` | `vite-plus/test/browser` | +| `vitest/node` | `vite-plus/test/node` | +| `vitest/config` | `vite-plus/test/config` | +| `vitest/reporters` | `vite-plus/test/reporters` | + +The full set is regenerated on every build from the upstream vitest `package.json`, so the exact list tracks vitest itself. + +In addition to vitest's own exports, the three `@vitest/browser-*` provider packages are projected under two parallel surfaces so existing user code keeps resolving after the deleted `@voidzero-dev/vite-plus-test` wrapper: + +| Provider Package | CLI Package Exports | +| ----------------------------- | ------------------------------------------------------------------------------------ | +| `@vitest/browser-playwright` | `vite-plus/test/browser-playwright`, `vite-plus/test/browser/providers/playwright` | +| `@vitest/browser-preview` | `vite-plus/test/browser-preview`, `vite-plus/test/browser/providers/preview` | +| `@vitest/browser-webdriverio` | `vite-plus/test/browser-webdriverio`, `vite-plus/test/browser/providers/webdriverio` | + +Each provider's own subpaths (e.g. `./context`) are mirrored under both alias prefixes. + +#### Why provider d.ts shims are inlined + +Provider `.d.ts` shims are NOT plain `export * from '@vitest/browser-playwright'` re-exports — they inline the upstream `.d.ts` content with `vitest/node` / `vitest/browser` / `@vitest/browser*` bare specifiers rewritten to relative paths inside `dist/test/`. The two private shims `dist/test/_at-vitest-browser.d.ts` and `dist/test/_at-vitest-browser/context.d.ts` re-export `@vitest/browser`/`@vitest/browser/context` and are referenced from those rewrites. -| Test Package Export | CLI Package Export | -| ------------------------------------------------- | ----------------------------------- | -| `@voidzero-dev/vite-plus-test` | `vite-plus/test` | -| `@voidzero-dev/vite-plus-test/browser` | `vite-plus/test/browser` | -| `@voidzero-dev/vite-plus-test/browser-playwright` | `vite-plus/test/browser-playwright` | -| `@voidzero-dev/vite-plus-test/plugins/runner` | `vite-plus/test/plugins/runner` | +This avoids a pnpm-edge type-identity split: when the upstream `.d.ts` is loaded by reference (`export * from '@vitest/browser-playwright'`), TypeScript resolves its internal `import { BrowserProvider } from 'vitest/node'` through the provider package's own pnpm-edge, which can be a different vitest copy than the one a user's `vite.config.ts` sees through `vite-plus`. The mismatch produces two structurally identical but nominally distinct `BrowserProvider` types, so `provider: playwright()` fails the user's typecheck. Rewriting the specifiers routes every type import through vite-plus's own subpath shims, guaranteeing a single vitest identity across the user's whole config. ### Conditional Export Handling The sync handles complex conditional exports with `import`/`require`/`node`/`types` conditions. -**Test package's main export** (`"."`): +**Vitest's main export** (`"."`): ```json ".": { @@ -390,23 +416,23 @@ For each condition, appropriate shim files are created: ### Shim File Contents -**ESM shim** (`dist/test/browser-playwright.js`): +**ESM shim** (`dist/test/browser.js`): ```javascript -export * from '@voidzero-dev/vite-plus-test/browser-playwright'; +export * from 'vitest/browser'; ``` **CJS shim** (`dist/test/index.cjs`): ```javascript -module.exports = require('@voidzero-dev/vite-plus-test'); +module.exports = require('vitest'); ``` -**Type shim** (`dist/test/browser-playwright.d.ts`): +**Type shim** (`dist/test/browser.d.ts`): ```typescript -import '@voidzero-dev/vite-plus-test/browser-playwright'; -export * from '@voidzero-dev/vite-plus-test/browser-playwright'; +import 'vitest/browser'; +export * from 'vitest/browser'; ``` Note: Type shims include a side-effect import to preserve module augmentations (e.g., `toMatchSnapshot` on the `Assertion` interface). @@ -499,8 +525,8 @@ See `package.json` for the complete list of exports. // Core package name for Vite compatibility exports const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core'; -// Test package name for re-exports -const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test'; +// Test package name for re-exports (vitest itself, not a bundled wrapper) +const TEST_PACKAGE_NAME = 'vitest'; ``` ### Package.json Exports Management @@ -539,7 +565,7 @@ All non-`./test*` exports are manually maintained in `package.json`. These fall All `./test*` exports are fully managed by `syncTestPackageExports()`. The build script: -1. Reads `packages/test/package.json` exports +1. Reads vitest's `package.json` exports (resolved via `createRequire`) 2. Creates shim files in `dist/test/` 3. Removes old `./test*` exports from `package.json` 4. Merges in newly generated test exports diff --git a/packages/cli/README.md b/packages/cli/README.md index 8c3fcd71b0..4bcd953cb7 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -187,12 +187,11 @@ If you are manually migrating a project to Vite+, install these dev dependencies npm install -D vite-plus @voidzero-dev/vite-plus-core@latest ``` -You need to add overrides to your package manager for `vite` and `vitest` so that other packages depending on Vite and Vitest will use the Vite+ versions: +You need to add overrides to your package manager for `vite` so that other packages depending on Vite (including Vitest) will use the Vite+ version: ```json "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } ``` @@ -201,15 +200,13 @@ If you are using `pnpm`, add this to your `pnpm-workspace.yaml`: ```yaml overrides: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest ``` Or, if you are using Yarn: ```json "resolutions": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } ``` diff --git a/packages/cli/build.ts b/packages/cli/build.ts index cd395f3b2f..fee85b977e 100644 --- a/packages/cli/build.ts +++ b/packages/cli/build.ts @@ -5,7 +5,7 @@ * 1. buildWithTsdown() - Bundles all CLI entry points via tsdown * 2. buildNapiBinding() - Builds the native Rust binding via NAPI * 3. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core - * 4. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test + * 4. syncTestPackageExports() - Creates shim files to re-export from vitest * 5. syncVersionsExport() - Generates ./versions module with bundled tool versions * 6. copyBundledDocs() - Copies docs into docs/ for bundled package access * 7. syncReadmeFromRoot() - Keeps package README in sync @@ -21,6 +21,7 @@ import { execSync } from 'node:child_process'; import { existsSync, readdirSync, statSync } from 'node:fs'; import { copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; @@ -30,12 +31,76 @@ import { format } from 'oxfmt'; import { generateLicenseFile } from '../../scripts/generate-license.js'; import corePkg from '../core/package.json' with { type: 'json' }; -import testPkg from '../test/package.json' with { type: 'json' }; const projectDir = dirname(fileURLToPath(import.meta.url)); -const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test'; +const TEST_PACKAGE_NAME = 'vitest'; const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core'; +// Browser providers projected under ./test/* and ./test/browser/providers/* so the +// public surface matches what the deleted `@voidzero-dev/vite-plus-test` wrapper exposed. +// Each entry maps the upstream package name to the short provider name used in the +// `./test/browser/providers/` alias path. +const BROWSER_PROVIDER_PACKAGES: ReadonlyArray<{ pkg: string; short: string }> = [ + { pkg: '@vitest/browser-playwright', short: 'playwright' }, + { pkg: '@vitest/browser-preview', short: 'preview' }, + { pkg: '@vitest/browser-webdriverio', short: 'webdriverio' }, +]; + +// Plugin shim entries: each `@vitest/*` package/subpath projected under +// `./test/plugins/` to restore the surface that the removed +// `@voidzero-dev/vite-plus-test` wrapper previously exposed. +const PLUGIN_SHIM_ENTRIES: ReadonlyArray = [ + ['@vitest/runner', 'runner'], + ['@vitest/runner/utils', 'runner-utils'], + ['@vitest/runner/types', 'runner-types'], + ['@vitest/utils', 'utils'], + ['@vitest/utils/source-map', 'utils-source-map'], + ['@vitest/utils/source-map/node', 'utils-source-map-node'], + ['@vitest/utils/error', 'utils-error'], + ['@vitest/utils/helpers', 'utils-helpers'], + ['@vitest/utils/display', 'utils-display'], + ['@vitest/utils/timers', 'utils-timers'], + ['@vitest/utils/offset', 'utils-offset'], + ['@vitest/utils/resolver', 'utils-resolver'], + ['@vitest/utils/serialize', 'utils-serialize'], + ['@vitest/utils/constants', 'utils-constants'], + ['@vitest/utils/diff', 'utils-diff'], + ['@vitest/spy', 'spy'], + ['@vitest/expect', 'expect'], + ['@vitest/snapshot', 'snapshot'], + ['@vitest/snapshot/environment', 'snapshot-environment'], + ['@vitest/snapshot/manager', 'snapshot-manager'], + ['@vitest/mocker', 'mocker'], + ['@vitest/mocker/node', 'mocker-node'], + ['@vitest/mocker/browser', 'mocker-browser'], + ['@vitest/mocker/redirect', 'mocker-redirect'], + ['@vitest/mocker/transforms', 'mocker-transforms'], + ['@vitest/mocker/automock', 'mocker-automock'], + ['@vitest/mocker/register', 'mocker-register'], + ['@vitest/pretty-format', 'pretty-format'], + ['@vitest/browser', 'browser'], + ['@vitest/browser/context', 'browser-context'], + ['@vitest/browser/client', 'browser-client'], + ['@vitest/browser/locators', 'browser-locators'], + ['@vitest/browser-playwright', 'browser-playwright'], + ['@vitest/browser-webdriverio', 'browser-webdriverio'], + ['@vitest/browser-preview', 'browser-preview'], +]; + +/** + * Vitest-related bare specifiers that appear in `@vitest/browser-*` d.ts files + * and the sub-path under `dist/test/` whose shim re-exports the same module. + * Longer prefixes are listed first so substring matches don't shadow them + * (e.g. `vitest/internal/browser` before `vitest/browser`). + */ +const VITEST_TYPE_SPECIFIER_REWRITES: ReadonlyArray = [ + ['@vitest/browser/context', '_at-vitest-browser/context'], + ['@vitest/browser', '_at-vitest-browser'], + ['vitest/internal/browser', 'internal/browser'], + ['vitest/browser', 'browser'], + ['vitest/node', 'node'], +]; + const { values: { ['skip-native']: skipNative, ['skip-ts']: skipTs }, } = parseArgs({ @@ -275,16 +340,21 @@ async function syncTypesDir(srcDir: string, destDir: string, relativePath: strin } /** - * Sync exports from @voidzero-dev/vite-plus-test to vite-plus + * Sync exports from vitest to vite-plus * - * This function reads the test package's exports and creates shim files that + * This function reads vitest's package.json exports and creates shim files that * re-export everything under the ./test/* subpath. This allows users to import - * from vite-plus/test/* instead of @voidzero-dev/vite-plus-test/*. + * from vite-plus/test/* instead of vitest/*. */ async function syncTestPackageExports() { console.log('\nSyncing test package exports...'); - const testPkgPath = join(projectDir, '../test/package.json'); + // Resolve vitest's package.json via Node's resolver so we always read the + // currently installed copy — packages/test/ no longer exists. + const require = createRequire(import.meta.url); + const testPkgPath = require.resolve(`${TEST_PACKAGE_NAME}/package.json`, { + paths: [projectDir], + }); const cliPkgPath = join(projectDir, 'package.json'); const testDistDir = join(projectDir, 'dist/test'); @@ -306,21 +376,225 @@ async function syncTestPackageExports() { // Convert ./foo to ./test/foo, . to ./test const cliExportPath = exportPath === '.' ? './test' : `./test${exportPath.slice(1)}`; + const shimBaseName = exportPath === '.' ? 'index' : exportPath.slice(2); + const importSpecifier = + exportPath === '.' ? TEST_PACKAGE_NAME : `${TEST_PACKAGE_NAME}${exportPath.slice(1)}`; // Create shim files and build export entry - const shimExport = await createShimForExport(exportPath, exportValue, testDistDir); + const shimExport = await createShimForExport( + shimBaseName, + exportValue, + importSpecifier, + testDistDir, + ); if (shimExport) { generatedExports[cliExportPath] = shimExport; console.log(` Created ${cliExportPath}`); } } + // Private shims for `@vitest/browser` and `@vitest/browser/context`. These + // are referenced as relative paths from the inlined browser-provider d.ts + // shims so that `@vitest/browser` resolves through vite-plus's own pnpm-edge + // (same one that owns the `vitest` direct dep) — preventing the two-vitest + // type-identity split that breaks user `provider: playwright()` typechecks. + await writePrivateAtVitestBrowserShims(testDistDir); + + // Mirror upstream @vitest/browser-* provider packages under ./test/ and + // ./test/browser/providers/. Existing vite-plus user code imports from these + // paths (e.g., `vite-plus/test/browser-playwright`) and must keep resolving after + // the bundled `@voidzero-dev/vite-plus-test` wrapper was removed. + for (const { pkg, short } of BROWSER_PROVIDER_PACKAGES) { + let providerPkgPath: string; + try { + providerPkgPath = require.resolve(`${pkg}/package.json`, { paths: [projectDir] }); + } catch (err) { + console.warn(` Skipping ${pkg} — not installed: ${(err as Error).message}`); + continue; + } + const providerPkg = JSON.parse(await readFile(providerPkgPath, 'utf-8')); + const providerPkgRoot = dirname(providerPkgPath); + const providerExports = (providerPkg.exports ?? {}) as Record; + + for (const [providerExportPath, providerExportValue] of Object.entries(providerExports)) { + if (providerExportPath === './package.json' || providerExportPath.includes('*')) { + continue; + } + + const providerSubPath = providerExportPath === '.' ? '' : providerExportPath.slice(1); + // Two CLI surfaces that map to the same provider shim: + // ./test/ → e.g. ./test/browser-playwright + // ./test/browser/providers/ → e.g. ./test/browser/providers/playwright + const pkgShortName = pkg.startsWith('@vitest/') ? pkg.slice('@vitest/'.length) : pkg; + const surfaces = [ + { + cliPath: `./test/${pkgShortName}${providerSubPath}`, + baseName: `${pkgShortName}${providerSubPath}`, + }, + { + cliPath: `./test/browser/providers/${short}${providerSubPath}`, + baseName: `browser/providers/${short}${providerSubPath}`, + }, + ]; + const importSpecifier = + providerExportPath === '.' ? pkg : `${pkg}${providerExportPath.slice(1)}`; + + for (const { cliPath, baseName } of surfaces) { + const shimBaseName = baseName.replace(/^\//, ''); + const shimExport = await createShimForExport( + shimBaseName, + providerExportValue, + importSpecifier, + testDistDir, + { providerPkgRoot }, + ); + if (shimExport) { + // Upstream `@vitest/browser-/context` is types-only and just + // re-exports from `@vitest/browser/context`. To make the migrated + // `vite-plus/test/browser-/context` import resolvable at + // runtime (Node ESM resolution requires `default`/`import`), emit a + // JS shim that re-exports from `@vitest/browser/context` and amend + // the export entry. + if (providerExportPath === './context') { + await ensureContextRuntimeShim(shimBaseName, testDistDir, shimExport); + } + generatedExports[cliPath] = shimExport; + console.log(` Created ${cliPath}`); + } + } + } + } + + // Emit `./test/browser/context` — vitest's exports map only covers `./browser` + // (which becomes `vite-plus/test/browser`), but the migration rewrites + // `@vitest/browser/context` → `vite-plus/test/browser/context`. Without this + // entry Node throws ERR_PACKAGE_PATH_NOT_EXPORTED at runtime. + generatedExports['./test/browser/context'] = await createBrowserContextExport(testDistDir); + console.log(' Created ./test/browser/context'); + + // Bare `./test/` shims for the bundled `@vitest/browser` surfaces + // the old `@voidzero-dev/vite-plus-test` wrapper used to expose: + // ./test/client, ./test/context, ./test/locators, ./test/matchers, ./test/utils + // `oxlint-plugin.ts` autofixes `@vitest/browser/client` → + // `vite-plus/test/client` and `@vitest/browser/locators` → + // `vite-plus/test/locators`, so the runtime targets MUST resolve. + const bareBrowserShims = await createBareBrowserShims(require, testDistDir); + for (const [cliPath, exportValue] of Object.entries(bareBrowserShims)) { + generatedExports[cliPath] = exportValue; + console.log(` Created ${cliPath}`); + } + + // Emit `./test/browser-compat` — used when downstream consumers point + // `@vitest/browser` at vite-plus via a pnpm/yarn override. The shim + // re-exports the four symbols vitest's browser plugin checks for to + // identify a compatible browser provider package. + generatedExports['./test/browser-compat'] = await createBrowserCompatExport(testDistDir); + console.log(' Created ./test/browser-compat'); + + for (const [importSpecifier, pluginName] of PLUGIN_SHIM_ENTRIES) { + const shimExport = await createShimForExport( + `plugins/${pluginName}`, + `${pluginName}.js`, + importSpecifier, + testDistDir, + ); + if (shimExport) { + generatedExports[`./test/plugins/${pluginName}`] = shimExport; + console.log(` Created ./test/plugins/${pluginName}`); + } + } + // Update CLI package.json await updateCliPackageJson(cliPkgPath, generatedExports); console.log(`\nSynced ${Object.keys(generatedExports).length} exports from test package`); } +/** + * `@vitest/browser` exports a handful of subpaths (`./client`, `./context`, + * `./locators`, `./matchers`, `./utils`) that the deleted vite-plus-test + * wrapper surfaced as bare `./test/` entries. Without these, code + * that imports `vite-plus/test/client` (and friends) — including code + * produced by `vp lint --fix` via the autofix rule in + * `packages/cli/src/oxlint-plugin.ts` — fails with + * `ERR_PACKAGE_PATH_NOT_EXPORTED`. + * + * `./matchers` and `./utils` resolve to a `dummy.js` upstream (types-only + * entrypoints) and we mirror that — `createShimForExport` is happy with the + * empty default file because it still creates a valid shim that just + * re-exports nothing at runtime; type imports continue to resolve. + */ +async function createBareBrowserShims( + require: NodeRequire, + testDistDir: string, +): Promise> { + const result: Record = {}; + let browserPkgPath: string; + try { + browserPkgPath = require.resolve('@vitest/browser/package.json', { paths: [projectDir] }); + } catch (err) { + console.warn( + ` Skipping bare browser shims — @vitest/browser not installed: ${(err as Error).message}`, + ); + return result; + } + const browserPkg = JSON.parse(await readFile(browserPkgPath, 'utf-8')); + const browserPkgRoot = dirname(browserPkgPath); + const browserExports = (browserPkg.exports ?? {}) as Record; + + const bareSubpaths = ['./client', './context', './locators', './matchers', './utils'] as const; + for (const sub of bareSubpaths) { + const exportValue = browserExports[sub]; + if (!exportValue) { + continue; + } + const subName = sub.slice(2); + const cliPath = `./test/${subName}`; + const shimBaseName = subName; + const importSpecifier = `@vitest/browser${sub.slice(1)}`; + const shimExport = await createShimForExport( + shimBaseName, + exportValue, + importSpecifier, + testDistDir, + { providerPkgRoot: browserPkgRoot }, + ); + if (shimExport) { + result[cliPath] = shimExport; + } + } + return result; +} + +/** + * Browser-compat shim — preserves the `./test/browser-compat` surface from + * the deleted wrapper. Re-exports the four symbols vitest's own browser + * plugin spotchecks for when treating a package as a browser provider + * override target. + */ +async function createBrowserCompatExport(testDistDir: string): Promise { + const dir = testDistDir; + await mkdir(dir, { recursive: true }); + const symbols = [ + 'asLocator', + 'defineBrowserCommand', + 'defineBrowserProvider', + 'parseKeyDef', + 'resolveScreenshotPath', + ]; + const jsPath = join(dir, 'browser-compat.js'); + const dtsPath = join(dir, 'browser-compat.d.ts'); + await writeFile(jsPath, `export { ${symbols.join(', ')} } from '@vitest/browser';\n`); + await writeFile( + dtsPath, + `import '@vitest/browser';\nexport { ${symbols.join(', ')} } from '@vitest/browser';\n`, + ); + return { + types: './dist/test/browser-compat.d.ts', + default: './dist/test/browser-compat.js', + }; +} + /** * Read version from a dependency's package.json in node_modules. * Uses readFile because these packages don't export ./package.json. @@ -347,8 +621,8 @@ async function readDepVersion(packageName: string): Promise { * Generate ./versions export module with bundled tool versions. * * Collects versions from: - * - core/test package.json bundledVersions (vite, rolldown, tsdown, vitest) - * - CLI dependency package.json (oxlint, oxfmt, oxlint-tsgolint) + * - core package.json bundledVersions (vite, rolldown, tsdown) + * - CLI dependency package.json (oxlint, oxfmt, oxlint-tsgolint, vitest) * * Generates dist/versions.js and dist/versions.d.ts with inlined constants. */ @@ -356,15 +630,14 @@ async function syncVersionsExport() { console.log('\nSyncing versions export...'); const distDir = join(projectDir, 'dist'); - // Collect versions from bundledVersions (core + test) + // Collect bundled versions from the core package const versions: Record = { ...(corePkg as Record).bundledVersions, - ...(testPkg as Record).bundledVersions, }; - // Collect versions from CLI dependencies (oxlint, oxfmt, oxlint-tsgolint) - // These don't export ./package.json, so we read from node_modules directly - const depTools = ['oxlint', 'oxfmt', 'oxlint-tsgolint'] as const; + // Read versions from CLI dependencies' installed package.json files + // (these packages don't export ./package.json, so node_modules is the source of truth) + const depTools = ['oxlint', 'oxfmt', 'oxlint-tsgolint', 'vitest'] as const; for (const name of depTools) { const version = await readDepVersion(name); if (version) { @@ -465,19 +738,189 @@ type ExportValue = }; /** - * Create shim file(s) for a single export and return the export entry for package.json + * Write private shims at `dist/test/_at-vitest-browser{.d.ts,/context.d.ts}` + * that re-export the `@vitest/browser` package. These are referenced by the + * inlined browser-provider d.ts shims via relative paths so all of + * `@vitest/browser`, `vitest/node`, etc. resolve through vite-plus's own + * pnpm-edge — the same edge that owns vite-plus's `vitest` direct dep. + * The underscore prefix marks them as private; they are not surfaced in the + * package.json `exports` map (TS resolves the relative paths directly). + */ +async function writePrivateAtVitestBrowserShims(testDistDir: string): Promise { + await mkdir(join(testDistDir, '_at-vitest-browser'), { recursive: true }); + await writeFile( + join(testDistDir, '_at-vitest-browser.d.ts'), + `import '@vitest/browser';\nexport * from '@vitest/browser';\n`, + ); + await writeFile( + join(testDistDir, '_at-vitest-browser/context.d.ts'), + `import '@vitest/browser/context';\nexport * from '@vitest/browser/context';\n`, + ); +} + +/** + * Write a JS shim for a provider-`/context` export and amend the export entry + * with a runtime target. + * + * Upstream `@vitest/browser-/context` is declared types-only (its + * `context.d.ts` simply re-exports from `@vitest/browser/context`). After the + * migration rewrites `@vitest/browser-/context` → + * `vite-plus/test/browser-/context`, Node ESM resolution fails with + * ERR_PACKAGE_PATH_NOT_EXPORTED unless the export entry has a `default`/`import` + * target. We re-export from `@vitest/browser/context` so the bundled + * `@vitest/browser` (vite-plus's own pnpm-edge) is reached at runtime. + */ +async function ensureContextRuntimeShim( + shimBaseName: string, + testDistDir: string, + shimExport: ExportValue, +): Promise { + if (typeof shimExport !== 'object' || shimExport === null) { + return; + } + const entry = shimExport as Record; + if (entry.default || entry.import) { + return; + } + const jsRelPath = `./dist/test/${shimBaseName}.js`; + const jsAbsPath = join(testDistDir, `${shimBaseName}.js`); + await mkdir(dirname(jsAbsPath), { recursive: true }); + await writeFile(jsAbsPath, `export * from '@vitest/browser/context';\n`); + entry.default = jsRelPath; +} + +/** + * Build the `./test/browser/context` export entry and write its JS/d.ts shims. + * + * Vitest's package.json only exposes `./browser` (mapped to `./test/browser`). + * The migration rewrites `@vitest/browser/context` → + * `vite-plus/test/browser/context`, so we add this path with both runtime and + * type targets that re-export from `@vitest/browser/context`. + */ +async function createBrowserContextExport(testDistDir: string): Promise { + const dir = join(testDistDir, 'browser'); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, 'context.js'), `export * from '@vitest/browser/context';\n`); + await writeFile( + join(dir, 'context.d.ts'), + `import '@vitest/browser/context';\nexport * from '@vitest/browser/context';\n`, + ); + return { + types: './dist/test/browser/context.d.ts', + default: './dist/test/browser/context.js', + }; +} + +/** + * Inline-copy a browser-provider's upstream `.d.ts` file into `outDtsPath` and + * rewrite vitest-related bare specifiers to relative paths inside the + * vite-plus test shim tree. See `VITEST_TYPE_SPECIFIER_REWRITES` and + * `writePrivateAtVitestBrowserShims` for the rationale. + * + * Specifiers that are user peer dependencies (`playwright`, `webdriverio`, + * `tinyrainbow`, etc.) and the `@vitest/browser-*` self-import are left bare. + */ +async function writeInlinedProviderDts( + outDtsPath: string, + upstreamDtsPath: string, + testDistDir: string, +): Promise { + const upstream = await readFile(upstreamDtsPath, 'utf-8'); + const outDir = dirname(outDtsPath); + // Resolve to the file basename appended to the relative dir, never to the + // bare directory. `relative('dist/test/x/', 'dist/test/y')` returns `'../y'` + // but `relative('dist/test/x/', 'dist/test/y/')` returns `'..'` — TS would + // then look for `dist/test/y/index.d.ts` instead of `dist/test/y.d.ts`. We + // always emit `/` so the basename lookup hits the file. + const relToShim = (sub: string): string => { + const r = relative(outDir, testDistDir).replaceAll('\\', '/'); + const prefix = r === '' ? '.' : r.startsWith('.') ? r : `./${r}`; + return `${prefix}/${sub}`; + }; + let result = upstream; + for (const [bare, sub] of VITEST_TYPE_SPECIFIER_REWRITES) { + const escaped = bare.replaceAll('/', '\\/'); + const pattern = new RegExp(`(['"])${escaped}\\1`, 'g'); + result = result.replaceAll(pattern, `'${relToShim(sub)}'`); + } + await mkdir(outDir, { recursive: true }); + await writeFile(outDtsPath, result); +} + +/** + * Resolve the upstream `.d.ts` path for a given export value. Returns null + * when the export does not declare a types file (runtime-only exports). + */ +function resolveUpstreamDtsPath( + providerPkgRoot: string, + exportValue: unknown, + condition: 'types' | 'require-types' = 'types', +): string | null { + if (typeof exportValue === 'string') { + return exportValue.endsWith('.d.ts') || exportValue.endsWith('.d.cts') + ? join(providerPkgRoot, exportValue) + : null; + } + if (typeof exportValue !== 'object' || exportValue === null) { + return null; + } + const value = exportValue as Record; + if (condition === 'types') { + if (typeof value.types === 'string') { + return join(providerPkgRoot, value.types); + } + if (typeof value.import === 'object' && value.import !== null) { + const types = (value.import as Record).types; + if (typeof types === 'string') { + return join(providerPkgRoot, types); + } + } + } else { + if (typeof value.require === 'object' && value.require !== null) { + const types = (value.require as Record).types; + if (typeof types === 'string') { + return join(providerPkgRoot, types); + } + } + } + return null; +} + +async function writeShimDts( + outDtsPath: string, + importSpecifier: string, + upstreamDtsPath: string | null, + testDistDir: string, +): Promise { + if (upstreamDtsPath) { + await writeInlinedProviderDts(outDtsPath, upstreamDtsPath, testDistDir); + return; + } + // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) + await writeFile( + outDtsPath, + `import '${importSpecifier}';\nexport * from '${importSpecifier}';\n`, + ); +} + +/** + * Create shim file(s) for a single export and return the export entry for package.json. + * + * @param shimBaseName Path under dist/test/ (e.g. 'index', 'config', 'browser-playwright/context'). + * @param exportValue The upstream package's export value for this entry. + * @param testImportSpecifier The bare import specifier the shim should re-export from + * (e.g. 'vitest', 'vitest/node', '@vitest/browser-playwright'). + * @param distDir Output dist/test directory. + * @param opts Optional shim context. Pass `providerPkgRoot` for browser-provider + * packages to inline-copy their upstream d.ts content with specifier rewrites. */ async function createShimForExport( - exportPath: string, + shimBaseName: string, exportValue: unknown, + testImportSpecifier: string, distDir: string, + opts: { providerPkgRoot?: string } = {}, ): Promise { - // Determine the import specifier for the test package - const testImportSpecifier = - exportPath === '.' ? TEST_PACKAGE_NAME : `${TEST_PACKAGE_NAME}${exportPath.slice(1)}`; - - // Convert export path to file path: ./foo/bar -> foo/bar, . -> index - const shimBaseName = exportPath === '.' ? 'index' : exportPath.slice(2); const shimDir = join(distDir, dirname(shimBaseName)); await mkdir(shimDir, { recursive: true }); @@ -490,11 +933,10 @@ async function createShimForExport( // Check if it's a type-only export if (exportValue.endsWith('.d.ts')) { const dtsPath = join(shimDirForFile, `${baseFileName}.d.ts`); - // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) - await writeFile( - dtsPath, - `import '${testImportSpecifier}';\nexport * from '${testImportSpecifier}';\n`, - ); + const upstream = opts.providerPkgRoot + ? resolveUpstreamDtsPath(opts.providerPkgRoot, exportValue, 'types') + : null; + await writeShimDts(dtsPath, testImportSpecifier, upstream, distDir); return { types: `./dist/test/${shimBaseName}.d.ts` }; } @@ -514,6 +956,8 @@ async function createShimForExport( shimDirForFile, baseFileName, shimBaseName, + distDir, + opts, ); } @@ -522,11 +966,10 @@ async function createShimForExport( if (value.types && typeof value.types === 'string') { const dtsPath = join(shimDirForFile, `${baseFileName}.d.ts`); - // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) - await writeFile( - dtsPath, - `import '${testImportSpecifier}';\nexport * from '${testImportSpecifier}';\n`, - ); + const upstream = opts.providerPkgRoot + ? resolveUpstreamDtsPath(opts.providerPkgRoot, value, 'types') + : null; + await writeShimDts(dtsPath, testImportSpecifier, upstream, distDir); (result as Record).types = `./dist/test/${shimBaseName}.d.ts`; } @@ -549,6 +992,11 @@ async function createShimForExport( * { import: { types, node, default }, require: { types, default } } * And flat structures like: * { types, require, default } + * + * Insertion order matters: Node.js package-exports conditions are order-sensitive. + * For dual-condition entries, `require` MUST come before `default` so that + * `require('vite-plus/test/config')` resolves to the `.cjs` shim instead of + * matching the catch-all `default` (which would point at the ESM file). */ async function createConditionalShim( value: Record, @@ -556,25 +1004,23 @@ async function createConditionalShim( shimDir: string, baseFileName: string, shimBaseName: string, + distDir: string, + opts: { providerPkgRoot?: string } = {}, ): Promise { - const result: ExportValue = {}; + // Build entries as an array of tuples so we control insertion order explicitly. + // Final order for flat entries: types, import (if present), require, default. + // `require` MUST come before `default` — `default` matches everything, so + // putting it first makes the `require` branch unreachable for CJS consumers. + const entries: Array<[string, ExportValue]> = []; // Handle top-level types (flat structure like { types, require, default }) if (value.types && typeof value.types === 'string' && !value.import) { const dtsPath = join(shimDir, `${baseFileName}.d.ts`); - // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) - await writeFile( - dtsPath, - `import '${testImportSpecifier}';\nexport * from '${testImportSpecifier}';\n`, - ); - (result as Record).types = `./dist/test/${shimBaseName}.d.ts`; - } - - // Handle top-level default (flat structure, only when no import condition) - if (value.default && typeof value.default === 'string' && !value.import) { - const jsPath = join(shimDir, `${baseFileName}.js`); - await writeFile(jsPath, `export * from '${testImportSpecifier}';\n`); - (result as Record).default = `./dist/test/${shimBaseName}.js`; + const upstream = opts.providerPkgRoot + ? resolveUpstreamDtsPath(opts.providerPkgRoot, value, 'types') + : null; + await writeShimDts(dtsPath, testImportSpecifier, upstream, distDir); + entries.push(['types', `./dist/test/${shimBaseName}.d.ts`]); } // Handle import condition @@ -584,17 +1030,16 @@ async function createConditionalShim( if (typeof importValue === 'string') { const jsPath = join(shimDir, `${baseFileName}.js`); await writeFile(jsPath, `export * from '${testImportSpecifier}';\n`); - (result as Record).import = `./dist/test/${shimBaseName}.js`; + entries.push(['import', `./dist/test/${shimBaseName}.js`]); } else if (typeof importValue === 'object' && importValue !== null) { const importResult: Record = {}; if (importValue.types && typeof importValue.types === 'string') { const dtsPath = join(shimDir, `${baseFileName}.d.ts`); - // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) - await writeFile( - dtsPath, - `import '${testImportSpecifier}';\nexport * from '${testImportSpecifier}';\n`, - ); + const upstream = opts.providerPkgRoot + ? resolveUpstreamDtsPath(opts.providerPkgRoot, value, 'types') + : null; + await writeShimDts(dtsPath, testImportSpecifier, upstream, distDir); importResult.types = `./dist/test/${shimBaseName}.d.ts`; } @@ -609,28 +1054,28 @@ async function createConditionalShim( importResult.default = `./dist/test/${shimBaseName}.js`; } - result.import = importResult; + entries.push(['import', importResult]); } } - // Handle require condition + // Handle require condition — emitted BEFORE `default` so CJS resolution + // picks the `.cjs` shim instead of the catch-all `default` entry. if (value.require) { const requireValue = value.require as Record; if (typeof requireValue === 'string') { const cjsPath = join(shimDir, `${baseFileName}.cjs`); await writeFile(cjsPath, `module.exports = require('${testImportSpecifier}');\n`); - result.require = `./dist/test/${shimBaseName}.cjs`; + entries.push(['require', `./dist/test/${shimBaseName}.cjs`]); } else if (typeof requireValue === 'object' && requireValue !== null) { const requireResult: Record = {}; if (requireValue.types && typeof requireValue.types === 'string') { const dctsPath = join(shimDir, `${baseFileName}.d.cts`); - // Include side-effect import to preserve module augmentations (e.g., toMatchSnapshot on Assertion) - await writeFile( - dctsPath, - `import '${testImportSpecifier}';\nexport * from '${testImportSpecifier}';\n`, - ); + const upstream = opts.providerPkgRoot + ? resolveUpstreamDtsPath(opts.providerPkgRoot, value, 'require-types') + : null; + await writeShimDts(dctsPath, testImportSpecifier, upstream, distDir); requireResult.types = `./dist/test/${shimBaseName}.d.cts`; } @@ -640,11 +1085,20 @@ async function createConditionalShim( requireResult.default = `./dist/test/${shimBaseName}.cjs`; } - result.require = requireResult; + entries.push(['require', requireResult]); } } - return result; + // Handle top-level default (flat structure, only when no import condition). + // Emitted LAST among siblings so `require` (and any specific condition) + // wins resolution against the catch-all `default`. + if (value.default && typeof value.default === 'string' && !value.import) { + const jsPath = join(shimDir, `${baseFileName}.js`); + await writeFile(jsPath, `export * from '${testImportSpecifier}';\n`); + entries.push(['default', `./dist/test/${shimBaseName}.js`]); + } + + return Object.fromEntries(entries) as ExportValue; } /** diff --git a/packages/cli/package.json b/packages/cli/package.json index 1c93c5e3f4..a20bde6ef9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -96,7 +96,6 @@ "./test": { "import": { "types": "./dist/test/index.d.ts", - "node": "./dist/test/index.js", "default": "./dist/test/index.js" }, "require": { @@ -148,8 +147,8 @@ }, "./test/config": { "types": "./dist/test/config.d.ts", - "default": "./dist/test/config.js", - "require": "./dist/test/config.cjs" + "require": "./dist/test/config.cjs", + "default": "./dist/test/config.js" }, "./test/coverage": { "types": "./dist/test/coverage.d.ts", @@ -171,8 +170,57 @@ "types": "./dist/test/worker.d.ts", "default": "./dist/test/worker.js" }, - "./test/browser-compat": { - "default": "./dist/test/browser-compat.js" + "./test/browser-playwright": { + "types": "./dist/test/browser-playwright.d.ts", + "default": "./dist/test/browser-playwright.js" + }, + "./test/browser/providers/playwright": { + "types": "./dist/test/browser/providers/playwright.d.ts", + "default": "./dist/test/browser/providers/playwright.js" + }, + "./test/browser-playwright/context": { + "types": "./dist/test/browser-playwright/context.d.ts", + "default": "./dist/test/browser-playwright/context.js" + }, + "./test/browser/providers/playwright/context": { + "types": "./dist/test/browser/providers/playwright/context.d.ts", + "default": "./dist/test/browser/providers/playwright/context.js" + }, + "./test/browser-preview": { + "types": "./dist/test/browser-preview.d.ts", + "default": "./dist/test/browser-preview.js" + }, + "./test/browser/providers/preview": { + "types": "./dist/test/browser/providers/preview.d.ts", + "default": "./dist/test/browser/providers/preview.js" + }, + "./test/browser-preview/context": { + "types": "./dist/test/browser-preview/context.d.ts", + "default": "./dist/test/browser-preview/context.js" + }, + "./test/browser/providers/preview/context": { + "types": "./dist/test/browser/providers/preview/context.d.ts", + "default": "./dist/test/browser/providers/preview/context.js" + }, + "./test/browser-webdriverio": { + "types": "./dist/test/browser-webdriverio.d.ts", + "default": "./dist/test/browser-webdriverio.js" + }, + "./test/browser/providers/webdriverio": { + "types": "./dist/test/browser/providers/webdriverio.d.ts", + "default": "./dist/test/browser/providers/webdriverio.js" + }, + "./test/browser-webdriverio/context": { + "types": "./dist/test/browser-webdriverio/context.d.ts", + "default": "./dist/test/browser-webdriverio/context.js" + }, + "./test/browser/providers/webdriverio/context": { + "types": "./dist/test/browser/providers/webdriverio/context.d.ts", + "default": "./dist/test/browser/providers/webdriverio/context.js" + }, + "./test/browser/context": { + "types": "./dist/test/browser/context.d.ts", + "default": "./dist/test/browser/context.js" }, "./test/client": { "default": "./dist/test/client.js" @@ -181,42 +229,20 @@ "types": "./dist/test/context.d.ts", "default": "./dist/test/context.js" }, - "./test/browser/context": { - "types": "./dist/test/browser/context.d.ts", - "default": "./dist/test/browser/context.js" - }, "./test/locators": { + "types": "./dist/test/locators.d.ts", "default": "./dist/test/locators.js" }, "./test/matchers": { + "types": "./dist/test/matchers.d.ts", "default": "./dist/test/matchers.js" }, "./test/utils": { "default": "./dist/test/utils.js" }, - "./test/browser-playwright": { - "types": "./dist/test/browser-playwright.d.ts", - "default": "./dist/test/browser-playwright.js" - }, - "./test/browser-webdriverio": { - "types": "./dist/test/browser-webdriverio.d.ts", - "default": "./dist/test/browser-webdriverio.js" - }, - "./test/browser-preview": { - "types": "./dist/test/browser-preview.d.ts", - "default": "./dist/test/browser-preview.js" - }, - "./test/browser/providers/playwright": { - "types": "./dist/test/browser/providers/playwright.d.ts", - "default": "./dist/test/browser/providers/playwright.js" - }, - "./test/browser/providers/webdriverio": { - "types": "./dist/test/browser/providers/webdriverio.d.ts", - "default": "./dist/test/browser/providers/webdriverio.js" - }, - "./test/browser/providers/preview": { - "types": "./dist/test/browser/providers/preview.d.ts", - "default": "./dist/test/browser/providers/preview.js" + "./test/browser-compat": { + "types": "./dist/test/browser-compat.d.ts", + "default": "./dist/test/browser-compat.js" }, "./test/plugins/runner": { "default": "./dist/test/plugins/runner.js" @@ -248,9 +274,6 @@ "./test/plugins/utils-timers": { "default": "./dist/test/plugins/utils-timers.js" }, - "./test/plugins/utils-highlight": { - "default": "./dist/test/plugins/utils-highlight.js" - }, "./test/plugins/utils-offset": { "default": "./dist/test/plugins/utils-offset.js" }, @@ -340,11 +363,17 @@ "dependencies": { "@oxc-project/types": "catalog:", "@oxlint/plugins": "catalog:", + "@vitest/browser": "catalog:", + "@vitest/browser-playwright": "catalog:", + "@vitest/browser-preview": "catalog:", + "@vitest/browser-webdriverio": "catalog:", "@voidzero-dev/vite-plus-core": "workspace:*", - "@voidzero-dev/vite-plus-test": "workspace:*", + "es-module-lexer": "^1.7.0", + "oxc-parser": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", - "oxlint-tsgolint": "catalog:" + "oxlint-tsgolint": "catalog:", + "vitest": "catalog:" }, "devDependencies": { "@napi-rs/cli": "catalog:", diff --git a/packages/cli/publish-native-addons.ts b/packages/cli/publish-native-addons.ts index ed9c5c51f2..b9dfb3dd35 100644 --- a/packages/cli/publish-native-addons.ts +++ b/packages/cli/publish-native-addons.ts @@ -53,12 +53,6 @@ if (existsSync(rustCliArtifactsDir)) { } } -// Build test package — versions are already bumped on main by prepare_release.yml. -execSync('pnpm --filter=@voidzero-dev/vite-plus-test build', { - cwd: repoRoot, - stdio: 'inherit', -}); - // Create npm directories for NAPI bindings await cli.createNpmDirs({ cwd: currentDir, diff --git a/packages/cli/snap-tests-global/migration-add-git-hooks/snap.txt b/packages/cli/snap-tests-global/migration-add-git-hooks/snap.txt index a3f2242548..4cbf5f238c 100644 --- a/packages/cli/snap-tests-global/migration-add-git-hooks/snap.txt +++ b/packages/cli/snap-tests-global/migration-add-git-hooks/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .vite-hooks/pre-commit # check pre-commit hook vp staged diff --git a/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt b/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt index 00f949c269..7150561501 100644 --- a/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt @@ -51,15 +51,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt b/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt index 608076c519..88f1e7eedf 100644 --- a/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt +++ b/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt @@ -54,15 +54,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt b/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt index bf72628953..7522a22436 100644 --- a/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt +++ b/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-composed-husky-custom-dir/snap.txt b/packages/cli/snap-tests-global/migration-composed-husky-custom-dir/snap.txt index ce8aa8021e..87930d80bd 100644 --- a/packages/cli/snap-tests-global/migration-composed-husky-custom-dir/snap.txt +++ b/packages/cli/snap-tests-global/migration-composed-husky-custom-dir/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .config/husky/pre-commit # pre-commit hook should be in custom dir vp staged diff --git a/packages/cli/snap-tests-global/migration-composed-husky-prepare/snap.txt b/packages/cli/snap-tests-global/migration-composed-husky-prepare/snap.txt index 0b78cecfe4..9449c4f80b 100644 --- a/packages/cli/snap-tests-global/migration-composed-husky-prepare/snap.txt +++ b/packages/cli/snap-tests-global/migration-composed-husky-prepare/snap.txt @@ -21,15 +21,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt index 1aa424701f..c8494ebce7 100644 --- a/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt index cb4a72420f..a4bad90173 100644 --- a/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check oxlint config and staged config merged into vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt b/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt index a62041e3a1..c267a1c475 100644 --- a/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f .lintstagedrc.json # check lintstagedrc.json is removed > cat vite.config.ts # check oxlint config merged into vite.config.ts diff --git a/packages/cli/snap-tests-global/migration-eslint-npx-wrapper/snap.txt b/packages/cli/snap-tests-global/migration-eslint-npx-wrapper/snap.txt index ce804c1e0b..6a34937308 100644 --- a/packages/cli/snap-tests-global/migration-eslint-npx-wrapper/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-npx-wrapper/snap.txt @@ -26,17 +26,53 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f eslint.config.mjs # check eslint config is removed \ No newline at end of file diff --git a/packages/cli/snap-tests-global/migration-eslint/snap.txt b/packages/cli/snap-tests-global/migration-eslint/snap.txt index 636589b8c2..a63e323bbe 100644 --- a/packages/cli/snap-tests-global/migration-eslint/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint/snap.txt @@ -24,18 +24,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f eslint.config.mjs # check eslint config is removed > cat vite.config.ts # check oxlint config merged into vite.config.ts diff --git a/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt index 067d68aba6..d9119ace3e 100644 --- a/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-existing-husky-v8-hooks/snap.txt b/packages/cli/snap-tests-global/migration-existing-husky-v8-hooks/snap.txt index 72ac97116d..a99de01fd3 100644 --- a/packages/cli/snap-tests-global/migration-existing-husky-v8-hooks/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-husky-v8-hooks/snap.txt @@ -24,18 +24,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .husky/pre-commit # hook file should be unchanged (still has bootstrap) . "$(dirname -- "$0")/_/husky.sh" diff --git a/packages/cli/snap-tests-global/migration-existing-husky-v8-multi-hooks/snap.txt b/packages/cli/snap-tests-global/migration-existing-husky-v8-multi-hooks/snap.txt index e9c4d445c6..c7fdd12381 100644 --- a/packages/cli/snap-tests-global/migration-existing-husky-v8-multi-hooks/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-husky-v8-multi-hooks/snap.txt @@ -24,18 +24,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .husky/pre-commit # hook file should be unchanged (still has bootstrap) . "$(dirname -- "$0")/_/husky.sh" diff --git a/packages/cli/snap-tests-global/migration-existing-husky/snap.txt b/packages/cli/snap-tests-global/migration-existing-husky/snap.txt index 260a9e510c..d12d519cac 100644 --- a/packages/cli/snap-tests-global/migration-existing-husky/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-husky/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .vite-hooks/pre-commit # check pre-commit hook rewritten to vp staged vp staged diff --git a/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt b/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt index bc0f54cc72..dabfa69415 100644 --- a/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f .lintstagedrc.json # check lintstagedrc.json (should be deleted after inlining to vite.config.ts) > cat vite.config.ts # check staged config migrated to vite.config.ts diff --git a/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt index 96c9e19d94..2594f21fe1 100644 --- a/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt @@ -21,18 +21,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-existing-prepare-script/snap.txt b/packages/cli/snap-tests-global/migration-existing-prepare-script/snap.txt index 3df5a5ee3e..04fd212766 100644 --- a/packages/cli/snap-tests-global/migration-existing-prepare-script/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-prepare-script/snap.txt @@ -22,18 +22,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .vite-hooks/pre-commit # check pre-commit hook vp staged diff --git a/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt b/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt index cff5f38ba1..78cd8c5d9e 100644 --- a/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt @@ -46,18 +46,54 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > vp migrate --no-interactive # run migration again to check if it is idempotent This project is already using Vite+! Happy coding! diff --git a/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt b/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt index 2da336e742..519fc06549 100644 --- a/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt @@ -48,18 +48,54 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > vp migrate --no-interactive # run migration again to check if it is idempotent This project is already using Vite+! Happy coding! diff --git a/packages/cli/snap-tests-global/migration-from-vitest-config/snap.txt b/packages/cli/snap-tests-global/migration-from-vitest-config/snap.txt index 0c3d5fe6f3..d23d48f4d4 100644 --- a/packages/cli/snap-tests-global/migration-from-vitest-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-vitest-config/snap.txt @@ -8,7 +8,7 @@ import { join } from 'node:path'; import { foo } from '@foo/vite-plugin-foo'; import { playwright } from 'vite-plus/test/browser-playwright'; -import { server } from 'vite-plus/test/browser-playwright/context'; +import { server } from 'vite-plus/test/browser/context'; import { preview } from 'vite-plus/test/browser-preview'; import { webdriverio } from 'vite-plus/test/browser-webdriverio'; import { userEvent } from 'vite-plus/test/browser/context'; @@ -51,15 +51,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-from-vitest-files/snap.txt b/packages/cli/snap-tests-global/migration-from-vitest-files/snap.txt index 0b688b13bf..6980957773 100644 --- a/packages/cli/snap-tests-global/migration-from-vitest-files/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-vitest-files/snap.txt @@ -26,21 +26,57 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat test/hello.ts # check test/hello.ts -import { server } from 'vite-plus/test/browser-playwright/context'; +import { server } from 'vite-plus/test/browser/context'; import { test, describe, expect, it } from 'vite-plus/test'; const { readFile } = server.commands; diff --git a/packages/cli/snap-tests-global/migration-hooks-skip-on-existing-hookspath/snap.txt b/packages/cli/snap-tests-global/migration-hooks-skip-on-existing-hookspath/snap.txt index 2fde12c7ed..8a68364d50 100644 --- a/packages/cli/snap-tests-global/migration-hooks-skip-on-existing-hookspath/snap.txt +++ b/packages/cli/snap-tests-global/migration-hooks-skip-on-existing-hookspath/snap.txt @@ -24,18 +24,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > git config --local core.hooksPath # should still be .custom-hooks .custom-hooks diff --git a/packages/cli/snap-tests-global/migration-husky-latest-dist-tag-v9-installed/snap.txt b/packages/cli/snap-tests-global/migration-husky-latest-dist-tag-v9-installed/snap.txt index 4d39f8f03b..a206964bed 100644 --- a/packages/cli/snap-tests-global/migration-husky-latest-dist-tag-v9-installed/snap.txt +++ b/packages/cli/snap-tests-global/migration-husky-latest-dist-tag-v9-installed/snap.txt @@ -21,15 +21,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-husky-latest-dist-tag/snap.txt b/packages/cli/snap-tests-global/migration-husky-latest-dist-tag/snap.txt index 88721e1e3a..24da13436e 100644 --- a/packages/cli/snap-tests-global/migration-husky-latest-dist-tag/snap.txt +++ b/packages/cli/snap-tests-global/migration-husky-latest-dist-tag/snap.txt @@ -23,15 +23,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-husky-or-prepare/snap.txt b/packages/cli/snap-tests-global/migration-husky-or-prepare/snap.txt index a98449620e..5ed837570a 100644 --- a/packages/cli/snap-tests-global/migration-husky-or-prepare/snap.txt +++ b/packages/cli/snap-tests-global/migration-husky-or-prepare/snap.txt @@ -21,15 +21,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-husky-semicolon-prepare/snap.txt b/packages/cli/snap-tests-global/migration-husky-semicolon-prepare/snap.txt index 42df326a40..8bedb4f985 100644 --- a/packages/cli/snap-tests-global/migration-husky-semicolon-prepare/snap.txt +++ b/packages/cli/snap-tests-global/migration-husky-semicolon-prepare/snap.txt @@ -21,15 +21,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-husky-v8-preserves-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-husky-v8-preserves-lint-staged/snap.txt index d56772a22d..a774cb3326 100644 --- a/packages/cli/snap-tests-global/migration-husky-v8-preserves-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-husky-v8-preserves-lint-staged/snap.txt @@ -27,15 +27,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt b/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt index 86da8a68b4..aa182bb13f 100644 --- a/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt +++ b/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt @@ -22,18 +22,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-lint-staged-merge-fail/snap.txt b/packages/cli/snap-tests-global/migration-lint-staged-merge-fail/snap.txt index 910f89ebd1..690f787b31 100644 --- a/packages/cli/snap-tests-global/migration-lint-staged-merge-fail/snap.txt +++ b/packages/cli/snap-tests-global/migration-lint-staged-merge-fail/snap.txt @@ -30,18 +30,54 @@ Please add staged config to vite.config.ts manually, see https://viteplus.dev/gu > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # vite config should be unchanged (merge failed) const config = { plugins: [] }; diff --git a/packages/cli/snap-tests-global/migration-lint-staged-ts-config/snap.txt b/packages/cli/snap-tests-global/migration-lint-staged-ts-config/snap.txt index 0644922652..6ba1c88d88 100644 --- a/packages/cli/snap-tests-global/migration-lint-staged-ts-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-lint-staged-ts-config/snap.txt @@ -25,18 +25,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat lint-staged.config.ts # check TS config is not modified export default { diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt index e2ad6ee309..3d2323f246 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt @@ -93,18 +93,54 @@ Documentation: https://viteplus.dev/guide/migrate > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check staged config migrated to vite.config.ts import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-merge-fail/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-merge-fail/snap.txt index dc4b6e426a..93995ebc9e 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-merge-fail/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-merge-fail/snap.txt @@ -27,18 +27,54 @@ Please add staged config to vite.config.ts manually, see https://viteplus.dev/gu > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .lintstagedrc.json # config file should be preserved when merge fails { diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-not-support/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-not-support/snap.txt index 8171e7d079..bee3bf5eb6 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-not-support/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-not-support/snap.txt @@ -38,15 +38,51 @@ export default { > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt index 0bc94af760..dd57b74f98 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt @@ -22,18 +22,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test -f .lintstagedrc.json && echo 'lintstagedrc.json still exists' || echo 'lintstagedrc.json was deleted' # should still exist lintstagedrc.json still exists diff --git a/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt b/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt index 5a6900d06e..eab8772149 100644 --- a/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt +++ b/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt @@ -50,15 +50,51 @@ export default { > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt b/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt index e24fa6682a..fd657cd36c 100644 --- a/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt +++ b/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt @@ -84,15 +84,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt index 144d97ebb0..c78562394b 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt @@ -44,8 +44,17 @@ export default defineConfig({ ], "catalog": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest", - "vite-plus": "latest" + "vitest": "4.1.7", + "vite-plus": "latest", + "@vitest/expect": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7" } }, "scripts": { @@ -68,7 +77,16 @@ export default defineConfig({ "packageManager": "bun@", "overrides": { "vite": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "@vitest/expect": "catalog:", + "@vitest/runner": "catalog:", + "@vitest/snapshot": "catalog:", + "@vitest/spy": "catalog:", + "@vitest/utils": "catalog:", + "@vitest/mocker": "catalog:", + "@vitest/pretty-format": "catalog:", + "@vitest/coverage-v8": "catalog:", + "@vitest/coverage-istanbul": "catalog:" } } diff --git a/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt index 5b8caecfe3..53597bbc03 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt @@ -38,7 +38,16 @@ packages: catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: @@ -46,14 +55,41 @@ overrides: 'supertest>superagent': vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' react-click-away-listener>react: peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat packages/app/package.json # check app package.json { diff --git a/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt index f877dfd403..5c92ec6d21 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt @@ -82,20 +82,56 @@ catalog: testnpm2: ^1.0.0 # test comment here to check if the comment is preserved vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest minimumReleaseAge: 1440 overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' minimumReleaseAgeExclude: - vite-plus - '@voidzero-dev/*' diff --git a/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt index 0874f30c30..5de84c74c3 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt @@ -65,7 +65,16 @@ export default defineConfig({ "packageManager": "yarn@", "resolutions": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vitest": "4.1.7", + "@vitest/expect": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7" } } @@ -73,7 +82,16 @@ export default defineConfig({ nodeLinker: node-modules catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest > cat packages/app/package.json # check app package.json diff --git a/packages/cli/snap-tests-global/migration-no-git-repo/snap.txt b/packages/cli/snap-tests-global/migration-no-git-repo/snap.txt index c31403fd00..9afdb35823 100644 --- a/packages/cli/snap-tests-global/migration-no-git-repo/snap.txt +++ b/packages/cli/snap-tests-global/migration-no-git-repo/snap.txt @@ -19,18 +19,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test -d .vite-hooks && echo 'hooks dir exists' || echo 'no hooks dir' hooks dir exists diff --git a/packages/cli/snap-tests-global/migration-no-hooks-with-husky/snap.txt b/packages/cli/snap-tests-global/migration-no-hooks-with-husky/snap.txt index 74aa2b4f85..8b823c5f5d 100644 --- a/packages/cli/snap-tests-global/migration-no-hooks-with-husky/snap.txt +++ b/packages/cli/snap-tests-global/migration-no-hooks-with-husky/snap.txt @@ -26,18 +26,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test -d .husky && echo '.husky directory exists' || echo 'No .husky directory' # verify no .husky directory No .husky directory diff --git a/packages/cli/snap-tests-global/migration-no-hooks/snap.txt b/packages/cli/snap-tests-global/migration-no-hooks/snap.txt index 925cb2962f..2f04ba4e65 100644 --- a/packages/cli/snap-tests-global/migration-no-hooks/snap.txt +++ b/packages/cli/snap-tests-global/migration-no-hooks/snap.txt @@ -17,18 +17,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test -d .vite-hooks && echo '.vite-hooks directory exists' || echo 'No .vite-hooks directory' # verify no .vite-hooks directory No .vite-hooks directory diff --git a/packages/cli/snap-tests-global/migration-other-hook-tool/snap.txt b/packages/cli/snap-tests-global/migration-other-hook-tool/snap.txt index eef56238fe..e6ed2a66c4 100644 --- a/packages/cli/snap-tests-global/migration-other-hook-tool/snap.txt +++ b/packages/cli/snap-tests-global/migration-other-hook-tool/snap.txt @@ -29,15 +29,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt b/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt index b140b1981d..5af856074a 100644 --- a/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt +++ b/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt @@ -49,15 +49,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt b/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt index 88d3d51bac..ea0bb36cce 100644 --- a/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt +++ b/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt @@ -51,15 +51,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-partially-migrated-pre-commit/snap.txt b/packages/cli/snap-tests-global/migration-partially-migrated-pre-commit/snap.txt index ec77acdd83..10d47cf6d2 100644 --- a/packages/cli/snap-tests-global/migration-partially-migrated-pre-commit/snap.txt +++ b/packages/cli/snap-tests-global/migration-partially-migrated-pre-commit/snap.txt @@ -24,18 +24,54 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat .husky/pre-commit # hook file should be unchanged (still has bootstrap) . "$(dirname -- "$0")/_/husky.sh" diff --git a/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt b/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt index 58db2eeebd..13d0aa9d55 100644 --- a/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt @@ -28,18 +28,54 @@ Prettier configuration detected. Auto-migrating to Oxfmt... > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f eslint.config.mjs # check eslint config is removed > test ! -f .prettierrc.json # check prettier config is removed diff --git a/packages/cli/snap-tests-global/migration-prettier-ignore-unknown/snap.txt b/packages/cli/snap-tests-global/migration-prettier-ignore-unknown/snap.txt index 9bae37b1c3..d126f6faa6 100644 --- a/packages/cli/snap-tests-global/migration-prettier-ignore-unknown/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-ignore-unknown/snap.txt @@ -26,17 +26,53 @@ Prettier configuration detected. Auto-migrating to Oxfmt... > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f .prettierrc.json # check prettier config is removed \ No newline at end of file diff --git a/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt index 4f54aaf230..2394bf1b2f 100644 --- a/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt @@ -23,18 +23,54 @@ Prettier configuration detected. Auto-migrating to Oxfmt... > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check oxfmt config and staged config merged into vite.config.ts import { defineConfig } from "vite-plus"; diff --git a/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt b/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt index f788ee36e4..08415367b8 100644 --- a/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt @@ -24,18 +24,54 @@ Prettier configuration detected. Auto-migrating to Oxfmt... > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > cat vite.config.ts # check oxfmt config merged into vite.config.ts with semi/singleQuote settings import { defineConfig } from "vite-plus"; diff --git a/packages/cli/snap-tests-global/migration-prettier/snap.txt b/packages/cli/snap-tests-global/migration-prettier/snap.txt index a9064ccaed..92cc82ff73 100644 --- a/packages/cli/snap-tests-global/migration-prettier/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier/snap.txt @@ -26,18 +26,54 @@ Prettier configuration detected. Auto-migrating to Oxfmt... > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' > test ! -f .prettierrc.json # check prettier config is removed > cat vite.config.ts # check oxfmt config merged into vite.config.ts diff --git a/packages/cli/snap-tests-global/migration-rewrite-declare-module/snap.txt b/packages/cli/snap-tests-global/migration-rewrite-declare-module/snap.txt index 20088a9b48..753912fd8d 100644 --- a/packages/cli/snap-tests-global/migration-rewrite-declare-module/snap.txt +++ b/packages/cli/snap-tests-global/migration-rewrite-declare-module/snap.txt @@ -20,7 +20,7 @@ declare module 'vite-plus' { } } -declare module 'vite-plus/test' { +declare module 'vitest' { export const describe: any; export const it: any; export const expect: any; @@ -28,7 +28,7 @@ declare module 'vite-plus/test' { export const afterAll: any; } -declare module 'vite-plus' { +declare module 'vitest/config' { export function defineConfig(config: any): any; const _default: typeof defineConfig; export default _default; @@ -49,15 +49,51 @@ declare module 'vite-plus' { > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-skip-vite-dependency/snap.txt b/packages/cli/snap-tests-global/migration-skip-vite-dependency/snap.txt index eaf7a8b8e1..55d3c2fe1f 100644 --- a/packages/cli/snap-tests-global/migration-skip-vite-dependency/snap.txt +++ b/packages/cli/snap-tests-global/migration-skip-vite-dependency/snap.txt @@ -44,15 +44,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt index 2110d21ce5..ebca02e4f6 100644 --- a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt +++ b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt @@ -44,15 +44,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-standalone-npm/snap.txt b/packages/cli/snap-tests-global/migration-standalone-npm/snap.txt index 54f57fb5b5..2090f5d98c 100644 --- a/packages/cli/snap-tests-global/migration-standalone-npm/snap.txt +++ b/packages/cli/snap-tests-global/migration-standalone-npm/snap.txt @@ -9,13 +9,22 @@ "name": "migration-standalone-npm", "devDependencies": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest", + "vitest": "4.1.7", "vite-plus": "latest" }, "packageManager": "npm@", "overrides": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vitest": "4.1.7", + "@vitest/expect": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7" } } diff --git a/packages/cli/snap-tests-global/migration-standalone-pnpm/snap.txt b/packages/cli/snap-tests-global/migration-standalone-pnpm/snap.txt index 93b4928ac8..a156c674eb 100644 --- a/packages/cli/snap-tests-global/migration-standalone-pnpm/snap.txt +++ b/packages/cli/snap-tests-global/migration-standalone-pnpm/snap.txt @@ -18,15 +18,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides, peerDependencyRules, and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-subpath/snap.txt b/packages/cli/snap-tests-global/migration-subpath/snap.txt index f68c634e75..1eb44c77d5 100644 --- a/packages/cli/snap-tests-global/migration-subpath/snap.txt +++ b/packages/cli/snap-tests-global/migration-subpath/snap.txt @@ -38,15 +38,51 @@ core.hooksPath is not set > cat foo/pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt index fbdf9b973a..d9e2991b1d 100644 --- a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt +++ b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt @@ -41,15 +41,51 @@ export default defineConfig({ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-vite-version/snap.txt b/packages/cli/snap-tests-global/migration-vite-version/snap.txt index 042250b04c..a3bb23f600 100644 --- a/packages/cli/snap-tests-global/migration-vite-version/snap.txt +++ b/packages/cli/snap-tests-global/migration-vite-version/snap.txt @@ -21,15 +21,51 @@ > cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/migration-vitest-peer-dep/snap.txt b/packages/cli/snap-tests-global/migration-vitest-peer-dep/snap.txt index 588753876e..9c0939021e 100644 --- a/packages/cli/snap-tests-global/migration-vitest-peer-dep/snap.txt +++ b/packages/cli/snap-tests-global/migration-vitest-peer-dep/snap.txt @@ -24,15 +24,51 @@ > cat pnpm-workspace.yaml catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + '@vitest/expect': + '@vitest/runner': + '@vitest/snapshot': + '@vitest/spy': + '@vitest/utils': + '@vitest/mocker': + '@vitest/pretty-format': + '@vitest/coverage-v8': + '@vitest/coverage-istanbul': vite-plus: latest overrides: vite: 'catalog:' vitest: 'catalog:' + '@vitest/expect': 'catalog:' + '@vitest/runner': 'catalog:' + '@vitest/snapshot': 'catalog:' + '@vitest/spy': 'catalog:' + '@vitest/utils': 'catalog:' + '@vitest/mocker': 'catalog:' + '@vitest/pretty-format': 'catalog:' + '@vitest/coverage-v8': 'catalog:' + '@vitest/coverage-istanbul': 'catalog:' peerDependencyRules: allowAny: - vite - vitest + - '@vitest/expect' + - '@vitest/runner' + - '@vitest/snapshot' + - '@vitest/spy' + - '@vitest/utils' + - '@vitest/mocker' + - '@vitest/pretty-format' + - '@vitest/coverage-v8' + - '@vitest/coverage-istanbul' allowedVersions: vite: '*' vitest: '*' + '@vitest/expect': '*' + '@vitest/runner': '*' + '@vitest/snapshot': '*' + '@vitest/spy': '*' + '@vitest/utils': '*' + '@vitest/mocker': '*' + '@vitest/pretty-format': '*' + '@vitest/coverage-v8': '*' + '@vitest/coverage-istanbul': '*' diff --git a/packages/cli/snap-tests-global/new-vite-monorepo-bun/snap.txt b/packages/cli/snap-tests-global/new-vite-monorepo-bun/snap.txt index 0cd639acdc..c1bcd720f0 100644 --- a/packages/cli/snap-tests-global/new-vite-monorepo-bun/snap.txt +++ b/packages/cli/snap-tests-global/new-vite-monorepo-bun/snap.txt @@ -3,6 +3,7 @@ AGENTS.md README.md apps +bunfig.toml package.json packages tsconfig.json @@ -25,9 +26,19 @@ vite.config.ts "prepare": "vp config" }, "devDependencies": { + "vite": "catalog:", "vite-plus": "catalog:" }, "overrides": { + "@vitest/coverage-istanbul": "catalog:", + "@vitest/coverage-v8": "catalog:", + "@vitest/expect": "catalog:", + "@vitest/mocker": "catalog:", + "@vitest/pretty-format": "catalog:", + "@vitest/runner": "catalog:", + "@vitest/snapshot": "catalog:", + "@vitest/spy": "catalog:", + "@vitest/utils": "catalog:", "vite": "catalog:", "vitest": "catalog:" }, @@ -37,7 +48,16 @@ vite.config.ts "packageManager": "bun@", "catalog": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest", + "vitest": "4.1.7", + "@vitest/expect": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", "vite-plus": "latest" } } diff --git a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt index 61de87f5c1..737627ab7a 100644 --- a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt +++ b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt @@ -59,18 +59,54 @@ catalog: "@types/node": ^24 typescript: ^5 vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + "@vitest/expect": + "@vitest/runner": + "@vitest/snapshot": + "@vitest/spy": + "@vitest/utils": + "@vitest/mocker": + "@vitest/pretty-format": + "@vitest/coverage-v8": + "@vitest/coverage-istanbul": vite-plus: latest overrides: vite: "catalog:" vitest: "catalog:" + "@vitest/expect": "catalog:" + "@vitest/runner": "catalog:" + "@vitest/snapshot": "catalog:" + "@vitest/spy": "catalog:" + "@vitest/utils": "catalog:" + "@vitest/mocker": "catalog:" + "@vitest/pretty-format": "catalog:" + "@vitest/coverage-v8": "catalog:" + "@vitest/coverage-istanbul": "catalog:" peerDependencyRules: allowAny: - vite - vitest + - "@vitest/expect" + - "@vitest/runner" + - "@vitest/snapshot" + - "@vitest/spy" + - "@vitest/utils" + - "@vitest/mocker" + - "@vitest/pretty-format" + - "@vitest/coverage-v8" + - "@vitest/coverage-istanbul" allowedVersions: vite: "*" vitest: "*" + "@vitest/expect": "*" + "@vitest/runner": "*" + "@vitest/snapshot": "*" + "@vitest/spy": "*" + "@vitest/utils": "*" + "@vitest/mocker": "*" + "@vitest/pretty-format": "*" + "@vitest/coverage-v8": "*" + "@vitest/coverage-istanbul": "*" > test -f vite-plus-monorepo/.gitignore && echo '.gitignore exists' || echo 'ERROR: .gitignore missing' # verify gitignore renamed from _gitignore .gitignore exists diff --git a/packages/cli/snap-tests/command-helper/snap.txt b/packages/cli/snap-tests/command-helper/snap.txt index 39a738783d..50477d2e68 100644 --- a/packages/cli/snap-tests/command-helper/snap.txt +++ b/packages/cli/snap-tests/command-helper/snap.txt @@ -257,11 +257,11 @@ Options: -h, --help Display this message > vp test -h # test help message -vp test/ +vitest/ WARN: no options were found for your subcommands so we printed the whole output Usage: - $ vp test [...filters] + $ vitest [...filters] Commands: run [...filters] @@ -275,16 +275,16 @@ Commands: complete [shell] For more info, run any command with the `--help` flag: - $ vp test run --help - $ vp test related --help - $ vp test watch --help - $ vp test dev --help - $ vp test bench --help - $ vp test init --help - $ vp test list --help - $ vp test --help - $ vp test complete --help - $ vp test --help --expand-help + $ vitest run --help + $ vitest related --help + $ vitest watch --help + $ vitest dev --help + $ vitest bench --help + $ vitest init --help + $ vitest list --help + $ vitest --help + $ vitest complete --help + $ vitest --help --expand-help Options: -v, --version Display version number diff --git a/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt b/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt index 3804d2905f..46d377d1e3 100644 --- a/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt +++ b/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt @@ -26,18 +26,54 @@ packages: - packages/* catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest + vitest: + "@vitest/expect": + "@vitest/runner": + "@vitest/snapshot": + "@vitest/spy": + "@vitest/utils": + "@vitest/mocker": + "@vitest/pretty-format": + "@vitest/coverage-v8": + "@vitest/coverage-istanbul": vite-plus: latest overrides: vite: "catalog:" vitest: "catalog:" + "@vitest/expect": "catalog:" + "@vitest/runner": "catalog:" + "@vitest/snapshot": "catalog:" + "@vitest/spy": "catalog:" + "@vitest/utils": "catalog:" + "@vitest/mocker": "catalog:" + "@vitest/pretty-format": "catalog:" + "@vitest/coverage-v8": "catalog:" + "@vitest/coverage-istanbul": "catalog:" peerDependencyRules: allowAny: - vite - vitest + - "@vitest/expect" + - "@vitest/runner" + - "@vitest/snapshot" + - "@vitest/spy" + - "@vitest/utils" + - "@vitest/mocker" + - "@vitest/pretty-format" + - "@vitest/coverage-v8" + - "@vitest/coverage-istanbul" allowedVersions: vite: "*" vitest: "*" + "@vitest/expect": "*" + "@vitest/runner": "*" + "@vitest/snapshot": "*" + "@vitest/spy": "*" + "@vitest/utils": "*" + "@vitest/mocker": "*" + "@vitest/pretty-format": "*" + "@vitest/coverage-v8": "*" + "@vitest/coverage-istanbul": "*" > test -d my-mono/.git && echo 'Git initialized' # git-init prompt covers bundled monorepo path Git initialized diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt b/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt index a33d958b0c..7cb09d00a9 100644 --- a/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt +++ b/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt @@ -59,22 +59,6 @@ Finished in ms on 1 file with rules using thread 5 │ ╰──── - × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser-playwright' instead of '@vitest/browser-playwright' in Vite+ projects. - ╭─[src/types.ts:6:16] - 5 │ - 6 │ declare module '@vitest/browser-playwright' {} - · ──────────────────────────── - 7 │ declare module '@vitest/browser-playwright/context' {} - ╰──── - - × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser/context' instead of '@vitest/browser-playwright/context' in Vite+ projects. - ╭─[src/types.ts:7:16] - 6 │ declare module '@vitest/browser-playwright' {} - 7 │ declare module '@vitest/browser-playwright/context' {} - · ──────────────────────────────────── - 8 │ - ╰──── - × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/client' instead of 'vite/client' in Vite+ projects. ╭─[src/types.ts:9:25] 8 │ @@ -83,7 +67,7 @@ Finished in ms on 1 file with rules using thread 10 │ ╰──── -Found 0 warnings and 7 errors. +Found 0 warnings and 5 errors. Finished in ms on 1 file with rules using threads. > vp lint --fix src/index.ts src/types.ts # rewrite vite and vitest imports via the vite-plus oxlint plugin @@ -106,8 +90,8 @@ type BrowserContext = typeof import('vite-plus/test/browser/context'); type BrowserClient = typeof import('vite-plus/test/client'); type PlaywrightProvider = typeof import('vite-plus/test/browser/providers/playwright'); -declare module 'vite-plus/test/browser-playwright' {} -declare module 'vite-plus/test/browser/context' {} +declare module '@vitest/browser-playwright' {} +declare module '@vitest/browser-playwright/context' {} import client = require('vite-plus/client'); diff --git a/packages/cli/snap-tests/test-inline-snapshot-indent/snap.txt b/packages/cli/snap-tests/test-inline-snapshot-indent/snap.txt index 2fb66bcc52..1cb18f6450 100644 --- a/packages/cli/snap-tests/test-inline-snapshot-indent/snap.txt +++ b/packages/cli/snap-tests/test-inline-snapshot-indent/snap.txt @@ -1,5 +1,6 @@ > vp test run -u src/inline-snapshot.test.ts # write inline snapshot via --update (regression test for #1553) - RUN + + RUN v ✓ src/inline-snapshot.test.ts (1 test) ms diff --git a/packages/cli/snap-tests/vite-plugins-async-test/snap.txt b/packages/cli/snap-tests/vite-plugins-async-test/snap.txt index cb8596d640..cd91b5d731 100644 --- a/packages/cli/snap-tests/vite-plugins-async-test/snap.txt +++ b/packages/cli/snap-tests/vite-plugins-async-test/snap.txt @@ -1,5 +1,6 @@ > vp test # async plugins factory should load vitest plugin with configureVitest hook - RUN + + RUN v ✓ src/index.test.ts (1 test) ms diff --git a/packages/cli/snap-tests/vite-plugins-async-test/src/index.test.ts b/packages/cli/snap-tests/vite-plugins-async-test/src/index.test.ts index 7f37cab022..d6f3f94935 100644 --- a/packages/cli/snap-tests/vite-plugins-async-test/src/index.test.ts +++ b/packages/cli/snap-tests/vite-plugins-async-test/src/index.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { expect, onTestFinished, test } from '@voidzero-dev/vite-plus-test'; +import { expect, onTestFinished, test } from 'vitest'; test('async plugin factory should load vitest plugin with configureVitest hook', () => { const markerPath = path.join(import.meta.dirname, '..', '.vitest-plugin-loaded'); diff --git a/packages/cli/src/__tests__/define-config-mocker-rewrite.spec.ts b/packages/cli/src/__tests__/define-config-mocker-rewrite.spec.ts new file mode 100644 index 0000000000..a50668ec67 --- /dev/null +++ b/packages/cli/src/__tests__/define-config-mocker-rewrite.spec.ts @@ -0,0 +1,572 @@ +import type { Plugin } from '@voidzero-dev/vite-plus-core'; +import { describe, expect, it } from 'vitest'; + +import { + AUTO_INLINE_DEPS, + computeAutoInlineList, + defineConfig, + rewriteVitePlusTestSpecifier, +} from '../define-config.ts'; + +const REWRITE_PLUGIN_NAME = 'vite-plus:vitest-specifier-rewrite'; +const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver'; + +function pluginName(p: unknown): string | undefined { + if ( + p && + typeof p === 'object' && + 'name' in p && + typeof (p as { name: unknown }).name === 'string' + ) { + return (p as { name: string }).name; + } + return undefined; +} + +describe('rewriteVitePlusTestSpecifier', () => { + it('is a no-op when source does not mention vite-plus/test', () => { + const code = "import { describe } from 'vitest';\nimport * as fs from 'node:fs';\n"; + expect(rewriteVitePlusTestSpecifier(code)).toBe(code); + }); + + it("rewrites `from 'vite-plus/test'` to `from 'vitest'`", () => { + const input = "import { vi } from 'vite-plus/test';\nvi.mock('./foo');\n"; + const expected = "import { vi } from 'vitest';\nvi.mock('./foo');\n"; + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('rewrites the double-quoted form too', () => { + const input = 'import { vi } from "vite-plus/test";\n'; + const expected = 'import { vi } from "vitest";\n'; + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite subpaths such as vite-plus/test/browser', () => { + const input = "import { context } from 'vite-plus/test/browser';\n"; + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it('does NOT rewrite a bare string literal containing vite-plus/test', () => { + const input = "const x = 'vite-plus/test';\nconsole.log(x);\n"; + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it("rewrites dynamic `import('vite-plus/test')`", () => { + const input = "const mod = await import('vite-plus/test');\n"; + const expected = "const mod = await import('vitest');\n"; + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it("rewrites `require('vite-plus/test')` while leaving the subpath form alone", () => { + const input = [ + "const a = require('vite-plus/test');", + "const b = require('vite-plus/test/browser');", + '', + ].join('\n'); + const expected = [ + "const a = require('vitest');", + "const b = require('vite-plus/test/browser');", + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it("does NOT rewrite `require('vite-plus/test')` inside a template literal", () => { + // Regression: previously the regex-based CJS pass anchored `require` at + // a `\s` boundary, so a `\n` + indentation inside a backtick template + // literal still matched. Snapshot/fixture strings containing example + // code must stay byte-identical. + const input = [ + 'const fixture = `', + " require('vite-plus/test')", + '`;', + 'console.log(fixture);', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it("does NOT rewrite `require('vite-plus/test')` inside a plain string literal", () => { + const input = [ + 'const literal = " require(\'vite-plus/test\')";', + 'console.log(literal);', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it("does NOT rewrite a member call like `.require('vite-plus/test')`", () => { + // The AST walker only matches CallExpression nodes whose callee is the + // identifier `require`. `obj.require(...)` is a MemberExpression callee + // and must be left alone. + const input = "const m = obj.require('vite-plus/test');\n"; + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it('rewrites a real require call alongside a fixture template literal', () => { + // Mixed scenario: a real CJS require should be rewritten, but the + // example code embedded in a template literal must stay untouched. + const input = [ + "const real = require('vite-plus/test');", + 'const fixture = `', + " require('vite-plus/test')", + '`;', + 'console.log(real, fixture);', + '', + ].join('\n'); + const expected = [ + "const real = require('vitest');", + 'const fixture = `', + " require('vite-plus/test')", + '`;', + 'console.log(real, fixture);', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('preserves all other imports in the file', () => { + const input = [ + "import { describe, it, expect } from 'vite-plus/test';", + "import * as fs from 'node:fs';", + "import { something } from 'vite-plus/test/browser';", + '', + ].join('\n'); + const expected = [ + "import { describe, it, expect } from 'vitest';", + "import * as fs from 'node:fs';", + "import { something } from 'vite-plus/test/browser';", + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it("does NOT rewrite `from 'vite-plus/test'` inside a template literal", () => { + const input = [ + "import { it } from 'vite-plus/test';", + "const fixture = `import { vi } from 'vite-plus/test'`;", + 'it("snapshots fixture", () => { console.log(fixture); });', + '', + ].join('\n'); + const expected = [ + "import { it } from 'vitest';", + "const fixture = `import { vi } from 'vite-plus/test'`;", + 'it("snapshots fixture", () => { console.log(fixture); });', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite the pattern inside a plain string-literal error message', () => { + const input = [ + "import { expect, it } from 'vite-plus/test';", + "it('reports the bad specifier', () => {", + ' const message = "Cannot resolve \'vite-plus/test\'";', + " expect(message).toContain('vite-plus/test');", + '});', + '', + ].join('\n'); + const expected = [ + "import { expect, it } from 'vitest';", + "it('reports the bad specifier', () => {", + ' const message = "Cannot resolve \'vite-plus/test\'";', + " expect(message).toContain('vite-plus/test');", + '});', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite the pattern inside a line/block comment or string concat', () => { + const input = [ + "// Reads 'vite-plus/test' off the import map and rewrites it", + "/* require('vite-plus/test') is the CJS form */", + "const composed = 'vite-' + 'plus/test';", + 'const literal = \'require("vite-plus/test")\';', + 'console.log(composed, literal);', + '', + ].join('\n'); + // None of these are real imports — output should be byte-identical. + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it("rewrites a real `import { vi } from 'vite-plus/test'` statement", () => { + const input = ["import { vi } from 'vite-plus/test';", "vi.mock('./foo');", ''].join('\n'); + const expected = ["import { vi } from 'vitest';", "vi.mock('./foo');", ''].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('rewrites imports in a JSX/TSX source where the ESM lexer cannot parse', () => { + const input = [ + "import { describe, it, expect, vi } from 'vite-plus/test';", + "import { render } from 'vitest-browser-react';", + "import { Suspense } from 'react';", + '', + "vi.mock('./router');", + "import { Route } from './route';", + '', + "describe('App', () => {", + " it('renders', () => {", + ' render(Loading...}>);', + ' });', + '});', + '', + ].join('\n'); + const expected = [ + "import { describe, it, expect, vi } from 'vitest';", + "import { render } from 'vitest-browser-react';", + "import { Suspense } from 'react';", + '', + "vi.mock('./router');", + "import { Route } from './route';", + '', + "describe('App', () => {", + " it('renders', () => {", + ' render(Loading...}>);', + ' });', + '});', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('rewrites dynamic imports in a JSX/TSX source too', () => { + const input = [ + 'function App() {', + " const promise = import('vite-plus/test');", + ' return
{promise.toString()}
;', + '}', + '', + ].join('\n'); + const expected = [ + 'function App() {', + " const promise = import('vitest');", + ' return
{promise.toString()}
;', + '}', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite vite-plus/test subpaths in JSX/TSX fallback either', () => { + const input = [ + "import { describe } from 'vite-plus/test';", + "import { ctx } from 'vite-plus/test/browser';", + 'function App() { return
; }', + '', + ].join('\n'); + const expected = [ + "import { describe } from 'vitest';", + "import { ctx } from 'vite-plus/test/browser';", + 'function App() { return
; }', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite the pattern inside a string literal in TSX (oxc-parser fallback)', () => { + // No real import — there's nothing to rewrite. The substring lives inside + // a double-quoted string literal, and the file contains JSX which forces + // the oxc-parser fallback path. + const input = [ + 'function App() {', + ' const msg = "from \'vite-plus/test\'";', + ' return
{msg}
;', + '}', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it('does NOT rewrite the pattern inside JSX text (oxc-parser fallback)', () => { + const input = ['function App() {', " return

from 'vite-plus/test'

;", '}', ''].join( + '\n', + ); + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); + + it("rewrites a real import but preserves a string literal containing 'vite-plus/test' in TSX", () => { + const input = [ + "import { vi } from 'vite-plus/test';", + 'function App() {', + ' const fixture = "import { vi } from \'vite-plus/test\'";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + const expected = [ + "import { vi } from 'vitest';", + 'function App() {', + ' const fixture = "import { vi } from \'vite-plus/test\'";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('rewrites a real dynamic import but preserves a string literal in TSX', () => { + const input = [ + 'function App() {', + " const mod = import('vite-plus/test');", + ' const fixture = "import(\'vite-plus/test\')";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + const expected = [ + 'function App() {', + " const mod = import('vitest');", + ' const fixture = "import(\'vite-plus/test\')";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it("rewrites `export * from 'vite-plus/test'` in TSX (oxc-parser fallback)", () => { + const input = [ + "export * from 'vite-plus/test';", + 'function App() { return
; }', + '', + ].join('\n'); + const expected = ["export * from 'vitest';", 'function App() { return
; }', ''].join( + '\n', + ); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it("rewrites `export { vi } from 'vite-plus/test'` in TSX (oxc-parser fallback)", () => { + const input = [ + "export { vi } from 'vite-plus/test';", + 'function App() { return
; }', + '', + ].join('\n'); + const expected = [ + "export { vi } from 'vitest';", + 'function App() { return
; }', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('rewrites a real re-export but preserves a string literal containing the same text in TSX', () => { + const input = [ + "export * from 'vite-plus/test';", + 'function App() {', + ' const fixture = "export * from \'vite-plus/test\'";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + const expected = [ + "export * from 'vitest';", + 'function App() {', + ' const fixture = "export * from \'vite-plus/test\'";', + ' return

{fixture}

;', + '}', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(expected); + }); + + it('does NOT rewrite a local `export { vi }` (no `from` clause) in TSX', () => { + const input = [ + 'const vi = 1;', + 'export { vi };', + "const note = 'vite-plus/test';", + 'function App() { return
{note}
; }', + '', + ].join('\n'); + expect(rewriteVitePlusTestSpecifier(input)).toBe(input); + }); +}); + +describe('defineConfig project plugin injection', () => { + it('injects rewrite + resolver + auto-inline plugins at the root plugins array', () => { + const existing: Plugin = { name: 'user-existing-root-plugin' }; + const result = defineConfig({ plugins: [existing] }) as { plugins: unknown[] }; + + expect(pluginName(result.plugins[0])).toBe(REWRITE_PLUGIN_NAME); + expect(pluginName(result.plugins[1])).toBe(RESOLVER_PLUGIN_NAME); + expect(pluginName(result.plugins[2])).toBe(AUTO_INLINE_PLUGIN_NAME); + expect(pluginName(result.plugins[3])).toBe('user-existing-root-plugin'); + }); + + it('injects rewrite + resolver + auto-inline plugins into an inline-object project entry, preserving existing plugins', () => { + const existing: Plugin = { name: 'user-unit-project-plugin' }; + const result = defineConfig({ + test: { + projects: [ + { + plugins: [existing], + test: { name: 'unit', include: ['test/unit/**/*.spec.ts'], environment: 'node' }, + }, + ], + }, + }) as { test: { projects: unknown[] } }; + + const project = result.test.projects[0] as { plugins: unknown[]; test: { name: string } }; + expect(project.test.name).toBe('unit'); + expect(pluginName(project.plugins[0])).toBe(REWRITE_PLUGIN_NAME); + expect(pluginName(project.plugins[1])).toBe(RESOLVER_PLUGIN_NAME); + expect(pluginName(project.plugins[2])).toBe(AUTO_INLINE_PLUGIN_NAME); + expect(pluginName(project.plugins[3])).toBe('user-unit-project-plugin'); + // Sanity: the existing plugin reference is preserved (clone shallow-copies the array). + expect(project.plugins[3]).toBe(existing); + }); + + it('injects plugins into the return value of a function-shaped project entry', () => { + const existing: Plugin = { name: 'user-fn-project-plugin' }; + const projectFn = () => ({ + plugins: [existing], + test: { name: 'nuxt', environment: 'happy-dom' as const }, + }); + const result = defineConfig({ + test: { projects: [projectFn] }, + }) as { test: { projects: unknown[] } }; + + const wrapped = result.test.projects[0]; + expect(typeof wrapped).toBe('function'); + + // Vitest passes a `ConfigEnv` to the function; we don't depend on its + // shape here, the wrapper just forwards it. + const fakeEnv = { mode: 'test', command: 'serve' as const }; + const resolved = (wrapped as (env: typeof fakeEnv) => { plugins: unknown[] })(fakeEnv); + expect(pluginName(resolved.plugins[0])).toBe(REWRITE_PLUGIN_NAME); + expect(pluginName(resolved.plugins[1])).toBe(RESOLVER_PLUGIN_NAME); + expect(pluginName(resolved.plugins[2])).toBe(AUTO_INLINE_PLUGIN_NAME); + expect(pluginName(resolved.plugins[3])).toBe('user-fn-project-plugin'); + }); + + it('passes string-glob project entries through unchanged', () => { + const result = defineConfig({ + test: { + projects: ['./packages/*', './apps/*'], + }, + }) as { test: { projects: unknown[] } }; + + expect(result.test.projects).toEqual(['./packages/*', './apps/*']); + }); + + it('handles projects with no existing plugins array', () => { + const result = defineConfig({ + test: { + projects: [ + { + test: { name: 'no-plugins', environment: 'node' }, + }, + ], + }, + }) as { test: { projects: unknown[] } }; + + const project = result.test.projects[0] as { plugins: unknown[]; test: { name: string } }; + expect(project.test.name).toBe('no-plugins'); + expect(project.plugins).toHaveLength(3); + expect(pluginName(project.plugins[0])).toBe(REWRITE_PLUGIN_NAME); + expect(pluginName(project.plugins[1])).toBe(RESOLVER_PLUGIN_NAME); + expect(pluginName(project.plugins[2])).toBe(AUTO_INLINE_PLUGIN_NAME); + }); +}); + +const AUTO_INLINE_PLUGIN_NAME = 'vite-plus:auto-inline-matcher-deps'; + +/** Builds a mock require-factory where only `installedPkgs` resolve. */ +function makeRequireFactory( + installedPkgs: string[], +): (from: string) => { resolve: (id: string) => string } { + return (_from: string) => ({ + resolve(id: string) { + if (installedPkgs.includes(id)) { + return `/mock/node_modules/${id}/index.js`; + } + throw new Error(`Cannot find module '${id}'`); + }, + }); +} + +/** A mock require-factory where every package resolves. */ +const allInstalledFactory = makeRequireFactory([ + '@testing-library/jest-dom', + '@storybook/test', + 'jest-extended', +]); + +/** A mock require-factory where no auto-inline package resolves. */ +const noneInstalledFactory = makeRequireFactory([]); + +describe('computeAutoInlineList', () => { + const ALL = [...AUTO_INLINE_DEPS]; + + it('inlines all packages when all are installed and no existing list', () => { + expect(computeAutoInlineList(undefined, '/project', allInstalledFactory)).toEqual(ALL); + }); + + it('inlines only installed packages — absent ones are skipped', () => { + const only = makeRequireFactory(['@testing-library/jest-dom']); + expect(computeAutoInlineList(undefined, '/project', only)).toEqual([ + '@testing-library/jest-dom', + ]); + }); + + it('returns null when no auto-inline package is installed', () => { + expect(computeAutoInlineList(undefined, '/project', noneInstalledFactory)).toBeNull(); + }); + + it('merges with an existing user inline array, preserving order and deduplicating', () => { + const existing: (string | RegExp)[] = ['my-pkg', '@testing-library/jest-dom']; + const result = computeAutoInlineList(existing, '/project', allInstalledFactory); + expect(result).toEqual([ + 'my-pkg', + '@testing-library/jest-dom', + '@storybook/test', + 'jest-extended', + ]); + // Original array must not be mutated. + expect(existing).toEqual(['my-pkg', '@testing-library/jest-dom']); + }); + + it("returns null when `inline: true` (user opted into 'inline everything')", () => { + expect(computeAutoInlineList(true, '/project', allInstalledFactory)).toBeNull(); + }); + + it('treats a regexp entry that matches an auto-inline pkg as already covered', () => { + const existing: (string | RegExp)[] = [/^@testing-library\//, /^@storybook\//]; + const result = computeAutoInlineList(existing, '/project', allInstalledFactory); + // Both '@testing-library/jest-dom' and '@storybook/test' are covered; + // only 'jest-extended' should be appended. + expect(result).toHaveLength(3); + expect(result![0]).toBeInstanceOf(RegExp); + expect(result![1]).toBeInstanceOf(RegExp); + expect(result![2]).toBe('jest-extended'); + }); + + it('returns null when all auto-inline packages are already in the existing list', () => { + const existing: (string | RegExp)[] = [...AUTO_INLINE_DEPS]; + expect(computeAutoInlineList(existing, '/project', allInstalledFactory)).toBeNull(); + }); + + it('passes the project root to the require factory', () => { + const capturedFromPaths: string[] = []; + const factory = (from: string) => { + capturedFromPaths.push(from); + return { resolve: (_id: string) => `/mock/node_modules/${_id}/index.js` }; + }; + computeAutoInlineList(undefined, '/custom/root', factory); + expect(capturedFromPaths).toEqual(['/custom/root/package.json']); + }); +}); + +describe('defineConfig auto-inline deps plugin registration', () => { + it('registers the auto-inline plugin in the root plugins array with enforce:pre and configResolved', () => { + const result = defineConfig({}) as { plugins: unknown[] }; + const plugin = result.plugins.find( + (p): p is Record => + !!p && typeof p === 'object' && (p as { name?: unknown }).name === AUTO_INLINE_PLUGIN_NAME, + ); + expect(plugin).toBeDefined(); + expect(plugin?.enforce).toBe('pre'); + expect(typeof plugin?.configResolved).toBe('function'); + }); +}); diff --git a/packages/cli/src/__tests__/define-config-test-field.spec.ts b/packages/cli/src/__tests__/define-config-test-field.spec.ts new file mode 100644 index 0000000000..f3a787b65f --- /dev/null +++ b/packages/cli/src/__tests__/define-config-test-field.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { + configDefaults, + coverageConfigDefaults, + defineConfig, + loadConfigFromFile, + mergeConfig, +} from '../index.js'; + +describe('defineConfig test field typing', () => { + it('accepts test config without TS error', () => { + const cfg = defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.spec.ts'], + }, + }); + void cfg; + }); + + it('accepts test config alongside other vite-plus fields', () => { + const cfg = defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, + lint: {}, + }); + void cfg; + }); + + it('re-exports shared and vitest-specific names without star-export conflicts', () => { + expect(typeof defineConfig).toBe('function'); + expect(typeof mergeConfig).toBe('function'); + expect(typeof loadConfigFromFile).toBe('function'); + expect(typeof configDefaults).toBe('object'); + expect(typeof coverageConfigDefaults).toBe('object'); + + const merged = mergeConfig({ test: { globals: true } }, { test: { environment: 'node' } }); + expect(merged.test.globals).toBe(true); + expect(merged.test.environment).toBe('node'); + }); +}); diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts new file mode 100644 index 0000000000..a00125e553 --- /dev/null +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -0,0 +1,102 @@ +import type { Plugin } from '@voidzero-dev/vite-plus-core'; +import { describe, expect, it } from 'vitest'; + +import { defineConfig, isVitestFamilySpecifier } from '../define-config.ts'; + +const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver'; + +function findPlugin(plugins: unknown, name: string): Record | undefined { + if (!Array.isArray(plugins)) { + return undefined; + } + return plugins.find( + (p): p is Record => + !!p && typeof p === 'object' && (p as { name?: unknown }).name === name, + ); +} + +describe('isVitestFamilySpecifier', () => { + it('matches the bare `vitest` specifier', () => { + expect(isVitestFamilySpecifier('vitest')).toBe(true); + }); + + it('matches `vitest/internal/browser`', () => { + expect(isVitestFamilySpecifier('vitest/internal/browser')).toBe(true); + }); + + it('matches `vitest/config`', () => { + expect(isVitestFamilySpecifier('vitest/config')).toBe(true); + }); + + it('matches `@vitest/browser`', () => { + expect(isVitestFamilySpecifier('@vitest/browser')).toBe(true); + }); + + it('matches `@vitest/browser/context`', () => { + expect(isVitestFamilySpecifier('@vitest/browser/context')).toBe(true); + }); + + it('matches `@vitest/expect`', () => { + expect(isVitestFamilySpecifier('@vitest/expect')).toBe(true); + }); + + it('matches a queried subpath (query stripped before matching)', () => { + expect(isVitestFamilySpecifier('vitest/internal/browser?v=1')).toBe(true); + }); + + it('does NOT match `vitest-foo` (not a subpath of vitest)', () => { + expect(isVitestFamilySpecifier('vitest-foo')).toBe(false); + }); + + it('does NOT match the bare scope `@vitest` (no trailing slash)', () => { + expect(isVitestFamilySpecifier('@vitest')).toBe(false); + }); + + it('does NOT match a relative id', () => { + expect(isVitestFamilySpecifier('./local')).toBe(false); + }); + + it('does NOT match an absolute id', () => { + expect(isVitestFamilySpecifier('/abs/path/vitest')).toBe(false); + }); + + it('does NOT match a virtual id', () => { + expect(isVitestFamilySpecifier('\0virtual')).toBe(false); + }); + + it('does NOT match an unrelated bare specifier', () => { + expect(isVitestFamilySpecifier('react')).toBe(false); + }); +}); + +describe('vitePlusVitestResolverPlugin', () => { + it('is injected into the root plugins array as an enforce:pre plugin with resolveId', () => { + const result = defineConfig({}) as { plugins: unknown[] }; + const plugin = findPlugin(result.plugins, RESOLVER_PLUGIN_NAME); + + expect(plugin).toBeDefined(); + expect(plugin?.name).toBe(RESOLVER_PLUGIN_NAME); + expect(plugin?.enforce).toBe('pre'); + expect(typeof plugin?.resolveId).toBe('function'); + }); + + it('is injected into each `test.projects` entry (before user plugins)', () => { + const existing: Plugin = { name: 'user-project-plugin' }; + const result = defineConfig({ + test: { + projects: [ + { test: { name: 'unit', environment: 'node' } }, + { plugins: [existing], test: { name: 'browser', environment: 'jsdom' } }, + ], + }, + }) as { test: { projects: unknown[] } }; + + for (const project of result.test.projects) { + const plugins = (project as { plugins?: unknown }).plugins; + const plugin = findPlugin(plugins, RESOLVER_PLUGIN_NAME); + expect(plugin).toBeDefined(); + expect(plugin?.enforce).toBe('pre'); + expect(typeof plugin?.resolveId).toBe('function'); + } + }); +}); diff --git a/packages/cli/src/__tests__/exports-map.spec.ts b/packages/cli/src/__tests__/exports-map.spec.ts new file mode 100644 index 0000000000..d4fcaa709c --- /dev/null +++ b/packages/cli/src/__tests__/exports-map.spec.ts @@ -0,0 +1,88 @@ +/** + * Regression tests for the generated package.json `exports` map. + * + * Node.js package-exports conditions are order-sensitive: when resolving + * `require('vite-plus/test/config')`, Node walks the condition object and + * picks the first matching key. `default` matches everything, so a wrongly + * ordered map like `{ types, default, require }` causes CJS consumers to + * load the ESM shim — the `.cjs` shim becomes unreachable. + * + * These tests pin the invariant that any dual-condition entry emits + * `require` BEFORE `default` and that runtime resolution returns the + * expected file extension for each consumer. + */ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import url from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const cliPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '../..'); +const cliPkgJsonPath = path.join(cliPkgDir, 'package.json'); +const requireFromHere = createRequire(import.meta.url); + +type ExportConditions = Record; + +function isConditionObject(value: unknown): value is ExportConditions { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +describe('package.json exports map', () => { + it('every dual-condition entry emits `require` before `default`', () => { + const pkg = JSON.parse(fs.readFileSync(cliPkgJsonPath, 'utf-8')); + const exports = pkg.exports as Record; + + const offenders: Array<{ path: string; order: string[] }> = []; + + function walk(subpath: string, value: unknown) { + if (!isConditionObject(value)) { + return; + } + const keys = Object.keys(value); + const requireIdx = keys.indexOf('require'); + const defaultIdx = keys.indexOf('default'); + if (requireIdx !== -1 && defaultIdx !== -1 && requireIdx > defaultIdx) { + offenders.push({ path: subpath, order: keys }); + } + for (const [k, v] of Object.entries(value)) { + walk(`${subpath} > ${k}`, v); + } + } + + for (const [subpath, value] of Object.entries(exports)) { + walk(subpath, value); + } + + expect(offenders, 'entries with require ordered after default').toEqual([]); + }); + + it('./test/config has both `require` and `default`, with `require` first', () => { + const pkg = JSON.parse(fs.readFileSync(cliPkgJsonPath, 'utf-8')); + const entry = (pkg.exports as Record)['./test/config']; + expect(isConditionObject(entry)).toBe(true); + const keys = Object.keys(entry as ExportConditions); + expect(keys).toContain('require'); + expect(keys).toContain('default'); + expect(keys.indexOf('require')).toBeLessThan(keys.indexOf('default')); + }); + + it('`require.resolve("vite-plus/test/config")` resolves to the .cjs shim', () => { + const resolved = requireFromHere.resolve('vite-plus/test/config'); + expect(resolved.endsWith('.cjs'), `resolved to ${resolved}`).toBe(true); + }); + + it('ESM `import.meta.resolve("vite-plus/test/config")` resolves to the .js shim', () => { + // import.meta.resolve is sync in modern Node (>= 20.6) and respects the + // `default` (ESM) condition for ESM consumers. + const resolved = import.meta.resolve('vite-plus/test/config'); + expect(resolved.endsWith('.js'), `resolved to ${resolved}`).toBe(true); + }); + + it('CJS shim at ./test/config delegates to vitest/config via require()', () => { + const cfg = requireFromHere('vite-plus/test/config') as Record; + expect(cfg).toBeTypeOf('object'); + // vitest/config re-exports defineConfig / configDefaults — sanity-check one. + expect(typeof cfg.defineConfig).toBe('function'); + }); +}); diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index 10ff3b8a21..2874b3904b 100644 --- a/packages/cli/src/__tests__/index.spec.ts +++ b/packages/cli/src/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, test, vi } from '@voidzero-dev/vite-plus-test'; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; import { configDefaults, @@ -90,6 +90,22 @@ test('lazyPlugins wraps sync function returning a Promise into array', () => { expect(result).not.toBeInstanceOf(Promise); }); +// defineConfig auto-injects three internal plugins before user-supplied +// plugins: vite-plus:vitest-specifier-rewrite, vite-plus:vitest-resolver, and +// vite-plus:auto-inline-matcher-deps. The helper below strips those prefix +// entries so tests can assert on user-supplied plugins only. +const REWRITE_PLUGIN_NAME = 'vite-plus:vitest-specifier-rewrite'; +const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver'; +const AUTO_INLINE_PLUGIN_NAME = 'vite-plus:auto-inline-matcher-deps'; +const userPlugins = (plugins: unknown): unknown[] => { + expect(Array.isArray(plugins)).toBe(true); + const arr = plugins as unknown[]; + expect((arr[0] as { name?: string })?.name).toBe(REWRITE_PLUGIN_NAME); + expect((arr[1] as { name?: string })?.name).toBe(RESOLVER_PLUGIN_NAME); + expect((arr[2] as { name?: string })?.name).toBe(AUTO_INLINE_PLUGIN_NAME); + return arr.slice(3); +}; + // lazyPlugins type compatibility tests — these verify at compile time that // lazyPlugins return types satisfy Vite's plugins?: PluginOption[] field. @@ -99,7 +115,7 @@ test('lazyPlugins sync return type satisfies plugins field', () => { const config = defineConfig({ plugins: lazyPlugins(() => [{ name: 'sync-type-test' }]), }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); test('lazyPlugins async return type satisfies plugins field', () => { @@ -119,7 +135,8 @@ test('lazyPlugins undefined return satisfies plugins field', () => { const config = defineConfig({ plugins: lazyPlugins(() => [{ name: 'skipped' }]), }); - expect(config.plugins).toBeUndefined(); + // lazyPlugins returns undefined, but defineConfig still injects its rewrite plugin. + expect(userPlugins(config.plugins).length).toBe(0); }); test('lazyPlugins with vitest configureVitest plugin satisfies plugins field', () => { @@ -132,7 +149,7 @@ test('lazyPlugins with vitest configureVitest plugin satisfies plugins field', ( }, ]), }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); // defineConfig compatibility tests @@ -141,35 +158,35 @@ test('defineConfig passes through plain plugins array', () => { const config = defineConfig({ plugins: [{ name: 'test-plugin' }], }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); test('defineConfig supports Plugin objects in plugins array', () => { const config = defineConfig({ plugins: [{ name: 'plugin-a' }, { name: 'plugin-b' }], }); - expect(config.plugins?.length).toBe(2); + expect(userPlugins(config.plugins).length).toBe(2); }); test('defineConfig supports falsy values in plugins array', () => { const config = defineConfig({ plugins: [{ name: 'real-plugin' }, false, null, undefined], }); - expect(config.plugins?.length).toBe(4); + expect(userPlugins(config.plugins).length).toBe(4); }); test('defineConfig supports nested plugin arrays', () => { const config = defineConfig({ plugins: [[{ name: 'nested-a' }, { name: 'nested-b' }], { name: 'top-level' }], }); - expect(config.plugins?.length).toBe(2); + expect(userPlugins(config.plugins).length).toBe(2); }); test('defineConfig supports Promise in plugins array', () => { const config = defineConfig({ plugins: [Promise.resolve({ name: 'async-plugin' })], }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); test('defineConfig supports mixed PluginOption types in array', () => { @@ -183,19 +200,20 @@ test('defineConfig supports mixed PluginOption types in array', () => { undefined, ], }); - expect(config.plugins?.length).toBe(6); + expect(userPlugins(config.plugins).length).toBe(6); }); test('defineConfig supports empty plugins array', () => { const config = defineConfig({ plugins: [], }); - expect(config.plugins?.length).toBe(0); + expect(userPlugins(config.plugins).length).toBe(0); }); test('defineConfig supports config without plugins', () => { const config = defineConfig({}); - expect(config.plugins).toBeUndefined(); + // defineConfig always injects its rewrite plugin even when user omits `plugins`. + expect(userPlugins(config.plugins).length).toBe(0); }); test('defineConfig supports function config with plain plugins array', () => { @@ -203,7 +221,7 @@ test('defineConfig supports function config with plain plugins array', () => { plugins: [{ name: 'fn-plugin' }], })); const config = configFn({ command: 'build', mode: 'production' }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); test('defineConfig supports async function config with plain plugins array', async () => { @@ -211,7 +229,7 @@ test('defineConfig supports async function config with plain plugins array', asy plugins: [{ name: 'async-fn-plugin' }], })); const config = await configFn({ command: 'build', mode: 'production' }); - expect(config.plugins?.length).toBe(1); + expect(userPlugins(config.plugins).length).toBe(1); }); test('defineConfig supports vitest plugin with configureVitest hook', () => { @@ -225,6 +243,7 @@ test('defineConfig supports vitest plugin with configureVitest hook', () => { }, ], }); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('vitest-plugin'); + const userOnly = userPlugins(config.plugins); + expect(userOnly.length).toBe(1); + expect((userOnly[0] as { name: string })?.name).toBe('vitest-plugin'); }); diff --git a/packages/cli/src/__tests__/oxlint-plugin.spec.ts b/packages/cli/src/__tests__/oxlint-plugin.spec.ts index 45a5d1bb44..281c34ec0e 100644 --- a/packages/cli/src/__tests__/oxlint-plugin.spec.ts +++ b/packages/cli/src/__tests__/oxlint-plugin.spec.ts @@ -66,6 +66,10 @@ describe('rewriteVitePlusImportSpecifier', () => { expect(rewriteVitePlusImportSpecifier('@vitest/browser/locators')).toBe( 'vite-plus/test/locators', ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser/matchers')).toBe( + 'vite-plus/test/matchers', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser/utils')).toBe('vite-plus/test/utils'); expect(rewriteVitePlusImportSpecifier('@vitest/browser-playwright/context')).toBe( 'vite-plus/test/browser/context', ); @@ -107,8 +111,43 @@ new RuleTester({ code: `type TestFn = typeof import('vite-plus/test')['test']`, filename: 'types.ts', }, + // `declare module 'vitest…'` / `declare module '@vitest/browser…'` are + // intentionally NOT autofixed — they target the upstream module identity + // so type augmentations merge with what `vite-plus/test*` re-exports. + { + code: `declare module 'vitest' {}`, + filename: 'types.ts', + }, + { + code: `declare module 'vitest/node' {}`, + filename: 'types.ts', + }, + { + code: `declare module '@vitest/browser' {}`, + filename: 'types.ts', + }, + { + code: `declare module '@vitest/browser/context' {}`, + filename: 'types.ts', + }, + { + code: `declare module '@vitest/browser-playwright' {}`, + filename: 'types.ts', + }, + { + code: `declare module '@vitest/browser-playwright/context' {}`, + filename: 'types.ts', + }, ], invalid: [ + { + // `declare module 'vite'` IS rewritten — the vite family doesn't + // re-export upstream vite types so augmentation works against either id. + code: `declare module 'vite' {}`, + errors: 1, + filename: 'types.ts', + output: `declare module 'vite-plus' {}`, + }, { code: `import { defineConfig } from 'vite'`, errors: 1, @@ -131,22 +170,20 @@ new RuleTester({ output: `type TestFn = typeof import('vite-plus/test')['test']`, }, { - code: `declare module '@vitest/browser-playwright' {}`, + code: `type BrowserClient = typeof import('@vitest/browser/client')`, errors: 1, filename: 'types.ts', - output: `declare module 'vite-plus/test/browser-playwright' {}`, + output: `type BrowserClient = typeof import('vite-plus/test/client')`, }, { - code: `declare module '@vitest/browser-playwright/context' {}`, + code: `import { expect } from '@vitest/browser/matchers'`, errors: 1, - filename: 'types.ts', - output: `declare module 'vite-plus/test/browser/context' {}`, + output: `import { expect } from 'vite-plus/test/matchers'`, }, { - code: `type BrowserClient = typeof import('@vitest/browser/client')`, + code: `import { getElementError } from '@vitest/browser/utils'`, errors: 1, - filename: 'types.ts', - output: `type BrowserClient = typeof import('vite-plus/test/client')`, + output: `import { getElementError } from 'vite-plus/test/utils'`, }, { code: `type PlaywrightProvider = typeof import('@vitest/browser-playwright/provider')`, diff --git a/packages/cli/src/__tests__/pack.spec.ts b/packages/cli/src/__tests__/pack.spec.ts index 4ab798865c..cda2669b89 100644 --- a/packages/cli/src/__tests__/pack.spec.ts +++ b/packages/cli/src/__tests__/pack.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@voidzero-dev/vite-plus-test'; +import { expect, test } from 'vitest'; import { build, diff --git a/packages/cli/src/__tests__/resolve-lint.spec.ts b/packages/cli/src/__tests__/resolve-lint.spec.ts index de1c7755f9..e0b6492651 100644 --- a/packages/cli/src/__tests__/resolve-lint.spec.ts +++ b/packages/cli/src/__tests__/resolve-lint.spec.ts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; -import { describe, expect, it } from '@voidzero-dev/vite-plus-test'; +import { describe, expect, it } from 'vitest'; import { lint } from '../resolve-lint.js'; diff --git a/packages/cli/src/__tests__/versions.spec.ts b/packages/cli/src/__tests__/versions.spec.ts index c8ef59dd9a..c66900fe7f 100644 --- a/packages/cli/src/__tests__/versions.spec.ts +++ b/packages/cli/src/__tests__/versions.spec.ts @@ -5,15 +5,16 @@ * that syncVersionsExport() produces correct artifacts. */ import fs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; import url from 'node:url'; -import { describe, expect, it } from '@voidzero-dev/vite-plus-test'; +import { describe, expect, it } from 'vitest'; const cliPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '../..'); const distDir = path.join(cliPkgDir, 'dist'); const corePkgPath = path.join(cliPkgDir, '../core/package.json'); -const testPkgPath = path.join(cliPkgDir, '../test/package.json'); +const vitestPkgPath = createRequire(import.meta.url).resolve('vitest/package.json'); describe('versions export', () => { describe('build artifacts', () => { @@ -48,15 +49,13 @@ describe('versions export', () => { } }); - it('should contain all test bundledVersions', async () => { - const testPkg = JSON.parse(fs.readFileSync(testPkgPath, 'utf-8')); + it('should contain vitest version matching installed package', async () => { + const vitestPkg = JSON.parse(fs.readFileSync(vitestPkgPath, 'utf-8')); const mod = await import('../../dist/versions.js'); const versions = mod.versions as Record; - for (const [key, value] of Object.entries( - testPkg.bundledVersions as Record, - )) { - expect(versions[key], `versions.${key} should match test bundledVersions`).toBe(value); - } + expect(versions.vitest, 'versions.vitest should match installed vitest version').toBe( + vitestPkg.version, + ); }); }); diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index df4f333c36..acec7d7cc6 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -1,10 +1,17 @@ +import { createRequire } from 'node:module'; + import type { PluginOption, UserConfig } from '@voidzero-dev/vite-plus-core'; +import { initSync, parse, type ImportSpecifier } from 'es-module-lexer'; +import { parseSync as oxcParseSync, Visitor } from 'oxc-parser'; +import type { OxfmtConfig } from 'oxfmt'; +import type { OxlintConfig } from 'oxlint'; import { defineConfig as viteDefineConfig, type ConfigEnv, -} from '@voidzero-dev/vite-plus-test/config'; -import type { OxfmtConfig } from 'oxfmt'; -import type { OxlintConfig } from 'oxlint'; + type TestProjectConfiguration, + type UserProjectConfigFn, +} from 'vitest/config'; +import type { InlineConfig as VitestInlineConfig } from 'vitest/node'; import type { PackUserConfig } from './pack.ts'; import type { RunConfig } from './run-config.ts'; @@ -39,6 +46,16 @@ declare module '@voidzero-dev/vite-plus-core' { */ defaultTemplate?: string; }; + + /** + * Vitest test configuration. + * + * Vitest augments vite's `UserConfig` with a `test` field via + * `declare module 'vite'`, but vite-plus-core is a fork of vite so that + * augmentation does not apply here. Re-declare it locally so user + * configs like `defineConfig({ test: { globals: true } })` typecheck. + */ + test?: VitestInlineConfig; } } @@ -52,6 +69,607 @@ type ViteUserConfigExport = | ViteUserConfigFnPromise | ViteUserConfigFn; +/** + * Rewrite bare-root `vite-plus/test` import specifiers to `vitest` so that + * `@vitest/mocker`'s static hoister (which hardcodes `hoistedModule = "vitest"`) + * recognizes calls like `vi.mock(...)`. Subpaths such as + * `vite-plus/test/browser` are intentionally left unchanged. + * + * Task #50 pins `vitest` and the `@vitest/*` family so both specifiers resolve + * to the same physical module, making this rewrite runtime-safe. + * + * Uses `es-module-lexer` for the fast path on plain JS/TS so only real ESM + * `import`/`export ... from` and dynamic `import()` specifiers are touched — + * string literals, template literals, and error messages that happen to + * contain `vite-plus/test` are left alone. When `es-module-lexer` cannot + * parse the source (typically JSX/TSX), we fall back to `oxc-parser`, which + * understands TSX and exposes precise import-specifier offsets so JSX text + * and string-literal occurrences of `vite-plus/test` stay untouched. + * CommonJS `require(...)` calls are handled by a separate `oxc-parser` walk — + * es-module-lexer / oxc-parser's ESM API only surface real ESM imports, so + * the require pass needs its own AST visitor (which also ensures `require` + * inside template literals and string contents stays untouched). + * + * Exported for unit testing. + */ +const TARGET_SPECIFIER = 'vite-plus/test'; +const TARGET_REPLACEMENT = 'vitest'; + +// Filename passed to `oxc-parser` for the JSX/TSX fallback. The extension is +// what selects TSX-mode parsing — the file does not need to exist on disk. +const OXC_FALLBACK_FILENAME = 'rewrite.tsx'; + +// Quoted forms of the target specifier that oxc-parser's `dynamicImports` +// entries can yield when the argument is a plain string literal. `moduleRequest` +// for a dynamic import does not expose `.value`, so we slice the source and +// compare against these literal forms. +const QUOTED_TARGETS: ReadonlySet = new Set([ + `'${TARGET_SPECIFIER}'`, + `"${TARGET_SPECIFIER}"`, +]); + +let esLexerInitialized = false; +function ensureLexerInit(): void { + if (esLexerInitialized) { + return; + } + initSync(); + esLexerInitialized = true; +} + +/** + * Fallback rewrite for sources that `es-module-lexer` cannot parse — most + * commonly JSX/TSX. Uses `oxc-parser` to locate real static and dynamic + * import specifiers so JSX text and string literals containing + * `vite-plus/test` are left alone. + * + * Returns the original `code` unchanged if `oxc-parser` itself throws — + * we never fall back to a regex on raw TSX, because that re-introduces the + * exact bug this function exists to fix. + */ +function rewriteWithOxcParser(code: string): string { + let staticImports: ReadonlyArray<{ + moduleRequest: { value: string; start: number; end: number }; + }>; + let dynamicImports: ReadonlyArray<{ moduleRequest: { start: number; end: number } }>; + let staticExports: ReadonlyArray<{ + entries: ReadonlyArray<{ + moduleRequest: { value: string; start: number; end: number } | null; + }>; + }>; + try { + const parsed = oxcParseSync(OXC_FALLBACK_FILENAME, code); + staticImports = parsed.module.staticImports; + dynamicImports = parsed.module.dynamicImports; + staticExports = parsed.module.staticExports; + } catch { + // Extremely malformed input that oxc-parser cannot recover from. Leave + // the source untouched rather than risk a regex-based rewrite that would + // again clobber JSX text / string literals. + return code; + } + + // Collect every rewrite as a (start, end, replacement) triple, then apply + // them in reverse offset order so earlier splices don't shift later ones. + // For both static and dynamic imports, `moduleRequest.start/end` bound the + // specifier INCLUDING its surrounding quotes, so we preserve the original + // quote character in the replacement. + type Edit = { start: number; end: number; replacement: string }; + const edits: Edit[] = []; + + for (const si of staticImports) { + const mr = si.moduleRequest; + if (mr.value !== TARGET_SPECIFIER) { + continue; + } + const quote = code[mr.start]; + if (quote !== '"' && quote !== "'") { + continue; + } + edits.push({ + start: mr.start, + end: mr.end, + replacement: `${quote}${TARGET_REPLACEMENT}${quote}`, + }); + } + + for (const di of dynamicImports) { + const mr = di.moduleRequest; + const slice = code.slice(mr.start, mr.end); + if (!QUOTED_TARGETS.has(slice)) { + continue; + } + const quote = slice[0]; + edits.push({ + start: mr.start, + end: mr.end, + replacement: `${quote}${TARGET_REPLACEMENT}${quote}`, + }); + } + + for (const entry of staticExports.flatMap((e) => e.entries)) { + const mr = entry.moduleRequest; + if (mr === null || mr.value !== TARGET_SPECIFIER) { + continue; + } + const quote = code[mr.start]; + if (quote !== '"' && quote !== "'") { + continue; + } + edits.push({ + start: mr.start, + end: mr.end, + replacement: `${quote}${TARGET_REPLACEMENT}${quote}`, + }); + } + + if (edits.length === 0) { + return code; + } + + edits.sort((a, b) => a.start - b.start); + let result = code; + for (let i = edits.length - 1; i >= 0; i--) { + const { start, end, replacement } = edits[i]; + result = result.slice(0, start) + replacement + result.slice(end); + } + return result; +} + +/** + * AST-driven rewrite for CommonJS `require('vite-plus/test')` calls. + * + * The previous implementation used a regex with a lookbehind that anchored + * `require` at a statement-ish boundary, but raw newline + whitespace inside + * a JS template literal also matches that boundary — so fixture/snapshot + * strings like `` `\n require('vite-plus/test')\n` `` were mutated even + * though they were not real require calls. + * + * Instead, walk the source with `oxc-parser` and only edit a string-literal + * argument when its parent is a `CallExpression` whose callee is the plain + * `Identifier "require"`. Template literals, string literals, JSX text, + * member calls like `obj.require(...)`, and string concatenation are all + * distinct AST nodes and therefore left untouched. + * + * Returns the input unchanged when `oxc-parser` cannot parse the source — + * we never fall back to a regex on raw user code. + */ +function rewriteRequireCalls(code: string): string { + // Cheap pre-filter: skip parsing when the file has no `require` token at all. + if (!code.includes('require')) { + return code; + } + + type Edit = { start: number; end: number; replacement: string }; + const edits: Edit[] = []; + + let parsed; + try { + parsed = oxcParseSync(OXC_FALLBACK_FILENAME, code); + } catch { + // Parser bailout — leave the source untouched. Never fall back to regex + // on raw user code; that re-introduces the template-literal false-positive + // that this function exists to fix. + return code; + } + + const visitor = new Visitor({ + CallExpression(node) { + const callee = node.callee; + if (callee.type !== 'Identifier' || callee.name !== 'require') { + return; + } + const first = node.arguments[0]; + if (!first || first.type !== 'Literal' || typeof first.value !== 'string') { + return; + } + if (first.value !== TARGET_SPECIFIER) { + return; + } + // `first.start`/`first.end` bound the literal INCLUDING its quotes. + const quote = code[first.start]; + if (quote !== '"' && quote !== "'") { + return; + } + edits.push({ + start: first.start, + end: first.end, + replacement: `${quote}${TARGET_REPLACEMENT}${quote}`, + }); + }, + }); + visitor.visit(parsed.program); + + if (edits.length === 0) { + return code; + } + + edits.sort((a, b) => a.start - b.start); + let result = code; + for (let i = edits.length - 1; i >= 0; i--) { + const { start, end, replacement } = edits[i]; + result = result.slice(0, start) + replacement + result.slice(end); + } + return result; +} + +export function rewriteVitePlusTestSpecifier(code: string): string { + if (!code.includes(TARGET_SPECIFIER)) { + return code; + } + + // Step 1: rewrite ESM static/dynamic imports via es-module-lexer (fast path). + let result = code; + let imports: ReadonlyArray | undefined; + let lexerThrew = false; + try { + ensureLexerInit(); + [imports] = parse(code); + } catch { + // Parse failure (JSX/TSX, non-JS file, syntax error before transformation, + // etc.). Fall back to the oxc-parser pass below. + imports = undefined; + lexerThrew = true; + } + + if (imports && imports.length > 0) { + // Walk in reverse so earlier offsets stay valid as we splice. + const matches = imports.filter((i) => i.n === TARGET_SPECIFIER); + for (let i = matches.length - 1; i >= 0; i--) { + const { s, e, d } = matches[i]; + // For static imports, `s`/`e` bound the specifier name without quotes. + // For dynamic imports (`d !== -1`), they bound the full string literal + // expression including its quotes, so wrap the replacement to preserve them. + const replacement = d === -1 ? TARGET_REPLACEMENT : `'${TARGET_REPLACEMENT}'`; + result = result.slice(0, s) + replacement + result.slice(e); + } + } else if (lexerThrew) { + // `es-module-lexer` can't parse JSX/TSX, so .tsx test files with + // `vi.mock(...)` were silently left with the original `vite-plus/test` + // specifier. That causes `@vitest/mocker` to refuse to hoist the mock + // and crash at runtime with `Cannot access '__vi_import_0__' before + // initialization`. Fall back to `oxc-parser`, which handles TSX and + // distinguishes real imports from JSX text / string literals. + result = rewriteWithOxcParser(code); + } + + // Step 2: rewrite CJS require() calls (not seen by es-module-lexer / the + // oxc-parser ESM API) via a dedicated AST walk. Template literals and + // string-literal contents that happen to contain + // `require('vite-plus/test')` are left untouched because the visitor only + // matches real `CallExpression` callee identifiers. + result = rewriteRequireCalls(result); + + return result; +} + +function vitePlusTestSpecifierRewritePlugin(): PluginOption { + return { + name: 'vite-plus:vitest-specifier-rewrite', + enforce: 'pre', + transform(code, id) { + if (id.includes('/node_modules/')) { + return null; + } + const newCode = rewriteVitePlusTestSpecifier(code); + if (newCode === code) { + return null; + } + return { code: newCode, map: null }; + }, + }; +} + +/** + * `require` anchored at THIS module's location so `require.resolve` reaches + * the `vitest` / `@vitest/*` family that the `vite-plus` package directly + * depends on — even from a consumer project where they are only transitive. + * + * `define-config.ts` is bundled by tsdown in BOTH formats: ESM (`shims: true`, + * which defines a module-scoped `__dirname`) and CJS (where `__dirname` is the + * Node global). The guard picks `__dirname` whenever it exists and otherwise + * falls back to `import.meta.url`; tsdown rewrites the latter to + * `pathToFileURL(__filename).href` in the CJS bundle, so it is safe in both. + */ +const vitePlusRequire = createRequire( + typeof __dirname !== 'undefined' ? __dirname : import.meta.url, +); + +/** + * `require` anchored at the resolved `vitest` package. The `@vitest/*` family + * (`@vitest/expect`, `@vitest/runner`, `@vitest/snapshot`, …) are dependencies + * of `vitest` itself — not direct deps of `vite-plus` — so they are reachable + * from `vitest`'s location but not from [[vitePlusRequire]]. Created lazily and + * cached; `null` once a creation attempt has failed so we never retry. + */ +let vitestRequire: NodeRequire | null | undefined; +function getVitestRequire(): NodeRequire | null { + if (vitestRequire !== undefined) { + return vitestRequire; + } + try { + vitestRequire = createRequire(vitePlusRequire.resolve('vitest')); + } catch { + vitestRequire = null; + } + return vitestRequire; +} + +/** + * Match the `vitest` / `@vitest/*` family of bare specifiers — the imports a + * browser-mode Vite dev server must resolve. Any query string is stripped + * first; relative (`./`), absolute (`/`), and virtual (`\0`) ids never match. + * + * Exported for unit testing. + */ +export function isVitestFamilySpecifier(id: string): boolean { + const bare = id.split('?')[0]; + if (bare.startsWith('.') || bare.startsWith('/') || bare.startsWith('\0')) { + return false; + } + return ( + bare === 'vitest' || + bare.startsWith('vitest/') || + bare === '@vitest/browser' || + bare.startsWith('@vitest/') + ); +} + +/** + * Rescue `vitest` / `@vitest/*` resolution for browser-mode tests. + * + * In an established project that depends only on `vite-plus`, both `vitest` + * and `@vitest/browser` are transitive deps. pnpm's isolated layout only + * exposes a package's *direct* deps, so the browser-mode Vite dev server + * (rooted at the consumer project) cannot resolve `vitest/internal/browser`, + * `@vitest/expect`, etc. Non-browser tests are unaffected — vitest's own + * module runner handles resolution there. + * + * This plugin is a pure fallback: it FIRST defers to the default resolver + * (`this.resolve(..., { skipSelf: true })`), so projects that already resolve + * fine see zero behavior change. Only when default resolution returns `null` + * does it fall back to resolving from `vite-plus`'s own dependency tree: + * `vitest` / `vitest/*` / `@vitest/browser*` via [[vitePlusRequire]], and the + * nested `@vitest/*` family (deps of `vitest`) via [[getVitestRequire]]. + */ +function vitePlusVitestResolverPlugin(): PluginOption { + return { + name: 'vite-plus:vitest-resolver', + enforce: 'pre', + async resolveId(id, importer, options) { + if (!isVitestFamilySpecifier(id)) { + return null; + } + // The failing imports are all clean bare specifiers; a queried id is + // outside the scope of this fallback — let the default resolver handle it. + if (id.includes('?')) { + return null; + } + // Prefer the project's own resolution. `skipSelf` avoids infinite + // recursion back into this plugin. + const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); + if (resolved) { + return resolved; + } + // Fallback: resolve from vite-plus's own dependency tree. `require.resolve` + // throws on failure — never let that escape the resolver. + try { + return vitePlusRequire.resolve(id); + } catch { + // `vitest` and `@vitest/browser*` are direct deps of vite-plus, but the + // nested `@vitest/*` family lives under `vitest` — resolve those from + // vitest's own location. + const fromVitest = getVitestRequire(); + if (fromVitest) { + try { + return fromVitest.resolve(id); + } catch { + return null; + } + } + return null; + } + }, + }; +} + +/** + * Packages that register Vitest `expect` matchers via `expect.extend()` from + * a side-effect import. When Vite serves these from a separate module graph + * than the test runtime, the matchers register on a different `expect` + * instance and `expect(...).` is undefined at call time (vitest + * issue #897). Inlining them into the test server's module graph forces + * registration on the same instance. + * + * Only packages that are **installed** in the consumer project are inlined. + * Absent packages are silently skipped so the server-deps optimizer never + * tries to resolve a name that does not exist in the project's node_modules. + * + * The check is deferred to a `configResolved` plugin hook so that + * `resolvedConfig.root` points at the actual project root (the value vite has + * already normalised), rather than relying on `process.cwd()` at config-load + * time (which can differ in workspace / monorepo setups). + * + * Exported for unit testing. + */ +export const AUTO_INLINE_DEPS: ReadonlyArray = [ + '@testing-library/jest-dom', + '@storybook/test', + 'jest-extended', +]; + +/** + * Compute the merged `test.server.deps.inline` list for a given project root, + * appending only those entries from [[AUTO_INLINE_DEPS]] that are actually + * installed in the project. + * + * Returns `null` when nothing needs to change (either `inline: true` or an + * empty result), so the caller can skip the mutation step. + * + * Exported for unit testing. The `_createRequire` parameter lets tests inject + * a controlled resolver without needing to spy on Node's ESM module namespace. + */ +export function computeAutoInlineList( + existingInline: (string | RegExp)[] | true | undefined, + projectRoot: string, + _createRequire: (from: string) => { resolve: (id: string) => string } = createRequire, +): (string | RegExp)[] | null { + // User opted into "inline everything" — don't touch. + if (existingInline === true) { + return null; + } + // Build a require resolver anchored at the project root so we only + // inline packages that are actually installed there. + const projectRequire = _createRequire(`${projectRoot}/package.json`); + // Start from a copy of the user-supplied array (or a fresh array when + // none was provided) so the originating user-config object is not mutated. + const merged: (string | RegExp)[] = Array.isArray(existingInline) ? [...existingInline] : []; + for (const pkg of AUTO_INLINE_DEPS) { + // Skip if already covered by a string or regexp entry. + if (merged.some((entry) => entry === pkg || (entry instanceof RegExp && entry.test(pkg)))) { + continue; + } + try { + projectRequire.resolve(pkg); + } catch { + // Package not installed in the project — skip silently. + continue; + } + merged.push(pkg); + } + // Return null when there's nothing new to inline so callers can bail early. + const hadEntries = Array.isArray(existingInline) ? existingInline.length : 0; + if (merged.length === hadEntries) { + return null; + } + return merged; +} + +function vitePlusAutoInlineMatcherPlugin(): PluginOption { + return { + name: 'vite-plus:auto-inline-matcher-deps', + enforce: 'pre', + configResolved(resolvedConfig) { + // Access the vitest test config via the augmented field. Vitest augments + // vite's `UserConfig` but not `ResolvedConfig`, so we use `any` here. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testConfig = (resolvedConfig as any).test as + | { server?: { deps?: { inline?: (string | RegExp)[] | true } } } + | undefined; + const merged = computeAutoInlineList(testConfig?.server?.deps?.inline, resolvedConfig.root); + if (merged === null) { + return; + } + // Mutate the resolved config so the finalised inline list is visible + // to vitest when it reads test.server.deps.inline. + if (!testConfig) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (resolvedConfig as any).test = { server: { deps: { inline: merged } } }; + } else { + if (!testConfig.server) { + testConfig.server = {}; + } + if (!testConfig.server.deps) { + testConfig.server.deps = {}; + } + testConfig.server.deps.inline = merged; + } + }, + }; +} + +/** + * Inject the rewrite plugin, the vitest resolver plugin, and the auto-inline + * matcher plugin into a single inline project config. Used both for root + * configs and for object-shaped entries inside `test.projects`. + * + * The shapes overlap (both have an optional top-level `plugins` array and + * an optional `test.server.deps.inline`), so a shared helper keeps the + * wiring consistent. + */ +function injectPluginIntoInlineConfig< + T extends { + plugins?: UserConfig['plugins']; + test?: { server?: { deps?: { inline?: unknown } } }; + }, +>(config: T): T { + return { + ...config, + plugins: [ + vitePlusTestSpecifierRewritePlugin(), + vitePlusVitestResolverPlugin(), + vitePlusAutoInlineMatcherPlugin(), + ...(config.plugins ?? []), + ], + } as T; +} + +/** + * Walk `config.test?.projects` and inject the rewrite plugin into each + * project entry. Vitest spins up an independent Vite pipeline per project, so + * root-level plugins do NOT propagate — without this, files matched by a + * project's `include` glob never get the `vite-plus/test` → `vitest` rewrite. + * + * Entry shapes (from `TestProjectConfiguration`): + * - string (glob path like `'./packages/*'`) → passed through unchanged. + * - object (inline config with `test: {...}`) → clone and prepend plugin. + * - function (sync or async) → wrap so its result is injected. + * - Promise (resolves to inline config) → chain `.then(injectPlugin)`. + */ +function injectPluginIntoProject(project: TestProjectConfiguration): TestProjectConfiguration { + if (typeof project === 'string') { + return project; + } + if (typeof project === 'function') { + const wrapped: UserProjectConfigFn = (env: ConfigEnv) => { + const result = project(env); + if (result instanceof Promise) { + return result.then(injectPluginIntoInlineConfig); + } + return injectPluginIntoInlineConfig(result); + }; + return wrapped; + } + if (project instanceof Promise) { + return project.then(injectPluginIntoInlineConfig); + } + if (typeof project === 'object' && project !== null) { + return injectPluginIntoInlineConfig(project); + } + return project; +} + +function injectPlugin(config: UserConfig): UserConfig { + const injected = injectPluginIntoInlineConfig(config); + const projects = injected.test?.projects; + if (!projects || projects.length === 0) { + return injected; + } + return { + ...injected, + test: { + ...injected.test, + projects: projects.map(injectPluginIntoProject), + }, + }; +} + +function injectPluginIntoConfig(config: ViteUserConfigExport): ViteUserConfigExport { + if (typeof config === 'function') { + return (env: ConfigEnv) => { + const result = config(env); + if (result instanceof Promise) { + return result.then(injectPlugin); + } + return injectPlugin(result); + }; + } + if (config instanceof Promise) { + return config.then(injectPlugin); + } + return injectPlugin(config); +} + export function defineConfig(config: UserConfig): UserConfig; export function defineConfig(config: Promise): Promise; export function defineConfig(config: ViteUserConfigFnObject): ViteUserConfigFnObject; @@ -59,7 +677,7 @@ export function defineConfig(config: ViteUserConfigFnPromise): ViteUserConfigFnP export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport; export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport { - return viteDefineConfig(config); + return viteDefineConfig(injectPluginIntoConfig(config)); } const VITE_COMMANDS = new Set(['dev', 'build', 'test', 'preview']); diff --git a/packages/cli/src/index.cts b/packages/cli/src/index.cts index 02f4874efb..44491df8ec 100644 --- a/packages/cli/src/index.cts +++ b/packages/cli/src/index.cts @@ -1,12 +1,24 @@ const vite = require('@voidzero-dev/vite-plus-core'); -const vitest = require('@voidzero-dev/vite-plus-test/config'); +const { + configDefaults, + coverageConfigDefaults, + defaultBrowserPort, + defaultExclude, + defaultInclude, + defineProject, +} = require('vitest/config'); const { defineConfig, lazyPlugins } = require('./define-config'); module.exports = { ...vite, - ...vitest, + configDefaults, + coverageConfigDefaults, + defaultBrowserPort, + defaultExclude, + defaultInclude, + defineProject, defineConfig, lazyPlugins, }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1d9d932662..ae6440f7c9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,6 +2,29 @@ import { defineConfig, lazyPlugins } from './define-config.ts'; export * from '@voidzero-dev/vite-plus-core'; -export * from '@voidzero-dev/vite-plus-test/config'; +export { + configDefaults, + coverageConfigDefaults, + defaultBrowserPort, + defaultExclude, + defaultInclude, + defineProject, +} from 'vitest/config'; + +export type { + TestProjectConfiguration, + TestProjectInlineConfiguration, + TestTagDefinition, + TestUserConfig, + UserProjectConfigExport, + UserProjectConfigFn, + UserWorkspaceConfig, + ViteUserConfig, + ViteUserConfigExport, + ViteUserConfigFn, + ViteUserConfigFnObject, + ViteUserConfigFnPromise, + WatcherTriggerPattern, +} from 'vitest/config'; export { defineConfig, lazyPlugins }; diff --git a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts index e4050a529b..5e7cc3a75d 100644 --- a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts +++ b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts @@ -14,9 +14,7 @@ vi.mock('../../utils/constants.js', async (importOriginal) => { VITE_PLUS_VERSION: 'file:/tmp/tgz/vite-plus-0.0.0.tgz', VITE_PLUS_OVERRIDE_PACKAGES: { vite: 'file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz', - vitest: 'file:/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz', '@voidzero-dev/vite-plus-core': 'file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz', - '@voidzero-dev/vite-plus-test': 'file:/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz', }, }; }); @@ -78,13 +76,11 @@ describe('rewriteMonorepo bun catalog with file: protocol', () => { // overrides should use file: paths directly, not catalog: const overrides = pkg.overrides as Record; expect(overrides.vite).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz'); - expect(overrides.vitest).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz'); + expect(overrides.vitest).toBeUndefined(); expect(overrides['@voidzero-dev/vite-plus-core']).toBe( 'file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz', ); - expect(overrides['@voidzero-dev/vite-plus-test']).toBe( - 'file:/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz', - ); + expect(overrides['@voidzero-dev/vite-plus-test']).toBeUndefined(); }); it('does not write file: paths into named catalogs', () => { @@ -127,6 +123,64 @@ describe('rewriteMonorepo bun catalog with file: protocol', () => { expect(pkg.devDependencies.vite).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz'); }); + it('writes bunfig.toml with `peer = false` so vitest peer-dep on vite does not break install', () => { + // vitest@4.1.7 declares peer vite^6/^7/^8. With overrides.vite pointing at + // file:vite-plus-core@0.0.0 (whose package.json version does not match), + // bun aborts the install. pnpm/yarn/npm tolerate this; bun has no equivalent + // to pnpm's peerDependencyRules and only respects the `[install] peer = false` + // setting in bunfig.toml. The migrator must emit that file or every bun + // user hits `error: vite@^6.0.0 || ^7.0.0 || ^8.0.0 failed to resolve`. + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: ['packages/*'], + packageManager: 'bun@1.3.11', + }), + ); + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const bunfigPath = path.join(tmpDir, 'bunfig.toml'); + expect(fs.existsSync(bunfigPath)).toBe(true); + expect(fs.readFileSync(bunfigPath, 'utf8')).toMatch(/^\[install\][\s\S]*peer\s*=\s*false/m); + }); + + it('preserves an existing bunfig.toml `peer` setting (does not overwrite user intent)', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: ['packages/*'], + packageManager: 'bun@1.3.11', + }), + ); + fs.writeFileSync(path.join(tmpDir, 'bunfig.toml'), '[install]\npeer = true\n'); + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + // User's explicit `peer = true` should remain — we don't silently flip it. + expect(fs.readFileSync(path.join(tmpDir, 'bunfig.toml'), 'utf8')).toMatch(/peer\s*=\s*true/); + }); + + it('appends `peer = false` under an existing [install] section without `peer` setting', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: ['packages/*'], + packageManager: 'bun@1.3.11', + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'bunfig.toml'), + '[install]\nregistry = "https://registry.npmjs.org/"\n', + ); + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const bunfig = fs.readFileSync(path.join(tmpDir, 'bunfig.toml'), 'utf8'); + expect(bunfig).toMatch(/registry\s*=\s*"https:\/\/registry\.npmjs\.org\/"/); + expect(bunfig).toMatch(/peer\s*=\s*false/); + }); + it('does not write file: paths into peer dependencies', () => { const pkg = { peerDependencies: { @@ -141,7 +195,7 @@ describe('rewriteMonorepo bun catalog with file: protocol', () => { rewritePackageJson(pkg, PackageManager.pnpm, true); expect(pkg.peerDependencies.vite).toBe('^7.0.0'); - expect(pkg.peerDependencies.vitest).toBe('*'); + expect(pkg.peerDependencies.vitest).toBe('catalog:test'); expect(pkg.optionalDependencies.vite).toBe( 'file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz', ); diff --git a/packages/cli/src/migration/__tests__/install-failure-guard.spec.ts b/packages/cli/src/migration/__tests__/install-failure-guard.spec.ts new file mode 100644 index 0000000000..6304c1f2fa --- /dev/null +++ b/packages/cli/src/migration/__tests__/install-failure-guard.spec.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const binPath = path.resolve(here, '..', 'bin.ts'); + +/** + * `runViteInstall` returns `{ status: 'failed', exitCode }` (it does not throw) + * when the install process exits non-zero. Earlier versions of `migration/bin.ts` + * only summed `installSummary.durationMs` and reported success regardless of + * `status`, which left the project's `node_modules` desynced from a freshly + * mutated `package.json` (network/auth failures, registry blips, pnpm lockfile + * conflicts after override resolution) while still printing + * "Dependencies installed" to the user. + * + * Two install sites need this handling: + * 1. The full migration path (`executeMigrationPlan`'s final reinstall, run + * AFTER manifest/source rewrites land). + * 2. The early-return path (`main` when `vite-plus` is already a dep but a + * stale-wrapper repair or ESLint/Prettier migration touches package.json). + * + * Both paths must funnel install results through `handleInstallResult` so that + * failures warn the user, append to `report.warnings`, and flip + * `process.exitCode`. This is a guard test — if a future refactor drops the + * helper or stops calling it from either path, this fails loudly so reviewers + * can re-evaluate the install-failure UX before it ships. + */ +describe('migration install failure handling', () => { + const binSource = fs.readFileSync(binPath, 'utf8'); + + describe('handleInstallResult helper', () => { + it('defines a `handleInstallResult` function in bin.ts', () => { + expect(binSource).toMatch(/function handleInstallResult\s*\(/); + }); + + it('branches on `installSummary.status === "installed"` and credits duration', () => { + expect(binSource).toMatch(/installSummary\.status === 'installed'/); + expect(binSource).toMatch(/return installSummary\.durationMs/); + }); + + it('branches on `installSummary.status === "failed"` and warns + flips exitCode', () => { + expect(binSource).toMatch(/installSummary\.status === 'failed'/); + expect(binSource).toMatch(/warnMsg\(/); + expect(binSource).toMatch(/report\.warnings\.push\(/); + expect(binSource).toMatch(/process\.exitCode\s*=/); + }); + }); + + describe('full migration path (executeMigrationPlan)', () => { + it('reconciles `finalInstallSummary` through `handleInstallResult`', () => { + // The full-path final reinstall must run through the helper so a failed + // install after manifest/source rewrites does not silently report a + // desynced project as a successful migration. + expect(binSource).toMatch(/finalInstallSummary[\s\S]{0,1500}handleInstallResult\(/); + }); + + it('does NOT add `finalInstallSummary.durationMs` directly to the return value', () => { + // The buggy round-7 version returned + // `initialInstallSummary.durationMs + finalInstallSummary.durationMs` + // unconditionally. The fix routes `finalInstallSummary` through the + // helper which returns 0 on failure. Catch any regression that re-adds + // the raw `.durationMs` access. + expect(binSource).not.toMatch(/finalInstallSummary\.durationMs/); + }); + }); + + describe('early-return path (main, hasVitePlusDependency branch)', () => { + it('reconciles `installSummary` through `handleInstallResult`', () => { + expect(binSource).toMatch( + /const installSummary = await runViteInstall\([\s\S]{0,1500}handleInstallResult\(/, + ); + }); + }); +}); diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index 9ad9bf0551..6638b6d033 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { parse as parseYaml } from 'yaml'; import { PackageManager } from '../../types/index.js'; +import { VITEST_VERSION } from '../../utils/constants.js'; import { createMigrationReport } from '../report.js'; // Mock VITE_PLUS_VERSION to a stable value for snapshot tests. @@ -172,6 +173,8 @@ describe('rewritePackageJson', () => { rewritePackageJson(pkg, PackageManager.yarn, true); expect(pkg.devDependencies.vite).toBe('catalog:'); + // vitest is a managed override key — non-catalog specs are rewritten to + // `catalog:` so the override is resolved through the catalog. expect(pkg.dependencies.vitest).toBe('catalog:'); expect((pkg.devDependencies as Record)['vite-plus']).toBe('catalog:'); }); @@ -191,7 +194,10 @@ describe('rewritePackageJson', () => { expect(pkg.devDependencies.vite).toBe('catalog:'); expect(pkg.optionalDependencies.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); - expect(pkg.optionalDependencies.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // vitest is now a managed override key — yarn optional deps receive the + // literal override version so the resolution doesn't depend on catalog + // lookup at the optionalDependency site. + expect(pkg.optionalDependencies.vitest).toBe('4.1.7'); expect((pkg.devDependencies as Record)['vite-plus']).toBe('catalog:'); }); @@ -327,6 +333,78 @@ describe('rewritePackageJson', () => { expect(pkg.dependencies).toHaveProperty('playwright', '^1.40.0'); expect(pkg.devDependencies).not.toHaveProperty('playwright'); }); + + it('adds a direct vitest devDependency when the package uses browser mode', async () => { + // A package that drives vitest browser mode but has no direct vitest dep + // (e.g. it only imports `vite-plus/test/browser-playwright`). `@vitest/browser` + // needs `vitest` resolvable from the package root, so the migration must + // pin it as a direct devDependency. + const pkg = { + devDependencies: { + playwright: '^1.58.0', + }, + }; + rewritePackageJson(pkg, PackageManager.pnpm, true, undefined, undefined, true); + expect(pkg.devDependencies).toHaveProperty('vitest', 'catalog:'); + expect(pkg.devDependencies).toHaveProperty('vite-plus', 'catalog:'); + }); + + it('uses a concrete vitest version for browser mode in non-catalog package managers', async () => { + const pkg = { + devDependencies: { + playwright: '^1.58.0', + }, + }; + rewritePackageJson(pkg, PackageManager.npm, false, undefined, undefined, true); + expect((pkg as { devDependencies?: Record }).devDependencies?.vitest).toBe( + VITEST_VERSION, + ); + }); + + it('does not overwrite an existing direct vitest dep in browser mode', async () => { + const pkg = { + devDependencies: { + vitest: '^4.0.0', + }, + }; + rewritePackageJson(pkg, PackageManager.pnpm, true, undefined, undefined, true); + // existing direct dep is normalized through the override path, not replaced + expect(pkg.devDependencies.vitest).toBe('catalog:'); + }); + + it('does not add vitest when browser mode is not detected', async () => { + const pkg = { + devDependencies: { + vite: '^7.0.0', + }, + }; + rewritePackageJson(pkg, PackageManager.pnpm, true, undefined, undefined, false); + expect(pkg.devDependencies).not.toHaveProperty('vitest'); + }); + + it('adds a direct vitest dep when a browser provider is declared but not source-imported', async () => { + // Config-only browser mode: vitest is enabled via `vite.config.ts` + // (e.g. `test.browser.provider: 'playwright'`) and the provider package is + // declared in devDependencies, but no source file `import`s it. The + // source-scan signal (`usesVitestBrowserMode`) is therefore false; the + // dep declaration in the original package.json must still drive the + // direct-`vitest` injection so the browser optimizer can resolve `vitest` + // from the package root under pnpm strict / Yarn PnP. + const pkg = { + devDependencies: { + '@vitest/browser': '^4.1.7', + '@vitest/browser-playwright': '^4.1.7', + }, + }; + rewritePackageJson(pkg, PackageManager.pnpm, true, undefined, undefined, false); + expect(pkg.devDependencies).toHaveProperty('vitest', 'catalog:'); + expect(pkg.devDependencies).toHaveProperty('vite-plus', 'catalog:'); + // The browser packages themselves are still stripped. + expect(pkg.devDependencies).not.toHaveProperty('@vitest/browser'); + expect(pkg.devDependencies).not.toHaveProperty('@vitest/browser-playwright'); + // The provider's runtime peer dep is preserved. + expect(pkg.devDependencies).toHaveProperty('playwright', '*'); + }); }); describe('rewriteEslintPackageJson', () => { @@ -936,7 +1014,9 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { const overrides = pnpm.overrides as Record; expect(overrides['some-pkg']).toBe('1.0.0'); expect(overrides.vite).toBeDefined(); - expect(overrides.vitest).toBeDefined(); + // vitest is pinned via overrides so downstream projects resolve a single + // vitest copy (the one vp-cli ships). + expect(overrides.vitest).toBe('4.1.7'); // peerDependencyRules should be present expect(pnpm.peerDependencyRules).toBeDefined(); @@ -966,8 +1046,9 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { const pkg = readJson(path.join(tmpDir, 'package.json')); const pnpm = pkg.pnpm as Record; const rules = pnpm.peerDependencyRules as Record; - // Custom entries preserved, Vite entries merged - expect(rules.allowAny).toEqual(expect.arrayContaining(['react', 'vite', 'vitest'])); + // Custom entries preserved, Vite entries merged (vitest is no longer + // injected as it's not a managed override key anymore). + expect(rules.allowAny).toEqual(expect.arrayContaining(['react', 'vite'])); // ignoreMissing preserved expect(rules.ignoreMissing).toEqual(['@types/node']); }); @@ -981,6 +1062,8 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { const yaml = readYaml(path.join(tmpDir, 'pnpm-workspace.yaml')); expect(yaml).toContain("vite: 'catalog:'"); + // vitest is now a managed override key — it resolves through the catalog + // like vite does. expect(yaml).toContain("vitest: 'catalog:'"); }); @@ -1024,12 +1107,16 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { catalogs: Record>; }; expect(yaml.overrides.vite).toBe('catalog:vite7'); + // vitest is now a managed override key — it is added to overrides as a + // `catalog:` reference, and its catalog entry is rewritten to the pinned + // vitest version vp-cli ships. expect(yaml.overrides.vitest).toBe('catalog:'); - expect(yaml.catalog.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(yaml.catalog.vitest).toBe('4.1.7'); expect(yaml.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(yaml.catalogs.vite7.react).toBe('^18.0.0'); expect(yaml.catalogs.vite7['vite-plus']).toBe('latest'); - expect(yaml.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // Named catalog vitest entries are also pinned to the managed override version. + expect(yaml.catalogs.test.vitest).toBe('4.1.7'); expect(yaml.catalogs.test.tsdown).toBeUndefined(); expect(yaml.catalogs.test['vite-plus']).toBeUndefined(); @@ -1040,10 +1127,117 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { expect(pkg.devDependencies.vite).toBe('catalog:vite7'); expect(pkg.devDependencies['vite-plus']).toBe('catalog:'); expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + // vitest peer `catalog:` is resolved against the pre-rewrite catalog + // (which still holds the user's `^4.0.0`); only the catalog file itself + // is later rewritten to the pinned vp-cli version. The peer range stays + // as the user wrote it. expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); expect(pkg.peerDependencies).not.toHaveProperty('tsdown'); }); + it('adds a direct vitest dep when a vite config enables browser mode', () => { + // A package whose vite config imports a browser provider but has no direct + // vitest dep — `@vitest/browser` needs `vitest` resolvable from the package + // root, so the migration must pin it. Mirrors the vibe-dashboard regression. + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { playwright: '^1.58.0' } }), + ); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + [ + "import { playwright } from 'vite-plus/test/browser-playwright';", + "import { defineConfig } from 'vite-plus';", + 'export default defineConfig({', + ' test: { browser: { enabled: true, provider: playwright() } },', + '});', + '', + ].join('\n'), + ); + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const pkg = readJson(path.join(tmpDir, 'package.json')); + const devDeps = pkg.devDependencies as Record; + expect(devDeps.vitest).toBe('catalog:'); + expect(devDeps['vite-plus']).toBe('catalog:'); + }); + + it('detects browser mode from a test file when the config has no hint', () => { + // Browser config can live in a workspace-referenced config under any name; + // the source scan also catches `@vitest/browser*` imports in test files. + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { vite: '^7.0.0' } }), + ); + fs.mkdirSync(path.join(tmpDir, 'src', '__tests__'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, 'src', '__tests__', 'app.test.ts'), + "import { page } from '@vitest/browser/context';\n", + ); + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const devDeps = readJson(path.join(tmpDir, 'package.json')).devDependencies as Record< + string, + string + >; + expect(devDeps.vitest).toBe('catalog:'); + }); + + it('does not add vitest for a package without browser mode', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { vite: '^7.0.0' } }), + ); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + "import { defineConfig } from 'vite';\nexport default defineConfig({});\n", + ); + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const devDeps = readJson(path.join(tmpDir, 'package.json')).devDependencies as Record< + string, + string + >; + expect(devDeps).not.toHaveProperty('vitest'); + }); + + it('detects browser mode from a declared provider dep with no source imports', () => { + // Config-only browser mode: `vite.config.ts` enables the browser runner by + // provider name (resolved by vitest at runtime) and the user lists + // `@vitest/browser-playwright` in devDependencies — but no source or config + // file imports `@vitest/browser*`. The source-scan signal is therefore + // false; the dep declaration is what tells us the package drives browser + // mode. After migration, `vitest` must still be pinned as a direct dep so + // the browser optimizer can resolve it under pnpm strict / Yarn PnP. + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'test', + devDependencies: { '@vitest/browser-playwright': '^4.1.7' }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + [ + "import { defineConfig } from 'vite';", + "export default defineConfig({ test: { browser: { provider: 'playwright' } } });", + '', + ].join('\n'), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const devDeps = readJson(path.join(tmpDir, 'package.json')).devDependencies as Record< + string, + string + >; + expect(devDeps.vitest).toBe('catalog:'); + expect(devDeps['vite-plus']).toBe('catalog:'); + expect(devDeps).not.toHaveProperty('@vitest/browser-playwright'); + // Provider's runtime peer dep is preserved. + expect(devDeps.playwright).toBe('*'); + }); + it('preserves named pnpm overrides when moving root overrides to pnpm-workspace.yaml', () => { fs.writeFileSync( path.join(tmpDir, 'package.json'), @@ -1071,6 +1265,7 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { catalogs: Record>; }; expect(yaml.overrides.vite).toBe('catalog:vite7'); + // vitest is now injected into overrides as a managed override key. expect(yaml.overrides.vitest).toBe('catalog:'); expect(yaml.overrides.react).toBe('^18.0.0'); expect(yaml.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); @@ -1115,6 +1310,7 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { overrides: Record; }; expect(yaml.overrides.vite).toBe('catalog:'); + // vitest is now a managed override key — added to overrides as catalog: ref. expect(yaml.overrides.vitest).toBe('catalog:'); }); @@ -1147,8 +1343,55 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { peerDependencies: Record; }; expect(pkg.peerDependencies.vite).toBe('*'); + // vitest is now a managed override key — peer dep catalog refs that + // resolve to the override target are coerced to '*'. expect(pkg.peerDependencies.vitest).toBe('*'); }); + + it('adds vitest only to the monorepo package that uses browser mode', () => { + // Root has no browser config; only `apps/dashboard` does. The browser-mode + // scan must stop at the nested package.json boundary so the root package + // does not inherit the sub-package's signal. + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'root', devDependencies: {} }), + ); + fs.writeFileSync(path.join(tmpDir, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n'); + const appDir = path.join(tmpDir, 'apps', 'dashboard'); + fs.mkdirSync(appDir, { recursive: true }); + fs.writeFileSync( + path.join(appDir, 'package.json'), + JSON.stringify({ name: '@vibe/dashboard', devDependencies: { playwright: '^1.58.0' } }), + ); + fs.writeFileSync( + path.join(appDir, 'vite.config.ts'), + [ + "import { playwright } from 'vite-plus/test/browser-playwright';", + "import { defineConfig } from 'vite-plus';", + 'export default defineConfig({ test: { browser: { provider: playwright() } } });', + '', + ].join('\n'), + ); + + const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.pnpm); + workspaceInfo.isMonorepo = true; + workspaceInfo.packages = [ + { name: '@vibe/dashboard', path: 'apps/dashboard', isTemplatePackage: false }, + ]; + rewriteMonorepo(workspaceInfo, true); + + const rootDeps = (readJson(path.join(tmpDir, 'package.json')).devDependencies ?? {}) as Record< + string, + string + >; + expect(rootDeps).not.toHaveProperty('vitest'); + + const appDeps = readJson(path.join(appDir, 'package.json')).devDependencies as Record< + string, + string + >; + expect(appDeps.vitest).toBe('catalog:'); + }); }); describe('rewriteMonorepo yarn catalog', () => { @@ -1196,7 +1439,9 @@ describe('rewriteMonorepo yarn catalog', () => { expect(yarnrc.nodeLinker).toBe('node-modules'); expect(yarnrc.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(yarnrc.catalogs.vite7.react).toBe('^18.0.0'); - expect(yarnrc.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // vitest is now a managed override key — existing catalog entries are + // rewritten to the pinned vp-cli vitest version. + expect(yarnrc.catalogs.test.vitest).toBe('4.1.7'); expect(yarnrc.catalogs.test.oxlint).toBeUndefined(); const pkg = readJson(path.join(tmpDir, 'package.json')) as { @@ -1205,6 +1450,9 @@ describe('rewriteMonorepo yarn catalog', () => { }; expect(pkg.devDependencies.vite).toBe('catalog:vite7'); expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + // vitest peer `catalog:test` is resolved against the pre-rewrite catalog + // (which still holds the user's `^4.0.0`). The peer range stays as the + // user wrote it; only the catalog file itself is later rewritten. expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); }); }); @@ -1298,7 +1546,9 @@ describe('rewriteMonorepo bun catalog', () => { expect(pkg.workspaces.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(pkg.workspaces.catalog['vite-plus']).toBe('latest'); expect(pkg.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); - expect(pkg.catalog.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // vitest is now a managed override key — pre-existing catalog entries are + // rewritten to the pinned vp-cli vitest version. + expect(pkg.catalog.vitest).toBe('4.1.7'); expect(pkg.catalog.tsdown).toBeUndefined(); expect(pkg.catalog.react).toBe('^19.0.0'); expect(pkg.catalog['vite-plus']).toBeUndefined(); @@ -1358,11 +1608,16 @@ describe('rewriteMonorepo bun catalog', () => { expect(pkg.catalogs.build.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(pkg.catalogs.build.react).toBe('^19.0.0'); expect(pkg.catalogs.build.tsdown).toBeUndefined(); - expect(pkg.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // vitest is now a managed override key — existing catalog entries are + // rewritten to the pinned version and `overrides.vitest` is injected + // as a `catalog:` ref so bun resolves it through the catalog. + expect(pkg.catalogs.test.vitest).toBe('4.1.7'); expect(pkg.overrides.vite).toBe('catalog:build'); expect(pkg.overrides.vitest).toBe('catalog:'); expect(pkg.devDependencies.vite).toBe('catalog:build'); expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + // vitest peer `catalog:test` is resolved against the pre-rewrite catalog + // (which still holds the user's `^4.0.0`). Peer range stays as-is. expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); }); @@ -1398,7 +1653,9 @@ describe('rewriteMonorepo bun catalog', () => { expect(pkg.workspaces.catalog['vite-plus']).toBe('latest'); expect(pkg.workspaces.catalogs.build.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(pkg.workspaces.catalogs.build.oxlint).toBeUndefined(); - expect(pkg.workspaces.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + // vitest is a managed override key — existing catalog entries are + // rewritten to the pinned vp-cli vitest version. + expect(pkg.workspaces.catalogs.test.vitest).toBe('4.1.7'); expect(pkg.workspaces.catalogs.test.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); expect(pkg.overrides.vite).toBe('catalog:'); }); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 06d9ce7f08..8ac8bc71a1 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -29,6 +29,7 @@ import { hasVitePlusDependency, readNearestPackageJson } from '../utils/package. import { displayRelative } from '../utils/path.ts'; import { cancelAndExit, + type CommandRunSummary, defaultInteractive, downloadPackageManager, promptGitHooks, @@ -36,7 +37,7 @@ import { selectPackageManager, upgradeYarn, } from '../utils/prompts.ts'; -import { accent, log, muted, printHeader } from '../utils/terminal.ts'; +import { accent, log, muted, printHeader, warnMsg } from '../utils/terminal.ts'; import { confirmBaseUrlFix, fixBaseUrlInTsconfig, @@ -547,6 +548,43 @@ function formatDuration(durationMs: number) { return `${Math.round(durationSeconds)}s`; } +/** + * Reconcile a CommandRunSummary from `runViteInstall` with the migration's + * duration counter and exit-code state. `runViteInstall` returns + * `{ status: 'failed', exitCode }` without throwing; treating that as a success + * (incrementing duration unconditionally) would let the migration claim + * "Dependencies installed" while node_modules is desynced from the just-mutated + * package.json. This helper centralizes the right handling: credit duration on + * success, warn + flip exitCode on failure, stay silent on skip. + */ +function handleInstallResult( + installSummary: CommandRunSummary, + rootDir: string, + report: MigrationReport, + // The pre-migration "initial" install is best-effort: the migration proceeds + // regardless of its outcome, and a post-migration "final" install runs with + // `--force` / `--no-frozen-lockfile` as the authoritative recovery. Only that + // final install's failure should flip `process.exitCode` so a successful + // recovery yields exit 0; the initial failure is still surfaced via + // `report.warnings` + the warn message. + options?: { propagateExitCode?: boolean }, +): number { + if (installSummary.status === 'installed') { + return installSummary.durationMs; + } + if (installSummary.status === 'failed') { + const exitCode = installSummary.exitCode ?? 1; + const message = `Dependency installation failed (exit code ${exitCode}). Run \`vp install\` manually in ${rootDir} to resync node_modules.`; + warnMsg(message); + report.warnings.push(message); + if (options?.propagateExitCode !== false) { + process.exitCode = exitCode; + } + return 0; + } + return 0; +} + function showMigrationSummary(options: { projectRoot: string; packageManager: string; @@ -859,12 +897,14 @@ async function executeMigrationPlan( } // 12. Reinstall after migration - // npm needs --force to re-resolve packages with newly added overrides, - // otherwise the stale lockfile prevents override resolution. + // The migration intentionally rewrites overrides/catalogs/deps, so the + // existing lockfile is guaranteed to be stale. Tell each package manager to + // re-resolve instead of refusing the install (pnpm/yarn default to + // frozen-lockfile under CI, npm/bun need an explicit --force). const installArgs = plan.packageManager === PackageManager.npm || plan.packageManager === PackageManager.bun ? ['--force'] - : undefined; + : ['--no-frozen-lockfile']; updateMigrationProgress('Installing dependencies'); const finalInstallSummary = await runViteInstall( workspaceInfo.rootDir, @@ -878,8 +918,23 @@ async function executeMigrationPlan( ); clearMigrationProgress(); + // Process the initial install first so the final install's exit code "wins": + // if the initial install failed but the final install succeeded, the + // migration should still report success (exit 0). The initial call opts out + // of exitCode propagation; only the final call may flip process.exitCode. + const initialInstallDurationMs = handleInstallResult( + initialInstallSummary, + workspaceInfo.rootDir, + report, + { propagateExitCode: false }, + ); + const finalInstallDurationMs = handleInstallResult( + finalInstallSummary, + workspaceInfo.rootDir, + report, + ); return { - installDurationMs: initialInstallSummary.durationMs + finalInstallSummary.durationMs, + installDurationMs: initialInstallDurationMs + finalInstallDurationMs, packageManagerVersion: downloadResult.version, report, }; @@ -1004,17 +1059,28 @@ async function main() { ); resolvedVersion = resolved.version; } + // ESLint/Prettier migration touched package.json; the lockfile is now + // stale, so disable frozen-lockfile mode (pnpm/yarn) or --force (npm/bun). + const eslintPrettierInstallArgs = + workspaceInfoOptional.packageManager === PackageManager.npm || + workspaceInfoOptional.packageManager === PackageManager.bun + ? ['--force'] + : ['--no-frozen-lockfile']; const installSummary = await runViteInstall( workspaceInfoOptional.rootDir, options.interactive, - undefined, + eslintPrettierInstallArgs, { silent: true, packageManager: workspaceInfoOptional.packageManager, packageManagerVersion: resolvedVersion, }, ); - installDurationMs += installSummary.durationMs; + installDurationMs += handleInstallResult( + installSummary, + workspaceInfoOptional.rootDir, + report, + ); didMigrate = true; report.eslintMigrated = eslintMigrated; report.prettierMigrated = prettierMigrated; diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 7852153a5b..6ebb9e6059 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -29,6 +29,7 @@ import { VITE_PLUS_NAME, VITE_PLUS_OVERRIDE_PACKAGES, VITE_PLUS_VERSION, + VITEST_VERSION, isForceOverrideMode, } from '../utils/constants.ts'; import { editJsonFile, isJsonFile, readJsonFile } from '../utils/json.ts'; @@ -94,6 +95,18 @@ const BROWSER_PROVIDER_PEER_DEPS: Record = { '@vitest/browser-webdriverio': 'webdriverio', }; +// Browser-provider package names that, when present in the user's deps +// before migration, signal vitest browser mode even if no source file +// imports them. This covers config-only browser-mode setups (e.g. +// `test.browser.provider: 'playwright'` in `vite.config.ts`) where the +// provider package is declared in `devDependencies` but never `import`ed. +const VITEST_BROWSER_DEP_NAMES = [ + '@vitest/browser', + '@vitest/browser-preview', + '@vitest/browser-playwright', + '@vitest/browser-webdriverio', +] as const; + const PUBLIC_PEER_DEPENDENCY_FALLBACKS: Record = { vite: '*', vitest: '*', @@ -122,6 +135,56 @@ const OXLINT_NATIVE_PLUGINS = new Set([ 'vue', ]); +// Legacy wrapper package names that may appear as the target of override +// aliases left over from earlier vite-plus migrations. `@voidzero-dev/vite-plus-test` +// was deleted; any catalog/override entry still pointing at it is stale. +const LEGACY_WRAPPER_PACKAGE_NAMES = ['@voidzero-dev/vite-plus-test'] as const; + +// Fallback specs used when normalizing a stale wrapper alias. Real user +// ranges (e.g. `vitest: ^3.0.0`) are preserved — only the wrapper alias is +// rewritten. For `vitest`, we substitute the vitest version vite-plus +// bundles so any `catalog:` reference the user still has resolves cleanly. +const LEGACY_WRAPPER_FALLBACK_VERSIONS: Record = { + vitest: VITEST_VERSION, +}; + +function isLegacyWrapperSpec(value: string | undefined): boolean { + if (!value) { + return false; + } + for (const name of LEGACY_WRAPPER_PACKAGE_NAMES) { + if (value === `npm:${name}` || value.startsWith(`npm:${name}@`)) { + return true; + } + } + return false; +} + +/** + * Rewrite or remove keys whose value points at a deleted vite-plus wrapper. + * When a fallback exists for the key (e.g. `vitest`), the value is replaced + * so existing `catalog:` references continue to resolve. Otherwise the key + * is dropped entirely. Returns true iff any entry was changed. + */ +function pruneLegacyWrapperAliases(record: Record | undefined): boolean { + if (!record) { + return false; + } + let mutated = false; + for (const key of Object.keys(record)) { + if (isLegacyWrapperSpec(record[key])) { + const fallback = LEGACY_WRAPPER_FALLBACK_VERSIONS[key]; + if (fallback !== undefined) { + record[key] = fallback; + } else { + delete record[key]; + } + mutated = true; + } + } + return mutated; +} + type PackageJsonDependencyField = | 'devDependencies' | 'dependencies' @@ -1040,6 +1103,11 @@ export function rewriteStandaloneProject( }; }; }>(packageJsonPath, (pkg) => { + // Strip stale `vite-plus-test` wrapper aliases before injecting new overrides + // so the deleted wrapper doesn't survive migration in any sink. + pruneLegacyWrapperAliases(pkg.resolutions); + pruneLegacyWrapperAliases(pkg.overrides); + pruneLegacyWrapperAliases(pkg.pnpm?.overrides); if (packageManager === PackageManager.yarn) { pkg.resolutions = { ...pkg.resolutions, @@ -1050,6 +1118,19 @@ export function rewriteStandaloneProject( ...pkg.overrides, ...VITE_PLUS_OVERRIDE_PACKAGES, }; + if (packageManager === PackageManager.bun) { + // Bun walks transitive peer-deps before resolving overrides; vitest + // 4.1.7 declares peer `vite ^6 || ^7 || ^8` and aborts with + // "vite@... failed to resolve" if `vite` isn't a direct dep somewhere + // in the tree, even when the override would redirect it. Mirror the + // override as a devDep so bun's resolver sees `vite` immediately; + // the override above still points it at vite-plus-core. + // See https://github.com/oven-sh/bun/issues/8406. + pkg.devDependencies = { + ...pkg.devDependencies, + vite: VITE_PLUS_OVERRIDE_PACKAGES.vite, + }; + } } else if (packageManager === PackageManager.pnpm) { // If package.json already has a "pnpm" field, keep using it; // otherwise use pnpm-workspace.yaml. @@ -1106,6 +1187,7 @@ export function rewriteStandaloneProject( usePnpmWorkspaceYaml, skipStagedMigration, catalogDependencyResolver, + usesVitestBrowserMode(projectPath), ); // ensure vite-plus is in devDependencies @@ -1139,6 +1221,8 @@ export function rewriteStandaloneProject( if (packageManager === PackageManager.yarn) { rewriteYarnrcYml(projectPath); + } else if (packageManager === PackageManager.bun) { + ensureBunfigPeerSuppression(projectPath); } // Merge extracted staged config into vite.config.ts, then remove lint-staged from package.json @@ -1278,6 +1362,7 @@ export function rewriteMonorepoProject( true, skipStagedMigration, catalogDependencyResolver, + usesVitestBrowserMode(projectPath), ); return pkg; }); @@ -1306,6 +1391,7 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { // overrides const overrides = doc.getIn(['overrides']); + pruneYamlMapLegacyWrapperAliases(overrides); for (const key of Object.keys(VITE_PLUS_OVERRIDE_PACKAGES)) { const currentVersion = getYamlMapScalarStringValue(overrides, key); const version = getCatalogDependencySpec( @@ -1633,6 +1719,29 @@ function getYamlMapScalarStringValue(map: unknown, key: string): string | undefi return undefined; } +function pruneYamlMapLegacyWrapperAliases(map: unknown): void { + if (!(map instanceof YAMLMap)) { + return; + } + const stale: Array<{ key: Scalar; fallback: string | undefined }> = []; + for (const item of map.items) { + const value = item.value instanceof Scalar ? item.value.value : undefined; + if (typeof value === 'string' && isLegacyWrapperSpec(value) && item.key instanceof Scalar) { + stale.push({ + key: item.key, + fallback: LEGACY_WRAPPER_FALLBACK_VERSIONS[item.key.value], + }); + } + } + for (const { key, fallback } of stale) { + if (fallback !== undefined) { + map.set(key, scalarString(fallback)); + } else { + map.delete(key); + } + } +} + function rewriteCatalog(doc: YamlDocument): void { for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { // ERR_PNPM_CATALOG_IN_OVERRIDES  Could not resolve a catalog in the overrides: The entry for 'vite' in catalog 'default' declares a dependency using the 'file' protocol @@ -1651,6 +1760,8 @@ function rewriteCatalog(doc: YamlDocument): void { doc.deleteIn(path); } } + // Drop any entry still pointing at the deleted `vite-plus-test` wrapper. + pruneYamlMapLegacyWrapperAliases(doc.getIn(['catalog'])); const catalogs = doc.getIn(['catalogs']); if (!(catalogs instanceof YAMLMap)) { @@ -1677,6 +1788,7 @@ function rewriteCatalog(doc: YamlDocument): void { doc.deleteIn(catalogPath); } } + pruneYamlMapLegacyWrapperAliases(item.value); } } @@ -1701,6 +1813,42 @@ function rewriteCatalogsObject(catalogs: Record>) } } +/** + * Bun rejects vitest@4.1.7's `vite^6/^7/^8` peer-dep when the user's project + * overrides `vite` to `@voidzero-dev/vite-plus-core` (whose package.json version + * does not match those ranges). pnpm/yarn/npm all tolerate this redirect; bun + * does not, and there is no `peerDependencyRules`-style escape hatch — only the + * `[install] peer = false` setting in `bunfig.toml`. + * + * `vite-plus`/`@voidzero-dev/vite-plus-core` already provide the vite surface + * the user needs, so disabling bun's auto-install of *missing* peers is safe in + * this configuration: any vitest peer that's not already pulled in transitively + * (jsdom, happy-dom, etc.) is marked optional upstream anyway. + * + * Writes/merges `bunfig.toml` at `projectPath` so the suppression applies on + * the migration's reinstall AND every subsequent `bun install` the user runs. + */ +function ensureBunfigPeerSuppression(projectPath: string): void { + const bunfigPath = path.join(projectPath, 'bunfig.toml'); + const block = '[install]\npeer = false\n'; + if (!fs.existsSync(bunfigPath)) { + fs.writeFileSync(bunfigPath, block); + return; + } + const existing = fs.readFileSync(bunfigPath, 'utf8'); + // Already configured? Leave the user's setting alone — they may have set + // `peer = true` deliberately for some other reason and we shouldn't override. + if (/^\s*peer\s*=\s*(true|false)\s*$/m.test(existing)) { + return; + } + // Append under existing [install] section, or add a new section. + const installSectionRe = /^\[install\][^[]*/m; + const next = installSectionRe.test(existing) + ? existing.replace(installSectionRe, (section) => `${section.trimEnd()}\npeer = false\n`) + : `${existing.trimEnd()}\n\n${block}`; + fs.writeFileSync(bunfigPath, next); +} + /** * Write catalog entries to root package.json for bun. * Bun stores catalogs in package.json under the `catalog` key, @@ -1730,27 +1878,37 @@ function rewriteBunCatalog(projectPath: string): void { }; rewriteCatalogObject(catalog, true); + pruneLegacyWrapperAliases(catalog); if (useWorkspacesCatalog) { workspacesObj.catalog = catalog; if (pkg.catalog) { rewriteCatalogObject(pkg.catalog, false); + pruneLegacyWrapperAliases(pkg.catalog); } } else { pkg.catalog = catalog; if (workspacesObj?.catalog) { rewriteCatalogObject(workspacesObj.catalog, false); + pruneLegacyWrapperAliases(workspacesObj.catalog); } } if (workspacesObj?.catalogs) { rewriteCatalogsObject(workspacesObj.catalogs); + for (const named of Object.values(workspacesObj.catalogs)) { + pruneLegacyWrapperAliases(named); + } } if (pkg.catalogs) { rewriteCatalogsObject(pkg.catalogs); + for (const named of Object.values(pkg.catalogs)) { + pruneLegacyWrapperAliases(named); + } } // bun overrides support catalog: references const overrides: Record = { ...pkg.overrides }; + pruneLegacyWrapperAliases(overrides); for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { overrides[key] = getCatalogDependencySpec(overrides[key], value, true); } @@ -1758,6 +1916,8 @@ function rewriteBunCatalog(projectPath: string): void { return pkg; }); + + ensureBunfigPeerSuppression(projectPath); } /** @@ -1795,6 +1955,11 @@ function rewriteRootWorkspacePackageJson( }; }; }>(packageJsonPath, (pkg) => { + // Strip stale `vite-plus-test` wrapper aliases before injecting new overrides + // so the deleted wrapper doesn't survive migration in any sink. + pruneLegacyWrapperAliases(pkg.resolutions); + pruneLegacyWrapperAliases(pkg.overrides); + pruneLegacyWrapperAliases(pkg.pnpm?.overrides); if (packageManager === PackageManager.yarn) { pkg.resolutions = { ...pkg.resolutions, @@ -1809,6 +1974,19 @@ function rewriteRootWorkspacePackageJson( }; } else if (packageManager === PackageManager.bun) { // bun overrides are handled in rewriteBunCatalog() with catalog: references + // Bun walks transitive peer-deps before resolving overrides; vitest 4.1.7 + // declares peer `vite ^6 || ^7 || ^8` and aborts unless `vite` is a direct + // dep at the workspace root. Mirror the override as a devDep; the override + // configured in rewriteBunCatalog still redirects it to vite-plus-core. + // See https://github.com/oven-sh/bun/issues/8406. + pkg.devDependencies = { + ...pkg.devDependencies, + vite: getCatalogDependencySpec( + pkg.devDependencies?.vite, + VITE_PLUS_OVERRIDE_PACKAGES.vite, + true, + ), + }; } else if (packageManager === PackageManager.pnpm) { const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES); if (isForceOverrideMode()) { @@ -1901,6 +2079,105 @@ function readPrepareRulesYaml(): string { return cachedPrepareRulesYaml; } +// Specifier fragments that signal vitest browser mode. `@vitest/browser` +// (upstream, pre-migration) and `vite-plus/test/browser` (post-migration, e.g. +// re-migrating an already-migrated project) both indicate the package drives +// vitest's browser runner. +const VITEST_BROWSER_SPECIFIER_HINTS = ['@vitest/browser', 'vite-plus/test/browser'] as const; + +// TypeScript/JavaScript source extensions scanned for browser-mode hints. +const VITEST_SCAN_EXTENSIONS = new Set([ + '.ts', + '.mts', + '.cts', + '.tsx', + '.js', + '.mjs', + '.cjs', + '.jsx', +]); + +// Directories never worth scanning for browser-mode hints — generated output, +// installed deps, VCS metadata. Skipped at every recursion level. +const VITEST_SCAN_SKIP_DIRS = new Set([ + 'node_modules', + 'dist', + 'build', + 'out', + 'coverage', + '.git', + '.next', + '.nuxt', + '.svelte-kit', + '.vite', + '.cache', +]); + +/** + * Detect whether a package uses vitest's browser mode. + * + * Upstream `@vitest/browser` injects `optimizeDeps.include` entries of the form + * `vitest > expect-type` (and `vitest > @vitest/snapshot > magic-string`, + * `vitest > @vitest/expect > chai`). Vite resolves the leading `vitest` segment + * from the Vite config root, so `vitest` MUST be resolvable as a package from + * the consuming package's directory. In a pnpm strict (non-hoisted) layout, + * `vitest` pulled in only transitively via `vite-plus` is NOT reachable from the + * package root — the optimizer then fails with `Failed to resolve dependency` + * and the browser test page hangs forever. + * + * When this returns true the migration adds `vitest` as a direct + * devDependency so it is hoisted next to the package and the optimizer chain + * resolves. The signal is any of the package's TS/JS files (config, workspace + * config under any name, or test file) referencing `@vitest/browser*` or + * `vite-plus/test/browser*`. The scan recurses through the package directory + * (skipping `node_modules`, build output, VCS metadata) so browser config in a + * non-standard filename or browser imports in test files are all caught. + * + * Recursion stops at nested `package.json` boundaries: a workspace sub-package + * is a separate package that the migration scans on its own pass, so the root + * package must not inherit a browser-mode signal from a sub-package. + */ +function usesVitestBrowserMode(projectPath: string): boolean { + const matchesBrowserHint = (content: string): boolean => + VITEST_BROWSER_SPECIFIER_HINTS.some((hint) => content.includes(hint)); + + const scanDir = (dir: string, isRoot: boolean): boolean => { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return false; + } + // A nested package.json marks a separate workspace package — it is migrated + // (and scanned) on its own pass, so don't let its files leak into this one. + if (!isRoot && entries.some((e) => e.isFile() && e.name === 'package.json')) { + return false; + } + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (VITEST_SCAN_SKIP_DIRS.has(entry.name)) { + continue; + } + if (scanDir(entryPath, false)) { + return true; + } + } else if (entry.isFile() && VITEST_SCAN_EXTENSIONS.has(path.extname(entry.name))) { + try { + if (matchesBrowserHint(fs.readFileSync(entryPath, 'utf8'))) { + return true; + } + } catch { + // Unreadable file — ignore and keep scanning. + } + } + } + return false; + }; + + return scanDir(projectPath, true); +} + export function rewritePackageJson( pkg: { scripts?: Record; @@ -1914,6 +2191,7 @@ export function rewritePackageJson( isMonorepo?: boolean, skipStagedMigration?: boolean, catalogDependencyResolver?: CatalogDependencyResolver, + vitestBrowserMode?: boolean, ): Record | null { if (pkg.scripts) { const updated = rewriteScripts( @@ -1944,6 +2222,14 @@ export function rewritePackageJson( { dependencyField: 'peerDependencies', dependencies: pkg.peerDependencies }, { dependencyField: 'optionalDependencies', dependencies: pkg.optionalDependencies }, ]; + // Scrub stale `npm:@voidzero-dev/vite-plus-test@...` aliases left over from + // earlier vite-plus migrations — the wrapper package no longer exists, so + // these entries would break `pnpm install`. Real user ranges are preserved. + for (const { dependencies } of dependencyGroups) { + if (pruneLegacyWrapperAliases(dependencies)) { + needVitePlus = true; + } + } for (const [key, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { for (const { dependencyField, dependencies } of dependencyGroups) { if (dependencies?.[key]) { @@ -1957,6 +2243,29 @@ export function rewritePackageJson( } } } + // Force-override mode (ecosystem CI / `vp create` E2E) must re-pin any + // pre-existing `vite-plus` range to the local tgz. Otherwise pnpm reads the + // published vite-plus metadata for transitive dep resolution (e.g. + // `@voidzero-dev/vite-plus-test`) even though the override replaces the + // vite-plus package itself, dragging the stale wrapper into node_modules. + if (isForceOverrideMode()) { + for (const { dependencies } of dependencyGroups) { + if (dependencies?.[VITE_PLUS_NAME]) { + dependencies[VITE_PLUS_NAME] = VITE_PLUS_VERSION; + needVitePlus = true; + } + } + } + // Capture browser-mode signal from the original deps BEFORE the removal loop + // strips them. A package can drive vitest browser mode purely through config + // (`test.browser.provider: 'playwright'` in `vite.config.ts`) without ever + // importing `@vitest/browser*` in source — the provider package is listed in + // devDependencies but vitest loads it by name. The source-scan signal + // (`usesVitestBrowserMode`) misses this case; the dep declaration is the + // authoritative intent signal. + const hasBrowserDepSignal = VITEST_BROWSER_DEP_NAMES.some((name) => + dependencyGroups.some(({ dependencies }) => dependencies?.[name] !== undefined), + ); // remove packages that are replaced with vite-plus for (const name of REMOVE_PACKAGES) { let wasRemoved = false; @@ -1983,6 +2292,36 @@ export function rewritePackageJson( pkg.devDependencies[peerDep] = '*'; } } + // Promote dep-derived signal to the same flag the source-scan feeds, so the + // downstream "add direct `vitest`" branch fires for config-only browser-mode + // setups too. + const effectiveBrowserMode = vitestBrowserMode || hasBrowserDepSignal; + // Trigger vite-plus install when a project has a vitest-adjacent package + // (e.g. `vitest-browser-svelte`) that declares vitest as a peer dep — even + // if the project has no vite/oxlint/tsdown dep to migrate. The peer dep is + // satisfied by the upstream vitest that vite-plus bundles as a direct dep. + // Note: peerDependencies count as "adjacent signal" but NOT as installed. + const installableNames = [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ...Object.keys(pkg.optionalDependencies ?? {}), + ]; + const adjacentSignals = [...installableNames, ...Object.keys(pkg.peerDependencies ?? {})]; + const isVitestAdjacent = + !installableNames.includes('vitest') && + adjacentSignals.some((name) => name !== 'vitest' && name.includes('vitest')); + if (isVitestAdjacent) { + needVitePlus = true; + } + // A package running vitest browser mode must have `vitest` directly + // resolvable from its own directory — `@vitest/browser` injects + // `optimizeDeps.include` entries (`vitest > expect-type`, etc.) that Vite + // resolves from the config root. A `vitest` pulled in only transitively via + // `vite-plus` is invisible in a pnpm strict layout, so the optimizer fails + // and the browser test page hangs. See `usesVitestBrowserMode`. + if (effectiveBrowserMode && !installableNames.includes('vitest')) { + needVitePlus = true; + } if (needVitePlus) { // add vite-plus to devDependencies const version = @@ -1991,21 +2330,36 @@ export function rewritePackageJson( ...pkg.devDependencies, [VITE_PLUS_NAME]: version, }; - // Add vitest to devDependencies when a remaining dependency likely peer-depends - // on vitest (e.g., vitest-browser-svelte). Without this, pnpm resolves the real - // vitest for peer deps instead of @voidzero-dev/vite-plus-test, causing - // third-party type augmentations to target the wrong module. + // Add vitest to devDependencies when a remaining dependency likely + // peer-depends on vitest (e.g., vitest-browser-svelte). Vite-plus already + // bundles upstream vitest 4.1.7 as a direct dep, so the runtime resolution + // works without this — but strict pnpm / yarn Plug'n'Play refuse to expose a + // transitive `vitest` to satisfy a peer dep declared by a different + // direct dep. Adding `vitest` to the user's devDependencies pins the + // peer-dep target to the same upstream version vite-plus ships with. const installableDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies, }; + // Add `vitest` as a direct devDependency when: + // - a remaining dependency likely peer-depends on vitest (e.g. + // vitest-browser-svelte), OR + // - the package runs vitest browser mode (`@vitest/browser` needs + // `vitest` resolvable from the package root — see usesVitestBrowserMode). + // Vite-plus already bundles upstream vitest 4.1.7 as a direct dep, but a + // strict pnpm / yarn PnP layout will not expose that transitive `vitest` + // to the package. Pinning it here points the dep at the same upstream + // version vite-plus ships with. if ( !installableDeps.vitest && - Object.keys(installableDeps).some((name) => name.includes('vitest')) + (effectiveBrowserMode || Object.keys(installableDeps).some((name) => name.includes('vitest'))) ) { - const ver = VITE_PLUS_OVERRIDE_PACKAGES.vitest; - pkg.devDependencies.vitest = getCatalogDependencySpec(undefined, ver, supportCatalog); + pkg.devDependencies.vitest = getCatalogDependencySpec( + undefined, + VITEST_VERSION, + supportCatalog, + ); } } return extractedStagedConfig; diff --git a/packages/cli/src/oxlint-plugin.ts b/packages/cli/src/oxlint-plugin.ts index f71e1f94db..708113bfc3 100644 --- a/packages/cli/src/oxlint-plugin.ts +++ b/packages/cli/src/oxlint-plugin.ts @@ -6,6 +6,22 @@ import { VITE_PLUS_OXLINT_PLUGIN_NAME, } from './oxlint-plugin-config.ts'; +// `declare module 'vitest…'` and `declare module '@vitest/browser…'` are +// intentionally preserved by `vp migrate` (see migration's import_rewriter and +// docs/guide/migrate.md) — `vite-plus/test*` is a thin re-export of upstream +// `vitest*`, so type augmentations have to target the upstream module identity +// to merge correctly. Autofixing those module declarations here would split the +// augmentation away from what imports actually resolve through. +function isVitestFamilyDeclareModuleSpecifier(specifier: string): boolean { + return ( + specifier === 'vitest' || + specifier.startsWith('vitest/') || + specifier === '@vitest/browser' || + specifier.startsWith('@vitest/browser/') || + specifier.startsWith('@vitest/browser-') + ); +} + function rewriteVitePlusImportSpecifier(specifier: string): string | null { if (specifier === 'vite') { return 'vite-plus'; @@ -31,10 +47,15 @@ function rewriteVitePlusImportSpecifier(specifier: string): string | null { return 'vite-plus/test/browser'; } + // `@vitest/browser/context` keeps the nested path (vite-plus exports + // `./test/browser/context`); the remaining subpaths are exposed only at the + // bare `./test/` surface, so the `/browser/` segment is dropped. const browserSubpathRewrites: Record = { '@vitest/browser/context': 'vite-plus/test/browser/context', '@vitest/browser/client': 'vite-plus/test/client', '@vitest/browser/locators': 'vite-plus/test/locators', + '@vitest/browser/matchers': 'vite-plus/test/matchers', + '@vitest/browser/utils': 'vite-plus/test/utils', }; if (specifier in browserSubpathRewrites) { return browserSubpathRewrites[specifier]; @@ -129,7 +150,15 @@ export const preferVitePlusImportsRule = defineRule({ if (node.global) { return; } - maybeReportLiteral(context, node.id); + const id = node.id; + if ( + id?.type === 'Literal' && + typeof id.value === 'string' && + isVitestFamilyDeclareModuleSpecifier(id.value) + ) { + return; + } + maybeReportLiteral(context, id); }, }; }, diff --git a/packages/cli/src/pack-bin.ts b/packages/cli/src/pack-bin.ts index f5bd6da041..8a953f1f94 100644 --- a/packages/cli/src/pack-bin.ts +++ b/packages/cli/src/pack-bin.ts @@ -13,43 +13,53 @@ import { cac } from 'cac'; import { resolveViteConfig } from './resolve-vite-config.ts'; +// Matches a `.d.ts` / `.d.mts` / `.d.cts` importer. +const RE_DTS = /\.d\.[cm]?ts$/; +// Bare specifier for postcss / lightningcss / vitest / `@vitest/*` +// (root or any subpath). +const EXTERNAL_DTS_PKG_RE = /^(?:postcss|lightningcss|vitest|@vitest\/[^/]+)(?:\/|$)/; + /** - * Rolldown plugin that transforms value imports/exports to type-only in external - * packages' .d.ts files. Some packages (e.g. postcss, lightningcss) use - * `import { X }` and `export { X } from` instead of their type-only equivalents, - * which causes MISSING_EXPORT warnings from the DTS bundler. + * Rolldown plugin that keeps `postcss` / `lightningcss` / `vitest` / `@vitest/*` + * external to the DTS bundle. + * + * The DTS bundler resolves these packages and tries to inline their `.d.ts` + * files. postcss ships its public types as a CJS `export = postcss` over a + * `declare namespace postcss { export { AtRule, ... } }` (see + * postcss/lib/postcss.d.ts), and its ESM types entry (postcss.d.mts) re-exports + * those names with `export { AtRule, ... } from './postcss.js'`. The bundler + * cannot map named imports onto an `export =`'d namespace's members, so every + * consumer `import type { AtRule } from 'postcss'` becomes a MISSING_EXPORT + * error. * - * Since .d.ts files contain only type information, all imports/exports are - * inherently type-only, so this transformation is always safe. + * vitest fails for a different reason: `vitest@4.1.7`'s `dist/index.d.ts` + * re-exports `ExpectPollOptions` from `@vitest/expect`, but `@vitest/expect` + * does not actually export that name. `tsc` tolerates the dangling re-export + * because it only resolves re-exports lazily on use, but the DTS bundler + * eagerly resolves every re-export while inlining and so hits the missing + * export. `@vitest/browser/matchers.d.ts` then re-imports the same name from + * `vitest`, propagating the failure. Established vite-plus projects reach + * these files through the `vite-plus/test*` shims, which re-export `vitest` / + * `@vitest/*` from declaration files. + * + * Marking these packages external for `.d.ts` importers leaves the import + * untouched in the emitted declarations (`import type { AtRule } from 'postcss'`), + * which is how third-party packages should be treated in a DTS bundle anyway. + * They are only externalized when imported *from a declaration file*, so runtime + * bundling is unaffected. */ -const EXTERNAL_DTS_INTERNAL_RE = /node_modules\/(postcss|lightningcss)\/.*\.d\.(ts|mts|cts)$/; -// Match consumer .d.ts files that import from postcss/lightningcss. -// In CI (installed from tgz): node_modules/vite-plus-core/dist/... -// In local development (symlinked workspace): packages/core/dist/... -const EXTERNAL_DTS_CONSUMER_RE = - /(?:vite-plus-core|packages\/core)\/.*lightningcssOptions\.d\.ts$|(?:vite-plus-core|packages\/core)\/dist\/.*\.d\.ts$/; -const EXTERNAL_DTS_FIX_RE = new RegExp( - `${EXTERNAL_DTS_INTERNAL_RE.source}|${EXTERNAL_DTS_CONSUMER_RE.source}`, -); - function externalDtsTypeOnlyPlugin() { return { name: 'vite-plus:external-dts-type-only', - transform: { - filter: { id: { include: [EXTERNAL_DTS_FIX_RE] } }, - handler(code: string, rawId: string) { + resolveId: { + order: 'pre' as const, + handler(id: string, importer: string | undefined) { // Normalize Windows backslash paths to forward slashes for regex matching - const id = rawId.replaceAll('\\', '/'); - if (EXTERNAL_DTS_INTERNAL_RE.test(id)) { - // postcss/lightningcss internal files: transform imports only - // (exports may include value re-exports like `export const Features`) - return code.replace(/^(import\s+)(?!type\s)/gm, 'import type '); + const normalizedImporter = importer?.replaceAll('\\', '/'); + if (normalizedImporter && RE_DTS.test(normalizedImporter) && EXTERNAL_DTS_PKG_RE.test(id)) { + return { id, external: true }; } - // Consumer files: only transform imports from postcss/lightningcss - return code.replace( - /^(import\s+)(?!type\s)(.+from\s+['"](?:postcss|lightningcss)['"])/gm, - 'import type $2', - ); + return undefined; }, }, }; @@ -143,8 +153,7 @@ cli : [viteConfig.pack ?? {}]; for (const packConfig of packConfigs) { const merged = { ...packConfig, ...flags }; - // Inject plugin to fix MISSING_EXPORT warnings from external .d.ts files - // (postcss, lightningcss use `import`/`export` instead of `import type`/`export type`) + // Keep postcss/lightningcss external to the dts bundle (see plugin doc) if (merged.dts) { const existingPlugins = Array.isArray(merged.plugins) ? merged.plugins : []; merged.plugins = [...existingPlugins, externalDtsTypeOnlyPlugin()]; diff --git a/packages/cli/src/resolve-test.ts b/packages/cli/src/resolve-test.ts index 8c547c8d3a..a00c216285 100644 --- a/packages/cli/src/resolve-test.ts +++ b/packages/cli/src/resolve-test.ts @@ -2,16 +2,23 @@ * Vitest tool resolver for the vite-plus CLI. * * This module exports a function that resolves the Vitest binary path - * to the bundled Vitest in the CLI distribution. The resolved path is - * passed back to the Rust core, which then executes Vitest for running tests. + * to the vitest package installed alongside the CLI (or in the user's + * project, resolved from the current working directory first). The + * resolved path is passed back to the Rust core, which then executes + * Vitest for running tests. * * Used for: `vite-plus test` command */ +import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { DEFAULT_ENVS, resolve } from './utils/constants.ts'; +interface VitestPackageJson { + bin?: string | Record; +} + /** * Resolves the Vitest binary path and environment variables. * @@ -21,13 +28,21 @@ import { DEFAULT_ENVS, resolve } from './utils/constants.ts'; * * Vitest is Vite's testing framework that provides a Jest-compatible * testing experience with Vite's fast HMR and transformation pipeline. - * The function points to the bundled Vitest in the CLI's dist directory. + * The function resolves vitest from the user's project first, falling + * back to the copy installed alongside the CLI. */ export async function test(): Promise<{ binPath: string; envs: Record; }> { - const binPath = join(dirname(resolve('@voidzero-dev/vite-plus-test')), 'dist', 'cli.js'); + const pkgJsonPath = resolve('vitest/package.json'); + const pkgRoot = dirname(pkgJsonPath); + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as VitestPackageJson; + const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.vitest; + if (!binRel) { + throw new Error(`Could not find 'vitest' bin entry in ${pkgJsonPath}`); + } + const binPath = join(pkgRoot, binRel); return { binPath, diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts index 595a1e70fb..6ea31b0815 100644 --- a/packages/cli/src/utils/constants.ts +++ b/packages/cli/src/utils/constants.ts @@ -3,11 +3,22 @@ import { createRequire } from 'node:module'; export const VITE_PLUS_NAME = 'vite-plus'; export const VITE_PLUS_VERSION = process.env.VP_VERSION || 'latest'; +export const VITEST_VERSION = '4.1.7'; + export const VITE_PLUS_OVERRIDE_PACKAGES: Record = process.env.VP_OVERRIDE_PACKAGES ? JSON.parse(process.env.VP_OVERRIDE_PACKAGES) : { vite: 'npm:@voidzero-dev/vite-plus-core@latest', - vitest: 'npm:@voidzero-dev/vite-plus-test@latest', + vitest: VITEST_VERSION, + '@vitest/expect': VITEST_VERSION, + '@vitest/runner': VITEST_VERSION, + '@vitest/snapshot': VITEST_VERSION, + '@vitest/spy': VITEST_VERSION, + '@vitest/utils': VITEST_VERSION, + '@vitest/mocker': VITEST_VERSION, + '@vitest/pretty-format': VITEST_VERSION, + '@vitest/coverage-v8': VITEST_VERSION, + '@vitest/coverage-istanbul': VITEST_VERSION, }; /** diff --git a/packages/test/.gitignore b/packages/test/.gitignore deleted file mode 100644 index 7f727e288b..0000000000 --- a/packages/test/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.d.ts -*.d.cts -/LICENSE -LICENSE.md -*.cjs -*.mjs -browser/ -dist/ diff --git a/packages/test/BUNDLING.md b/packages/test/BUNDLING.md deleted file mode 100644 index 4866da58e1..0000000000 --- a/packages/test/BUNDLING.md +++ /dev/null @@ -1,427 +0,0 @@ -# Test Package Bundling Architecture - -This document explains how `@voidzero-dev/vite-plus-test` bundles vitest and its dependencies. - -## Overview - -The test package uses a **hybrid bundling strategy**: - -1. **COPY** all `@vitest/*` packages (preserves browser/Node.js separation) -2. **BUNDLE** only leaf dependencies like `chai`, `pathe` (reduces install size) -3. **Separate entries** (`index.js` vs `index-node.js`) prevent Node.js code from loading in browsers - -This approach avoids the critical issue of Rolldown creating shared chunks that mix Node.js-only code (like `__vite__injectQuery`) with browser code, which causes runtime crashes. - -## Dependencies Classification - -### Copied Packages (`dist/@vitest/`) - -These 11 `@vitest/*` packages are **copied** (not bundled) to preserve their original file structure: - -| Package | Purpose | -| ----------------------------- | ---------------------------------------------------- | -| `@vitest/runner` | Test runner core | -| `@vitest/utils` | Utilities (source-map, error, display, timers, etc.) | -| `@vitest/spy` | Spy/mock implementation | -| `@vitest/expect` | Assertion library | -| `@vitest/snapshot` | Snapshot testing | -| `@vitest/mocker` | Module mocking (node, browser, automock) | -| `@vitest/pretty-format` | Output formatting | -| `@vitest/browser` | Browser testing support | -| `@vitest/browser-playwright` | Playwright integration | -| `@vitest/browser-webdriverio` | WebdriverIO integration | -| `@vitest/browser-preview` | Preview (testing-library) integration | - -**Why copy instead of bundle?** Bundling would create shared chunks that mix browser-safe and Node.js-only code. Copying preserves the original separation. - -### Bundled Leaf Dependencies (`dist/vendor/`) - -These packages are bundled using Rolldown into `dist/vendor/*.mjs`: - -| Package | Purpose | -| --------------------- | ----------------------------------- | -| `chai` | Assertion library (core of expect) | -| `pathe` | Path utilities | -| `tinyrainbow` | Terminal colors | -| `magic-string` | String manipulation for source maps | -| `estree-walker` | AST traversal | -| `why-is-node-running` | Debug tool for hanging processes | - -These were moved from `dependencies` to `devDependencies` since they're bundled. - -### Runtime Dependencies (NOT Bundled) - -These remain in `dependencies` and are installed with the package: - -| Package | Reason Not Bundled | -| ----------------- | --------------------------------------------- | -| `sirv` | Static file server - complex runtime behavior | -| `ws` | WebSocket server - native bindings | -| `pixelmatch` | Image comparison - optional feature | -| `pngjs` | PNG handling - optional feature | -| `es-module-lexer` | ESM parsing - small, used at runtime | -| `expect-type` | Type testing - small | -| `obug` | Debugging - small | -| `picomatch` | Glob matching - small | -| `std-env` | Environment detection - small | -| `tinybench` | Benchmarking - optional feature | -| `tinyexec` | Command execution - small | -| `tinyglobby` | File globbing - small | - -### External Blocklist (Must NOT Bundle) - -These packages are explicitly kept external in `EXTERNAL_BLOCKLIST` during the Rolldown build: - -| Package | Reason | -| ----------------------- | ----------------------------------------- | -| `playwright` | Native bindings, user must install | -| `webdriverio` | Native bindings, user must install | -| `debug` | Environment detection breaks when bundled | -| `happy-dom` | Optional peer dependency | -| `jsdom` | Optional peer dependency | -| `@edge-runtime/vm` | Optional peer dependency | -| `@standard-schema/spec` | Types-only import from @vitest/expect | -| `msw`, `msw/*` | Optional peer dependency for mocking | - -### Browser Plugin Exclude List - -Additionally, these packages are added to the **browser plugin's exclude list** (in `patchVitestBrowserPackage`), which prevents Vite's optimizer from bundling them during browser tests: - -| Package | Reason | -| --------------------- | ------------------------------------------------ | -| `lightningcss` | Native bindings | -| `@tailwindcss/oxide` | Native bindings | -| `tailwindcss` | Pulls in @tailwindcss/oxide | -| `@vitest/browser` | Needs vendor-aliases plugin resolution | -| `@vitest/ui` | Optional peer dependency | -| `@vitest/mocker/node` | Imports @voidzero-dev/vite-plus-core (Node-only) | - -This is a different mechanism than `EXTERNAL_BLOCKLIST` - it controls runtime optimization, not build-time bundling. - ---- - -## Migration Guide - -For maintainers developing the vitest/vite migration feature, here are the transformations needed. - -### Import Rewrites - -| Original Import | Rewritten Import | -| ------------------------------------ | --------------------------------------------------------- | -| `from "@vitest/browser-playwright"` | `from "@voidzero-dev/vite-plus-test/browser-playwright"` | -| `from "@vitest/browser-webdriverio"` | `from "@voidzero-dev/vite-plus-test/browser-webdriverio"` | -| `from "@vitest/browser-preview"` | `from "@voidzero-dev/vite-plus-test/browser-preview"` | -| `from "vite"` | `from "@voidzero-dev/vite-plus-core"` | -| `from "vite/module-runner"` | `from "@voidzero-dev/vite-plus-core/module-runner"` | -| `import('vitest')` | `import('@voidzero-dev/vite-plus-test')` | - -**Note**: `@voidzero-dev/vite-plus-core` is the bundled version of upstream vite (Vite v8 beta). See [Core Package Bundling](../core/BUNDLING.md) for details on what it contains. - -**Note**: The `import('vitest')` → `import('@voidzero-dev/vite-plus-test')` rewrite is critical for `globals.d.ts`, which declares global types like `typeof import('vitest')['test']`. Without this rewrite, `vitest` is not resolvable from the `@voidzero-dev/vite-plus-test` package context in pnpm's strict `node_modules` layout. TypeScript silently treats unresolved dynamic type imports as `any`, but oxlint's type-aware linting treats them as `error` types, causing `no-unsafe-call` errors. The rewrite turns this into a self-reference that resolves correctly via Node.js package self-referencing. - -**Note:** When using pnpm overrides, you have three options for browser provider imports: - -- `vitest/browser-playwright` (or `vitest/browser-webdriverio`, `vitest/browser-preview`) - works when `vitest` is overridden to our package (Recommended) -- `@voidzero-dev/vite-plus-test/browser-playwright` - direct import from test package -- `vite-plus/test/plugins/browser-playwright` - direct import from CLI package - -Importing from `@vitest/browser-*` packages directly requires additional overrides for those specific packages. - -### package.json Changes - -**Remove these devDependencies** (now bundled): - -```json -{ - "devDependencies": { - "@vitest/browser": "...", // Remove - "@vitest/browser-playwright": "...", // Remove (if using playwright) - "@vitest/browser-webdriverio": "...", // Remove (if using webdriverio) - "@vitest/browser-preview": "...", // Remove (if using testing-library) - "@vitest/ui": "..." // Remove (peer dep, not bundled but optional) - } -} -``` - -**Add pnpm overrides**: - -```yaml -# pnpm-workspace.yaml -overrides: - vite: 'file:path/to/vite-plus-core.tgz' - vitest: 'file:path/to/vite-plus-test.tgz' - '@vitest/browser': 'file:path/to/vite-plus-test.tgz' - '@vitest/browser-playwright': 'file:path/to/vite-plus-test.tgz' - '@vitest/browser-webdriverio': 'file:path/to/vite-plus-test.tgz' - '@vitest/browser-preview': 'file:path/to/vite-plus-test.tgz' -``` - -Or using npm package names: - -```yaml -overrides: - vite: 'npm:@voidzero-dev/vite-plus-core' - vitest: 'npm:@voidzero-dev/vite-plus-test' - '@vitest/browser': 'npm:@voidzero-dev/vite-plus-test' - '@vitest/browser-playwright': 'npm:@voidzero-dev/vite-plus-test' - '@vitest/browser-webdriverio': 'npm:@voidzero-dev/vite-plus-test' - '@vitest/browser-preview': 'npm:@voidzero-dev/vite-plus-test' -``` - -### Config File Updates - -```typescript -// Before (playwright) -import { playwright } from '@vitest/browser-playwright'; - -// After - Option 1 (Recommended): Via vitest subpath (works when vitest is overridden) -import { playwright } from 'vitest/browser-playwright'; - -// After - Option 2: Direct import from test package -import { playwright } from '@voidzero-dev/vite-plus-test/browser-playwright'; - -// After - Option 3: Direct import from CLI package -import { playwright } from 'vite-plus/test/plugins/browser-playwright'; -``` - -Similarly for WebdriverIO: - -```typescript -import { webdriverio } from 'vitest/browser-webdriverio'; -``` - -And for Preview (testing-library): - -```typescript -import { preview } from 'vitest/browser-preview'; -``` - -### Plugin Exports for pnpm Overrides - -The package provides `./plugins/*` exports to enable pnpm overrides for all `@vitest/*` packages: - -``` -@vitest/runner -> @voidzero-dev/vite-plus-test/plugins/runner -@vitest/utils -> @voidzero-dev/vite-plus-test/plugins/utils -@vitest/utils/error -> @voidzero-dev/vite-plus-test/plugins/utils-error -@vitest/spy -> @voidzero-dev/vite-plus-test/plugins/spy -@vitest/expect -> @voidzero-dev/vite-plus-test/plugins/expect -@vitest/snapshot -> @voidzero-dev/vite-plus-test/plugins/snapshot -@vitest/mocker -> @voidzero-dev/vite-plus-test/plugins/mocker -@vitest/pretty-format -> @voidzero-dev/vite-plus-test/plugins/pretty-format -@vitest/browser -> @voidzero-dev/vite-plus-test/plugins/browser -@vitest/browser-playwright -> @voidzero-dev/vite-plus-test/plugins/browser-playwright -@vitest/browser-webdriverio -> @voidzero-dev/vite-plus-test/plugins/browser-webdriverio -@vitest/browser-preview -> @voidzero-dev/vite-plus-test/plugins/browser-preview -``` - ---- - -## Ensuring Bundle Stability - -### Build-time Validation - -The build script includes `validateExternalDeps()` which: - -1. Scans all bundled JS files using `oxc-parser` -2. Extracts all external import specifiers -3. Verifies every external dependency is declared in `dependencies` or `peerDependencies` -4. Reports undeclared externals that would fail at runtime - -If this validation fails, the build will report which packages need to be added. - -### Testing Strategy - -| Test Type | What It Validates | -| --------------------------------------------------------------- | -------------------------------------------------- | -| **Snap tests** (`packages/cli/snap-tests/vitest-browser-mode/`) | Browser mode works after bundling | -| **Ecosystem CI** (`ecosystem-ci/`) | Real-world projects work with bundled vitest | -| **CI workflows** | Multi-platform validation (Ubuntu, Windows, macOS) | - -### Vitest Upgrade Checklist - -When upgrading the vitest version: - -1. **Update version** in `packages/test/package.json`: - - ```json - { - "devDependencies": { - "vitest-dev": "^NEW_VERSION", - "@vitest/runner": "NEW_VERSION", - "@vitest/utils": "NEW_VERSION" - // ... all @vitest/* packages - } - } - ``` - -2. **Run build**: - - ```bash - pnpm -C packages/test build - ``` - -3. **Check for new externals**: If `validateExternalDeps()` reports new undeclared dependencies: - - Add to `dependencies` if it should be installed at runtime - - Add to `EXTERNAL_BLOCKLIST` if it should remain external (native bindings, optional) - - If it's a new leaf dep, it will be automatically bundled - -4. **Run tests**: - - ```bash - pnpm test - ``` - -5. **Run ecosystem CI**: - ```bash - pnpm -C ecosystem-ci test - ``` - -### Common Upgrade Issues - -| Issue | Cause | Solution | -| ----------------------- | ------------------------------ | -------------------------------------------------- | -| New undeclared external | New vitest dependency | Add to `dependencies` or `EXTERNAL_BLOCKLIST` | -| Browser test crashes | Node.js code leaked to browser | Check import rewriting in `rewriteVitestImports()` | -| Missing export | New @vitest/\* subpath export | Add to `VITEST_PACKAGE_TO_PATH` | -| pnpm override fails | New plugin export needed | Add to `createPluginExports()` | - ---- - -## Technical Reference - -### Build Flow - -``` -1. bundleVitest() Copy vitest-dev dist/ -> dist/ -2. copyVitestPackages() Copy @vitest/* -> dist/@vitest/ -3. convertTabsToSpaces() Normalize formatting for patches -4. collectLeafDependencies() Parse imports with oxc-parser -5. bundleLeafDeps() Bundle chai, pathe, etc -> dist/vendor/ -6. rewriteVitestImports() Rewrite @vitest/*, vitest/*, vite -7. patchVitestPkgRootPaths() Fix distRoot for relocated files -8. patchVitestBrowserPackage() Inject vendor-aliases plugin -9. patchBrowserProviderLocators() Fix browser-safe imports -10. Post-processing: - - patchVendorPaths() - - createBrowserCompatShim() - - createModuleRunnerStub() Browser-safe stub - - createNodeEntry() index-node.js with browser-provider - - copyBrowserClientFiles() - - createBrowserEntryFiles() browser/ entry files at package root - - patchModuleAugmentations() Rewrite @vitest/expect, @vitest/runner augmentations - - patchChaiTypeReference() Add @types/chai triple-slash reference - - createPluginExports() dist/plugins/* for pnpm overrides - - mergePackageJson() - - validateExternalDeps() -``` - -### Output Structure - -``` -browser/ # Entry files for ./browser export -├── context.js # Runtime guard (throws if not in browser) -└── context.d.ts # Re-exports from dist/@vitest/browser/context.d.ts -dist/ -├── @vitest/ # Copied packages (browser/Node.js safe) -│ ├── runner/ -│ ├── utils/ -│ ├── spy/ -│ ├── expect/ -│ ├── snapshot/ -│ ├── mocker/ -│ ├── pretty-format/ -│ ├── browser/ -│ └── browser-playwright/ -├── vendor/ # Bundled leaf dependencies -│ ├── chai.mjs -│ ├── pathe.mjs -│ ├── tinyrainbow.mjs -│ ├── magic-string.mjs -│ ├── estree-walker.mjs -│ ├── why-is-node-running.mjs -│ └── vitest_*.mjs # Browser stubs -├── plugins/ # Shims for pnpm overrides -│ ├── runner.mjs -│ ├── utils.mjs -│ └── ... (33+ files) -├── chunks/ # Vitest core chunks -├── client/ # Browser client files -├── index.js # Browser-safe entry -├── index-node.js # Node.js entry (includes browser-provider) -├── module-runner-stub.js # Browser-safe module-runner -└── browser-compat.js # @vitest/browser compatibility shim -``` - -### Browser/Node.js Separation - -The critical design decision is maintaining separation between browser and Node.js code: - -| Entry Point | Used By | Contains | -| -------------------- | --------------------- | --------------------------------- | -| `dist/index.js` | Browser tests | No Node.js-only code | -| `dist/index-node.js` | Node.js (config, CLI) | Includes browser-provider exports | - -This is achieved through: - -1. Conditional exports in package.json (`"node": "./dist/index-node.js"`) -2. Browser-safe stubs for `module-runner` -3. Import rewriting to prevent Node.js code from being pulled into browser bundles -4. `vendor-aliases` plugin injection to resolve imports at runtime: - - Handles `@vitest/*` imports → resolves to copied `dist/@vitest/` files - - Handles `vitest/*` subpaths → resolves to dist files (enables `vitest/browser-playwright` usage) - - Handles `vitest/browser-playwright`, `vitest/browser-webdriverio`, `vitest/browser-preview` → resolves to bundled browser providers - - Handles `@voidzero-dev/vite-plus-test/*` subpaths → maps to equivalent vitest paths - - Handles `vite-plus/test/*` subpaths → maps to equivalent vitest paths (CLI package) - - Intercepts `vitest/browser`, `@voidzero-dev/vite-plus-test/browser`, `vite-plus/test/browser` → returns virtual module ID for BrowserContext plugin - -### Key Constants - -```typescript -// Packages copied to dist/@vitest/ -const VITEST_PACKAGES_TO_COPY = [ - '@vitest/runner', - '@vitest/utils', - '@vitest/spy', - '@vitest/expect', - '@vitest/snapshot', - '@vitest/mocker', - '@vitest/pretty-format', - '@vitest/browser', - '@vitest/browser-playwright', - '@vitest/browser-webdriverio', - '@vitest/browser-preview', -]; - -// Packages that must NOT be bundled (from build.ts lines 131-158) -const EXTERNAL_BLOCKLIST = new Set([ - // Our own packages - resolved at runtime - '@voidzero-dev/vite-plus-core', - '@voidzero-dev/vite-plus-core/module-runner', - 'vite', - 'vitest', - - // Peer dependencies - consumers must provide these - '@edge-runtime/vm', - '@opentelemetry/api', - '@standard-schema/spec', // Types-only import from @vitest/expect - 'happy-dom', - 'jsdom', - - // Optional dependencies with bundling issues or native bindings - 'debug', // environment detection broken when bundled - 'playwright', // native bindings - 'webdriverio', // native bindings - - // Runtime deps (in package.json dependencies) - not bundled, resolved at install time - 'sirv', - 'ws', - 'pixelmatch', - 'pngjs', - - // MSW (Mock Service Worker) - optional peer dep of @vitest/mocker - 'msw', - 'msw/browser', - 'msw/core/http', -]); -``` diff --git a/packages/test/__tests__/build-artifacts.spec.ts b/packages/test/__tests__/build-artifacts.spec.ts deleted file mode 100644 index 641b98260c..0000000000 --- a/packages/test/__tests__/build-artifacts.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Verify that the @voidzero-dev/vite-plus-test build output (dist/) - * contains the expected files and that patches applied during the build - * (in build.ts) produce correct artifacts. - * - * These tests run against the already-built dist/ directory, ensuring - * that re-packaging patches produce correct artifacts. - */ -import fs from 'node:fs'; -import path from 'node:path'; -import url from 'node:url'; - -import { describe, expect, it } from 'vitest'; - -const testPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..'); -const distDir = path.join(testPkgDir, 'dist'); - -function findCliApiChunk(): string { - const chunksDir = path.join(distDir, 'chunks'); - const files = fs.readdirSync(chunksDir); - const chunk = files.find((f) => f.startsWith('cli-api.') && f.endsWith('.js')); - if (!chunk) { - throw new Error('cli-api chunk not found in dist/chunks/'); - } - return path.join(chunksDir, chunk); -} - -describe('build artifacts', () => { - describe('@vitest/browser/context.js', () => { - const contextPath = path.join(distDir, '@vitest/browser/context.js'); - - it('should exist', () => { - expect(fs.existsSync(contextPath), `${contextPath} should exist`).toBe(true); - }); - - it('should export page, cdp, and utils', () => { - const content = fs.readFileSync(contextPath, 'utf-8'); - expect(content).toMatch(/export\s*\{[^}]*page[^}]*\}/); - expect(content).toMatch(/export\s*\{[^}]*cdp[^}]*\}/); - expect(content).toMatch(/export\s*\{[^}]*utils[^}]*\}/); - }); - }); - - /** - * The vitest:vendor-aliases plugin must NOT resolve @vitest/browser/context - * to the static file. If it does, the BrowserContext plugin's virtual module - * (which provides the `server` export) is bypassed. - * - * See: https://github.com/voidzero-dev/vite-plus/issues/1086 - */ - describe('vitest:vendor-aliases plugin (regression test for #1086)', () => { - const browserIndexPath = path.join(distDir, '@vitest/browser/index.js'); - - it('should not map @vitest/browser/context in vendorMap', () => { - const content = fs.readFileSync(browserIndexPath, 'utf-8'); - // The vendorMap inside vitest:vendor-aliases should NOT contain - // '@vitest/browser/context' — it must be left for BrowserContext - // plugin to resolve as a virtual module. - const vendorAliasesMatch = content.match( - /name:\s*['"]vitest:vendor-aliases['"][\s\S]*?const vendorMap\s*=\s*\{([\s\S]*?)\}/, - ); - expect(vendorAliasesMatch, 'vitest:vendor-aliases plugin should exist').toBeTruthy(); - const vendorMapContent = vendorAliasesMatch![1]; - expect(vendorMapContent).not.toContain("'@vitest/browser/context'"); - }); - }); - - /** - * `convertTabsToSpaces()` in build.ts must not touch tabs inside string - * literals. Upstream `@vitest/snapshot` decides multi-line snapshot - * indentation via `indent.includes("\t")` — where `"\t"` is a literal - * tab byte in the bundled source. A blanket tab→spaces rewrite turned - * this into `indent.includes(" ")`, so every 2-space indent matched - * and the tab-appending branch always ran, producing tab-indented - * snapshots in 2-space files. - * - * See: https://github.com/voidzero-dev/vite-plus/issues/1553 - */ - describe('snapshot indent check (regression test for #1553)', () => { - const snapshotIndexPath = path.join(distDir, '@vitest/snapshot/index.js'); - - it('preserves the literal tab byte inside the indent.includes string', () => { - const content = fs.readFileSync(snapshotIndexPath, 'utf-8'); - expect(content).toContain('indent.includes("\t")'); - expect(content).not.toMatch(/indent\.includes\(" "\)/); - }); - }); - - /** - * Third-party packages that call `expect.extend()` internally - * (e.g., @testing-library/jest-dom) break under npm override because - * the vitest module instance is split, causing matchers to be registered - * on a different `chai` instance than the test runner uses. - * - * The build patches vitest's ModuleRunnerTransform plugin to auto-add - * these packages to `server.deps.inline`, so they are processed through - * Vite's transform pipeline and share the same module instance. - * - * See: https://github.com/voidzero-dev/vite-plus/issues/897 - */ - describe('server.deps.inline auto-inline (regression test for #897)', () => { - it('should contain the expected auto-inline packages', () => { - const content = fs.readFileSync(findCliApiChunk(), 'utf-8'); - expect(content).toContain('Auto-inline packages'); - expect(content).toContain('"@testing-library/jest-dom"'); - expect(content).toContain('"@storybook/test"'); - expect(content).toContain('"jest-extended"'); - }); - - it('should not override user inline config when set to true', () => { - const content = fs.readFileSync(findCliApiChunk(), 'utf-8'); - expect(content).toContain('server.deps.inline !== true'); - }); - }); -}); diff --git a/packages/test/build.ts b/packages/test/build.ts deleted file mode 100644 index dd313749cf..0000000000 --- a/packages/test/build.ts +++ /dev/null @@ -1,2649 +0,0 @@ -// Build Script for @voidzero-dev/vite-plus-test -// -// Bundles vitest and @vitest/* dependencies with browser/Node.js separation. -// -// ┌─────────────────────────────────────────────────────────────────────┐ -// │ BUILD FLOW │ -// ├─────────────────────────────────────────────────────────────────────┤ -// │ 1. bundleVitest() Copy vitest-dev → dist/ │ -// │ 2. copyVitestPackages() Copy @vitest/* → dist/@vitest/ │ -// │ 3. collectLeafDependencies() Parse imports with oxc-parser │ -// │ 4. bundleLeafDeps() Bundle chai, pathe, etc → dist/vendor/ │ -// │ 5. rewriteVitestImports() Rewrite @vitest/*, vitest/*, vite │ -// │ 6. patchVitestPkgRootPaths() Fix distRoot for relocated files │ -// │ 7. patchVitestBrowserPackage() Inject vendor-aliases plugin │ -// │ 8. patchBrowserProviderLocators() Fix browser-safe imports │ -// │ 9. Post-processing: │ -// │ - patchVendorPaths() │ -// │ - createBrowserCompatShim() │ -// │ - createModuleRunnerStub() Browser-safe stub │ -// │ - createNodeEntry() index-node.js with browser-provider│ -// │ - copyBrowserClientFiles() │ -// │ - createPluginExports() dist/plugins/* for pnpm overrides │ -// │ - mergePackageJson() │ -// │ - validateExternalDeps() │ -// └─────────────────────────────────────────────────────────────────────┘ -// -// Output Structure: -// dist/@vitest/* - Copied packages (browser/Node.js safe) -// dist/vendor/* - Bundled leaf dependencies -// dist/plugins/* - Shims for pnpm overrides -// dist/index.js - Browser-safe entry -// dist/index-node.js - Node.js entry (includes browser-provider) -// -// Key Design: -// - COPY @vitest/* to preserve browser/Node.js separation -// - BUNDLE only leaf deps (chai, etc.) to reduce install size -// - Separate entries prevent __vite__injectQuery errors in browser - -import { existsSync } from 'node:fs'; -import { - copyFile, - glob as fsGlob, - mkdir, - readFile, - readdir, - rm, - stat, - writeFile, -} from 'node:fs/promises'; -import { builtinModules } from 'node:module'; -import { basename, join, parse, resolve, dirname, relative } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { parseSync } from 'oxc-parser'; -import { format } from 'oxfmt'; -import { build } from 'rolldown'; -import { dts } from 'rolldown-plugin-dts'; - -import { generateLicenseFile } from '../../scripts/generate-license.js'; -import pkg from './package.json' with { type: 'json' }; - -const projectDir = dirname(fileURLToPath(import.meta.url)); -const vitestSourceDir = resolve(projectDir, 'node_modules/vitest-dev'); -const distDir = resolve(projectDir, 'dist'); -const vendorDir = resolve(distDir, 'vendor'); - -const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core'; -const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test'; - -// @vitest/* packages to copy (not bundle) to preserve browser/Node.js separation -// These are copied from node_modules to dist/@vitest/ to avoid shared chunks -// that mix Node.js-only code with browser code -const VITEST_PACKAGES_TO_COPY = [ - '@vitest/runner', - '@vitest/utils', - '@vitest/spy', - '@vitest/expect', - '@vitest/snapshot', - '@vitest/mocker', - '@vitest/pretty-format', - '@vitest/browser', - '@vitest/browser-playwright', - '@vitest/browser-webdriverio', - '@vitest/browser-preview', -] as const; - -// Mapping from @vitest/* package specifiers to their paths within dist/@vitest/ -// Used for import rewriting and vendor-aliases plugin -const VITEST_PACKAGE_TO_PATH: Record = { - // @vitest/runner - '@vitest/runner': '@vitest/runner/index.js', - '@vitest/runner/utils': '@vitest/runner/utils.js', - '@vitest/runner/types': '@vitest/runner/types.js', - // @vitest/utils - '@vitest/utils': '@vitest/utils/index.js', - '@vitest/utils/source-map': '@vitest/utils/source-map.js', - '@vitest/utils/source-map/node': '@vitest/utils/source-map/node.js', - '@vitest/utils/error': '@vitest/utils/error.js', - '@vitest/utils/helpers': '@vitest/utils/helpers.js', - '@vitest/utils/display': '@vitest/utils/display.js', - '@vitest/utils/timers': '@vitest/utils/timers.js', - '@vitest/utils/highlight': '@vitest/utils/highlight.js', - '@vitest/utils/offset': '@vitest/utils/offset.js', - '@vitest/utils/resolver': '@vitest/utils/resolver.js', - '@vitest/utils/serialize': '@vitest/utils/serialize.js', - '@vitest/utils/constants': '@vitest/utils/constants.js', - '@vitest/utils/diff': '@vitest/utils/diff.js', - // @vitest/spy - '@vitest/spy': '@vitest/spy/index.js', - // @vitest/expect - '@vitest/expect': '@vitest/expect/index.js', - // @vitest/snapshot - '@vitest/snapshot': '@vitest/snapshot/index.js', - '@vitest/snapshot/environment': '@vitest/snapshot/environment.js', - '@vitest/snapshot/manager': '@vitest/snapshot/manager.js', - // @vitest/mocker - '@vitest/mocker': '@vitest/mocker/index.js', - '@vitest/mocker/node': '@vitest/mocker/node.js', - '@vitest/mocker/browser': '@vitest/mocker/browser.js', - '@vitest/mocker/redirect': '@vitest/mocker/redirect.js', - '@vitest/mocker/transforms': '@vitest/mocker/transforms.js', - '@vitest/mocker/automock': '@vitest/mocker/automock.js', - '@vitest/mocker/register': '@vitest/mocker/register.js', - // @vitest/pretty-format - '@vitest/pretty-format': '@vitest/pretty-format/index.js', - // @vitest/browser - '@vitest/browser': '@vitest/browser/index.js', - '@vitest/browser/context': '@vitest/browser/context.js', - '@vitest/browser/client': '@vitest/browser/client.js', - '@vitest/browser/locators': '@vitest/browser/locators.js', - // @vitest/browser-playwright - '@vitest/browser-playwright': '@vitest/browser-playwright/index.js', - // @vitest/browser-webdriverio - '@vitest/browser-webdriverio': '@vitest/browser-webdriverio/index.js', - // @vitest/browser-preview - '@vitest/browser-preview': '@vitest/browser-preview/index.js', -}; - -// Packages that should NOT be bundled into dist/vendor/ (remain external at runtime) -// There are two categories: -// 1. Runtime deps (also in package.json dependencies) - installed with the package, not bundled -// 2. Peer/optional deps (also in peerDependencies) - users must install themselves -const EXTERNAL_BLOCKLIST = new Set([ - // Our own packages - resolved at runtime - CORE_PACKAGE_NAME, - `${CORE_PACKAGE_NAME}/module-runner`, - 'vite', - 'vitest', - - // Peer dependencies - consumers must provide these - '@edge-runtime/vm', - '@opentelemetry/api', - '@standard-schema/spec', // Types-only import from @vitest/expect - 'happy-dom', - 'jsdom', - - // Optional dependencies with bundling issues or native bindings - 'debug', // environment detection broken when bundled - 'playwright', // native bindings - 'webdriverio', // native bindings - - // Runtime deps (in package.json dependencies) - not bundled, resolved at install time - 'sirv', - 'ws', - 'pixelmatch', - 'pngjs', - - // MSW (Mock Service Worker) - optional peer dep of @vitest/mocker - 'msw', - 'msw/browser', - 'msw/core/http', -]); - -// CJS packages that need their default export destructured to named exports -const CJS_REEXPORT_PACKAGES = new Set(['expect-type']); - -// Node built-in modules (including node: prefix variants) -const NODE_BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]); - -// Step 1: Copy vitest-dev dist files (rewriting vite -> core package) -await bundleVitest(); - -// Step 1.5: Rebrand vitest CLI output as "vp test" with vite-plus version -await brandVitest(); - -// Step 2: Copy @vitest/* packages from node_modules to dist/@vitest/ -// This preserves the original file structure to maintain browser/Node.js separation -await copyVitestPackages(); - -// Step 2.5: Convert tabs to spaces in all copied JS files for consistent formatting -await convertTabsToSpaces(); - -// Step 3: Collect leaf dependencies from copied @vitest/* files -// These are external packages like tinyrainbow, pathe, chai, etc. -const leafDeps = await collectLeafDependencies(); - -// Step 4: Bundle only leaf dependencies into dist/vendor/ -// Unlike bundling @vitest/* directly, this avoids shared chunks that mix browser/Node.js code -const leafDepToVendorPath = await bundleLeafDeps(leafDeps); - -// Step 5: Rewrite imports in copied @vitest/* and vitest-dev files -// - @vitest/* -> relative paths to dist/@vitest/ -// - leaf deps -> relative paths to dist/vendor/ -// - vite -> @voidzero-dev/vite-plus-core -await rewriteVitestImports(leafDepToVendorPath); - -// Step 6: Fix pkgRoot resolution in all @vitest/* packages -// Files are now at dist/@vitest/*/index.js, so "../.." needs to become "../../.." -await patchVitestPkgRootPaths(); - -// Step 7: Patch @vitest/browser package (vendor-aliases plugin, exclude list) -await patchVitestBrowserPackage(); - -// Step 8: Patch browser provider locators.js files for browser-safe imports -await patchBrowserProviderLocators(); - -// Step 9: Post-processing -await patchVendorPaths(); -await patchVitestCoreResolver(); -await createBrowserCompatShim(); -await createModuleRunnerStub(); -await createNodeEntry(); -await copyBrowserClientFiles(); -await createBrowserEntryFiles(); -await patchModuleAugmentations(); -await patchChaiTypeReference(); -await patchMockerHoistedModule(); -await patchServerDepsInline(); -const pluginExports = await createPluginExports(); -await mergePackageJson(pluginExports); -generateLicenseFile({ - title: 'Vite-Plus test license', - packageName: 'Vite-Plus', - outputPath: join(projectDir, 'LICENSE'), - coreLicensePath: join(projectDir, '..', '..', 'LICENSE'), - bundledPaths: [distDir], - resolveFrom: [projectDir, join(projectDir, '..', '..')], - extraPackages: [ - { packageDir: vitestSourceDir }, - ...VITEST_PACKAGES_TO_COPY.map((packageName) => ({ - packageDir: resolve(projectDir, 'node_modules', packageName), - })), - ], -}); -if (!existsSync(join(projectDir, 'LICENSE'))) { - throw new Error('LICENSE was not generated during build'); -} -await validateExternalDeps(); - -async function mergePackageJson(pluginExports: Array<{ exportPath: string; shimFile: string }>) { - const vitestPackageJsonPath = join(vitestSourceDir, 'package.json'); - const destPackageJsonPath = resolve(projectDir, 'package.json'); - - const vitestPkg = JSON.parse(await readFile(vitestPackageJsonPath, 'utf-8')); - const destPkg = JSON.parse(await readFile(destPackageJsonPath, 'utf-8')); - - // Fields to merge from vitest-dev package.json (excluding dependencies since we bundle them) - const fieldsToMerge = [ - 'imports', - 'exports', - 'main', - 'module', - 'types', - 'engines', - 'peerDependencies', - 'peerDependenciesMeta', - ] as const; - - for (const field of fieldsToMerge) { - if (vitestPkg[field] !== undefined) { - destPkg[field] = vitestPkg[field]; - } - } - - // Remove bundled @vitest/* packages from peerDependencies - // These browser provider packages are now bundled, so users don't need to install them - const bundledPeerDeps = [ - '@vitest/browser-playwright', - '@vitest/browser-webdriverio', - '@vitest/browser-preview', - ]; - if (destPkg.peerDependencies) { - for (const dep of bundledPeerDeps) { - delete destPkg.peerDependencies[dep]; - } - } - if (destPkg.peerDependenciesMeta) { - for (const dep of bundledPeerDeps) { - delete destPkg.peerDependenciesMeta[dep]; - } - } - - destPkg.bundledVersions = { - ...destPkg.bundledVersions, - vitest: vitestPkg.version, - }; - - // Add @vitest/browser compatible export (for when this package overrides @vitest/browser) - // The main "." export is what's used when code imports from @vitest/browser - if (destPkg.exports) { - // Add conditional Node.js export to the main entry - // Node.js code (like @vitest/browser-playwright) uses index-node.js which includes - // browser-provider exports. Browser code uses index.js which is safe. - // This separation prevents Node.js-only code (like __vite__injectQuery) from being - // loaded in the browser, which would cause "Identifier already declared" errors. - // - // IMPORTANT: The 'browser' condition must come BEFORE 'node' because vitest passes - // custom --conditions (like 'browser') to worker processes when frameworks like Nuxt - // set edge/cloudflare presets. Without the 'browser' condition here, Node.js would - // match 'node' first, loading index-node.js which imports @vitest/browser/index.js, - // which imports 'ws'. With --conditions browser active, 'ws' resolves to its browser - // stub (ws/browser.js) that doesn't export WebSocketServer, causing a SyntaxError. - // See: https://github.com/voidzero-dev/vite-plus/issues/831 - if (destPkg.exports['.'] && destPkg.exports['.'].import) { - destPkg.exports['.'].import = { - types: destPkg.exports['.'].import.types, - browser: destPkg.exports['.'].import.default, - node: './dist/index-node.js', - default: destPkg.exports['.'].import.default, - }; - } - - destPkg.exports['./browser-compat'] = { - default: './dist/browser-compat.js', - }; - - // Add @vitest/browser-compatible subpath exports - // These are needed when this package is used as a pnpm override for @vitest/browser - // Files are copied to dist/ (not dist/vendor/) to match path resolution in bundled code - destPkg.exports['./client'] = { - default: './dist/client.js', - }; - // Point to @vitest/browser/context.js so that tests and init scripts share the same module - // This is critical: the init script (locators.js) calls page.extend() on this module, - // and tests must use the SAME module instance to see the extended methods - destPkg.exports['./context'] = { - types: './browser/context.d.ts', - default: './dist/@vitest/browser/context.js', - }; - // Also export ./browser/context for users importing vite-plus/test/browser/context - destPkg.exports['./browser/context'] = { - types: './browser/context.d.ts', - default: './dist/@vitest/browser/context.js', - }; - destPkg.exports['./locators'] = { - default: './dist/locators.js', - }; - destPkg.exports['./matchers'] = { - default: './dist/dummy.js', // Placeholder - }; - destPkg.exports['./utils'] = { - default: './dist/dummy.js', // Placeholder - }; - - // Add @vitest/browser-playwright compatible export - // Users can import { playwright } from 'vitest/browser-playwright' - destPkg.exports['./browser-playwright'] = { - types: './dist/@vitest/browser-playwright/index.d.ts', - default: './dist/@vitest/browser-playwright/index.js', - }; - - // Add @vitest/browser-webdriverio compatible export - // Users can import { webdriverio } from 'vitest/browser-webdriverio' - destPkg.exports['./browser-webdriverio'] = { - types: './dist/@vitest/browser-webdriverio/index.d.ts', - default: './dist/@vitest/browser-webdriverio/index.js', - }; - - // Add @vitest/browser-preview compatible export - // Users can import { preview } from 'vitest/browser-preview' - destPkg.exports['./browser-preview'] = { - types: './dist/@vitest/browser-preview/index.d.ts', - default: './dist/@vitest/browser-preview/index.js', - }; - - // Add browser/providers/* alias exports for compatibility - // Some vitest examples use the nested path format - destPkg.exports['./browser/providers/playwright'] = { - types: './dist/@vitest/browser-playwright/index.d.ts', - default: './dist/@vitest/browser-playwright/index.js', - }; - destPkg.exports['./browser/providers/webdriverio'] = { - types: './dist/@vitest/browser-webdriverio/index.d.ts', - default: './dist/@vitest/browser-webdriverio/index.js', - }; - destPkg.exports['./browser/providers/preview'] = { - types: './dist/@vitest/browser-preview/index.d.ts', - default: './dist/@vitest/browser-preview/index.js', - }; - - // Add plugin exports for all bundled @vitest/* packages - // This allows pnpm overrides to redirect: @vitest/runner -> vitest/plugins/runner - for (const { exportPath, shimFile } of pluginExports) { - destPkg.exports[exportPath] = { - default: shimFile, - }; - } - } - - // Merge vitest dependencies into devDependencies (since we bundle them) - // Skip packages that are already in dependencies (runtime deps) - if (vitestPkg.dependencies) { - destPkg.devDependencies = destPkg.devDependencies || {}; - for (const [dep, version] of Object.entries(vitestPkg.dependencies)) { - // Skip vite - we use our own core package - if (dep === 'vite') { - continue; - } - // Skip packages already in dependencies (they're runtime deps, not dev-only) - if (destPkg.dependencies && destPkg.dependencies[dep]) { - continue; - } - // Don't override existing devDependencies - if (!destPkg.devDependencies[dep]) { - destPkg.devDependencies[dep] = version; - } - } - } - - const { code, errors } = await format( - destPackageJsonPath, - JSON.stringify(destPkg, null, 2) + '\n', - { - experimentalSortPackageJson: true, - }, - ); - if (errors.length > 0) { - for (const error of errors) { - console.error(error); - } - process.exit(1); - } - await writeFile(destPackageJsonPath, code); -} - -async function bundleVitest() { - const vitestDestDir = projectDir; - - await mkdir(vitestDestDir, { recursive: true }); - - // Get all vitest files excluding node_modules and package.json - const vitestFiles = fsGlob(join(vitestSourceDir, '**/*'), { - exclude: [ - join(vitestSourceDir, 'node_modules/**'), - join(vitestSourceDir, 'package.json'), - join(vitestSourceDir, 'README.md'), - join(vitestSourceDir, 'LICENSE.md'), - ], - }); - - for await (const file of vitestFiles) { - const stats = await stat(file); - if (!stats.isFile()) { - continue; - } - - const relativePath = file.replace(vitestSourceDir, ''); - const destPath = join(vitestDestDir, relativePath); - - await mkdir(parse(destPath).dir, { recursive: true }); - - // Rewrite vite imports in .js, .mjs, and .cjs files - if ( - file.endsWith('.js') || - file.endsWith('.mjs') || - file.endsWith('.cjs') || - file.endsWith('.d.ts') || - file.endsWith('.d.cts') - ) { - let content = await readFile(file, 'utf-8'); - content = content - .replaceAll(/from ['"]vite['"]/g, `from '${CORE_PACKAGE_NAME}'`) - .replaceAll(/import\(['"]vite['"]\)/g, `import('${CORE_PACKAGE_NAME}')`) - .replaceAll(/require\(['"]vite['"]\)/g, `require('${CORE_PACKAGE_NAME}')`) - .replaceAll(/require\("vite"\)/g, `require("${CORE_PACKAGE_NAME}")`) - .replaceAll(`import 'vite';`, `import '${CORE_PACKAGE_NAME}';`) - .replaceAll(`'vite/module-runner'`, `'${CORE_PACKAGE_NAME}/module-runner'`) - .replaceAll(`declare module "vite"`, `declare module "${CORE_PACKAGE_NAME}"`) - .replaceAll(/import\(['"]vitest['"]\)/g, `import('${TEST_PACKAGE_NAME}')`); - console.log(`Replaced vite imports in ${destPath}`); - await writeFile(destPath, content, 'utf-8'); - } else { - await copyFile(file, destPath); - } - } -} - -/** - * Rebrand vitest CLI output as "vp test" with Vite+ banner styling. - * Patches bundled chunks to replace vitest branding and align banner output. - */ -async function brandVitest() { - const chunksDir = resolve(projectDir, 'dist/chunks'); - const cacFiles: string[] = []; - for await (const file of fsGlob(join(chunksDir, 'cac.*.js'))) { - cacFiles.push(file); - } - if (cacFiles.length === 0) { - throw new Error('brandVitest: no cac chunk found in dist/chunks/'); - } - for (const cacFile of cacFiles) { - let content = await readFile(cacFile, 'utf-8'); - - function patchString(label: string, search: string | RegExp, replacement: string) { - const before = content; - content = - typeof search === 'string' - ? content.replace(search, replacement) - : content.replace(search, replacement); - if (content === before) { - throw new Error( - `brandVitest: failed to patch "${label}" — pattern not found in ${cacFile}`, - ); - } - } - - // 1. CLI name: cac("vitest") → cac("vp test") - patchString('cac name', 'cac("vitest")', 'cac("vp test")'); - - // 2. Version: var version = "" → use VP_VERSION env var with fallback - patchString( - 'version', - /var version = "(\d+\.\d+\.\d+[^"]*)"/, - 'var version = process.env.VP_VERSION || "$1"', - ); - - // 3. Banner regex: /^vitest\/\d+\.\d+\.\d+$/ → /^vp test\/[\d.]+$/ - patchString('banner regex', '/^vitest\\/\\d+\\.\\d+\\.\\d+$/', '/^vp test\\/[\\d.]+$/'); - - // 4. Help text: $ vitest --help → $ vp test --help - patchString('help text', '$ vitest --help --expand-help', '$ vp test --help --expand-help'); - - await writeFile(cacFile, content, 'utf-8'); - console.log(`Branded vitest → vp test in ${cacFile}`); - } - - const cliApiFiles: string[] = []; - for await (const file of fsGlob(join(chunksDir, 'cli-api.*.js'))) { - cliApiFiles.push(file); - } - if (cliApiFiles.length === 0) { - throw new Error('brandVitest: no cli-api chunk found in dist/chunks/'); - } - - for (const cliApiFile of cliApiFiles) { - let content = await readFile(cliApiFile, 'utf-8'); - - function patchString(label: string, search: string | RegExp, replacement: string) { - const before = content; - content = - typeof search === 'string' - ? content.replace(search, replacement) - : content.replace(search, replacement); - if (content === before) { - throw new Error( - `brandVitest: failed to patch "${label}" — pattern not found in ${cliApiFile}`, - ); - } - } - - // Remove one extra leading newline before DEV/RUN banner. - patchString( - 'banner leading newline', - /printBanner\(\) \{\n\t\tthis\.log\(\);\n/, - 'printBanner() {\n', - ); - - // Use a blue badge for both DEV and RUN. - patchString( - 'banner color', - /const color = this\.ctx\.config\.watch \? "blue" : "[a-z]+";\n\t\tconst mode = this\.ctx\.config\.watch \? "DEV" : "RUN";/, - 'const mode = this.ctx.config.watch ? "DEV" : "RUN";\n\t\tconst label = c.bold(c.inverse(c.blue(` ${mode} `)));', - ); - - // Remove the version from the banner line and render a high-contrast label. - patchString( - 'banner version text', - /this\.log\(withLabel\(color, mode, (?:""|`v\$\{this\.ctx\.version\} `)\) \+ c\.gray\(this\.ctx\.config\.root\)\);/, - 'this.log(`${label} ${c.gray(this.ctx.config.root)}`);', - ); - - await writeFile(cliApiFile, content, 'utf-8'); - console.log(`Branded vitest banner in ${cliApiFile}`); - } -} - -/** - * Copy @vitest/* packages from node_modules to dist/@vitest/ - * This preserves the original file structure to maintain browser/Node.js separation. - * Unlike bundling with Rolldown, copying avoids creating shared chunks that mix - * Node.js-only code with browser code. - */ -async function copyVitestPackages() { - console.log('\nCopying @vitest/* packages to dist/@vitest/...'); - - const vitestDir = resolve(distDir, '@vitest'); - await rm(vitestDir, { recursive: true, force: true }); - await mkdir(vitestDir, { recursive: true }); - - let totalCopied = 0; - - for (const pkg of VITEST_PACKAGES_TO_COPY) { - const pkgName = pkg.replace('@vitest/', ''); - const srcDir = resolve(projectDir, `node_modules/${pkg}/dist`); - const destPkgDir = resolve(vitestDir, pkgName); - - try { - await stat(srcDir); - } catch { - console.log(` Warning: ${pkg} not installed, skipping`); - continue; - } - - console.log(` Copying ${pkg}...`); - const copied = await copyDirRecursive(srcDir, destPkgDir); - totalCopied += copied; - console.log(` -> ${copied} files`); - - // Copy root .d.ts files from @vitest/browser package directory. - // These are type definitions that live at the package root (not in dist/), - // e.g. context.d.ts, matchers.d.ts, aria-role.d.ts, utils.d.ts. - // Dynamically scan instead of hardcoding to handle future upstream additions. - if (pkg === '@vitest/browser') { - const pkgRoot = resolve(projectDir, `node_modules/${pkg}`); - try { - const pkgEntries = await readdir(pkgRoot); - for (const entry of pkgEntries) { - if (entry.endsWith('.d.ts')) { - await copyFile(join(pkgRoot, entry), join(destPkgDir, entry)); - console.log(` + copied ${entry}`); - totalCopied++; - } - } - } catch { - // Package root not readable, skip - } - } - } - - console.log(`\nCopied ${totalCopied} files to dist/@vitest/`); -} - -/** - * Recursively copy a directory - */ -async function copyDirRecursive(srcDir: string, destDir: string): Promise { - await mkdir(destDir, { recursive: true }); - const entries = await readdir(srcDir, { withFileTypes: true }); - let count = 0; - - for (const entry of entries) { - const srcPath = join(srcDir, entry.name); - const destPath = join(destDir, entry.name); - - if (entry.isDirectory()) { - count += await copyDirRecursive(srcPath, destPath); - } else if (entry.isFile()) { - await copyFile(srcPath, destPath); - count++; - } - } - - return count; -} - -/** - * Collect leaf dependencies from copied @vitest/* files AND vitest core dist files. - * These are external packages that should be bundled (tinyrainbow, pathe, chai, expect-type, etc.) - * but NOT @vitest/*, vitest/*, vite/*, node built-ins, or blocklisted packages. - */ -async function collectLeafDependencies(): Promise> { - console.log('\nCollecting leaf dependencies from dist/...'); - - const leafDeps = new Set(); - const vitestDir = resolve(distDir, '@vitest'); - - // Scan both @vitest/* packages AND vitest core dist files - const jsFiles = fsGlob([ - join(vitestDir, '**/*.js'), - join(distDir, '*.js'), - join(distDir, 'chunks/*.js'), - ]); - - for await (const file of jsFiles) { - const content = await readFile(file, 'utf-8'); - const result = parseSync(file, content, { sourceType: 'module' }); - - // Collect ESM static imports - for (const imp of result.module.staticImports) { - const specifier = imp.moduleRequest.value; - if (isLeafDependency(specifier)) { - leafDeps.add(specifier); - } - } - - // Collect ESM static exports (re-exports) - for (const exp of result.module.staticExports) { - for (const entry of exp.entries) { - if (entry.moduleRequest) { - const specifier = entry.moduleRequest.value; - if (isLeafDependency(specifier)) { - leafDeps.add(specifier); - } - } - } - } - - // Collect dynamic imports (only string literals) - for (const dynImp of result.module.dynamicImports) { - const rawText = content.slice(dynImp.moduleRequest.start, dynImp.moduleRequest.end); - if ( - (rawText.startsWith("'") && rawText.endsWith("'")) || - (rawText.startsWith('"') && rawText.endsWith('"')) - ) { - const specifier = rawText.slice(1, -1); - if (isLeafDependency(specifier)) { - leafDeps.add(specifier); - } - } - } - } - - console.log(`Found ${leafDeps.size} leaf dependencies:`); - for (const dep of leafDeps) { - console.log(` - ${dep}`); - } - - return leafDeps; -} - -/** - * Check if a specifier is a leaf dependency that should be bundled. - * Leaf deps are external packages that are NOT: - * - @vitest/* (we copy these) - * - vitest or vitest/* (we copy vitest-dev) - * - vite or vite/* (we use our core package) - * - Node.js built-ins - * - Blocklisted packages - * - Relative paths - */ -function isLeafDependency(specifier: string): boolean { - // Relative paths - if (specifier.startsWith('.') || specifier.startsWith('/')) { - return false; - } - // @vitest/* packages (we copy these) - if (specifier.startsWith('@vitest/')) { - return false; - } - // vitest or vitest/* (we copy vitest-dev) - if (specifier === 'vitest' || specifier.startsWith('vitest/')) { - return false; - } - // vite or vite/* (we use our core package) - if (specifier === 'vite' || specifier.startsWith('vite/')) { - return false; - } - // Node.js built-ins - if (NODE_BUILTINS.has(specifier)) { - return false; - } - // Blocklisted packages - if (EXTERNAL_BLOCKLIST.has(specifier)) { - return false; - } - // Node.js subpath imports (#module-evaluator, etc.) - if (specifier.startsWith('#')) { - return false; - } - // Invalid specifiers - if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*/.test(specifier)) { - return false; - } - return true; -} - -/** - * Bundle only leaf dependencies into dist/vendor/. - * Only bundles non-@vitest deps (tinyrainbow, pathe, chai, etc.) - * to avoid shared chunks that mix Node.js and browser code. - */ -async function bundleLeafDeps(leafDeps: Set): Promise> { - console.log('\nBundling leaf dependencies...'); - - await rm(vendorDir, { recursive: true, force: true }); - await mkdir(vendorDir, { recursive: true }); - - const specifierToVendorPath = new Map(); - - if (leafDeps.size === 0) { - console.log(' No leaf dependencies to bundle.'); - return specifierToVendorPath; - } - - // Build input object with all leaf deps - const input: Record = {}; - for (const dep of leafDeps) { - const safeName = safeFileName(dep); - input[safeName] = dep; - } - - try { - await build({ - input, - output: { - dir: vendorDir, - format: 'esm', - entryFileNames: '[name].mjs', - chunkFileNames: 'shared-[hash].mjs', - }, - platform: 'neutral', - treeshake: false, - external: [ - // Keep node built-ins external - ...NODE_BUILTINS, - // Keep blocklisted packages external - ...EXTERNAL_BLOCKLIST, - // Keep @vitest/* external (we copy them) - /@vitest\//, - // Keep vitest external (we copy it) - /^vitest(\/.*)?$/, - // Keep vite external (we use core package) - /^vite(\/.*)?$/, - ], - resolve: { - conditionNames: ['node', 'import', 'default'], - mainFields: ['module', 'main'], - }, - logLevel: 'warn', - }); - - const dtsInput = { ...input }; - - for (const name of Object.keys(dtsInput)) { - const vendorDtsPath = join(vendorDir, `vendor_${name}.d.ts`); - dtsInput[name] = vendorDtsPath; - await writeFile(vendorDtsPath, `export * from '${name}';`, 'utf-8'); - } - - await build({ - input: dtsInput, - output: { - dir: vendorDir, - format: 'esm', - entryFileNames: '[name].mts', - }, - plugins: [ - dts({ - dtsInput: true, - oxc: true, - resolver: 'oxc', - emitDtsOnly: true, - tsconfig: false, - }), - ], - }); - - for (const p of Object.values(dtsInput)) { - await rm(p); - } - - // Register all specifiers - for (const dep of leafDeps) { - const safeName = safeFileName(dep); - const vendorFilePath = join(vendorDir, `${safeName}.mjs`); - specifierToVendorPath.set(dep, vendorFilePath); - console.log(` -> vendor/${safeName}.mjs`); - - // Fix CJS packages that need named exports extracted from default - if (CJS_REEXPORT_PACKAGES.has(dep)) { - await fixCjsNamedExports(vendorFilePath, dep); - } - } - } catch (error) { - console.error('Failed to bundle leaf dependencies:', error); - throw error; - } - - console.log(`\nBundled ${specifierToVendorPath.size} leaf dependencies.`); - return specifierToVendorPath; -} - -/** - * Rewrite imports in all copied @vitest/* files and vitest-dev dist files. - * This handles: - * - @vitest/* -> relative paths to dist/@vitest/ - * - vitest/* -> relative paths to dist/ - * - vite -> @voidzero-dev/vite-plus-core - * - leaf deps -> relative paths to dist/vendor/ - */ -async function rewriteVitestImports(leafDepToVendorPath: Map) { - console.log('\nRewriting imports in @vitest/* and vitest core files...'); - - const vitestDir = resolve(distDir, '@vitest'); - let rewrittenCount = 0; - - // Scan both @vitest/* packages AND vitest core dist files - // Include .d.ts files so TypeScript type imports also get rewritten - const jsFiles = fsGlob([ - join(vitestDir, '**/*.js'), - join(vitestDir, '**/*.d.ts'), - join(distDir, '*.js'), - join(distDir, '*.d.ts'), - join(distDir, 'chunks/*.js'), - join(distDir, 'chunks/*.d.ts'), - ]); - - for await (const file of jsFiles) { - let content = await readFile(file, 'utf-8'); - const fileDir = dirname(file); - - // Build specifier map for this file - const specifierMap = new Map(); - - // Add @vitest/* mappings (relative paths) - for (const [pkg, destPath] of Object.entries(VITEST_PACKAGE_TO_PATH)) { - const absoluteDest = resolve(distDir, destPath); - let relativePath = relative(fileDir, absoluteDest); - relativePath = relativePath.split('\\').join('/'); // Windows fix - if (!relativePath.startsWith('.')) { - relativePath = './' + relativePath; - } - specifierMap.set(pkg, absoluteDest); - } - - // Add vitest/* mappings (relative to dist/) - const vitestSubpathRewrites: Record = { - vitest: resolve(distDir, 'index.js'), - 'vitest/node': resolve(distDir, 'node.js'), - 'vitest/config': resolve(distDir, 'config.js'), - // vitest/browser exports page, server, CDPSession, BrowserCommands, etc from @vitest/browser/context - // This matches vitest's own package.json exports: "./browser" -> "./browser/context.d.ts" - 'vitest/browser': resolve(distDir, '@vitest/browser/context.js'), - // vitest/internal/browser exports browser-safe __INTERNAL and stringify (NOT @vitest/browser/index.js which has Node.js code) - 'vitest/internal/browser': resolve(distDir, 'browser.js'), - 'vitest/runners': resolve(distDir, 'runners.js'), - 'vitest/suite': resolve(distDir, 'suite.js'), - 'vitest/environments': resolve(distDir, 'environments.js'), - 'vitest/coverage': resolve(distDir, 'coverage.js'), - 'vitest/reporters': resolve(distDir, 'reporters.js'), - 'vitest/snapshot': resolve(distDir, 'snapshot.js'), - 'vitest/mocker': resolve(distDir, 'mocker.js'), - }; - for (const [specifier, absolutePath] of Object.entries(vitestSubpathRewrites)) { - specifierMap.set(specifier, absolutePath); - } - - // Add leaf dep mappings (relative to vendor/) - for (const [specifier, vendorPath] of leafDepToVendorPath) { - specifierMap.set(specifier, vendorPath); - } - - // For files inside @vitest/browser/, preserve 'vitest/browser' as a bare specifier. - // These files run in browser context where the vitest:vendor-aliases plugin - // resolves 'vitest/browser' to the virtual module '\0vitest/browser', - // which provides browser-safe context API (page, server, userEvent, utils). - // Without this, 'vitest/browser' gets rewritten to './index.js' which resolves - // to the Node.js server file (~9000 lines of node:fs, ws, etc.) - if (file.includes('@vitest/browser') || file.includes('@vitest\\browser')) { - specifierMap.delete('vitest/browser'); - } - - // Rewrite using AST - const rewritten = rewriteImportsWithAst(content, file, false, specifierMap); - - // Also rewrite vite -> core package (simple string replacement since it's a package name) - let finalContent = rewritten - .replaceAll(/from ['"]vite['"]/g, `from '${CORE_PACKAGE_NAME}'`) - .replaceAll(/import\(['"]vite['"]\)/g, `import('${CORE_PACKAGE_NAME}')`) - .replaceAll(`'vite/module-runner'`, `'${CORE_PACKAGE_NAME}/module-runner'`); - - // Special handling for @vitest/mocker entry files that have redundant side-effect imports - // The original files have: import 'magic-string'; export {...} from './chunk-automock.js'; import 'estree-walker'; - // This is problematic because: - // 1. Side-effect imports are redundant (chunk files already import what they need) - // 2. Having imports after exports can confuse some module parsers - // Fix: Remove redundant side-effect imports from vendor deps in entry files - if (file.includes('@vitest/mocker') || file.includes('@vitest\\mocker')) { - // Get the base filename - const baseName = file.split(/[/\\]/).pop(); - // Only process entry files (not chunk files) - if (baseName && !baseName.startsWith('chunk-')) { - // Remove side-effect imports from vendor deps (these are redundant since chunk files import them) - finalContent = finalContent.replace(/import\s*['"][^'"]*vendor[^'"]*\.mjs['"];?\s*/g, ''); - } - } - - if (finalContent !== content) { - await writeFile(file, finalContent, 'utf-8'); - rewrittenCount++; - } - } - - console.log(` Rewrote imports in ${rewrittenCount} files`); - - // Also rewrite imports in the main vitest-dev dist files - console.log('\nRewriting imports in vitest-dev dist files...'); - let mainRewrittenCount = 0; - - const mainJsFiles = fsGlob(join(distDir, '*.js')); - const chunksJsFiles = fsGlob(join(distDir, 'chunks', '*.js')); - const workersJsFiles = fsGlob(join(distDir, 'workers', '*.js')); - - for await (const file of mainJsFiles) { - const rewritten = await rewriteDistFile(file, leafDepToVendorPath); - if (rewritten) { - mainRewrittenCount++; - } - } - for await (const file of chunksJsFiles) { - const rewritten = await rewriteDistFile(file, leafDepToVendorPath); - if (rewritten) { - mainRewrittenCount++; - } - } - for await (const file of workersJsFiles) { - const rewritten = await rewriteDistFile(file, leafDepToVendorPath); - if (rewritten) { - mainRewrittenCount++; - } - } - - console.log(` Rewrote imports in ${mainRewrittenCount} dist files`); -} - -/** - * Rewrite imports in a vitest-dev dist file. - * Returns true if the file was modified. - */ -async function rewriteDistFile( - file: string, - leafDepToVendorPath: Map, -): Promise { - let content = await readFile(file, 'utf-8'); - - // Build specifier map - const specifierMap = new Map(); - - // Add @vitest/* mappings - for (const [pkg, destPath] of Object.entries(VITEST_PACKAGE_TO_PATH)) { - const absoluteDest = resolve(distDir, destPath); - specifierMap.set(pkg, absoluteDest); - } - - // Add leaf dep mappings - for (const [specifier, vendorPath] of leafDepToVendorPath) { - specifierMap.set(specifier, vendorPath); - } - - // Add vitest/* subpath mappings - // NOTE: Do NOT include 'vitest/browser' - it must be handled by - // the vitest:browser:virtual-module:context plugin at runtime - const vitestSubpathRewrites: Record = { - vitest: resolve(distDir, 'index.js'), - 'vitest/node': resolve(distDir, 'node.js'), - 'vitest/config': resolve(distDir, 'config.js'), - // 'vitest/browser' - intentionally omitted, handled by virtual module plugin - 'vitest/internal/browser': resolve(distDir, 'browser.js'), - 'vitest/runners': resolve(distDir, 'runners.js'), - 'vitest/suite': resolve(distDir, 'suite.js'), - 'vitest/environments': resolve(distDir, 'environments.js'), - 'vitest/coverage': resolve(distDir, 'coverage.js'), - 'vitest/reporters': resolve(distDir, 'reporters.js'), - 'vitest/snapshot': resolve(distDir, 'snapshot.js'), - 'vitest/mocker': resolve(distDir, 'mocker.js'), - }; - for (const [specifier, absolutePath] of Object.entries(vitestSubpathRewrites)) { - specifierMap.set(specifier, absolutePath); - } - - // Add mappings for ./vendor/vitest_*.mjs relative imports - // These are vitest-dev's bundled @vitest/* packages that we've copied to dist/@vitest/ - const vendorToVitest: Record = { - './vendor/vitest_runner.mjs': resolve(distDir, '@vitest/runner/index.js'), - './vendor/vitest_runners.mjs': resolve(distDir, 'runners.js'), - './vendor/vitest_browser.mjs': resolve(distDir, '@vitest/browser/context.js'), - './vendor/vitest_internal_browser.mjs': resolve(distDir, 'browser.js'), - './vendor/vitest_utils.mjs': resolve(distDir, '@vitest/utils/index.js'), - './vendor/vitest_spy.mjs': resolve(distDir, '@vitest/spy/index.js'), - './vendor/vitest_snapshot.mjs': resolve(distDir, '@vitest/snapshot/index.js'), - './vendor/vitest_expect.mjs': resolve(distDir, '@vitest/expect/index.js'), - }; - for (const [vendorPath, destPath] of Object.entries(vendorToVitest)) { - specifierMap.set(vendorPath, destPath); - } - - let rewritten = rewriteImportsWithAst(content, file, false, specifierMap); - - // Strip module-runner side-effect import from index.js - // This import is Node.js-only and causes browser tests to hang when vitest/index.js - // is loaded in browser context (to get describe, it, expect, etc.) - // The module-runner contains Node.js code (process.platform, etc.) that browsers can't execute - if (basename(file) === 'index.js') { - rewritten = rewritten.replace( - /import\s*['"]@voidzero-dev\/vite-plus-core\/module-runner['"];?\s*/g, - '', - ); - } - - if (rewritten !== content) { - await writeFile(file, rewritten, 'utf-8'); - return true; - } - return false; -} - -/** - * Rewrite imports using oxc-parser AST for precise replacements - */ -function rewriteImportsWithAst( - content: string, - filePath: string, - isCjs: boolean, - specifierToVendorPath: Map, -): string { - // Use Map to deduplicate replacements by start position - const replacementMap = new Map(); - - // Helper to get relative path for a specifier - const getRelativePath = (specifier: string): string | null => { - const vendorPath = specifierToVendorPath.get(specifier); - if (!vendorPath) { - return null; - } - let relativePath = relative(dirname(filePath), vendorPath); - // Normalize to forward slashes for ES module imports (Windows uses backslashes) - relativePath = relativePath.split('\\').join('/'); - if (!relativePath.startsWith('.')) { - relativePath = './' + relativePath; - } - return relativePath; - }; - - // Helper to add replacement (deduplicates by start position) - const addReplacement = (start: number, end: number, newValue: string) => { - if (!replacementMap.has(start)) { - replacementMap.set(start, [start, end, newValue]); - } - }; - - // Parse with oxc-parser - const result = parseSync(filePath, content, { - sourceType: isCjs ? 'script' : 'module', - }); - - // Collect ESM static imports - for (const imp of result.module.staticImports) { - const specifier = imp.moduleRequest.value; - const relativePath = getRelativePath(specifier); - if (relativePath) { - // Replace the module request (including quotes) - addReplacement(imp.moduleRequest.start, imp.moduleRequest.end, `'${relativePath}'`); - } - } - - // Collect ESM static exports (re-exports) - for (const exp of result.module.staticExports) { - for (const entry of exp.entries) { - if (entry.moduleRequest) { - const specifier = entry.moduleRequest.value; - const relativePath = getRelativePath(specifier); - if (relativePath) { - addReplacement(entry.moduleRequest.start, entry.moduleRequest.end, `'${relativePath}'`); - } - } - } - } - - // Collect dynamic imports (only string literals) - for (const dynImp of result.module.dynamicImports) { - const rawText = content.slice(dynImp.moduleRequest.start, dynImp.moduleRequest.end); - if ( - (rawText.startsWith("'") && rawText.endsWith("'")) || - (rawText.startsWith('"') && rawText.endsWith('"')) - ) { - const specifier = rawText.slice(1, -1); - const relativePath = getRelativePath(specifier); - if (relativePath) { - addReplacement(dynImp.moduleRequest.start, dynImp.moduleRequest.end, `'${relativePath}'`); - } - } - } - - // For CJS files, also handle require() calls using regex (oxc-parser doesn't track these) - if (isCjs) { - const requireRegex = /require\s*\(\s*(['"])([^'"]+)\1\s*\)/g; - let match; - while ((match = requireRegex.exec(content)) !== null) { - const specifier = match[2]; - const relativePath = getRelativePath(specifier); - if (relativePath) { - // Calculate the position of just the string literal (including quotes) - const stringStart = match.index + match[0].indexOf(match[1]); - const stringEnd = stringStart + match[1].length + specifier.length + match[1].length; - addReplacement(stringStart, stringEnd, `'${relativePath}'`); - } - } - } - - // Sort replacements in reverse order (end to start) to preserve positions - // eslint-disable-next-line unicorn/no-array-sort -- safe: sorting a fresh spread copy - const replacements = [...replacementMap.values()].sort((a, b) => b[0] - a[0]); - - // Apply replacements - let result_content = content; - for (const [start, end, newValue] of replacements) { - result_content = result_content.slice(0, start) + newValue + result_content.slice(end); - } - - return result_content; -} - -/** - * Fix CJS packages that only export default - extract named exports from the default export - */ -async function fixCjsNamedExports(vendorFilePath: string, specifier: string) { - let content = await readFile(vendorFilePath, 'utf-8'); - - // Match pattern like: export default require_xxx(); - // and: export { }; - const defaultExportMatch = content.match(/export default (require_\w+)\(\);/); - - if (defaultExportMatch) { - const requireFn = defaultExportMatch[1]; - console.log(` Fixing CJS named exports for ${specifier}...`); - - const emptyExportMatch = content.match(/export \{\s*\};/); - if (emptyExportMatch) { - // Pattern: export default require_xxx();\nexport { }; - content = content.replace( - /export default (require_\w+)\(\);\s*\nexport \{\s*\};/, - `const __cjs_export__ = ${requireFn}();\nexport const { expectTypeOf } = __cjs_export__;\nexport default __cjs_export__;`, - ); - } else { - // Pattern: export default require_xxx(); (no empty export block) - content = content.replace( - /export default (require_\w+)\(\);/, - `const __cjs_export__ = ${requireFn}();\nexport const { expectTypeOf } = __cjs_export__;\nexport default __cjs_export__;`, - ); - } - - await writeFile(vendorFilePath, content, 'utf-8'); - } -} - -/** - * Create a safe filename from a specifier - */ -function safeFileName(specifier: string): string { - return specifier.replace(/[@/]/g, '_').replace(/^_/, ''); -} - -/** - * Patch pkgRoot/distRoot paths in vendor files. - * The bundled code assumes files are in dist/, but vendor files are in dist/vendor/ - * So "../.." needs to become "../../.." to correctly resolve to package root. - * Also patches relative file references like "context.js" to "../context.js". - */ -async function patchVendorPaths() { - console.log('\nPatching vendor paths...'); - - // Patterns that need one more level up due to vendor subdirectory - const pathPatterns = [ - // Package root calculation: "../.." -> "../../.." - { - original: `resolve$1(fileURLToPath(import.meta.url), "../..")`, - fixed: `resolve$1(fileURLToPath(import.meta.url), "../../..")`, - }, - // context.js reference: "context.js" -> "../context.js" - // This is used in browser server to resolve the vitest/browser/context export - { - original: `resolve$1(__dirname$1, "context.js")`, - fixed: `resolve$1(__dirname$1, "../context.js")`, - }, - ]; - - const vendorFiles = fsGlob(join(vendorDir, '*.mjs')); - let patchedCount = 0; - - for await (const file of vendorFiles) { - let content = await readFile(file, 'utf-8'); - let modified = false; - - for (const { original, fixed } of pathPatterns) { - if (content.includes(original)) { - content = content.replaceAll(original, fixed); - modified = true; - } - } - - if (modified) { - await writeFile(file, content, 'utf-8'); - console.log(` Patched paths in ${relative(distDir, file)}`); - patchedCount++; - } - } - - if (patchedCount === 0) { - console.log(' No vendor files needed path patching'); - } else { - console.log(` Successfully patched ${patchedCount} file(s)`); - } -} - -/** - * Patch VitestCoreResolver to resolve vite-plus/test directly. - * - * Problem: CLI's `export * from '@voidzero-dev/vite-plus-test'` creates a re-export - * chain that breaks module identity in Vite's SSR transform. expect.extend() - * mutations aren't visible through the re-export. - * - * Fix: Make VitestCoreResolver resolve both vite-plus/test and - * @voidzero-dev/vite-plus-test directly to dist/index.js, bypassing re-exports. - */ -async function patchVitestCoreResolver() { - console.log('\nPatching VitestCoreResolver for CLI package alias...'); - - let cliApiChunk: string | undefined; - for await (const chunk of fsGlob(join(distDir, 'chunks/cli-api.*.js'))) { - cliApiChunk = chunk; - break; - } - - if (!cliApiChunk) { - throw new Error('cli-api chunk not found'); - } - let content = await readFile(cliApiChunk, 'utf8'); - - // Find the VitestCoreResolver resolveId function and add our package aliases - const oldPattern = `async resolveId(id) { - if (id === "vitest") return resolve(distDir, "index.js"); - if (id.startsWith("@vitest/") || id.startsWith("vitest/"))`; - - const newCode = `async resolveId(id) { - if (id === "vitest") return resolve(distDir, "index.js"); - // Resolve CLI test path and test package directly to dist/index.js - // This bypasses the re-export chain and ensures module identity is preserved - if (id === "vite-plus/test" || id === "@voidzero-dev/vite-plus-test") { - return resolve(distDir, "index.js"); - } - // Handle subpaths: vite-plus/test/* -> vitest/* - if (id.startsWith("vite-plus/test/")) { - const subpath = id.slice("vite-plus/test/".length); - return this.resolve("vitest/" + subpath, join(ctx.config.root, "index.html"), { skipSelf: true }); - } - // Handle subpaths: @voidzero-dev/vite-plus-test/* -> vitest/* - if (id.startsWith("@voidzero-dev/vite-plus-test/")) { - const subpath = id.slice("@voidzero-dev/vite-plus-test/".length); - return this.resolve("vitest/" + subpath, join(ctx.config.root, "index.html"), { skipSelf: true }); - } - if (id.startsWith("@vitest/") || id.startsWith("vitest/"))`; - - if (!content.includes(oldPattern)) { - throw new Error( - 'Could not find VitestCoreResolver pattern to patch in ' + - cliApiChunk + - '. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } - - content = content.replace(oldPattern, newCode); - await writeFile(cliApiChunk, content); - console.log(' Patched VitestCoreResolver to resolve vite-plus/test directly'); -} - -/** - * Convert leading tabs to spaces in all JS files in dist/ for consistent - * formatting. This allows our patching code to use space-based patterns - * instead of tabs. - * - * Only leading whitespace is rewritten — tabs inside string or template - * literals are semantically meaningful (e.g. `indent.includes("\t")` in - * @vitest/snapshot picks the snapshot indent style by checking for a - * literal tab byte) and must be preserved. - * - * See: https://github.com/voidzero-dev/vite-plus/issues/1553 - */ -async function convertTabsToSpaces() { - console.log('\nConverting leading tabs to spaces in dist/...'); - - let convertedCount = 0; - - for await (const file of fsGlob(resolve(distDir, '**/*.js'))) { - const content = await readFile(file, 'utf-8'); - const converted = content.replace(/^\t+/gm, (match) => ' '.repeat(match.length)); - if (converted !== content) { - await writeFile(file, converted); - convertedCount++; - } - } - - console.log(` Converted ${convertedCount} files`); -} - -/** - * Fix pkgRoot path resolution in all `@vitest/*` packages. - * The original packages use resolve(import.meta.url, "../..") to find their package root. - * But our files are at `dist/@vitest/star/index.js`, so we need to go up 3 levels, not 2. - */ -async function patchVitestPkgRootPaths() { - console.log('\nFixing distRoot paths in @vitest/* packages...'); - - const vitestDir = resolve(distDir, '@vitest'); - let patchedCount = 0; - - for (const pkg of VITEST_PACKAGES_TO_COPY) { - const pkgName = pkg.replace('@vitest/', ''); - const indexPath = join(vitestDir, pkgName, 'index.js'); - - try { - await stat(indexPath); - } catch { - continue; - } - - let content = await readFile(indexPath, 'utf-8'); - - // The original @vitest/browser had index.js in the dist/ folder, so: - // pkgRoot = resolve(import.meta.url, "../..") -> @vitest/browser - // distRoot = resolve(pkgRoot, "dist") -> @vitest/browser/dist - // But our file is at dist/@vitest/browser/index.js, so distRoot should just be - // the directory containing index.js (not pkgRoot/dist) - // Replace both lines with just making distRoot = dirname of index.js - // Use regex to handle both top-level and indented occurrences - const oldPattern = - /( *)const pkgRoot = resolve\(fileURLToPath\(import\.meta\.url\), "\.\.\/\.\."\);\n\1const distRoot = resolve\(pkgRoot, "dist"\);/g; - const newContent = content.replace( - oldPattern, - '$1const distRoot = dirname(fileURLToPath(import.meta.url));', - ); - - if (newContent !== content) { - await writeFile(indexPath, newContent, 'utf-8'); - const matchCount = (content.match(oldPattern) || []).length; - console.log(` Fixed ${pkg}/index.js (${matchCount} occurrences)`); - patchedCount++; - } - } - - console.log(` Patched ${patchedCount} packages`); -} - -/** - * Patch the copied @vitest/browser package to: - * 1. Inject vitest:vendor-aliases plugin for @vitest/* resolution - * 2. Add native deps to the exclude list - * 3. Remove include patterns for bundled deps - */ -async function patchVitestBrowserPackage() { - console.log('\nPatching @vitest/browser package...'); - - const browserIndexPath = join(distDir, '@vitest/browser/index.js'); - - try { - await stat(browserIndexPath); - } catch { - console.log(' Warning: @vitest/browser not found in dist, skipping'); - return; - } - - let content = await readFile(browserIndexPath, 'utf-8'); - - // 1. Inject vitest:vendor-aliases plugin into BrowserPlugin return array - // This allows imports like @vitest/runner to be resolved to our copied @vitest files - // Exclude @vitest/browser/context from vendor-aliases so that BrowserContext - // plugin's resolveId can intercept the bare specifier and return the virtual - // module (which includes the dynamically generated `server` export). - // Without this, vendor-aliases resolves the bare specifier to the static - // context.js file (which has no `server`), bypassing BrowserContext entirely. - // See: https://github.com/voidzero-dev/vite-plus/issues/1086 - const VENDOR_ALIASES_EXCLUDE = new Set(['@vitest/browser/context']); - - const mappingEntries = Object.entries(VITEST_PACKAGE_TO_PATH) - .filter(([pkg]) => pkg.startsWith('@vitest/') && !VENDOR_ALIASES_EXCLUDE.has(pkg)) - .map(([pkg, file]) => `'${pkg}': resolve(packageRoot, '${file}')`) - .join(',\n '); - - // distRoot is @vitest/browser/ so we need to go up two levels to reach the actual dist root - const vendorAliasesPlugin = `{ - name: 'vitest:vendor-aliases', - enforce: 'pre', - resolveId(id) { - // distRoot is @vitest/browser/, packageRoot is the actual dist/ directory - const packageRoot = resolve(distRoot, '../..'); - // Resolve module-runner to a browser-safe stub - // This is critical: module-runner contains Node.js-only code (process.platform, etc.) - // that causes browsers to hang when loaded - if (id === '${CORE_PACKAGE_NAME}/module-runner' || id === 'vite/module-runner') { - return resolve(packageRoot, 'module-runner-stub.js'); - } - // Mark vite/core as external to prevent Node.js-only code from being bundled - // This prevents __vite__injectQuery duplication errors in browser tests - if (id === '${CORE_PACKAGE_NAME}' || id === 'vite') { - return { id, external: true }; - } - // Handle vitest/browser and package aliases - // Return virtual module ID so BrowserContext plugin can load it - // Supports: vitest/browser, @voidzero-dev/vite-plus-test/browser, vite-plus/test/browser - if (id === 'vitest/browser' || id === '@voidzero-dev/vite-plus-test/browser' || id === 'vite-plus/test/browser') { - return '\\0vitest/browser'; - } - // Handle vitest/* subpaths (resolve to our dist files) - // Also handle @voidzero-dev package aliases that resolve to the same files - const vitestSubpathMap = { - 'vitest': resolve(packageRoot, 'index.js'), - '@voidzero-dev/vite-plus-test': resolve(packageRoot, 'index.js'), - 'vite-plus/test': resolve(packageRoot, 'index.js'), - 'vitest/node': resolve(packageRoot, 'node.js'), - 'vitest/config': resolve(packageRoot, 'config.js'), - 'vitest/internal/browser': resolve(packageRoot, 'browser.js'), - 'vitest/runners': resolve(packageRoot, 'runners.js'), - 'vitest/suite': resolve(packageRoot, 'suite.js'), - 'vitest/environments': resolve(packageRoot, 'environments.js'), - 'vitest/coverage': resolve(packageRoot, 'coverage.js'), - 'vitest/reporters': resolve(packageRoot, 'reporters.js'), - 'vitest/snapshot': resolve(packageRoot, 'snapshot.js'), - 'vitest/mocker': resolve(packageRoot, 'mocker.js'), - // Browser providers - resolve to our bundled @vitest/browser-* packages - 'vitest/browser-playwright': resolve(packageRoot, '@vitest/browser-playwright/index.js'), - 'vitest/browser-webdriverio': resolve(packageRoot, '@vitest/browser-webdriverio/index.js'), - 'vitest/browser-preview': resolve(packageRoot, '@vitest/browser-preview/index.js'), - }; - if (vitestSubpathMap[id]) { - return vitestSubpathMap[id]; - } - // Handle @voidzero-dev/vite-plus-test/* subpaths (same as vitest/*) - if (id.startsWith('@voidzero-dev/vite-plus-test/')) { - const subpath = id.slice('@voidzero-dev/vite-plus-test/'.length); - const vitestEquiv = 'vitest/' + subpath; - if (vitestSubpathMap[vitestEquiv]) { - return vitestSubpathMap[vitestEquiv]; - } - } - // Handle vite-plus/test/* subpaths (CLI package paths, same as vitest/*) - if (id.startsWith('vite-plus/test/')) { - const subpath = id.slice('vite-plus/test/'.length); - const vitestEquiv = 'vitest/' + subpath; - if (vitestSubpathMap[vitestEquiv]) { - return vitestSubpathMap[vitestEquiv]; - } - } - // Handle @vitest/* packages (resolve to our copied files) - const vendorMap = { - ${mappingEntries} - }; - if (vendorMap[id]) { - return vendorMap[id]; - } - } - }`; - - // Find BrowserPlugin return array and inject plugin - const pluginArrayPattern = /(return \[)(\n +\{\n +enforce: "pre",\n +name: "vitest:browser",)/; - if (pluginArrayPattern.test(content)) { - content = content.replace(pluginArrayPattern, `$1\n ${vendorAliasesPlugin},$2`); - console.log(' Injected vitest:vendor-aliases plugin'); - } else { - throw new Error( - 'Failed to inject vendor-aliases plugin in @vitest/browser/index.js: pattern not found. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } - - // 2. Patch exclude list to add native deps - // Pattern: const exclude = ["vitest", ... - const excludePattern = /(const exclude = \[)(\n?\s*"vitest",)/; - // Exclude packages that: - // Packages to exclude from Vite's dependency pre-bundling (optimizeDeps.exclude) - const packagesToExclude = [ - // @vitest packages that need our resolveId plugin - '@vitest/browser', - '@vitest/ui', - '@vitest/ui/reporter', - '@vitest/mocker/node', // imports @voidzero-dev/vite-plus-core - - // Our package aliases - preserve module identity with init scripts - // This ensures both init scripts (loaded via /@fs/) and tests use the same page singleton - '@voidzero-dev/vite-plus-test', - '@voidzero-dev/vite-plus-test/browser', - '@voidzero-dev/vite-plus-test/browser/context', - 'vite-plus/test', - 'vite-plus/test/browser', - 'vite-plus/test/browser/context', - - // Node.js only packages - 'vite', - '@voidzero-dev/vite-plus-core', - '@voidzero-dev/vite-plus-core/module-runner', - - // Native bindings - 'lightningcss', - '@tailwindcss/oxide', - 'tailwindcss', // pulls in @tailwindcss/oxide - ]; - - const excludeListStr = packagesToExclude.map((pkg) => `"${pkg}"`).join(',\n '); - const excludeReplacement = `$1\n ${excludeListStr},$2`; - if (excludePattern.test(content)) { - content = content.replace(excludePattern, excludeReplacement); - console.log(' Patched exclude list with native deps'); - } else { - throw new Error( - 'Failed to patch exclude list in @vitest/browser/index.js: pattern not found. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } - - // 3. Remove include patterns that reference bundled deps - // These patterns like "vitest > expect-type" don't work with our bundled setup - // since the deps are already bundled into vendor files - const includePatterns = [ - '"vitest > expect-type"', - '"vitest > @vitest/snapshot > magic-string"', - '"vitest > @vitest/expect > chai"', - ]; - for (const pattern of includePatterns) { - content = content.replace( - new RegExp(`\\s*${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')},?`, 'g'), - '', - ); - } - console.log(' Removed bundled deps from include list'); - - // 4. Patch BrowserContext to also handle our package aliases as fallback - // This allows direct imports from our package without requiring vitest override - // Supports: vitest/browser, @voidzero-dev/vite-plus-test/browser, vite-plus/test/browser - const browserContextPattern = /if \(id === ID_CONTEXT\) \{/; - if (browserContextPattern.test(content)) { - content = content.replace( - browserContextPattern, - `if (id === ID_CONTEXT || id === "@voidzero-dev/vite-plus-test/browser" || id === "vite-plus/test/browser") {`, - ); - console.log(' Patched BrowserContext to handle package aliases'); - } else { - throw new Error( - 'Failed to patch BrowserContext in @vitest/browser/index.js: pattern not found. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } - - // 5. Patch version to use VP_VERSION, preventing the "Running mixed versions" warning - const versionPattern = /var version = "(\d+\.\d+\.\d+[^"]*)"/; - const beforeVersion = content; - content = content.replace(versionPattern, 'var version = process.env.VP_VERSION || "$1"'); - if (content === beforeVersion) { - throw new Error( - 'Failed to patch version in @vitest/browser/index.js: pattern not found. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } - console.log(' Patched version to use VP_VERSION env var'); - - await writeFile(browserIndexPath, content, 'utf-8'); - console.log(' Successfully patched @vitest/browser/index.js'); -} - -/** - * Patch browser provider locators.js files to use browser-safe imports. - * - * The original files import from '../browser/index.js' which includes Node.js server code. - * We need to change them to import from browser-safe files instead. - * - * Providers handled: - * - @vitest/browser-playwright: import { page, server } from '../browser/index.js'; - * - @vitest/browser-webdriverio: import { page, server, utils } from '../browser/index.js'; - * - @vitest/browser-preview: import { page, server, utils, userEvent } from '../browser/index.js'; - */ -async function patchBrowserProviderLocators() { - console.log('\nPatching browser provider locators.js files...'); - - const providers = [ - { name: 'browser-playwright', extraImports: [] as string[] }, - { name: 'browser-webdriverio', extraImports: ['utils'] }, - { name: 'browser-preview', extraImports: ['utils', 'userEvent'] }, - ]; - - for (const provider of providers) { - const locatorsPath = join(distDir, `@vitest/${provider.name}/locators.js`); - - try { - await stat(locatorsPath); - } catch { - console.log(` Warning: @vitest/${provider.name}/locators.js not found, skipping`); - continue; - } - - let content = await readFile(locatorsPath, 'utf-8'); - let patched = false; - - // 1. Patch the vitest/browser import to separate page (from context.js) and other imports - // After rewriteVitestImports(), the import is: import { page, server, ... } from '../browser/index.js'; - // We need: - // - page from '../browser/context.js' (browser-safe) - // - server removed (we'll use window.__vitest_worker__.config instead) - // - other imports (utils, userEvent) still from '../browser/index.js' - - if (provider.extraImports.length === 0) { - // playwright: just import page from context.js - const serverImportPattern = - /import \{ page, server \} from ['"]\.\.\/browser\/index\.js['"];?/; - if (serverImportPattern.test(content)) { - content = content.replace( - serverImportPattern, - `import { page } from '../browser/context.js';`, - ); - console.log(` [${provider.name}] Changed server import to browser-safe context import`); - patched = true; - } - } else { - // webdriverio/preview: import page from context.js, keep other imports from index.js - const extraImportsStr = provider.extraImports.join(', '); - const importPattern = new RegExp( - `import \\{ page, server, ${extraImportsStr} \\} from ['"]\\.\\./browser/index\\.js['"];?`, - ); - if (importPattern.test(content)) { - const replacement = `import { page } from '../browser/context.js';\nimport { ${extraImportsStr} } from '../browser/index.js';`; - content = content.replace(importPattern, replacement); - console.log( - ` [${provider.name}] Split imports: page from context.js, {${extraImportsStr}} from index.js`, - ); - patched = true; - } - } - - if (!patched) { - console.log(` Warning: [${provider.name}] Could not find server import to patch`); - } - - // 2. Replace all server.config references with browser-accessible window.__vitest_worker__.config - // This handles both: - // - server.config.browser.locators.testIdAttribute - // - server.config.browser.ui - const serverConfigPattern = /server\.config\./g; - const matchCount = (content.match(serverConfigPattern) || []).length; - if (matchCount > 0) { - content = content.replace(serverConfigPattern, `window.__vitest_worker__.config.`); - console.log( - ` [${provider.name}] Replaced ${matchCount} server.config references with window.__vitest_worker__.config`, - ); - } - - await writeFile(locatorsPath, content, 'utf-8'); - console.log(` Successfully patched @vitest/${provider.name}/locators.js`); - } -} - -/** - * Create browser-compat.js shim that re-exports @vitest/browser compatible symbols. - * This allows our package to be used as an override for @vitest/browser. - */ -async function createBrowserCompatShim() { - console.log('\nCreating browser-compat shim...'); - - const browserIndexPath = join(distDir, '@vitest/browser/index.js'); - - try { - await stat(browserIndexPath); - } catch { - console.log(' Warning: @vitest/browser/index.js not found, skipping'); - return; - } - - const browserSymbols = [ - 'resolveScreenshotPath', - 'defineBrowserProvider', - 'parseKeyDef', - 'defineBrowserCommand', - ]; - - const shimContent = `// Re-export @vitest/browser compatible symbols -// This allows this package to be used as an override for @vitest/browser -export { ${browserSymbols.join(', ')} } from './@vitest/browser/index.js'; -`; - - const shimPath = join(distDir, 'browser-compat.js'); - await writeFile(shimPath, shimContent, 'utf-8'); - console.log(` Created ${relative(projectDir, shimPath)}`); -} - -/** - * Create a browser-safe stub for module-runner. - * The real module-runner contains Node.js-only code (process.platform, Buffer, etc.) - * that causes browsers to hang when loaded. This stub provides empty/placeholder - * exports so that browser code can import without errors. - */ -async function createModuleRunnerStub() { - console.log('\nCreating browser-safe module-runner stub...'); - - const stubContent = `// Browser-safe stub for module-runner -// The real module-runner contains Node.js-only code that crashes browsers -// This stub provides placeholder exports for browser compatibility - -// Stub class - browser doesn't actually use these -export class EvaluatedModules { - constructor() { - this.idToModuleMap = new Map(); - this.fileToModulesMap = new Map(); - this.urlToIdModuleMap = new Map(); - } - getModuleById() { return undefined; } - getModulesByFile() { return []; } - getModuleByUrl() { return undefined; } - ensureModule() { return {}; } - invalidateModule() {} - clear() {} -} - -export class ModuleRunner { - constructor() {} - async import() { throw new Error('ModuleRunner is not available in browser'); } - evaluatedModules = new EvaluatedModules(); -} - -export class ESModulesEvaluator { - constructor() {} - async runExternalModule() { return {}; } - async runViteModule() { return {}; } -} - -// Stub functions -export function createDefaultImportMeta() { return {}; } -export function createNodeImportMeta() { return {}; } -export function createWebSocketModuleRunnerTransport() { return {}; } -export function normalizeModuleId(id) { return id; } - -// SSR-related constants (browser doesn't use these) -export const ssrDynamicImportKey = '__vite_ssr_dynamic_import__'; -export const ssrExportAllKey = '__vite_ssr_exportAll__'; -export const ssrExportNameKey = '__vite_ssr_export__'; -export const ssrImportKey = '__vite_ssr_import__'; -export const ssrImportMetaKey = '__vite_ssr_import_meta__'; -export const ssrModuleExportsKey = '__vite_ssr_exports__'; -`; - - const stubPath = join(distDir, 'module-runner-stub.js'); - await writeFile(stubPath, stubContent, 'utf-8'); - console.log(` Created ${relative(projectDir, stubPath)}`); -} - -/** - * Create a Node.js-specific entry that includes @vitest/browser symbols. - * Browser code will use index.js (no browser-provider imports) to avoid loading Node.js code. - * Node.js code (like @vitest/browser-playwright) will use index-node.js which includes - * the browser symbols needed for pnpm override compatibility. - * - * This separation is critical because @vitest/browser/index.js imports from vitest/node, - * which contains Node.js-only code (including __vite__injectQuery) that crashes browsers. - */ -async function createNodeEntry() { - console.log('\nCreating Node.js-specific entry for @vitest/browser override...'); - - const browserIndexPath = join(distDir, '@vitest/browser/index.js'); - - try { - await stat(browserIndexPath); - } catch { - console.log(' Warning: @vitest/browser/index.js not found, skipping'); - return; - } - - const browserSymbols = [ - 'resolveScreenshotPath', - 'defineBrowserProvider', - 'parseKeyDef', - 'defineBrowserCommand', - ]; - - // Create index-node.js that re-exports everything from index.js plus browser symbols - const nodeEntry = `// Node.js-specific entry that includes @vitest/browser provider symbols -// Browser code should use index.js which doesn't pull in Node.js-only code -export * from './index.js'; - -// Re-export @vitest/browser symbols for pnpm override compatibility -// These are only needed when this package overrides @vitest/browser in Node.js context -export { ${browserSymbols.join(', ')} } from './@vitest/browser/index.js'; -`; - - const nodeEntryPath = join(distDir, 'index-node.js'); - await writeFile(nodeEntryPath, nodeEntry, 'utf-8'); - console.log(` Created dist/index-node.js with @vitest/browser exports`); -} - -/** - * Copy ALL files from @vitest/browser's dist to our dist. - * The bundled code in dist/vendor/ calculates paths like: - * pkgRoot = resolve(import.meta.url, "../..") -> package root - * distRoot = resolve(pkgRoot, "dist") -> dist/ - * Then looks for client/ files at distRoot, so we copy to dist/ not dist/vendor/. - */ -async function copyBrowserClientFiles() { - console.log('\nCopying @vitest/browser files to dist...'); - - // Find @vitest/browser's dist directory - const vitestBrowserDist = resolve(projectDir, 'node_modules/@vitest/browser/dist'); - - // Check if it exists - try { - await stat(vitestBrowserDist); - } catch { - console.log(' Warning: @vitest/browser not installed, skipping'); - return; - } - - // Copy all files from @vitest/browser/dist to our dist/ - // The bundled code at dist/vendor/ resolves paths relative to dist/ - // Use recursive directory traversal to include dotfiles (glob doesn't handle them well) - let copiedCount = 0; - - // Rewrite imports in copied JS files to use our dist files - // The relative path depends on the file's location relative to dist/ - function rewriteImports(content: string, destPath: string): string { - const fileDir = parse(destPath).dir; - - // Calculate relative path from file location to vendor directory - const vendorPath = join(distDir, 'vendor'); - let relativeToVendor = relative(fileDir, vendorPath); - // Ensure path starts with ./ for relative imports - if (!relativeToVendor.startsWith('.')) { - relativeToVendor = './' + relativeToVendor; - } - - // Calculate relative path from file location to dist directory - let relativeToDist = relative(fileDir, distDir); - if (!relativeToDist.startsWith('.')) { - relativeToDist = './' + relativeToDist; - } - - // Rewrite @vitest/* imports to use our copied @vitest files - for (const [pkg, distPath] of Object.entries(VITEST_PACKAGE_TO_PATH)) { - if (!pkg.startsWith('@vitest/')) { - continue; - } - // Pattern: from"@vitest/runner" or from "@vitest/runner" - const importPattern = new RegExp(`from\\s*["']${pkg.replace('/', '\\/')}["']`, 'g'); - content = content.replace(importPattern, `from"${relativeToDist}/${distPath}"`); - } - - // Rewrite vitest/* subpath imports to use our dist files - // These are the actual entry points for vitest's browser-safe exports - const vitestSubpathRewrites: Record = { - 'vitest/browser': `${relativeToDist}/context.js`, // vitest/browser exports context API - 'vitest/internal/browser': `${relativeToDist}/browser.js`, - 'vitest/runners': `${relativeToDist}/runners.js`, - }; - for (const [specifier, destFile] of Object.entries(vitestSubpathRewrites)) { - const importPattern = new RegExp(`from\\s*["']${specifier.replace('/', '\\/')}["']`, 'g'); - content = content.replace(importPattern, `from"${destFile}"`); - } - - // Special handling for @vitest/browser/client -> our client.js - // This is needed because the browser client files import from @vitest/browser/client - const browserClientPattern = /from\s*["']@vitest\/browser\/client["']/g; - content = content.replace(browserClientPattern, `from"${relativeToDist}/client.js"`); - - // Handle imports from ./index.js which is Node.js-only code - // In browser context, 'server' should read from __vitest_browser_runner__ at runtime - // Replace: import{server}from'./index.js' with a browser-safe stub - const serverStub = `const server = { - get browser() { return window.__vitest_browser_runner__?.config?.browser?.name; }, - get config() { return window.__vitest_browser_runner__?.config || {}; }, - get commands() { return window.__vitest_browser_runner__?.commands || {}; }, - get provider() { return window.__vitest_browser_runner__?.provider; }, -};`; - content = content.replace( - /import\s*\{\s*server\s*\}\s*from\s*['"]\.\/index\.js['"];?/g, - serverStub, - ); - - // Remove side-effect imports from ./index.js (Node.js-only) - // Pattern: import'./index.js'; at the end of an import statement - content = content.replace(/import\s*['"]\.\/index\.js['"];?/g, ''); - - return content; - } - - async function copyDirRecursive(srcDir: string, destDir: string) { - const entries = await readdir(srcDir, { withFileTypes: true }); - - for (const entry of entries) { - const srcPath = join(srcDir, entry.name); - const destPath = join(destDir, entry.name); - - if (entry.isDirectory()) { - await mkdir(destPath, { recursive: true }); - await copyDirRecursive(srcPath, destPath); - } else if (entry.isFile()) { - // Skip if file already exists (our bundled code takes precedence) - try { - await stat(destPath); - continue; - } catch { - // File doesn't exist, copy it - } - await mkdir(parse(destPath).dir, { recursive: true }); - - // For JS files, rewrite imports; otherwise just copy - if (entry.name.endsWith('.js')) { - let content = await readFile(srcPath, 'utf-8'); - content = rewriteImports(content, destPath); - await writeFile(destPath, content, 'utf-8'); - } else { - await copyFile(srcPath, destPath); - } - copiedCount++; - } - } - } - - await copyDirRecursive(vitestBrowserDist, distDir); - - // Create dummy.js for placeholder exports (matchers, utils) - const dummyContent = '// Placeholder for browser compatibility\nexport {};\n'; - await writeFile(join(distDir, 'dummy.js'), dummyContent, 'utf-8'); - - console.log(` Copied ${copiedCount} files from @vitest/browser to dist`); - - // Create vendor stubs for browser packages that aren't bundled - // Other dist files reference these vendor paths but we don't bundle browser packages - // to avoid Node.js code leakage. Instead, we create stubs that re-export from actual dist files. - console.log(' Creating vendor stubs for browser packages...'); - const browserVendorStubs = [ - { - vendorFile: 'vitest_browser.mjs', - // vitest/browser exports the context API (page, server, userEvent) - content: `// Stub for browser context - re-exports from our context.js -export * from '../context.js'; -`, - }, - { - vendorFile: 'vitest_internal_browser.mjs', - // vitest/internal/browser is browser.js - content: `// Stub for internal browser API - re-exports from our browser.js -export * from '../browser.js'; -`, - }, - { - vendorFile: 'vitest_runners.mjs', - // vitest/runners - content: `// Stub for runners - re-exports from our runners.js -export * from '../runners.js'; -`, - }, - { - vendorFile: 'vitest_runner.mjs', - // @vitest/runner (note: singular, not plural like vitest_runners which is vitest/runners) - content: `// Stub for @vitest/runner - re-exports from our copied @vitest/runner -export * from '../@vitest/runner/index.js'; -`, - }, - ]; - - for (const { vendorFile, content } of browserVendorStubs) { - const stubPath = join(distDir, 'vendor', vendorFile); - await writeFile(stubPath, content, 'utf-8'); - } - console.log(` Created ${browserVendorStubs.length} vendor stubs`); -} - -/** - * Create browser/ directory at package root with context files. - * The package exports "./browser" pointing to these files: - * - browser/context.js: Runtime guard (throws if used outside browser mode) - * - browser/context.d.ts: Re-exports types from dist/@vitest/browser/context.d.ts - * - * These files are NOT tracked in git (.gitignore excludes browser/) - * but ARE included in the package (package.json files: ["browser/**"]) - */ -async function createBrowserEntryFiles() { - console.log('\nCreating browser/ entry files...'); - - const browserDir = resolve(projectDir, 'browser'); - await mkdir(browserDir, { recursive: true }); - - // 1. Copy context.js from @vitest/browser (runtime guard) - const srcContextJs = resolve(projectDir, 'node_modules/@vitest/browser/context.js'); - const destContextJs = join(browserDir, 'context.js'); - await copyFile(srcContextJs, destContextJs); - console.log(' Created browser/context.js'); - - // 2. Create context.d.ts that re-exports from our bundled types - const contextDtsContent = `// Re-export browser context types from bundled @vitest/browser package -// This provides: page, userEvent, server, commands, utils, locators, cdp, Locator, etc. -// The bundled context.d.ts has imports rewritten to point to our dist files -export * from '../dist/@vitest/browser/context.d.ts' -`; - const destContextDts = join(browserDir, 'context.d.ts'); - await writeFile(destContextDts, contextDtsContent, 'utf-8'); - console.log(' Created browser/context.d.ts'); -} - -/** - * Patch module augmentations in global.d.*.d.ts files. - * - * The original vitest types use module augmentation like: - * declare module "@vitest/expect" { interface Assertion { toMatchSnapshot: ... } } - * - * Since we bundle @vitest/* packages inside dist/@vitest/*, the bare specifier - * "@vitest/expect" doesn't exist as a package for consumers. This breaks the - * module augmentation - TypeScript can't find @vitest/expect to augment. - * - * The fix has two parts: - * 1. Change module augmentation to use relative paths that TypeScript CAN resolve: - * declare module "../@vitest/expect/index.js" { ... } - * 2. Merge augmented interface/type definitions into the target .d.ts files so that - * downstream DTS bundlers (rolldown) can resolve them without cross-file augmentation. - */ -async function patchModuleAugmentations() { - console.log('\nPatching module augmentations in global.d.*.d.ts files...'); - - const chunksDir = join(distDir, 'chunks'); - const globalDtsFiles: string[] = []; - - // Find all global.d.*.d.ts files - for await (const file of fsGlob(join(chunksDir, 'global.d.*.d.ts'))) { - globalDtsFiles.push(file); - } - - if (globalDtsFiles.length === 0) { - console.log(' No global.d.*.d.ts files found'); - return; - } - - // Module augmentation mappings: bare specifier -> [relative path, target .d.ts file] - const augmentationMappings: Record = { - '@vitest/expect': { - relativePath: '../@vitest/expect/index.js', - targetFile: join(distDir, '@vitest/expect/index.d.ts'), - }, - '@vitest/runner': { - relativePath: '../@vitest/runner/index.js', - targetFile: join(distDir, '@vitest/runner/utils.d.ts'), - }, - }; - - for (const file of globalDtsFiles) { - let content = await readFile(file, 'utf-8'); - let modified = false; - - for (const [bareSpecifier, { relativePath, targetFile }] of Object.entries( - augmentationMappings, - )) { - const oldPattern = `declare module "${bareSpecifier}"`; - - // Extract the augmentation block content using brace matching - const startIdx = content.indexOf(oldPattern); - const braceStart = startIdx !== -1 ? content.indexOf('{', startIdx) : -1; - if (braceStart === -1) { - continue; - } - - let depth = 0; - let braceEnd = -1; - for (let i = braceStart; i < content.length; i++) { - if (content[i] === '{') { - depth++; - } else if (content[i] === '}') { - depth--; - if (depth === 0) { - braceEnd = i; - break; - } - } - } - if (braceEnd === -1) { - continue; - } - - const innerContent = content.slice(braceStart + 1, braceEnd).trim(); - - // Merge only NEW type declarations into the target .d.ts file. - // Interfaces that already exist (e.g., ExpectStatic, Assertion, MatcherState) must NOT - // be re-declared, as that would shadow extends clauses and break call signatures. - if (innerContent && existsSync(targetFile)) { - let targetContent = await readFile(targetFile, 'utf-8'); - - // Extract individual interface blocks from the augmentation content - const interfaceRegex = /(?:export\s+)?interface\s+(\w+)(?:<[^>]*>)?\s*\{/g; - let match; - const newDeclarations: string[] = []; - - while ((match = interfaceRegex.exec(innerContent)) !== null) { - const name = match[1]; - // Only merge if this interface does NOT already exist in the target file. - // Check both direct declarations (interface Name) and re-exports (export type { Name }). - const hasDirectDecl = new RegExp(`\\binterface\\s+${name}\\b`).test(targetContent); - const exportTypeMatch = targetContent.match(/export\s+type\s*\{([^}]*)\}/); - const isReExported = - exportTypeMatch != null && new RegExp(`\\b${name}\\b`).test(exportTypeMatch[1]); - if (hasDirectDecl || isReExported) { - console.log( - ` Skipped existing interface "${name}" (already in ${basename(targetFile)})`, - ); - continue; - } - - // Extract this interface block using brace matching - const ifaceStart = match.index; - const ifaceBraceStart = innerContent.indexOf('{', ifaceStart); - let ifaceDepth = 0; - let ifaceBraceEnd = -1; - for (let i = ifaceBraceStart; i < innerContent.length; i++) { - if (innerContent[i] === '{') { - ifaceDepth++; - } else if (innerContent[i] === '}') { - ifaceDepth--; - if (ifaceDepth === 0) { - ifaceBraceEnd = i; - break; - } - } - } - if (ifaceBraceEnd === -1) { - continue; - } - - let block = innerContent.slice(ifaceStart, ifaceBraceEnd + 1).trim(); - if (!block.startsWith('export')) { - block = `export ${block}`; - } - newDeclarations.push(block); - console.log(` Merged new interface "${name}" into ${basename(targetFile)}`); - } - - if (newDeclarations.length > 0) { - targetContent += `\n// Merged from module augmentation: declare module "${bareSpecifier}"\n${newDeclarations.join('\n')}\n`; - await writeFile(targetFile, targetContent, 'utf-8'); - } - } - - // Rewrite declare module path to relative - const newPattern = `declare module "${relativePath}"`; - content = content.replaceAll(oldPattern, newPattern); - modified = true; - console.log(` Patched: ${bareSpecifier} -> ${relativePath} in ${basename(file)}`); - } - - if (modified) { - await writeFile(file, content, 'utf-8'); - } - } - - // Re-export BrowserCommands from context.d.ts (imported but not exported) - const contextDtsPath = join(distDir, '@vitest/browser/context.d.ts'); - if (existsSync(contextDtsPath)) { - let content = await readFile(contextDtsPath, 'utf-8'); - if ( - content.includes('BrowserCommands') && - !content.match(/export\s+(type\s+)?\{[^}]*BrowserCommands/) - ) { - content += '\nexport type { BrowserCommands };\n'; - await writeFile(contextDtsPath, content, 'utf-8'); - console.log(' Added BrowserCommands re-export to context.d.ts'); - } - } - - // Validate: ensure no duplicate top-level interface declarations were introduced by merging. - // Only count interfaces at the module scope (not nested inside declare global, namespace, etc.) - for (const [bareSpecifier, { targetFile }] of Object.entries(augmentationMappings)) { - if (!existsSync(targetFile)) { - continue; - } - const finalContent = await readFile(targetFile, 'utf-8'); - - // Extract top-level interface names by tracking brace depth - const topLevelInterfaces: string[] = []; - let depth = 0; - for (let i = 0; i < finalContent.length; i++) { - if (finalContent[i] === '{') { - depth++; - } else if (finalContent[i] === '}') { - depth--; - } else if (depth === 0) { - const remaining = finalContent.slice(i); - const m = remaining.match(/^interface\s+(\w+)/); - if (m) { - topLevelInterfaces.push(m[1]); - i += m[0].length - 1; - } - } - } - - const counts = new Map(); - for (const name of topLevelInterfaces) { - counts.set(name, (counts.get(name) || 0) + 1); - } - - for (const [name, count] of counts) { - if (count > 1) { - throw new Error( - `Interface "${name}" is declared ${count} times at top level in ${basename(targetFile)}. ` + - `Module augmentation merge for "${bareSpecifier}" likely created a duplicate ` + - `declaration that will shadow extends clauses and break type signatures.`, - ); - } - } - } -} - -/** - * Add triple-slash reference to @types/chai in @vitest/expect types. - * - * The @vitest/expect types use the Chai namespace (e.g., Chai.Assertion) which - * is defined in @types/chai. Without a reference directive, TypeScript won't - * automatically find the Chai types, causing the `not` property and other - * chai-specific features to be missing from the Assertion interface. - */ -async function patchChaiTypeReference() { - console.log('\nAdding @types/chai reference to @vitest/expect types...'); - - const expectIndexDts = join(distDir, '@vitest/expect/index.d.ts'); - - let content = await readFile(expectIndexDts, 'utf-8'); - - // Check if reference already exists - if (content.includes('/// \n${content}`; - - await writeFile(expectIndexDts, content, 'utf-8'); - console.log(' Added /// to @vitest/expect/index.d.ts'); -} - -/** - * Patch the vitest mocker to recognize @voidzero-dev packages as valid sources for vi/vitest. - * - * The mocker's hoistMocks function checks if `vi` is imported from the 'vitest' module. - * When users import from 'vite-plus/test' instead, the mocker doesn't - * recognize it and throws "There are some problems in resolving the mocks API". - * - * This patch modifies the equality check to also accept our package names: - * - vite-plus/test - * - @voidzero-dev/vite-plus-test - */ -async function patchMockerHoistedModule() { - console.log('\nPatching vitest mocker to recognize @voidzero-dev packages...'); - - // The hoistedModule check may be in node.js or chunk-hoistMocks.js depending on the vitest version - const candidateFiles = [ - join(distDir, '@vitest/mocker/node.js'), - join(distDir, '@vitest/mocker/chunk-hoistMocks.js'), - ]; - - // Find and replace the hoistedModule check - // Original: if (hoistedModule === source) { - // New: if (hoistedModule === source || source === "vite-plus/test" || source === "@voidzero-dev/vite-plus-test") { - const originalCheck = 'if (hoistedModule === source) {'; - const newCheck = - 'if (hoistedModule === source || source === "vite-plus/test" || source === "@voidzero-dev/vite-plus-test") {'; - - let patched = false; - for (const candidatePath of candidateFiles) { - let content: string; - try { - content = await readFile(candidatePath, 'utf-8'); - } catch { - continue; - } - if (content.includes(originalCheck)) { - content = content.replace(originalCheck, newCheck); - await writeFile(candidatePath, content, 'utf-8'); - console.log(` Patched hoistMocks to recognize @voidzero-dev packages in ${candidatePath}`); - patched = true; - break; - } - } - - if (!patched) { - throw new Error( - 'Could not find hoistedModule check to patch in @vitest/mocker. ' + - 'This likely means vitest code has changed and the patch needs to be updated.', - ); - } -} - -/** - * Patch vitest's ModuleRunnerTransform plugin to automatically add known - * packages that use `expect.extend()` internally to `server.deps.inline`. - * - * When third-party libraries (e.g., @testing-library/jest-dom) call - * `require('vitest').expect.extend(matchers)`, the npm override causes - * a separate module instance to be created, so matchers are registered - * on a different `chai` instance than the one used by the test runner. - * - * By inlining these packages via `server.deps.inline`, the Vite module - * runner processes them through its transform pipeline, ensuring they - * share the same module instance as the test runner. - * - * See: https://github.com/voidzero-dev/vite-plus/issues/897 - */ -async function patchServerDepsInline() { - console.log('\nPatching server.deps.inline for expect.extend compatibility...'); - - let cliApiChunk: string | undefined; - for await (const chunk of fsGlob(join(distDir, 'chunks/cli-api.*.js'))) { - cliApiChunk = chunk; - break; - } - - if (!cliApiChunk) { - throw new Error('cli-api chunk not found for patchServerDepsInline'); - } - - let content = await readFile(cliApiChunk, 'utf-8'); - - // Packages that internally call expect.extend() and break under npm override. - // These must be inlined so they share the same vitest module instance. - const inlinePackages = ['@testing-library/jest-dom', '@storybook/test', 'jest-extended']; - - // Find the configResolved handler in ModuleRunnerTransform (vitest:environments-module-runner) - // and inject our inline packages after the existing server.deps.inline logic. - const original = `if (external.length) { - testConfig.server.deps.external ??= []; - testConfig.server.deps.external.push(...external); - }`; - - const patched = `if (external.length) { - testConfig.server.deps.external ??= []; - testConfig.server.deps.external.push(...external); - } - // Auto-inline packages that use expect.extend() internally (#897) - // Only inline packages that are actually installed in the project. - if (testConfig.server.deps.inline !== true) { - testConfig.server.deps.inline ??= []; - if (Array.isArray(testConfig.server.deps.inline)) { - const _require = createRequire(config.root + "/package.json"); - const autoInline = ${JSON.stringify(inlinePackages)}; - for (const pkg of autoInline) { - if (testConfig.server.deps.inline.includes(pkg)) continue; - try { - _require.resolve(pkg); - testConfig.server.deps.inline.push(pkg); - } catch { - // Package not installed in the project — skip silently - } - } - } - }`; - - if (!content.includes(original)) { - throw new Error( - 'Could not find server.deps.external pattern in ' + - cliApiChunk + - '. This likely means vitest code has changed and the patch needs to be updated.', - ); - } - - content = content.replace(original, patched); - await writeFile(cliApiChunk, content, 'utf-8'); - console.log(` Added auto-inline for: ${inlinePackages.join(', ')}`); -} - -/** - * Create /plugins/* exports for all copied @vitest/* packages. - * This allows pnpm overrides to redirect @vitest/* imports to our copied versions. - * e.g., @vitest/runner -> vitest/plugins/runner - * @vitest/utils/error -> vitest/plugins/utils-error - */ -async function createPluginExports() { - console.log('\nCreating plugin exports for @vitest/* packages...'); - - const pluginsDir = join(distDir, 'plugins'); - // Clean up stale plugin files from previous builds - await rm(pluginsDir, { recursive: true, force: true }); - await mkdir(pluginsDir, { recursive: true }); - - const createdExports: Array<{ exportPath: string; shimFile: string }> = []; - - for (const [pkg, distPath] of Object.entries(VITEST_PACKAGE_TO_PATH)) { - // Only create exports for @vitest/* packages - if (!pkg.startsWith('@vitest/')) { - continue; - } - // Convert @vitest/runner -> runner, @vitest/utils/error -> utils-error - // @vitest/utils/source-map/node -> utils-source-map-node - const exportName = pkg.replace('@vitest/', '').replaceAll('/', '-'); - const shimFileName = `${exportName}.mjs`; - const shimPath = join(pluginsDir, shimFileName); - - // Create the shim file that re-exports everything from @vitest/ - const shimContent = `// Re-export ${pkg} from copied @vitest package -export * from '../${distPath}'; -`; - - await writeFile(shimPath, shimContent, 'utf-8'); - createdExports.push({ - exportPath: `./plugins/${exportName}`, - shimFile: `./dist/plugins/${shimFileName}`, - }); - console.log(` Created plugins/${shimFileName} -> ${distPath}`); - } - - return createdExports; -} - -/** - * Validate that all external dependencies in dist are listed in package.json - */ -async function validateExternalDeps() { - console.log('\nValidating external dependencies...'); - - // Collect all declared dependencies - const declaredDeps = new Set([ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ]); - - // Also include self-references - declaredDeps.add(pkg.name); - declaredDeps.add('vitest'); // Self-reference via vitest name - - // Collect all external specifiers from ALL dist files (including vendor) - const externalSpecifiers = new Map>(); // specifier -> files - - const allJsFiles = fsGlob(join(distDir, '**/*.{js,mjs,cjs}')); - - for await (const file of allJsFiles) { - const content = await readFile(file, 'utf-8'); - const isCjs = file.endsWith('.cjs'); - - // Parse with oxc-parser - const result = parseSync(file, content, { - sourceType: isCjs ? 'script' : 'module', - }); - - const specifiers = new Set(); - - // Collect ESM static imports - for (const imp of result.module.staticImports) { - specifiers.add(imp.moduleRequest.value); - } - - // Collect ESM static exports (re-exports) - for (const exp of result.module.staticExports) { - for (const entry of exp.entries) { - if (entry.moduleRequest) { - specifiers.add(entry.moduleRequest.value); - } - } - } - - // Collect dynamic imports (only string literals) - for (const dynImp of result.module.dynamicImports) { - const rawText = content.slice(dynImp.moduleRequest.start, dynImp.moduleRequest.end); - if ( - (rawText.startsWith("'") && rawText.endsWith("'")) || - (rawText.startsWith('"') && rawText.endsWith('"')) - ) { - specifiers.add(rawText.slice(1, -1)); - } - } - - // For CJS files, also scan for require() calls - if (isCjs) { - const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; - let match; - while ((match = requireRegex.exec(content)) !== null) { - specifiers.add(match[1]); - } - } - - // Filter and record external specifiers - for (const specifier of specifiers) { - // Skip relative paths - if (specifier.startsWith('.') || specifier.startsWith('/')) { - continue; - } - // Skip node built-ins - if (NODE_BUILTINS.has(specifier)) { - continue; - } - // Skip Node.js subpath imports - if (specifier.startsWith('#')) { - continue; - } - - // Get the package name (handle scoped packages and subpaths) - const packageName = getPackageName(specifier); - if (!packageName) { - continue; - } - - // Check if it's declared - if (declaredDeps.has(packageName)) { - continue; - } - // Check if it's in the blocklist (intentionally external) - if (EXTERNAL_BLOCKLIST.has(packageName) || EXTERNAL_BLOCKLIST.has(specifier)) { - continue; - } - - // Record undeclared external - if (!externalSpecifiers.has(specifier)) { - externalSpecifiers.set(specifier, new Set()); - } - externalSpecifiers.get(specifier)!.add(relative(distDir, file)); - } - } - - if (externalSpecifiers.size === 0) { - console.log(' ✓ All external dependencies are declared in package.json'); - return; - } - - // Group by package name - const byPackage = new Map>(); - for (const [specifier, _files] of externalSpecifiers) { - const packageName = getPackageName(specifier)!; - if (!byPackage.has(packageName)) { - byPackage.set(packageName, new Set()); - } - byPackage.get(packageName)!.add(specifier); - } - - console.log(`\n ⚠ Found ${byPackage.size} undeclared external dependencies:\n`); - for (const [packageName, specifiers] of byPackage.entries()) { - const files = externalSpecifiers.get([...specifiers][0])!; - console.log(` ${packageName}`); - for (const specifier of specifiers) { - if (specifier !== packageName) { - console.log(` - ${specifier}`); - } - } - console.log( - ` (used in: ${[...files].slice(0, 3).join(', ')}${files.size > 3 ? '...' : ''})`, - ); - } -} - -/** - * Extract the package name from a specifier (handles scoped packages and subpaths) - */ -function getPackageName(specifier: string): string | null { - // Scoped package: @scope/name or @scope/name/subpath - if (specifier.startsWith('@')) { - const parts = specifier.split('/'); - if (parts.length >= 2) { - return `${parts[0]}/${parts[1]}`; - } - return null; - } - // Regular package: name or name/subpath - const parts = specifier.split('/'); - return parts[0] || null; -} diff --git a/packages/test/package.json b/packages/test/package.json deleted file mode 100644 index df4bc6e2a2..0000000000 --- a/packages/test/package.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "name": "@voidzero-dev/vite-plus-test", - "version": "0.1.22", - "description": "The Unified Toolchain for the Web", - "homepage": "https://viteplus.dev/guide", - "bugs": { - "url": "https://github.com/voidzero-dev/vite-plus/issues" - }, - "license": "MIT", - "author": "VoidZero Inc.", - "repository": { - "type": "git", - "url": "git+https://github.com/voidzero-dev/vite-plus.git", - "directory": "packages/test" - }, - "files": [ - "*.cjs", - "*.cts", - "*.d.ts", - "*.mjs", - "browser/**", - "dist/**" - ], - "type": "module", - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "imports": { - "#module-evaluator": { - "types": "./dist/module-evaluator.d.ts", - "default": "./dist/module-evaluator.js" - }, - "#nodejs-worker-loader": "./dist/nodejs-worker-loader.js" - }, - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "browser": "./dist/index.js", - "node": "./dist/index-node.js", - "default": "./dist/index.js" - }, - "require": { - "types": "./index.d.cts", - "default": "./index.cjs" - } - }, - "./browser": { - "types": "./browser/context.d.ts", - "default": "./browser/context.js" - }, - "./package.json": "./package.json", - "./optional-types.js": { - "types": "./optional-types.d.ts" - }, - "./optional-runtime-types.js": { - "types": "./optional-runtime-types.d.ts" - }, - "./src/*": "./src/*", - "./globals": { - "types": "./globals.d.ts" - }, - "./jsdom": { - "types": "./jsdom.d.ts" - }, - "./importMeta": { - "types": "./importMeta.d.ts" - }, - "./import-meta": { - "types": "./import-meta.d.ts" - }, - "./node": { - "types": "./dist/node.d.ts", - "default": "./dist/node.js" - }, - "./internal/browser": { - "types": "./dist/browser.d.ts", - "default": "./dist/browser.js" - }, - "./runners": { - "types": "./dist/runners.d.ts", - "default": "./dist/runners.js" - }, - "./suite": { - "types": "./dist/suite.d.ts", - "default": "./dist/suite.js" - }, - "./environments": { - "types": "./dist/environments.d.ts", - "default": "./dist/environments.js" - }, - "./config": { - "types": "./config.d.ts", - "require": "./dist/config.cjs", - "default": "./dist/config.js" - }, - "./coverage": { - "types": "./coverage.d.ts", - "default": "./dist/coverage.js" - }, - "./reporters": { - "types": "./dist/reporters.d.ts", - "default": "./dist/reporters.js" - }, - "./snapshot": { - "types": "./dist/snapshot.d.ts", - "default": "./dist/snapshot.js" - }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "default": "./dist/runtime.js" - }, - "./worker": { - "types": "./worker.d.ts", - "default": "./dist/worker.js" - }, - "./browser-compat": { - "default": "./dist/browser-compat.js" - }, - "./client": { - "default": "./dist/client.js" - }, - "./context": { - "types": "./browser/context.d.ts", - "default": "./dist/@vitest/browser/context.js" - }, - "./browser/context": { - "types": "./browser/context.d.ts", - "default": "./dist/@vitest/browser/context.js" - }, - "./locators": { - "default": "./dist/locators.js" - }, - "./matchers": { - "default": "./dist/dummy.js" - }, - "./utils": { - "default": "./dist/dummy.js" - }, - "./browser-playwright": { - "types": "./dist/@vitest/browser-playwright/index.d.ts", - "default": "./dist/@vitest/browser-playwright/index.js" - }, - "./browser-webdriverio": { - "types": "./dist/@vitest/browser-webdriverio/index.d.ts", - "default": "./dist/@vitest/browser-webdriverio/index.js" - }, - "./browser-preview": { - "types": "./dist/@vitest/browser-preview/index.d.ts", - "default": "./dist/@vitest/browser-preview/index.js" - }, - "./browser/providers/playwright": { - "types": "./dist/@vitest/browser-playwright/index.d.ts", - "default": "./dist/@vitest/browser-playwright/index.js" - }, - "./browser/providers/webdriverio": { - "types": "./dist/@vitest/browser-webdriverio/index.d.ts", - "default": "./dist/@vitest/browser-webdriverio/index.js" - }, - "./browser/providers/preview": { - "types": "./dist/@vitest/browser-preview/index.d.ts", - "default": "./dist/@vitest/browser-preview/index.js" - }, - "./plugins/runner": { - "default": "./dist/plugins/runner.mjs" - }, - "./plugins/runner-utils": { - "default": "./dist/plugins/runner-utils.mjs" - }, - "./plugins/runner-types": { - "default": "./dist/plugins/runner-types.mjs" - }, - "./plugins/utils": { - "default": "./dist/plugins/utils.mjs" - }, - "./plugins/utils-source-map": { - "default": "./dist/plugins/utils-source-map.mjs" - }, - "./plugins/utils-source-map-node": { - "default": "./dist/plugins/utils-source-map-node.mjs" - }, - "./plugins/utils-error": { - "default": "./dist/plugins/utils-error.mjs" - }, - "./plugins/utils-helpers": { - "default": "./dist/plugins/utils-helpers.mjs" - }, - "./plugins/utils-display": { - "default": "./dist/plugins/utils-display.mjs" - }, - "./plugins/utils-timers": { - "default": "./dist/plugins/utils-timers.mjs" - }, - "./plugins/utils-highlight": { - "default": "./dist/plugins/utils-highlight.mjs" - }, - "./plugins/utils-offset": { - "default": "./dist/plugins/utils-offset.mjs" - }, - "./plugins/utils-resolver": { - "default": "./dist/plugins/utils-resolver.mjs" - }, - "./plugins/utils-serialize": { - "default": "./dist/plugins/utils-serialize.mjs" - }, - "./plugins/utils-constants": { - "default": "./dist/plugins/utils-constants.mjs" - }, - "./plugins/utils-diff": { - "default": "./dist/plugins/utils-diff.mjs" - }, - "./plugins/spy": { - "default": "./dist/plugins/spy.mjs" - }, - "./plugins/expect": { - "default": "./dist/plugins/expect.mjs" - }, - "./plugins/snapshot": { - "default": "./dist/plugins/snapshot.mjs" - }, - "./plugins/snapshot-environment": { - "default": "./dist/plugins/snapshot-environment.mjs" - }, - "./plugins/snapshot-manager": { - "default": "./dist/plugins/snapshot-manager.mjs" - }, - "./plugins/mocker": { - "default": "./dist/plugins/mocker.mjs" - }, - "./plugins/mocker-node": { - "default": "./dist/plugins/mocker-node.mjs" - }, - "./plugins/mocker-browser": { - "default": "./dist/plugins/mocker-browser.mjs" - }, - "./plugins/mocker-redirect": { - "default": "./dist/plugins/mocker-redirect.mjs" - }, - "./plugins/mocker-transforms": { - "default": "./dist/plugins/mocker-transforms.mjs" - }, - "./plugins/mocker-automock": { - "default": "./dist/plugins/mocker-automock.mjs" - }, - "./plugins/mocker-register": { - "default": "./dist/plugins/mocker-register.mjs" - }, - "./plugins/pretty-format": { - "default": "./dist/plugins/pretty-format.mjs" - }, - "./plugins/browser": { - "default": "./dist/plugins/browser.mjs" - }, - "./plugins/browser-context": { - "default": "./dist/plugins/browser-context.mjs" - }, - "./plugins/browser-client": { - "default": "./dist/plugins/browser-client.mjs" - }, - "./plugins/browser-locators": { - "default": "./dist/plugins/browser-locators.mjs" - }, - "./plugins/browser-playwright": { - "default": "./dist/plugins/browser-playwright.mjs" - }, - "./plugins/browser-webdriverio": { - "default": "./dist/plugins/browser-webdriverio.mjs" - }, - "./plugins/browser-preview": { - "default": "./dist/plugins/browser-preview.mjs" - } - }, - "scripts": { - "build": "oxnode -C dev ./build.ts" - }, - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@voidzero-dev/vite-plus-core": "workspace:*", - "es-module-lexer": "^1.7.0", - "obug": "^2.1.1", - "pixelmatch": "^7.1.0", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "std-env": "^4.0.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "ws": "^8.18.3" - }, - "devDependencies": { - "@blazediff/core": "1.9.1", - "@oxc-node/cli": "catalog:", - "@oxc-node/core": "catalog:", - "@vitest/browser": "4.1.7", - "@vitest/browser-playwright": "4.1.7", - "@vitest/browser-preview": "4.1.7", - "@vitest/browser-webdriverio": "4.1.7", - "@vitest/expect": "4.1.7", - "@vitest/mocker": "4.1.7", - "@vitest/pretty-format": "4.1.7", - "@vitest/runner": "4.1.7", - "@vitest/snapshot": "4.1.7", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", - "chai": "^6.2.1", - "convert-source-map": "^2.0.0", - "estree-walker": "^3.0.3", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "oxc-parser": "catalog:", - "oxfmt": "catalog:", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "rolldown": "workspace:*", - "rolldown-plugin-dts": "catalog:", - "tinyrainbow": "^3.1.0", - "vitest-dev": "^4.1.7", - "why-is-node-running": "^2.3.0" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/coverage-istanbul": "4.1.7", - "@vitest/coverage-v8": "4.1.7", - "@vitest/ui": "4.1.7", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "bundledVersions": { - "vitest": "4.1.7" - } -} diff --git a/packages/test/tsconfig.json b/packages/test/tsconfig.json deleted file mode 100644 index 62406593a8..0000000000 --- a/packages/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true - }, - "files": [], - "include": [], - "exclude": ["**/*"] -} diff --git a/packages/tools/package.json b/packages/tools/package.json index 139901c821..f9c7ae6d02 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -12,15 +12,16 @@ }, "dependencies": { "@yarnpkg/fslib": "catalog:", - "@yarnpkg/shell": "catalog:" + "@yarnpkg/shell": "catalog:", + "nanotar": "catalog:" }, "devDependencies": { "@oxc-node/cli": "catalog:", "@oxc-node/core": "catalog:", "@types/semver": "catalog:", - "@voidzero-dev/vite-plus-test": "workspace:*", "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 index e90d2192b4..4bdecf2b09 100644 --- a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap +++ b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap @@ -1,5 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`replaceUnstableOutput() > replace bracketed pnpm registry request error warning log 1`] = `"Progress: resolved"`; + exports[`replaceUnstableOutput() > replace date 1`] = ` "Start at " @@ -47,10 +49,7 @@ exports[`replaceUnstableOutput() > replace ignore pnpm request warning log 1`] = Packages:" `; -exports[`replaceUnstableOutput() > replace ignore tarball download average speed warning log 1`] = ` -"WARN  Tarball download average speed 29 KiB/s (size 56 KiB) is below 50 KiB/s: https://registry./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./undici/-/undici-7.16.0.tgz (GET)" -`; +exports[`replaceUnstableOutput() > replace ignore tarball download average speed warning log 1`] = `""`; exports[`replaceUnstableOutput() > replace pnpm progress plus markers with 1`] = ` "Scope: all workspace projects diff --git a/packages/tools/src/__tests__/utils.spec.ts b/packages/tools/src/__tests__/utils.spec.ts index 76fca7cf45..8ba3c767d0 100644 --- a/packages/tools/src/__tests__/utils.spec.ts +++ b/packages/tools/src/__tests__/utils.spec.ts @@ -3,7 +3,7 @@ import fs from 'node:fs'; import { homedir, tmpdir } from 'node:os'; import path from 'node:path'; -import { describe, expect, test } from '@voidzero-dev/vite-plus-test'; +import { describe, expect, test } from 'vitest'; import { isPassThroughEnv, replaceUnstableOutput } from '../utils.ts'; @@ -73,8 +73,8 @@ 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. +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: @@ -185,7 +185,7 @@ Done in 171ms using pnpm v10.16.1 test('replace ignore pnpm request warning log', () => { const output = ` Foo bar - WARN  Request took ms: https://registry.npmjs.org/testnpm2 + WARN Request took ms: https://registry.npmjs.org/testnpm2 Packages: `; expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); @@ -217,20 +217,31 @@ https://registry.yarnpkg.com/testnpm2/-/testnpm2-1.0.0.tgz 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/test-vite-plus-install 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', () => { + test('replace bracketed pnpm registry request error warning log', () => { + // pnpm v11 prints transient network errors with a bracketed `[WARN]` prefix + // and an ASCII space. Make sure the redactor drops these so single-run + // ECONNRESET/ENOTFOUND flakes don't get baked into snap.txt. 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) +[WARN] GET https://registry.npmjs.org/testnpm2 error (ECONNRESET). Will retry in 10 seconds. 2 retries left. +[WARN] GET https://registry.npmjs.org/testnpm2 error (ETIMEDOUT). Will retry in 10 seconds. 1 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) +`; + expect(replaceUnstableOutput(output)).toMatchSnapshot(); + }); + test('replace hash values', () => { const output = ` npm notice shasum: 65c35f9599054722ecde040abd4a19682a723cdc diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 5a6fa9b58e..d8847ff5fa 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -29,10 +29,14 @@ switch (subcommand) { const { brandVite } = await import('./brand-vite.ts'); brandVite(); break; + case 'repack-vite-tgz': + const { repackViteTgz } = await import('./repack-vite-tgz.ts'); + await repackViteTgz(); + break; default: console.error(`Unknown subcommand: ${subcommand}`); console.error( - 'Available subcommands: snap-test, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite', + 'Available subcommands: snap-test, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite, repack-vite-tgz', ); process.exit(1); } diff --git a/packages/tools/src/repack-vite-tgz.ts b/packages/tools/src/repack-vite-tgz.ts new file mode 100644 index 0000000000..c97cdf07e5 --- /dev/null +++ b/packages/tools/src/repack-vite-tgz.ts @@ -0,0 +1,81 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +import { createTarGzip, parseTarGzip, type TarFileInput } from 'nanotar'; + +interface PackageJson { + name?: string; + version?: string; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; +} + +function stripVitePlusCoreSelfRefs(pkg: PackageJson, newName: string): void { + for (const field of [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', + ] as const) { + const group = pkg[field]; + if (!group) { + continue; + } + for (const [key, value] of Object.entries(group)) { + if ( + (key === newName || key === '@voidzero-dev/vite-plus-core') && + typeof value === 'string' && + value.includes('@voidzero-dev/vite-plus-core') + ) { + delete group[key]; + } + } + } +} + +export async function repackViteTgz() { + const [inputPath, outputPath, newName, newVersion] = process.argv.slice(3); + + if (!inputPath || !outputPath || !newName) { + console.error('Usage: tool repack-vite-tgz [new-version]'); + process.exit(1); + } + + const inputBytes = await readFile(inputPath); + const entries = await parseTarGzip(inputBytes); + + let patched = 0; + const repacked: TarFileInput[] = entries.map((entry) => { + let data = entry.data; + if (entry.name === 'package/package.json' && data) { + const pkg = JSON.parse(new TextDecoder().decode(data)) as PackageJson; + pkg.name = newName; + if (newVersion) { + pkg.version = newVersion; + } + // Strip any self-ref (`vite: npm:@voidzero-dev/vite-plus-core@...`) + // injected by the workspace-level vite -> vite-plus-core override at + // pnpm pack time. Once we rename the tgz to "vite", this becomes a + // circular self-dependency that confuses pnpm's resolver and triggers a + // registry lookup for the alias target version. + stripVitePlusCoreSelfRefs(pkg, newName); + data = new TextEncoder().encode(JSON.stringify(pkg, null, 2) + '\n'); + patched += 1; + } + return { name: entry.name, data, attrs: entry.attrs }; + }); + + if (patched !== 1) { + console.error(`Expected exactly one package/package.json entry, found ${patched}`); + process.exit(1); + } + + const outBytes = await createTarGzip(repacked); + await writeFile(outputPath, outBytes); + console.log( + `Repacked ${inputPath} -> ${outputPath} (name=${newName}${ + newVersion ? `, version=${newVersion}` : '' + }, ${outBytes.byteLength} bytes)`, + ); +} diff --git a/packages/tools/src/snap-test.ts b/packages/tools/src/snap-test.ts index d234f2bdd7..b8c0bdde1b 100755 --- a/packages/tools/src/snap-test.ts +++ b/packages/tools/src/snap-test.ts @@ -265,9 +265,7 @@ function replaceInstalledCheckoutPackages(rootDir: string, repoRoot: string): vo const replacements = new Map([ ['node_modules/vite-plus', path.join(repoRoot, 'packages', 'cli')], ['node_modules/vite', path.join(repoRoot, 'packages', 'core')], - ['node_modules/vitest', path.join(repoRoot, 'packages', 'test')], ['node_modules/@voidzero-dev/vite-plus-core', path.join(repoRoot, 'packages', 'core')], - ['node_modules/@voidzero-dev/vite-plus-test', path.join(repoRoot, 'packages', 'test')], ]); while (stack.length > 0) { diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index f71e42d32d..96c2417a68 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -578,10 +578,12 @@ function mergePnpmWorkspaces( (rolldownVite.minimumReleaseAgeExclude || []).forEach((item) => excludeSet.add(item)); result.minimumReleaseAgeExclude = Array.from(excludeSet); - // Copy patchedDependencies from vite (with path prefix) - if (rolldownVite.patchedDependencies) { - result.patchedDependencies = {}; - for (const [dep, patchPath] of Object.entries(rolldownVite.patchedDependencies)) { + // Merge patchedDependencies: start with vite-plus root patches so they are + // preserved across syncs (e.g. patches/@vitest__mocker@*), then layer vite's + // patches with the `vite/` directory prefix on top. + if (main.patchedDependencies || rolldownVite.patchedDependencies) { + result.patchedDependencies = { ...main.patchedDependencies }; + for (const [dep, patchPath] of Object.entries(rolldownVite.patchedDependencies ?? {})) { // Prepend vite directory to patch paths result.patchedDependencies[dep] = patchPath.startsWith('./') ? `./${VITE_DIR}/${patchPath.slice(2)}` diff --git a/packages/tools/src/utils.ts b/packages/tools/src/utils.ts index 4ffbf0a5bd..ff52981c6f 100644 --- a/packages/tools/src/utils.ts +++ b/packages/tools/src/utils.ts @@ -80,12 +80,18 @@ export function replaceUnstableOutput(output: string, cwd?: string) { // ignore pnpm progress .replaceAll(/Progress: resolved \d+, reused \d+, downloaded \d+, added \d+\n/g, '') // ignore pnpm warn - .replaceAll(/ ?WARN\s+Skip\s+adding .+?\n/g, '') - .replaceAll(/ ?WARN\s+Request\s+took .+?\n/g, '') + // pnpm prefixes warnings with either an ASCII space or a thin space (U+2009), + // so accept both. Using `\s?` would also swallow newlines and could over-match. + .replaceAll(/[  ]?WARN\s+Skip\s+adding .+?\n/g, '') + .replaceAll(/[  ]?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 - .replaceAll(/ ?WARN\s+GET\s+https:\/\/registry\..+?\n/g, '') + // Matches both `WARN` (older pnpm) and `[WARN]` (newer pnpm) prefixes. + // Also drops transient network warnings like + // `[WARN] GET https://registry./pkg error (ECONNRESET). Will retry in 10 seconds. 2 retries left.` + // so single-run flakes don't get baked into snapshots. + .replaceAll(/[  ]?\[?WARN\]?\s+GET\s+https:\/\/registry\..+?\n/g, '') // ignore bun resolution progress (appears intermittently depending on cache state) .replaceAll(/Resolving dependencies\n/g, '') .replaceAll(/Resolved, downloaded and extracted \[\d+\]\n/g, '') @@ -138,7 +144,7 @@ export function replaceUnstableOutput(output: string, cwd?: string) { // ignore npm registry domain .replaceAll(/(https?:\/\/registry\.)[^/\s]+(\/?)/g, '$1$2') // ignore pnpm tarball download average speed warning log - .replaceAll(/ WARN  Tarball download average speed .+?\n/g, '') + .replaceAll(/[  ]?\[?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') diff --git a/patches/@vitest__mocker@4.1.7.patch b/patches/@vitest__mocker@4.1.7.patch new file mode 100644 index 0000000000..7168c0daac --- /dev/null +++ b/patches/@vitest__mocker@4.1.7.patch @@ -0,0 +1,18 @@ +diff --git a/dist/chunk-hoistMocks.js b/dist/chunk-hoistMocks.js +index 15cdee2083ce2cf400ecb84a68d2d3e608e6358e..a64794962cab9e6cad64665958168019d9eb61bc 100644 +--- a/dist/chunk-hoistMocks.js ++++ b/dist/chunk-hoistMocks.js +@@ -380,8 +380,11 @@ function hoistMocks(code, id, parse, options = {}) { + function defineImport(importNode) { + const source = importNode.source.value; + // always hoist vitest import to top of the file, so +- // "vi" helpers can access it +- if (hoistedModule === source) { ++ // "vi" helpers can access it. ++ // vite-plus patch: also recognize the public `vite-plus/test` ++ // specifier so `import { vi } from 'vite-plus/test'; vi.mock(...)` ++ // hoists correctly without an extra source rewrite. ++ if (hoistedModule === source || source === "vite-plus/test") { + hoistedModuleImported = true; + return; + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f8d0ca3e5..0dd64bfde2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,18 @@ catalogs: '@typescript/native-preview': specifier: 7.0.0-dev.20260122.2 version: 7.0.0-dev.20260122.2 + '@vitest/browser': + specifier: 4.1.7 + version: 4.1.7 + '@vitest/browser-playwright': + specifier: 4.1.7 + version: 4.1.7 + '@vitest/browser-preview': + specifier: 4.1.7 + version: 4.1.7 + '@vitest/browser-webdriverio': + specifier: 4.1.7 + version: 4.1.7 '@yarnpkg/fslib': specifier: ^3.1.3 version: 3.1.4 @@ -203,7 +215,7 @@ catalogs: version: 4.60.4 semver: specifier: ^7.8.0 - version: 7.8.0 + version: 7.8.1 serve-static: specifier: ^2.0.0 version: 2.2.0 @@ -221,7 +233,7 @@ catalogs: version: 2.0.1 terser: specifier: ^5.44.1 - version: 5.47.1 + version: 5.48.0 tinybench: specifier: ^6.0.0 version: 6.0.0 @@ -240,12 +252,15 @@ catalogs: validate-npm-package-name: specifier: ^7.0.2 version: 7.0.2 + vitest: + specifier: 4.1.7 + version: 4.1.7 vue: specifier: ^3.5.21 version: 3.5.34 ws: specifier: ^8.20.1 - version: 8.20.1 + version: 8.21.0 yaml: specifier: ^2.8.1 version: 2.9.0 @@ -256,13 +271,14 @@ catalogs: overrides: rolldown: workspace:rolldown@* vite: workspace:@voidzero-dev/vite-plus-core@* - vite-plus: workspace:* - vitest: workspace:@voidzero-dev/vite-plus-test@* - vitest-dev: npm:vitest@^4.1.7 + vite-plus: workspace:vite-plus@* packageExtensionsChecksum: sha256-Tldxs3DhJEw/FFBonUidqhCBqApA0zxQnop3Y+BTO3U= patchedDependencies: + '@vitest/mocker@4.1.7': + hash: adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f + path: patches/@vitest__mocker@4.1.7.patch chokidar@3.6.0: hash: 8a4f9e2b397e6034b91a0508faae3cecb97f222313faa129d7cb0eb71e9d0e84 path: vite/patches/chokidar@3.6.0.patch @@ -320,11 +336,11 @@ importers: specifier: workspace:@voidzero-dev/vite-plus-core@* version: link:packages/core vite-plus: - specifier: workspace:* + specifier: workspace:vite-plus@* version: link:packages/cli vitest: - specifier: workspace:@voidzero-dev/vite-plus-test@* - version: link:packages/test + specifier: 'catalog:' + version: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) zod: specifier: 'catalog:' version: 4.3.5 @@ -337,12 +353,27 @@ importers: '@oxlint/plugins': specifier: 'catalog:' version: 1.61.0 + '@vitest/browser': + specifier: 'catalog:' + version: 4.1.7(vite@packages+core)(vitest@4.1.7) + '@vitest/browser-playwright': + specifier: 'catalog:' + version: 4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7) + '@vitest/browser-preview': + specifier: 'catalog:' + version: 4.1.7(vite@packages+core)(vitest@4.1.7) + '@vitest/browser-webdriverio': + specifier: 'catalog:' + version: 4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1) '@voidzero-dev/vite-plus-core': specifier: workspace:* version: link:../core - '@voidzero-dev/vite-plus-test': - specifier: workspace:* - version: link:../test + es-module-lexer: + specifier: ^1.7.0 + version: 1.7.0 + oxc-parser: + specifier: 'catalog:' + version: 0.133.0 oxfmt: specifier: 'catalog:' version: 0.52.0(vite-plus@packages+cli) @@ -352,6 +383,9 @@ importers: oxlint-tsgolint: specifier: 'catalog:' version: 0.23.0 + vitest: + specifier: 'catalog:' + version: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) devDependencies: '@napi-rs/cli': specifier: 'catalog:' @@ -415,7 +449,7 @@ importers: version: 0.25.1(@typescript/native-preview@7.0.0-dev.20260122.2)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@rolldown+packages+rolldown)(typescript@6.0.2) semver: specifier: 'catalog:' - version: 7.8.0 + version: 7.8.1 tsdown: specifier: 'catalog:' version: 0.22.0(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.22.0)(@tsdown/exe@0.22.0)(@typescript/native-preview@7.0.0-dev.20260122.2)(@vitejs/devtools@0.2.0(@pnpm/logger@1001.0.1)(typescript@6.0.2)(vite@packages+core))(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(publint@0.3.21)(tsx@4.22.3)(typescript@6.0.2)(unplugin-unused@0.5.6)(unrun@0.3.0) @@ -481,7 +515,7 @@ importers: version: 5.0.1(postcss@8.5.15) terser: specifier: ^5.16.0 - version: 5.47.1 + version: 5.48.0 tsx: specifier: ^4.8.1 version: 4.22.3 @@ -557,7 +591,7 @@ importers: version: 3.7.1(picomatch@4.0.4)(rollup@4.60.4) semver: specifier: ^7.7.3 - version: 7.8.0 + version: 7.8.1 tinyglobby: specifier: ^0.2.15 version: 0.2.16 @@ -600,160 +634,6 @@ importers: specifier: 'catalog:' version: 0.22.0(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.22.0)(@tsdown/exe@0.22.0)(@typescript/native-preview@7.0.0-dev.20260122.2)(@vitejs/devtools@0.2.0(@pnpm/logger@1001.0.1)(typescript@6.0.2)(vite@packages+core))(publint@0.3.21)(tsx@4.22.3)(typescript@6.0.2)(unplugin-unused@0.5.6)(unrun@0.3.0) - packages/test: - dependencies: - '@edge-runtime/vm': - specifier: '*' - version: 5.0.0 - '@opentelemetry/api': - specifier: ^1.9.0 - version: 1.9.0 - '@standard-schema/spec': - specifier: ^1.1.0 - version: 1.1.0 - '@types/chai': - specifier: ^5.2.2 - version: 5.2.3 - '@types/node': - specifier: ^20.0.0 || ^22.0.0 || >=24.0.0 - version: 24.12.4 - '@vitest/coverage-istanbul': - specifier: 4.1.7 - version: 4.1.7(vitest@4.1.7) - '@vitest/coverage-v8': - specifier: 4.1.7 - version: 4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7) - '@vitest/ui': - specifier: 4.1.7 - version: 4.1.7(vitest@4.1.7) - '@voidzero-dev/vite-plus-core': - specifier: workspace:* - version: link:../core - es-module-lexer: - specifier: ^1.7.0 - version: 1.7.0 - happy-dom: - specifier: '*' - version: 20.0.10 - jsdom: - specifier: '*' - version: 27.2.0 - obug: - specifier: ^2.1.1 - version: 2.1.1 - pixelmatch: - specifier: ^7.1.0 - version: 7.1.0 - pngjs: - specifier: ^7.0.0 - version: 7.0.0 - sirv: - specifier: ^3.0.2 - version: 3.0.2(patch_hash=c07c56eb72faea34341d465cde2314e89db472106ed378181e3447893af6bf95) - std-env: - specifier: ^4.0.0 - version: 4.0.0 - tinybench: - specifier: ^2.9.0 - version: 2.9.0 - tinyexec: - specifier: ^1.0.2 - version: 1.1.2 - tinyglobby: - specifier: ^0.2.15 - version: 0.2.16 - vite: - specifier: workspace:@voidzero-dev/vite-plus-core@* - version: link:../core - ws: - specifier: ^8.18.3 - version: 8.20.1 - devDependencies: - '@blazediff/core': - specifier: 1.9.1 - version: 1.9.1 - '@oxc-node/cli': - specifier: 'catalog:' - version: 0.1.0 - '@oxc-node/core': - specifier: 'catalog:' - version: 0.1.0 - '@vitest/browser': - specifier: 4.1.7 - version: 4.1.7(vite@packages+core)(vitest@4.1.7) - '@vitest/browser-playwright': - specifier: 4.1.7 - version: 4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7) - '@vitest/browser-preview': - specifier: 4.1.7 - version: 4.1.7(vite@packages+core)(vitest@4.1.7) - '@vitest/browser-webdriverio': - specifier: 4.1.7 - version: 4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1) - '@vitest/expect': - specifier: 4.1.7 - version: 4.1.7 - '@vitest/mocker': - specifier: 4.1.7 - version: 4.1.7(vite@packages+core) - '@vitest/pretty-format': - specifier: 4.1.7 - version: 4.1.7 - '@vitest/runner': - specifier: 4.1.7 - version: 4.1.7 - '@vitest/snapshot': - specifier: 4.1.7 - version: 4.1.7 - '@vitest/spy': - specifier: 4.1.7 - version: 4.1.7 - '@vitest/utils': - specifier: 4.1.7 - version: 4.1.7 - chai: - specifier: ^6.2.1 - version: 6.2.2 - convert-source-map: - specifier: ^2.0.0 - version: 2.0.0 - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 - expect-type: - specifier: ^1.2.2 - version: 1.3.0 - magic-string: - specifier: ^0.30.21 - version: 0.30.21 - oxc-parser: - specifier: 'catalog:' - version: 0.133.0 - oxfmt: - specifier: 'catalog:' - version: 0.52.0(vite-plus@packages+cli) - pathe: - specifier: ^2.0.3 - version: 2.0.3 - picomatch: - specifier: ^4.0.3 - version: 4.0.4 - rolldown: - specifier: workspace:rolldown@* - version: link:../../rolldown/packages/rolldown - rolldown-plugin-dts: - specifier: 'catalog:' - version: 0.25.1(@typescript/native-preview@7.0.0-dev.20260122.2)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@rolldown+packages+rolldown)(typescript@6.0.2) - tinyrainbow: - specifier: ^3.1.0 - version: 3.1.0 - vitest-dev: - specifier: npm:vitest@^4.1.7 - version: vitest@4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) - why-is-node-running: - specifier: ^2.3.0 - version: 2.3.0 - packages/tools: dependencies: '@yarnpkg/fslib': @@ -762,6 +642,9 @@ importers: '@yarnpkg/shell': specifier: 'catalog:' version: 4.1.3(typanion@3.14.0) + nanotar: + specifier: 'catalog:' + version: 0.3.0 devDependencies: '@oxc-node/cli': specifier: 'catalog:' @@ -772,15 +655,15 @@ importers: '@types/semver': specifier: 'catalog:' version: 7.7.1 - '@voidzero-dev/vite-plus-test': - specifier: workspace:* - version: link:../test minimatch: specifier: 'catalog:' version: 10.2.4 semver: specifier: 'catalog:' - version: 7.8.0 + version: 7.8.1 + vitest: + specifier: 'catalog:' + version: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) yaml: specifier: 'catalog:' version: 2.9.0 @@ -801,7 +684,7 @@ importers: version: 2.1.1 knip: specifier: ^6.13.1 - version: 6.14.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + version: 6.14.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) playwright-chromium: specifier: ^1.60.0 version: 1.60.0 @@ -818,7 +701,7 @@ importers: specifier: 'catalog:' version: 6.0.2 vite-plus: - specifier: workspace:* + specifier: workspace:vite-plus@* version: link:../packages/cli rolldown/packages/bench: @@ -1005,7 +888,7 @@ importers: version: 2.0.1 terser: specifier: 'catalog:' - version: 5.47.1 + version: 5.48.0 rolldown/packages/test-dev-server: dependencies: @@ -1023,7 +906,7 @@ importers: version: 2.2.0 ws: specifier: 'catalog:' - version: 8.20.1 + version: 8.21.0 devDependencies: '@types/connect': specifier: 'catalog:' @@ -1134,8 +1017,8 @@ importers: specifier: workspace:@voidzero-dev/vite-plus-core@* version: link:../packages/core vitest: - specifier: workspace:@voidzero-dev/vite-plus-test@* - version: link:../packages/test + specifier: ^4.1.7 + version: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) vite/packages/create-vite: devDependencies: @@ -1198,7 +1081,7 @@ importers: version: 6.15.1 terser: specifier: ^5.16.0 - version: 5.47.1 + version: 5.48.0 devDependencies: acorn: specifier: ^8.16.0 @@ -1296,7 +1179,7 @@ importers: version: 0.4.3 baseline-browser-mapping: specifier: ^2.10.31 - version: 2.10.31 + version: 2.10.32 cac: specifier: ^7.0.0 version: 7.0.0 @@ -1410,13 +1293,13 @@ importers: version: 3.1.0 terser: specifier: ^5.47.1 - version: 5.47.1 + version: 5.48.0 ufo: specifier: ^1.6.4 version: 1.6.4 ws: specifier: ^8.20.1 - version: 8.20.1 + version: 8.21.0 optionalDependencies: fsevents: specifier: ~2.3.3 @@ -2059,10 +1942,6 @@ packages: resolution: {integrity: sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==} engines: {node: ^22.18.0 || >=24.11.0} - '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} - '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} @@ -2523,10 +2402,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -5114,37 +4989,23 @@ packages: resolution: {integrity: sha512-OlTlJej7YN6VwV7zJJoNeaCsctF+JXpzpZ4oBHUbrQFfIq+0KW2f07rprCLh9N/zRIZ0v4Mchn1QDDmWMUhPKw==} peerDependencies: playwright: '*' - vitest: workspace:@voidzero-dev/vite-plus-test@* + vitest: 4.1.7 '@vitest/browser-preview@4.1.7': resolution: {integrity: sha512-zwxt6Nu9naWZnWFK5ZBeevQ6yh4r92cRuP3Bm1rF/u4CaBevpVCaKygWaKKhvf6kupnbARQtPMWkqmU2yvUEuw==} peerDependencies: - vitest: workspace:@voidzero-dev/vite-plus-test@* + vitest: 4.1.7 '@vitest/browser-webdriverio@4.1.7': resolution: {integrity: sha512-3CeH4+nO9RBwGPV+hiSnaqUYJM18wtPu52CzIU6TjVELQXyEiouBugjqZI/4l16wbQbd4HzEcyI+YKH48x+SpA==} peerDependencies: - vitest: workspace:@voidzero-dev/vite-plus-test@* + vitest: 4.1.7 webdriverio: '*' '@vitest/browser@4.1.7': resolution: {integrity: sha512-N2JFGfXoEGVAut+kHeru9dD4BUMq/q5xDvBARNl0tUsly3m5KglLOu8VO/6MkDfOlgxXTycojkt6gBKsuyR+IQ==} peerDependencies: - vitest: workspace:@voidzero-dev/vite-plus-test@* - - '@vitest/coverage-istanbul@4.1.7': - resolution: {integrity: sha512-EbruXy+E9MJk+y7sFzriYfoI4JP2Ow+SyWDkewFOWFjzrbQBHlEgi6dGE7pxge8Z+W+7oJOxAVVb6mQHKCCZlw==} - peerDependencies: - vitest: workspace:@voidzero-dev/vite-plus-test@* - - '@vitest/coverage-v8@4.1.7': - resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} - peerDependencies: - '@vitest/browser': 4.1.7 - vitest: workspace:@voidzero-dev/vite-plus-test@* - peerDependenciesMeta: - '@vitest/browser': - optional: true + vitest: 4.1.7 '@vitest/expect@4.1.7': resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} @@ -5172,11 +5033,6 @@ packages: '@vitest/spy@4.1.7': resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/ui@4.1.7': - resolution: {integrity: sha512-TP6utB2yX6rsJNVRo2qAlsi48i1YwFTrLV2tnTtWqJaYX7m4lRCCLirZBjU6xC5m0RsPHr+L2+N+eIPhgEzFfw==} - peerDependencies: - vitest: workspace:@voidzero-dev/vite-plus-test@* - '@vitest/utils@4.1.7': resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} @@ -5399,9 +5255,6 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} - ast-v8-to-istanbul@1.0.0: - resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - ast-walker-scope@0.8.3: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} @@ -5484,8 +5337,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.31: - resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} engines: {node: '>=6.0.0'} hasBin: true @@ -6592,9 +6445,6 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - htmlfy@0.8.1: resolution: {integrity: sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==} @@ -6815,18 +6665,6 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -6834,9 +6672,6 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - js-tokens@10.0.0: - resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6906,8 +6741,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - knip@6.14.1: - resolution: {integrity: sha512-SN3Ly0ixzj5CQkY/rc4OPHpWrCC0XRIIjgdP76G9Cni5k72ur5jBYOyvJuF5oPTM14v8eHcMUgPbElHa+lnR0g==} + knip@6.14.2: + resolution: {integrity: sha512-Vg3JhIINjZew1I7qAFI4UHemW1mc4azP/BxJvsq9eGDfxpGO7oVCuD/bsWkog9TO/ZwwJeAeOMFZ1kd9jnY9+Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7114,17 +6949,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - matcher-collection@2.0.1: resolution: {integrity: sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==} engines: {node: 6.* || 8.* || >= 10.*} @@ -7386,7 +7214,7 @@ packages: hasBin: true peerDependencies: svelte: ^5.0.0 - vite-plus: workspace:* + vite-plus: workspace:vite-plus@* peerDependenciesMeta: svelte: optional: true @@ -7403,7 +7231,7 @@ packages: hasBin: true peerDependencies: oxlint-tsgolint: '>=0.22.1' - vite-plus: workspace:* + vite-plus: workspace:vite-plus@* peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -7544,10 +7372,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pixelmatch@7.1.0: - resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} - hasBin: true - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -8081,8 +7905,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -8226,8 +8050,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - srvx@0.11.15: - resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} + srvx@0.11.16: + resolution: {integrity: sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==} engines: {node: '>=20.16.0'} hasBin: true @@ -8377,8 +8201,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - terser@5.47.1: - resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} engines: {node: '>=10'} hasBin: true @@ -8950,8 +8774,8 @@ packages: resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==} engines: {node: '>=16.14'} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9027,7 +8851,8 @@ packages: snapshots: - '@acemir/cssom@0.9.24': {} + '@acemir/cssom@0.9.24': + optional: true '@adobe/css-tools@4.3.3': {} @@ -9040,7 +8865,7 @@ snapshots: cjs-module-lexer: 1.4.3 fflate: 0.8.2 lru-cache: 11.2.7 - semver: 7.8.0 + semver: 7.8.1 typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 @@ -9051,6 +8876,7 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 lru-cache: 11.2.7 + optional: true '@asamuzakjp/dom-selector@6.7.4': dependencies: @@ -9059,8 +8885,10 @@ snapshots: css-tree: 3.1.0 is-potential-custom-element-name: 1.0.1 lru-cache: 11.2.7 + optional: true - '@asamuzakjp/nwsapi@2.3.9': {} + '@asamuzakjp/nwsapi@2.3.9': + optional: true '@ast-grep/napi-darwin-arm64@0.42.1': optional: true @@ -9822,8 +9650,6 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.5 '@babel/helper-validator-identifier': 8.0.0-rc.5 - '@bcoe/v8-coverage@1.0.2': {} - '@blazediff/core@1.9.1': {} '@braidai/lang@1.1.2': {} @@ -9857,17 +9683,19 @@ snapshots: dependencies: '@simple-libs/child-process-utils': 1.0.1 '@simple-libs/stream-utils': 1.2.0 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 - '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@5.1.0': + optional: true '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + optional: true '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: @@ -9875,20 +9703,26 @@ snapshots: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + optional: true '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-tokenizer': 3.0.4 + optional: true - '@csstools/css-syntax-patches-for-csstree@1.0.17': {} + '@csstools/css-syntax-patches-for-csstree@1.0.17': + optional: true - '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@3.0.4': + optional: true - '@edge-runtime/primitives@6.0.0': {} + '@edge-runtime/primitives@6.0.0': + optional: true '@edge-runtime/vm@5.0.0': dependencies: '@edge-runtime/primitives': 6.0.0 + optional: true '@emnapi/core@1.10.0': dependencies: @@ -10321,8 +10155,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@istanbuljs/schema@0.1.3': {} - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10363,7 +10195,7 @@ snapshots: es-toolkit: 1.42.0 js-yaml: 4.1.1 obug: 2.1.1 - semver: 7.8.0 + semver: 7.8.1 typanion: 3.14.0 optionalDependencies: '@emnapi/runtime': 1.10.0 @@ -10395,7 +10227,7 @@ snapshots: es-toolkit: 1.42.0 js-yaml: 4.1.1 obug: 2.1.1 - semver: 7.8.0 + semver: 7.8.1 typanion: 3.14.0 optionalDependencies: '@emnapi/runtime': 1.10.0 @@ -10841,7 +10673,8 @@ snapshots: '@octokit/request-error': 7.1.0 '@octokit/webhooks-methods': 6.0.0 - '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api@1.9.0': + optional: true '@oxc-node/cli@0.1.0': dependencies: @@ -11611,7 +11444,7 @@ snapshots: '@pnpm/logger': 1001.0.1 '@pnpm/semver.peer-range': 1000.0.0 '@pnpm/types': 1001.3.0 - semver: 7.8.0 + semver: 7.8.1 '@pnpm/read-project-manifest@1001.2.6(@pnpm/logger@1001.0.1)': dependencies: @@ -11632,7 +11465,7 @@ snapshots: '@pnpm/semver.peer-range@1000.0.0': dependencies: - semver: 7.8.0 + semver: 7.8.1 '@pnpm/text.comments-parser@1000.0.0': dependencies: @@ -11664,7 +11497,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.8.0 + semver: 7.8.1 tar-fs: 3.1.1 yargs: 17.7.2 transitivePeerDependencies: @@ -11859,7 +11692,7 @@ snapshots: '@tsdown/exe@0.22.0(tsdown@0.22.0)': dependencies: obug: 2.1.1 - semver: 7.8.0 + semver: 7.8.1 tinyexec: 1.1.2 tsdown: 0.22.0(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.22.0)(@tsdown/exe@0.22.0)(@typescript/native-preview@7.0.0-dev.20260122.2)(@vitejs/devtools@0.2.0(@pnpm/logger@1001.0.1)(typescript@6.0.2)(vite@packages+core))(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(publint@0.3.21)(tsx@4.22.3)(typescript@6.0.2)(unplugin-unused@0.5.6)(unrun@0.3.0) @@ -11997,7 +11830,8 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} - '@types/whatwg-mimetype@3.0.2': {} + '@types/whatwg-mimetype@3.0.2': + optional: true '@types/which@2.0.2': {} @@ -12078,7 +11912,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 - semver: 7.8.0 + semver: 7.8.1 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 @@ -12255,7 +12089,7 @@ snapshots: unconfig: 7.5.0 unstorage: 1.17.5 vue-virtual-scroller: 3.0.4(vue@3.5.34(typescript@6.0.2)) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12308,7 +12142,7 @@ snapshots: unconfig: 7.5.0 unstorage: 1.17.5 vue-virtual-scroller: 3.0.4(vue@3.5.34(typescript@6.0.2)) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12354,7 +12188,7 @@ snapshots: tinyexec: 1.1.2 vite: link:packages/core vue: 3.5.34(typescript@6.0.2) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12398,7 +12232,7 @@ snapshots: tinyexec: 1.1.2 vite: link:packages/core vue: 3.5.34(typescript@6.0.2) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12434,7 +12268,7 @@ snapshots: picocolors: 1.1.1 prompts: 2.4.2 publint: 0.3.21 - semver: 7.8.0 + semver: 7.8.1 tinyexec: 1.1.2 transitivePeerDependencies: - conventional-commits-filter @@ -12442,10 +12276,10 @@ snapshots: '@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7)': dependencies: '@vitest/browser': 4.1.7(vite@packages+core)(vitest@4.1.7) - '@vitest/mocker': 4.1.7(vite@packages+core) + '@vitest/mocker': 4.1.7(patch_hash=adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f)(vite@packages+core) playwright: 1.57.0 tinyrainbow: 3.1.0 - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) + vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) transitivePeerDependencies: - bufferutil - msw @@ -12457,7 +12291,7 @@ snapshots: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/browser': 4.1.7(vite@packages+core)(vitest@4.1.7) - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) + vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) transitivePeerDependencies: - bufferutil - msw @@ -12467,7 +12301,7 @@ snapshots: '@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1)': dependencies: '@vitest/browser': 4.1.7(vite@packages+core)(vitest@4.1.7) - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) + vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) webdriverio: 9.20.1 transitivePeerDependencies: - bufferutil @@ -12478,52 +12312,20 @@ snapshots: '@vitest/browser@4.1.7(vite@packages+core)(vitest@4.1.7)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.7(vite@packages+core) + '@vitest/mocker': 4.1.7(patch_hash=adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f)(vite@packages+core) '@vitest/utils': 4.1.7 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2(patch_hash=c07c56eb72faea34341d465cde2314e89db472106ed378181e3447893af6bf95) tinyrainbow: 3.1.0 - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) - ws: 8.20.1 + vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) + ws: 8.21.0 transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/coverage-istanbul@4.1.7(vitest@4.1.7)': - dependencies: - '@babel/core': 7.29.0 - '@istanbuljs/schema': 0.1.3 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - tinyrainbow: 3.1.0 - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) - transitivePeerDependencies: - - supports-color - - '@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7)': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.7 - ast-v8-to-istanbul: 1.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.0.0 - tinyrainbow: 3.1.0 - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) - optionalDependencies: - '@vitest/browser': 4.1.7(vite@packages+core)(vitest@4.1.7) - '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 @@ -12533,7 +12335,7 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@packages+core)': + '@vitest/mocker@4.1.7(patch_hash=adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f)(vite@packages+core)': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 @@ -12559,17 +12361,6 @@ snapshots: '@vitest/spy@4.1.7': {} - '@vitest/ui@4.1.7(vitest@4.1.7)': - dependencies: - '@vitest/utils': 4.1.7 - fflate: 0.8.2 - flatted: 3.4.2 - pathe: 2.0.3 - sirv: 3.0.2(patch_hash=c07c56eb72faea34341d465cde2314e89db472106ed378181e3447893af6bf95) - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vitest: 4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core) - '@vitest/utils@4.1.7': dependencies: '@vitest/pretty-format': 4.1.7 @@ -12862,12 +12653,6 @@ snapshots: dependencies: tslib: 2.8.1 - ast-v8-to-istanbul@1.0.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - ast-walker-scope@0.8.3: dependencies: '@babel/parser': 7.29.3 @@ -12946,7 +12731,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.31: {} + baseline-browser-mapping@2.10.32: {} basic-ftp@5.0.5: {} @@ -12955,6 +12740,7 @@ snapshots: bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 + optional: true binary-extensions@2.3.0: {} @@ -13031,7 +12817,7 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.31 + baseline-browser-mapping: 2.10.32 caniuse-lite: 1.0.30001785 electron-to-chromium: 1.5.331 node-releases: 2.0.37 @@ -13229,7 +13015,7 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.8 meow: 13.2.0 - semver: 7.8.0 + semver: 7.8.1 conventional-changelog@7.2.0(conventional-commits-filter@5.0.0): dependencies: @@ -13308,6 +13094,7 @@ snapshots: dependencies: mdn-data: 2.12.2 source-map-js: 1.2.1 + optional: true css-value@0.0.1: {} @@ -13320,6 +13107,7 @@ snapshots: '@asamuzakjp/css-color': 4.1.0 '@csstools/css-syntax-patches-for-csstree': 1.0.17 css-tree: 3.1.0 + optional: true csstype@3.2.3: {} @@ -13337,6 +13125,7 @@ snapshots: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 + optional: true debug@2.6.9: dependencies: @@ -13352,7 +13141,8 @@ snapshots: decamelize@6.0.1: {} - decimal.js@10.6.0: {} + decimal.js@10.6.0: + optional: true decircular@0.1.1: {} @@ -13404,7 +13194,7 @@ snapshots: mrmime: 2.0.1 pathe: 2.0.3 valibot: 1.4.0(typescript@6.0.2) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - crossws @@ -13421,7 +13211,7 @@ snapshots: nostics: 0.2.0 pathe: 2.0.3 valibot: 1.4.0(typescript@6.0.2) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - crossws @@ -13601,7 +13391,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) - semver: 7.8.0 + semver: 7.8.1 eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -13627,7 +13417,7 @@ snapshots: eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.4 - semver: 7.8.0 + semver: 7.8.1 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: @@ -13645,7 +13435,7 @@ snapshots: globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: ts-declaration-location: 1.0.7(typescript@6.0.2) typescript: 6.0.2 @@ -14055,7 +13845,7 @@ snapshots: h3@2.0.1-rc.22: dependencies: rou3: 0.8.1 - srvx: 0.11.15 + srvx: 0.11.16 handlebars@4.7.8: dependencies: @@ -14071,6 +13861,7 @@ snapshots: '@types/node': 20.19.25 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 + optional: true has-flag@3.0.0: {} @@ -14106,8 +13897,7 @@ snapshots: html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 - - html-escaper@2.0.2: {} + optional: true htmlfy@0.8.1: {} @@ -14239,7 +14029,8 @@ snapshots: is-plain-obj@4.1.0: {} - is-potential-custom-element-name@1.0.1: {} + is-potential-custom-element-name@1.0.1: + optional: true is-reference@1.2.1: dependencies: @@ -14279,19 +14070,6 @@ snapshots: isexe@3.1.1: {} - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -14300,8 +14078,6 @@ snapshots: jiti@2.7.0: {} - js-tokens@10.0.0: {} - js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -14337,12 +14113,13 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 - ws: 8.20.1 + ws: 8.21.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + optional: true jsesc@0.5.0: {} @@ -14379,7 +14156,7 @@ snapshots: kleur@3.0.3: {} - knip@6.14.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + knip@6.14.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: fdir: 6.5.0(picomatch@4.0.4) formatly: 0.3.0 @@ -14602,28 +14379,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.5.2: - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - make-dir@2.1.0: dependencies: pify: 4.0.1 semver: 5.7.2 optional: true - make-dir@4.0.0: - dependencies: - semver: 7.8.0 - matcher-collection@2.0.1: dependencies: '@types/minimatch': 3.0.5 minimatch: 3.1.5 - mdn-data@2.12.2: {} + mdn-data@2.12.2: + optional: true meow@13.2.0: {} @@ -14758,13 +14526,13 @@ snapshots: normalize-package-data@7.0.1: dependencies: hosted-git-info: 8.1.0 - semver: 7.8.0 + semver: 7.8.1 validate-npm-package-license: 3.0.4 normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.2 - semver: 7.8.0 + semver: 7.8.1 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -15237,10 +15005,6 @@ snapshots: pirates@4.0.7: {} - pixelmatch@7.1.0: - dependencies: - pngjs: 7.0.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -15527,7 +15291,8 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: {} + require-from-string@2.0.2: + optional: true resolve-from@4.0.0: {} @@ -15783,6 +15548,7 @@ snapshots: saxes@6.0.0: dependencies: xmlchars: 2.2.0 + optional: true scheduler@0.27.0: {} @@ -15799,7 +15565,7 @@ snapshots: semver@6.3.1: {} - semver@7.8.0: {} + semver@7.8.1: {} send@1.2.0: dependencies: @@ -15946,7 +15712,7 @@ snapshots: sprintf-js@1.0.3: {} - srvx@0.11.15: {} + srvx@0.11.16: {} stable-hash-x@0.2.0: {} @@ -16060,7 +15826,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - symbol-tree@3.2.4: {} + symbol-tree@3.2.4: + optional: true sync-child-process@1.0.2: dependencies: @@ -16095,7 +15862,7 @@ snapshots: - bare-abort-controller - react-native-b4a - terser@5.47.1: + terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -16123,11 +15890,13 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@7.0.19: {} + tldts-core@7.0.19: + optional: true tldts@7.0.19: dependencies: tldts-core: 7.0.19 + optional: true to-regex-range@5.0.1: dependencies: @@ -16142,10 +15911,12 @@ snapshots: tough-cookie@6.0.0: dependencies: tldts: 7.0.19 + optional: true tr46@6.0.0: dependencies: punycode: 2.3.1 + optional: true tree-kill@1.2.2: {} @@ -16171,7 +15942,7 @@ snapshots: picomatch: 4.0.4 rolldown: link:rolldown/packages/rolldown rolldown-plugin-dts: 0.25.1(@typescript/native-preview@7.0.0-dev.20260122.2)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@rolldown+packages+rolldown)(typescript@6.0.2) - semver: 7.8.0 + semver: 7.8.1 tinyexec: 1.1.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 @@ -16204,7 +15975,7 @@ snapshots: picomatch: 4.0.4 rolldown: link:rolldown/packages/rolldown rolldown-plugin-dts: 0.25.1(@typescript/native-preview@7.0.0-dev.20260122.2)(rolldown@rolldown+packages+rolldown)(typescript@6.0.2) - semver: 7.8.0 + semver: 7.8.1 tinyexec: 1.1.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 @@ -16425,10 +16196,44 @@ snapshots: vary@1.1.2: {} - vitest@4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7))(@vitest/browser-preview@4.1.7(vite@packages+core)(vitest@4.1.7))(@vitest/browser-webdriverio@4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1))(@vitest/coverage-istanbul@4.1.7(vitest@4.1.7))(@vitest/coverage-v8@4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7))(@vitest/ui@4.1.7(vitest@4.1.7))(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core): + vitest@4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@packages+core) + '@vitest/mocker': 4.1.7(patch_hash=adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f)(vite@packages+core) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: link:packages/core + why-is-node-running: 2.3.0 + optionalDependencies: + '@edge-runtime/vm': 5.0.0 + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.3 + '@vitest/browser-playwright': 4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7) + '@vitest/browser-preview': 4.1.7(vite@packages+core)(vitest@4.1.7) + '@vitest/browser-webdriverio': 4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1) + happy-dom: 20.0.10 + jsdom: 27.2.0 + transitivePeerDependencies: + - msw + + vitest@4.1.7(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-playwright@4.1.7)(@vitest/browser-preview@4.1.7)(@vitest/browser-webdriverio@4.1.7)(happy-dom@20.0.10)(jsdom@27.2.0)(vite@packages+core): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(patch_hash=adef9b6b029d1060f7181d0b0171a1b6991aaad36a979388e12e34333397733f)(vite@packages+core) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -16454,9 +16259,6 @@ snapshots: '@vitest/browser-playwright': 4.1.7(playwright@1.57.0)(vite@packages+core)(vitest@4.1.7) '@vitest/browser-preview': 4.1.7(vite@packages+core)(vitest@4.1.7) '@vitest/browser-webdriverio': 4.1.7(vite@packages+core)(vitest@4.1.7)(webdriverio@9.20.1) - '@vitest/coverage-istanbul': 4.1.7(vitest@4.1.7) - '@vitest/coverage-v8': 4.1.7(@vitest/browser@4.1.7)(vitest@4.1.7) - '@vitest/ui': 4.1.7(vitest@4.1.7) happy-dom: 20.0.10 jsdom: 27.2.0 transitivePeerDependencies: @@ -16502,6 +16304,7 @@ snapshots: w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 + optional: true wait-port@1.1.0: dependencies: @@ -16534,7 +16337,7 @@ snapshots: deepmerge-ts: 7.1.5 https-proxy-agent: 7.0.6 undici: 6.22.0 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -16578,7 +16381,8 @@ snapshots: - supports-color - utf-8-validate - webidl-conversions@8.0.0: {} + webidl-conversions@8.0.0: + optional: true webpack-virtual-modules@0.6.2: {} @@ -16586,7 +16390,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 - whatwg-mimetype@3.0.0: {} + whatwg-mimetype@3.0.0: + optional: true whatwg-mimetype@4.0.0: {} @@ -16594,6 +16399,7 @@ snapshots: dependencies: tr46: 6.0.0 webidl-conversions: 8.0.0 + optional: true which@2.0.2: dependencies: @@ -16656,15 +16462,17 @@ snapshots: js-yaml: 4.1.1 write-file-atomic: 5.0.1 - ws@8.20.1: {} + ws@8.21.0: {} wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 - xml-name-validator@5.0.0: {} + xml-name-validator@5.0.0: + optional: true - xmlchars@2.2.0: {} + xmlchars@2.2.0: + optional: true y18n@5.0.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d45393dcb8..623fbdc1d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,8 +40,10 @@ catalog: '@types/ws': ^8.18.0 '@typescript/native-preview': 7.0.0-dev.20260122.2 '@vitejs/plugin-react': ^5.1.2 - '@vitest/browser': ^4.1.6 - '@vitest/browser-playwright': ^4.1.6 + '@vitest/browser': 4.1.7 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 '@voidzero-dev/vitepress-theme': 4.8.3 '@vueuse/core': ^14.0.0 '@yarnpkg/fslib': ^3.1.3 @@ -116,7 +118,7 @@ catalog: vitepress-plugin-group-icons: ^1.7.1 vitepress-plugin-llms: ^1.1.0 vitepress-plugin-og: ^0.0.5 - vitest: ^4.1.6 + vitest: 4.1.7 vue: ^3.5.21 web-tree-sitter: ^0.26.0 ws: ^8.20.1 @@ -168,23 +170,22 @@ minimumReleaseAgeExclude: overrides: rolldown: workspace:rolldown@* vite: workspace:@voidzero-dev/vite-plus-core@* - vite-plus: workspace:* - vitest: workspace:@voidzero-dev/vite-plus-test@* - vitest-dev: npm:vitest@^4.1.7 + vite-plus: workspace:vite-plus@* packageExtensions: + '@rollup/plugin-dynamic-import-vars': + dependencies: + '@types/estree': ^1.0.0 sass-embedded: peerDependencies: source-map-js: '*' peerDependenciesMeta: source-map-js: optional: true - '@rollup/plugin-dynamic-import-vars': - dependencies: - '@types/estree': ^1.0.0 patchedDependencies: - sirv@3.0.2: vite/patches/sirv@3.0.2.patch + '@vitest/mocker@4.1.7': patches/@vitest__mocker@4.1.7.patch chokidar@3.6.0: vite/patches/chokidar@3.6.0.patch dotenv-expand@13.0.0: vite/patches/dotenv-expand@13.0.0.patch + sirv@3.0.2: vite/patches/sirv@3.0.2.patch peerDependencyRules: allowAny: - vite diff --git a/rfcs/cli-output-polish.md b/rfcs/cli-output-polish.md index c3f4010d8b..97427add66 100644 --- a/rfcs/cli-output-polish.md +++ b/rfcs/cli-output-polish.md @@ -298,7 +298,9 @@ The `vite_install` crate also has `Warning:` and `Note:` messages across multipl ### Phase 3: Rebrand vitest Output -Vitest is bundled (not cloned source) via `@voidzero-dev/vite-plus-test`. Its build script (`packages/test/build.ts`) copies and rewrites vitest's dist files. We patch the bundled cac chunk during the build to rebrand CLI output. +> **Note:** This phase has been reverted. The `@voidzero-dev/vite-plus-test` bundled wrapper was removed in favor of consuming upstream `vitest` directly, since the `vite` → `@voidzero-dev/vite-plus-core` package manager override already handles the dependency redirection. Vitest output is no longer rebranded. + +Historical context (no longer applies): Vitest was bundled (not cloned source) via `@voidzero-dev/vite-plus-test`. Its build script (`packages/test/build.ts`) copied and rewrote vitest's dist files. We patched the bundled cac chunk during the build to rebrand CLI output. #### 3.1 Approach: Build-time patching of bundled cac chunk diff --git a/rfcs/migration-command.md b/rfcs/migration-command.md index b510d06e8b..7ec105cc78 100644 --- a/rfcs/migration-command.md +++ b/rfcs/migration-command.md @@ -203,12 +203,10 @@ Wrote agent instructions to AGENTS.md }, "devDependencies": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest", "@vitejs/plugin-react": "^4.2.0" }, "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } } ``` @@ -223,7 +221,6 @@ Wrote agent instructions to AGENTS.md }, "devDependencies": { "vite": "catalog:", - "vitest": "catalog:", "@vitejs/plugin-react": "^4.2.0", "vite-plus": "catalog:" }, @@ -236,15 +233,12 @@ Wrote agent instructions to AGENTS.md ```yaml catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest vite-plus: latest overrides: vite: 'catalog:' - vitest: 'catalog:' peerDependencyRules: allowAny: - vite - - vitest allowedVersions: vite: '*' vitest: '*' @@ -259,16 +253,14 @@ Projects that already have a `pnpm` field in `package.json` (e.g., with `overrid "name": "my-package", "devDependencies": { "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest", "vite-plus": "latest" }, "pnpm": { "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" }, "peerDependencyRules": { - "allowAny": ["vite", "vitest"], + "allowAny": ["vite"], "allowedVersions": { "vite": "*", "vitest": "*" } } } @@ -289,6 +281,7 @@ Projects that already have a `pnpm` field in `package.json` (e.g., with `overrid - rewrite `import from '@vitest/browser/{name}'` to `import from 'vite-plus/test/browser/{name}'`, e.g.: `import from '@vitest/browser/context'` to `import from 'vite-plus/test/browser/context'` - rewrite `import from '@vitest/browser-playwright'` to `import from 'vite-plus/test/browser-playwright'` - rewrite `import from '@vitest/browser-playwright/{name}'` to `import from 'vite-plus/test/browser-playwright/{name}'` +- `declare module 'vitest'`, `declare module 'vitest/{name}'`, and `declare module '@vitest/browser*'` are intentionally **not** rewritten — `vite-plus/test*` is a thin re-export of upstream `vitest*`, so type augmentations have to target the upstream module identity to merge correctly **Note**: For Yarn, use `resolutions` instead of `overrides`. @@ -503,15 +496,12 @@ For monorepo projects and standalone projects without existing `pnpm` config in ```yaml catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest vite-plus: latest overrides: vite: 'catalog:' - vitest: 'catalog:' peerDependencyRules: allowAny: - vite - - vitest allowedVersions: vite: '*' vitest: '*' @@ -524,12 +514,10 @@ peerDependencyRules: ```json { "devDependencies": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" }, "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + "vite": "npm:@voidzero-dev/vite-plus-core@latest" } } ``` @@ -541,7 +529,6 @@ peerDependencyRules: ```yaml catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest ``` `package.json` @@ -549,8 +536,7 @@ catalog: ```json { "resolutions": { - "vite": "catalog:", - "vitest": "catalog:" + "vite": "catalog:" } } ``` diff --git a/vite.config.ts b/vite.config.ts index 18e9c63990..79915549cc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -66,12 +66,6 @@ export default defineConfig({ 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', 'packages/cli/snap-tests-global/migration-lint-staged-ts-config', 'ecosystem-ci/*/**', - 'packages/test/**.cjs', - 'packages/test/**.cts', - 'packages/test/**.d.mjs', - 'packages/test/**.d.ts', - 'packages/test/**.mjs', - 'packages/test/browser/', 'packages/cli/src/run-config.ts', 'vite', 'rolldown', @@ -106,7 +100,6 @@ export default defineConfig({ 'vp run rolldown#build-node', 'vp run vite#build-types', 'vp run @voidzero-dev/vite-plus-core#build', - 'vp run @voidzero-dev/vite-plus-test#build', 'vp run vite-plus#build', ].join(' && '), },