@ohos-ports/ember-estree 0.6.11-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +420 -0
- package/package.json +63 -0
- package/src/index.d.ts +68 -0
- package/src/index.js +3 -0
- package/src/parse.js +388 -0
- package/src/print.js +1095 -0
- package/src/tokens.js +177 -0
- package/src/transforms.js +279 -0
package/src/parse.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Strategy:
|
|
3
|
+
*
|
|
4
|
+
* 1. parse out the <template>...</template> regions (content-tag)
|
|
5
|
+
* 2. create placeholder JS for the template regions (backtick/static-block, same char length)
|
|
6
|
+
* 3. parse as js/ts — default: oxc-parser, or a custom parser via options
|
|
7
|
+
* 4. splice in processed Glimmer ASTs, invoking visitors during traversal
|
|
8
|
+
* 5. Merge Glimmer visitor keys into the result
|
|
9
|
+
* 6. Done
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseSync, visitorKeys as oxcVisitorKeys } from "@ohos-ports/oxc-parser";
|
|
13
|
+
import { Preprocessor } from "content-tag";
|
|
14
|
+
|
|
15
|
+
import { processTemplate, DocumentLines, glimmerVisitorKeys, setParent } from "./transforms.js";
|
|
16
|
+
|
|
17
|
+
// Base visitor-keys map for the outer-AST walk: oxc-parser's own keys (covers
|
|
18
|
+
// standard ESTree + TS), plus the `File` wrapper we add on the default path,
|
|
19
|
+
// plus Glimmer's keys. Used to iterate only declared child slots instead of
|
|
20
|
+
// every enumerable property on every node.
|
|
21
|
+
//
|
|
22
|
+
// When `options.parser` returns `visitorKeys`, callers merge on top — but if
|
|
23
|
+
// their parser's AST is oxc-compatible, this base is already sufficient.
|
|
24
|
+
const DEFAULT_VISITOR_KEYS = {
|
|
25
|
+
...oxcVisitorKeys,
|
|
26
|
+
File: ["program"],
|
|
27
|
+
...glimmerVisitorKeys,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Swap `oldNode` for `newNode` in whichever slot of `parent` currently holds it.
|
|
31
|
+
// Used to splice a GlimmerTemplate directly into the outer AST without
|
|
32
|
+
// allocating new ancestor objects — keeps WeakMap-keyed data (scope manager,
|
|
33
|
+
// esTreeNodeToTSNodeMap) attached to the existing nodes.
|
|
34
|
+
function replaceInParent(parent, oldNode, newNode) {
|
|
35
|
+
for (const key of Object.keys(parent)) {
|
|
36
|
+
const v = parent[key];
|
|
37
|
+
if (v === oldNode) {
|
|
38
|
+
parent[key] = newNode;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(v)) {
|
|
42
|
+
const idx = v.indexOf(oldNode);
|
|
43
|
+
if (idx !== -1) {
|
|
44
|
+
v[idx] = newNode;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const preprocessor = new Preprocessor();
|
|
53
|
+
|
|
54
|
+
// Node types that placeholders parse into (backtick/static-block format)
|
|
55
|
+
const PLACEHOLDER_TYPES = new Set([
|
|
56
|
+
"ExpressionStatement",
|
|
57
|
+
"StaticBlock",
|
|
58
|
+
"TemplateLiteral",
|
|
59
|
+
"ExportDefaultDeclaration",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse Ember source and return an ESTree-compatible AST.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} source
|
|
66
|
+
* @param {object} [options]
|
|
67
|
+
* @param {string} [options.filePath] - File path for language detection
|
|
68
|
+
* @param {boolean} [options.tokens] - Generate a flat token stream on the AST (needed by ESLint; skipped by default)
|
|
69
|
+
* @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
|
|
70
|
+
* @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }.
|
|
71
|
+
* Recommended to return `visitorKeys` describing the parser's AST; when omitted, oxc-parser's
|
|
72
|
+
* keys are used (fine for oxc-compatible ASTs, incomplete for parsers that emit bespoke node types).
|
|
73
|
+
* @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
|
|
74
|
+
* handlers, or a factory `(outerAst) => handlers` invoked once after parsing (before any
|
|
75
|
+
* template splicing) to give callers a view of the raw JS/TS tree. Handlers fire on every
|
|
76
|
+
* node during traversal — outer JS/TS nodes AND spliced Glimmer subtrees — in a single pass.
|
|
77
|
+
* The pseudo-type `GlimmerBlockParams` fires on any node that carries `blockParams`.
|
|
78
|
+
* @return {object}
|
|
79
|
+
*/
|
|
80
|
+
export function toTree(source, options = {}) {
|
|
81
|
+
const generateTokens = !!options.tokens;
|
|
82
|
+
|
|
83
|
+
if (options.templateOnly) {
|
|
84
|
+
return processTemplate(source, new DocumentLines(source), {
|
|
85
|
+
templateRange: [0, source.length],
|
|
86
|
+
tokens: generateTokens,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let parseResults = preprocessor.parse(source);
|
|
91
|
+
let js = toPlaceholderJS(source, parseResults);
|
|
92
|
+
|
|
93
|
+
const useCustomParser = !!options.parser;
|
|
94
|
+
|
|
95
|
+
// Parse the placeholder JS — use custom parser or default oxc
|
|
96
|
+
let result;
|
|
97
|
+
if (useCustomParser) {
|
|
98
|
+
result = options.parser(js);
|
|
99
|
+
if (!result.ast) {
|
|
100
|
+
result = { ast: result };
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
let filename = options.filePath || "input.ts";
|
|
104
|
+
if (filename.includes(".gts")) {
|
|
105
|
+
filename = filename.replace(/\.gts$/, ".ts");
|
|
106
|
+
}
|
|
107
|
+
let oxcResult = parseSync(filename, js);
|
|
108
|
+
result = {
|
|
109
|
+
ast: {
|
|
110
|
+
type: "File",
|
|
111
|
+
program: oxcResult.program,
|
|
112
|
+
comments: oxcResult.comments || [],
|
|
113
|
+
start: oxcResult.program.start,
|
|
114
|
+
end: oxcResult.program.end,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resolve user visitors against the outer AST. A plain object is used
|
|
120
|
+
// as-is; a factory is called once so callers can introspect the raw
|
|
121
|
+
// JS/TS tree before any template splicing. Default to `{}` so downstream
|
|
122
|
+
// dispatch can be a bare `visitors[type]` lookup without null-guards.
|
|
123
|
+
const visitors =
|
|
124
|
+
typeof options.visitors === "function"
|
|
125
|
+
? (options.visitors(result.ast) ?? {})
|
|
126
|
+
: (options.visitors ?? {});
|
|
127
|
+
const hasVisitors = Object.keys(visitors).length > 0;
|
|
128
|
+
// Guard against dispatching a handler twice on the same node.
|
|
129
|
+
// Visitors that relocate nodes (e.g. moving Glimmer comments into
|
|
130
|
+
// `program.comments`) would otherwise fire a second time when the walk
|
|
131
|
+
// reaches the new location.
|
|
132
|
+
const seen = new WeakSet();
|
|
133
|
+
const hasTemplates = parseResults.length > 0;
|
|
134
|
+
|
|
135
|
+
// Nothing to walk — attach visitor keys and return.
|
|
136
|
+
if (!hasTemplates && !hasVisitors) {
|
|
137
|
+
if (useCustomParser) {
|
|
138
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
142
|
+
return result.ast;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const codeLines = hasTemplates ? new DocumentLines(source) : null;
|
|
146
|
+
const templateInfos = [];
|
|
147
|
+
const templateRangeByStart = hasTemplates
|
|
148
|
+
? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
|
|
149
|
+
: null;
|
|
150
|
+
|
|
151
|
+
// Process a matched placeholder node: create Glimmer AST and tokens.
|
|
152
|
+
// `placeholderNode` is the original JS/TS node being swapped out; we stash
|
|
153
|
+
// it on templateInfos so consumers can forward its parser-services mapping
|
|
154
|
+
// (e.g. esTreeNodeToTSNodeMap) onto the GlimmerTemplate that replaces it.
|
|
155
|
+
function processPlaceholder(parseResult, placeholderNode) {
|
|
156
|
+
let templateContent = parseResult.contents;
|
|
157
|
+
let contentRange = [
|
|
158
|
+
parseResult.contentRange.startUtf16Codepoint,
|
|
159
|
+
parseResult.contentRange.endUtf16Codepoint,
|
|
160
|
+
];
|
|
161
|
+
let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
|
|
162
|
+
|
|
163
|
+
const { ast } = processTemplate(templateContent, codeLines, {
|
|
164
|
+
templateRange: contentRange,
|
|
165
|
+
tokens: generateTokens,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Fix the Template root to cover the full <template>...</template> range
|
|
169
|
+
ast.range = fullRange;
|
|
170
|
+
ast.start = fullRange[0];
|
|
171
|
+
ast.end = fullRange[1];
|
|
172
|
+
ast.loc = {
|
|
173
|
+
start: codeLines.offsetToPosition(fullRange[0]),
|
|
174
|
+
end: codeLines.offsetToPosition(fullRange[1]),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (generateTokens) {
|
|
178
|
+
// Add tokens for the <template> and </template> tags
|
|
179
|
+
const openEnd = contentRange[0];
|
|
180
|
+
const closeStart = contentRange[1];
|
|
181
|
+
const openTag = source.slice(fullRange[0], openEnd);
|
|
182
|
+
const closeTag = source.slice(closeStart, fullRange[1]);
|
|
183
|
+
const makeToken = (value, range) => ({
|
|
184
|
+
type: "Punctuator",
|
|
185
|
+
value,
|
|
186
|
+
range,
|
|
187
|
+
start: range[0],
|
|
188
|
+
end: range[1],
|
|
189
|
+
loc: {
|
|
190
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
191
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
ast.tokens = [
|
|
195
|
+
makeToken(openTag, [fullRange[0], openEnd]),
|
|
196
|
+
...(ast.tokens || []),
|
|
197
|
+
makeToken(closeTag, [closeStart, fullRange[1]]),
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
templateInfos.push({ utf16Range: fullRange, ast, placeholder: placeholderNode });
|
|
202
|
+
return ast;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if a node matches a template range
|
|
206
|
+
function matchPlaceholder(node) {
|
|
207
|
+
let range = node.range || [node.start, node.end];
|
|
208
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration) {
|
|
209
|
+
const decl = node.declaration;
|
|
210
|
+
range = decl.range || [decl.start, decl.end];
|
|
211
|
+
}
|
|
212
|
+
const parseResult = templateRangeByStart.get(range[0]);
|
|
213
|
+
if (
|
|
214
|
+
!parseResult ||
|
|
215
|
+
(parseResult.range.endUtf16Codepoint !== range[1] &&
|
|
216
|
+
parseResult.range.endUtf16Codepoint !== range[1] + 1)
|
|
217
|
+
) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return parseResult;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Walk the outer AST keyed on visitorKeys — iterating only declared child
|
|
224
|
+
// slots instead of every enumerable property on every node. Custom parsers
|
|
225
|
+
// may supply their own keys; those override the defaults for types they
|
|
226
|
+
// recognise, and Glimmer keys stay on top for the spliced subtrees.
|
|
227
|
+
const allVisitorKeys =
|
|
228
|
+
useCustomParser && result.visitorKeys
|
|
229
|
+
? { ...DEFAULT_VISITOR_KEYS, ...result.visitorKeys, ...glimmerVisitorKeys }
|
|
230
|
+
: DEFAULT_VISITOR_KEYS;
|
|
231
|
+
|
|
232
|
+
function walkWithKeys(node, parentPath) {
|
|
233
|
+
if (!node || !node.type) return;
|
|
234
|
+
|
|
235
|
+
if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
|
|
236
|
+
const parseResult = matchPlaceholder(node);
|
|
237
|
+
if (parseResult) {
|
|
238
|
+
// Splice in place: write the GlimmerTemplate directly into the parent's
|
|
239
|
+
// slot instead of allocating new ancestor objects. This preserves node
|
|
240
|
+
// identity for every ancestor, which matters for WeakMap-keyed data
|
|
241
|
+
// held by custom parsers (scope manager, esTreeNodeToTSNodeMap).
|
|
242
|
+
const ast = processPlaceholder(parseResult, node);
|
|
243
|
+
const parent = parentPath?.node ?? null;
|
|
244
|
+
if (parent) replaceInParent(parent, node, ast);
|
|
245
|
+
setParent(ast, parent);
|
|
246
|
+
// Recurse into the Glimmer subtree so visitors fire on its nodes too.
|
|
247
|
+
// The Glimmer root's parentPath reflects its true JS parent — the
|
|
248
|
+
// placeholder (TemplateLiteral / StaticBlock) is an internal artifact.
|
|
249
|
+
if (hasVisitors) walkWithKeys(ast, parentPath);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
255
|
+
|
|
256
|
+
if (hasVisitors && !seen.has(node)) {
|
|
257
|
+
seen.add(node);
|
|
258
|
+
const handler = visitors[node.type];
|
|
259
|
+
if (handler) handler(node, path);
|
|
260
|
+
if ("blockParams" in node && visitors.GlimmerBlockParams) {
|
|
261
|
+
visitors.GlimmerBlockParams(node, path);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const keys = allVisitorKeys[node.type];
|
|
266
|
+
if (!keys) return;
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const child = node[key];
|
|
269
|
+
if (!child) continue;
|
|
270
|
+
if (Array.isArray(child)) {
|
|
271
|
+
for (const item of child) {
|
|
272
|
+
if (item && typeof item === "object" && item.type) {
|
|
273
|
+
walkWithKeys(item, path);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} else if (typeof child === "object" && child.type) {
|
|
277
|
+
walkWithKeys(child, path);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
walkWithKeys(result.ast, null);
|
|
283
|
+
|
|
284
|
+
// Splice template tokens into the AST token stream.
|
|
285
|
+
//
|
|
286
|
+
// `tokens` is the flat lexed stream (keywords, punctuators, identifiers,
|
|
287
|
+
// literals) that ESLint, formatters, and source-map tooling consume —
|
|
288
|
+
// `SourceCode.getTokens()` reads it directly.
|
|
289
|
+
//
|
|
290
|
+
// We replaced each <template>...</template> region with a backtick
|
|
291
|
+
// placeholder before handing the source to the JS/TS parser, so the
|
|
292
|
+
// parser's tokens for those ranges describe the placeholder, not the
|
|
293
|
+
// real source. Here we swap them out for the real lexemes:
|
|
294
|
+
// 1. a fabricated `<template>` Punctuator (added in processPlaceholder)
|
|
295
|
+
// 2. the Glimmer AST's own tokens (from transforms.js)
|
|
296
|
+
// 3. a fabricated `</template>` Punctuator
|
|
297
|
+
// so consumers see a position-accurate token stream matching the
|
|
298
|
+
// original source byte-for-byte across JS and Glimmer regions.
|
|
299
|
+
//
|
|
300
|
+
// Tokens are sorted by range, so use binary search for O(log n) lookup.
|
|
301
|
+
// Only splice if the caller asked for tokens — otherwise `ti.ast.tokens`
|
|
302
|
+
// wasn't populated by processPlaceholder, and a custom parser may still
|
|
303
|
+
// have returned its own token stream we shouldn't touch.
|
|
304
|
+
const astRoot = result.ast.program || result.ast;
|
|
305
|
+
if (generateTokens && astRoot.tokens) {
|
|
306
|
+
for (const ti of templateInfos) {
|
|
307
|
+
const [tStart, tEnd] = ti.utf16Range;
|
|
308
|
+
const tokens = astRoot.tokens;
|
|
309
|
+
// Binary search for first token with range[0] >= tStart
|
|
310
|
+
let lo = 0;
|
|
311
|
+
let hi = tokens.length;
|
|
312
|
+
while (lo < hi) {
|
|
313
|
+
const mid = (lo + hi) >>> 1;
|
|
314
|
+
if (tokens[mid].range[0] < tStart) lo = mid + 1;
|
|
315
|
+
else hi = mid;
|
|
316
|
+
}
|
|
317
|
+
const firstIdx = lo;
|
|
318
|
+
if (firstIdx >= tokens.length || tokens[firstIdx].range[0] >= tEnd) continue;
|
|
319
|
+
let lastIdx = firstIdx;
|
|
320
|
+
while (lastIdx < tokens.length && tokens[lastIdx].range[1] <= tEnd) {
|
|
321
|
+
lastIdx++;
|
|
322
|
+
}
|
|
323
|
+
tokens.splice(firstIdx, lastIdx - firstIdx, ...ti.ast.tokens);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (useCustomParser) {
|
|
328
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
329
|
+
result.templateInfos = templateInfos;
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Default path: return bare AST with visitorKeys attached
|
|
334
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
335
|
+
return result.ast;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const parse = toTree;
|
|
339
|
+
|
|
340
|
+
// ── Placeholder JS ────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Replaces <template>...</template> regions with placeholder expressions
|
|
344
|
+
* of the same character length that are valid JS/TS.
|
|
345
|
+
*
|
|
346
|
+
* Expression templates become: `content ` (backtick, space-padded)
|
|
347
|
+
* Class member templates become: static{`content `} (static block, space-padded)
|
|
348
|
+
*
|
|
349
|
+
* This format is compatible with all JS/TS parsers including
|
|
350
|
+
* oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
|
|
351
|
+
*/
|
|
352
|
+
function toPlaceholderJS(source, parseResults) {
|
|
353
|
+
// Build result in forward order using parts array (avoids intermediate string allocations)
|
|
354
|
+
const parts = [];
|
|
355
|
+
let cursor = 0;
|
|
356
|
+
|
|
357
|
+
for (const pr of parseResults) {
|
|
358
|
+
const start = pr.range.startUtf16Codepoint;
|
|
359
|
+
const end = pr.range.endUtf16Codepoint;
|
|
360
|
+
const tplLength = end - start;
|
|
361
|
+
|
|
362
|
+
parts.push(source.slice(cursor, start));
|
|
363
|
+
|
|
364
|
+
// Blank out backticks and dollar signs instead of backslash-escaping
|
|
365
|
+
// them: escaping grows the content, and once the growth exceeds the
|
|
366
|
+
// padding slack the placeholder no longer lines up with the original
|
|
367
|
+
// region — matchPlaceholder then rejects it and the raw placeholder
|
|
368
|
+
// leaks into the AST (ember-tooling/ember-eslint-parser#230). The
|
|
369
|
+
// content is discarded when the Glimmer AST is spliced in, so only
|
|
370
|
+
// its length and line structure matter.
|
|
371
|
+
const content = source
|
|
372
|
+
.slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint)
|
|
373
|
+
.replace(/[`$]/g, " ");
|
|
374
|
+
|
|
375
|
+
if (pr.type === "class-member") {
|
|
376
|
+
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
|
|
377
|
+
parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
|
|
378
|
+
} else {
|
|
379
|
+
const spaces = tplLength - content.length - 2; // "`" + "`" = 2
|
|
380
|
+
parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
cursor = end;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
parts.push(source.slice(cursor));
|
|
387
|
+
return parts.join("");
|
|
388
|
+
}
|