diff --git a/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts b/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts deleted file mode 100644 index e549db97f8bb..000000000000 --- a/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { NodePath, PluginObject, PluginPass, types } from '@babel/core'; -import type { Visitor } from 'istanbul-lib-instrument'; -import assert from 'node:assert'; - -/** - * A babel plugin factory function for adding istanbul instrumentation. - * - * @returns A babel plugin object instance. - */ -export default function ( - programVisitor: typeof import('istanbul-lib-instrument').programVisitor, -): PluginObject { - const visitors = new WeakMap(); - - return { - visitor: { - Program: { - enter(path: NodePath, state: PluginPass) { - const visitor = programVisitor(types, state.filename, { - // Babel returns a Converter object from the `convert-source-map` package - inputSourceMap: (state.file.inputMap as undefined | { toObject(): object })?.toObject(), - }); - visitors.set(path, visitor); - - visitor.enter(path); - }, - exit(path: NodePath) { - const visitor = visitors.get(path); - assert(visitor, 'Instrumentation visitor should always be present for program path.'); - - visitor.exit(path); - visitors.delete(path); - }, - }, - }, - }; -} diff --git a/packages/angular/build/src/tools/babel/plugins/types.d.ts b/packages/angular/build/src/tools/babel/plugins/types.d.ts index 4ff052dcb136..aa1f40580491 100644 --- a/packages/angular/build/src/tools/babel/plugins/types.d.ts +++ b/packages/angular/build/src/tools/babel/plugins/types.d.ts @@ -7,14 +7,14 @@ */ declare module 'istanbul-lib-instrument' { - export interface Visitor { - enter(path: import('@babel/core').NodePath): void; - exit(path: import('@babel/core').NodePath): void; + export interface Instrumenter { + instrumentSync(code: string, filename: string, inputSourceMap?: object): string; + lastSourceMap(): object | undefined; } - export function programVisitor( - types: typeof import('@babel/core').types, - filePath?: string, - options?: { inputSourceMap?: object | null }, - ): Visitor; + export function createInstrumenter(options?: { + produceSourceMap?: boolean; + esModules?: boolean; + coverageVariable?: string; + }): Instrumenter; } diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts index 734a8a994882..3916930b06bd 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts @@ -11,7 +11,7 @@ import fs from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; import Piscina from 'piscina'; -import { removeSourceMappingURL } from '../../utils/source-map'; +import { loadInputSourceMap, removeSourceMappingURL } from '../../utils/source-map'; interface JavaScriptTransformRequest { filename: string; @@ -36,6 +36,51 @@ const textEncoder = new TextEncoder(); */ const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare'; +async function instrumentCoverage( + filename: string, + data: string, + useInputSourcemap: boolean, +): Promise { + try { + let resolvedPath = 'istanbul-lib-instrument'; + try { + const requireFn = createRequire(filename); + resolvedPath = requireFn.resolve('istanbul-lib-instrument'); + } catch { + // Fallback to pool worker import traversal + } + + const { createInstrumenter } = (await import( + resolvedPath + )) as typeof import('istanbul-lib-instrument'); + const instrumenter = createInstrumenter({ + produceSourceMap: useInputSourcemap, + esModules: true, + }); + + const inputSourceMap = useInputSourcemap ? loadInputSourceMap(filename, data) : undefined; + const instrumentedCode = instrumenter.instrumentSync( + data, + filename, + inputSourceMap as Parameters[2], + ); + const lastMap = instrumenter.lastSourceMap(); + + if (useInputSourcemap && lastMap) { + const inlineMap = Buffer.from(JSON.stringify(lastMap)).toString('base64'); + + return instrumentedCode + `\n//# sourceMappingURL=data:application/json;base64,${inlineMap}`; + } + + return removeSourceMappingURL(instrumentedCode); + } catch (error) { + throw new Error( + `The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`, + { cause: error }, + ); + } +} + export default async function transformJavaScript( request: JavaScriptTransformRequest, ): Promise { @@ -59,50 +104,26 @@ async function transformJavaScriptImpl( data: string, options: Omit, ): Promise { - const shouldLink = !options.skipLinker && (await requiresLinking(filename, data)); const useInputSourcemap = options.sourcemap && (!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); - const babelPlugins: PluginItem[] = []; + let code = data; if (options.instrumentForCoverage) { - try { - let resolvedPath = 'istanbul-lib-instrument'; - try { - const requireFn = createRequire(filename); - resolvedPath = requireFn.resolve('istanbul-lib-instrument'); - } catch { - // Fallback to pool worker import traversal - } - - const istanbul = await import(resolvedPath); - const programVisitor = istanbul.programVisitor ?? istanbul.default?.programVisitor; - - if (!programVisitor) { - throw new Error('programVisitor is not available in istanbul-lib-instrument.'); - } - - const { default: coveragePluginFactory } = - await import('../babel/plugins/add-code-coverage.js'); - babelPlugins.push(coveragePluginFactory(programVisitor) as unknown as PluginItem); - } catch (error) { - throw new Error( - `The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`, - { cause: error }, - ); - } + code = await instrumentCoverage(filename, code, useInputSourcemap); } + const shouldLink = !options.skipLinker && (await requiresLinking(filename, code)); + const babelPlugins: PluginItem[] = []; + if (shouldLink) { // Lazy load the linker plugin only when linking is required const linkerPlugin = await createLinkerPlugin(options); babelPlugins.push(linkerPlugin as unknown as PluginItem); } - let code = data; - - // If Babel is needed, run it first + // If Babel is needed (e.g. for linking), run it if (babelPlugins.length > 0) { const result = await transformAsync(code, { filename,