Repository navigation
Conversation
Resolve interpolation values across modules through every ES module
import/export form:
- default and namespace imports (`import d from`, `import * as ns from`),
including `${ns.member}` and nested `${ns.inner.member}` access
- re-exports (`export { x } from`, default-as-name, name-as-default),
`export * from` aggregations, `export * as ns from`, and transitive
chains through barrel files
- `export default` of css tagged templates and static expressions
(literals, signed numbers, parenthesized/TS-assertion-wrapped forms)
- type-only imports/exports are ignored (statement-level and inline
modifiers, including `export type * from`)
- aliased tags (`import { css as styled } from 'ecij'`), with a scope
check so shadowed bindings are not treated as the tag
Resolution follows ESM semantics: explicit exports shadow `export *`
sources even when not statically resolvable, ambiguous star exports are
not resolved (warn instead of picking a source), and `export *` never
forwards the default export.
Hardening that came out of review:
- a pending-load wait graph prevents build deadlocks on cyclic and
self-referencing barrels while still awaiting concurrent siblings
- external modules in re-export chains are skipped instead of loaded
- class names from skipped extractions no longer leak into consumers
(cross-module and same-file, with forward references still resolving)
- stylesheet dependencies are only recorded for successful resolutions,
so probed-but-failing star sources no longer drag dead CSS in
- watch mode: traversed dependencies are registered via addWatchFile and
evicted from caches on watchChange
Tested with the Vite pipeline and with plain rolldown (which hands raw
TypeScript to the transform), 12 -> 24 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Walkthrough
Pre-merge checks:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
test/fixtures/sibling-tokens.ts (1)
1-1: ⚡ Quick winUse SCREAMING_SNAKE_CASE for module-level constant.
The exported constant
padshould useSCREAMING_SNAKE_CASEper the coding guidelines for this project. As per coding guidelines, "Use SCREAMING_SNAKE_CASE for module-level constants" applies to**/*.{ts,tsx,js}files.♻️ Proposed fix
-export const pad = 4; +export const PAD = 4;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/sibling-tokens.ts` at line 1, Rename the module-level exported constant `pad` to SCREAMING_SNAKE_CASE (`PAD`) and update its export declaration (`export const pad`) to `export const PAD`, then update all usages/imports in the codebase to reference `PAD` instead of `pad` (search for the symbol `pad` to find call sites), ensuring build and tests pass after the rename.Source: Coding guidelines
test/fixtures/self-barrel-tokens.ts (1)
1-1: ⚡ Quick winUse SCREAMING_SNAKE_CASE for module-level constant.
The exported constant
tokenColorshould useSCREAMING_SNAKE_CASEper the coding guidelines for this project. As per coding guidelines, "Use SCREAMING_SNAKE_CASE for module-level constants" applies to**/*.{ts,tsx,js}files.♻️ Proposed fix
-export const tokenColor = 'teal'; +export const TOKEN_COLOR = 'teal';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/self-barrel-tokens.ts` at line 1, The module-level constant tokenColor violates the SCREAMING_SNAKE_CASE rule; rename the exported symbol to TOKEN_COLOR and update every usage/import to match (change export const tokenColor -> export const TOKEN_COLOR and update any references/imports across the codebase, including barrels or tests that import tokenColor). Ensure TypeScript/ES exports and any default/barrel re-exports still resolve after renaming.Source: Coding guidelines
test/fixtures/typed.input.ts (1)
14-20: 💤 Low valueModule-level constant uses camelCase instead of SCREAMING_SNAKE_CASE.
The exported constant
usesTypedToneshould beUSES_TYPED_TONEper the guideline requiring SCREAMING_SNAKE_CASE for module-level constants. However, as with the other fixture files, this is a test fixture where readability and natural naming may take precedence. As per coding guidelines, "Use SCREAMING_SNAKE_CASE for module-level constants" applies to**/*.{ts,tsx,js}files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/typed.input.ts` around lines 14 - 20, The exported module-level constant usesTypedTone violates the SCREAMING_SNAKE_CASE rule; rename the symbol usesTypedTone to USES_TYPED_TONE and update any local references/usages in this fixture so the CSS template string remains the same but the exported identifier uses SCREAMING_SNAKE_CASE (ensure you change the export name wherever usesTypedTone is referenced).Source: Coding guidelines
test/fixtures/typed-decoy.ts (1)
3-3: 💤 Low valueModule-level constant uses camelCase instead of SCREAMING_SNAKE_CASE.
Per coding guidelines,
typedToneshould beTYPED_TONE(SCREAMING_SNAKE_CASE for module-level constants). However, since this is a test fixture where the export name is significant for import resolution testing, and renaming would cascade to all consumers, this may be an acceptable deviation. Consider whether test fixtures should follow relaxed naming rules.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/typed-decoy.ts` at line 3, The module-level constant exported as typedTone violates the SCREAMING_SNAKE_CASE convention; either rename the export to TYPED_TONE to follow guidelines (update all imports/consumers accordingly) or, if this is an intentional test fixture exception, add a brief comment above the export explaining the deliberate deviation so reviewers know it’s intentional; locate the export named typedTone in the fixture and apply one of these two changes consistently.Source: Coding guidelines
test/fixtures/typed-tokens.ts (1)
1-1: 💤 Low valueModule-level constant uses camelCase instead of SCREAMING_SNAKE_CASE.
Per coding guidelines,
typedToneshould beTYPED_TONE. Same consideration as intyped-decoy.ts: this is a test fixture where the export name matters for resolution testing. As per coding guidelines, "Use SCREAMING_SNAKE_CASE for module-level constants" applies to**/*.{ts,tsx,js}files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/typed-tokens.ts` at line 1, The module-level constant is named typedTone but must follow SCREAMING_SNAKE_CASE; rename the export symbol typedTone to TYPED_TONE and update any consumers/tests that import or reference typedTone to use TYPED_TONE instead (match the change made in typed-decoy.ts style); ensure the exported value remains the same ('salmon') and that any resolution tests or fixtures referencing the original symbol are adjusted accordingly.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/index.ts`:
- Around line 1101-1183: The code mutates the shared stylesheetDependencies
while resolving interpolations in processInterpolatedDeclaration, causing
imports to be committed even when a declaration is later skipped or deferred;
fix by creating a per-declaration Set (e.g., localDependencies) inside
processInterpolatedDeclaration and use it for any dependency additions performed
by resolveValue/resolveMemberPath paths instead of mutating
stylesheetDependencies directly, ensure all early returns ('skipped' or
'deferred') do not touch the global set, and after
addProcessedDeclaration(declaration, cssContent) succeeds merge
localDependencies into the global stylesheetDependencies Set.
- Around line 426-438: isCssTagTemplate currently only checks scope.identifiers
and thus misses bindings declared later in the same scope; update it to also
scan the scope's AST block for any declarations that bind the same name
(node.tag.name) even if they haven't been added to scope.identifiers yet.
Specifically, inside isCssTagTemplate (and where you reference
scope.identifiers), inspect scope.block (or scope.node) for VariableDeclarator
ids, FunctionDeclaration ids, ClassDeclaration ids (and similar binding sites)
whose name equals node.tag.name and return false if any are found; keep the
existing parent-scope walk for earlier shadowing as well.
---
Nitpick comments:
In `@test/fixtures/self-barrel-tokens.ts`:
- Line 1: The module-level constant tokenColor violates the SCREAMING_SNAKE_CASE
rule; rename the exported symbol to TOKEN_COLOR and update every usage/import to
match (change export const tokenColor -> export const TOKEN_COLOR and update any
references/imports across the codebase, including barrels or tests that import
tokenColor). Ensure TypeScript/ES exports and any default/barrel re-exports
still resolve after renaming.
In `@test/fixtures/sibling-tokens.ts`:
- Line 1: Rename the module-level exported constant `pad` to
SCREAMING_SNAKE_CASE (`PAD`) and update its export declaration (`export const
pad`) to `export const PAD`, then update all usages/imports in the codebase to
reference `PAD` instead of `pad` (search for the symbol `pad` to find call
sites), ensuring build and tests pass after the rename.
In `@test/fixtures/typed-decoy.ts`:
- Line 3: The module-level constant exported as typedTone violates the
SCREAMING_SNAKE_CASE convention; either rename the export to TYPED_TONE to
follow guidelines (update all imports/consumers accordingly) or, if this is an
intentional test fixture exception, add a brief comment above the export
explaining the deliberate deviation so reviewers know it’s intentional; locate
the export named typedTone in the fixture and apply one of these two changes
consistently.
In `@test/fixtures/typed-tokens.ts`:
- Line 1: The module-level constant is named typedTone but must follow
SCREAMING_SNAKE_CASE; rename the export symbol typedTone to TYPED_TONE and
update any consumers/tests that import or reference typedTone to use TYPED_TONE
instead (match the change made in typed-decoy.ts style); ensure the exported
value remains the same ('salmon') and that any resolution tests or fixtures
referencing the original symbol are adjusted accordingly.
In `@test/fixtures/typed.input.ts`:
- Around line 14-20: The exported module-level constant usesTypedTone violates
the SCREAMING_SNAKE_CASE rule; rename the symbol usesTypedTone to
USES_TYPED_TONE and update any local references/usages in this fixture so the
CSS template string remains the same but the exported identifier uses
SCREAMING_SNAKE_CASE (ensure you change the export name wherever usesTypedTone
is referenced).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e19a3158-9acc-42d8-b238-2df58492bf4b
📒 Files selected for processing (56)
README.mdsrc/index.tstest/fixtures/ambiguous-a.tstest/fixtures/ambiguous-b.tstest/fixtures/ambiguous-barrel.tstest/fixtures/ambiguous.input.tstest/fixtures/barrel-a.tstest/fixtures/barrel-b.tstest/fixtures/barrel-c.tstest/fixtures/barrel-d.tstest/fixtures/barrel-deep-only.input.tstest/fixtures/barrel-nested.tstest/fixtures/barrel.input.tstest/fixtures/barrel.tstest/fixtures/broken-export.input.tstest/fixtures/broken-export.tstest/fixtures/cycle-a.input.tstest/fixtures/cycle-b.tstest/fixtures/default-passthrough-source.tstest/fixtures/default-passthrough.tstest/fixtures/export-default-css.tstest/fixtures/export-default-literal.tstest/fixtures/export-default-local.tstest/fixtures/export-default-negative.tstest/fixtures/export-default-wrapped-css.tstest/fixtures/external-barrel.tstest/fixtures/external-star.input.tstest/fixtures/external-tokens.tstest/fixtures/import-export-hardening.input.tstest/fixtures/import-export.input.tstest/fixtures/named-styles.tstest/fixtures/namespace-edge-cases.input.tstest/fixtures/ns-chain-star.tstest/fixtures/ns-chain.tstest/fixtures/reexports-named.tstest/fixtures/reexports-namespace.tstest/fixtures/reexports-star.tstest/fixtures/same-file-classes.input.tstest/fixtures/self-barrel-tokens.tstest/fixtures/self-barrel.input.tstest/fixtures/self-barrel.tstest/fixtures/sibling-consumer.tstest/fixtures/sibling-producer.tstest/fixtures/sibling-tokens.tstest/fixtures/siblings.input.tstest/fixtures/star-over-default.tstest/fixtures/star-precedence-a.tstest/fixtures/star-precedence.input.tstest/fixtures/star-precedence.tstest/fixtures/tag-alias.input.tstest/fixtures/typed-barrel.tstest/fixtures/typed-decoy.tstest/fixtures/typed-tokens.tstest/fixtures/typed.input.tstest/plugin.test.tsvitest.config.ts
| function isCssTagTemplate(node: TaggedTemplateExpression, scope: Scope): boolean { | ||
| if (!(node.tag.type === 'Identifier' && cssTagNames.has(node.tag.name))) { | ||
| return false; | ||
| } | ||
|
|
||
| // A local binding shadowing the imported tag means this is not the ecij tag | ||
| for (let current: Scope | null = scope; current !== null; current = current.parent) { | ||
| if (current.identifiers.has(node.tag.name)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Shadow checks miss later bindings in the same scope.
isCssTagTemplate() only sees names already inserted into scope.identifiers, but those bindings are recorded when the visitor reaches their declarations later on (for example, Line 565 and Line 672). A later let/const/var/function/class styled = ... still shadows the imported alias from the start of the scope, so styled\...`can be extracted here even though runtime resolution hits a TDZ or hoisted local binding instead ofecij`'s tag.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/index.ts` around lines 426 - 438, isCssTagTemplate currently only checks
scope.identifiers and thus misses bindings declared later in the same scope;
update it to also scan the scope's AST block for any declarations that bind the
same name (node.tag.name) even if they haven't been added to scope.identifiers
yet. Specifically, inside isCssTagTemplate (and where you reference
scope.identifiers), inspect scope.block (or scope.node) for VariableDeclarator
ids, FunctionDeclaration ids, ClassDeclaration ids (and similar binding sites)
whose name equals node.tag.name and return false if any are found; keep the
existing parent-scope walk for earlier shadowing as well.
| async function processInterpolatedDeclaration( | ||
| declaration: Declaration, | ||
| finalAttempt: boolean, | ||
| ): Promise<'extracted' | 'deferred' | 'skipped'> { | ||
| const { quasis, expressions } = declaration.node.quasi; | ||
|
|
||
| let cssContent = ''; | ||
| let allResolved = true; | ||
|
|
||
| for (let i = 0; i < quasis.length; i++) { | ||
| cssContent += quasis[i]!.value.raw; | ||
|
|
||
| if (i < expressions.length) { | ||
| const expression = expressions[i]!; | ||
|
|
||
| let resolvedValue: string | undefined; | ||
|
|
||
| if ( | ||
| expression.type === 'Literal' && | ||
| (typeof expression.value === 'string' || typeof expression.value === 'number') | ||
| ) { | ||
| resolvedValue = String(expression.value); | ||
| } else if ( | ||
| expression.type === 'UnaryExpression' && | ||
| (expression.operator === '-' || expression.operator === '+') && | ||
| expression.argument.type === 'Literal' && | ||
| typeof expression.argument.value === 'number' | ||
| ) { | ||
| resolvedValue = String( | ||
| expression.operator === '-' ? -expression.argument.value : expression.argument.value, | ||
| ); | ||
| } else if (expression.type === 'Identifier') { | ||
| resolvedValue = await resolveValue(expression.name, declaration.scope); | ||
|
|
||
| if (resolvedValue === undefined) { | ||
| // Cannot resolve - skip this entire css`` block | ||
| const expression = unwrapExpression(expressions[i]!); | ||
|
|
||
| let resolvedValue = resolveStaticExpression(expression); | ||
|
|
||
| if (resolvedValue === undefined) { | ||
| const memberPath = | ||
| expression.type === 'MemberExpression' | ||
| ? flattenMemberExpressionPath(expression) | ||
| : undefined; | ||
|
|
||
| if (expression.type === 'Identifier') { | ||
| resolvedValue = await resolveValue(expression.name, declaration.scope); | ||
|
|
||
| // A class name of a same-file declaration that has not been | ||
| // extracted: defer in case it is a forward reference whose | ||
| // declaration is still pending; once no progress can be made | ||
| // it is a failed extraction and must not leak (no rule exists). | ||
| if ( | ||
| resolvedValue !== undefined && | ||
| ownClassNames.has(resolvedValue) && | ||
| !extractedClasses.has(resolvedValue) | ||
| ) { | ||
| if (!finalAttempt) return 'deferred'; | ||
| resolvedValue = undefined; | ||
| } | ||
|
|
||
| if (resolvedValue === undefined) { | ||
| // Cannot resolve - skip this entire css`` block | ||
| context.warn( | ||
| { | ||
| pluginCode: 'UNRESOLVED_INTERPOLATION', | ||
| message: `skipped CSS extraction — could not resolve "${expression.name}" to a static string or number`, | ||
| }, | ||
| expression.start, | ||
| ); | ||
| return 'skipped'; | ||
| } | ||
| } else if (memberPath !== undefined) { | ||
| // Namespace member access: `${ns.foo}` / `${ns.inner.foo}` | ||
| resolvedValue = await resolveMemberPath(memberPath, declaration.scope); | ||
|
|
||
| if (resolvedValue === undefined) { | ||
| context.warn( | ||
| { | ||
| pluginCode: 'UNRESOLVED_INTERPOLATION', | ||
| message: `skipped CSS extraction — could not resolve "${memberPath.join('.')}" to a static string or number`, | ||
| }, | ||
| expression.start, | ||
| ); | ||
| return 'skipped'; | ||
| } | ||
| } else { | ||
| // Complex expression - skip this entire css`` block | ||
| context.warn( | ||
| { | ||
| pluginCode: 'UNRESOLVED_INTERPOLATION', | ||
| message: `skipped CSS extraction — could not resolve "${expression.name}" to a static string or number`, | ||
| pluginCode: 'COMPLEX_INTERPOLATION', | ||
| message: | ||
| 'skipped CSS extraction — interpolation is not a static string, number, or identifier', | ||
| }, | ||
| expression.start, | ||
| ); | ||
| allResolved = false; | ||
| break; | ||
| return 'skipped'; | ||
| } | ||
| } else { | ||
| // Complex expression - skip this entire css`` block | ||
| context.warn( | ||
| { | ||
| pluginCode: 'COMPLEX_INTERPOLATION', | ||
| message: | ||
| 'skipped CSS extraction — interpolation is not a static string, number, or identifier', | ||
| }, | ||
| expression.start, | ||
| ); | ||
| allResolved = false; | ||
| break; | ||
| } | ||
|
|
||
| cssContent += resolvedValue; | ||
| } | ||
| } | ||
|
|
||
| // Only process if all interpolations were resolved | ||
| if (allResolved) { | ||
| addProcessedDeclaration(declaration, cssContent); | ||
| addProcessedDeclaration(declaration, cssContent); | ||
| return 'extracted'; |
There was a problem hiding this comment.
Only commit stylesheet dependencies after the declaration is emitted.
This path evaluates interpolations against the shared stylesheetDependencies set as it goes. If an early interpolation resolves through resolveExportValue()/resolveMemberPath() but a later one returns 'skipped' or 'deferred', the dependency import remains even though this declaration never reaches addProcessedDeclaration(). That can pull unrelated CSS into the final output. Keep a per-declaration dependency set and merge it only after extraction succeeds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/index.ts` around lines 1101 - 1183, The code mutates the shared
stylesheetDependencies while resolving interpolations in
processInterpolatedDeclaration, causing imports to be committed even when a
declaration is later skipped or deferred; fix by creating a per-declaration Set
(e.g., localDependencies) inside processInterpolatedDeclaration and use it for
any dependency additions performed by resolveValue/resolveMemberPath paths
instead of mutating stylesheetDependencies directly, ensure all early returns
('skipped' or 'deferred') do not touch the global set, and after
addProcessedDeclaration(declaration, cssContent) succeeds merge
localDependencies into the global stylesheetDependencies Set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Decide css tag shadowing after the visit, once every scope is fully populated: `let`/`const`/`class` bindings are hoisted to the top of their block (TDZ) and `var`/function declarations to their function scope, so a local binding declared *after* a css`` template still shadows the imported tag. Such templates were wrongly extracted; they are now left untouched. - Track stylesheet dependencies per declaration and only commit them once the declaration is extracted, so a skipped declaration no longer registers the stylesheets of modules it merely probed. - Hoist `isBoundInScopeChain` to module level and reuse it for both checks. - Document that test fixtures mirror user code and are exempt from the naming conventions (the camelCase fixture-export nitpicks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `var x = css`${y}`` inside a block used the function scope (where the
`var` binding lands) to resolve its interpolations, so a block-scoped `y`
shadowing an outer one resolved to the outer value, a block-only `y` was
unresolvable, and a loop variable could resolve to an outer binding of the
same name. Interpolations (and the tag) are now resolved in the scope the
template appears in; only the binding of `x` stays function-scoped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the aliased-tag fixture/test: the tag-shadowing fixture imports the tag as `css as styled` and extracts a module-level template the same way. - Drop the type-only section of import-export.input.ts: the plain-rolldown typed test covers the same fixtures and assertion, and is the one that exercises the plugin's own `isType` handling (Vite strips types first). - Fold the var-in-block cases into scoping-advanced.input.ts (sections 32-33), next to the existing var-hoisting cases. - Replace scoping-helper.ts with the equivalent named-styles.ts in the scoping and dependency-skipped fixtures. - Drop basic.input.ts (a copy of comprehensive's first block); the classPrefix test builds the identical.input.ts entry instead. 28 -> 26 tests, 70 -> 66 fixtures; line/branch coverage of src/index.ts is unchanged (verified against the pre-change lcov). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop `multiShadow`/`afterMultiShadow` (section 1 with three variables) and `deeplyNested` (sections 2-4 combined) from scoping.input.ts: neither adds a code path or observable beyond the cases it recombines. Line/branch coverage of src/index.ts is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Track `VariableDeclaration` kinds as a stack: a declarator following a sibling whose initializer contains nested declarations (e.g. a function body) lost its `var` kind and was bound to the block instead of the function scope, so a later interpolation resolved an outer binding. - README: add a "Static resolution" section describing what the plugin resolves (literals, scoped bindings, imports through re-exports and barrels with ESM precedence) and how failures surface; clarify that the tag must be a named import. - AGENTS.md: describe all five hooks (`watchChange` is new in this PR) and the cross-module resolution/caching machinery in the transform hook. - Rename the dependency loop variable that shadowed the hook's `id`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`let`/`var` bindings with a literal initializer were resolved to that literal even when reassigned later (`let color = 'red'; color = 'blue'`), baking the wrong value into the CSS. Assignment targets (`x = …`, `x++`, `for (x of …)`, destructuring assignments) are now collected during the visit and, once every scope is populated, mark their binding — and any export of it — as unresolvable. `<T>expr` type assertions are unwrapped like the other TypeScript wrappers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`resolveImportedFile` ran `context.resolve` + `context.load` for every interpolation from the same module, and again on every deferred retry. Memoize the resolved path per (importer, source) for the duration of the transform; parse info and extracted classes are read live regardless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the array/object pattern, default, rest and TypeScript assertion branches of the reassignment walker with one template per target kind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vite strips `(x as T) = …` before plugin transforms run, so the plain rolldown test is the one place the assertion branch of the reassignment walker is reachable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fixes Findings from an independent review of src/index.ts, each reproduced before fixing: - Vite dev: resolved ids carry no `external` flag, so `external !== false` treated every import as external and no cross-module interpolation ever resolved; `ModuleInfo.code` throws in dev; and `this.load` does not run transforms there. Only a set `external` flag now counts, the module-graph code read is guarded, and in a dev environment the imported module is transformed through `environment.transformRequest`. Covered by a new dev-server test. - Query-suffixed imports (`?raw`, `?url`, …) were resolved as the plain file and inlined its default export; ids with anything but Vite's marker queries are now treated as unresolvable. - A redeclared `var` with an initializer simply overwrote the recorded value, so all uses saw the last initializer; it is now a reassignment. - Injected imports were prepended before a hashbang (a parse error in both Rolldown and Vite builds) or ahead of `'use client'`; they are inserted after the prologue. - Function bodies shared the parameter scope, so parameter defaults could see (and be shadowed by) body declarations; bodies get their own scope. - In import cycles, extraction depended on declaration order; declarations whose interpolations need no import are now extracted before any import is followed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…king
Findings from the max-effort code review and a second independent review,
each reproduced before fixing:
- A `?raw`/`?url` variant of a module was transformed under the plain
module's id and overwrote its cached parse info and classes; query-variant
modules are now skipped.
- Under `"sideEffects": false` a module whose exports were all inlined was
dropped together with its injected stylesheet imports, losing the rules
its own CSS relies on; the transform now returns `moduleSideEffects: true`
when it injects imports (the never-reached `resolveId` branch is gone).
- An ambiguous `export *` name reached through another `export *` shared its
origin key with a value and was resolved; ambiguity is its own result kind
and propagates.
- Loop and switch heads shared the body scope, so a body-level `let` could
swallow a write in the head; bodies get their own scope, head targets are
recorded in the enclosing one, and the switch scope starts with the first
case.
- Function bodies are function scopes of their own: body `var`s no longer
hoist next to the parameters (parameter defaults saw them), and a body
`var` named like a parameter counts as a write to it.
- `export default x` before `var x` snapshots `undefined`, not the later
initializer; such defaults are unresolvable.
- Vite dev: module code is read from the environment's module graph (also
for `\0` virtual modules) and an unreadable module is no longer cached as
empty for the build; it is reported once with `UNREADABLE_MODULE`.
- Sources are parsed as the language Rolldown assigned (`meta.moduleType`),
so a `.js` module declared as JSX is no longer parsed as plain JS; parse
failures are reported with `UNPARSEABLE_MODULE`.
- Raw TypeScript: `enum`, `namespace` (with its own scope) and `import x =`
now bind their names.
- A module's own class reached back through a barrel goes through the same
deferral as a direct forward reference.
- Dependencies are recorded as module paths and mapped to stylesheets when
imports are emitted; a module still in flight is imported itself, so
output no longer depends on scheduling.
- Cleanups: one pattern walker for bindings and assignment targets, one
resolution path for `${name}` and `${ns.member}`, namespace hops share a
helper, and a retry round of skips no longer counts as progress.
Documented: cycle ordering and cross-plugin `this.load` limitations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Findings from a third independent review of the previous round: - Regression: gating the transform on Vite's marker-query allowlist skipped source modules whose id carries a query, e.g. an HTML page's inline `<script type="module">` (`index.html?html-proxy&index=0.js`), worker entries and framework sub-modules, leaving their templates untransformed. A module's identity is now its id minus the marker queries: query variants (`?raw`, `?url`, inline scripts, …) keep their own parse info, classes and stylesheets instead of being skipped or clobbering the plain module, each inline script gets its own class hashes, and values imported with a query resolve from the variant's own code (`?raw` yields the file's text, `?url` its asset URL). `watchChange` evicts the variants of a changed file too. Covered by an HTML-entry build test. - A `var` is `undefined` until its declaration has run, yet templates placed before it, or after a conditional/loop/try initialization, inlined the initializer. Such bindings are now non-static: a `var` is only used by templates after its unconditional declaration. (`const`/class forward references stay supported — those would throw at runtime, so static evaluation is the only meaningful outcome.) - Test for the raw-TypeScript `import x = …` handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`let`/`var` bindings with a literal initializer were resolved like `const` ones since PR #14, which required reassignment tracking and, for `var`, position and control-flow analysis to avoid inlining a value the runtime never had — and still left gaps (hoisted callees, labeled blocks, Annex B catch parameters). A static style token is idiomatically a `const`, and a `const` has clean semantics: one assignment, TDZ before it (which the plugin deliberately resolves, as for same-file forward references). Only `const` declarators now record a value; `let`/`var` bindings (and their exports) are recorded as unknown, so they still shadow outer bindings and imports — including `var` hoisting to the function scope — but a template using one is skipped with UNRESOLVED_INTERPOLATION. A css`` template assigned to a `let`/`var` is still extracted; only references to it need a `const`. Removed with it: reassignment tracking, redeclared/parameter-shadowing `var` writes, conditional-depth and declaration-position bookkeeping, forward `export default` handling, and the fixtures that existed only to exercise those semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary First slice split out of #68 so it can be reviewed on its own: the static-resolution and scoping semantics of `parseFile`, with none of the import/export machinery. **Only `const` bindings have a static value.** `let`/`var` bindings, parameters, and destructured bindings are still recorded so they shadow outer bindings and imports (including `var` hoisting to the function scope), but a template using one is skipped with `UNRESOLVED_INTERPOLATION`. A `css` template assigned to a `let`/`var` is still extracted. Only references to it need a `const`. `parseFile` records the initializer of each `const`, and `resolveValue` evaluates it on demand, like an interpolation: a literal, signed numbers included (`const offset = -5`), or another binding that resolves (`const accent = brand`), class names and imports included. An imported binding is resolved in the module that declares it, so an exported alias resolves for its importers too. The values are cached for the transform, unknown ones included, and a binding is cached as unknown while it is resolved, so a cycle stays unknown. Resolution is static, so a template or a `const` may reference a `const` declared later in the file (a TDZ error at runtime). The plugin keeps resolving it on purpose, but the README does not document it, so users do not rely on it. Re-exported imports (`export { x }` of an import, and `export … from`) are still not followed. Resolving `let`/`var` values would require reassignment tracking and, for `var`, position and control-flow analysis to avoid inlining a value the runtime never had (hoisted callees, conditional initialization, `break label`, Annex B `catch`). Static style tokens are idiomatically `const` anyway. **Tag shadowing.** A local binding named like the tag means the template is not ecij's `css` and is left alone. Templates are collected during the visit and decided once every scope is populated, so bindings declared *after* the template count too (`let`/`const`/`class` are hoisted to their block, `var`/functions to their function). The tag is recognized through its import binding (`cssTagNames`), so `import { css as styled } from 'ecij'` works as a by-product. **Scopes.** - Function bodies are scopes of their own: parameter defaults cannot see the body's declarations, and body `var`s hoist into the body rather than next to the parameters - Loop and catch bodies are nested in the scope of the loop head or the catch parameter instead of sharing it, so a body-level declaration cannot leak into the loop head - The switch scope is entered with the first case, so the discriminant is evaluated in the enclosing scope - The `var` declarator kind is tracked as a stack: a declarator following a sibling whose initializer contains nested declarations, like a function body, lost its kind, so a `var` was bound to the block (and a `const` would no longer resolve) - A template's interpolations are resolved in the scope where the template appears (previously a `var`-declared template used the function scope, so a block-scoped binding shadowing an outer one resolved to the outer value) Docs: the README gains a "Static resolution" section, with an example of the supported interpolations, and AGENTS.md's transform description matches. AGENTS.md also records why the `const` cache only lives for one transform. **Behavior changes vs `main`** - `let`/`var` bindings with a literal initializer no longer resolve and warn instead - `const` bindings of signed numbers now resolve - A `const` bound to another binding that resolves, local or imported, now resolves too, also for the modules importing it - A binding exported under several names now resolves under each of them - Templates whose tag is shadowed by a local binding are no longer extracted - Aliased tag imports are now supported ## Test plan - `variable scoping` and `advanced scoping` snapshots updated for the `const`-only rule (§7 `var`, §11 for-head `let`, and §23 `var` in a block now warn). New advanced sections 32–36: `var` declarator kind stack, `let` not resolved, parameter defaults vs. body `const`/`var`, and `var`-declared templates resolving where they appear - The barrel test re-exports `scoping.input.ts`, so it now records that fixture's `var` warning - New `tag-shadow-hoisting` fixture and test: later `const`/`var`/`function`/`class`, self-reference, enclosing-scope declaration, nested-block negative control, and module level. Only the two unshadowed templates are extracted - `literal-interpolation` gains a `const` bound to a signed number - New `const-alias` fixture and test: aliases of literals and class names, an alias of a shadowing binding, a chain declared after its use, an alias of an import, and exported aliases of a literal, a class name, and an import resolve. Aliases of a `let` and a cycle warn - Vite builds of scratch projects: a `const` cycle across two modules and a re-export cycle terminate unresolved, a chain of aliases across three modules resolves with the stylesheets in dependency order, modules interpolating each other's aliased class names build, and a binding exported under two names resolves under both - `node --run test` (locally and with `CI=true` against `dist/`), `lint`, `typecheck`, `format:check`, and `build` pass. `src/index.ts` statement coverage is 99.6%, and its one uncovered line (TypeScript parameter properties) is uncovered on `main` too ## Relationship to #68 The code here started out textually identical to the corresponding regions of #68. Merging `main` into it (IIFE class names, hook filters, cycle handling, and the dev server updates) and the review simplifications since then mean they no longer match, so merging `main` back into #68 afterwards will need conflict resolution in these regions too. Candidate next slices: - Raw TypeScript handling (`enum`/`namespace`/`import =`, `moduleType`-aware parsing) - Prologue-safe import injection (#68's `moduleSideEffects` is no longer needed: `main` keeps such modules alive with IIFE class names) - The rest of the import/export resolution core: re-exports, and default and namespace imports and exports - Dev-server resolution and module identity for query variants 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
main took in the first slice of this PR (#79) and solved several of its other problems differently since August, so the merge ports the rest of this branch onto main's design instead of keeping both: - Values, class names included, come from the parse of each module, and const initializers resolve lazily (#79). Re-exports, `export *`, default and namespace imports, and `export default` resolve through the same evaluator, with ESM precedence and ambiguity. - Cycles are handled by main's pendingLoads and isWaitingFor (#102), so the wait graph, the deferred same-file passes, and the import-last ordering are gone, along with the guard against inlining the class names of skipped templates, which needed the imported modules' transforms. - Class names are IIFEs (#87, #98), so `moduleSideEffects: true` is gone. - Vite's dev server keeps main's behavior (#104) and does not transform imported modules through transformRequest. There, the code of modules that no file holds comes from their `load` hooks. - UNREADABLE_MODULE and UNPARSEABLE_MODULE are gone: a module whose code cannot be read exports nothing, and modules are parsed as the language Rolldown assigned them. - Kept: raw TypeScript bindings, `meta.moduleType` parsing, prologue-safe imports, query-variant module identity, skipping external modules, and resolving each import once per transform. Tests: main's suite, plus this branch's tests of the kept features, with new cases for the paths that no fixture reached. The tests of the removed behavior, and the cycle test that main already has, are dropped. basic.input.ts is restored for main's inlineConst tests, and this branch's barrel.ts is now barrel-index.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow ran `npm version` itself but could only push the tag, as the default branch ruleset blocks its push to `main`, so the version commit never landed on `main`. Now `npm version` runs locally, and pushing the version commit and its tag runs the workflow. ## How to release On an up-to-date `main`: ```sh npm version <patch|minor|major|x.y.z> git push --atomic --follow-tags origin main ``` `npm version` commits the new version and creates an annotated `vX.Y.Z` tag. `--follow-tags` pushes that tag along with `main`, and `--atomic` makes the push all or nothing. As an admin, you bypass the default branch ruleset for the push to `main`. The same steps are documented at the top of `release.yml`. ## Changes - The workflow runs on `push` of `v*` tags instead of `workflow_dispatch`. - The `build` job checks that the tagged commit is on `main` (replacing the `github.ref` guard) and that the tag matches `package.json`'s version, before installing anything. Then it runs the same checks and build as before. - The `release` job no longer configures git, runs `npm version`, or pushes the tag. It publishes `dist` and creates the GitHub release for the existing tag with `--verify-tag`, so `contents: write` is now only used for the release. - AGENTS.md points to the workflow's header for the release steps. The workflow file name is unchanged, so npm's trusted publisher config still matches. ## Verification - With a local bare remote, I confirmed that `npm version minor` creates an annotated tag, and that `git push --atomic --follow-tags origin main` pushes both refs, or neither when the remote rejects `main`. - The ancestry check passes for `v0.5.0`'s commit and fails for #68's head. The version check passes for `v0.5.0` and fails for `v0.5.1` on the current `package.json`. - zizmor reports nothing with the regular persona. actionlint only flags `cache-mode`, which it doesn't know yet. ## Suggested follow-up Now that the workflow no longer creates tags, enable **Restrict creations** in the `Release tags` ruleset. With the admin bypass, only admins can then create `v*` tags, which means only admins can start a release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When a template interpolates values imported from another module, `resolveValue` resolved, loaded, and parsed that module again for every value, calling `context.resolve` and `context.load` each time. A transform now does this once per imported module, however many values it interpolates from it. The modules are keyed by importer and import source, as values can also come through the imports of an imported module. **Benchmarks** Medians with Vite 8.3.5, main against this branch. The JS, CSS, and final source maps are identical. | Workload | main | This branch | |---|---|---| | Vite build of a synthetic project: 40 components × 8 templates × 5 tokens from one `tokens.ts` | 54.2 ms | 32.8 ms (−39%) | | Cold dev server transforms of those 40 components | 71.1 ms | 39.8 ms (−44%) | | Vite build of react-data-grid's `src`, with 9 interpolations in 15 modules | 21.9 ms | 21.8 ms | A transform now records each dependency and checks `isWaitingFor` once per imported module instead of once per value. The uncovered lines are unchanged: the tests reuse a loaded module 15 times. An import that can't be resolved stays untested, as on main. #68 includes the same change (`importedFiles`), so whichever lands second needs to account for it. `AGENTS.md` now says that a transform handles each imported module once. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ecij parsed the module it transforms as the language its file extension
implies, ignoring the module type the bundler assigned it. Rolldown
parses a `.js` file with JSX as JSX when configured with `moduleTypes: {
'.js': 'jsx' }`, but ecij failed to parse it and silently left its
templates untransformed. The output kept the raw `css` call, which
throws at runtime, and no CSS or warning came out.
- `transform` now parses the module as `meta.moduleType` when that is a
language the parser accepts (`js`, `jsx`, `ts`, or `tsx`), and otherwise
falls back to the file extension. The modules that interpolated values
come from are still read from disk and parsed by their extension.
- A new test builds a `.js` fixture with JSX through plain Rolldown with
`moduleTypes: { '.js': 'jsx' }`. On main, its output keeps the `css`
template and no stylesheet is extracted.
- `AGENTS.md` describes how the module is parsed.
Vite users see no change. Vite compiles TypeScript and JSX to JavaScript
before ecij runs, and passes `moduleType: 'js'` along with that code,
both in builds and in the dev server. A Vite build of react-data-grid's
`src` is byte-for-byte identical to main's, source maps included. The
fix is for Rolldown and tsdown users with custom `moduleTypes`.
This is ported from #68, which passes `meta.moduleType` to the parser
the same way.
The uncovered lines are unchanged. The fallback for a module type that
isn't a language, like `json`, is the one new branch the tests don't
reach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
main reshaped the code this branch builds on since the last merge, so the merge ports the branch onto main's design again: - The scope visitor is shared across parses (#109), so the callbacks for raw TypeScript bindings and `export default` join it, and the default export is recorded through its visit state. - Imported modules are resolved, loaded, and parsed once per transform through main's loadImportedModule (#124), which now also skips external modules, keys modules by moduleKey, and records dependencies by file. - Modules are parsed as the language the bundler assigned them through main's parseLangOf (#125), which this branch had too. - extractCssFromCode keeps main's single pass (#114), and hotUpdate its early return, with this branch's handling of query variants. - readModuleCode moved to module level, as consistent-function-scoping is on since #109. Tests: the inline snapshots of this branch's tests follow Rolldown's new module comments (`// path` instead of `//#region path` and `//#endregion`), with no other change. The dev server test of virtual modules uses main's shared getCode and getStylesheets helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first commit of this branch turned it off along with the import and export work, unlike main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Summary
Removes the last big TODO: interpolations resolve across modules through every ES module import and export form. Query variants and modules without a file get their own identity, and raw TypeScript is handled. The static resolution model this builds on (
const-only, lazily resolved values, scoping) landed inmainthrough #79, which started out as the first slice of this PR.Supported import and export forms
import d from,import * as ns from), including${ns.member}and nested${ns.inner.member}export { x } from,export { default as x } from,export { x as default } from,export * from(exceptdefault),export * as ns from, and chains through barrel files. This includes the export of an imported binding, likeimport d from 'mod'; export { d };, which re-exports it, as in the specexport defaultof css templates and of static expressions (literals, signed numbers, and their parenthesized and type-asserted forms)?rawis the file's text, and?urlits URLResolution semantics
main'sconstbindings: on demand, cached for the transformexport *, even when their value is not staticexport *provide through different bindings is ambiguous and not resolved, and the ambiguity propagates through further hops. Two paths to the same binding are fineexport *never providesdefaultexport *sources that do not provide a name never imports theirsRaw TypeScript (plain Rolldown): an
enumbinds its name, and ambient enums bind nothing. TypeScript namespaces are not supported, beyond the scope of their own thatmaingives them (#130).Module identity: a module's identity is its id without Vite's marker queries (
?v=,?t=, …). Query variants (?raw,?url, an HTML page's inline<script type="module">served asindex.html?html-proxy&index=N.js, framework sub-modules) keep their own parse, class names, class hashes, and stylesheet. Onmain, a?rawrequest of a module replaces the module's parse.watchChangeforgets the variants of an edited file too. Modules that no file holds, like virtual modules and query variants, are read from the module graph, or in Vite's dev server, which does not keep the code of the modules loaded throughthis.load(), through theirloadhooks.Output: stylesheet imports are injected after a hashbang and the directive prologue (
'use client'), instead of before them, which is a parse error onmain.Behavior changes vs
mainexport … from,export *, andexport * as nsresolve, with ESM precedence and ambiguity'blue' as const${theme.color}, warns withUNRESOLVED_INTERPOLATIONand its path, instead ofCOMPLEX_INTERPOLATIONMerging
main(2026-10-17)mainsolved several of this PR's problems differently since August, so the merge ported the rest of this branch ontomain's design instead of keeping both:constresolution (Resolve only const bindings; scope templates and tags correctly #79) replaces the eager evaluation, and the export records became references to bindingspendingLoadsandisWaitingFor(Stop builds from hanging on mutually interpolated class names #102) replace the wait graph, the deferred same-file passes, and the import-last orderinginlineConst#87, Bump the dependencies group with 7 updates #98) replacemoduleSideEffects: true, which did not help barrels in the measurements for Bump the dependencies group with 7 updates #98main's behavior (Update the CSS of modules that depend on edited modules in the Vite dev server #104) and does not transform imported modules throughenvironment.transformRequestDropped with them:
csscall of such a template still throwsmainUNREADABLE_MODULE, as a module whose code cannot be read now exports nothing, andUNPARSEABLE_MODULE, as modules are parsed as the language Rolldown assigned themKnown limitations
csstag must be a named import from'ecij'. A namespace import or a re-export of the tag leaves its templates untransformedthis.loadcannot be detected and would hang the buildMerging
main(2026-11-04)mainreshaped the code this PR builds on since the previous merge, so the merge ports it ontomain's design again:export defaultjoin it, and the default export is recorded through its visit statemain'sloadImportedModule(Resolve and load each imported module once per transform #124), which this PR extends to skip external modules, key modules bymoduleKey, and record dependencies by filemain'sparseLangOf(Parse modules as the language the bundler assigned them #125), which came from this PRextractCssFromCodekeepsmain's single pass (Do the plugin's work in fewer passes #114), andhotUpdateits early return, with this PR's handling of query variantsreadModuleCodemoved to module level, asconsistent-function-scopingis on since Share one scope visitor across parses to stop leaking memory #109The inline snapshots of this PR's tests follow Rolldown's new module comments (
// pathinstead of//#region pathand//#endregion), with no other change. Vite builds of react-data-grid'ssrcand of a token-heavy project are byte-for-byte identical tomain's, source maps included.Merging
main(2026-11-07)mainreworked the import handling this PR builds on, so the merge ports it ontomain's design once more:importKindandexportKind, and exported functions, classes, enums, and destructured bindings are recorded, so that they take precedence overexport *NamespaceImportbindings, and an exported import re-exports it, as in the spec, so that twoexport *that provide the same binding through different modules are not ambiguousexport defaultbinds*default*like onmain(Bind imports in the module-level scope, and resolve default imports #131), and its templates get nodefaultname, so their class names hash likemain'sresolveExportreturns the module-level binding an export comes from, whichmain'sresolveBindingresolves and caches (Resolve values without promises unless an import has to be loaded #133), and the value imports the stylesheet of the module that declares itmain's cache of pending parses (Share pending parses of imported modules between transforms #128), with this PR'sreadModuleCodefor the modules that no file holdsimport x = …, and the raw TypeScript fixture dropped its namespace cases. Enums still shadowmain's import-forms test now extracts the namespace member and theexport … fromre-export it skipped. New fixture cases cover the export of a namespace import and a member of a local object. Vite builds of react-data-grid'ssrcand of two token-heavy projects are byte-for-byte identical tomain's, source maps included. The 400-component token project builds in 182 ms against 172 ms onmain(+6%), as each imported value goes throughresolveExport.Test plan
main's, this PR's tests of the kept features, and a Vite dev server test of a virtual module and a?rawvariant. The kept tests cover the default, namespace, and re-export matrix, barrels with explicit-over-star precedence, namespace edge cases, a self-referencing barrel, external modules, query variants, explicit exports shadowingexport *, ambiguous names, raw TypeScript through plain Rolldown, concurrent siblings, failedexport *probes, an HTML entry's inline script, and the prologuemainalready has, are removed.basic.input.tsis restored formain'sinlineConsttests, and this PR'sbarrel.tsis nowbarrel-index.tsnode --run test(locally and withCI=trueagainstdist/),lint,typecheck,format:check, andbuildpass.src/index.tsstatement coverage is 99.7%, and its one uncovered line (TypeScript parameter properties) is uncovered onmaintooconstcycle across two modules and a re-export cycle terminate unresolved, a chain of aliases across three modules resolves with the stylesheets in dependency order, modules interpolating each other's aliased class names build, and a binding exported under two names resolves under both🤖 Generated with Claude Code
Summary by CodeRabbit
Documentation
New Features
Bug Fixes