Skip to content

feat: report star re-exports on both import and export sides - #215

Draft
BridgeAR wants to merge 7 commits into
guybedford:v3from
BridgeAR:BridgeAR/2026-06-30-star-export-220
Draft

feat: report star re-exports on both import and export sides#215
BridgeAR wants to merge 7 commits into
guybedford:v3from
BridgeAR:BridgeAR/2026-06-30-star-export-220

Conversation

@BridgeAR

@BridgeAR BridgeAR commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

export * from 'module' is both a dependency edge and a re-export, but the lexer surfaced only the specifier — typed identically to a side-effect import 'module', so consumers cannot tell the two apart without a regex over the source. This reports the star on both sides:

  1. A new import type StaticReexportStar = 8 marks the specifier of a plain export * from 'x', distinguishing it from a side-effect import 'x' (type 1), which is otherwise the same shape.
  2. The export side reports the name * for a plain export *, its name span pointing at the literal * and its statement range matching the import's, so the two halves correlate. export * as ns from 'x' is unchanged — it already reports ns.

Why

import-in-the-middle currently runs a regex over every re-export statement to recover the star re-exports the lexer dropped. Typing the specifier lets it drop that regex and read imported.t === 8 directly — the information the lexer already had while tokenizing, at no extra scan cost.

Test plan

  • npm run build && npm test (wasm + asm.js builds)
  • New Export star reexport* cases in test/_unit.cjs cover type 8 vs side-effect 1, export name *, comments between tokens, facade + module-syntax detection, and statement-range correlation.

Refs: #76
Refs: nodejs/import-in-the-middle#259

@BridgeAR
BridgeAR marked this pull request as ready for review July 1, 2026 17:20

@guybedford guybedford left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to land this. Formally speaking though it is a breaking change unless we make it export metadata-based.

If we wanted a non-breaking version, RollupJS used to have an internal convention here (not sure if it still does) of making the export name itself *module. That is, if the export name begins with * then it is a star export from that module.

It's a hack and this approach is better though. Would just be nice to bunch with any other major changes for a 3.x.

@guybedford

Copy link
Copy Markdown
Owner

If we're doing a break for 3.x, making exports return an object would likely be smarter then as well.

@BridgeAR

BridgeAR commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I think having v3 makes most sense

@guybedford guybedford added this to the 3.0.0 milestone Jul 12, 2026
* perf(lexer): skip identifier and number runs

On the 3,057 KiB sample corpus, Wasm drops from 15,356.31 to 13,010.46 us per sweep on Node 18.20.8 (-15.28%) and from 11,724.34 to 9,856.69 us on Node 24.18.0 (-15.93%). The asm.js build drops from 15,934.34 to 12,945.57 us on Node 24.18.0 (-18.76%).

The fast path adds 379 raw / 105 gzip bytes to the full Wasm binary. Measurements used 1,500 warmup sweeps followed by 9 interleaved trials of 50 sweeps, dropping the best and worst trials.
BridgeAR added 6 commits July 25, 2026 13:55
lexer.js reimplemented the WebAssembly lexer in hand-written JS, kept in sync
by hand, but was unreachable through the package `exports` (only `.` and
`./js` are exposed - the wasm and asm.js builds) and `chomp test` never ran
it, so it could drift silently. The asm.js build already covers the
no-WebAssembly case, and the removed `if (!js)` test guards now run
unconditionally, matching their existing behavior under the wasm and asm
suites.
A dynamic import whose entire argument is a template literal with substitutions
returned n: undefined, so a consumer resolving the specifier (a bundler or glob
importer) had nothing to work with. It now reports the static skeleton as a
glob with each ${...} collapsed to a single "*": import(`./locales/${x}.js`)
yields "./locales/*.js".

Only a lone template literal qualifies. A template concatenated with anything
else (import(`a` + b)) has no static skeleton and still returns undefined.

Fixes: guybedford#137
The glob walker that builds a dynamic-import template skeleton skips over
strings, nested templates, and comments inside each ${...} substitution, but it
cannot tell a regex literal from division without the main parser's token
context. A regex carrying a "}" closed the substitution early and emitted a
wrong specifier instead of bailing: import(`a${ /x}y/g }b`) reported "a*y/g }b"
rather than undefined.

skipInterpolation now flags a bare "/" (one that does not open a // or /*
comment) and the three decoders drop n to undefined rather than guess. This
over-bails the rare division case (import(`a${ b/c }d`)), which is acceptable:
a missing glob is recoverable, a wrong one is not.

Fixes: guybedford#137
The interpolated-template glob walked each ${...} substitution with a
hand-rolled scanner in all three decoders. None of them could tell a regex
literal from division without token context, so a regex carrying a "}" closed
the substitution early: import(`a${ /x}y/g }b`) reported "a*y/g }b". The prior
fix bailed to undefined on any bare "/", which also dropped legitimate division
(import(`a${ b/c }d`)).

The parser already resolves regex vs division for the whole source and descends
into ${ ... } for nested-import detection, so it now records each top-level
substitution's end on the dynamic import (struct TemplateSpan). The decoders
splice a "*" per span and jump the body, dropping their skipInterpolation /
skipQuoted / skipComment scanners and the interpolationError bail. Both
ambiguous cases now resolve correctly: "a*b" and "a*d".

Fixes: guybedford#137
…ects

## Summary

Interpolated template glob tracking leaked into minimal builds and diverged between decoders. Missing parser spans could evaluate a substitution, reload an exhausted span list into a loop, or lose the outer glob around a nested import.

## Why

Keep span tracking behind LEXER_MIN, use parser-recorded spans as the substitution boundary, and copy static source without eval. The full Wasm and asm.js builds now agree while minimal output stays unchanged.

Static parts remain raw source, so escapes are not cooked and a literal * stays literal.

## Test plan

- chomp test

Fixes: guybedford#137
## Summary

- Add `StaticReexportStar` as import type 8 for `export * from`.
- Report the matching `*` export with a span that correlates to the import.
- Cover full and minimal builds without reading fields omitted by the minimal API.

## Why

Star re-exports are dependency edges and exports, but the lexer only exposed
their specifier as a plain static import. Consumers had to recover the missing
distinction by parsing the source again.

## Test plan

- `chomp test:wasm test:minimal:wasm`

Refs: guybedford#76
Refs: nodejs/import-in-the-middle#259
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-06-30-star-export-220 branch from 6282f21 to b4b5a29 Compare August 2, 2026 14:31
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.

2 participants