ember-estree 0.5.1 → 0.6.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/README.md CHANGED
@@ -63,14 +63,35 @@ Both `toTree` and `parse` accept an options object as their second argument.
63
63
 
64
64
  All options are optional.
65
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.
66
+ | Option | Type | Description |
67
+ | -------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
68
+ | `filePath` | `string` | Used for language detection. |
69
+ | `tokens` | `boolean` | Generate a flat `ast.tokens` array. Required by ESLint; skipped by default so codemods and type-checkers pay nothing. |
70
+ | `templateOnly` | `boolean` | Parse the source as a raw Glimmer template. Use for `.hbs` files. |
71
+ | `parser` | `(placeholderJS: string) => { ast, ... }` | Use a custom JS/TS parser instead of the default oxc-parser. See [Custom parser](#custom-parser). |
72
+ | `visitors` | `VisitorMap` <br /> or `(outerAst) => VisitorMap` | Callbacks fired on every node during traversal — JS/TS and Glimmer — in a single pass. See [Visitors](#visitors). |
73
+
74
+ Handler signature is `(node, path) => void`, where `path = { node, parent, parentPath }` — a linked list that walks all the way back through the JS/TS root, so visitors can locate the enclosing scope or class from within a Glimmer subtree.
75
+
76
+ ### Token stream
77
+
78
+ Pass `tokens: true` to populate `ast.tokens` with a flat, position-sorted array of lexemes spanning the full file — including Glimmer tokens spliced in place of each `<template>` region. This is what ESLint's `SourceCode` needs; omit it for codemods or type-checkers that don't use the token stream.
79
+
80
+ ```js
81
+ import { toTree } from "ember-estree";
82
+
83
+ const result = toTree(source, {
84
+ tokens: true,
85
+ parser: myTsParser,
86
+ });
87
+ // result.ast.program.tokens now contains JS + Glimmer tokens in source order
88
+ ```
89
+
90
+ For `.hbs` files via `templateOnly`, pass both flags:
91
+
92
+ ```js
93
+ toTree(hbsSource, { templateOnly: true, tokens: true });
94
+ ```
74
95
 
75
96
  ### Custom parser
76
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-estree",
3
- "version": "0.5.1",
3
+ "version": "0.6.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
+ "@typescript-eslint/parser": "^8.59.0",
41
42
  "mitata": "^1.0.34",
42
43
  "oxfmt": "^0.40.0",
43
44
  "oxlint": "^1.55.0",
package/src/parse.js CHANGED
@@ -13,7 +13,29 @@ import { parseSync } from "oxc-parser";
13
13
  import { Preprocessor } from "content-tag";
14
14
  import { walk } from "zimmerframe";
15
15
 
16
- import { processTemplate, DocumentLines, glimmerVisitorKeys } from "./transforms.js";
16
+ import { processTemplate, DocumentLines, glimmerVisitorKeys, setParent } from "./transforms.js";
17
+
18
+ // Swap `oldNode` for `newNode` in whichever slot of `parent` currently holds it.
19
+ // Used to splice a GlimmerTemplate directly into the outer AST without
20
+ // allocating new ancestor objects — keeps WeakMap-keyed data (scope manager,
21
+ // esTreeNodeToTSNodeMap) attached to the existing nodes.
22
+ function replaceInParent(parent, oldNode, newNode) {
23
+ for (const key of Object.keys(parent)) {
24
+ const v = parent[key];
25
+ if (v === oldNode) {
26
+ parent[key] = newNode;
27
+ return true;
28
+ }
29
+ if (Array.isArray(v)) {
30
+ const idx = v.indexOf(oldNode);
31
+ if (idx !== -1) {
32
+ v[idx] = newNode;
33
+ return true;
34
+ }
35
+ }
36
+ }
37
+ return false;
38
+ }
17
39
 
18
40
  const preprocessor = new Preprocessor();
19
41
 
@@ -31,6 +53,7 @@ const PLACEHOLDER_TYPES = new Set([
31
53
  * @param {string} source
32
54
  * @param {object} [options]
33
55
  * @param {string} [options.filePath] - File path for language detection
56
+ * @param {boolean} [options.tokens] - Generate a flat token stream on the AST (needed by ESLint; skipped by default)
34
57
  * @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
35
58
  * @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }
36
59
  * @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
@@ -41,8 +64,13 @@ const PLACEHOLDER_TYPES = new Set([
41
64
  * @return {object}
42
65
  */
43
66
  export function toTree(source, options = {}) {
67
+ const generateTokens = !!options.tokens;
68
+
44
69
  if (options.templateOnly) {
45
- return processTemplate(source, new DocumentLines(source), [0, source.length]);
70
+ return processTemplate(source, new DocumentLines(source), {
71
+ templateRange: [0, source.length],
72
+ tokens: generateTokens,
73
+ });
46
74
  }
47
75
 
48
76
  let parseResults = preprocessor.parse(source);
@@ -106,8 +134,11 @@ export function toTree(source, options = {}) {
106
134
  ? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
107
135
  : null;
108
136
 
109
- // Process a matched placeholder node: create Glimmer AST and tokens
110
- function processPlaceholder(parseResult) {
137
+ // Process a matched placeholder node: create Glimmer AST and tokens.
138
+ // `placeholderNode` is the original JS/TS node being swapped out; we stash
139
+ // it on templateInfos so consumers can forward its parser-services mapping
140
+ // (e.g. esTreeNodeToTSNodeMap) onto the GlimmerTemplate that replaces it.
141
+ function processPlaceholder(parseResult, placeholderNode) {
111
142
  let templateContent = parseResult.contents;
112
143
  let contentRange = [
113
144
  parseResult.contentRange.startUtf16Codepoint,
@@ -115,7 +146,10 @@ export function toTree(source, options = {}) {
115
146
  ];
116
147
  let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
117
148
 
118
- const { ast } = processTemplate(templateContent, codeLines, contentRange);
149
+ const { ast } = processTemplate(templateContent, codeLines, {
150
+ templateRange: contentRange,
151
+ tokens: generateTokens,
152
+ });
119
153
 
120
154
  // Fix the Template root to cover the full <template>...</template> range
121
155
  ast.range = fullRange;
@@ -126,29 +160,31 @@ export function toTree(source, options = {}) {
126
160
  end: codeLines.offsetToPosition(fullRange[1]),
127
161
  };
128
162
 
129
- // Add tokens for the <template> and </template> tags
130
- const openEnd = contentRange[0];
131
- const closeStart = contentRange[1];
132
- const openTag = source.slice(fullRange[0], openEnd);
133
- const closeTag = source.slice(closeStart, fullRange[1]);
134
- const makeToken = (value, range) => ({
135
- type: "Punctuator",
136
- value,
137
- range,
138
- start: range[0],
139
- end: range[1],
140
- loc: {
141
- start: codeLines.offsetToPosition(range[0]),
142
- end: codeLines.offsetToPosition(range[1]),
143
- },
144
- });
145
- ast.tokens = [
146
- makeToken(openTag, [fullRange[0], openEnd]),
147
- ...(ast.tokens || []),
148
- makeToken(closeTag, [closeStart, fullRange[1]]),
149
- ];
163
+ if (generateTokens) {
164
+ // Add tokens for the <template> and </template> tags
165
+ const openEnd = contentRange[0];
166
+ const closeStart = contentRange[1];
167
+ const openTag = source.slice(fullRange[0], openEnd);
168
+ const closeTag = source.slice(closeStart, fullRange[1]);
169
+ const makeToken = (value, range) => ({
170
+ type: "Punctuator",
171
+ value,
172
+ range,
173
+ start: range[0],
174
+ end: range[1],
175
+ loc: {
176
+ start: codeLines.offsetToPosition(range[0]),
177
+ end: codeLines.offsetToPosition(range[1]),
178
+ },
179
+ });
180
+ ast.tokens = [
181
+ makeToken(openTag, [fullRange[0], openEnd]),
182
+ ...(ast.tokens || []),
183
+ makeToken(closeTag, [closeStart, fullRange[1]]),
184
+ ];
185
+ }
150
186
 
151
- templateInfos.push({ utf16Range: fullRange, ast });
187
+ templateInfos.push({ utf16Range: fullRange, ast, placeholder: placeholderNode });
152
188
  return ast;
153
189
  }
154
190
 
@@ -175,21 +211,23 @@ export function toTree(source, options = {}) {
175
211
  if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
176
212
  const parseResult = matchPlaceholder(node);
177
213
  if (parseResult) {
178
- const ast = processPlaceholder(parseResult);
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
- //
186
- // Pass `state` (the placeholder's inherited parent context) so the
187
- // Glimmer root's parentPath reflects its true JS parent. The
214
+ const ast = processPlaceholder(parseResult, node);
215
+ // Splice in place: write the GlimmerTemplate directly into the
216
+ // parent's slot instead of returning it from the visitor. Returning
217
+ // would trigger zimmerframe's apply_mutations, which shallow-clones
218
+ // every ancestor up to the root orphaning any WeakMap-keyed data
219
+ // held by custom parsers (scope manager, esTreeNodeToTSNodeMap).
220
+ // In-place mutation preserves node identity for all ancestors.
221
+ const parent = state?.parentPath?.node ?? null;
222
+ if (parent) replaceInParent(parent, node, ast);
223
+ setParent(ast, parent);
224
+ // Dispatch visitors on the Glimmer subtree. We pass `state` so the
225
+ // Glimmer root's parentPath reflects its true JS parent — the
188
226
  // placeholder (TemplateLiteral / StaticBlock) is an internal
189
- // artifact the GlimmerTemplate logically lives where the
190
- // placeholder was, so its parent is e.g. VariableDeclarator or
191
- // ClassBody, not the placeholder itself.
192
- return hasVisitors ? visit(ast, state) : ast;
227
+ // artifact. Not returning anything keeps apply_mutations from
228
+ // firing up the ancestor chain.
229
+ if (hasVisitors) visit(ast, state);
230
+ return;
193
231
  }
194
232
  }
195
233
 
@@ -229,8 +267,11 @@ export function toTree(source, options = {}) {
229
267
  // original source byte-for-byte across JS and Glimmer regions.
230
268
  //
231
269
  // Tokens are sorted by range, so use binary search for O(log n) lookup.
270
+ // Only splice if the caller asked for tokens — otherwise `ti.ast.tokens`
271
+ // wasn't populated by processPlaceholder, and a custom parser may still
272
+ // have returned its own token stream we shouldn't touch.
232
273
  const astRoot = result.ast.program || result.ast;
233
- if (astRoot.tokens) {
274
+ if (generateTokens && astRoot.tokens) {
234
275
  for (const ti of templateInfos) {
235
276
  const [tStart, tEnd] = ti.utf16Range;
236
277
  const tokens = astRoot.tokens;
package/src/tokens.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Token stream generation for Glimmer templates.
3
+ *
4
+ * Only needed by ESLint consumers — codemods, type-checkers, and formatters
5
+ * do not use the flat token stream and skip this entirely by omitting
6
+ * `{ tokens: true }` from processTemplate options.
7
+ *
8
+ * ESLint-specific note: comment tokens are placed at range[0]+1 rather than
9
+ * range[0] because ESLint's createIndexMap infinite-loops when a token and an
10
+ * ast.comments entry share range[0] — both inner loops fail the strict-less-than
11
+ * guard and the outer loop never advances. Tracked upstream as eslint/eslint#20492.
12
+ */
13
+
14
+ function isAlphaNumeric(code) {
15
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
16
+ }
17
+
18
+ /**
19
+ * Lex a Glimmer template into a flat token array.
20
+ * Tracks line/column incrementally from a single seed call to
21
+ * doc.offsetToPosition — O(n + log L) instead of O(n log L).
22
+ */
23
+ export function tokenize(template, doc, startOffset) {
24
+ const tokens = [];
25
+ let wordStart = -1;
26
+
27
+ // Seed position from the start offset — one binary search total.
28
+ // Then track line/column incrementally (DocumentLines counts only \n
29
+ // as line separators, so this matches offsetToPosition exactly).
30
+ let { line: curLine, column: curCol } = doc.offsetToPosition(startOffset);
31
+ let wordLine = 0,
32
+ wordCol = 0;
33
+
34
+ for (let i = 0; i < template.length; i++) {
35
+ const code = template.charCodeAt(i);
36
+ if (isAlphaNumeric(code)) {
37
+ if (wordStart < 0) {
38
+ wordStart = i;
39
+ wordLine = curLine;
40
+ wordCol = curCol;
41
+ }
42
+ curCol++;
43
+ } else {
44
+ if (wordStart >= 0) {
45
+ const absStart = startOffset + wordStart;
46
+ const absEnd = startOffset + i;
47
+ tokens.push({
48
+ type: "word",
49
+ value: template.slice(wordStart, i),
50
+ range: [absStart, absEnd],
51
+ start: absStart,
52
+ end: absEnd,
53
+ loc: {
54
+ start: { line: wordLine, column: wordCol, index: absStart },
55
+ end: { line: curLine, column: curCol, index: absEnd },
56
+ },
57
+ });
58
+ wordStart = -1;
59
+ }
60
+ if (code === 10 /* \n */) {
61
+ curLine++;
62
+ curCol = 0;
63
+ } else {
64
+ if (code !== 32 && code !== 9 && code !== 13 && code !== 11 /* non-whitespace */) {
65
+ const absPos = startOffset + i;
66
+ tokens.push({
67
+ type: "Punctuator",
68
+ value: template[i],
69
+ range: [absPos, absPos + 1],
70
+ start: absPos,
71
+ end: absPos + 1,
72
+ loc: {
73
+ start: { line: curLine, column: curCol, index: absPos },
74
+ end: { line: curLine, column: curCol + 1, index: absPos + 1 },
75
+ },
76
+ });
77
+ }
78
+ curCol++;
79
+ }
80
+ }
81
+ }
82
+
83
+ if (wordStart >= 0) {
84
+ const absStart = startOffset + wordStart;
85
+ const absEnd = startOffset + template.length;
86
+ tokens.push({
87
+ type: "word",
88
+ value: template.slice(wordStart),
89
+ range: [absStart, absEnd],
90
+ start: absStart,
91
+ end: absEnd,
92
+ loc: {
93
+ start: { line: wordLine, column: wordCol, index: absStart },
94
+ end: { line: curLine, column: curCol, index: absEnd },
95
+ },
96
+ });
97
+ }
98
+
99
+ return tokens;
100
+ }
101
+
102
+ /**
103
+ * Merge the raw Glimmer token stream with text nodes and comment tokens,
104
+ * dropping raw tokens that fall inside comment or text-node intervals.
105
+ *
106
+ * All inputs are sorted by range[0] (sequential document scan), so the
107
+ * entire operation is a single O(n+m+k) pass with advancing pointers
108
+ * rather than O(n log m) per-token binary searches.
109
+ */
110
+ export function buildTokenStream(rawTokens, comments, textNodes, templateContent, offset) {
111
+ // Comment tokens shifted by 1 to avoid the ESLint createIndexMap conflict
112
+ const commentTokens = comments.map((c) => {
113
+ const start = c.range[0] + 1;
114
+ const end = c.range[1];
115
+ return {
116
+ type: "Block",
117
+ value: templateContent.slice(start - offset, end - offset),
118
+ range: [start, end],
119
+ start,
120
+ end,
121
+ loc: c.loc,
122
+ };
123
+ });
124
+
125
+ const spliceables = linearMerge(textNodes, commentTokens);
126
+
127
+ const result = [];
128
+ let ri = 0; // rawTokens
129
+ let si = 0; // spliceables
130
+ let ci = 0; // comment intervals (for skip detection)
131
+ let ni = 0; // text-node intervals (for skip detection)
132
+
133
+ while (ri < rawTokens.length || si < spliceables.length) {
134
+ if (ri >= rawTokens.length) {
135
+ result.push(spliceables[si++]);
136
+ continue;
137
+ }
138
+
139
+ const tok = rawTokens[ri];
140
+
141
+ // Advance interval pointers past intervals that end before this token
142
+ while (ci < comments.length && comments[ci].range[1] <= tok.range[0]) ci++;
143
+ while (ni < textNodes.length && textNodes[ni].range[1] <= tok.range[0]) ni++;
144
+
145
+ // Skip raw token if it falls inside a comment or text-node interval
146
+ if (
147
+ (ci < comments.length && comments[ci].range[0] <= tok.range[0]) ||
148
+ (ni < textNodes.length && textNodes[ni].range[0] <= tok.range[0])
149
+ ) {
150
+ ri++;
151
+ continue;
152
+ }
153
+
154
+ // Emit the earlier of the next spliceable or this raw token
155
+ if (si < spliceables.length && spliceables[si].range[0] < tok.range[0]) {
156
+ result.push(spliceables[si++]);
157
+ } else {
158
+ result.push(tok);
159
+ ri++;
160
+ }
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ function linearMerge(a, b) {
167
+ const result = Array.from({ length: a.length + b.length });
168
+ let ai = 0,
169
+ bi = 0,
170
+ ri = 0;
171
+ while (ai < a.length && bi < b.length) {
172
+ result[ri++] = a[ai].range[0] <= b[bi].range[0] ? a[ai++] : b[bi++];
173
+ }
174
+ while (ai < a.length) result[ri++] = a[ai++];
175
+ while (bi < b.length) result[ri++] = b[bi++];
176
+ return result;
177
+ }
package/src/transforms.js CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  preprocess as glimmerPreprocess,
8
8
  } from "@glimmer/syntax";
9
9
 
10
+ import { tokenize, buildTokenStream } from "./tokens.js";
11
+
10
12
  /**
11
13
  * Converts between character offsets and line/column positions.
12
14
  * Lines are 1-based, columns are 0-based (matching ESTree & Glimmer conventions).
@@ -62,7 +64,7 @@ export const glimmerVisitorKeys = (() => {
62
64
  // Block: blockParams
63
65
  const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
64
66
  const _parentDesc = { value: null, configurable: true, enumerable: false, writable: true };
65
- function setParent(node, parent) {
67
+ export function setParent(node, parent) {
66
68
  _parentDesc.value = parent;
67
69
  Object.defineProperty(node, "parent", _parentDesc);
68
70
  }
@@ -82,89 +84,6 @@ function removeFromParent(nodes) {
82
84
  }
83
85
  }
84
86
 
85
- function isAlphaNumeric(code) {
86
- return !(!(code > 47 && code < 58) && !(code > 64 && code < 91) && !(code > 96 && code < 123));
87
- }
88
-
89
- function isWhiteSpaceCode(code) {
90
- return code === 32 || code === 9 || code === 13 || code === 10 || code === 11;
91
- }
92
-
93
- function tokenize(template, doc, startOffset) {
94
- const tokens = [];
95
- let wordStart = -1;
96
- function pushToken(value, type, range) {
97
- tokens.push({
98
- type,
99
- value,
100
- range,
101
- start: range[0],
102
- end: range[1],
103
- loc: {
104
- start: { ...doc.offsetToPosition(range[0]), index: range[0] },
105
- end: { ...doc.offsetToPosition(range[1]), index: range[1] },
106
- },
107
- });
108
- }
109
- for (let i = 0; i < template.length; i++) {
110
- const code = template.charCodeAt(i);
111
- if (isAlphaNumeric(code)) {
112
- if (wordStart < 0) wordStart = i;
113
- } else {
114
- if (wordStart >= 0) {
115
- pushToken(template.slice(wordStart, i), "word", [startOffset + wordStart, startOffset + i]);
116
- wordStart = -1;
117
- }
118
- if (!isWhiteSpaceCode(code)) {
119
- pushToken(template[i], "Punctuator", [startOffset + i, startOffset + i + 1]);
120
- }
121
- }
122
- }
123
- if (wordStart >= 0) {
124
- pushToken(template.slice(wordStart), "word", [
125
- startOffset + wordStart,
126
- startOffset + template.length,
127
- ]);
128
- }
129
- return tokens;
130
- }
131
-
132
- function buildTokenStream(rawTokens, comments, textNodes) {
133
- const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]);
134
- const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]);
135
-
136
- function isCovered(tokenRange, intervals) {
137
- let lo = 0;
138
- let hi = intervals.length - 1;
139
- while (lo <= hi) {
140
- const mid = (lo + hi) >> 1;
141
- const iv = intervals[mid];
142
- if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) return true;
143
- if (iv[0] > tokenRange[0]) hi = mid - 1;
144
- else lo = mid + 1;
145
- }
146
- return false;
147
- }
148
-
149
- const filteredTokens = rawTokens.filter(
150
- (t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals),
151
- );
152
-
153
- const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]);
154
- const result = [];
155
- let ti = 0;
156
- for (const token of filteredTokens) {
157
- while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) {
158
- result.push(sortedTextNodes[ti++]);
159
- }
160
- result.push(token);
161
- }
162
- while (ti < sortedTextNodes.length) {
163
- result.push(sortedTextNodes[ti++]);
164
- }
165
- return result;
166
- }
167
-
168
87
  /**
169
88
  * Parse and transform a Glimmer template into an ESTree-compatible AST.
170
89
  * Internal — consumed by toTree.
@@ -173,7 +92,8 @@ function buildTokenStream(rawTokens, comments, textNodes) {
173
92
  * positions, create parts/blockParamNodes, nullify empty hashes, and
174
93
  * prefix types. No separate collect-then-transform loop.
175
94
  */
176
- export function processTemplate(templateContent, codeLines, templateRange) {
95
+ export function processTemplate(templateContent, codeLines, options = {}) {
96
+ const { templateRange, tokens: generateTokens = false } = options;
177
97
  const offset = templateRange[0];
178
98
  const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
179
99
 
@@ -335,7 +255,15 @@ export function processTemplate(templateContent, codeLines, templateRange) {
335
255
 
336
256
  removeFromParent(emptyTextNodes);
337
257
 
338
- ast.tokens = buildTokenStream(tokenize(templateContent, codeLines, offset), comments, textNodes);
258
+ if (generateTokens) {
259
+ ast.tokens = buildTokenStream(
260
+ tokenize(templateContent, codeLines, offset),
261
+ comments,
262
+ textNodes,
263
+ templateContent,
264
+ offset,
265
+ );
266
+ }
339
267
  ast.contents = templateContent;
340
268
 
341
269
  return { ast, comments };