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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { BuilderOutput } from '@angular-devkit/architect';
import assert from 'node:assert';
import path from 'node:path';
import { isMatch } from 'picomatch';
import type { InlineConfig, Vitest } from 'vitest/node';
import { assertIsError } from '../../../../utils/error';
import { toPosixPath } from '../../../../utils/path';
Expand Down Expand Up @@ -141,7 +142,9 @@ export class VitestExecutor implements TestExecutor {
} = this.options;

let vitestNodeModule;
let vitestCoverageModule;
try {
vitestCoverageModule = await import('vitest/coverage');
vitestNodeModule = await import('vitest/node');
} catch (error: unknown) {
assertIsError(error);
Expand All @@ -154,6 +157,21 @@ export class VitestExecutor implements TestExecutor {
}
const { startVitest } = vitestNodeModule;

// Augment BaseCoverageProvider to include logic to support the built virtual files.
// Temporary workaround to avoid the direct filesystem checks in the base provider that
// were introduced in v4. Also ensures that all built virtual files are available.
const builtVirtualFiles = this.buildResultFiles;
vitestCoverageModule.BaseCoverageProvider.prototype.isIncluded = function (filename) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patching private APIs instead of filing bug report on upstream is not recommended, ever.

I'm not sure why exactly Angular requires such hacky patch but it might as well be real bug on Vitest.

Copy link
Member Author

@clydin clydin Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. It's temporary and we will be filing a bug and investigating further. This is intended to unblock downstream integration testing and allows us to test for potential breakage across a wider array of user projects in the interim. Additionally, in this case isIncluded is a public method on a public class that if it were changed upstream would cause breakage for all concrete providers so could not effectively be changed outside a major version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, @clydin, is there any design doc or anything that highlights the pros and cons of separate build + virtual files vs. a vite transform plugin?

I am myself passionate about symmetry and would love to have everything built and bundled the same way but I feel that this is also breaking some of Vitest assumptions.

const relativeFilename = path.relative(workspaceRoot, filename);
if (!this.options.include || builtVirtualFiles.has(relativeFilename)) {
return !isMatch(relativeFilename, this.options.exclude);
} else {
return isMatch(relativeFilename, this.options.include, {
ignore: this.options.exclude,
});
}
};

// Setup vitest browser options if configured
const browserOptions = await setupBrowserConfiguration(
browsers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,26 @@ export function createVitestPlugins(
outputFile.origin === 'memory'
? Buffer.from(outputFile.contents).toString('utf-8')
: await readFile(outputFile.inputPath, 'utf-8');
const map = sourceMapFile
const sourceMapText = sourceMapFile
? sourceMapFile.origin === 'memory'
? Buffer.from(sourceMapFile.contents).toString('utf-8')
: await readFile(sourceMapFile.inputPath, 'utf-8')
: undefined;

// Vitest will include files in the coverage report if the sourcemap contains no sources.
// For builder-internal generated code chunks, which are typically helper functions,
// a virtual source is added to the sourcemap to prevent them from being incorrectly
// included in the final coverage report.
const map = sourceMapText ? JSON.parse(sourceMapText) : undefined;
if (map) {
if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) {
map.sources = ['virtual:builder'];
}
}

return {
code,
map: map ? JSON.parse(map) : undefined,
map,
};
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
expect(harness.hasFile('coverage/test/index.html')).toBeFalse();
harness.expectFile('coverage/test/index.html').toNotExist();
});

it('should generate a code coverage report when coverage is true', async () => {
Expand All @@ -39,7 +39,19 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
expect(harness.hasFile('coverage/test/index.html')).toBeTrue();
harness.expectFile('coverage/test/index.html').toExist();
});

it('should generate a code coverage report when coverage is true', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
coverage: true,
coverageReporters: ['json'] as any,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('coverage/test/coverage-final.json').content.toContain('app.component.ts');
});
});
});