Skip to content

Keep the declarations of TypeScript namespaces out of the module scope - #130

Merged
nstepien merged 5 commits into
mainfrom
namespace-scope
Nov 7, 2026
Merged

nstepien merged 5 commits into
mainfrom
namespace-scope

Conversation

@nstepien

@nstepien nstepien commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

TypeScript namespaces are discouraged and stay unsupported, but they could break the code around them. SCOPE_VISITOR gave namespace bodies no scope of their own, so their declarations landed in the module scope:

  • A namespace's css turned off extraction for the whole module. A const css in a namespace replaced the ecij import, so none of the module's templates were extracted, nothing warned, and their css calls threw at runtime.
  • A namespace's bindings overrode the module's. A module-level ${accent} got the namespace's red instead of the module's blue.

A namespace body now gets its own scope, so its declarations stay inside it. TSModuleBlock is also the body of declare module and declare global blocks, and a namespace compiles to a function, so the scope is function-scoped for var declarations. The comment above the callback says that namespaces are otherwise not supported.

ecij sees namespaces in two places: the modules that plain Rolldown and tsdown pass to its transform, as they compile TypeScript afterwards, and the modules it reads from disk to resolve imported values, with any bundler, Vite included. Vite compiles namespaces to functions before ecij transforms a module.

Templates inside a namespace are now resolved in its scope, but that is a side effect, which the test does not cover. A template that used the export of another block of the same namespace no longer resolves, as main only resolved it through the leak, and Rolldown's own TypeScript transform does not resolve it either.

Test

The new test builds test/fixtures/namespace-scoping.input.ts with plain Rolldown. A namespace declares an accent and a css, and the module-level template is extracted with the module's accent. On main, nothing is extracted, and without the namespace's css, the template gets the namespace's red. The fixture's namespace needs @ts-expect-error, as the test tsconfig enables erasableSyntaxOnly. The coverage is unchanged, with the new callback covered.

🤖 Generated with Claude Code

SCOPE_VISITOR gave namespace bodies (TSModuleBlock, also used by
`declare module` and `declare global`) no scope of their own, so their
declarations landed in the scope around them. Where ecij sees
namespaces, that is the modules that plain Rolldown and tsdown hand to
its transform and the modules it reads from disk, this had two effects:
- A namespace's `const css` shadowed the ecij import in the whole
  module, so none of its templates were extracted, without a warning,
  and their runtime `css` calls threw.
- A namespace's bindings leaked into the module scope, overriding the
  module's own bindings of the same name, so a module-level `${accent}`
  could resolve to the namespace's `accent`.

Namespace bodies now have a scope, which `var` declarations hoist to,
as a namespace compiles to a function.

A template that interpolates an export of another block of the same
namespace no longer resolves it. Rolldown's TypeScript transform leaves
such references unresolved too, and Vite, which compiles namespaces
before ecij transforms the module, already skipped these templates.

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

coderabbitai Bot commented Nov 6, 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 3 commits November 6, 2026 22:47
The logs of a plain Rolldown build hold absolute paths, which
buildWithPlugin normalizes for the Vite builds, so its normalization
moves to a normalizeLog helper that both use.

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

Namespaces are not supported: their scope only stops their declarations
from overriding the module's bindings, ecij's css included. The test no
longer covers the templates inside namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien nstepien changed the title Scope the declarations of TypeScript namespaces to their body Keep the declarations of TypeScript namespaces out of the module scope Nov 7, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review November 7, 2026 05:21
@nstepien
nstepien merged commit 253c262 into main Nov 7, 2026
11 checks passed
@nstepien
nstepien deleted the namespace-scope branch November 7, 2026 05:22
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