Skip to content

Commit

Permalink
fix(optimize-css): indexing should account for nested components
Browse files Browse the repository at this point in the history
  • Loading branch information
metonym committed Jan 19, 2025
1 parent fabbc48 commit 87f1b85
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 22 deletions.
21 changes: 19 additions & 2 deletions scripts/extract-selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parse } from "svelte/compiler";
import { type ANode, walk } from "estree-walker";
import { parse } from "svelte/compiler";
import { CARBON_PREFIX } from "../src/constants";

type ExtractSelectorsProps = {
Expand All @@ -11,9 +11,20 @@ export function extractSelectors(props: ExtractSelectorsProps) {
const { code, filename } = props;
const ast = parse(code, { filename });
const selectors: Map<string, any> = new Map();
const components: Set<string> = new Set();

walk(ast, {
enter(node) {
// A component may compose other components.
// Record these references for later processing.
if (node.type === "InlineComponent") {
if (node.name === "svelte:component") {
components.add(node.expression.name);
} else {
components.add(node.name);
}
}

if (node.type === "Attribute" && node.name === "class") {
// class="c1"
// class="c1 c2"
Expand Down Expand Up @@ -79,5 +90,11 @@ export function extractSelectors(props: ExtractSelectorsProps) {
}
});

return [...new Set(classes)];
return {
/** Unique classes in the current component. */
classes: [...new Set(classes)],

/** Unique components that are referenced by the current component. */
components: [...new Set(components)],
};
}
72 changes: 61 additions & 11 deletions scripts/index-components.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Glob } from "bun";
import { walk } from "estree-walker";
import path from "node:path";
import { parse } from "svelte/compiler";
import { walk } from "estree-walker";
import { CarbonSvelte } from "../src/constants";
import { isSvelteFile } from "../src/utils";
import { extractSelectors } from "./extract-selectors";
Expand All @@ -18,7 +18,20 @@ type Identifier = string;

type IdentifierValue = { path: string; classes: string[] };

/** Map of components/files exported from the barrel file. */
const exports_map = new Map<Identifier, null | IdentifierValue>();
/** Map of internal components. */
const internal_components = new Map<Identifier, null | IdentifierValue>();
/**
* A map of components that contain other components.
* Once all components have been processed, use this map to add
* the classes of the sub-components to the parent component.
* @example
* ```ts
* ["Accordion", ["AccordionSkeleton"]]
* ```
*/
const sub_components = new Map<Identifier, Identifier[]>();

walk(parse(`<script>${index_file}</script>`), {
enter(node) {
Expand All @@ -31,21 +44,58 @@ walk(parse(`<script>${index_file}</script>`), {
const files = new Glob("**/*.{js,svelte}").scan(path.join(carbon_path, "src"));

for await (const file of files) {
// Skip processing icon components, which do not have classes.
if (file.startsWith("icons/")) {
continue;
}

const moduleName = path.parse(file).name;

if (exports_map.has(moduleName)) {
const map: IdentifierValue = {
path: `${CarbonSvelte.Components}/src/${file}`,
classes: [],
};

if (isSvelteFile(file)) {
const file_path = path.join(carbon_path, "src/", file);
const file_text = await Bun.file(file_path).text();
map.classes = extractSelectors({ code: file_text, filename: file });
const map: IdentifierValue = {
path: `${CarbonSvelte.Components}/src/${file}`,
classes: [],
};

if (isSvelteFile(file)) {
const file_path = path.join(carbon_path, "src/", file);
const file_text = await Bun.file(file_path).text();
const selectors = extractSelectors({ code: file_text, filename: file });

map.classes = selectors.classes;

if (selectors.components.length > 0) {
sub_components.set(moduleName, selectors.components);
}
}

if (exports_map.has(moduleName)) {
exports_map.set(moduleName, map);
} else if (isSvelteFile(file)) {
internal_components.set(moduleName, map);
}
}

for (const [parent, components] of sub_components.entries()) {
const parent_map = exports_map.get(parent);

if (parent_map) {
const sub_classes = components.flatMap((component) => {
if (exports_map.has(component)) {
return exports_map.get(component)!.classes;
} else if (internal_components.has(component)) {
return internal_components.get(component)!.classes;
}

// Components that fall through here are icon components,
// which do not have classes and can be ignored.
return [];
});

if (sub_classes.length > 0) {
parent_map.classes = [
...new Set([...parent_map.classes, ...sub_classes]),
];
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/component-index.ts

Large diffs are not rendered by default.

28 changes: 20 additions & 8 deletions tests/extract-selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,47 @@ describe("extractSelectors", () => {
code: '<div class="test-class"></div>',
filename: "test.svelte",
});
expect(result).toEqual([".test-class"]);
expect(result.classes).toEqual([".test-class"]);
});

test("extracts multiple classes from class attribute", () => {
const result = extractSelectors({
code: '<div class="class1 class2 class3"></div>',
filename: "test.svelte",
});
expect(result).toEqual([".class1", ".class2", ".class3"]);
expect(result.classes).toEqual([".class1", ".class2", ".class3"]);
});

test("extracts class directives", () => {
const result = extractSelectors({
code: "<div class:active={isActive}></div>",
filename: "test.svelte",
});
expect(result).toEqual([".active"]);
expect(result.classes).toEqual([".active"]);
});

test("extracts global selectors", () => {
const result = extractSelectors({
code: "<style>:global(div) { color: red; }</style>",
filename: "test.svelte",
});
expect(result).toEqual([".div"]);
expect(result.classes).toEqual([".div"]);
});

test("handles Carbon prefix classes", () => {
const result = extractSelectors({
code: '<div class="bx--button bx--text"></div>',
filename: "test.svelte",
});
expect(result).toEqual([".bx--button", ".bx--text"]);
expect(result.classes).toEqual([".bx--button", ".bx--text"]);
});

test("deduplicates repeated classes", () => {
const result = extractSelectors({
code: '<div class="test test test"></div>',
filename: "test.svelte",
});
expect(result).toEqual([".test"]);
expect(result.classes).toEqual([".test"]);
});

test("handles mixed scenarios", () => {
Expand All @@ -59,14 +59,26 @@ describe("extractSelectors", () => {
`,
filename: "test.svelte",
});
expect(result).toEqual([".regular-class", ".bx--carbon-class", ".active"]);
expect(result.classes).toEqual([
".regular-class",
".bx--carbon-class",
".active",
]);
});

test("handles empty class attributes", () => {
const result = extractSelectors({
code: '<div class=""></div>',
filename: "test.svelte",
});
expect(result).toEqual([]);
expect(result.classes).toEqual([]);
});

test("handles inline components", () => {
const result = extractSelectors({
code: "<div><Test /><Component /></div>",
filename: "test.svelte",
});
expect(result.components).toEqual(["Test", "Component"]);
});
});

0 comments on commit 87f1b85

Please sign in to comment.