Skip to content

Commit f9a7d52

Browse files
clydinalan-agius4
authored andcommitted
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 25034f9 commit f9a7d52

3 files changed

Lines changed: 82 additions & 112 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: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { type PluginItem, transformAsync } from '@babel/core';
1010
import { createRequire } from 'node:module';
1111
import Piscina from 'piscina';
1212
import { useBabelLinker } from '../../utils/environment-options.js';
13-
import { removeSourceMappingURL } from '../../utils/source-map';
13+
import { loadInputSourceMap, removeSourceMappingURL } from '../../utils/source-map';
1414

1515
interface JavaScriptTransformRequest {
1616
filename: string;
@@ -35,6 +35,51 @@ const textEncoder = new TextEncoder();
3535
*/
3636
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';
3737

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

65-
const babelPlugins: PluginItem[] = [];
110+
let code = data;
66111

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

95-
let code = data;
96-
97116
if (shouldLink) {
98117
if (useBabelLinker) {
99118
const { createEs2015LinkerPlugin } = await import('@angular/compiler-cli/linker/babel');
100119
const { ConsoleLogger, LogLevel } = await import('@angular/compiler-cli');
101120

102-
babelPlugins.push(
103-
createEs2015LinkerPlugin({
104-
fileSystem: {
105-
exists: () => false,
106-
readFile: () => '',
107-
resolve: (...paths: string[]) => paths.join('/'),
108-
dirname: (path: string) => path.split('/').slice(0, -1).join('/'),
109-
relative: (_from: string, to: string) => to,
110-
} as never,
111-
logger: new ConsoleLogger(LogLevel.info),
112-
linkerJitMode: options.jit,
113-
// This is a workaround until https://github.com/angular/angular/issues/42769 is fixed.
114-
sourceMapping: false,
115-
}) as PluginItem,
116-
);
121+
const result = await transformAsync(code, {
122+
filename,
123+
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
124+
sourceMaps: useInputSourcemap ? 'inline' : false,
125+
compact: false,
126+
configFile: false,
127+
babelrc: false,
128+
browserslistConfigFile: false,
129+
plugins: [
130+
createEs2015LinkerPlugin({
131+
fileSystem: {
132+
exists: () => false,
133+
readFile: () => '',
134+
resolve: (...paths: string[]) => paths.join('/'),
135+
dirname: (path: string) => path.split('/').slice(0, -1).join('/'),
136+
relative: (_from: string, to: string) => to,
137+
} as never,
138+
logger: new ConsoleLogger(LogLevel.info),
139+
linkerJitMode: options.jit,
140+
// This is a workaround until https://github.com/angular/angular/issues/42769 is fixed.
141+
sourceMapping: false,
142+
}) as PluginItem,
143+
],
144+
});
145+
146+
code = result?.code ?? code;
117147
} else {
118148
oxcLinkerModule ??= await import('../angular/linker/oxc-linker.js');
119149
const result = oxcLinkerModule.linkWithOxc(filename, code, {
@@ -130,21 +160,6 @@ async function transformJavaScriptImpl(
130160
}
131161
}
132162

133-
// If Babel is needed for code coverage or babel linker fallback, run it
134-
if (babelPlugins.length > 0) {
135-
const result = await transformAsync(code, {
136-
filename,
137-
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
138-
sourceMaps: useInputSourcemap ? 'inline' : false,
139-
compact: false,
140-
configFile: false,
141-
babelrc: false,
142-
browserslistConfigFile: false,
143-
plugins: babelPlugins,
144-
});
145-
code = result?.code ?? code;
146-
}
147-
148163
// Run advanced optimizations using our fast oxc-transform
149164
if (options.advancedOptimizations) {
150165
const { transform } = await import('../babel/plugins/oxc-transform.js');

0 commit comments

Comments
 (0)