Skip to content

Resolve the members of namespace imports - #135

Merged
nstepien merged 3 commits into
mainfrom
namespace-imports
Nov 8, 2026
Merged

nstepien merged 3 commits into
mainfrom
namespace-imports

Conversation

@nstepien

@nstepien nstepien commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Interpolations can now read the exports of a module through a namespace import:

import * as tokens from './tokens';

export const buttonClass = css`
  color: ${tokens.color};
`;

Until now, a namespace import was an unknown binding, and ${tokens.color} skipped its whole template with a COMPLEX_INTERPOLATION warning.

How it works

  • import * as ns from 'mod' binds ns in the module-level scope as a NamespaceImport, like named and default imports.
  • A namespace import resolves to the parse of its module, which stands for the namespace. A value is now either a string or a namespace (StaticValue), and only strings are interpolated, so ${ns} is skipped instead of interpolating [object Object].
  • resolveMember replaces resolveImport and, like it, is async: it waits for the namespace, resolves its member to the export of that name, and imports the module's stylesheet first when the member is a string, as it may be one of its class names. resolveBinding now resolves a named or default import as the member of the namespace of its module.
  • Namespaces resolve through the same bindings as other values, so they also resolve when exported again (export { ns }), imported by name, aliased by a const, or nested (${ns.inner.x}).
  • A member is returned as a promise even when its namespace is already resolved, so AGENTS.md now says that a value is only returned as a promise when it is read through an import or a member. Returning members without one would take a second branch for both the namespace and the value, and is not measurably faster (see the benchmarks). resolveBinding stays synchronous, as returning every binding as a promise is measurably slower with many named imports.

Warnings

  • Warnings name the whole chain of members that does not resolve, like could not resolve "ns.x.length". Chains of members of an identifier now get UNRESOLVED_INTERPOLATION instead of COMPLEX_INTERPOLATION, including the members of local objects, like theme.color, which are still not resolved.
  • Computed members, like ns['x'], are not resolved and still get COMPLEX_INTERPOLATION.

Not included

  • Re-exports, which stay in the README's TODO: export * as ns from 'mod', export { x } from 'mod', and export *. A member that a module re-exports, like ns.reexported, is skipped.
  • Computed members, though ns['string name'] is the only way to read an export named by a string through a namespace. Named imports of such exports (import { 'string name' as name }) resolve.
  • The css tag itself must still be a named import, as the code filter only matches named imports from ecij: ecij.css from import * as ecij from 'ecij' is not extracted.

Tests

  • The import-forms test now extracts helper.default.
  • A new test, resolve the members of namespace imports, covers values and class names, with the imported stylesheet first in the CSS, a namespace exported again and read as a member or imported by name, const aliases of a namespace and of a member, and the forms that are skipped: a re-exported member, a namespace interpolated as a value, a member of a string, a member of a local object, a computed member, and a parameter shadowing the namespace.
  • 28 tests pass. The only uncovered line is still the TSParameterProperty branch, with 15 uncovered branches against 16 on main.
  • Checked outside the suite: a plain Rolldown build (raw TypeScript) extracts the same templates. In the Vite dev server, editing tokens.ts updates the CSS of a module that reads tokens.color through a namespace, and of the module that reads its class name through another namespace.

Benchmarks

Medians of Vite builds of a synthetic project: N components × 8 templates × 5 tokens from one tokens.ts, each variant in its own process, with the range over 4 processes. The JS and CSS of the named-import project are identical on main and on this branch, and the namespace project's CSS only differs in its class name hashes.

Workload main, named imports This branch, named imports This branch, namespace import
40 components 26.3–27.4 ms 26.7–27.3 ms 24.9–25.8 ms
400 components 138.4–143.8 ms 138.4–139.0 ms 127.7–129.9 ms

A namespace import resolves one binding per module instead of one per token. Returning members without a promise once their namespace is resolved was not measurably faster: in another run at 400 components, the namespace project built in 128.9–130.0 ms that way, leaving out one process at 139.2 ms, and in 128.8–132.2 ms with async. Making resolveBinding async too made the named-import project about 3% slower at 400 components, with a median of 4.3 ms over 10 paired runs that were all slower, and made no difference at 40 components or with a namespace import. Benchmarking both plugins in the same process measured this branch 1.5–2 ms slower at 40 components, but not at 400, and separate processes do not reproduce it.

🤖 Generated with Claude Code

Interpolations can read the exports of a module through a namespace
import, like `${tokens.color}`, including class names, whose stylesheet
is imported first, as with named imports. Until now, a namespace import
was an unknown binding, so `${ns.x}` skipped its template.

A namespace import is now a binding of the module-level scope, like
other imports, and resolves to the parse of its module, which stands
for the namespace. A value is therefore either a string or a namespace,
and only strings are interpolated. A member of a namespace resolves to
the export of that name, so a named import is resolved as the member
of the namespace of its module. Namespaces resolve through the same
bindings as other values: exported again, imported by name, aliased by
a `const`, and nested, like `${ns.inner.x}`.

Members are resolved by name only, so computed members, like
`ns['x']`, are still skipped. Warnings name the whole chain of members
that does not resolve, like `ns.x.length`, and member expressions now
get `UNRESOLVED_INTERPOLATION` instead of `COMPLEX_INTERPOLATION`.

Resolution stays synchronous unless an import has to be loaded first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 8, 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 and others added 2 commits November 8, 2026 04:57
Returning a member without a promise once its namespace is resolved
took a .then branch for both the namespace and the value, and was not
measurably faster, even with 16,000 member reads per build.
resolveMember now awaits them, like resolveImport on main, and imports
the stylesheet itself, as importStylesheet was left with one caller.
AGENTS.md and the resolveValue comment now say that a value is only
returned as a promise when it is read through an import or a member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveImport only chose between the namespace of a module and one of
its members, so resolveBinding now does that itself. resolveBinding
stays synchronous: as an async function, it made the build of 400
components importing 40 tokens each by name about 3% slower, in all 10
paired runs, with no difference at 40 components or with a namespace
import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review November 8, 2026 05:31
@nstepien
nstepien merged commit 49192f8 into main Nov 8, 2026
11 checks passed
@nstepien
nstepien deleted the namespace-imports branch November 8, 2026 05:34
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