Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ export async function createVitestConfigPlugin(
test: {
name: projectName,
include,
fsModuleCache: false,
// CLI provider browser options override, if present
...(browser ? { browser } : {}),
// Only override if the user explicitly set it via CLI
Expand All @@ -301,7 +300,6 @@ export async function createVitestConfigPlugin(

return {
test: {
fsModuleCache: false,
coverage: await generateCoverageOption(
options.coverage,
testConfig?.coverage,
Expand Down Expand Up @@ -335,12 +333,42 @@ export function createVitestPlugins(pluginOptions: PluginOptions): Vite.Plugin[]
);
let vitestConfig: ResolvedConfig;

/** The build output path served for a module, whether it is a test entry point or a built artifact. */
const getOutputPath = (id: string): string => {
const entryPoint = testFileToEntryPoint.get(id);

return entryPoint ? entryPoint + '.js' : toPosixPath(path.relative(workspaceRoot, id));
};

return [
{
name: 'angular:test-in-memory-provider',
enforce: 'pre',
configureVitest(context: VitestPluginContext) {
vitestConfig = context.vitest.config;

// Vitest keys its file system module cache on the file found on disk. A test entry point is
// served from its bundle instead, which also holds the application code it imports, so the
// key must follow the served build output, or the cache would return a stale bundle.
// Vitest 4.1 only has the experimental name, and 4.0 has no file system module cache at all.
const pluginContext: Partial<VitestPluginContext> = context;
const defineCacheKeyGenerator =
pluginContext.defineCacheKeyGenerator ??
pluginContext.experimental_defineCacheKeyGenerator;
defineCacheKeyGenerator?.(({ id }) => {
const outputPath = getOutputPath(id);
const outputFile = buildResultFiles.get(outputPath);
if (!outputFile) {
return undefined;
}
if (outputFile.origin !== 'memory') {
return false;
}

const sourceMapFile = buildResultFiles.get(outputPath + '.map');

return `${outputFile.hash}:${sourceMapFile?.origin === 'memory' ? sourceMapFile.hash : ''}`;
});
},
resolveId: (id: string, importer: string | undefined) => {
// Fast path for test entry points.
Expand Down Expand Up @@ -399,12 +427,9 @@ export function createVitestPlugins(pluginOptions: PluginOptions): Vite.Plugin[]
async load(id: string) {
assert(buildResultFiles.size > 0, 'buildResult must be available for in-memory loading.');

// Attempt to load as a source test file.
const entryPoint = testFileToEntryPoint.get(id);
let outputPath;
if (entryPoint) {
outputPath = entryPoint + '.js';

// Attempt to load as a source test file, or else as a built artifact.
const outputPath = getOutputPath(id);
if (testFileToEntryPoint.has(id)) {
// Setup files must not be wrapped in a virtual import stub because Vitest only invalidates
// the setup file itself between test files; wrapping it would cause the underlying bundle
// to be cached, preventing per-test hooks from running on subsequent test files.
Expand All @@ -417,10 +442,6 @@ export function createVitestPlugins(pluginOptions: PluginOptions): Vite.Plugin[]
code: `import "./${outputPath}";`,
};
}
} else {
// Attempt to load as a built artifact.
const relativePath = path.relative(workspaceRoot, id);
outputPath = toPosixPath(relativePath);
}

const outputFile = buildResultFiles.get(outputPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { readdirSync } from 'node:fs';
import { execute } from '../../index';
import {
BASE_OPTIONS,
Expand Down Expand Up @@ -650,7 +651,47 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {

const secondRun = await harness.executeOnce();
expect(secondRun.result?.success).toBeTrue();
harness.expectDirectory('.vitest-cache').toNotExist();
harness.expectDirectory('.vitest-cache').toExist();
});

it('should reuse the cached modules across runs when nothing changes', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
runnerConfig: 'vitest.config.mts',
});

await harness.writeFiles({
'vitest.config.mts': `
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
fsModuleCache: true,
fsModuleCachePath: './.vitest-cache',
},
});
`,
'src/app/value.ts': `export const VALUE = 'initial';`,
'src/app/app.component.spec.ts': `
import { test, expect } from 'vitest';
import { VALUE } from './value';

test('should use the value from imported module', () => {
expect(VALUE).toBe('initial');
});
`,
});
const cacheEntries = () =>
readdirSync(harness.resolvePath('.vitest-cache'), { recursive: true }).sort();

const firstRun = await harness.executeOnce();
expect(firstRun.result?.success).toBeTrue();
const entriesAfterFirstRun = cacheEntries();
expect(entriesAfterFirstRun.length).toBeGreaterThan(0);

// An unchanged build produces the same bundles, so no new cache entry is written.
const secondRun = await harness.executeOnce();
expect(secondRun.result?.success).toBeTrue();
expect(cacheEntries()).toEqual(entriesAfterFirstRun);
});
});
});
Loading