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
61 changes: 49 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function ecij({
declarations.push({
className,
node,
hasInterpolations: node.quasi.expressions.length > 0,
hasInterpolations: node.quasi.expressions.length !== 0,
});

// Record generated class names for css declarations
Expand Down Expand Up @@ -241,6 +241,7 @@ export function ecij({
transformedCode: string;
hasExtractions: boolean;
cssContent: string;
modulesWithSideEffects: Set<string>;
}> {
const { declarations, localIdentifiers, importedIdentifiers } =
await parseFile(context, filePath, code);
Expand All @@ -255,6 +256,7 @@ export function ecij({
end: number;
className: string;
}> = [];
const modulesWithSideEffects = new Set<string>();

// Helper to resolve a value from an identifier
async function resolveValue(
Expand All @@ -273,12 +275,19 @@ export function ecij({
const resolvedId = await context.resolve(source, filePath);

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

const { declarations, exportNameToValueMap } = await parseFile(
context,
resolvedId.id,
id,
);

return exportNameToValueMap.get(imported);
if (exportNameToValueMap.has(imported)) {
if (declarations.length !== 0) {
modulesWithSideEffects.add(id);
}
return exportNameToValueMap.get(imported)!;
}
}
}

Expand Down Expand Up @@ -359,6 +368,7 @@ export function ecij({
transformedCode: code,
hasExtractions: false,
cssContent: '',
modulesWithSideEffects,
};
}

Expand Down Expand Up @@ -391,6 +401,7 @@ export function ecij({
transformedCode,
hasExtractions: true,
cssContent,
modulesWithSideEffects,
};
}

Expand All @@ -404,11 +415,20 @@ export function ecij({
},

resolveId(id) {
// Intercept ecij imports to prevent Vite from trying to resolve them
// They will be removed during transformation
// Ensure CSS modules are treated as having side effects
if (extractedCssPerFile.has(id)) {
return { id };
return id;
}

// Ensure JS modules with CSS extractions are included,
// otherwise they may be tree-shaken away if
// all their exports are evaluated away
if (parsedFileInfoCache.has(id)) {
if (parsedFileInfoCache.get(id)!.declarations.length !== 0) {
return id;
}
}

return null;
},

Expand All @@ -417,6 +437,7 @@ export function ecij({
if (extractedCssPerFile.has(id)) {
return extractedCssPerFile.get(id)!;
}

return null;
},

Expand All @@ -438,8 +459,12 @@ export function ecij({
const cleanId = queryIndex === -1 ? id : id.slice(0, queryIndex);

// Extract CSS from the code
const { transformedCode, hasExtractions, cssContent } =
await extractCssFromCode(this, code, cleanId);
const {
transformedCode,
hasExtractions,
cssContent,
modulesWithSideEffects,
} = await extractCssFromCode(this, code, cleanId);

if (!hasExtractions) {
return null;
Expand All @@ -460,12 +485,24 @@ export function ecij({
// Store the CSS extractions for this file
extractedCssPerFile.set(cssModuleId, cssContent);

const importStatements: string[] = [];

// Include side-effect imports for modules from which class names were imported.
// Otherwise, the original imports may be treated as being free of side-effects,
// leading those imports to be omitted from the final bundle,
// along with their extracted CSS.
for (const id of modulesWithSideEffects) {
importStatements.push(`import ${JSON.stringify(id)};\n`);
}

// use JSON.stringify to properly escape the module ID,
// including \ delimiters on Windows.
const importStatement = `import ${JSON.stringify(cssModuleId)};`;
importStatements.push(`import ${JSON.stringify(cssModuleId)}\n;`);

const importStatement = importStatements.join('');

// Add CSS module import at the top of the file.
return `${importStatement}\n${transformedCode}`;
// Add side-effect/CSS module imports at the top of the file.
return `${importStatement}${transformedCode}`;
},
},
};
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/imported-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const width = 40.123;
const backgroundColor = 'white';

export const redClass = css`
/* red class */
color: ${red};
`;

Expand Down
8 changes: 5 additions & 3 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ test('comprehensive CSS-in-JS patterns', async () => {
// - Imported class name interpolation
// - Nested interpolations
// - Inline CSS (not assigned to variable)
// TODO:
// - preserve redClass in the CSS output
expect(result.js).toMatchInlineSnapshot(`
"//#region test/fixtures/comprehensive.input.ts
const buttonClass = "css-39ccb25d";
Expand All @@ -49,7 +47,11 @@ test('comprehensive CSS-in-JS patterns', async () => {
export { buttonClass, getButtonClass, importedClass, nestedClass, primaryClass, secondaryClass };"
`);
expect(result.css).toMatchInlineSnapshot(`
".css-39ccb25d {
".css-348273b1 {
/* red class */
color: red;
}
.css-39ccb25d {
/* button */
border: 1px solid blue;
padding: 10px;
Expand Down