Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The plugin implements three Rolldown lifecycle hooks:

`parseFile` walks every program with a single module-level `Visitor` (`visitProgram`), whose callbacks keep the state of the visit in module-level variables, which is safe as a visit is synchronous. Do not create a `Visitor` per file: each instance keeps its callbacks alive for the rest of the process, along with everything they capture.

When the CSS interpolates values imported from another module, `transform` reads them from that module's parse, which `parseFile` caches. `const` initializers are resolved on demand, following the imports of their module, and cached for one transform only: the dev server keeps the parse of a module across edits to the modules it imports, so a longer-lived cache would go stale. It also waits for `this.load()` of that module, whose own transform registers its stylesheet, and imports that stylesheet first to keep the cascade order. Modules that interpolate each other's class names would wait for each other forever, so it skips loading a module whose transform is already waiting for the current one (`isWaitingFor`), and whose stylesheet is therefore not registered yet. Vite's dev server does not transform modules loaded through `this.load()`, so there a stylesheet is only imported once its module has been transformed. Do not work around this: the order only decides specificity ties, the README does not promise it, and Vite does not keep CSS in the same order between dev and builds either.
When the CSS interpolates values imported from another module, `transform` reads them from that module's parse, which `parseFile` caches. `const` initializers are resolved on demand, following the imports of their module, and cached for one transform only: the dev server keeps the parse of a module across edits to the modules it imports, so a longer-lived cache would go stale. It resolves, loads, and parses each imported module once per transform, however many values it interpolates from it. It also waits for `this.load()` of that module, whose own transform registers its stylesheet, and imports that stylesheet first to keep the cascade order. Modules that interpolate each other's class names would wait for each other forever, so it skips loading a module whose transform is already waiting for the current one (`isWaitingFor`), and whose stylesheet is therefore not registered yet. Vite's dev server does not transform modules loaded through `this.load()`, so there a stylesheet is only imported once its module has been transformed. Do not work around this: the order only decides specificity ties, the README does not promise it, and Vite does not keep CSS in the same order between dev and builds either.

The caches are cleared in `buildEnd`, which the dev server only calls when it closes. There, `watchChange` forgets the parse of an edited module and the stylesheets that change with it, and the Vite-only `hotUpdate` hook adds the modules whose CSS depends on it to the update, as Vite would otherwise reuse their transforms and only update their imports. `transform` records these dependencies in `dependentsPerFile` as it resolves imported values.

Expand Down
88 changes: 55 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,58 @@ export function ecij(configuration?: Configuration | null): Plugin {
// is not shared with other transforms, as values read through imports can change in between.
const constValues = new Map<Expression, string | undefined>();

// The modules that interpolated values are imported from, keyed by importer and import
// source, so that each one is resolved, loaded, and parsed once per transform
const importedModules = new Map<string, ReturnType<typeof resolveImportedModule>>();

function loadImportedModule(source: string, importer: string) {
const key = `${importer}\0${source}`;
let importedModule = importedModules.get(key);

if (importedModule === undefined) {
importedModule = resolveImportedModule(source, importer);
importedModules.set(key, importedModule);
}

return importedModule;
}

async function resolveImportedModule(source: string, importer: string) {
// Resolve the import path relative to the importer
const resolvedId = await context.resolve(source, importer);
if (resolvedId == null) return;

const { id } = resolvedId;

// Record the dependency so that editing the imported module updates the transformed one
let dependents = dependentsPerFile.get(id);

if (dependents === undefined) {
dependents = new Set();
dependentsPerFile.set(id, dependents);
}

dependents.add(filePath);

// Wait for the imported module's transform, which registers the stylesheet that
// `resolveValue` imports. Vite's dev server does not transform modules loaded this way,
// so there the stylesheet is only imported once that module has been transformed.
// Loading a module whose transform waits for this one would never finish, which
// happens when modules interpolate each other's class names. Its stylesheet is then
// not registered yet, but values come from the module's parse either way.
if (!isWaitingFor(id, filePath)) {
pendingLoads.set(filePath, id);

try {
await context.load(resolvedId);
} finally {
pendingLoads.delete(filePath);
}
}

return { id, importedFileInfo: await parseFile(context, id) };
}

// Statically evaluates an expression of a file, like an interpolation or the initializer of a
// `const`, to its string value: a string or number literal, including signed numbers (`-5`,
// `+5`), or an identifier bound to such a value or to a class name, in the file or imported.
Expand Down Expand Up @@ -617,40 +669,10 @@ export function ecij(configuration?: Configuration | null): Plugin {

if (importedIdentifier !== undefined) {
const { source, imported } = importedIdentifier;
const loadedModule = await loadImportedModule(source, fileInfo.filePath);

// Resolve the import path relative to the importer
const resolvedId = await context.resolve(source, fileInfo.filePath);

if (resolvedId != null) {
const { id } = resolvedId;

// Record the dependency so that editing the imported module updates the transformed one
let dependents = dependentsPerFile.get(id);

if (dependents === undefined) {
dependents = new Set();
dependentsPerFile.set(id, dependents);
}

dependents.add(filePath);

// Wait for the imported module's transform, which registers the stylesheet imported
// below. Vite's dev server does not transform modules loaded this way, so there the
// stylesheet is only imported once that module has been transformed.
// Loading a module whose transform waits for this one would never finish, which
// happens when modules interpolate each other's class names. Its stylesheet is then
// not registered yet, but values come from the module's parse either way.
if (!isWaitingFor(id, filePath)) {
pendingLoads.set(filePath, id);

try {
await context.load(resolvedId);
} finally {
pendingLoads.delete(filePath);
}
}

const importedFileInfo = await parseFile(context, id);
if (loadedModule !== undefined) {
const { id, importedFileInfo } = loadedModule;
const localName = importedFileInfo.exportedLocalNames.get(imported);

// Only bindings declared in the module are resolved, not the imports it exports
Expand Down
Loading