ember-estree 0.4.2 → 0.5.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 +103 -0
- package/package.json +4 -4
- package/src/index.d.ts +15 -11
- package/src/parse.js +69 -109
- package/src/print.js +1 -1
- package/src/transforms.js +37 -44
package/README.md
CHANGED
|
@@ -57,6 +57,109 @@ print({
|
|
|
57
57
|
// => "<template>Hello</template>"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
Both `toTree` and `parse` accept an options object as their second argument.
|
|
63
|
+
|
|
64
|
+
All options are optional.
|
|
65
|
+
|
|
66
|
+
| Option | Type | Description |
|
|
67
|
+
| -------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `filePath` | `string` | Used for language detection. |
|
|
69
|
+
| `templateOnly` | `boolean` | Parse the source as a raw Glimmer template. Use for `.hbs` files. |
|
|
70
|
+
| `parser` | `(placeholderJS: string) => { ast, ... }` | Use a custom JS/TS parser instead of the default oxc-parser. See [Custom parser](#custom-parser). |
|
|
71
|
+
| `visitors` | `VisitorMap` <br /> or `(outerAst) => VisitorMap` | Callbacks fired on every node during traversal — JS/TS and Glimmer — in a single pass. See [Visitors](#visitors). |
|
|
72
|
+
|
|
73
|
+
Handler signature is `(node, path) => void`, where `path = { node, parent, parentPath }` — a linked list walking back to the root.
|
|
74
|
+
|
|
75
|
+
### Custom parser
|
|
76
|
+
|
|
77
|
+
Pass any JS/TS parser that returns an ESTree-compatible AST. ember-estree handles template splicing and Glimmer traversal on top of it.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { parseSync } from "oxc-parser";
|
|
81
|
+
import { toTree } from "ember-estree";
|
|
82
|
+
|
|
83
|
+
const result = toTree(source, {
|
|
84
|
+
parser: (js) => ({
|
|
85
|
+
ast: parseSync("input.ts", js).program,
|
|
86
|
+
visitorKeys: {
|
|
87
|
+
/* ...parser's visitor keys... */
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The parser receives a placeholder-JS string (templates replaced with backtick expressions of equal length) and must return at least `{ ast }`. Additional fields like `scopeManager`, `visitorKeys`, or `services` are preserved on the returned result.
|
|
94
|
+
|
|
95
|
+
### Visitors
|
|
96
|
+
|
|
97
|
+
Pass `visitors` to observe or rewrite the tree in a single traversal. Handlers fire on both outer JS/TS nodes and spliced Glimmer subtrees, and a single node is never dispatched twice — safe to relocate nodes mid-walk.
|
|
98
|
+
|
|
99
|
+
The pseudo-type `GlimmerBlockParams` fires on any node that carries a `blockParams` array.
|
|
100
|
+
|
|
101
|
+
**Plain-object form** — use when you only need the type → handler map:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { toTree } from "ember-estree";
|
|
105
|
+
|
|
106
|
+
const identifiers = [];
|
|
107
|
+
toTree(source, {
|
|
108
|
+
visitors: {
|
|
109
|
+
Identifier: (node) => identifiers.push(node.name),
|
|
110
|
+
GlimmerPathExpression: (node) => identifiers.push(node.original),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Factory form** — use when you need the outer JS/TS AST up front (for example, to attach state to it before the walk):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { toTree, print } from "ember-estree";
|
|
119
|
+
|
|
120
|
+
const ast = toTree(`const world = "🌍"; const X = <template>{{world}}</template>;`, {
|
|
121
|
+
visitors: () => ({
|
|
122
|
+
Identifier: (node) => (node.name = node.name.toUpperCase()),
|
|
123
|
+
GlimmerPathExpression(node) {
|
|
124
|
+
node.original = node.original.toUpperCase();
|
|
125
|
+
if (node.head) node.head.name = node.original;
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
print(ast.program);
|
|
131
|
+
// => 'const WORLD = "🌍";\nconst X = <template>{{WORLD}}</template>;'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Collecting Glimmer comments into `program.comments`** — useful when adapting the AST for ESLint, which reads comments from the Program node:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const ast = toTree(source, {
|
|
138
|
+
visitors: (outerAst) => {
|
|
139
|
+
outerAst.program.comments = [...(outerAst.comments ?? [])];
|
|
140
|
+
const push = (node) => outerAst.program.comments.push(node);
|
|
141
|
+
return {
|
|
142
|
+
GlimmerCommentStatement: push,
|
|
143
|
+
GlimmerMustacheCommentStatement: push,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Removing nodes mid-traversal** — siblings are splice-safe:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
toTree(source, {
|
|
153
|
+
visitors: () => ({
|
|
154
|
+
GlimmerMustacheCommentStatement(node, path) {
|
|
155
|
+
const siblings = path.parent?.body ?? path.parent?.children;
|
|
156
|
+
const idx = siblings?.indexOf(node) ?? -1;
|
|
157
|
+
if (idx >= 0) siblings.splice(idx, 1);
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
60
163
|
## Examples
|
|
61
164
|
|
|
62
165
|
The [`examples/`](./examples) directory contains ready-to-run integrations:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-estree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "ESTree generator for gjs and gts file used by ember",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"AST",
|
|
@@ -42,16 +42,16 @@
|
|
|
42
42
|
"oxfmt": "^0.40.0",
|
|
43
43
|
"oxlint": "^1.55.0",
|
|
44
44
|
"publint": "^0.3.18",
|
|
45
|
-
"release-plan": "^0.
|
|
45
|
+
"release-plan": "^0.18.0",
|
|
46
46
|
"typescript": "^5.9.3",
|
|
47
47
|
"vitest": "^3.2.4"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"format": "oxfmt",
|
|
51
|
-
"format:check": "oxfmt --check",
|
|
52
50
|
"bench": "node --expose-gc tests/parser.bench.mjs",
|
|
53
51
|
"bench:compare": "node scripts/bench-compare.mjs",
|
|
54
52
|
"bench:summary": "./scripts/local-bench-summary.sh",
|
|
53
|
+
"format": "oxfmt",
|
|
54
|
+
"format:check": "oxfmt --check",
|
|
55
55
|
"lint": "oxlint && pnpm format:check && publint",
|
|
56
56
|
"lint:fix": "oxlint --fix && oxfmt",
|
|
57
57
|
"test": "vitest run"
|
package/src/index.d.ts
CHANGED
|
@@ -30,11 +30,6 @@ export interface VisitorPath {
|
|
|
30
30
|
export interface ParseOptions {
|
|
31
31
|
filePath?: string;
|
|
32
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
33
|
/**
|
|
39
34
|
* Custom JS/TS parser. Called with the placeholder JS string
|
|
40
35
|
* (templates replaced with backtick expressions of equal length).
|
|
@@ -42,15 +37,24 @@ export interface ParseOptions {
|
|
|
42
37
|
*/
|
|
43
38
|
parser?: (placeholderJS: string) => { ast: ASTNode; [key: string]: unknown };
|
|
44
39
|
/**
|
|
45
|
-
* Callbacks
|
|
46
|
-
*
|
|
40
|
+
* Callbacks fired on each node during traversal — outer JS/TS nodes AND
|
|
41
|
+
* spliced Glimmer subtrees — so callers can gather information or mutate
|
|
42
|
+
* the tree in a single pass.
|
|
43
|
+
*
|
|
44
|
+
* Pass either a plain handler map, or a factory `(outerAst) => handlers`
|
|
45
|
+
* that's called once after parsing (before template splicing) when you
|
|
46
|
+
* need a view of the raw JS/TS tree up front.
|
|
47
|
+
*
|
|
48
|
+
* The pseudo-type `GlimmerBlockParams` fires on any node that carries
|
|
49
|
+
* a `blockParams` array.
|
|
47
50
|
*/
|
|
48
|
-
visitors?:
|
|
49
|
-
[glimmerNodeType: string]: (node: ASTNode, path: VisitorPath) => void;
|
|
50
|
-
GlimmerBlockParams?: (node: ASTNode, path: VisitorPath) => void;
|
|
51
|
-
};
|
|
51
|
+
visitors?: VisitorMap | ((outerAst: ASTNode) => VisitorMap | null | undefined);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export type VisitorMap = {
|
|
55
|
+
[nodeType: string]: (node: ASTNode, path: VisitorPath) => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
54
58
|
export class DocumentLines {
|
|
55
59
|
constructor(source: string);
|
|
56
60
|
positionToOffset(pos: Position): number;
|
package/src/parse.js
CHANGED
|
@@ -33,21 +33,22 @@ const PLACEHOLDER_TYPES = new Set([
|
|
|
33
33
|
* @param {string} [options.filePath] - File path for language detection
|
|
34
34
|
* @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
|
|
35
35
|
* @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
|
|
36
|
-
* @param {object}
|
|
36
|
+
* @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
|
|
37
|
+
* handlers, or a factory `(outerAst) => handlers` invoked once after parsing (before any
|
|
38
|
+
* template splicing) to give callers a view of the raw JS/TS tree. Handlers fire on every
|
|
39
|
+
* node during traversal — outer JS/TS nodes AND spliced Glimmer subtrees — in a single pass.
|
|
40
|
+
* The pseudo-type `GlimmerBlockParams` fires on any node that carries `blockParams`.
|
|
37
41
|
* @return {object}
|
|
38
42
|
*/
|
|
39
43
|
export function toTree(source, options = {}) {
|
|
40
|
-
const templateOpts = options.includeParentLinks === false ? { includeParentLinks: false } : {};
|
|
41
|
-
|
|
42
44
|
if (options.templateOnly) {
|
|
43
|
-
return processTemplate(source, new DocumentLines(source), [0, source.length]
|
|
45
|
+
return processTemplate(source, new DocumentLines(source), [0, source.length]);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
let parseResults = preprocessor.parse(source);
|
|
47
49
|
let js = toPlaceholderJS(source, parseResults);
|
|
48
50
|
|
|
49
51
|
const useCustomParser = !!options.parser;
|
|
50
|
-
const visitors = options.visitors || null;
|
|
51
52
|
|
|
52
53
|
// Parse the placeholder JS — use custom parser or default oxc
|
|
53
54
|
let result;
|
|
@@ -73,8 +74,24 @@ export function toTree(source, options = {}) {
|
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
//
|
|
77
|
-
|
|
77
|
+
// Resolve user visitors against the outer AST. A plain object is used
|
|
78
|
+
// as-is; a factory is called once so callers can introspect the raw
|
|
79
|
+
// JS/TS tree before any template splicing. Default to `{}` so downstream
|
|
80
|
+
// dispatch can be a bare `visitors[type]` lookup without null-guards.
|
|
81
|
+
const visitors =
|
|
82
|
+
typeof options.visitors === "function"
|
|
83
|
+
? (options.visitors(result.ast) ?? {})
|
|
84
|
+
: (options.visitors ?? {});
|
|
85
|
+
const hasVisitors = Object.keys(visitors).length > 0;
|
|
86
|
+
// Guard against dispatching a handler twice on the same node.
|
|
87
|
+
// Visitors that relocate nodes (e.g. moving Glimmer comments into
|
|
88
|
+
// `program.comments`) would otherwise fire a second time when the walk
|
|
89
|
+
// reaches the new location.
|
|
90
|
+
const seen = new WeakSet();
|
|
91
|
+
const hasTemplates = parseResults.length > 0;
|
|
92
|
+
|
|
93
|
+
// Nothing to walk — attach visitor keys and return.
|
|
94
|
+
if (!hasTemplates && !hasVisitors) {
|
|
78
95
|
if (useCustomParser) {
|
|
79
96
|
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
80
97
|
return result;
|
|
@@ -83,12 +100,11 @@ export function toTree(source, options = {}) {
|
|
|
83
100
|
return result.ast;
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
const codeLines = new DocumentLines(source);
|
|
87
|
-
const allComments = [];
|
|
103
|
+
const codeLines = hasTemplates ? new DocumentLines(source) : null;
|
|
88
104
|
const templateInfos = [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
const templateRangeByStart = hasTemplates
|
|
106
|
+
? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
|
|
107
|
+
: null;
|
|
92
108
|
|
|
93
109
|
// Process a matched placeholder node: create Glimmer AST and tokens
|
|
94
110
|
function processPlaceholder(parseResult) {
|
|
@@ -99,12 +115,7 @@ export function toTree(source, options = {}) {
|
|
|
99
115
|
];
|
|
100
116
|
let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
|
|
101
117
|
|
|
102
|
-
const { ast
|
|
103
|
-
templateContent,
|
|
104
|
-
codeLines,
|
|
105
|
-
contentRange,
|
|
106
|
-
templateOpts,
|
|
107
|
-
);
|
|
118
|
+
const { ast } = processTemplate(templateContent, codeLines, contentRange);
|
|
108
119
|
|
|
109
120
|
// Fix the Template root to cover the full <template>...</template> range
|
|
110
121
|
ast.range = fullRange;
|
|
@@ -137,7 +148,6 @@ export function toTree(source, options = {}) {
|
|
|
137
148
|
makeToken(closeTag, [closeStart, fullRange[1]]),
|
|
138
149
|
];
|
|
139
150
|
|
|
140
|
-
allComments.push(...comments);
|
|
141
151
|
templateInfos.push({ utf16Range: fullRange, ast });
|
|
142
152
|
return ast;
|
|
143
153
|
}
|
|
@@ -160,101 +170,57 @@ export function toTree(source, options = {}) {
|
|
|
160
170
|
return parseResult;
|
|
161
171
|
}
|
|
162
172
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
167
|
-
|
|
168
|
-
if (visitors && node.type.startsWith("Glimmer")) {
|
|
169
|
-
const handler = visitors[node.type];
|
|
170
|
-
if (handler) handler(node, path);
|
|
171
|
-
if ("blockParams" in node && visitors.GlimmerBlockParams) {
|
|
172
|
-
visitors.GlimmerBlockParams(node, path);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const keys = glimmerVisitorKeys[node.type];
|
|
177
|
-
if (!keys) return;
|
|
178
|
-
for (const key of keys) {
|
|
179
|
-
const child = node[key];
|
|
180
|
-
if (!child) continue;
|
|
181
|
-
if (Array.isArray(child)) {
|
|
182
|
-
for (const item of child) {
|
|
183
|
-
walkGlimmerTree(item, path);
|
|
184
|
-
}
|
|
185
|
-
} else if (typeof child === "object" && child.type) {
|
|
186
|
-
walkGlimmerTree(child, path);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (useCustomParser) {
|
|
192
|
-
// Custom parser path: mutate the parser's AST in-place, invoke visitors.
|
|
193
|
-
// Use the parser's visitorKeys to traverse efficiently (avoids Object.keys).
|
|
194
|
-
const parserVisitorKeys = result.visitorKeys || {};
|
|
195
|
-
|
|
196
|
-
function visitNode(node, parentPath) {
|
|
197
|
-
if (!node || typeof node !== "object" || !node.type) return;
|
|
198
|
-
|
|
199
|
-
const path = { node, parent: parentPath?.node ?? null, parentPath };
|
|
200
|
-
|
|
201
|
-
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
173
|
+
result.ast = walk(result.ast, null, {
|
|
174
|
+
_(node, { next, visit, state }) {
|
|
175
|
+
if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
|
|
202
176
|
const parseResult = matchPlaceholder(node);
|
|
203
177
|
if (parseResult) {
|
|
204
178
|
const ast = processPlaceholder(parseResult);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return;
|
|
179
|
+
// Zimmerframe treats a visitor that returns a node as having
|
|
180
|
+
// taken responsibility for the subtree — it splices the result
|
|
181
|
+
// in but does NOT descend into it. So when any handlers are
|
|
182
|
+
// configured we re-enter the walk manually via `visit()` to
|
|
183
|
+
// dispatch them across the Glimmer nodes. With no handlers the
|
|
184
|
+
// walk would be pure overhead, so just return the subtree.
|
|
185
|
+
return hasVisitors ? visit(ast, null) : ast;
|
|
213
186
|
}
|
|
214
187
|
}
|
|
215
188
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
} else if (typeof child === "object" && child.type) {
|
|
229
|
-
visitNode(child, path);
|
|
189
|
+
const path = {
|
|
190
|
+
node,
|
|
191
|
+
parent: state?.parentPath?.node ?? null,
|
|
192
|
+
parentPath: state?.parentPath ?? null,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (hasVisitors && !seen.has(node)) {
|
|
196
|
+
seen.add(node);
|
|
197
|
+
const handler = visitors[node.type];
|
|
198
|
+
if (handler) handler(node, path);
|
|
199
|
+
if ("blockParams" in node && visitors.GlimmerBlockParams) {
|
|
200
|
+
visitors.GlimmerBlockParams(node, path);
|
|
230
201
|
}
|
|
231
202
|
}
|
|
232
|
-
}
|
|
233
203
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
result.ast = walk(result.ast, null, {
|
|
238
|
-
_(node, { next }) {
|
|
239
|
-
if (PLACEHOLDER_TYPES.has(node.type)) {
|
|
240
|
-
const parseResult = matchPlaceholder(node);
|
|
241
|
-
if (parseResult) {
|
|
242
|
-
return processPlaceholder(parseResult);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
next();
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// Walk Glimmer subtrees for visitors (after zimmerframe splicing)
|
|
250
|
-
if (visitors) {
|
|
251
|
-
for (const ti of templateInfos) {
|
|
252
|
-
walkGlimmerTree(ti.ast, null);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
204
|
+
next({ parentPath: path });
|
|
205
|
+
},
|
|
206
|
+
});
|
|
256
207
|
|
|
257
208
|
// Splice template tokens into the AST token stream.
|
|
209
|
+
//
|
|
210
|
+
// `tokens` is the flat lexed stream (keywords, punctuators, identifiers,
|
|
211
|
+
// literals) that ESLint, formatters, and source-map tooling consume —
|
|
212
|
+
// `SourceCode.getTokens()` reads it directly.
|
|
213
|
+
//
|
|
214
|
+
// We replaced each <template>...</template> region with a backtick
|
|
215
|
+
// placeholder before handing the source to the JS/TS parser, so the
|
|
216
|
+
// parser's tokens for those ranges describe the placeholder, not the
|
|
217
|
+
// real source. Here we swap them out for the real lexemes:
|
|
218
|
+
// 1. a fabricated `<template>` Punctuator (added in processPlaceholder)
|
|
219
|
+
// 2. the Glimmer AST's own tokens (from transforms.js)
|
|
220
|
+
// 3. a fabricated `</template>` Punctuator
|
|
221
|
+
// so consumers see a position-accurate token stream matching the
|
|
222
|
+
// original source byte-for-byte across JS and Glimmer regions.
|
|
223
|
+
//
|
|
258
224
|
// Tokens are sorted by range, so use binary search for O(log n) lookup.
|
|
259
225
|
const astRoot = result.ast.program || result.ast;
|
|
260
226
|
if (astRoot.tokens) {
|
|
@@ -279,12 +245,6 @@ export function toTree(source, options = {}) {
|
|
|
279
245
|
}
|
|
280
246
|
}
|
|
281
247
|
|
|
282
|
-
// Merge comments
|
|
283
|
-
if (allComments.length) {
|
|
284
|
-
if (!astRoot.comments) astRoot.comments = [];
|
|
285
|
-
astRoot.comments.push(...allComments);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
248
|
if (useCustomParser) {
|
|
289
249
|
result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
|
|
290
250
|
result.templateInfos = templateInfos;
|
package/src/print.js
CHANGED
|
@@ -860,7 +860,7 @@ export function print(node) {
|
|
|
860
860
|
return `<!--${node.value ?? ""}-->`;
|
|
861
861
|
|
|
862
862
|
case "GlimmerMustacheCommentStatement":
|
|
863
|
-
return `{{! ${node.value ?? ""} }}`;
|
|
863
|
+
return node.longForm ? `{{!-- ${node.value ?? ""} --}}` : `{{! ${node.value ?? ""} }}`;
|
|
864
864
|
|
|
865
865
|
case "GlimmerElementModifierStatement": {
|
|
866
866
|
const path = print(node.path);
|
package/src/transforms.js
CHANGED
|
@@ -61,6 +61,11 @@ export const glimmerVisitorKeys = (() => {
|
|
|
61
61
|
// VarHead: name, original
|
|
62
62
|
// Block: blockParams
|
|
63
63
|
const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
|
|
64
|
+
const _parentDesc = { value: null, configurable: true, enumerable: false, writable: true };
|
|
65
|
+
function setParent(node, parent) {
|
|
66
|
+
_parentDesc.value = parent;
|
|
67
|
+
Object.defineProperty(node, "parent", _parentDesc);
|
|
68
|
+
}
|
|
64
69
|
function defOwn(obj, key) {
|
|
65
70
|
_desc.value = obj[key];
|
|
66
71
|
Object.defineProperty(obj, key, _desc);
|
|
@@ -168,12 +173,7 @@ function buildTokenStream(rawTokens, comments, textNodes) {
|
|
|
168
173
|
* positions, create parts/blockParamNodes, nullify empty hashes, and
|
|
169
174
|
* prefix types. No separate collect-then-transform loop.
|
|
170
175
|
*/
|
|
171
|
-
export function processTemplate(
|
|
172
|
-
templateContent,
|
|
173
|
-
codeLines,
|
|
174
|
-
templateRange,
|
|
175
|
-
{ includeParentLinks = true } = {},
|
|
176
|
-
) {
|
|
176
|
+
export function processTemplate(templateContent, codeLines, templateRange) {
|
|
177
177
|
const offset = templateRange[0];
|
|
178
178
|
const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
|
|
179
179
|
|
|
@@ -187,7 +187,6 @@ export function processTemplate(
|
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
const ast = glimmerPreprocess(templateContent, { mode: "codemod" });
|
|
190
|
-
const allNodes = [];
|
|
191
190
|
const comments = [];
|
|
192
191
|
const textNodes = [];
|
|
193
192
|
const emptyTextNodes = [];
|
|
@@ -197,8 +196,7 @@ export function processTemplate(
|
|
|
197
196
|
// children using raw visitor keys. Type prefixing happens inline
|
|
198
197
|
// AFTER recursing (so children see the original type during lookup).
|
|
199
198
|
function visit(n, parent) {
|
|
200
|
-
n
|
|
201
|
-
allNodes.push(n);
|
|
199
|
+
setParent(n, parent);
|
|
202
200
|
|
|
203
201
|
// Categorize
|
|
204
202
|
if (n.type === "CommentStatement" || n.type === "MustacheCommentStatement") {
|
|
@@ -248,23 +246,26 @@ export function processTemplate(
|
|
|
248
246
|
n.end = n.range[1];
|
|
249
247
|
n.loc = toFileLoc(n.range);
|
|
250
248
|
|
|
249
|
+
if (n.type === "MustacheCommentStatement") {
|
|
250
|
+
n.longForm = templateContent.slice(n.start - offset, n.start - offset + 4) === "{{!-";
|
|
251
|
+
}
|
|
252
|
+
|
|
251
253
|
// Create parts for ElementNode
|
|
252
254
|
if (n.type === "ElementNode") {
|
|
253
255
|
n.name = n.tag;
|
|
254
256
|
const p = n.path.head;
|
|
255
257
|
const partRange = toFileRange(p.loc);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
];
|
|
258
|
+
const part = {
|
|
259
|
+
type: "GlimmerElementNodePart",
|
|
260
|
+
original: p.original,
|
|
261
|
+
name: p.original,
|
|
262
|
+
range: partRange,
|
|
263
|
+
start: partRange[0],
|
|
264
|
+
end: partRange[1],
|
|
265
|
+
loc: toFileLoc(partRange),
|
|
266
|
+
};
|
|
267
|
+
setParent(part, n);
|
|
268
|
+
n.parts = [part];
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
// Create blockParamNodes
|
|
@@ -272,27 +273,31 @@ export function processTemplate(
|
|
|
272
273
|
if (n.params && n.params.length === n.blockParams.length) {
|
|
273
274
|
n.blockParamNodes = n.params.map((p) => {
|
|
274
275
|
const range = toFileRange(p.loc);
|
|
275
|
-
|
|
276
|
+
const bp = {
|
|
276
277
|
type: "GlimmerBlockParam",
|
|
277
278
|
name: p.original || p.name,
|
|
278
279
|
original: p.original,
|
|
279
|
-
parent: n,
|
|
280
280
|
range,
|
|
281
281
|
start: range[0],
|
|
282
282
|
end: range[1],
|
|
283
283
|
loc: toFileLoc(range),
|
|
284
284
|
};
|
|
285
|
+
setParent(bp, n);
|
|
286
|
+
return bp;
|
|
285
287
|
});
|
|
286
288
|
} else {
|
|
287
|
-
n.blockParamNodes = n.blockParams.map((bpName) =>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
n.blockParamNodes = n.blockParams.map((bpName) => {
|
|
290
|
+
const bp = {
|
|
291
|
+
type: "GlimmerBlockParam",
|
|
292
|
+
name: bpName,
|
|
293
|
+
range: [n.range[0], n.range[1]],
|
|
294
|
+
start: n.range[0],
|
|
295
|
+
end: n.range[1],
|
|
296
|
+
loc: toFileLoc(n.range),
|
|
297
|
+
};
|
|
298
|
+
setParent(bp, n);
|
|
299
|
+
return bp;
|
|
300
|
+
});
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
303
|
|
|
@@ -329,21 +334,9 @@ export function processTemplate(
|
|
|
329
334
|
visit(ast, null);
|
|
330
335
|
|
|
331
336
|
removeFromParent(emptyTextNodes);
|
|
332
|
-
removeFromParent(comments);
|
|
333
|
-
for (const comment of comments) {
|
|
334
|
-
comment.type = "Block";
|
|
335
|
-
}
|
|
336
337
|
|
|
337
338
|
ast.tokens = buildTokenStream(tokenize(templateContent, codeLines, offset), comments, textNodes);
|
|
338
339
|
ast.contents = templateContent;
|
|
339
340
|
|
|
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
341
|
return { ast, comments };
|
|
349
342
|
}
|