Yuku
Walk and build
walk visits a node and everything under it, depth-first in source order. From Zig, see traversal.
import { walk } from "yuku";
walk(file.program, {
Identifier(node, ctx) {},
CallExpression: {
enter(node, ctx) {},
leave(node, ctx) {},
},
Function(node) {}, // declarations, expressions, and arrows
enter(node) {}, // every node
leave(node) {},
});
Handlers are keyed by node type, by alias (Expression, Statement, Declaration, ModuleDeclaration, Function, Class, Method, Loop, Pattern, JSX, TSType), or by enter and leave. A list of visitors walks in one pass.
walk(file.program, [noDebugger, noConsole, preferConst]);
The context
ctx.parent; // null at the walk root
ctx.key; // the field of the parent holding it
ctx.index; // its index in an array field, or null
ctx.ancestors(); // from the root down to the parent
The semantic model answers for any node a walk visits, as file.scopeOf(node), file.bindingOf(node), and file.referenceOf(node).
Editing
| Call | Effect |
|---|---|
ctx.skip() | Skip the children |
ctx.stop() | End the walk |
ctx.replace(node) | Put a node, or a list in an array field, in its place, then visit it |
ctx.remove() | Remove it |
ctx.insertBefore(node) | Insert a sibling the walk passes over |
ctx.insertAfter(node) | Insert a sibling the walk visits next |
walk(file.program, {
DebuggerStatement(node, ctx) {
ctx.remove();
},
});
Async work
A walk runs synchronously. For async work, collect the nodes in one walk, await them together, and edit in a second.
const calls = [];
walk(file.program, { CallExpression: (node) => calls.push(node) });
const results = new Map(await Promise.all(calls.map(async (node) => [node, await rewrite(node)])));
walk(file.program, {
CallExpression(node, ctx) {
const result = results.get(node);
if (result !== undefined) ctx.replace(result);
},
});
Build
b has a constructor for every node type. It sets start and end to 0, and defaults each field that may be false, null, or empty.
import { b } from "yuku";
b.CallExpression({ callee: b.Identifier({ name: "f" }) });
// { type: "CallExpression", start: 0, end: 0, arguments: [], optional: false, callee: ... }
b.ArrowFunctionExpression({ body: b.Identifier({ name: "x" }), expression: true }); // () => x
A field that tells the shapes of a type apart, such as computed on a MemberExpression, has no default.
Utils
yuku/utils has the small helpers.
import { decodeJSXEntities, is, isIdentifierName, isValidIdentifier, unwrap } from "yuku/utils";
is.CallExpression(node);
is.Expression(node); // aliases too
is.oneOf(node, ["FunctionDeclaration", "ClassDeclaration"]);
is.StringLiteral(node); // also NumericLiteral, BooleanLiteral, NullLiteral,
// BigIntLiteral, and RegExpLiteral
is.StaticMemberExpression(node); // also ComputedMemberExpression and PrivateFieldExpression
is.Directive(node);
is.Wrapper(node); // parentheses or an erased TypeScript assertion
unwrap(node); // the expression inside every wrapper
isValidIdentifier("class"); // false, reserved
isIdentifierName("class"); // true
decodeJSXEntities("a & b"); // "a & b"
It also has isIdentifierStart, isIdentifierChar, isKeyword, isReservedWord, isStrictReservedWord, and isEvalOrArguments.