Repository navigation
Share pending parses of imported modules between transforms - #128
Merged
Merged
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
This was referenced Nov 6, 2026
The parse cache only stored finished parses. The transforms of the modules that import the same module run concurrently: in a build, they all wait for this.load() of that module and resume together, so each missed the cache and read and parsed the file again. A project of 400 components interpolating values of one tokens.ts parsed it 400 times per build, and in a cold dev server. The cache now holds the promise of each parse, stored as soon as it is requested, so the file is read and parsed once. parseFile only parses, and loadParsedFile returns the cached or pending parse of a module read from disk, like loadImportedModule does for the imported modules of a transform. Medians with Vite 8.3.5, against the previous commit, with identical JS, CSS, and final source maps: - Vite build, 40 components x 8 templates x 5 tokens: 28.5 ms to 23.1 ms (-19%) - The same with 400 components: 224.0 ms to 171.6 ms (-23%) - Cold dev server transforms of the 40 components: 35.9 ms to 31.8 ms (-11%), of the 400 components: 386.1 ms to 339.9 ms (-12%) - Vite build of react-data-grid's src, which reads no module from disk: 20.7 ms to 21.0 ms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
force-pushed
the
share-pending-parses
branch
from
November 6, 2026 22:47
736580d to
ed1a7d1
Compare
The parse of an imported module is awaited either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
marked this pull request as ready for review
November 7, 2026 01:26
nstepien
added a commit
that referenced
this pull request
Nov 7, 2026
…131) **Imports in the module-level scope** Imports were kept apart from the scopes: their sources in `importedIdentifiers`, and the local names of ecij's `css` in `cssTagNames`. `resolveValue` only looked an import up once no scope bound the name, and the tag check combined both lookups. Imports are now bindings of the module-level scope, like its declarations: - The scope chain finds them, so `findBindingScope` is the only lookup. - `resolveBinding` resolves them next to `const` bindings, through a new `resolveImport`, which holds what was the import branch of `resolveValue`. - The tag check reads the binding of the tag: ecij's `css` is `{ type: 'ImportBinding', source: 'ecij', imported: 'css' }`, and a local binding of the same name shadows it, as before. - `ParsedFileInfo.importedIdentifiers` and `cssTagNames` are gone. Namespace imports are recorded as bindings with unknown values, which they were in effect before. **Default imports** A default import is now an import of the binding exported as `default`, like `import { default as x }`. `export default` binds its value to `*default*`, the local name the spec gives it, which no identifier can be. So the exported value resolves like a `const` initializer: a literal, a binding, or a css`` template, whose class name it takes, with its stylesheet imported first. Functions and classes exported by default don't resolve, like any expression `resolveValue` doesn't support, and a TypeScript interface binds nothing. The README no longer lists default imports and exports as missing. **Imports exported again** Values that a module exports as imported bindings now resolve in the module they are imported from. On #128, `import { a } from './x'; export { a }` didn't resolve, while `export const b = a` did. `export { a } from './x'` is still skipped, as it binds nothing in the module (the `TODO: handle re-exports`). **Cycles** Imports are cached with the `const` values of the transform (`constValues` is now `bindingValues`), unknown while they resolve. So imports that modules export to each other stay unknown instead of being followed forever. A new test covers it in the Vite dev server, which transforms modules one by one, as a bundle can't link such modules. Without the cache, the test worker runs out of memory within about 10 seconds. **Unsupported expressions** `resolveValue` now returns as soon as a literal or a unary expression turns out to be unsupported, so each expression type is handled in one place. A negated `const` (`${-gap}`), which stays unresolved, covers the unary case in the literal interpolation test. **Tests** The import-forms test, now named "resolve the supported import and export forms, and skip the others", covers: - default imports, of a value and of a class name - an interface exported by default - a value and a class name exported again - members of a namespace import, which stay unresolved The uncovered lines are unchanged from #128, with one fewer uncovered branch. The two guards of `resolveImport` are one-liners, like the one in `resolveImportedModule`, as an import that can't be resolved stays untested. `AGENTS.md` now says that imports are module-level bindings, where default exports are bound, and that cycles through imports stay unknown. **Benchmarks** The JS, CSS, and final source maps of the benchmark builds are unchanged, react-data-grid's included, and their timings moved by a few percent in both directions against #128. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
added a commit
that referenced
this pull request
Nov 7, 2026
Main reworked the import handling underneath this branch: imports read from the top-level statements (#127), parses cached while pending (#128), imports as bindings of the module-level scope with default imports through `*default*` (#131), values resolved without promises (#133), and TypeScript namespaces given a scope of their own (#130). src/index.ts was taken from main and this branch's features ported onto it: - Namespace imports are `NamespaceImport` bindings, and member chains (`ns.a.b`) resolve through the exports of each namespace. - Exports are `moduleExports` (local, re-export, namespace) plus `exportStarSources`, read from the top-level statements, and `resolveExport` follows them with ESM precedence and ambiguity. Exporting an imported binding re-exports it, as in the spec. Exported functions, classes, enums, and destructured bindings are recorded, so that they take precedence over `export *`. - Type-only imports and exports are skipped through `importKind` and `exportKind`, as the module record is no longer read. - Main's `isCssTag`, `bindingValues`, and `MaybePromise` resolution are kept. An import resolves through `resolveExport` and imports the stylesheet of the module that declares the value. Main's decisions win where they differ from this branch: - TypeScript namespaces are not supported: the namespace-name and `import x =` bindings are dropped, along with the typed fixture's namespace cases, as main's namespace test covers containment. Enums still shadow. - Templates exported by default get no `default` name, so their class names hash like main's. Main's import-forms test now extracts the namespace member and the re-export it skipped. Two fixture cases cover the export of a namespace import and a member of a local object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
The parse cache only stored finished parses. The transforms of the modules that import the same module run concurrently: in a build, they all wait for
this.load()of that module and resume together, so each one missed the cache and read and parsed the file again. In a project of 400 components that interpolate values of onetokens.ts, it was parsed 400 times per build, and in a cold dev server too.The cache now holds the pending parse of each module read from disk, stored as soon as it is requested, so the file is read and parsed once. The parse of a module being transformed is stored as is.
parseFileonly parses now, andloadParsedFilereturns the cached or pending parse of a module read from disk, likeloadImportedModuledoes for the imported modules of a transform (#124). Caching the promise is safe for the same reason: a parse never waits for a value to resolve, so it can't wait for itself.Benchmarks
Medians with Vite 8.3.5, #127 against this branch. The JS, CSS, and final source maps are identical.
tokens.tssrc, which reads no module from disktokens.tsis now read and parsed once per build and per cold dev server, instead of 400 times.Other effects
buildEndorwatchChangeclears it. It fails the transform that interpolates the module's values either way.watchChangeforgot the module can no longer store its stale parse, as the pending parse is cached before the read starts.Alternative
parsefromrolldown/utils, which parses on another thread, was also measured. On its own, it gave up to 11% on these builds, the most where the duplicate parses overlapped, and 0–4% on cold dev transforms. On top of this branch, it gave at most 4%, while Rolldown advises preferringparseSync, so it's left out.The tests pass unchanged, with the same uncovered lines and branches as #127.
AGENTS.mdnow says why the parse is cached while pending.🤖 Generated with Claude Code