|
6 | 6 | * found in the LICENSE file at https://angular.dev/license |
7 | 7 | */ |
8 | 8 |
|
9 | | -import type { DecodedSourceMap } from '@ampproject/remapping'; |
10 | 9 | import { ConsoleLogger, LogLevel } from '@angular/compiler-cli'; |
11 | 10 | import type { DeclarationScope } from '@angular/compiler-cli/linker'; |
12 | 11 | import { FileLinker, LinkerEnvironment, needsLinking } from '@angular/compiler-cli/linker'; |
13 | 12 | import type { |
14 | 13 | AbsoluteFsPath, |
15 | 14 | ReadonlyFileSystem, |
16 | 15 | } from '@angular/compiler-cli/src/ngtsc/file_system'; |
17 | | -import type { CallExpression, Node } from '@oxc-project/types'; |
18 | | -import MagicString from 'magic-string'; |
19 | | -import { parseSync, visitorKeys } from 'oxc-parser'; |
| 16 | +import type { CallExpression } from '@oxc-project/types'; |
20 | 17 | import { OxcAstHost } from './oxc-ast-host'; |
21 | 18 | import { StringAstFactory } from './string-ast-factory'; |
22 | 19 |
|
@@ -49,131 +46,54 @@ const noopFileSystem: ReadonlyFileSystem = { |
49 | 46 | relative: (_from: string, to: string) => to, |
50 | 47 | } as unknown as ReadonlyFileSystem; |
51 | 48 |
|
52 | | -const SHARED_LOGGER = new ConsoleLogger(LogLevel.info); |
| 49 | +export { needsLinking }; |
53 | 50 |
|
54 | | -const SHARED_AST_HOST = new OxcAstHost(); |
55 | | -const SHARED_DECLARATION_SCOPE = new InlineDeclarationScope(); |
| 51 | +let SHARED_LOGGER: ConsoleLogger; |
| 52 | +let SHARED_AST_HOST: OxcAstHost; |
| 53 | +let SHARED_DECLARATION_SCOPE: InlineDeclarationScope; |
56 | 54 |
|
57 | 55 | /** |
58 | | - * Recursively traverses ESTree AST nodes with subtree pruning. |
59 | | - * When `onCallExpression` returns `true` for a linked `CallExpression`, |
60 | | - * child traversal into `callee` and `arguments` is skipped. |
61 | | - * |
62 | | - * Why subtree pruning is safe for the linker: |
63 | | - * - Angular partial declarations (`ɵɵngDeclareComponent`, `ɵɵngDeclareDirective`, |
64 | | - * etc.) are never nested inside each other. |
65 | | - * - Once a declaration `CallExpression` is linked and replaced, there can never be |
66 | | - * another partial declaration within its metadata argument object. Pruning its |
67 | | - * subtree avoids traversing hundreds of unnecessary metadata argument nodes per |
68 | | - * component. |
69 | | - */ |
70 | | -function visitNode( |
71 | | - node: Node | Node[] | null | undefined, |
72 | | - onCallExpression: (node: CallExpression) => boolean, |
73 | | -): void { |
74 | | - if (node === null || node === undefined || typeof node !== 'object') { |
75 | | - return; |
76 | | - } |
77 | | - |
78 | | - if (Array.isArray(node)) { |
79 | | - for (let i = 0; i < node.length; i++) { |
80 | | - visitNode(node[i], onCallExpression); |
81 | | - } |
82 | | - |
83 | | - return; |
84 | | - } |
85 | | - |
86 | | - const nodeType = node.type; |
87 | | - if (!nodeType) { |
88 | | - return; |
89 | | - } |
90 | | - |
91 | | - if (nodeType === 'CallExpression') { |
92 | | - if (onCallExpression(node)) { |
93 | | - // Subtree pruning: partial declarations cannot be nested, so skip child traversal. |
94 | | - return; |
95 | | - } |
96 | | - } |
97 | | - |
98 | | - const keys = visitorKeys[nodeType]; |
99 | | - if (keys) { |
100 | | - for (let i = 0; i < keys.length; i++) { |
101 | | - const child = (node as unknown as Record<string, Node | Node[] | null | undefined>)[keys[i]]; |
102 | | - if (child !== undefined && child !== null) { |
103 | | - visitNode(child, onCallExpression); |
104 | | - } |
105 | | - } |
106 | | - } |
107 | | -} |
108 | | - |
109 | | -export interface OxcLinkerOptions { |
110 | | - sourcemap?: boolean; |
111 | | - jit?: boolean; |
112 | | - skipCheck?: boolean; |
113 | | -} |
114 | | - |
115 | | -/** |
116 | | - * Executes Angular partial declaration linking on the specified JavaScript file |
117 | | - * using `oxc-parser` and `magic-string`. |
118 | | - * |
119 | | - * @param filename The full path to the file. |
120 | | - * @param code The source code content. |
121 | | - * @param options Linker options (sourcemap, jit, skipCheck). |
122 | | - * @returns An object containing the transformed code and optional source map. |
| 56 | + * Manages Angular partial declaration linking using Oxc AST nodes. |
123 | 57 | */ |
124 | | -export function linkWithOxc(filename: string, code: string, options: OxcLinkerOptions = {}) { |
125 | | - if (!options.skipCheck && !needsLinking(filename, code)) { |
126 | | - return { code, map: undefined }; |
| 58 | +export class OxcLinker { |
| 59 | + readonly #fileLinker: FileLinker<unknown, string, unknown, string | undefined>; |
| 60 | + |
| 61 | + constructor(filename: string, code: string, jit = false) { |
| 62 | + SHARED_LOGGER ??= new ConsoleLogger(LogLevel.info); |
| 63 | + SHARED_AST_HOST ??= new OxcAstHost(); |
| 64 | + SHARED_DECLARATION_SCOPE ??= new InlineDeclarationScope(); |
| 65 | + |
| 66 | + const astFactory = new StringAstFactory(code); |
| 67 | + const linkerEnvironment = LinkerEnvironment.create( |
| 68 | + noopFileSystem, |
| 69 | + SHARED_LOGGER, |
| 70 | + SHARED_AST_HOST, |
| 71 | + astFactory, |
| 72 | + { linkerJitMode: jit, sourceMapping: false }, |
| 73 | + ); |
| 74 | + |
| 75 | + this.#fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code); |
127 | 76 | } |
128 | 77 |
|
129 | | - const astFactory = new StringAstFactory(code); |
130 | | - |
131 | | - const linkerEnvironment = LinkerEnvironment.create( |
132 | | - noopFileSystem, |
133 | | - SHARED_LOGGER, |
134 | | - SHARED_AST_HOST, |
135 | | - astFactory, |
136 | | - { linkerJitMode: options.jit ?? false, sourceMapping: false }, |
137 | | - ); |
138 | | - |
139 | | - const fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code); |
140 | | - const { program } = parseSync(filename, code, { range: true }); |
141 | | - |
142 | | - let s: MagicString | undefined; |
143 | | - let hasLinked = false; |
144 | | - |
145 | | - visitNode(program, (node) => { |
| 78 | + /** |
| 79 | + * Attempts to link an Angular partial declaration CallExpression. |
| 80 | + * |
| 81 | + * @param node The CallExpression AST node to check and link. |
| 82 | + * @returns The linked code string if the node is a partial declaration, or undefined otherwise. |
| 83 | + */ |
| 84 | + linkCallExpression(node: CallExpression): string | undefined { |
146 | 85 | const calleeName = SHARED_AST_HOST.getSymbolName(node.callee); |
147 | | - if (calleeName && fileLinker.isPartialDeclaration(calleeName)) { |
148 | | - const args = SHARED_AST_HOST.parseArguments(node); |
149 | | - const linkedCode = fileLinker.linkPartialDeclaration( |
150 | | - calleeName, |
151 | | - args, |
152 | | - SHARED_DECLARATION_SCOPE, |
153 | | - ); |
154 | | - |
155 | | - s ??= new MagicString(code); |
156 | | - s.overwrite(node.start, node.end, linkedCode as string); |
157 | | - hasLinked = true; |
158 | | - |
159 | | - return true; |
| 86 | + if (!calleeName || !this.#fileLinker.isPartialDeclaration(calleeName)) { |
| 87 | + return undefined; |
160 | 88 | } |
161 | 89 |
|
162 | | - return false; |
163 | | - }); |
| 90 | + const args = SHARED_AST_HOST.parseArguments(node); |
| 91 | + const linkedCode = this.#fileLinker.linkPartialDeclaration( |
| 92 | + calleeName, |
| 93 | + args, |
| 94 | + SHARED_DECLARATION_SCOPE, |
| 95 | + ); |
164 | 96 |
|
165 | | - if (!hasLinked || !s) { |
166 | | - return { code, map: undefined }; |
| 97 | + return linkedCode as string; |
167 | 98 | } |
168 | | - |
169 | | - let map: DecodedSourceMap | undefined; |
170 | | - if (options.sourcemap) { |
171 | | - const rawMap = s.generateDecodedMap({ hires: true, source: filename }); |
172 | | - map = { ...rawMap, version: 3 }; |
173 | | - } |
174 | | - |
175 | | - return { |
176 | | - code: s.toString(), |
177 | | - map, |
178 | | - }; |
179 | 99 | } |
0 commit comments