From 033eeec1eb460c997bb05da13db28ffd6d1a7e83 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sat, 21 Mar 2026 21:25:03 +0900 Subject: [PATCH 1/3] fix(test): reuse existing globalExpect to fix expect.extend from third-party libraries --- packages/test/build.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/test/build.ts b/packages/test/build.ts index a2c5df4249..9b8bcfe57b 100644 --- a/packages/test/build.ts +++ b/packages/test/build.ts @@ -224,6 +224,7 @@ await createBrowserEntryFiles(); await patchModuleAugmentations(); await patchChaiTypeReference(); await patchMockerHoistedModule(); +await patchGlobalExpectReuse(); const pluginExports = await createPluginExports(); await mergePackageJson(pluginExports); generateLicenseFile({ @@ -2352,6 +2353,62 @@ async function patchMockerHoistedModule() { } } +/** + * Patch globalExpect initialization to reuse an existing instance. + * + * When vitest is aliased via npm overrides (e.g., vitest → @voidzero-dev/vite-plus-test), + * the override module may be loaded as a separate instance from the test runner's vitest core. + * Both instances execute `const globalExpect = createExpect()` and set + * `globalThis[GLOBAL_EXPECT]`, causing the later one to overwrite the first. + * + * This patch makes the override module reuse an existing globalExpect if one is already set, + * so that `expect.extend()` calls from third-party libraries (e.g., @testing-library/jest-dom) + * register matchers on the same instance used by the test runner. + * + * See: https://github.com/voidzero-dev/vite-plus/issues/897 + */ +async function patchGlobalExpectReuse() { + console.log('\nPatching globalExpect to reuse existing instance...'); + + const chunksDir = join(distDir, 'chunks'); + const files = await readdir(chunksDir); + const testChunk = files.find((f) => f.startsWith('test.') && f.endsWith('.js')); + + if (!testChunk) { + throw new Error('Could not find test chunk file in dist/chunks/'); + } + + const testChunkPath = join(chunksDir, testChunk); + let content = await readFile(testChunkPath, 'utf-8'); + + const original = `const globalExpect = createExpect(); +Object.defineProperty(globalThis, GLOBAL_EXPECT, { + value: globalExpect, + writable: true, + configurable: true +});`; + + const patched = `const globalExpect = globalThis[GLOBAL_EXPECT] ?? createExpect(); +if (!globalThis[GLOBAL_EXPECT]) { + Object.defineProperty(globalThis, GLOBAL_EXPECT, { + value: globalExpect, + writable: true, + configurable: true + }); +}`; + + if (!content.includes(original)) { + throw new Error( + 'Could not find globalExpect initialization pattern in test chunk. ' + + 'This likely means vitest code has changed and the patch needs to be updated.', + ); + } + + content = content.replace(original, patched); + await writeFile(testChunkPath, content, 'utf-8'); + console.log(` Patched globalExpect in ${testChunk}`); +} + /** * Create /plugins/* exports for all copied @vitest/* packages. * This allows pnpm overrides to redirect @vitest/* imports to our copied versions. From f0bd6f2ba587ff515f8470cf049f4b859727dcf9 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Mon, 23 Mar 2026 18:05:34 +0900 Subject: [PATCH 2/3] fix(test): auto-inline third-party packages that use expect.extend() internally --- .../test/__tests__/build-artifacts.spec.ts | 55 +++++++++++++ packages/test/build.ts | 78 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 packages/test/__tests__/build-artifacts.spec.ts diff --git a/packages/test/__tests__/build-artifacts.spec.ts b/packages/test/__tests__/build-artifacts.spec.ts new file mode 100644 index 0000000000..f70d8ba3ab --- /dev/null +++ b/packages/test/__tests__/build-artifacts.spec.ts @@ -0,0 +1,55 @@ +/** + * Verify that the @voidzero-dev/vite-plus-test build output (dist/) + * contains the expected patches applied during the build (in build.ts). + * + * 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', () => { + /** + * 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'); + // The patch should check `testConfig.server.deps.inline !== true` + expect(content).toContain('server.deps.inline !== true'); + }); + }); +}); diff --git a/packages/test/build.ts b/packages/test/build.ts index 9b8bcfe57b..b636f5f908 100644 --- a/packages/test/build.ts +++ b/packages/test/build.ts @@ -225,6 +225,7 @@ await patchModuleAugmentations(); await patchChaiTypeReference(); await patchMockerHoistedModule(); await patchGlobalExpectReuse(); +await patchServerDepsInline(); const pluginExports = await createPluginExports(); await mergePackageJson(pluginExports); generateLicenseFile({ @@ -2409,6 +2410,83 @@ if (!globalThis[GLOBAL_EXPECT]) { console.log(` Patched globalExpect in ${testChunk}`); } +/** + * 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. From 32c47cd3dc474b7e1bb44cdd77031b88572af4df Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Mon, 23 Mar 2026 22:14:49 +0900 Subject: [PATCH 3/3] chore: add e2e test --- .github/workflows/e2e-test.yml | 4 ++++ ecosystem-ci/repo.json | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9bfd7415fc..c7b094cbae 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -285,6 +285,10 @@ jobs: vp run lint vp run test:types vp test --project unit + - name: vite-plus-jest-dom-repro + node-version: 24 + command: | + vp test run exclude: # frm-stack uses Docker (testcontainers) which doesn't work the same way on Windows - os: windows-latest diff --git a/ecosystem-ci/repo.json b/ecosystem-ci/repo.json index 796c5658a2..06fcd1088d 100644 --- a/ecosystem-ci/repo.json +++ b/ecosystem-ci/repo.json @@ -91,5 +91,11 @@ "branch": "main", "hash": "230b7c7ddb6bb8551ce797144f0ce0f047ff8d7d", "forceFreshMigration": true + }, + "vite-plus-jest-dom-repro": { + "repository": "https://github.com/why-reproductions-are-required/vite-plus-jest-dom-repro.git", + "branch": "master", + "hash": "01bd9ce1ac66ee3c21ed8a7f14311317d87fb999", + "forceFreshMigration": true } }