Skip to content

Commit 609c25d

Browse files
committed
refactor(@angular/build): use standalone Instrumenter API for code coverage
This refactors the code coverage instrumentation in the esbuild pipeline to bypass Babel and use the standalone `Instrumenter` API from `istanbul-lib-instrument` directly. The instrumenter is only used by the Karma/Jasmine testing implementation. Since first-party code coverage instrumentation and third-party Angular linking are almost always mutually exclusive for any given file, we do not benefit from composing them into a single Babel pass. Using the high-level standalone API simplifies the transformer worker and allows us to completely remove the custom `add-code-coverage.ts` Babel plugin.
1 parent eaedbd8 commit 609c25d

3 files changed

Lines changed: 57 additions & 85 deletions

File tree

packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

packages/angular/build/src/tools/babel/plugins/types.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
*/
88

99
declare module 'istanbul-lib-instrument' {
10-
export interface Visitor {
11-
enter(path: import('@babel/core').NodePath<types.Program>): void;
12-
exit(path: import('@babel/core').NodePath<types.Program>): void;
10+
export interface Instrumenter {
11+
instrumentSync(code: string, filename: string, inputSourceMap?: object): string;
12+
lastSourceMap(): object | undefined;
1313
}
1414

15-
export function programVisitor(
16-
types: typeof import('@babel/core').types,
17-
filePath?: string,
18-
options?: { inputSourceMap?: object | null },
19-
): Visitor;
15+
export function createInstrumenter(options?: {
16+
produceSourceMap?: boolean;
17+
esModules?: boolean;
18+
coverageVariable?: string;
19+
}): Instrumenter;
2020
}

packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,46 @@ const textEncoder = new TextEncoder();
3636
*/
3737
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';
3838

39+
async function instrumentCoverage(
40+
filename: string,
41+
data: string,
42+
useInputSourcemap: boolean,
43+
): Promise<string> {
44+
try {
45+
let resolvedPath = 'istanbul-lib-instrument';
46+
try {
47+
const requireFn = createRequire(filename);
48+
resolvedPath = requireFn.resolve('istanbul-lib-instrument');
49+
} catch {
50+
// Fallback to pool worker import traversal
51+
}
52+
53+
const { createInstrumenter } = (await import(
54+
resolvedPath
55+
)) as typeof import('istanbul-lib-instrument');
56+
const instrumenter = createInstrumenter({
57+
produceSourceMap: useInputSourcemap,
58+
esModules: true,
59+
});
60+
61+
const instrumentedCode = instrumenter.instrumentSync(data, filename);
62+
const lastMap = instrumenter.lastSourceMap();
63+
64+
if (useInputSourcemap && lastMap) {
65+
const inlineMap = Buffer.from(JSON.stringify(lastMap)).toString('base64');
66+
67+
return instrumentedCode + `\n//# sourceMappingURL=data:application/json;base64,${inlineMap}`;
68+
}
69+
70+
return removeSourceMappingURL(instrumentedCode);
71+
} catch (error) {
72+
throw new Error(
73+
`The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`,
74+
{ cause: error },
75+
);
76+
}
77+
}
78+
3979
export default async function transformJavaScript(
4080
request: JavaScriptTransformRequest,
4181
): Promise<unknown> {
@@ -59,41 +99,18 @@ async function transformWithBabel(
5999
data: string,
60100
options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>,
61101
): Promise<string> {
62-
const shouldLink = !options.skipLinker && (await requiresLinking(filename, data));
63102
const useInputSourcemap =
64103
options.sourcemap &&
65104
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
66105

67-
const plugins: PluginItem[] = [];
68-
106+
let code = data;
69107
if (options.instrumentForCoverage) {
70-
try {
71-
let resolvedPath = 'istanbul-lib-instrument';
72-
try {
73-
const requireFn = createRequire(filename);
74-
resolvedPath = requireFn.resolve('istanbul-lib-instrument');
75-
} catch {
76-
// Fallback to pool worker import traversal
77-
}
78-
79-
const istanbul = await import(resolvedPath);
80-
const programVisitor = istanbul.programVisitor ?? istanbul.default?.programVisitor;
81-
82-
if (!programVisitor) {
83-
throw new Error('programVisitor is not available in istanbul-lib-instrument.');
84-
}
85-
86-
const { default: coveragePluginFactory } =
87-
await import('../babel/plugins/add-code-coverage.js');
88-
plugins.push(coveragePluginFactory(programVisitor) as unknown as PluginItem);
89-
} catch (error) {
90-
throw new Error(
91-
`The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`,
92-
{ cause: error },
93-
);
94-
}
108+
code = await instrumentCoverage(filename, code, useInputSourcemap);
95109
}
96110

111+
const shouldLink = !options.skipLinker && (await requiresLinking(filename, code));
112+
const plugins: PluginItem[] = [];
113+
97114
if (shouldLink) {
98115
// Lazy load the linker plugin only when linking is required
99116
const linkerPlugin = await createLinkerPlugin(options);
@@ -116,13 +133,13 @@ async function transformWithBabel(
116133
);
117134
}
118135

119-
// If no additional transformations are needed, return the data directly
136+
// If no additional transformations are needed, return the code directly
120137
if (plugins.length === 0) {
121138
// Strip sourcemaps if they should not be used
122-
return useInputSourcemap ? data : removeSourceMappingURL(data);
139+
return useInputSourcemap ? code : removeSourceMappingURL(code);
123140
}
124141

125-
const result = await transformAsync(data, {
142+
const result = await transformAsync(code, {
126143
filename,
127144
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
128145
sourceMaps: useInputSourcemap ? 'inline' : false,
@@ -133,7 +150,7 @@ async function transformWithBabel(
133150
plugins,
134151
});
135152

136-
const outputCode = result?.code ?? data;
153+
const outputCode = result?.code ?? code;
137154

138155
// Strip sourcemaps if they should not be used.
139156
// Babel will keep the original comments even if sourcemaps are disabled.

0 commit comments

Comments
 (0)