Skip to content

Commit e2ddb1d

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 9671cc6 commit e2ddb1d

3 files changed

Lines changed: 60 additions & 84 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: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import fs from 'node:fs';
1111
import { createRequire } from 'node:module';
1212
import path from 'node:path';
1313
import Piscina from 'piscina';
14-
import { removeSourceMappingURL } from '../../utils/source-map';
14+
import { loadInputSourceMap, removeSourceMappingURL } from '../../utils/source-map';
1515

1616
interface JavaScriptTransformRequest {
1717
filename: string;
@@ -36,6 +36,51 @@ 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 inputSourceMap = useInputSourcemap ? loadInputSourceMap(filename, data) : undefined;
62+
const instrumentedCode = instrumenter.instrumentSync(
63+
data,
64+
filename,
65+
inputSourceMap as Parameters<typeof instrumenter.instrumentSync>[2],
66+
);
67+
const lastMap = instrumenter.lastSourceMap();
68+
69+
if (useInputSourcemap && lastMap) {
70+
const inlineMap = Buffer.from(JSON.stringify(lastMap)).toString('base64');
71+
72+
return instrumentedCode + `\n//# sourceMappingURL=data:application/json;base64,${inlineMap}`;
73+
}
74+
75+
return removeSourceMappingURL(instrumentedCode);
76+
} catch (error) {
77+
throw new Error(
78+
`The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`,
79+
{ cause: error },
80+
);
81+
}
82+
}
83+
3984
export default async function transformJavaScript(
4085
request: JavaScriptTransformRequest,
4186
): Promise<unknown> {
@@ -59,50 +104,26 @@ async function transformJavaScriptImpl(
59104
data: string,
60105
options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>,
61106
): Promise<string> {
62-
const shouldLink = !options.skipLinker && (await requiresLinking(filename, data));
63107
const useInputSourcemap =
64108
options.sourcemap &&
65109
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
66110

67-
const babelPlugins: PluginItem[] = [];
111+
let code = data;
68112

69113
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-
babelPlugins.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-
}
114+
code = await instrumentCoverage(filename, code, useInputSourcemap);
95115
}
96116

117+
const shouldLink = !options.skipLinker && (await requiresLinking(filename, code));
118+
const babelPlugins: PluginItem[] = [];
119+
97120
if (shouldLink) {
98121
// Lazy load the linker plugin only when linking is required
99122
const linkerPlugin = await createLinkerPlugin(options);
100123
babelPlugins.push(linkerPlugin as unknown as PluginItem);
101124
}
102125

103-
let code = data;
104-
105-
// If Babel is needed, run it first
126+
// If Babel is needed (e.g. for linking), run it
106127
if (babelPlugins.length > 0) {
107128
const result = await transformAsync(code, {
108129
filename,

0 commit comments

Comments
 (0)