From a1a721bca11d94a7c8343c4318117582dbf7971e Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:12:46 -0400 Subject: [PATCH] feat(@angular/build): migrate Angular Linker to oxc-parser and magic-string This refactors the Angular linker processing in the ESBuild pipeline to use `oxc-parser` and `magic-string` instead of `@babel/core`. By using the lightweight AST and precise token spans provided by OXC, the linker is able to process partial declarations via targeted `magic-string` overwrites in-place. This removes the dependency on `@babel/core` and the linker Babel plugin, yielding faster build startup times and improved compilation performance. --- .../src/tools/angular/linker/oxc-ast-host.ts | 297 ++++++++++++++++++ .../src/tools/angular/linker/oxc-linker.ts | 117 +++++++ .../tools/angular/linker/oxc-linker_spec.ts | 56 ++++ .../angular/linker/string-ast-factory.ts | 276 ++++++++++++++++ .../esbuild/javascript-transformer-worker.ts | 69 +--- 5 files changed, 760 insertions(+), 55 deletions(-) create mode 100644 packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts create mode 100644 packages/angular/build/src/tools/angular/linker/oxc-linker.ts create mode 100644 packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts create mode 100644 packages/angular/build/src/tools/angular/linker/string-ast-factory.ts diff --git a/packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts b/packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts new file mode 100644 index 000000000000..b7663132ab43 --- /dev/null +++ b/packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts @@ -0,0 +1,297 @@ +/** + * @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 type { AstHost, Range } from '@angular/compiler-cli/linker'; +import { FatalLinkerError } from '@angular/compiler-cli/linker'; +import type { + ArrayExpression, + ArrowFunctionExpression, + BooleanLiteral, + CallExpression, + Function as FunctionNode, + Node, + NullLiteral, + NumericLiteral, + ObjectExpression, + StringLiteral, + UnaryExpression, +} from '@oxc-project/types'; + +function isNode(node: unknown): node is Node { + return typeof node === 'object' && node !== null && 'type' in node; +} + +/** + * An implementation of `AstHost` that queries information from `oxc-parser` AST nodes. + */ +export class OxcAstHost implements AstHost { + getSymbolName(node: unknown): string | null { + if (!isNode(node)) { + return null; + } + + if (node.type === 'Identifier') { + return node.name; + } else if (node.type === 'MemberExpression') { + if (!node.computed && node.property.type === 'Identifier') { + return node.property.name; + } + } + + return null; + } + + isStringLiteral(node: unknown): node is StringLiteral { + return isNode(node) && node.type === 'Literal' && typeof node.value === 'string'; + } + + parseStringLiteral(str: unknown): string { + if (!this.isStringLiteral(str)) { + throw new FatalLinkerError(str as object, 'Unsupported syntax, expected a string literal.'); + } + + return str.value; + } + + isNumericLiteral(node: unknown): node is NumericLiteral { + return isNode(node) && node.type === 'Literal' && typeof node.value === 'number'; + } + + parseNumericLiteral(num: unknown): number { + if (!this.isNumericLiteral(num)) { + throw new FatalLinkerError(num as object, 'Unsupported syntax, expected a numeric literal.'); + } + + return num.value; + } + + isBooleanLiteral(node: unknown): node is BooleanLiteral | UnaryExpression { + if (!isNode(node)) { + return false; + } + + return ( + (node.type === 'Literal' && typeof node.value === 'boolean') || isMinifiedBooleanLiteral(node) + ); + } + + parseBooleanLiteral(bool: unknown): boolean { + if (!this.isBooleanLiteral(bool)) { + throw new FatalLinkerError(bool as object, 'Unsupported syntax, expected a boolean literal.'); + } + + if (bool.type === 'Literal') { + return bool.value; + } else { + const arg = bool.argument; + if (arg.type === 'Literal' && typeof arg.value === 'number') { + return !arg.value; + } + throw new FatalLinkerError(bool as object, 'Unsupported syntax, expected a boolean literal.'); + } + } + + isNull(node: unknown): node is NullLiteral { + return isNode(node) && node.type === 'Literal' && node.value === null; + } + + isArrayLiteral(node: unknown): node is ArrayExpression { + return isNode(node) && node.type === 'ArrayExpression'; + } + + parseArrayLiteral(array: unknown): unknown[] { + if (!this.isArrayLiteral(array)) { + throw new FatalLinkerError(array as object, 'Unsupported syntax, expected an array literal.'); + } + + const result: unknown[] = []; + + for (const element of array.elements) { + if (element === null) { + throw new FatalLinkerError( + array as object, + 'Unsupported syntax, element in array not to be empty.', + ); + } + if (element.type === 'SpreadElement') { + throw new FatalLinkerError( + element as object, + 'Unsupported syntax, element in array not to use spread syntax.', + ); + } + result.push(element); + } + + return result; + } + + isObjectLiteral(node: unknown): node is ObjectExpression { + return isNode(node) && node.type === 'ObjectExpression'; + } + + parseObjectLiteral(obj: unknown): Map { + if (!this.isObjectLiteral(obj)) { + throw new FatalLinkerError(obj as object, 'Unsupported syntax, expected an object literal.'); + } + + const result = new Map(); + + for (const property of obj.properties) { + if (property.type !== 'Property') { + throw new FatalLinkerError( + property as object, + 'Unsupported syntax, expected a property assignment.', + ); + } + + const keyNode = property.key; + + let key: string; + if (keyNode.type === 'Identifier') { + key = keyNode.name; + } else if (this.isStringLiteral(keyNode)) { + key = this.parseStringLiteral(keyNode); + } else if (this.isNumericLiteral(keyNode)) { + key = String(this.parseNumericLiteral(keyNode)); + } else { + throw new FatalLinkerError( + keyNode as object, + 'Unsupported syntax, expected a property name.', + ); + } + + result.set(key, property.value); + } + + return result; + } + + isFunctionExpression(node: unknown): node is FunctionNode | ArrowFunctionExpression { + if (!isNode(node)) { + return false; + } + + return ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ); + } + + parseReturnValue(fn: unknown): unknown { + if (!this.isFunctionExpression(fn)) { + throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.'); + } + + const body = fn.body; + if (!body || !isNode(body)) { + throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function body.'); + } + + if (body.type !== 'BlockStatement') { + return body; + } + + const statements = body.body; + if (statements.length !== 1) { + throw new FatalLinkerError( + body as object, + 'Unsupported syntax, expected a function body with a single return statement.', + ); + } + + const stmt = statements[0]; + if (stmt.type !== 'ReturnStatement') { + throw new FatalLinkerError( + stmt as object, + 'Unsupported syntax, expected a function body with a single return statement.', + ); + } + + if (!stmt.argument) { + throw new FatalLinkerError( + stmt as object, + 'Unsupported syntax, expected function to return a value.', + ); + } + + return stmt.argument; + } + + parseParameters(fn: unknown): unknown[] { + if (!this.isFunctionExpression(fn)) { + throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.'); + } + + return fn.params; + } + + isCallExpression(node: unknown): node is CallExpression { + return isNode(node) && node.type === 'CallExpression'; + } + + parseCallee(call: unknown): unknown { + if (!this.isCallExpression(call)) { + throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.'); + } + + return call.callee; + } + + parseArguments(call: unknown): unknown[] { + if (!this.isCallExpression(call)) { + throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.'); + } + + const result: unknown[] = []; + + for (const arg of call.arguments) { + if (arg.type === 'SpreadElement') { + throw new FatalLinkerError( + arg as object, + 'Unsupported syntax, argument not to use spread syntax.', + ); + } + result.push(arg); + } + + return result; + } + + getRange(node: unknown): Range { + if (!isNode(node) || typeof node.start !== 'number' || typeof node.end !== 'number') { + throw new FatalLinkerError( + node as object, + 'Unable to read range for node - it is missing location information.', + ); + } + + return { + startPos: node.start, + startLine: 0, + startCol: 0, + endPos: node.end, + }; + } +} + +function isMinifiedBooleanLiteral(node: Node): node is UnaryExpression { + if (node.type !== 'UnaryExpression') { + return false; + } + + const arg = node.argument; + + return ( + node.prefix === true && + node.operator === '!' && + arg.type === 'Literal' && + typeof arg.value === 'number' && + (arg.value === 0 || arg.value === 1) + ); +} diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts new file mode 100644 index 000000000000..2025bb25437d --- /dev/null +++ b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts @@ -0,0 +1,117 @@ +/** + * @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 type { EncodedSourceMap } from '@ampproject/remapping'; +import remapping from '@ampproject/remapping'; +import type { DeclarationScope } from '@angular/compiler-cli/linker'; +import { FileLinker, LinkerEnvironment, needsLinking } from '@angular/compiler-cli/linker'; +import type { + AbsoluteFsPath, + ReadonlyFileSystem, +} from '@angular/compiler-cli/src/ngtsc/file_system'; +import type { Logger } from '@angular/compiler-cli/src/ngtsc/logging'; +import MagicString from 'magic-string'; +import { Visitor, parseSync } from 'oxc-parser'; +import { loadInputSourceMap } from '../../../utils/source-map'; +import { OxcAstHost } from './oxc-ast-host'; +import { StringAstFactory } from './string-ast-factory'; + +class InlineDeclarationScope implements DeclarationScope { + getConstantScopeRef(): null { + return null; + } +} + +const noopFileSystem: ReadonlyFileSystem = { + exists: () => false, + readFile: () => '', + resolve: (...paths: string[]) => paths.join('/'), + dirname: (path: string) => path.split('/').slice(0, -1).join('/'), + relative: (_from: string, to: string) => to, +} as unknown as ReadonlyFileSystem; + +const noopLogger: Logger = { + level: 1, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +}; + +export interface OxcLinkerOptions { + sourcemap?: boolean; + jit?: boolean; +} + +/** + * Executes Angular partial declaration linking on the specified JavaScript file + * using `oxc-parser` and `magic-string`. + * + * @param filename The full path to the file. + * @param code The source code content. + * @param options Linker options (sourcemap, jit). + * @returns An object containing the transformed code and optional source map. + */ +export function linkWithOxc(filename: string, code: string, options: OxcLinkerOptions = {}) { + if (!needsLinking(filename, code)) { + return { code, map: undefined }; + } + + const s = new MagicString(code); + const astHost = new OxcAstHost(); + const astFactory = new StringAstFactory(code); + + const linkerEnvironment = LinkerEnvironment.create( + noopFileSystem, + noopLogger, + astHost, + astFactory, + { linkerJitMode: options.jit ?? false }, + ); + + const fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code); + const declarationScope = new InlineDeclarationScope(); + + const { program } = parseSync(filename, code, { range: true }); + let hasLinked = false; + + const visitor = new Visitor({ + CallExpression(node) { + const calleeName = astHost.getSymbolName(node.callee); + if (calleeName && fileLinker.isPartialDeclaration(calleeName)) { + const args = astHost.parseArguments(node); + const linkedCode = fileLinker.linkPartialDeclaration(calleeName, args, declarationScope); + + s.overwrite(node.start, node.end, linkedCode as string); + hasLinked = true; + } + }, + }); + + visitor.visit(program); + + if (!hasLinked) { + return { code, map: undefined }; + } + + let map: string | undefined; + if (options.sourcemap) { + const rawMap = s.generateMap({ hires: true, source: filename }); + const inputMap = loadInputSourceMap(filename, code); + if (inputMap) { + map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString(); + } else { + map = rawMap.toString(); + } + } + + return { + code: s.toString(), + map, + }; +} diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts new file mode 100644 index 000000000000..148c67f9dce3 --- /dev/null +++ b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts @@ -0,0 +1,56 @@ +/** + * @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 { linkWithOxc } from './oxc-linker'; + +describe('linkWithOxc', () => { + it('should not modify code that does not need linking', () => { + const input = 'const x = 1;'; + const result = linkWithOxc('test.js', input); + expect(result.code).toBe(input); + expect(result.map).toBeUndefined(); + }); + + it('should link a partial directive declaration', () => { + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + `; + + const result = linkWithOxc('test.js', input); + expect(result.code).toContain('i0.ɵɵdefineDirective'); + expect(result.code).not.toContain('i0.ɵɵngDeclareDirective'); + }); + + it('should link a partial component declaration', () => { + const input = ` + import * as i0 from "@angular/core"; + export class MyComponent {} + MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyComponent, + isStandalone: true, + selector: "my-cmp", + template: "Hello" + }); + `; + + const result = linkWithOxc('test.js', input); + expect(result.code).toContain('i0.ɵɵdefineComponent'); + expect(result.code).not.toContain('i0.ɵɵngDeclareComponent'); + }); +}); diff --git a/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts new file mode 100644 index 000000000000..681b3a2da1d8 --- /dev/null +++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts @@ -0,0 +1,276 @@ +/** + * @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 type { + AstFactory, + BinaryOperator, + LeadingComment, + ObjectLiteralProperty, + SourceMapRange, + TemplateLiteral, + UnaryOperator, + VariableDeclarationType, +} from '@angular/compiler-cli/src/ngtsc/translator'; +import type { + BuiltInType, + Parameter, +} from '@angular/compiler-cli/src/ngtsc/translator/src/api/ast_factory'; + +/** + * An implementation of `AstFactory` that generates JavaScript code strings directly. + */ +export class StringAstFactory implements AstFactory { + constructor(private readonly sourceCode: string = '') {} + + private render(expr: unknown): string { + if (typeof expr === 'string') { + return expr; + } + if ( + typeof expr === 'object' && + expr !== null && + typeof (expr as { start?: number; end?: number }).start === 'number' && + typeof (expr as { end?: number }).end === 'number' + ) { + const { start, end } = expr as { start: number; end: number }; + + return this.sourceCode.slice(start, end); + } + + return String(expr); + } + + attachComments(_statement: string, _leadingComments: LeadingComment[]): void {} + + createArrayLiteral(elements: unknown[]): string { + return `[${elements.map((e) => this.render(e)).join(', ')}]`; + } + + createAssignment(target: unknown, operator: BinaryOperator, value: unknown): string { + return `(${this.render(target)} ${operator} ${this.render(value)})`; + } + + createBinaryExpression( + leftOperand: unknown, + operator: BinaryOperator, + rightOperand: unknown, + ): string { + return `(${this.render(leftOperand)} ${operator} ${this.render(rightOperand)})`; + } + + createBlock(body: string[]): string { + return `{\n${body.join('\n')}\n}`; + } + + createCallExpression(callee: unknown, args: unknown[], pure: boolean): string { + let renderedCallee = this.render(callee); + if (renderedCallee.startsWith('function') || renderedCallee.includes('=>')) { + renderedCallee = `(${renderedCallee})`; + } + const annotation = pure ? '/*@__PURE__*/ ' : ''; + + return `${annotation}${renderedCallee}(${args.map((a) => this.render(a)).join(', ')})`; + } + + createCallChain(callee: unknown, args: unknown[], pure: boolean, isOptional: boolean): string { + const annotation = pure ? '/*@__PURE__*/ ' : ''; + const operator = isOptional ? '?.' : ''; + + return `${annotation}${this.render(callee)}${operator}(${args.map((a) => this.render(a)).join(', ')})`; + } + + createConditional(condition: unknown, thenExpression: unknown, elseExpression: unknown): string { + return `(${this.render(condition)} ? ${this.render(thenExpression)} : ${this.render(elseExpression)})`; + } + + createElementAccess(expression: unknown, element: unknown): string { + return `${this.render(expression)}[${this.render(element)}]`; + } + + createElementAccessChain(expression: unknown, element: unknown, isOptional: boolean): string { + const operator = isOptional ? '?.' : ''; + + return `${this.render(expression)}${operator}[${this.render(element)}]`; + } + + createExpressionStatement(expression: unknown): string { + return `${this.render(expression)};`; + } + + createFunctionDeclaration( + functionName: string, + parameters: Parameter[], + body: string, + ): string { + const params = parameters.map((p) => p.name).join(', '); + + return `function ${functionName}(${params}) ${body}`; + } + + createFunctionExpression( + functionName: string | null, + parameters: Parameter[], + body: string, + ): string { + const name = functionName ? ` ${functionName}` : ''; + const params = parameters.map((p) => p.name).join(', '); + + return `function${name}(${params}) ${body}`; + } + + createArrowFunctionExpression(parameters: Parameter[], body: unknown): string { + const params = parameters.map((p) => p.name).join(', '); + + return `(${params}) => ${this.render(body)}`; + } + + createDynamicImport(url: unknown): string { + return `import(${this.render(url)})`; + } + + createIdentifier(name: string): string { + return name; + } + + createIfStatement( + condition: unknown, + thenStatement: string, + elseStatement: string | null, + ): string { + const elseClause = elseStatement ? ` else ${elseStatement}` : ''; + + return `if (${this.render(condition)}) ${thenStatement}${elseClause}`; + } + + createLiteral(value: string | number | boolean | null | undefined): string { + if (value === undefined) { + return 'undefined'; + } + + return JSON.stringify(value); + } + + createNewExpression(expression: unknown, args: unknown[]): string { + return `new ${this.render(expression)}(${args.map((a) => this.render(a)).join(', ')})`; + } + + createObjectLiteral(properties: ObjectLiteralProperty[]): string { + const props = properties.map((p) => { + if (p.kind === 'spread') { + return `...${this.render(p.expression)}`; + } + + const key = p.quoted ? JSON.stringify(p.propertyName) : p.propertyName; + + return `${key}: ${this.render(p.value)}`; + }); + + return `{\n${props.join(',\n')}\n}`; + } + + createParenthesizedExpression(expression: unknown): string { + return `(${this.render(expression)})`; + } + + createPropertyAccess(expression: unknown, propertyName: string): string { + return `${this.render(expression)}.${propertyName}`; + } + + createPropertyAccessChain( + expression: unknown, + propertyName: string, + isOptional: boolean, + ): string { + const operator = isOptional ? '?.' : ''; + + return `${this.render(expression)}${operator}.${propertyName}`; + } + + createReturnStatement(expression: unknown | null): string { + return `return${expression !== null ? ` ${this.render(expression)}` : ''};`; + } + + createTaggedTemplate(tag: unknown, template: TemplateLiteral): string { + return `${this.render(tag)}${this.createTemplateLiteral(template)}`; + } + + createTemplateLiteral(template: TemplateLiteral): string { + let result = '`'; + for (let i = 0; i < template.elements.length; i++) { + result += template.elements[i].raw; + if (i < template.expressions.length) { + result += `\${${this.render(template.expressions[i])}}`; + } + } + result += '`'; + + return result; + } + + createThrowStatement(expression: unknown): string { + return `throw ${this.render(expression)};`; + } + + createTypeOfExpression(expression: unknown): string { + return `typeof ${this.render(expression)}`; + } + + createVoidExpression(expression: unknown): string { + return `void ${this.render(expression)}`; + } + + createUnaryExpression(operator: UnaryOperator, operand: unknown): string { + return `${operator}${this.render(operand)}`; + } + + createVariableDeclaration( + variableName: string, + initializer: unknown | null, + variableType: VariableDeclarationType, + _type: string | null = null, + ): string { + const init = initializer !== null ? ` = ${this.render(initializer)}` : ''; + + return `${variableType} ${variableName}${init};`; + } + + createRegularExpressionLiteral(body: string, flags: string | null): string { + return `/${body}/${flags ?? ''}`; + } + + createSpreadElement(expression: unknown): string { + return `...${this.render(expression)}`; + } + + createBuiltInType(_type: BuiltInType): string { + return ''; + } + + createExpressionType(_expression: unknown, _typeParams: string[] | null): string { + return ''; + } + + createArrayType(_elementType: string): string { + return ''; + } + + createMapType(_valueType: string): string { + return ''; + } + + transplantType(_type: string): string { + return ''; + } + + setSourceMapRange( + node: T, + _sourceMapRange: SourceMapRange | null, + ): T { + return node; + } +} 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..11792fba9e45 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts @@ -7,9 +7,7 @@ */ import { type PluginItem, transformAsync } from '@babel/core'; -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'; @@ -48,12 +46,6 @@ export default async function transformJavaScript( return Piscina.move(textEncoder.encode(transformedData)); } -/** - * Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function. - */ -let linkerPluginCreator: - typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | undefined; - async function transformJavaScriptImpl( filename: string, data: string, @@ -94,15 +86,23 @@ async function transformJavaScriptImpl( } } + let code = data; + if (shouldLink) { - // Lazy load the linker plugin only when linking is required - const linkerPlugin = await createLinkerPlugin(options); - babelPlugins.push(linkerPlugin as unknown as PluginItem); + const { linkWithOxc } = await import('../angular/linker/oxc-linker.js'); + const result = linkWithOxc(filename, code, { + sourcemap: useInputSourcemap, + jit: options.jit, + }); + code = result.code; + if (useInputSourcemap && result.map) { + code = removeSourceMappingURL(code); + const base64Map = Buffer.from(result.map).toString('base64'); + code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`; + } } - let code = data; - - // If Babel is needed, run it first + // If Babel is needed for code coverage, run it if (babelPlugins.length > 0) { const result = await transformAsync(code, { filename, @@ -157,44 +157,3 @@ async function requiresLinking(path: string, source: string): Promise { // and the result would be an unnecessary no-op additional plugin pass. return source.includes(LINKER_DECLARATION_PREFIX); } - -async function createLinkerPlugin(options: Omit) { - linkerPluginCreator ??= (await import('@angular/compiler-cli/linker/babel')) - .createEs2015LinkerPlugin; - - const linkerPlugin = linkerPluginCreator({ - linkerJitMode: options.jit, - // This is a workaround until https://github.com/angular/angular/issues/42769 is fixed. - sourceMapping: false, - logger: { - level: 1, // Info level - debug(...args: string[]) { - // eslint-disable-next-line no-console - console.debug(args); - }, - info(...args: string[]) { - // eslint-disable-next-line no-console - console.info(args); - }, - warn(...args: string[]) { - // eslint-disable-next-line no-console - console.warn(args); - }, - error(...args: string[]) { - // eslint-disable-next-line no-console - console.error(args); - }, - }, - fileSystem: { - resolve: path.resolve, - exists: fs.existsSync, - dirname: path.dirname, - relative: path.relative, - readFile: fs.readFileSync, - // Node.JS types don't overlap the Compiler types. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, - }); - - return linkerPlugin; -}