ember-estree 0.3.0 → 0.4.1
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/package.json +6 -2
- package/src/index.d.ts +38 -78
- package/src/index.js +1 -2
- package/src/parse.js +298 -134
- package/src/transforms.js +242 -118
- package/src/utils.js +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-estree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "ESTree generator for gjs and gts file used by ember",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"AST",
|
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@glimmer/env": "^0.1.7",
|
|
33
34
|
"@glimmer/syntax": "^0.95.0",
|
|
34
35
|
"content-tag": "^4.1.0",
|
|
35
|
-
"ember-template-recast": "^6.1.5",
|
|
36
36
|
"oxc-parser": "^0.119.0",
|
|
37
37
|
"zimmerframe": "^1.1.4"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@tsconfig/node-lts": "^22.0.2",
|
|
41
|
+
"mitata": "^1.0.34",
|
|
41
42
|
"oxfmt": "^0.40.0",
|
|
42
43
|
"oxlint": "^1.55.0",
|
|
43
44
|
"publint": "^0.3.18",
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"scripts": {
|
|
49
50
|
"format": "oxfmt",
|
|
50
51
|
"format:check": "oxfmt --check",
|
|
52
|
+
"bench": "node --expose-gc tests/parser.bench.mjs",
|
|
53
|
+
"bench:compare": "node scripts/bench-compare.mjs",
|
|
54
|
+
"bench:summary": "./scripts/local-bench-summary.sh",
|
|
51
55
|
"lint": "oxlint && pnpm format:check && publint",
|
|
52
56
|
"lint:fix": "oxlint --fix && oxfmt",
|
|
53
57
|
"test": "vitest run"
|
package/src/index.d.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Options accepted by `parse` and `toTree`.
|
|
3
|
-
*/
|
|
4
|
-
export interface ParseOptions {
|
|
5
|
-
/** Path to the file being parsed, used to determine the language (js/ts). */
|
|
6
|
-
filePath?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A 1-based line / 0-based column position, matching ESTree and Glimmer
|
|
11
|
-
* conventions.
|
|
12
|
-
*/
|
|
13
1
|
export interface Position {
|
|
14
2
|
line: number;
|
|
15
3
|
column: number;
|
|
16
4
|
}
|
|
17
5
|
|
|
18
|
-
/**
|
|
19
|
-
* Minimal shape shared by every AST node (ESTree, TypeScript, and Glimmer).
|
|
20
|
-
*/
|
|
21
6
|
export interface ASTNode {
|
|
22
7
|
type: string;
|
|
23
8
|
start?: number;
|
|
@@ -25,80 +10,55 @@ export interface ASTNode {
|
|
|
25
10
|
[key: string]: unknown;
|
|
26
11
|
}
|
|
27
12
|
|
|
28
|
-
/**
|
|
29
|
-
* The `File`-like wrapper returned by `toTree` and `parse`.
|
|
30
|
-
*
|
|
31
|
-
* Mirrors the shape produced internally:
|
|
32
|
-
* ```
|
|
33
|
-
* { type: "File", program: Program, comments: Comment[], start, end }
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
13
|
export interface FileNode extends ASTNode {
|
|
37
14
|
type: "File";
|
|
38
15
|
program: ASTNode;
|
|
39
16
|
comments: ASTNode[];
|
|
40
17
|
}
|
|
41
18
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
export interface TemplateResult {
|
|
20
|
+
ast: ASTNode;
|
|
21
|
+
comments: ASTNode[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VisitorPath {
|
|
25
|
+
node: ASTNode;
|
|
26
|
+
parent: ASTNode | null;
|
|
27
|
+
parentPath: VisitorPath | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParseOptions {
|
|
31
|
+
filePath?: string;
|
|
32
|
+
templateOnly?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Include `parent` references on Glimmer AST nodes.
|
|
35
|
+
* Defaults to `true`. Set to `false` for JSON-serializable output.
|
|
36
|
+
*/
|
|
37
|
+
includeParentLinks?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Custom JS/TS parser. Called with the placeholder JS string
|
|
40
|
+
* (templates replaced with backtick expressions of equal length).
|
|
41
|
+
* Must return at least `{ ast }`.
|
|
42
|
+
*/
|
|
43
|
+
parser?: (placeholderJS: string) => { ast: ASTNode; [key: string]: unknown };
|
|
44
|
+
/**
|
|
45
|
+
* Callbacks invoked for Glimmer nodes during the AST splice traversal.
|
|
46
|
+
* Runs in DFS order, so parent nodes are visited before children.
|
|
47
|
+
*/
|
|
48
|
+
visitors?: {
|
|
49
|
+
[glimmerNodeType: string]: (node: ASTNode, path: VisitorPath) => void;
|
|
50
|
+
GlimmerBlockParams?: (node: ASTNode, path: VisitorPath) => void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export class DocumentLines {
|
|
47
55
|
constructor(source: string);
|
|
48
|
-
|
|
49
|
-
/** Converts a `{ line, column }` position to a character offset. */
|
|
50
56
|
positionToOffset(pos: Position): number;
|
|
51
|
-
|
|
52
|
-
/** Converts a character offset to a `{ line, column }` position. */
|
|
53
57
|
offsetToPosition(offset: number): Position;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* AST with embedded Glimmer template nodes.
|
|
59
|
-
*
|
|
60
|
-
* @param source The raw source code of the file.
|
|
61
|
-
* @param options Optional parse options.
|
|
62
|
-
* @returns A `File`-shaped object with a `.program` property.
|
|
63
|
-
*/
|
|
64
|
-
export function toTree(source: string, options?: ParseOptions): FileNode;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse Ember .gjs/.gts source code into an ESTree-compatible AST with
|
|
68
|
-
* embedded Glimmer template nodes.
|
|
69
|
-
*
|
|
70
|
-
* @param source The source code to parse.
|
|
71
|
-
* @param options Optional parse options.
|
|
72
|
-
* @returns The ESTree-compatible AST.
|
|
73
|
-
*/
|
|
74
|
-
export function parse(source: string, options?: ParseOptions): FileNode;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Recursively print an AST node back to source code.
|
|
78
|
-
*
|
|
79
|
-
* Handles ESTree, TypeScript, and Glimmer template node types.
|
|
80
|
-
* JSX nodes are not supported — Ember uses Glimmer templates instead.
|
|
81
|
-
*
|
|
82
|
-
* @param node The AST node to print.
|
|
83
|
-
* @returns The printed source string.
|
|
84
|
-
*/
|
|
60
|
+
export function toTree(source: string, options?: ParseOptions): FileNode | TemplateResult;
|
|
61
|
+
export function parse(source: string, options?: ParseOptions): FileNode | TemplateResult;
|
|
85
62
|
export function print(node: ASTNode): string;
|
|
86
63
|
|
|
87
|
-
|
|
88
|
-
* Build and return the Glimmer visitor keys map with a `"Glimmer"` prefix on
|
|
89
|
-
* every key (e.g. `"GlimmerElementNode"`).
|
|
90
|
-
*
|
|
91
|
-
* The result is cached after the first call.
|
|
92
|
-
*
|
|
93
|
-
* @returns A map of Glimmer node type names to arrays of child-property names.
|
|
94
|
-
*/
|
|
95
|
-
export function buildGlimmerVisitorKeys(): Record<string, string[]>;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Recursively remove all `parent` references from an AST.
|
|
99
|
-
* Useful when you need to serialize the tree to JSON,
|
|
100
|
-
* since parent back-references create circular structures.
|
|
101
|
-
*
|
|
102
|
-
* Mutates the tree in place and returns it.
|
|
103
|
-
*/
|
|
104
|
-
export function removeParentReferences(ast: ASTNode): ASTNode;
|
|
64
|
+
export const glimmerVisitorKeys: Record<string, string[]>;
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { toTree, parse } from "./parse.js";
|
|
2
2
|
export { print } from "./print.js";
|
|
3
|
-
export {
|
|
4
|
-
export { removeParentReferences } from "./utils.js";
|
|
3
|
+
export { glimmerVisitorKeys, DocumentLines } from "./transforms.js";
|
package/src/parse.js
CHANGED
|
@@ -1,176 +1,340 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Strategy:
|
|
3
3
|
*
|
|
4
|
-
* 1. parse out the <template>...</template> regions
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 3. parse the string/contents as js/ts to generate an ESTree
|
|
12
|
-
*
|
|
13
|
-
* 4. parse each template region to generate an AST from that
|
|
14
|
-
*
|
|
15
|
-
* 5. convert the AST from `@glimmer/syntax` to ESTree
|
|
16
|
-
* - NOTE: it may already be ESTree
|
|
17
|
-
*
|
|
18
|
-
* 6. splice in the template ESTrees into the JS/TS ESTree
|
|
19
|
-
*
|
|
20
|
-
* 7. Done
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Docs for dependencies:
|
|
25
|
-
* - https://github.com/embroider-build/content-tag/
|
|
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
|
|
26
10
|
*/
|
|
27
11
|
|
|
28
12
|
import { parseSync } from "oxc-parser";
|
|
29
|
-
import templateRecast from "ember-template-recast";
|
|
30
13
|
import { Preprocessor } from "content-tag";
|
|
31
14
|
import { walk } from "zimmerframe";
|
|
32
15
|
|
|
33
|
-
import {
|
|
16
|
+
import { processTemplate, DocumentLines, glimmerVisitorKeys } from "./transforms.js";
|
|
34
17
|
|
|
35
18
|
const preprocessor = new Preprocessor();
|
|
36
19
|
|
|
20
|
+
// Node types that placeholders parse into (backtick/static-block format)
|
|
21
|
+
const PLACEHOLDER_TYPES = new Set([
|
|
22
|
+
"ExpressionStatement",
|
|
23
|
+
"StaticBlock",
|
|
24
|
+
"TemplateLiteral",
|
|
25
|
+
"ExportDefaultDeclaration",
|
|
26
|
+
]);
|
|
27
|
+
|
|
37
28
|
/**
|
|
29
|
+
* Parse Ember source and return an ESTree-compatible AST.
|
|
30
|
+
*
|
|
38
31
|
* @param {string} source
|
|
39
|
-
* @param {object} options
|
|
40
|
-
* @
|
|
32
|
+
* @param {object} [options]
|
|
33
|
+
* @param {string} [options.filePath] - File path for language detection
|
|
34
|
+
* @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
|
|
35
|
+
* @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
|
|
36
|
+
* @param {object} [options.visitors] - Callbacks invoked for Glimmer nodes during traversal
|
|
37
|
+
* @return {object}
|
|
41
38
|
*/
|
|
42
39
|
export function toTree(source, options = {}) {
|
|
40
|
+
const templateOpts = options.includeParentLinks === false ? { includeParentLinks: false } : {};
|
|
41
|
+
|
|
42
|
+
if (options.templateOnly) {
|
|
43
|
+
return processTemplate(source, new DocumentLines(source), [0, source.length], templateOpts);
|
|
44
|
+
}
|
|
45
|
+
|
|
43
46
|
let parseResults = preprocessor.parse(source);
|
|
44
47
|
let js = toPlaceholderJS(source, parseResults);
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
49
|
+
const useCustomParser = !!options.parser;
|
|
50
|
+
const visitors = options.visitors || null;
|
|
51
|
+
|
|
52
|
+
// Parse the placeholder JS — use custom parser or default oxc
|
|
53
|
+
let result;
|
|
54
|
+
if (useCustomParser) {
|
|
55
|
+
result = options.parser(js);
|
|
56
|
+
if (!result.ast) {
|
|
57
|
+
result = { ast: result };
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
let filename = options.filePath || "input.ts";
|
|
61
|
+
let oxcResult = parseSync(filename, js);
|
|
62
|
+
result = {
|
|
63
|
+
ast: {
|
|
64
|
+
type: "File",
|
|
65
|
+
program: oxcResult.program,
|
|
66
|
+
comments: oxcResult.comments || [],
|
|
67
|
+
start: oxcResult.program.start,
|
|
68
|
+
end: oxcResult.program.end,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no templates, return early
|
|
74
|
+
if (!parseResults.length) {
|
|
75
|
+
if (useCustomParser) {
|
|
76
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
80
|
+
return result.ast;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const codeLines = new DocumentLines(source);
|
|
84
|
+
const allComments = [];
|
|
85
|
+
const templateInfos = [];
|
|
86
|
+
|
|
87
|
+
// Build a map of template ranges for lookup
|
|
88
|
+
const templateRangeByStart = new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]));
|
|
89
|
+
|
|
90
|
+
// Process a matched placeholder node: create Glimmer AST and tokens
|
|
91
|
+
function processPlaceholder(parseResult) {
|
|
92
|
+
let templateContent = parseResult.contents;
|
|
93
|
+
let contentRange = [
|
|
94
|
+
parseResult.contentRange.startUtf16Codepoint,
|
|
95
|
+
parseResult.contentRange.endUtf16Codepoint,
|
|
96
|
+
];
|
|
97
|
+
let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
|
|
98
|
+
|
|
99
|
+
const { ast, comments } = processTemplate(
|
|
100
|
+
templateContent,
|
|
101
|
+
codeLines,
|
|
102
|
+
contentRange,
|
|
103
|
+
templateOpts,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Fix the Template root to cover the full <template>...</template> range
|
|
107
|
+
ast.range = fullRange;
|
|
108
|
+
ast.start = fullRange[0];
|
|
109
|
+
ast.end = fullRange[1];
|
|
110
|
+
ast.loc = {
|
|
111
|
+
start: codeLines.offsetToPosition(fullRange[0]),
|
|
112
|
+
end: codeLines.offsetToPosition(fullRange[1]),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add tokens for the <template> and </template> tags
|
|
116
|
+
const openEnd = contentRange[0];
|
|
117
|
+
const closeStart = contentRange[1];
|
|
118
|
+
const openTag = source.slice(fullRange[0], openEnd);
|
|
119
|
+
const closeTag = source.slice(closeStart, fullRange[1]);
|
|
120
|
+
const makeToken = (value, range) => ({
|
|
121
|
+
type: "Punctuator",
|
|
122
|
+
value,
|
|
123
|
+
range,
|
|
124
|
+
start: range[0],
|
|
125
|
+
end: range[1],
|
|
126
|
+
loc: {
|
|
127
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
128
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
ast.tokens = [
|
|
132
|
+
makeToken(openTag, [fullRange[0], openEnd]),
|
|
133
|
+
...(ast.tokens || []),
|
|
134
|
+
makeToken(closeTag, [closeStart, fullRange[1]]),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
allComments.push(...comments);
|
|
138
|
+
templateInfos.push({ utf16Range: fullRange, ast });
|
|
139
|
+
return ast;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if a node matches a template range
|
|
143
|
+
function matchPlaceholder(node) {
|
|
144
|
+
let range = node.range || [node.start, node.end];
|
|
145
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration) {
|
|
146
|
+
const decl = node.declaration;
|
|
147
|
+
range = decl.range || [decl.start, decl.end];
|
|
148
|
+
}
|
|
149
|
+
const parseResult = templateRangeByStart.get(range[0]);
|
|
150
|
+
if (
|
|
151
|
+
!parseResult ||
|
|
152
|
+
(parseResult.range.endUtf16Codepoint !== range[1] &&
|
|
153
|
+
parseResult.range.endUtf16Codepoint !== range[1] + 1)
|
|
154
|
+
) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return parseResult;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Walk Glimmer subtree, invoking visitors with full path context
|
|
161
|
+
function walkGlimmerTree(node, parentPath) {
|
|
162
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
163
|
+
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
164
|
+
|
|
165
|
+
if (visitors && node.type.startsWith("Glimmer")) {
|
|
166
|
+
const handler = visitors[node.type];
|
|
167
|
+
if (handler) handler(node, path);
|
|
168
|
+
if ("blockParams" in node && visitors.GlimmerBlockParams) {
|
|
169
|
+
visitors.GlimmerBlockParams(node, path);
|
|
84
170
|
}
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
});
|
|
171
|
+
}
|
|
88
172
|
|
|
89
|
-
|
|
173
|
+
const keys = glimmerVisitorKeys[node.type];
|
|
174
|
+
if (!keys) return;
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
const child = node[key];
|
|
177
|
+
if (!child) continue;
|
|
178
|
+
if (Array.isArray(child)) {
|
|
179
|
+
for (const item of child) {
|
|
180
|
+
walkGlimmerTree(item, path);
|
|
181
|
+
}
|
|
182
|
+
} else if (typeof child === "object" && child.type) {
|
|
183
|
+
walkGlimmerTree(child, path);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
90
187
|
|
|
91
|
-
|
|
92
|
-
|
|
188
|
+
if (useCustomParser) {
|
|
189
|
+
// Custom parser path: mutate the parser's AST in-place, invoke visitors.
|
|
190
|
+
// Use the parser's visitorKeys to traverse efficiently (avoids Object.keys).
|
|
191
|
+
const parserVisitorKeys = result.visitorKeys || {};
|
|
93
192
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* with embedded Glimmer template nodes.
|
|
97
|
-
*
|
|
98
|
-
* @param {string} source - The source code to parse
|
|
99
|
-
* @param {object} [options] - Parse options
|
|
100
|
-
* @return {object} The ESTree-compatible AST
|
|
101
|
-
*/
|
|
102
|
-
export function parse(source, options = {}) {
|
|
103
|
-
let ast = toTree(source, options);
|
|
193
|
+
function visitNode(node, parentPath) {
|
|
194
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
104
195
|
|
|
105
|
-
|
|
106
|
-
}
|
|
196
|
+
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
107
197
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
198
|
+
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
199
|
+
const parseResult = matchPlaceholder(node);
|
|
200
|
+
if (parseResult) {
|
|
201
|
+
const ast = processPlaceholder(parseResult);
|
|
202
|
+
for (const key of Object.keys(node)) {
|
|
203
|
+
if (!(key in ast) && key !== "parent") {
|
|
204
|
+
delete node[key];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Object.assign(node, ast);
|
|
208
|
+
if (visitors) walkGlimmerTree(node, parentPath);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
113
212
|
|
|
114
|
-
|
|
115
|
-
|
|
213
|
+
// Use visitorKeys for efficient child traversal
|
|
214
|
+
const keys = parserVisitorKeys[node.type];
|
|
215
|
+
if (!keys) return;
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
const child = node[key];
|
|
218
|
+
if (!child) continue;
|
|
219
|
+
if (Array.isArray(child)) {
|
|
220
|
+
for (const item of child) {
|
|
221
|
+
if (item && typeof item === "object" && item.type) {
|
|
222
|
+
visitNode(item, path);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (typeof child === "object" && child.type) {
|
|
226
|
+
visitNode(child, path);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
116
230
|
|
|
117
|
-
|
|
118
|
-
}
|
|
231
|
+
visitNode(result.ast, null);
|
|
232
|
+
} else {
|
|
233
|
+
// Default oxc path: use zimmerframe walk (returns new tree)
|
|
234
|
+
result.ast = walk(result.ast, null, {
|
|
235
|
+
_(node, { next }) {
|
|
236
|
+
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
237
|
+
const parseResult = matchPlaceholder(node);
|
|
238
|
+
if (parseResult) {
|
|
239
|
+
return processPlaceholder(parseResult);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
next();
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Walk Glimmer subtrees for visitors (after zimmerframe splicing)
|
|
247
|
+
if (visitors) {
|
|
248
|
+
for (const ti of templateInfos) {
|
|
249
|
+
walkGlimmerTree(ti.ast, null);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
119
253
|
|
|
120
|
-
|
|
121
|
-
|
|
254
|
+
// Splice template tokens into the AST token stream.
|
|
255
|
+
// Tokens are sorted by range, so use binary search for O(log n) lookup.
|
|
256
|
+
const astRoot = result.ast.program || result.ast;
|
|
257
|
+
if (astRoot.tokens) {
|
|
258
|
+
for (const ti of templateInfos) {
|
|
259
|
+
const [tStart, tEnd] = ti.utf16Range;
|
|
260
|
+
const tokens = astRoot.tokens;
|
|
261
|
+
// Binary search for first token with range[0] >= tStart
|
|
262
|
+
let lo = 0;
|
|
263
|
+
let hi = tokens.length;
|
|
264
|
+
while (lo < hi) {
|
|
265
|
+
const mid = (lo + hi) >>> 1;
|
|
266
|
+
if (tokens[mid].range[0] < tStart) lo = mid + 1;
|
|
267
|
+
else hi = mid;
|
|
268
|
+
}
|
|
269
|
+
const firstIdx = lo;
|
|
270
|
+
if (firstIdx >= tokens.length || tokens[firstIdx].range[0] >= tEnd) continue;
|
|
271
|
+
let lastIdx = firstIdx;
|
|
272
|
+
while (lastIdx < tokens.length && tokens[lastIdx].range[1] <= tEnd) {
|
|
273
|
+
lastIdx++;
|
|
274
|
+
}
|
|
275
|
+
tokens.splice(firstIdx, lastIdx - firstIdx, ...ti.ast.tokens);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
122
278
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
279
|
+
// Merge comments
|
|
280
|
+
if (allComments.length) {
|
|
281
|
+
if (!astRoot.comments) astRoot.comments = [];
|
|
282
|
+
astRoot.comments.push(...allComments);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (useCustomParser) {
|
|
286
|
+
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
287
|
+
result.templateInfos = templateInfos;
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Default path: return bare AST with visitorKeys attached
|
|
292
|
+
result.ast.visitorKeys = glimmerVisitorKeys;
|
|
293
|
+
return result.ast;
|
|
126
294
|
}
|
|
127
295
|
|
|
296
|
+
export const parse = toTree;
|
|
297
|
+
|
|
298
|
+
// ── Placeholder JS ────────────────────────────────────────────────────
|
|
299
|
+
|
|
128
300
|
/**
|
|
129
|
-
* Replaces <template>...</template> regions
|
|
130
|
-
*
|
|
131
|
-
* are valid JavaScript, so oxc-parser can parse them.
|
|
132
|
-
*
|
|
133
|
-
* Expression templates become: TEMPLATE_TEMPLATE(`...`)
|
|
134
|
-
* Class member templates become: [_TEMPLATE_(`...`)] = 0;
|
|
301
|
+
* Replaces <template>...</template> regions with placeholder expressions
|
|
302
|
+
* of the same character length that are valid JS/TS.
|
|
135
303
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* <template> (10) + </template> (11) = 21 character overhead.
|
|
304
|
+
* Expression templates become: `content ` (backtick, space-padded)
|
|
305
|
+
* Class member templates become: static{`content `} (static block, space-padded)
|
|
139
306
|
*
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
142
|
-
* @returns {string}
|
|
307
|
+
* This format is compatible with all JS/TS parsers including
|
|
308
|
+
* oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
|
|
143
309
|
*/
|
|
144
310
|
function toPlaceholderJS(source, parseResults) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
for (let pr of parseResults) {
|
|
149
|
-
let start = pr.range.startUtf16Codepoint;
|
|
150
|
-
let end = pr.range.endUtf16Codepoint;
|
|
151
|
-
|
|
152
|
-
let openingTag, closingTag;
|
|
153
|
-
switch (pr.type) {
|
|
154
|
-
case "expression":
|
|
155
|
-
openingTag = "TEMPLATE_TEMPLATE(`";
|
|
156
|
-
closingTag = "`)";
|
|
157
|
-
break;
|
|
158
|
-
case "class-member":
|
|
159
|
-
openingTag = "[_TEMPLATE_(`";
|
|
160
|
-
closingTag = "`)] = 0;";
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
311
|
+
// Build result in forward order using parts array (avoids intermediate string allocations)
|
|
312
|
+
const parts = [];
|
|
313
|
+
let cursor = 0;
|
|
163
314
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
315
|
+
for (const pr of parseResults) {
|
|
316
|
+
const start = pr.range.startUtf16Codepoint;
|
|
317
|
+
const end = pr.range.endUtf16Codepoint;
|
|
318
|
+
const tplLength = end - start;
|
|
319
|
+
|
|
320
|
+
parts.push(source.slice(cursor, start));
|
|
168
321
|
|
|
169
|
-
|
|
322
|
+
const content = source
|
|
323
|
+
.slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint)
|
|
324
|
+
.replace(/`/g, "\\`")
|
|
325
|
+
.replace(/\$/g, "\\$");
|
|
326
|
+
|
|
327
|
+
if (pr.type === "class-member") {
|
|
328
|
+
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
|
|
329
|
+
parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
|
|
330
|
+
} else {
|
|
331
|
+
const spaces = tplLength - content.length - 2; // "`" + "`" = 2
|
|
332
|
+
parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
|
|
333
|
+
}
|
|
170
334
|
|
|
171
|
-
|
|
172
|
-
offset += replacement.length - (end - start);
|
|
335
|
+
cursor = end;
|
|
173
336
|
}
|
|
174
337
|
|
|
175
|
-
|
|
338
|
+
parts.push(source.slice(cursor));
|
|
339
|
+
return parts.join("");
|
|
176
340
|
}
|
package/src/transforms.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Glimmer AST → ESTree transform utilities.
|
|
3
|
-
*
|
|
4
|
-
* Ported from ember-eslint-parser's transforms.js, adapted for
|
|
5
|
-
* ember-estree's ESM architecture. Handles:
|
|
6
|
-
*
|
|
7
|
-
* - Type prefixing (all Glimmer types get a "Glimmer" prefix)
|
|
8
|
-
* - Range / loc fixing (converts template-local positions to file-level)
|
|
9
|
-
* - ElementNode `parts` and `name` fields
|
|
10
|
-
* - blockParams → virtual node creation
|
|
11
|
-
* - Empty hash nullification
|
|
12
|
-
* - Empty text node removal
|
|
13
3
|
*/
|
|
14
4
|
|
|
15
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
visitorKeys as rawGlimmerVisitorKeys,
|
|
7
|
+
preprocess as glimmerPreprocess,
|
|
8
|
+
} from "@glimmer/syntax";
|
|
16
9
|
|
|
17
10
|
/**
|
|
18
11
|
* Converts between character offsets and line/column positions.
|
|
@@ -45,38 +38,34 @@ export class DocumentLines {
|
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
|
-
*
|
|
41
|
+
* Glimmer visitor keys map with "Glimmer" prefix.
|
|
42
|
+
* Computed once at module load.
|
|
49
43
|
*/
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
export const glimmerVisitorKeys = (() => {
|
|
45
|
+
const keys = {};
|
|
46
|
+
for (const [k, v] of Object.entries(rawGlimmerVisitorKeys)) {
|
|
47
|
+
keys[`Glimmer${k}`] = v;
|
|
48
|
+
}
|
|
49
|
+
keys.GlimmerElementNode = [...keys.GlimmerElementNode, "blockParamNodes", "parts"];
|
|
50
|
+
keys.GlimmerProgram = ["body", "blockParamNodes"];
|
|
51
|
+
keys.GlimmerTemplate = ["body"];
|
|
52
|
+
return keys;
|
|
53
|
+
})();
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
All(node, path) {
|
|
58
|
-
node.parent = path.parentNode;
|
|
59
|
-
allNodes.push(node);
|
|
60
|
-
if (node.type === "CommentStatement" || node.type === "MustacheCommentStatement") {
|
|
61
|
-
comments.push(node);
|
|
62
|
-
}
|
|
63
|
-
if (node.type === "TextNode") {
|
|
64
|
-
node.value = node.chars;
|
|
65
|
-
if (node.value.trim().length !== 0 || (node.parent && node.parent.type === "AttrNode")) {
|
|
66
|
-
textNodes.push(node);
|
|
67
|
-
} else {
|
|
68
|
-
emptyTextNodes.push(node);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
});
|
|
55
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
73
56
|
|
|
74
|
-
|
|
57
|
+
// @glimmer/syntax nodes use prototype getters that form circular chains,
|
|
58
|
+
// crashing traversers like esrecurse. We snapshot configurable getters:
|
|
59
|
+
// ElementNode: tag, blockParams, selfClosing
|
|
60
|
+
// PathExpression: original
|
|
61
|
+
// VarHead: name, original
|
|
62
|
+
// Block: blockParams
|
|
63
|
+
const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
|
|
64
|
+
function defOwn(obj, key) {
|
|
65
|
+
_desc.value = obj[key];
|
|
66
|
+
Object.defineProperty(obj, key, _desc);
|
|
75
67
|
}
|
|
76
68
|
|
|
77
|
-
/**
|
|
78
|
-
* Remove nodes from their parent's children/body/parts arrays.
|
|
79
|
-
*/
|
|
80
69
|
function removeFromParent(nodes) {
|
|
81
70
|
for (const node of nodes) {
|
|
82
71
|
const children =
|
|
@@ -88,114 +77,223 @@ function removeFromParent(nodes) {
|
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
79
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
80
|
+
function isAlphaNumeric(code) {
|
|
81
|
+
return !(!(code > 47 && code < 58) && !(code > 64 && code < 91) && !(code > 96 && code < 123));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isWhiteSpaceCode(code) {
|
|
85
|
+
return code === 32 || code === 9 || code === 13 || code === 10 || code === 11;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tokenize(template, doc, startOffset) {
|
|
89
|
+
const tokens = [];
|
|
90
|
+
let wordStart = -1;
|
|
91
|
+
function pushToken(value, type, range) {
|
|
92
|
+
tokens.push({
|
|
93
|
+
type,
|
|
94
|
+
value,
|
|
95
|
+
range,
|
|
96
|
+
start: range[0],
|
|
97
|
+
end: range[1],
|
|
98
|
+
loc: {
|
|
99
|
+
start: { ...doc.offsetToPosition(range[0]), index: range[0] },
|
|
100
|
+
end: { ...doc.offsetToPosition(range[1]), index: range[1] },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < template.length; i++) {
|
|
105
|
+
const code = template.charCodeAt(i);
|
|
106
|
+
if (isAlphaNumeric(code)) {
|
|
107
|
+
if (wordStart < 0) wordStart = i;
|
|
108
|
+
} else {
|
|
109
|
+
if (wordStart >= 0) {
|
|
110
|
+
pushToken(template.slice(wordStart, i), "word", [startOffset + wordStart, startOffset + i]);
|
|
111
|
+
wordStart = -1;
|
|
112
|
+
}
|
|
113
|
+
if (!isWhiteSpaceCode(code)) {
|
|
114
|
+
pushToken(template[i], "Punctuator", [startOffset + i, startOffset + i + 1]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
101
117
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
118
|
+
if (wordStart >= 0) {
|
|
119
|
+
pushToken(template.slice(wordStart), "word", [
|
|
120
|
+
startOffset + wordStart,
|
|
121
|
+
startOffset + template.length,
|
|
122
|
+
]);
|
|
104
123
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
return tokens;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildTokenStream(rawTokens, comments, textNodes) {
|
|
128
|
+
const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]);
|
|
129
|
+
const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]);
|
|
130
|
+
|
|
131
|
+
function isCovered(tokenRange, intervals) {
|
|
132
|
+
let lo = 0;
|
|
133
|
+
let hi = intervals.length - 1;
|
|
134
|
+
while (lo <= hi) {
|
|
135
|
+
const mid = (lo + hi) >> 1;
|
|
136
|
+
const iv = intervals[mid];
|
|
137
|
+
if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) return true;
|
|
138
|
+
if (iv[0] > tokenRange[0]) hi = mid - 1;
|
|
139
|
+
else lo = mid + 1;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const filteredTokens = rawTokens.filter(
|
|
145
|
+
(t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]);
|
|
149
|
+
const result = [];
|
|
150
|
+
let ti = 0;
|
|
151
|
+
for (const token of filteredTokens) {
|
|
152
|
+
while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) {
|
|
153
|
+
result.push(sortedTextNodes[ti++]);
|
|
154
|
+
}
|
|
155
|
+
result.push(token);
|
|
156
|
+
}
|
|
157
|
+
while (ti < sortedTextNodes.length) {
|
|
158
|
+
result.push(sortedTextNodes[ti++]);
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
109
161
|
}
|
|
110
162
|
|
|
111
163
|
/**
|
|
112
|
-
*
|
|
164
|
+
* Parse and transform a Glimmer template into an ESTree-compatible AST.
|
|
165
|
+
* Internal — consumed by toTree.
|
|
113
166
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* @param {[number, number]} opts.templateRange - [start, end] byte range of the full <template>...</template> block
|
|
118
|
-
* @param {string} opts.source - The full source code
|
|
119
|
-
* @returns {object} The transformed AST
|
|
167
|
+
* Single recursive pass: collect, categorize, snapshot getters, fix
|
|
168
|
+
* positions, create parts/blockParamNodes, nullify empty hashes, and
|
|
169
|
+
* prefix types. No separate collect-then-transform loop.
|
|
120
170
|
*/
|
|
121
|
-
export function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const toFileRange = (loc) => {
|
|
130
|
-
const locObj = loc.toJSON ? loc.toJSON() : loc;
|
|
131
|
-
return [
|
|
132
|
-
contentOffset + contentDoc.positionToOffset(locObj.start),
|
|
133
|
-
contentOffset + contentDoc.positionToOffset(locObj.end),
|
|
134
|
-
];
|
|
135
|
-
};
|
|
171
|
+
export function processTemplate(
|
|
172
|
+
templateContent,
|
|
173
|
+
codeLines,
|
|
174
|
+
templateRange,
|
|
175
|
+
{ includeParentLinks = true } = {},
|
|
176
|
+
) {
|
|
177
|
+
const offset = templateRange[0];
|
|
178
|
+
const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
|
|
136
179
|
|
|
180
|
+
const toFileRange = (loc) => [
|
|
181
|
+
offset + docLines.positionToOffset(loc.start),
|
|
182
|
+
offset + docLines.positionToOffset(loc.end),
|
|
183
|
+
];
|
|
137
184
|
const toFileLoc = (range) => ({
|
|
138
|
-
start:
|
|
139
|
-
end:
|
|
185
|
+
start: codeLines.offsetToPosition(range[0]),
|
|
186
|
+
end: codeLines.offsetToPosition(range[1]),
|
|
140
187
|
});
|
|
141
188
|
|
|
142
|
-
const
|
|
189
|
+
const ast = glimmerPreprocess(templateContent, { mode: "codemod" });
|
|
190
|
+
const allNodes = [];
|
|
191
|
+
const comments = [];
|
|
192
|
+
const textNodes = [];
|
|
193
|
+
const emptyTextNodes = [];
|
|
143
194
|
|
|
144
|
-
|
|
145
|
-
|
|
195
|
+
// Single recursive pass over the glimmer AST. Processes each node
|
|
196
|
+
// fully (getters, positions, parts, blockParams) then recurses into
|
|
197
|
+
// children using raw visitor keys. Type prefixing happens inline
|
|
198
|
+
// AFTER recursing (so children see the original type during lookup).
|
|
199
|
+
function visit(n, parent) {
|
|
200
|
+
n.parent = parent;
|
|
201
|
+
allNodes.push(n);
|
|
146
202
|
|
|
147
|
-
//
|
|
148
|
-
if (n.type === "
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
203
|
+
// Categorize
|
|
204
|
+
if (n.type === "CommentStatement" || n.type === "MustacheCommentStatement") {
|
|
205
|
+
comments.push(n);
|
|
206
|
+
}
|
|
207
|
+
if (n.type === "TextNode") {
|
|
208
|
+
n.value = n.chars;
|
|
209
|
+
if (n.value.trim().length !== 0 || (parent && parent.type === "AttrNode")) {
|
|
210
|
+
textNodes.push(n);
|
|
211
|
+
} else {
|
|
212
|
+
emptyTextNodes.push(n);
|
|
158
213
|
}
|
|
159
214
|
}
|
|
160
215
|
|
|
161
|
-
//
|
|
162
|
-
|
|
216
|
+
// Snapshot configurable prototype getters
|
|
217
|
+
switch (n.type) {
|
|
218
|
+
case "ElementNode":
|
|
219
|
+
defOwn(n, "tag");
|
|
220
|
+
defOwn(n, "blockParams");
|
|
221
|
+
defOwn(n, "selfClosing");
|
|
222
|
+
if (n.path?.head) {
|
|
223
|
+
defOwn(n.path.head, "name");
|
|
224
|
+
defOwn(n.path.head, "original");
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case "PathExpression":
|
|
228
|
+
defOwn(n, "original");
|
|
229
|
+
if (n.head) {
|
|
230
|
+
defOwn(n.head, "name");
|
|
231
|
+
defOwn(n.head, "original");
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
case "Block":
|
|
235
|
+
defOwn(n, "blockParams");
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fix positions
|
|
240
|
+
if (n.type === "PathExpression") {
|
|
241
|
+
n.head.range = toFileRange(n.head.loc);
|
|
242
|
+
n.head.start = n.head.range[0];
|
|
243
|
+
n.head.end = n.head.range[1];
|
|
244
|
+
n.head.loc = toFileLoc(n.head.range);
|
|
245
|
+
}
|
|
246
|
+
n.range = n.type === "Template" ? [...templateRange] : toFileRange(n.loc);
|
|
163
247
|
n.start = n.range[0];
|
|
164
248
|
n.end = n.range[1];
|
|
165
249
|
n.loc = toFileLoc(n.range);
|
|
166
250
|
|
|
167
|
-
//
|
|
251
|
+
// Create parts for ElementNode
|
|
168
252
|
if (n.type === "ElementNode") {
|
|
169
253
|
n.name = n.tag;
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const tagEnd = tagStart + n.tag.length;
|
|
173
|
-
const tagRange = [tagStart, tagEnd];
|
|
254
|
+
const p = n.path.head;
|
|
255
|
+
const partRange = toFileRange(p.loc);
|
|
174
256
|
n.parts = [
|
|
175
257
|
{
|
|
176
|
-
original: n.tag,
|
|
177
|
-
name: n.tag,
|
|
178
258
|
type: "GlimmerElementNodePart",
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
259
|
+
original: p.original,
|
|
260
|
+
name: p.original,
|
|
261
|
+
parent: n,
|
|
262
|
+
range: partRange,
|
|
263
|
+
start: partRange[0],
|
|
264
|
+
end: partRange[1],
|
|
265
|
+
loc: toFileLoc(partRange),
|
|
183
266
|
},
|
|
184
267
|
];
|
|
185
268
|
}
|
|
186
269
|
|
|
187
|
-
//
|
|
270
|
+
// Create blockParamNodes
|
|
188
271
|
if ("blockParams" in n && Array.isArray(n.blockParams)) {
|
|
189
|
-
n.
|
|
190
|
-
|
|
272
|
+
if (n.params && n.params.length === n.blockParams.length) {
|
|
273
|
+
n.blockParamNodes = n.params.map((p) => {
|
|
274
|
+
const range = toFileRange(p.loc);
|
|
275
|
+
return {
|
|
276
|
+
type: "GlimmerBlockParam",
|
|
277
|
+
name: p.original || p.name,
|
|
278
|
+
original: p.original,
|
|
279
|
+
parent: n,
|
|
280
|
+
range,
|
|
281
|
+
start: range[0],
|
|
282
|
+
end: range[1],
|
|
283
|
+
loc: toFileLoc(range),
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
n.blockParamNodes = n.blockParams.map((bpName) => ({
|
|
191
288
|
type: "GlimmerBlockParam",
|
|
192
|
-
name,
|
|
193
|
-
|
|
289
|
+
name: bpName,
|
|
290
|
+
parent: n,
|
|
291
|
+
range: [n.range[0], n.range[1]],
|
|
194
292
|
start: n.range[0],
|
|
195
293
|
end: n.range[1],
|
|
196
294
|
loc: toFileLoc(n.range),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
199
297
|
}
|
|
200
298
|
|
|
201
299
|
// Nullify empty hashes
|
|
@@ -203,23 +301,49 @@ export function processGlimmerTemplate(templateAST, { contentOffset, templateRan
|
|
|
203
301
|
(n.type === "MustacheStatement" ||
|
|
204
302
|
n.type === "BlockStatement" ||
|
|
205
303
|
n.type === "SubExpression") &&
|
|
206
|
-
n.hash
|
|
207
|
-
n.hash.pairs &&
|
|
208
|
-
n.hash.pairs.length === 0
|
|
304
|
+
n.hash?.pairs?.length === 0
|
|
209
305
|
) {
|
|
210
306
|
n.hash = null;
|
|
211
307
|
}
|
|
212
308
|
|
|
213
|
-
//
|
|
309
|
+
// Recurse into children BEFORE prefixing type (visitor keys use original type)
|
|
310
|
+
const keys = rawGlimmerVisitorKeys[n.type];
|
|
311
|
+
if (keys) {
|
|
312
|
+
for (const key of keys) {
|
|
313
|
+
const child = n[key];
|
|
314
|
+
if (!child) continue;
|
|
315
|
+
if (Array.isArray(child)) {
|
|
316
|
+
for (const item of child) {
|
|
317
|
+
if (item && typeof item === "object" && item.type) visit(item, n);
|
|
318
|
+
}
|
|
319
|
+
} else if (typeof child === "object" && child.type) {
|
|
320
|
+
visit(child, n);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Prefix type after children are visited
|
|
214
326
|
n.type = `Glimmer${n.type}`;
|
|
215
327
|
}
|
|
216
328
|
|
|
217
|
-
|
|
329
|
+
visit(ast, null);
|
|
330
|
+
|
|
218
331
|
removeFromParent(emptyTextNodes);
|
|
219
332
|
removeFromParent(comments);
|
|
220
333
|
for (const comment of comments) {
|
|
221
334
|
comment.type = "Block";
|
|
222
335
|
}
|
|
223
336
|
|
|
224
|
-
|
|
337
|
+
ast.tokens = buildTokenStream(tokenize(templateContent, codeLines, offset), comments, textNodes);
|
|
338
|
+
ast.contents = templateContent;
|
|
339
|
+
|
|
340
|
+
if (!includeParentLinks) {
|
|
341
|
+
for (const n of allNodes) {
|
|
342
|
+
delete n.parent;
|
|
343
|
+
if (n.parts) for (const p of n.parts) delete p.parent;
|
|
344
|
+
if (n.blockParamNodes) for (const p of n.blockParamNodes) delete p.parent;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { ast, comments };
|
|
225
349
|
}
|
package/src/utils.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional utility functions for working with ESTree ASTs
|
|
3
|
-
* produced by ember-estree.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { walk } from "zimmerframe";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Recursively remove all `parent` references from an AST.
|
|
10
|
-
* Useful when you need to serialize the tree to JSON (e.g. for zmod),
|
|
11
|
-
* since parent back-references create circular structures.
|
|
12
|
-
*
|
|
13
|
-
* Mutates the tree in place and returns it.
|
|
14
|
-
*/
|
|
15
|
-
export function removeParentReferences(ast) {
|
|
16
|
-
return walk(ast, null, {
|
|
17
|
-
_(node, { next }) {
|
|
18
|
-
delete node.parent;
|
|
19
|
-
next();
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
}
|