Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 3 additions & 51 deletions packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { MagicString } from 'magic-string';
import assert from 'node:assert';
import { deserialize } from 'node:v8';
import { workerData } from 'node:worker_threads';
import { parseSync, visitorKeys } from 'oxc-parser';
import { parseSync } from 'oxc-parser';
import { traversePostOrder } from '../oxc/traversal';
import { loadLocaleData } from './i18n-locale-plugin';
import { createSharedTranslationProxy } from './i18n-translation-reader';

Expand Down Expand Up @@ -284,55 +285,6 @@ async function loadLocalizeTools(): Promise<LocalizeUtilityModule> {
return localizeToolsModule;
}

/**
* Traverses ESTree AST nodes in post-order (bottom-up) without recursion.
* Bottom-up traversal ensures that nested `$localize` expressions are transformed and
* written to MagicString before outer containing templates are evaluated.
*
* @param root The root AST node to traverse.
* @param onExit Callback invoked on each AST node in post-order.
*/
function walkAstPostOrder(root: Node, onExit: (node: Node) => void): void {
const traverseStack: Node[] = [root];
const postOrderNodes: Node[] = [];

while (traverseStack.length > 0) {
const current = traverseStack.pop();
if (!current) {
continue;
}

postOrderNodes.push(current);

const keys = visitorKeys[current.type];
if (!keys) {
continue;
}

for (let i = 0; i < keys.length; i++) {
const child = (current as unknown as Record<string, Node | Node[]>)[keys[i]];
if (!child) {
continue;
}

if (Array.isArray(child)) {
for (const item of child) {
if (item) {
traverseStack.push(item);
}
}
} else {
traverseStack.push(child);
}
}
}

// Process collected nodes in reverse order to achieve bottom-up (post-order) traversal
for (let i = postOrderNodes.length - 1; i >= 0; i--) {
onExit(postOrderNodes[i]);
}
}

/**
* Metadata for a `$localize` tagged template expression extracted from the AST.
*/
Expand Down Expand Up @@ -372,7 +324,7 @@ function extractLocalizeMetadata(filename: string, code: string): FileLocalizeMe
const localeInsertSites: { start: number; end: number }[] = [];
let diagnostics: string[] | undefined;

