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 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.4.2",
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.17.4",
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 invoked for Glimmer nodes during the AST splice traversal.
46
- * Runs in DFS order, so parent nodes are visited before children.
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} [options.visitors] - Callbacks invoked for Glimmer nodes during traversal
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], templateOpts);
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
- // If no templates, return early
77
- if (!parseResults.length) {
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
- // Build a map of template ranges for lookup
91
- const templateRangeByStart = new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]));
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, comments } = processTemplate(
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
- // Walk Glimmer subtree, invoking visitors with full path context
164
- function walkGlimmerTree(node, parentPath) {
165
- if (!node || typeof node !== "object" || !node.type) return;
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
- for (const key of Object.keys(node)) {
206
- if (!(key in ast) && key !== "parent") {
207
- delete node[key];
208
- }
209
- }
210
- Object.assign(node, ast);
211
- if (visitors) walkGlimmerTree(node, parentPath);
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
- // Use visitorKeys for efficient child traversal
217
- const keys = parserVisitorKeys[node.type];
218
- if (!keys) return;
219
- for (const key of keys) {
220
- const child = node[key];
221
- if (!child) continue;
222
- if (Array.isArray(child)) {
223
- for (const item of child) {
224
- if (item && typeof item === "object" && item.type) {
225
- visitNode(item, path);
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
- visitNode(result.ast, null);
235
- } else {
236
- // Default oxc path: use zimmerframe walk (returns new tree)
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.parent = parent;
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
- n.parts = [
257
- {
258
- type: "GlimmerElementNodePart",
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),
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
- return {
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
- type: "GlimmerBlockParam",
289
- name: bpName,
290
- parent: n,
291
- range: [n.range[0], n.range[1]],
292
- start: n.range[0],
293
- end: n.range[1],
294
- loc: toFileLoc(n.range),
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
  }