Repository navigation
Bind imports in the module-level scope, and resolve default imports - #131
Merged
Merged
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
nstepien
force-pushed
the
root-scope-imports
branch
from
November 7, 2026 01:21
20aa0ae to
4b7b869
Compare
Imports were kept apart from the scopes, in importedIdentifiers, and
the css tag in cssTagNames. resolveValue only looked them up when 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, resolveBinding resolves them next to `const`
bindings, and the tag check reads the binding of the tag.
Values exported as imported bindings, like `export { a }` after
`import { a } from './x'`, now resolve in the module they are imported
from, as `export const b = a` already did. `export { a } from './x'`
is still skipped, as it binds nothing in the module.
Imports are cached with the `const` values of the transform, unknown
while they resolve, so imports that modules export to each other stay
unknown instead of being followed forever. A Vite dev server test
covers it, as a bundle cannot link such modules. Without the cache, the
test worker runs out of memory.
The JS, CSS, and source maps of the benchmark builds are unchanged, and
their timings move by a few percent in both directions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
A TypeScript interface exported by default binds nothing.
Namespace imports stay unknown, and the import-forms test now shows
that their members are not resolved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
force-pushed
the
root-scope-imports
branch
from
November 7, 2026 01:28
4b7b869 to
70cf5b7
Compare
… it skips Each expression type is now handled in one place. A negated const covers the unary expressions that are not signed numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
marked this pull request as ready for review
November 7, 2026 04:55
nstepien
added a commit
that referenced
this pull request
Nov 7, 2026
`resolveValue` and `resolveBinding` were async, so every interpolation went through a promise, even a literal or a `const` declared in the module. They now return the value itself, and only return a promise when an import has to be loaded first. Cached bindings, literals, and local `const` bindings resolve synchronously. The binding cache still holds values, not promises: while a binding resolves, it is cached as unknown, and its value replaces it once resolved. A cached promise would make a cycle wait for itself. `AGENTS.md` and the comment on `bindingValues` now say so. **Benchmarks** Medians with Vite 8.3.5, #131 against this branch. The JS, CSS, and final source maps are identical. | Workload | #131 | This branch | |---|---|---| | Vite build of a synthetic project: 40 components × 8 templates × 5 tokens from one `tokens.ts` | 25.2 ms | 25.5 ms | | The same with 400 components | 175.4 ms | 170.1 ms (−3%) | | The same, with this branch built first in each pair | 184.8 ms | 179.2 ms (−3%) | | Cold dev server transforms of the 40 components | 37.6 ms | 37.5 ms | | Cold dev server transforms of the 400 components | 369.5 ms | 357.5 ms (−3%) | | Vite build of react-data-grid's `src` | 22.0 ms | 21.7 ms | **Alternatives** - Sharing the values between transforms, in `WeakMap`s keyed by AST nodes, goes stale in the dev server: editing `tokens.ts` keeps the parse of a module that exports one of its values again, so the old value stays cached. A transform that reads a cached value would also skip importing the stylesheet of its module and registering itself as a dependent for HMR. And concurrent transforms would see the bindings that others are resolving as cycles. This also enables the `prefer-const` lint rule, which the code already follows. `denyWarnings` makes it fail the lint like the other rules. The tests pass unchanged, and the only uncovered line is the same as in #131. 🤖 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.
Imports in the module-level scope
Imports were kept apart from the scopes: their sources in
importedIdentifiers, and the local names of ecij'scssincssTagNames.resolveValueonly 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:findBindingScopeis the only lookup.resolveBindingresolves them next toconstbindings, through a newresolveImport, which holds what was the import branch ofresolveValue.cssis{ type: 'ImportBinding', source: 'ecij', imported: 'css' }, and a local binding of the same name shadows it, as before.ParsedFileInfo.importedIdentifiersandcssTagNamesare 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, likeimport { default as x }.export defaultbinds its value to*default*, the local name the spec gives it, which no identifier can be. So the exported value resolves like aconstinitializer: 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 expressionresolveValuedoesn'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, whileexport const b = adid.export { a } from './x'is still skipped, as it binds nothing in the module (theTODO: handle re-exports).Cycles
Imports are cached with the
constvalues of the transform (constValuesis nowbindingValues), 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
resolveValuenow 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 negatedconst(${-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:
The uncovered lines are unchanged from #128, with one fewer uncovered branch.
The two guards of
resolveImportare one-liners, like the one inresolveImportedModule, as an import that can't be resolved stays untested.AGENTS.mdnow 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