ember-estree 0.0.0 → 0.1.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 +70 -0
- package/package.json +38 -12
- package/src/index.js +3 -15
- package/src/parse.js +111 -0
- package/src/print.js +913 -0
- package/src/transforms.js +230 -0
- package/examples/jscodeshift/node_modules/.bin/jscodeshift +0 -21
- package/examples/jscodeshift/node_modules/.bin/vitest +0 -21
- package/examples/jscodeshift/package.json +0 -18
- package/pnpm-workspace.yaml +0 -3
package/README.md
CHANGED
|
@@ -1 +1,71 @@
|
|
|
1
1
|
# ember-estree
|
|
2
|
+
|
|
3
|
+
ESTree-compatible AST parser for Ember's `.gjs` and `.gts` files.
|
|
4
|
+
|
|
5
|
+
Parses `<template>` tags into [Glimmer](https://github.com/emberjs/ember.js/) AST nodes that are embedded directly in the ESTree, so tools like linters and codemods can work with both the JavaScript/TypeScript _and_ template portions of a single file.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add ember-estree
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Parsing
|
|
16
|
+
|
|
17
|
+
`toTree` returns a `File` node whose `.program` is a standard ESTree `Program`, with any `<template>` regions represented as `Glimmer*` AST nodes.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { toTree } from "ember-estree";
|
|
21
|
+
|
|
22
|
+
let ast = toTree(`
|
|
23
|
+
import Component from "@glimmer/component";
|
|
24
|
+
|
|
25
|
+
export default class Demo extends Component {
|
|
26
|
+
<template>Hello, {{this.name}}!</template>
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
console.log(ast.type); // "File"
|
|
31
|
+
console.log(ast.program.body.length); // 2 — ImportDeclaration + ClassDeclaration
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`parse` is a lower-level alternative that returns the `Program` node directly.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { parse } from "ember-estree";
|
|
38
|
+
|
|
39
|
+
let program = parse(`const x = <template>hi</template>;`);
|
|
40
|
+
console.log(program.type); // "Program"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Printing
|
|
44
|
+
|
|
45
|
+
`print` converts an AST node (ESTree _or_ Glimmer) back to source code.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { print } from "ember-estree";
|
|
49
|
+
|
|
50
|
+
print({ type: "Identifier", name: "foo" });
|
|
51
|
+
// => "foo"
|
|
52
|
+
|
|
53
|
+
print({
|
|
54
|
+
type: "GlimmerTemplate",
|
|
55
|
+
body: [{ type: "GlimmerTextNode", chars: "Hello" }],
|
|
56
|
+
});
|
|
57
|
+
// => "<template>Hello</template>"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
The [`examples/`](./examples) directory contains ready-to-run integrations:
|
|
63
|
+
|
|
64
|
+
| Example | Description |
|
|
65
|
+
| ------------------------------------------- | -------------------------------------------------------------------- |
|
|
66
|
+
| [`eslint-parser`](./examples/eslint-parser) | Custom ESLint parser that understands `<template>` |
|
|
67
|
+
| [`zmod`](./examples/zmod) | Codemod toolkit using [zmod](https://github.com/nicolo-ribaudo/zmod) |
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-estree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "ESTree generator for gjs and gts file used by ember",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"AST",
|
|
7
|
+
"codemod",
|
|
8
|
+
"ember",
|
|
9
|
+
"estree",
|
|
10
|
+
"glimmer",
|
|
11
|
+
"traversal",
|
|
12
|
+
"walker"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "NullVoxPopuli",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git@github.com:NullVoxPopuli/ember-estree.git"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
5
23
|
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
24
|
+
"main": "src/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./src/index.js"
|
|
27
|
+
},
|
|
10
28
|
"dependencies": {
|
|
11
|
-
"@babel/core": "^7.28.4",
|
|
12
|
-
"@babel/parser": "^7.28.4",
|
|
13
|
-
"@babel/plugin-transform-typescript": "^7.28.0",
|
|
14
29
|
"@glimmer/syntax": "^0.95.0",
|
|
15
|
-
"content-tag": "^
|
|
16
|
-
"
|
|
17
|
-
"
|
|
30
|
+
"content-tag-utils": "^0.5.1",
|
|
31
|
+
"ember-template-recast": "^6.1.5",
|
|
32
|
+
"oxc-parser": "^0.119.0",
|
|
33
|
+
"zimmerframe": "^1.1.4"
|
|
18
34
|
},
|
|
19
35
|
"devDependencies": {
|
|
36
|
+
"@tsconfig/node-lts": "^22.0.2",
|
|
37
|
+
"oxfmt": "^0.40.0",
|
|
38
|
+
"oxlint": "^1.55.0",
|
|
39
|
+
"publint": "^0.3.18",
|
|
40
|
+
"release-plan": "^0.17.4",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
20
42
|
"vitest": "^3.2.4"
|
|
21
43
|
},
|
|
22
44
|
"scripts": {
|
|
23
|
-
"
|
|
45
|
+
"format": "oxfmt",
|
|
46
|
+
"format:check": "oxfmt --check",
|
|
47
|
+
"lint": "oxlint && pnpm format:check && publint",
|
|
48
|
+
"lint:fix": "oxlint --fix && oxfmt",
|
|
49
|
+
"test": "vitest run"
|
|
24
50
|
}
|
|
25
51
|
}
|
package/src/index.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 1. parse out the <template>...</template> regions
|
|
5
|
-
* - we haven't shipped "content-tag" through TC39, so for now, gjs and gts are invalid JavaScript
|
|
6
|
-
* 2. create a new string/contents of the file with a placeholder for the template regisions
|
|
7
|
-
* - this will be used later to splice in the Template AST Nodes
|
|
8
|
-
* - the placeholder should be the same dimensions as the template region
|
|
9
|
-
* 3. parse the string/contents as js/ts to generate an ESTree
|
|
10
|
-
* 4. parse each template region to generate an AST from that
|
|
11
|
-
* 5. convert the AST from `@glimmer/syntax` to ESTree
|
|
12
|
-
* - NOTE: it may already be ESTree
|
|
13
|
-
* 6. splice in the template ESTrees into the JS/TS ESTree
|
|
14
|
-
* 7. Done
|
|
15
|
-
*/
|
|
1
|
+
export { toTree, parse } from "./parse.js";
|
|
2
|
+
export { print } from "./print.js";
|
|
3
|
+
export { buildGlimmerVisitorKeys, DocumentLines } from "./transforms.js";
|
package/src/parse.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Strategy:
|
|
3
|
+
*
|
|
4
|
+
* 1. parse out the <template>...</template> regions
|
|
5
|
+
* - we haven't shipped "content-tag" through TC39, so for now, gjs and gts are invalid JavaScript
|
|
6
|
+
*
|
|
7
|
+
* 2. create a new string/contents of the file with a placeholder for the template regisions
|
|
8
|
+
* - this will be used later to splice in the Template AST Nodes
|
|
9
|
+
* - the placeholder should be the same dimensions as the template region
|
|
10
|
+
*
|
|
11
|
+
* 3. parse the string/contents as js/ts to generate an ESTree
|
|
12
|
+
*
|
|
13
|
+
* 4. parse each template region to generate an AST from that
|
|
14
|
+
*
|
|
15
|
+
* 5. convert the AST from `@glimmer/syntax` to ESTree
|
|
16
|
+
* - NOTE: it may already be ESTree
|
|
17
|
+
*
|
|
18
|
+
* 6. splice in the template ESTrees into the JS/TS ESTree
|
|
19
|
+
*
|
|
20
|
+
* 7. Done
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Docs for dependencies:
|
|
25
|
+
* - https://github.com/embroider-build/content-tag/
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { parseSync } from "oxc-parser";
|
|
29
|
+
import templateRecast from "ember-template-recast";
|
|
30
|
+
import { Transformer } from "content-tag-utils";
|
|
31
|
+
import { walk } from "zimmerframe";
|
|
32
|
+
|
|
33
|
+
import { processGlimmerTemplate } from "./transforms.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} source
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @return {object} A File-like AST with a `.program` property
|
|
39
|
+
*/
|
|
40
|
+
export function toTree(source, options = {}) {
|
|
41
|
+
let t = new Transformer(source);
|
|
42
|
+
let js = t.toString({ placeholders: true });
|
|
43
|
+
|
|
44
|
+
let filename = options.filePath || "input.ts";
|
|
45
|
+
let oxcResult = parseSync(filename, js);
|
|
46
|
+
|
|
47
|
+
// Wrap in a File-like node to match the expected structure
|
|
48
|
+
let outerAST = {
|
|
49
|
+
type: "File",
|
|
50
|
+
program: oxcResult.program,
|
|
51
|
+
comments: oxcResult.comments || [],
|
|
52
|
+
start: oxcResult.program.start,
|
|
53
|
+
end: oxcResult.program.end,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let parseResults = t.parseResults;
|
|
57
|
+
|
|
58
|
+
outerAST = walk(outerAST, null, {
|
|
59
|
+
_(node, { next }) {
|
|
60
|
+
if (isExpressionPlaceholder(node)) {
|
|
61
|
+
let parseResult = parseResults.find((r) => {
|
|
62
|
+
// WARNING: these are byte ranges
|
|
63
|
+
return node.start === r.range.start && node.end === r.range.end;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let content = t.stringUtils.originalContentOf(parseResult);
|
|
67
|
+
let templateAST = templateRecast.parse(content);
|
|
68
|
+
|
|
69
|
+
let contentOffset = parseResult.contentRange.start;
|
|
70
|
+
let templateRange = [parseResult.range.start, parseResult.range.end];
|
|
71
|
+
|
|
72
|
+
return processGlimmerTemplate(templateAST, {
|
|
73
|
+
contentOffset,
|
|
74
|
+
templateRange,
|
|
75
|
+
source,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
next();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let ast = outerAST;
|
|
83
|
+
|
|
84
|
+
return ast;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse Ember .gjs/.gts source code into an ESTree-compatible AST
|
|
89
|
+
* with embedded Glimmer template nodes.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} source - The source code to parse
|
|
92
|
+
* @param {object} [options] - Parse options
|
|
93
|
+
* @return {object} The ESTree-compatible AST
|
|
94
|
+
*/
|
|
95
|
+
export function parse(source, options = {}) {
|
|
96
|
+
let ast = toTree(source, options);
|
|
97
|
+
|
|
98
|
+
return ast;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//////////////////////////////////////////////////
|
|
102
|
+
//
|
|
103
|
+
// Helpers
|
|
104
|
+
//
|
|
105
|
+
//////////////////////////////////////////////////
|
|
106
|
+
|
|
107
|
+
function isExpressionPlaceholder(node) {
|
|
108
|
+
if (node.type !== "CallExpression") return;
|
|
109
|
+
|
|
110
|
+
return node.callee.name === "TEMPLATE_TEMPLATE";
|
|
111
|
+
}
|