walkAstPostOrder(program, (node) => {
traversePostOrder(program, (node) => {
if (node.type === 'Literal') {
if (typeof node.value === 'string' && node.value === '___NG_LOCALE_INSERT___') {
localeInsertSites.push({ start: node.start, end: node.end });
Expand Down
218 changes: 88 additions & 130 deletions packages/angular/build/src/tools/oxc/oxc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import type { DecodedSourceMap } from '@ampproject/remapping';
import { needsLinking } from '@angular/compiler-cli/linker';
import type { BindingIdentifier, Class, Node } from '@oxc-project/types';
import { MagicString } from 'magic-string';
import { Visitor, parseSync } from 'oxc-parser';
import { parseSync } from 'oxc-parser';
import { OxcLinker } from '../angular/linker/oxc-linker';
import { traversePostOrder } from './traversal';

export interface OxcTransformOptions {
sourcemap?: boolean;
Expand Down Expand Up @@ -320,11 +321,6 @@ export function transform(filename: string, code: string, options: OxcTransformO
return editedRanges.some((r) => start >= r.start && end <= r.end);
}

// Track function nesting depth and closest function expression wrapper
let functionDepth = 0;
let classDepth = 0;
const functionStack: Node[] = [];

/**
* Scans and rewrites TypeScript emitted enum declarations in the statement block.
* Wraps enum statements inside a pure IIFE assignable directly to the enum variable.
Expand Down Expand Up @@ -630,153 +626,115 @@ export function transform(filename: string, code: string, options: OxcTransformO
}
}

const visitor = new Visitor({
ClassDeclaration(node) {
classDepth++;
},
'ClassDeclaration:exit'() {
classDepth--;
},
ClassExpression(node) {
classDepth++;
},
'ClassExpression:exit'() {
classDepth--;
},
FunctionDeclaration(node) {
functionDepth++;
functionStack.push(node);
},
'FunctionDeclaration:exit'() {
functionDepth--;
functionStack.pop();
},
FunctionExpression(node) {
functionDepth++;
functionStack.push(node);
},
'FunctionExpression:exit'() {
functionDepth--;
functionStack.pop();
},
ArrowFunctionExpression(node) {
functionDepth++;
functionStack.push(node);
},
'ArrowFunctionExpression:exit'() {
functionDepth--;
functionStack.pop();
},
'Program:exit'(node) {
if (advancedOptimizations) {
adjustTypeScriptEnumsInStatements(node.body);
adjustStaticMembersInStatements(node.body);
}
},
'BlockStatement:exit'(node) {
if (advancedOptimizations) {
adjustTypeScriptEnumsInStatements(node.body);
adjustStaticMembersInStatements(node.body);
}
},
CallExpression(node) {
if (isAlreadyEdited(node.start, node.end)) {
return;
}
traversePostOrder(program, (node, { functionDepth, classDepth, parentFunc }) => {
switch (node.type) {
case 'Program':
case 'BlockStatement':
if (advancedOptimizations) {
adjustTypeScriptEnumsInStatements(node.body);
adjustStaticMembersInStatements(node.body);
}
break;

case 'CallExpression': {
if (isAlreadyEdited(node.start, node.end)) {
return;
}

if (linker) {
const linkedCode = linker.linkCallExpression(node);
if (linkedCode !== undefined) {
source.overwrite(node.start, node.end, linkedCode);
markEdited(node.start, node.end);

if (linker) {
const linkedCode = linker.linkCallExpression(node);
if (linkedCode !== undefined) {
source.overwrite(node.start, node.end, linkedCode);
markEdited(node.start, node.end);
return;
}
}

if (!advancedOptimizations) {
return;
}
}

if (!advancedOptimizations) {
return;
}
// 1. Elide Angular Metadata check
let calleeName: string | undefined;
if (node.callee.type === 'Identifier') {
calleeName = node.callee.name;
} else if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier'
) {
calleeName = node.callee.property.name;
}

// 1. Elide Angular Metadata check
let calleeName: string | undefined;
if (node.callee.type === 'Identifier') {
calleeName = node.callee.name;
} else if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier'
) {
calleeName = node.callee.property.name;
}
if (calleeName && angularMetadataFunctions.has(calleeName)) {
if (
parentFunc &&
(parentFunc.type === 'FunctionExpression' ||
parentFunc.type === 'ArrowFunctionExpression')
) {
source.overwrite(node.start, node.end, 'void 0');
markEdited(node.start, node.end);

return;
}
}

if (calleeName && angularMetadataFunctions.has(calleeName)) {
const parentFunc = functionStack[functionStack.length - 1];
// 2. Mark Top-Level Pure Functions check
if (!pureAnnotate || functionDepth > 0 || classDepth > 0 || topLevelSafeMode) {
return;
}

const callee = unwrapParentheses(node.callee);
if (
parentFunc &&
(parentFunc.type === 'FunctionExpression' ||
parentFunc.type === 'ArrowFunctionExpression')
(callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') &&
node.arguments.length !== 0
) {
source.overwrite(node.start, node.end, 'void 0');
markEdited(node.start, node.end);

return;
}
}

// 2. Mark Top-Level Pure Functions check
if (!pureAnnotate || functionDepth > 0 || classDepth > 0 || topLevelSafeMode) {
return;
}
if (
callee.type === 'Identifier' &&
(isTslibHelperName(callee.name) || isBabelHelperName(callee.name))
) {
return;
}

const callee = unwrapParentheses(node.callee);
if (
(callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') &&
node.arguments.length !== 0
) {
return;
if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
}
break;
}

if (
callee.type === 'Identifier' &&
(isTslibHelperName(callee.name) || isBabelHelperName(callee.name))
) {
return;
}
case 'NewExpression': {
if (
!advancedOptimizations ||
!pureAnnotate ||
functionDepth > 0 ||
classDepth > 0 ||
isAlreadyEdited(node.start, node.end)
) {
return;
}

if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
}
},
NewExpression(node) {
if (
!advancedOptimizations ||
!pureAnnotate ||
functionDepth > 0 ||
classDepth > 0 ||
isAlreadyEdited(node.start, node.end)
) {
return;
}
if (!topLevelSafeMode) {
if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
}

if (!topLevelSafeMode) {
if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
return;
}

return;
}

const callee = node.callee;
if (callee.type === 'Identifier' && sideEffectFreeConstructors.has(callee.name)) {
if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
const newCallee = node.callee;
if (newCallee.type === 'Identifier' && sideEffectFreeConstructors.has(newCallee.name)) {
if (!hasPureComment(node.start)) {
source.appendLeft(node.start, '/*#__PURE__*/ ');
}
}
break;
}
},
}
});

visitor.visit(program);

let map: DecodedSourceMap | undefined;
if (options.sourcemap) {
const rawMap = source.generateDecodedMap({ hires: true, source: filename });
Expand Down
Loading
Loading