Skip to content

Commit b7fb852

Browse files
committed
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.
1 parent 5c8a055 commit b7fb852

6 files changed

Lines changed: 951 additions & 54 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { AstHost, Range } from '@angular/compiler-cli/linker';
10+
import { FatalLinkerError } from '@angular/compiler-cli/linker';
11+
import type {
12+
ArrayExpression,
13+
ArrowFunctionExpression,
14+
BooleanLiteral,
15+
CallExpression,
16+
Function as FunctionNode,
17+
Node,
18+
NullLiteral,
19+
NumericLiteral,
20+
ObjectExpression,
21+
StringLiteral,
22+
UnaryExpression,
23+
} from '@oxc-project/types';
24+
25+
function isNode(node: unknown): node is Node {
26+
return typeof node === 'object' && node !== null && 'type' in node;
27+
}
28+
29+
/**
30+
* An implementation of `AstHost` that queries information from `oxc-parser` AST nodes.
31+
*/
32+
export class OxcAstHost implements AstHost<unknown> {
33+
getSymbolName(node: unknown): string | null {
34+
if (!isNode(node)) {
35+
return null;
36+
}
37+
38+
if (node.type === 'Identifier') {
39+
return node.name;
40+
} else if (node.type === 'MemberExpression') {
41+
if (!node.computed && node.property.type === 'Identifier') {
42+
return node.property.name;
43+
}
44+
}
45+
46+
return null;
47+
}
48+
49+
isStringLiteral(node: unknown): node is StringLiteral {
50+
return isNode(node) && node.type === 'Literal' && typeof node.value === 'string';
51+
}
52+
53+
parseStringLiteral(str: unknown): string {
54+
if (!this.isStringLiteral(str)) {
55+
throw new FatalLinkerError(str as object, 'Unsupported syntax, expected a string literal.');
56+
}
57+
58+
return str.value;
59+
}
60+
61+
isNumericLiteral(node: unknown): node is NumericLiteral {
62+
return isNode(node) && node.type === 'Literal' && typeof node.value === 'number';
63+
}
64+
65+
parseNumericLiteral(num: unknown): number {
66+
if (!this.isNumericLiteral(num)) {
67+
throw new FatalLinkerError(num as object, 'Unsupported syntax, expected a numeric literal.');
68+
}
69+
70+
return num.value;
71+
}
72+
73+
isBooleanLiteral(node: unknown): node is BooleanLiteral | UnaryExpression {
74+
if (!isNode(node)) {
75+
return false;
76+
}
77+
78+
return (
79+
(node.type === 'Literal' && typeof node.value === 'boolean') || isMinifiedBooleanLiteral(node)
80+
);
81+
}
82+
83+
parseBooleanLiteral(bool: unknown): boolean {
84+
if (isNode(bool)) {
85+
if (bool.type === 'Literal' && typeof bool.value === 'boolean') {
86+
return bool.value;
87+
}
88+
if (isMinifiedBooleanLiteral(bool)) {
89+
return !bool.argument.value;
90+
}
91+
}
92+
93+
throw new FatalLinkerError(bool as object, 'Unsupported syntax, expected a boolean literal.');
94+
}
95+
96+
isNull(node: unknown): node is NullLiteral {
97+
return isNode(node) && node.type === 'Literal' && node.value === null;
98+
}
99+
100+
isArrayLiteral(node: unknown): node is ArrayExpression {
101+
return isNode(node) && node.type === 'ArrayExpression';
102+
}
103+
104+
parseArrayLiteral(array: unknown): unknown[] {
105+
if (!this.isArrayLiteral(array)) {
106+
throw new FatalLinkerError(array as object, 'Unsupported syntax, expected an array literal.');
107+
}
108+
109+
const result: unknown[] = [];
110+
111+
for (const element of array.elements) {
112+
if (element === null) {
113+
throw new FatalLinkerError(
114+
array as object,
115+
'Unsupported syntax, element in array not to be empty.',
116+
);
117+
}
118+
if (element.type === 'SpreadElement') {
119+
throw new FatalLinkerError(
120+
element as object,
121+
'Unsupported syntax, element in array not to use spread syntax.',
122+
);
123+
}
124+
result.push(element);
125+
}
126+
127+
return result;
128+
}
129+
130+
isObjectLiteral(node: unknown): node is ObjectExpression {
131+
return isNode(node) && node.type === 'ObjectExpression';
132+
}
133+
134+
parseObjectLiteral(obj: unknown): Map<string, unknown> {
135+
if (!this.isObjectLiteral(obj)) {
136+
throw new FatalLinkerError(obj as object, 'Unsupported syntax, expected an object literal.');
137+
}
138+
139+
const result = new Map<string, unknown>();
140+
141+
for (const property of obj.properties) {
142+
if (property.type !== 'Property') {
143+
throw new FatalLinkerError(
144+
property as object,
145+
'Unsupported syntax, expected a property assignment.',
146+
);
147+
}
148+
149+
const keyNode = property.key;
150+
151+
let key: string;
152+
if (keyNode.type === 'Identifier') {
153+
key = keyNode.name;
154+
} else if (this.isStringLiteral(keyNode)) {
155+
key = keyNode.value;
156+
} else if (this.isNumericLiteral(keyNode)) {
157+
key = String(keyNode.value);
158+
} else {
159+
throw new FatalLinkerError(
160+
keyNode as object,
161+
'Unsupported syntax, expected a property name.',
162+
);
163+
}
164+
165+
result.set(key, property.value);
166+
}
167+
168+
return result;
169+
}
170+
171+
isFunctionExpression(node: unknown): node is FunctionNode | ArrowFunctionExpression {
172+
if (!isNode(node)) {
173+
return false;
174+
}
175+
176+
return (
177+
node.type === 'FunctionDeclaration' ||
178+
node.type === 'FunctionExpression' ||
179+
node.type === 'ArrowFunctionExpression'
180+
);
181+
}
182+
183+
parseReturnValue(fn: unknown): unknown {
184+
if (!this.isFunctionExpression(fn)) {
185+
throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
186+
}
187+
188+
const body = fn.body;
189+
if (!body || !isNode(body)) {
190+
throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function body.');
191+
}
192+
193+
if (body.type !== 'BlockStatement') {
194+
return body;
195+
}
196+
197+
const statements = body.body;
198+
if (statements.length !== 1) {
199+
throw new FatalLinkerError(
200+
body as object,
201+
'Unsupported syntax, expected a function body with a single return statement.',
202+
);
203+
}
204+
205+
const stmt = statements[0];
206+
if (stmt.type !== 'ReturnStatement') {
207+
throw new FatalLinkerError(
208+
stmt as object,
209+
'Unsupported syntax, expected a function body with a single return statement.',
210+
);
211+
}
212+
213+
if (!stmt.argument) {
214+
throw new FatalLinkerError(
215+
stmt as object,
216+
'Unsupported syntax, expected function to return a value.',
217+
);
218+
}
219+
220+
return stmt.argument;
221+
}
222+
223+
parseParameters(fn: unknown): unknown[] {
224+
if (!this.isFunctionExpression(fn)) {
225+
throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
226+
}
227+
228+
return fn.params;
229+
}
230+
231+
isCallExpression(node: unknown): node is CallExpression {
232+
return isNode(node) && node.type === 'CallExpression';
233+
}
234+
235+
parseCallee(call: unknown): unknown {
236+
if (!this.isCallExpression(call)) {
237+
throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
238+
}
239+
240+
return call.callee;
241+
}
242+
243+
parseArguments(call: unknown): unknown[] {
244+
if (!this.isCallExpression(call)) {
245+
throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
246+
}
247+
248+
const result: unknown[] = [];
249+
250+
for (const arg of call.arguments) {
251+
if (arg.type === 'SpreadElement') {
252+
throw new FatalLinkerError(
253+
arg as object,
254+
'Unsupported syntax, argument not to use spread syntax.',
255+
);
256+
}
257+
result.push(arg);
258+
}
259+
260+
return result;
261+
}
262+
263+
getRange(node: unknown): Range {
264+
if (!isNode(node) || typeof node.start !== 'number' || typeof node.end !== 'number') {
265+
throw new FatalLinkerError(
266+
node as object,
267+
'Unable to read range for node - it is missing location information.',
268+
);
269+
}
270+
271+
return {
272+
startPos: node.start,
273+
startLine: 0,
274+
startCol: 0,
275+
endPos: node.end,
276+
};
277+
}
278+
}
279+
280+
function isMinifiedBooleanLiteral(
281+
node: Node,
282+
): node is UnaryExpression & { argument: NumericLiteral } {
283+
if (node.type !== 'UnaryExpression') {
284+
return false;
285+
}
286+
287+
const arg = node.argument;
288+
289+
return (
290+
node.prefix === true &&
291+
node.operator === '!' &&
292+
arg.type === 'Literal' &&
293+
typeof arg.value === 'number' &&
294+
(arg.value === 0 || arg.value === 1)
295+
);
296+
}

0 commit comments

Comments
 (0)