feat: lex type-only TypeScript imports and exports - #2
Draft
BridgeAR wants to merge 2 commits into
Draft
Conversation
A multi-declarator export reported only the first binding:
`export const a = 1, b = 2` yielded `a`, never `b` - the initializer scanner
stopped at the first declarator instead of skipping each initializer to its
separating comma.
skipExpression now rides consumeToken, the one tokenizer the main loop uses, so
its regex, keyword and import resolution cannot diverge from the main scan. That
also fixes four cases a separate, simplified scanner got wrong:
1. A '/' after a value keyword (return/typeof/yield/void) opens a regex, so the
comma in `export const f = () => { return /,/g }, b = 2` no longer truncates
the list.
2. A '}' closing a block leaves the next '/' in statement position (a regex).
3. import() in an initializer or a destructuring default is detected.
4. A numeric or BigInt property key such as `{ 0.5: b }` is consumed whole.
The lexer only understood JavaScript, so a TypeScript module's `import type` / `export type` either parsed as a value import/export or failed outright (issue guybedford#72: `export type { A } from 'x'`). Running a separate TypeScript transform first is no faster than what Node.js type stripping already does at runtime, and pulling a full TypeScript parser (swc) into the package would dwarf its footprint. Instead the existing C lexer learns the syntax directly, behind a `LEX_TS` compile-time switch that produces a second Wasm build. The JavaScript build is byte-for-byte the same code path with no added branches, so non-TypeScript users pay nothing; the TypeScript Wasm is a few hundred bytes larger than the JavaScript one and ships only behind the new `es-module-lexer/ts` entry. Type-only imports and exports are reported rather than elided, marked with a new `tp` boolean on every import and export specifier (always false from `parse`). Inline modifiers are tracked per specifier, so `export { type A, b }` marks only A. The accept/reject boundary matches Node.js type stripping: this first increment covers type-only imports and exports; annotations, generics, `as`/`satisfies` and the rest of the erasable surface follow next. Fixes: guybedford#72
guybedford
force-pushed
the
BridgeAR/2026-06-16-export-binding-detection
branch
from
June 28, 2026 22:15
a5e5a16 to
9c25cc0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Teaches the lexer to understand the type-only TypeScript syntax that fails or mis-parses today (issue guybedford#72:
export type { A } from 'x'), without a separate transform step and without pulling a TypeScript parser into the package.The existing C lexer learns the syntax directly behind a
LEX_TScompile-time switch that produces a second Wasm build:es-module-lexer/tsentry exposingparseTs/initTs.tpboolean on every import and export specifier (alwaysfalsefromparse). Inline modifiers are tracked per specifier, soexport { type A, b }marks onlyA.Why
Running a TypeScript transform first is no faster than what Node.js type stripping already does at runtime; a single lexer that handles both languages is the win. Reporting (rather than eliding) type-only specifiers serves consumers building a module graph, who need to see the type edges.
Scope
The accept boundary matches Node.js type stripping. This first increment covers type-only imports and exports. Type annotations, generics,
as/satisfies, non-null!and the rest of the erasable surface are planned as a follow-up; non-erasable syntax (enum, runtimenamespace, parameter properties, legacy decorators) stays out of scope.Notes for review
consumeToken/skipExpressionrefactor is the base); review this PR's diff against that branch.lib/lexer.ts.wasmand the rebuiltlib/lexer.wasmare produced by the build (chomp build); the committed binaries are left to the canonical toolchain / CI rather than this environment's emcc, whose output differs in size from the committed artifact.lexer.js) only gainstp: falseparity; its pre-existing defer-phase gap is unrelated.Test plan
chomp testruns the newtest:tsjob (TS=1) plus the existing wasm/asm suites.test/typescript/: type-only imports, type-only exports, regex/division non-regression, attribute/phase permutations, and a JS-superset guard asserting plain-JS inputs reporttp: false.