ember-estree 0.4.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.
Files changed (2) hide show
  1. package/package.json +5 -1
  2. package/src/transforms.js +83 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-estree",
3
- "version": "0.4.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",
@@ -38,6 +38,7 @@
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/transforms.js CHANGED
@@ -54,46 +54,15 @@ export const glimmerVisitorKeys = (() => {
54
54
 
55
55
  // ── Internal helpers ──────────────────────────────────────────────────
56
56
 
57
- /**
58
- * Recursively collect all nodes in a Glimmer AST using visitor keys.
59
- * Sets parent references during traversal.
60
- */
61
- function collectNodes(node, parent, allNodes, comments, textNodes, emptyTextNodes) {
62
- node.parent = parent;
63
- allNodes.push(node);
64
- if (node.type === "CommentStatement" || node.type === "MustacheCommentStatement") {
65
- comments.push(node);
66
- }
67
- if (node.type === "TextNode") {
68
- node.value = node.chars;
69
- if (node.value.trim().length !== 0 || (parent && parent.type === "AttrNode")) {
70
- textNodes.push(node);
71
- } else {
72
- emptyTextNodes.push(node);
73
- }
74
- }
75
- const keys = rawGlimmerVisitorKeys[node.type];
76
- if (!keys) return;
77
- for (const key of keys) {
78
- const child = node[key];
79
- if (!child) continue;
80
- if (Array.isArray(child)) {
81
- for (const item of child) {
82
- if (item && typeof item === "object" && item.type) {
83
- collectNodes(item, node, allNodes, comments, textNodes, emptyTextNodes);
84
- }
85
- }
86
- } else if (typeof child === "object" && child.type) {
87
- collectNodes(child, node, allNodes, comments, textNodes, emptyTextNodes);
88
- }
89
- }
90
- }
91
-
92
- // Reusable descriptor — shadows prototype getters with own properties.
93
- // @glimmer/syntax nodes have getter chains that cause esrecurse infinite recursion.
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
94
63
  const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
95
- function defOwn(obj, key, val) {
96
- _desc.value = val;
64
+ function defOwn(obj, key) {
65
+ _desc.value = obj[key];
97
66
  Object.defineProperty(obj, key, _desc);
98
67
  }
99
68
 
@@ -194,6 +163,10 @@ function buildTokenStream(rawTokens, comments, textNodes) {
194
163
  /**
195
164
  * Parse and transform a Glimmer template into an ESTree-compatible AST.
196
165
  * Internal — consumed by toTree.
166
+ *
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.
197
170
  */
198
171
  export function processTemplate(
199
172
  templateContent,
@@ -202,7 +175,7 @@ export function processTemplate(
202
175
  { includeParentLinks = true } = {},
203
176
  ) {
204
177
  const offset = templateRange[0];
205
- const docLines = new DocumentLines(templateContent);
178
+ const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
206
179
 
207
180
  const toFileRange = (loc) => [
208
181
  offset + docLines.positionToOffset(loc.start),
@@ -218,27 +191,67 @@ export function processTemplate(
218
191
  const comments = [];
219
192
  const textNodes = [];
220
193
  const emptyTextNodes = [];
221
- collectNodes(ast, null, allNodes, comments, textNodes, emptyTextNodes);
222
194
 
223
- for (const n of allNodes) {
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);
202
+
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);
213
+ }
214
+ }
215
+
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
224
240
  if (n.type === "PathExpression") {
225
241
  n.head.range = toFileRange(n.head.loc);
226
242
  n.head.start = n.head.range[0];
227
243
  n.head.end = n.head.range[1];
228
244
  n.head.loc = toFileLoc(n.head.range);
229
245
  }
230
-
231
246
  n.range = n.type === "Template" ? [...templateRange] : toFileRange(n.loc);
232
247
  n.start = n.range[0];
233
248
  n.end = n.range[1];
234
249
  n.loc = toFileLoc(n.range);
235
250
 
251
+ // Create parts for ElementNode
236
252
  if (n.type === "ElementNode") {
237
- defOwn(n, "tag", n.tag);
238
253
  n.name = n.tag;
239
254
  const p = n.path.head;
240
- defOwn(p, "name", p.name);
241
- defOwn(p, "original", p.original);
242
255
  const partRange = toFileRange(p.loc);
243
256
  n.parts = [
244
257
  {
@@ -254,11 +267,10 @@ export function processTemplate(
254
267
  ];
255
268
  }
256
269
 
270
+ // Create blockParamNodes
257
271
  if ("blockParams" in n && Array.isArray(n.blockParams)) {
258
272
  if (n.params && n.params.length === n.blockParams.length) {
259
273
  n.blockParamNodes = n.params.map((p) => {
260
- defOwn(p, "name", p.name);
261
- defOwn(p, "original", p.original);
262
274
  const range = toFileRange(p.loc);
263
275
  return {
264
276
  type: "GlimmerBlockParam",
@@ -272,9 +284,9 @@ export function processTemplate(
272
284
  };
273
285
  });
274
286
  } else {
275
- n.blockParamNodes = n.blockParams.map((name) => ({
287
+ n.blockParamNodes = n.blockParams.map((bpName) => ({
276
288
  type: "GlimmerBlockParam",
277
- name,
289
+ name: bpName,
278
290
  parent: n,
279
291
  range: [n.range[0], n.range[1]],
280
292
  start: n.range[0],
@@ -284,26 +296,38 @@ export function processTemplate(
284
296
  }
285
297
  }
286
298
 
299
+ // Nullify empty hashes
287
300
  if (
288
301
  (n.type === "MustacheStatement" ||
289
302
  n.type === "BlockStatement" ||
290
303
  n.type === "SubExpression") &&
291
- n.hash &&
292
- n.hash.pairs &&
293
- n.hash.pairs.length === 0
304
+ n.hash?.pairs?.length === 0
294
305
  ) {
295
306
  n.hash = null;
296
307
  }
297
308
 
298
- n.type = `Glimmer${n.type}`;
299
-
300
- // Snapshot PathExpression.head getters (name/original)
301
- if (n.type === "GlimmerPathExpression" && n.head) {
302
- defOwn(n.head, "name", n.head.name);
303
- defOwn(n.head, "original", n.head.original);
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
+ }
304
323
  }
324
+
325
+ // Prefix type after children are visited
326
+ n.type = `Glimmer${n.type}`;
305
327
  }
306
328
 
329
+ visit(ast, null);
330
+
307
331
  removeFromParent(emptyTextNodes);
308
332
  removeFromParent(comments);
309
333
  for (const comment of comments) {