ember-estree 0.1.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-estree",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "ESTree generator for gjs and gts file used by ember",
5
5
  "keywords": [
6
6
  "AST",
@@ -22,12 +22,16 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "main": "src/index.js",
25
+ "types": "src/index.d.ts",
25
26
  "exports": {
26
- ".": "./src/index.js"
27
+ ".": {
28
+ "types": "./src/index.d.ts",
29
+ "default": "./src/index.js"
30
+ }
27
31
  },
28
32
  "dependencies": {
29
33
  "@glimmer/syntax": "^0.95.0",
30
- "content-tag-utils": "^0.5.1",
34
+ "content-tag": "^4.1.0",
31
35
  "ember-template-recast": "^6.1.5",
32
36
  "oxc-parser": "^0.119.0",
33
37
  "zimmerframe": "^1.1.4"
package/src/index.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Options accepted by `parse` and `toTree`.
3
+ */
4
+ export interface ParseOptions {
5
+ /** Path to the file being parsed, used to determine the language (js/ts). */
6
+ filePath?: string;
7
+ }
8
+
9
+ /**
10
+ * A 1-based line / 0-based column position, matching ESTree and Glimmer
11
+ * conventions.
12
+ */
13
+ export interface Position {
14
+ line: number;
15
+ column: number;
16
+ }
17
+
18
+ /**
19
+ * Minimal shape shared by every AST node (ESTree, TypeScript, and Glimmer).
20
+ */
21
+ export interface ASTNode {
22
+ type: string;
23
+ start?: number;
24
+ end?: number;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ /**
29
+ * The `File`-like wrapper returned by `toTree` and `parse`.
30
+ *
31
+ * Mirrors the shape produced internally:
32
+ * ```
33
+ * { type: "File", program: Program, comments: Comment[], start, end }
34
+ * ```
35
+ */
36
+ export interface FileNode extends ASTNode {
37
+ type: "File";
38
+ program: ASTNode;
39
+ comments: ASTNode[];
40
+ }
41
+
42
+ /**
43
+ * Converts between character offsets and line/column positions within a
44
+ * source string.
45
+ */
46
+ export class DocumentLines {
47
+ constructor(source: string);
48
+
49
+ /** Converts a `{ line, column }` position to a character offset. */
50
+ positionToOffset(pos: Position): number;
51
+
52
+ /** Converts a character offset to a `{ line, column }` position. */
53
+ offsetToPosition(offset: number): Position;
54
+ }
55
+
56
+ /**
57
+ * Parse Ember .gjs/.gts source code and return a File-like ESTree-compatible
58
+ * AST with embedded Glimmer template nodes.
59
+ *
60
+ * @param source The raw source code of the file.
61
+ * @param options Optional parse options.
62
+ * @returns A `File`-shaped object with a `.program` property.
63
+ */
64
+ export function toTree(source: string, options?: ParseOptions): FileNode;
65
+
66
+ /**
67
+ * Parse Ember .gjs/.gts source code into an ESTree-compatible AST with
68
+ * embedded Glimmer template nodes.
69
+ *
70
+ * @param source The source code to parse.
71
+ * @param options Optional parse options.
72
+ * @returns The ESTree-compatible AST.
73
+ */
74
+ export function parse(source: string, options?: ParseOptions): FileNode;
75
+
76
+ /**
77
+ * Recursively print an AST node back to source code.
78
+ *
79
+ * Handles ESTree, TypeScript, and Glimmer template node types.
80
+ * JSX nodes are not supported — Ember uses Glimmer templates instead.
81
+ *
82
+ * @param node The AST node to print.
83
+ * @returns The printed source string.
84
+ */
85
+ export function print(node: ASTNode): string;
86
+
87
+ /**
88
+ * Build and return the Glimmer visitor keys map with a `"Glimmer"` prefix on
89
+ * every key (e.g. `"GlimmerElementNode"`).
90
+ *
91
+ * The result is cached after the first call.
92
+ *
93
+ * @returns A map of Glimmer node type names to arrays of child-property names.
94
+ */
95
+ export function buildGlimmerVisitorKeys(): Record<string, string[]>;
96
+
97
+ /**
98
+ * Recursively remove all `parent` references from an AST.
99
+ * Useful when you need to serialize the tree to JSON,
100
+ * since parent back-references create circular structures.
101
+ *
102
+ * Mutates the tree in place and returns it.
103
+ */
104
+ export function removeParentReferences(ast: ASTNode): ASTNode;
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { toTree, parse } from "./parse.js";
2
2
  export { print } from "./print.js";
3
3
  export { buildGlimmerVisitorKeys, DocumentLines } from "./transforms.js";
4
+ export { removeParentReferences } from "./utils.js";
package/src/parse.js CHANGED
@@ -27,19 +27,21 @@
27
27
 
28
28
  import { parseSync } from "oxc-parser";
29
29
  import templateRecast from "ember-template-recast";
30
- import { Transformer } from "content-tag-utils";
30
+ import { Preprocessor } from "content-tag";
31
31
  import { walk } from "zimmerframe";
32
32
 
33
33
  import { processGlimmerTemplate } from "./transforms.js";
34
34
 
35
+ const preprocessor = new Preprocessor();
36
+
35
37
  /**
36
38
  * @param {string} source
37
39
  * @param {object} options
38
40
  * @return {object} A File-like AST with a `.program` property
39
41
  */
40
42
  export function toTree(source, options = {}) {
41
- let t = new Transformer(source);
42
- let js = t.toString({ placeholders: true });
43
+ let parseResults = preprocessor.parse(source);
44
+ let js = toPlaceholderJS(source, parseResults);
43
45
 
44
46
  let filename = options.filePath || "input.ts";
45
47
  let oxcResult = parseSync(filename, js);
@@ -53,21 +55,26 @@ export function toTree(source, options = {}) {
53
55
  end: oxcResult.program.end,
54
56
  };
55
57
 
56
- let parseResults = t.parseResults;
57
-
58
+ // content-tag v4 provides UTF-16 codepoint offsets that match
59
+ // JavaScript string indices and oxc-parser character offsets directly,
60
+ // so no byte-to-character conversion is needed.
58
61
  outerAST = walk(outerAST, null, {
59
62
  _(node, { next }) {
60
63
  if (isExpressionPlaceholder(node) || isClassMemberPlaceholder(node)) {
61
64
  let parseResult = parseResults.find((r) => {
62
- // WARNING: these are byte ranges
63
- return node.start === r.range.start && node.end === r.range.end;
65
+ return (
66
+ node.start === r.range.startUtf16Codepoint && node.end === r.range.endUtf16Codepoint
67
+ );
64
68
  });
65
69
 
66
- let content = t.stringUtils.originalContentOf(parseResult);
70
+ let content = parseResult.contents;
67
71
  let templateAST = templateRecast.parse(content);
68
72
 
69
- let contentOffset = parseResult.contentRange.start;
70
- let templateRange = [parseResult.range.start, parseResult.range.end];
73
+ let contentOffset = parseResult.contentRange.startUtf16Codepoint;
74
+ let templateRange = [
75
+ parseResult.range.startUtf16Codepoint,
76
+ parseResult.range.endUtf16Codepoint,
77
+ ];
71
78
 
72
79
  return processGlimmerTemplate(templateAST, {
73
80
  contentOffset,
@@ -117,3 +124,53 @@ function isClassMemberPlaceholder(node) {
117
124
  node.computed && node.key?.type === "CallExpression" && node.key.callee?.name === "_TEMPLATE_"
118
125
  );
119
126
  }
127
+
128
+ /**
129
+ * Replaces <template>...</template> regions in source with
130
+ * placeholder expressions of the same character length that
131
+ * are valid JavaScript, so oxc-parser can parse them.
132
+ *
133
+ * Expression templates become: TEMPLATE_TEMPLATE(`...`)
134
+ * Class member templates become: [_TEMPLATE_(`...`)] = 0;
135
+ *
136
+ * Both placeholder forms use exactly 21 characters for the
137
+ * opening + closing wrappers, matching the original
138
+ * <template> (10) + </template> (11) = 21 character overhead.
139
+ *
140
+ * @param {string} source
141
+ * @param {Array<object>} parseResults
142
+ * @returns {string}
143
+ */
144
+ function toPlaceholderJS(source, parseResults) {
145
+ let result = source;
146
+ let offset = 0;
147
+
148
+ for (let pr of parseResults) {
149
+ let start = pr.range.startUtf16Codepoint;
150
+ let end = pr.range.endUtf16Codepoint;
151
+
152
+ let openingTag, closingTag;
153
+ switch (pr.type) {
154
+ case "expression":
155
+ openingTag = "TEMPLATE_TEMPLATE(`";
156
+ closingTag = "`)";
157
+ break;
158
+ case "class-member":
159
+ openingTag = "[_TEMPLATE_(`";
160
+ closingTag = "`)] = 0;";
161
+ break;
162
+ }
163
+
164
+ let content = source.slice(
165
+ pr.contentRange.startUtf16Codepoint,
166
+ pr.contentRange.endUtf16Codepoint,
167
+ );
168
+
169
+ let replacement = openingTag + content + closingTag;
170
+
171
+ result = result.slice(0, start + offset) + replacement + result.slice(end + offset);
172
+ offset += replacement.length - (end - start);
173
+ }
174
+
175
+ return result;
176
+ }
package/src/transforms.js CHANGED
@@ -221,10 +221,5 @@ export function processGlimmerTemplate(templateAST, { contentOffset, templateRan
221
221
  comment.type = "Block";
222
222
  }
223
223
 
224
- // Clear parent references (they cause circular JSON issues)
225
- for (const n of allNodes) {
226
- n.parent = null;
227
- }
228
-
229
224
  return templateAST;
230
225
  }
package/src/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Optional utility functions for working with ESTree ASTs
3
+ * produced by ember-estree.
4
+ */
5
+
6
+ import { walk } from "zimmerframe";
7
+
8
+ /**
9
+ * Recursively remove all `parent` references from an AST.
10
+ * Useful when you need to serialize the tree to JSON (e.g. for zmod),
11
+ * since parent back-references create circular structures.
12
+ *
13
+ * Mutates the tree in place and returns it.
14
+ */
15
+ export function removeParentReferences(ast) {
16
+ return walk(ast, null, {
17
+ _(node, { next }) {
18
+ delete node.parent;
19
+ next();
20
+ },
21
+ });
22
+ }