Skip to content

feat: lex type-only TypeScript imports and exports - #2

Draft
BridgeAR wants to merge 2 commits into
BridgeAR/2026-06-16-export-binding-detectionfrom
BridgeAR/2026-06-27-typescript-type-stripping
Draft

feat: lex type-only TypeScript imports and exports#2
BridgeAR wants to merge 2 commits into
BridgeAR/2026-06-16-export-binding-detectionfrom
BridgeAR/2026-06-27-typescript-type-stripping

Conversation

@BridgeAR

Copy link
Copy Markdown
Owner

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_TS compile-time switch that produces a second Wasm build:

  1. The JavaScript build is the same code path with no added branches — non-TypeScript users pay nothing. The TypeScript Wasm is ~540 bytes larger than the JavaScript one (11.9 KB vs 11.4 KB in this toolchain) and ships only behind a new es-module-lexer/ts entry exposing parseTs / initTs.
  2. 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.

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, runtime namespace, parameter properties, legacy decorators) stays out of scope.

Notes for review

  • Stacked on fix: detect every binding in export var/let/const declarations guybedford/es-module-lexer#199 (its consumeToken / skipExpression refactor is the base); review this PR's diff against that branch.
  • lib/lexer.ts.wasm and the rebuilt lib/lexer.wasm are 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.
  • The pure-JS port (lexer.js) only gains tp: false parity; its pre-existing defer-phase gap is unrelated.

Test plan

  • chomp test runs the new test:ts job (TS=1) plus the existing wasm/asm suites.
  • New suites under test/typescript/: type-only imports, type-only exports, regex/division non-regression, attribute/phase permutations, and a JS-superset guard asserting plain-JS inputs report tp: false.
  • Verified locally in the emsdk 6.0.0 toolchain: TypeScript suite 34/34, JavaScript Wasm suite 136/136.

BridgeAR added 2 commits June 22, 2026 12:55
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
guybedford force-pushed the BridgeAR/2026-06-16-export-binding-detection branch from a5e5a16 to 9c25cc0 Compare June 28, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant