Skip to content

Resolve values without promises unless an import has to be loaded - #133

Merged
nstepien merged 2 commits into
mainfrom
sync-resolution
Nov 7, 2026
Merged

nstepien merged 2 commits into
mainfrom
sync-resolution

Conversation

@nstepien

@nstepien nstepien commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

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 WeakMaps 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

@nstepien
nstepien added this pull request to stack #129 November 7, 2026 03:50
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@nstepien nstepien changed the title sync resolution Resolve values without promises unless an import has to be loaded Nov 7, 2026
Base automatically changed from root-scope-imports to main November 7, 2026 04:55
nstepien and others added 2 commits November 7, 2026 04:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveValue and resolveBinding return the value itself when it does not
depend on an import that has to be loaded first. The binding cache still
holds values, not promises, as a cycle would wait for its own promise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review November 7, 2026 04:59
@nstepien
nstepien merged commit 6284da9 into main Nov 7, 2026
11 checks passed
@nstepien
nstepien deleted the sync-resolution branch November 7, 2026 04:59
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>
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