Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ecosystem-ci/repo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
42 changes: 39 additions & 3 deletions packages/test/__tests__/build-artifacts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* contains the expected files and that patches applied during the build
* (in build.ts) produce correct artifacts.
*
* This is important because vite-plus re-packages vitest with custom
* patches, and missing exports or incorrect patches can break
* third-party integrations (e.g., @storybook/addon-vitest, #1086).
* 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';
Expand All @@ -16,6 +15,16 @@ 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');
Expand Down Expand Up @@ -55,4 +64,31 @@ describe('build artifacts', () => {
expect(vendorMapContent).not.toContain("'@vitest/browser/context'");
});
});

/**
* 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');
});
});
});
78 changes: 78 additions & 0 deletions packages/test/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ await createBrowserEntryFiles();
await patchModuleAugmentations();
await patchChaiTypeReference();
await patchMockerHoistedModule();
await patchServerDepsInline();
const pluginExports = await createPluginExports();
await mergePackageJson(pluginExports);
generateLicenseFile({
Expand Down Expand Up @@ -2360,6 +2361,83 @@ async function patchMockerHoistedModule() {
}
}

/**
* 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.
Expand Down
Loading