yuku-parser
A high-performance, spec-compliant JavaScript and TypeScript parser, part of Yuku.
Install
npm install yuku-parser
It runs on a native binary for each platform. In browsers, edge runtimes, and on platforms without one, it runs on @yuku-engine/wasm.
Usage
import { parse } from "yuku-parser";
const { program, comments, diagnostics } = parse("const x = 1 + 2;");
ESTree / TypeScript-ESTree
For JavaScript and JSX, the AST is fully conformant with the ESTree specification, identical to what Acorn produces. For TypeScript, it conforms to the TypeScript-ESTree format used by @typescript-eslint. For both, it matches the AST Oxc produces.
On top of the base specs, the AST carries:
- Stage 3 decorators.
- Stage 3 import defer and import source. The dynamic forms are an
ImportExpression with phase set to "defer" or "source".
- A
hashbang field on Program for #!/usr/bin/env node lines.
Any other deviation from Acorn's ESTree or @typescript-eslint's TypeScript-ESTree is a bug.
AST types
Every node type is exported, from the Node union down to individual types, listed in the type definitions.
import type { Expression, Identifier, Node, Statement } from "yuku-parser";
Options
All options are optional.
parse(source, { lang: "tsx", sourceType: "module" });
sourceType | "module", "script", "commonjs" | "module" | Module mode enables import/export, import.meta, top-level await, and strict mode. CommonJS mode parses script code whose top level behaves like a function body, allowing top-level return, new.target, and using. |
lang | "js", "ts", "jsx", "tsx", "dts" | "js" | The syntax extensions to enable. |
preserveParens | true, false | true | Keep ParenthesizedExpression nodes. When off, only the inner expression is kept. |
semanticErrors | true, false | false | Also report semantic errors. See Semantic errors. |
attachComments | true, false | false | Also attach each comment to its host node. See Comments. |
tokens | true, false | false | Keep every token in result.tokens. See Tokens. |
Path helpers
lang and sourceType can be inferred from a file path. .d.ts, .tsx, .ts, and .jsx select their language, and .cjs and .cts select CommonJS.
import { langFromPath, sourceTypeFromPath } from "yuku-parser";
langFromPath("app.tsx");
langFromPath("types.d.ts");
sourceTypeFromPath("app.cjs");
sourceTypeFromPath("app.mjs");
Result
parse returns a ParseResult.
interface ParseResult {
program: Program;
comments: Comment[];
tokens?: TokenList;
diagnostics: Diagnostic[];
}
The parser recovers from errors, so a parse with diagnostics still returns a tree of everything it could read.
Diagnostics
interface Diagnostic {
severity: "error" | "warning" | "hint" | "info";
message: string;
help: string | null;
start: number;
end: number;
labels: { start: number; end: number; message: string }[];
}
Every Yuku package reports diagnostics in this shape.
Semantic errors
The parser reports syntax errors. Errors that need scopes and bindings, such as duplicate let declarations, break outside a loop, and unresolved private fields, come from a separate and cheap semantic pass, enabled with semanticErrors. Leave it off when a linter or type checker already validates the code.
parse(`let x = 1; let x = 2;`, { semanticErrors: true }).diagnostics;
result.comments always holds every comment in source order, with its source span.
const { comments } = parse(`// a line comment\nconst x = 1; /* a block comment */`);
for (const c of comments) {
console.log(c.type, JSON.stringify(c.value), c.start, c.end);
}
interface Comment {
type: "Line" | "Block";
value: string;
start: number;
end: number;
}
The span covers the whole comment, so source.slice(c.start, c.end) returns the raw text.
attachComments: true also hangs each comment on the AST node it sits next to, read off node.comments. Attached comments move with their node through transforms, which is what yuku-codegen prints from.
const { program } = parse(`// header\nfunction foo() {} // trailing`, { attachComments: true });
program.body[0].comments;
interface AttachedComment {
type: "Line" | "Block";
position: "before" | "after" | "inside";
sameLine: boolean;
value: string;
}
position is where the comment sits relative to its host: "before" leads it, "after" trails it, and "inside" is interior to an otherwise empty host, like function f() { /* hi */ }. sameLine is true when the comment shares a source line with the host's adjacent edge, the start for "before" and the end for "after", and always false for "inside".
Tokens
tokens: true keeps every token the parser consumed. The result carries a TokenList, a view over the parser's token table. Nothing is decoded up front, a token is an index, and each accessor is one typed-array read.
import { parse, TokenKind } from "yuku-parser";
const { tokens } = parse(source, { tokens: true });
for (let i = 0; i < tokens.length; i++) {
if (tokens.kind(i) === TokenKind.Arrow) console.log(tokens.start(i), tokens.text(i));
}
tokens.kind(i)
tokens.text(i)
tokens.start(i)
tokens.end(i)
tokens.isKeyword(i)
tokens.isReserved(i)
tokens.isUnconditionallyReserved(i)
tokens.isStrictModeReserved(i)
tokens.isIdentifierLike(i)
tokens.isNumericLiteral(i)
tokens.isBinaryOperator(i)
tokens.isLogicalOperator(i)
tokens.isUnaryOperator(i)
tokens.isAssignmentOperator(i)
tokens.precedence(i)
tokens.newlineBefore(i)
tokens.escaped(i)
tokens.invalidEscape(i)
tokens.loneSurrogate(i)
Every node other than Program, TemplateElement, and JSXEmptyExpression starts on a token start and ends on a token end, so a node's tokens are a contiguous run. The queries take a node and answer with an index, -1 when there is none. They are binary searches, so they replace a token store without building one.
tokens.range(node)
tokens.first(node)
tokens.last(node)
tokens.before(node)
tokens.after(node)
tokens.at(offset)
Tokens are as the parser resolved them: a regex is one RegexLiteral, and the >> closing a nested generic is two GreaterThan. Comments are not tokens. The kinds are listed in tokens.d.ts.
Why an index and not an array of objects? On a 1 MB file, about 215,000 tokens, tokens: true adds 1 ms to the parse and scanning every kind(i) another 0.4 ms. An object per token would add 8 ms and 20 to 50 MB of heap, which is what tokens cost in espree, acorn, and Babel, and 70 ms in typescript-estree.
Walking the AST
yuku-ast walks the AST with typed visitors and in-place mutation, and builds and checks nodes.
import { parse } from "yuku-parser";
import { walk } from "yuku-ast";
walk(parse(`console.log("hello");`).program, {
Identifier(node) {
console.log(node.name);
},
});
Semantic analysis
yuku-analyzer adds scopes, symbols, resolved references, closure analysis, and cross-file module linking, computed natively in the same pass as the parse.
License
MIT