yuku-ast 0.0.3

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 ADDED
@@ -0,0 +1,247 @@
1
+ # yuku-ast
2
+
3
+ A fast, fully typed AST toolkit for [`yuku-parser`](https://www.npmjs.com/package/yuku-parser):
4
+ node builders, type guards, a walker, and identifier validators. No runtime dependencies.
5
+
6
+ | Import | Provides |
7
+ | --------------------- | ------------------------------------------------------------ |
8
+ | `yuku-ast` | `walk`, `walkAsync`, `b` (node builders), `is` (type guards) |
9
+ | `yuku-ast/identifier` | identifier and reserved-word validators |
10
+
11
+ ```ts
12
+ import { parse } from "yuku-parser";
13
+ import { walk } from "yuku-ast";
14
+
15
+ const { program } = parse("const greet = (name) => `hi ${name}`;");
16
+
17
+ walk(program, {
18
+ Identifier(node) {
19
+ console.log(node.name); // greet, name, name
20
+ },
21
+ });
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ bun add yuku-ast yuku-parser
28
+ ```
29
+
30
+ `yuku-parser` is a peer dependency.
31
+
32
+ ## Visitors
33
+
34
+ A visitor is keyed by node `type`, by an alias group, or by the universal
35
+ `enter` / `leave`. Each handler receives the typed node and a [`path`](#the-path).
36
+
37
+ ```ts
38
+ walk(program, {
39
+ // a concrete node type, `node` is precisely typed
40
+ CallExpression(node, path) {
41
+ if (path.parent?.type === "AwaitExpression") return;
42
+ },
43
+
44
+ // an alias group fires for a whole family
45
+ Function(node) {
46
+ node.params; // FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | ...
47
+ },
48
+
49
+ // enter/leave for any node type
50
+ MemberExpression: {
51
+ enter(node) {},
52
+ leave(node) {},
53
+ },
54
+
55
+ // run for every node
56
+ enter(node) {},
57
+ leave(node) {},
58
+ });
59
+ ```
60
+
61
+ Aliases: `Expression`, `Statement`, `Declaration`, `ModuleDeclaration`,
62
+ `Function`, `Class`, `Method`, `Loop`, `Pattern`, `JSX`, `TSType`.
63
+
64
+ When several handlers match a node, `enter` runs outermost first
65
+ (universal, alias, concrete) and `leave` runs in reverse.
66
+
67
+ ## The path
68
+
69
+ Every visitor receives a `path` describing where it is and how to change the tree.
70
+ Each node has its own path, so it stays valid after the visit returns.
71
+
72
+ ```ts
73
+ node: T // the current node
74
+ parent: Node | null // owner, or null at the root
75
+ key: string | null // field on the parent holding this node
76
+ index: number | null // position within an array field, else null
77
+ ancestors: readonly Node[] // root down to the parent
78
+ state: S // value threaded through the walk
79
+
80
+ skip() // do not descend into this node
81
+ stop() // end the walk
82
+ replace(next) // swap this node, then walk the replacement
83
+ remove() // detach from the parent
84
+ insertBefore(node) // add a sibling before (array fields)
85
+ insertAfter(node) // add a sibling after (array fields)
86
+ ```
87
+
88
+ ```ts
89
+ walk(program, {
90
+ DebuggerStatement(_node, path) {
91
+ path.remove();
92
+ },
93
+ Identifier(node, path) {
94
+ if (node.name === "foo") path.replace(b.identifier("bar"));
95
+ },
96
+ });
97
+ ```
98
+
99
+ `replace` walks into the replacement's children but does not re-run its own
100
+ `enter`. A synthetic replacement (one built with `b.*`) inherits the replaced
101
+ node's source span, so generated output still maps back to the original.
102
+
103
+ ## Type guards (`is`)
104
+
105
+ A guard for every node `type`, the alias families, and the variants that share a
106
+ `type` string. All accept `null` / `undefined` and return `false`.
107
+
108
+ ```ts
109
+ import { is } from "yuku-ast";
110
+
111
+ is.CallExpression(node); // every concrete type
112
+ is.Expression(node); // families
113
+ is.StringLiteral(node); // Literal variants
114
+ is.StaticMemberExpression(node); // MemberExpression variants
115
+ is.BindingIdentifier(node); // Identifier roles, via `kind`
116
+
117
+ walk(program, {
118
+ Literal(node, path) {
119
+ if (is.ArrayExpression(path.parent)) {
120
+ // path.parent is narrowed to ArrayExpression
121
+ }
122
+ },
123
+ });
124
+ ```
125
+
126
+ ## Builders (`b`)
127
+
128
+ A builder for every node type. Required fields are positional, the rest go in a
129
+ trailing options object. Synthetic nodes get a zero span.
130
+
131
+ ```ts
132
+ import { b } from "yuku-ast";
133
+
134
+ b.identifier("x");
135
+ b.callExpression(b.identifier("f"), [b.numericLiteral(1)]);
136
+ b.arrowFunctionExpression([b.identifier("a", "binding")], b.identifier("a"));
137
+ b.variableDeclaration("const", [
138
+ b.variableDeclarator(b.identifier("x", "binding"), b.numericLiteral(0)),
139
+ ]);
140
+ ```
141
+
142
+ ## Transform and print
143
+
144
+ Pair it with [`yuku-codegen`](https://www.npmjs.com/package/yuku-codegen) to parse,
145
+ rewrite, and print back to source.
146
+
147
+ ```ts
148
+ import { parse } from "yuku-parser";
149
+ import { print } from "yuku-codegen";
150
+ import { is, b, walk } from "yuku-ast";
151
+
152
+ const { program } = parse("const x = 1; debugger; foo(x, 2);");
153
+
154
+ walk(program, {
155
+ DebuggerStatement(_node, path) {
156
+ path.remove();
157
+ },
158
+ Identifier(node, path) {
159
+ if (node.name === "foo") path.replace(b.identifier("bar"));
160
+ },
161
+ Literal(node, path) {
162
+ if (is.NumericLiteral(node)) path.replace(b.numericLiteral((node.value ?? 0) * 10));
163
+ },
164
+ });
165
+
166
+ console.log(print(program).code);
167
+ // const x = 10;
168
+ // bar(x, 20);
169
+ ```
170
+
171
+ `walk` edits the tree in place, so a transform is just a reusable visitor object,
172
+ and passes compose: run as many as you like, then print once.
173
+
174
+ ```ts
175
+ import { b, walk, type Visitors } from "yuku-ast";
176
+
177
+ const stripDebugger: Visitors = {
178
+ DebuggerStatement: (_node, path) => path.remove(),
179
+ };
180
+ const inlineDevFlag: Visitors = {
181
+ Identifier(node, path) {
182
+ if (node.name === "__DEV__") path.replace(b.booleanLiteral(false));
183
+ },
184
+ };
185
+
186
+ const { program } = parse("if (__DEV__) log(); debugger; run();");
187
+ for (const transform of [stripDebugger, inlineDevFlag]) {
188
+ walk(program, transform);
189
+ }
190
+ console.log(print(program).code);
191
+ // if (false) log();
192
+ // run();
193
+ ```
194
+
195
+ Replacements built with `b.*` inherit the replaced node's source span, so
196
+ `yuku-codegen` source maps still point back to the original input.
197
+
198
+ ## Async
199
+
200
+ `walkAsync` is the same walk with awaitable visitors, run sequentially in
201
+ depth-first order. Reach for it only when a visitor must await I/O; `walk` is
202
+ faster otherwise.
203
+
204
+ ```ts
205
+ import { walkAsync } from "yuku-ast";
206
+
207
+ await walkAsync(program, {
208
+ async ImportDeclaration(node, path) {
209
+ if (!(await exists(node.source.value))) path.remove();
210
+ },
211
+ });
212
+ ```
213
+
214
+ ## Identifiers
215
+
216
+ Identifier and reserved-word helpers for raw strings and code points.
217
+
218
+ ```ts
219
+ import { isIdentifierName, isValidIdentifier } from "yuku-ast/identifier";
220
+
221
+ isIdentifierName("π"); // true - a well-formed IdentifierName
222
+ isIdentifierName("class"); // true - purely syntactic, keywords included
223
+ isValidIdentifier("class"); // false - rejects reserved words (use for bindings)
224
+ isValidIdentifier("π"); // true
225
+ ```
226
+
227
+ | Function | Checks |
228
+ | ------------------------------------------------ | ------------------------------------------------------------------------- |
229
+ | `isIdentifierName(name)` | `name` is a syntactically valid `IdentifierName` |
230
+ | `isValidIdentifier(name, reserved?)` | a valid binding name; rejects reserved words unless `reserved` is `false` |
231
+ | `isIdentifierStart(cp)` / `isIdentifierChar(cp)` | a code point may start / continue an identifier |
232
+ | `isKeyword(word)` | `word` is a core grammar keyword (`if`, `class`, …) |
233
+ | `isReservedWord(word, inModule?)` | reserved everywhere (`enum`; `await` in modules) |
234
+ | `isStrictReservedWord(word, inModule?)` | also reserved in strict mode (`let`, `yield`, …) |
235
+ | `isStrictBindOnlyReservedWord(word)` | `eval` / `arguments` |
236
+ | `isStrictBindReservedWord(word, inModule?)` | strict reserved, plus `eval` / `arguments` |
237
+
238
+ ## Performance
239
+
240
+ The walk reads a fixed table of child fields per node type rather than reflecting
241
+ over keys, and dispatches through a cached, per-type handler list. On a large
242
+ TypeScript file it walks several times faster than `@babel/traverse` and on par
243
+ with the lightest reflection walkers, while staying fully typed.
244
+
245
+ ## License
246
+
247
+ MIT
@@ -0,0 +1,82 @@
1
+ //#region src/identifier/index.d.ts
2
+ /**
3
+ * Returns `true` if the Unicode code point `cp` can **start** an identifier:
4
+ * any character with the Unicode `ID_Start` property, plus `$` and `_`.
5
+ *
6
+ * Takes a numeric code point (as from {@link String.prototype.codePointAt}),
7
+ * mirroring Babel's `isIdentifierStart`. Returns `false` for values that are
8
+ * not a valid code point.
9
+ */
10
+ declare function isIdentifierStart(cp: number): boolean;
11
+ /**
12
+ * Returns `true` if the Unicode code point `cp` can appear **after** the first
13
+ * character of an identifier: any character with the Unicode `ID_Continue`
14
+ * property, plus `$`, `_`, and the ZWNJ (U+200C) / ZWJ (U+200D) joiners.
15
+ *
16
+ * Takes a numeric code point, mirroring Babel's `isIdentifierChar`. Returns
17
+ * `false` for values that are not a valid code point.
18
+ */
19
+ declare function isIdentifierChar(cp: number): boolean;
20
+ /**
21
+ * Returns `true` if `name` is a valid ECMAScript `IdentifierName`: the grammar
22
+ * an identifier token must satisfy.
23
+ *
24
+ * Unlike the `is.IdentifierName` node guard, this validates a raw string rather
25
+ * than an AST node.
26
+ *
27
+ * @example
28
+ * isIdentifierName("foo"); // true
29
+ * isIdentifierName("$_0"); // true
30
+ * isIdentifierName("π"); // true
31
+ * isIdentifierName("class"); // true - syntactically an IdentifierName
32
+ * isIdentifierName("1foo"); // false - starts with a digit
33
+ * isIdentifierName("foo-bar"); // false - `-` is not an identifier character
34
+ * isIdentifierName(""); // false
35
+ */
36
+ declare function isIdentifierName(name: string): boolean;
37
+ /**
38
+ * Returns `true` if `word` is a reserved keyword of the core grammar (`if`,
39
+ * `class`, `typeof`, `return`, …). Does not include `enum`, `await`, or the
40
+ * strict-mode-only words; see {@link isReservedWord} and
41
+ * {@link isStrictReservedWord} for those.
42
+ */
43
+ declare function isKeyword(word: string): boolean;
44
+ /**
45
+ * Returns `true` if `word` is unconditionally reserved: `enum` in any context,
46
+ * and `await` when `inModule` (module / async contexts).
47
+ */
48
+ declare function isReservedWord(word: string, inModule?: boolean): boolean;
49
+ /**
50
+ * Returns `true` if `word` is reserved in strict mode: everything
51
+ * {@link isReservedWord} covers, plus `let`, `static`, `yield`, `implements`,
52
+ * and friends.
53
+ */
54
+ declare function isStrictReservedWord(word: string, inModule?: boolean): boolean;
55
+ /**
56
+ * Returns `true` for `eval` and `arguments`, which are reserved only as
57
+ * assignment / binding targets in strict mode.
58
+ */
59
+ declare function isStrictBindOnlyReservedWord(word: string): boolean;
60
+ /**
61
+ * Returns `true` if `word` is reserved as a strict-mode binding target:
62
+ * everything {@link isStrictReservedWord} covers, plus `eval` / `arguments`.
63
+ */
64
+ declare function isStrictBindReservedWord(word: string, inModule?: boolean): boolean;
65
+ /**
66
+ * Returns `true` if `name` can be used as an identifier **binding**: a valid
67
+ * {@link isIdentifierName} that, when `reserved` is `true` (the default), is
68
+ * neither a keyword nor a strict-mode reserved word.
69
+ *
70
+ * This is the check to reach for when turning an arbitrary string (e.g. a
71
+ * module specifier) into a local binding name.
72
+ *
73
+ * @example
74
+ * isValidIdentifier("foo"); // true
75
+ * isValidIdentifier("class"); // false - reserved word
76
+ * isValidIdentifier("await"); // false - reserved in modules
77
+ * isValidIdentifier("eval"); // true - valid identifier (strict-bind only)
78
+ * isValidIdentifier("class", false); // true - reserved words allowed
79
+ */
80
+ declare function isValidIdentifier(name: string, reserved?: boolean): boolean;
81
+ //#endregion
82
+ export { isIdentifierChar, isIdentifierName, isIdentifierStart, isKeyword, isReservedWord, isStrictBindOnlyReservedWord, isStrictBindReservedWord, isStrictReservedWord, isValidIdentifier };
@@ -0,0 +1,156 @@
1
+ //#region src/identifier/index.ts
2
+ const ID_START = /^[$_\p{ID_Start}]$/u;
3
+ const ID_CONTINUE = /^[$\u200C\u200D\p{ID_Continue}]$/u;
4
+ const IDENTIFIER_NAME = /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
5
+ const MAX_CODE_POINT = 1114111;
6
+ /**
7
+ * Returns `true` if the Unicode code point `cp` can **start** an identifier:
8
+ * any character with the Unicode `ID_Start` property, plus `$` and `_`.
9
+ *
10
+ * Takes a numeric code point (as from {@link String.prototype.codePointAt}),
11
+ * mirroring Babel's `isIdentifierStart`. Returns `false` for values that are
12
+ * not a valid code point.
13
+ */
14
+ function isIdentifierStart(cp) {
15
+ if (!Number.isInteger(cp) || cp < 0 || cp > MAX_CODE_POINT) return false;
16
+ return ID_START.test(String.fromCodePoint(cp));
17
+ }
18
+ /**
19
+ * Returns `true` if the Unicode code point `cp` can appear **after** the first
20
+ * character of an identifier: any character with the Unicode `ID_Continue`
21
+ * property, plus `$`, `_`, and the ZWNJ (U+200C) / ZWJ (U+200D) joiners.
22
+ *
23
+ * Takes a numeric code point, mirroring Babel's `isIdentifierChar`. Returns
24
+ * `false` for values that are not a valid code point.
25
+ */
26
+ function isIdentifierChar(cp) {
27
+ if (!Number.isInteger(cp) || cp < 0 || cp > MAX_CODE_POINT) return false;
28
+ return ID_CONTINUE.test(String.fromCodePoint(cp));
29
+ }
30
+ /**
31
+ * Returns `true` if `name` is a valid ECMAScript `IdentifierName`: the grammar
32
+ * an identifier token must satisfy.
33
+ *
34
+ * Unlike the `is.IdentifierName` node guard, this validates a raw string rather
35
+ * than an AST node.
36
+ *
37
+ * @example
38
+ * isIdentifierName("foo"); // true
39
+ * isIdentifierName("$_0"); // true
40
+ * isIdentifierName("π"); // true
41
+ * isIdentifierName("class"); // true - syntactically an IdentifierName
42
+ * isIdentifierName("1foo"); // false - starts with a digit
43
+ * isIdentifierName("foo-bar"); // false - `-` is not an identifier character
44
+ * isIdentifierName(""); // false
45
+ */
46
+ function isIdentifierName(name) {
47
+ return IDENTIFIER_NAME.test(name);
48
+ }
49
+ const keywords = new Set([
50
+ "break",
51
+ "case",
52
+ "catch",
53
+ "continue",
54
+ "debugger",
55
+ "default",
56
+ "do",
57
+ "else",
58
+ "finally",
59
+ "for",
60
+ "function",
61
+ "if",
62
+ "return",
63
+ "switch",
64
+ "throw",
65
+ "try",
66
+ "var",
67
+ "const",
68
+ "while",
69
+ "with",
70
+ "new",
71
+ "this",
72
+ "super",
73
+ "class",
74
+ "extends",
75
+ "export",
76
+ "import",
77
+ "null",
78
+ "true",
79
+ "false",
80
+ "in",
81
+ "instanceof",
82
+ "typeof",
83
+ "void",
84
+ "delete"
85
+ ]);
86
+ const strictReservedWords = new Set([
87
+ "implements",
88
+ "interface",
89
+ "let",
90
+ "package",
91
+ "private",
92
+ "protected",
93
+ "public",
94
+ "static",
95
+ "yield"
96
+ ]);
97
+ const strictBindOnlyReservedWords = new Set(["eval", "arguments"]);
98
+ /**
99
+ * Returns `true` if `word` is a reserved keyword of the core grammar (`if`,
100
+ * `class`, `typeof`, `return`, …). Does not include `enum`, `await`, or the
101
+ * strict-mode-only words; see {@link isReservedWord} and
102
+ * {@link isStrictReservedWord} for those.
103
+ */
104
+ function isKeyword(word) {
105
+ return keywords.has(word);
106
+ }
107
+ /**
108
+ * Returns `true` if `word` is unconditionally reserved: `enum` in any context,
109
+ * and `await` when `inModule` (module / async contexts).
110
+ */
111
+ function isReservedWord(word, inModule = false) {
112
+ return inModule && word === "await" || word === "enum";
113
+ }
114
+ /**
115
+ * Returns `true` if `word` is reserved in strict mode: everything
116
+ * {@link isReservedWord} covers, plus `let`, `static`, `yield`, `implements`,
117
+ * and friends.
118
+ */
119
+ function isStrictReservedWord(word, inModule = false) {
120
+ return isReservedWord(word, inModule) || strictReservedWords.has(word);
121
+ }
122
+ /**
123
+ * Returns `true` for `eval` and `arguments`, which are reserved only as
124
+ * assignment / binding targets in strict mode.
125
+ */
126
+ function isStrictBindOnlyReservedWord(word) {
127
+ return strictBindOnlyReservedWords.has(word);
128
+ }
129
+ /**
130
+ * Returns `true` if `word` is reserved as a strict-mode binding target:
131
+ * everything {@link isStrictReservedWord} covers, plus `eval` / `arguments`.
132
+ */
133
+ function isStrictBindReservedWord(word, inModule = false) {
134
+ return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
135
+ }
136
+ /**
137
+ * Returns `true` if `name` can be used as an identifier **binding**: a valid
138
+ * {@link isIdentifierName} that, when `reserved` is `true` (the default), is
139
+ * neither a keyword nor a strict-mode reserved word.
140
+ *
141
+ * This is the check to reach for when turning an arbitrary string (e.g. a
142
+ * module specifier) into a local binding name.
143
+ *
144
+ * @example
145
+ * isValidIdentifier("foo"); // true
146
+ * isValidIdentifier("class"); // false - reserved word
147
+ * isValidIdentifier("await"); // false - reserved in modules
148
+ * isValidIdentifier("eval"); // true - valid identifier (strict-bind only)
149
+ * isValidIdentifier("class", false); // true - reserved words allowed
150
+ */
151
+ function isValidIdentifier(name, reserved = true) {
152
+ if (reserved && (isKeyword(name) || isStrictReservedWord(name, true))) return false;
153
+ return isIdentifierName(name);
154
+ }
155
+ //#endregion
156
+ export { isIdentifierChar, isIdentifierName, isIdentifierStart, isKeyword, isReservedWord, isStrictBindOnlyReservedWord, isStrictBindReservedWord, isStrictReservedWord, isValidIdentifier };