ember-estree 0.6.0 → 0.6.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-estree",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "ESTree generator for gjs and gts file used by ember",
5
5
  "keywords": [
6
6
  "AST",
@@ -33,18 +33,19 @@
33
33
  "@glimmer/env": "^0.1.7",
34
34
  "@glimmer/syntax": "^0.95.0",
35
35
  "content-tag": "^4.1.0",
36
- "oxc-parser": "^0.119.0",
37
- "zimmerframe": "^1.1.4"
36
+ "oxc-parser": "^0.119.0"
38
37
  },
39
38
  "devDependencies": {
40
39
  "@tsconfig/node-lts": "^22.0.2",
40
+ "@typescript-eslint/parser": "^8.59.0",
41
41
  "mitata": "^1.0.34",
42
42
  "oxfmt": "^0.40.0",
43
43
  "oxlint": "^1.55.0",
44
44
  "publint": "^0.3.18",
45
45
  "release-plan": "^0.18.0",
46
46
  "typescript": "^5.9.3",
47
- "vitest": "^3.2.4"
47
+ "vitest": "^3.2.4",
48
+ "zimmerframe": "^1.1.4"
48
49
  },
49
50
  "scripts": {
50
51
  "bench": "node --expose-gc tests/parser.bench.mjs",
package/src/parse.js CHANGED
@@ -9,11 +9,45 @@
9
9
  * 6. Done
10
10
  */
11
11
 
12
- import { parseSync } from "oxc-parser";
12
+ import { parseSync, visitorKeys as oxcVisitorKeys } from "oxc-parser";
13
13
  import { Preprocessor } from "content-tag";
14
- import { walk } from "zimmerframe";
15
14
 
16
- import { processTemplate, DocumentLines, glimmerVisitorKeys } from "./transforms.js";
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
+ }
17
51
 
18
52
  const preprocessor = new Preprocessor();
19
53
 
@@ -33,7 +67,9 @@ const PLACEHOLDER_TYPES = new Set([
33
67
  * @param {string} [options.filePath] - File path for language detection
34
68
  * @param {boolean} [options.tokens] - Generate a flat token stream on the AST (needed by ESLint; skipped by default)
35
69
  * @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
36
- * @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
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).
37
73
  * @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
38
74
  * handlers, or a factory `(outerAst) => handlers` invoked once after parsing (before any
39
75
  * template splicing) to give callers a view of the raw JS/TS tree. Handlers fire on every
@@ -112,8 +148,11 @@ export function toTree(source, options = {}) {
112
148
  ? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
113
149
  : null;
114
150
 
115
- // Process a matched placeholder node: create Glimmer AST and tokens
116
- function processPlaceholder(parseResult) {
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) {
117
156
  let templateContent = parseResult.contents;
118
157
  let contentRange = [
119
158
  parseResult.contentRange.startUtf16Codepoint,
@@ -159,7 +198,7 @@ export function toTree(source, options = {}) {
159
198
  ];
160
199
  }
161
200
 
162
- templateInfos.push({ utf16Range: fullRange, ast });
201
+ templateInfos.push({ utf16Range: fullRange, ast, placeholder: placeholderNode });
163
202
  return ast;
164
203
  }
165
204
 
@@ -181,47 +220,66 @@ export function toTree(source, options = {}) {
181
220
  return parseResult;
182
221
  }
183
222
 
184
- result.ast = walk(result.ast, null, {
185
- _(node, { next, visit, state }) {
186
- if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
187
- const parseResult = matchPlaceholder(node);
188
- if (parseResult) {
189
- const ast = processPlaceholder(parseResult);
190
- // Zimmerframe treats a visitor that returns a node as having
191
- // taken responsibility for the subtree — it splices the result
192
- // in but does NOT descend into it. So when any handlers are
193
- // configured we re-enter the walk manually via `visit()` to
194
- // dispatch them across the Glimmer nodes. With no handlers the
195
- // walk would be pure overhead, so just return the subtree.
196
- //
197
- // Pass `state` (the placeholder's inherited parent context) so the
198
- // Glimmer root's parentPath reflects its true JS parent. The
199
- // placeholder (TemplateLiteral / StaticBlock) is an internal
200
- // artifact the GlimmerTemplate logically lives where the
201
- // placeholder was, so its parent is e.g. VariableDeclarator or
202
- // ClassBody, not the placeholder itself.
203
- return hasVisitors ? visit(ast, state) : ast;
204
- }
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;
205
251
  }
252
+ }
206
253
 
207
- const path = {
208
- node,
209
- parent: state?.parentPath?.node ?? null,
210
- parentPath: state?.parentPath ?? null,
211
- };
212
-
213
- if (hasVisitors && !seen.has(node)) {
214
- seen.add(node);
215
- const handler = visitors[node.type];
216
- if (handler) handler(node, path);
217
- if ("blockParams" in node && visitors.GlimmerBlockParams) {
218
- visitors.GlimmerBlockParams(node, path);
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
+ }
219
275
  }
276
+ } else if (typeof child === "object" && child.type) {
277
+ walkWithKeys(child, path);
220
278
  }
279
+ }
280
+ }
221
281
 
222
- next({ parentPath: path });
223
- },
224
- });
282
+ walkWithKeys(result.ast, null);
225
283
 
226
284
  // Splice template tokens into the AST token stream.
227
285
  //
@@ -240,8 +298,11 @@ export function toTree(source, options = {}) {
240
298
  // original source byte-for-byte across JS and Glimmer regions.
241
299
  //
242
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.
243
304
  const astRoot = result.ast.program || result.ast;
244
- if (astRoot.tokens) {
305
+ if (generateTokens && astRoot.tokens) {
245
306
  for (const ti of templateInfos) {
246
307
  const [tStart, tEnd] = ti.utf16Range;
247
308
  const tokens = astRoot.tokens;
package/src/transforms.js CHANGED
@@ -64,7 +64,7 @@ export const glimmerVisitorKeys = (() => {
64
64
  // Block: blockParams
65
65
  const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
66
66
  const _parentDesc = { value: null, configurable: true, enumerable: false, writable: true };
67
- function setParent(node, parent) {
67
+ export function setParent(node, parent) {
68
68
  _parentDesc.value = parent;
69
69
  Object.defineProperty(node, "parent", _parentDesc);
70
70
  }