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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@robomous/ui-core",
"version": "0.2.0",
"version": "0.2.1",
"description": "Robomous design system: twenty-one owned React components, the design tokens they resolve through, and the gates that hold the rules behind them.",
"license": "Apache-2.0",
"repository": {
Expand Down
90 changes: 50 additions & 40 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions src/gates/design.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,44 @@ test("src/index.ts exports every component module in src/components", () => {
`or move the module next to its only caller:\n${missing.join("\n")}`,
).toEqual([]);
});

/**
* The stylesheet's `@source` still reaches the components.
*
* `styles.css` ships as source and a consumer's Tailwind compiles it. That
* compiler auto-detects the *consumer's* files and never walks
* `node_modules`, so `@source` is the only thing that puts this package's
* class strings in front of it. The directive resolves relative to the
* stylesheet, so moving the stylesheet moves the target — and pointing it one
* directory too shallow costs a consumer every utility that only this package
* writes, with no error anywhere: the CSS compiles, it is simply missing
* `h-8`, `line-clamp-1` and every variant utility the components rely on.
*
* Nothing else here can catch that, because nothing here compiles CSS. This
* resolves the path and asks whether the components are under it.
*/
test("the stylesheet's @source resolves to a directory that holds the components", () => {
const stylesheet = read("src/theme/styles.css");
const directives = [...stylesheet.matchAll(/^\s*@source\s+"([^"]+)"\s*;/gm)].map((m) => m[1]);
expect(directives.length, "styles.css declares no @source, so it scans nothing").toBeGreaterThan(
0,
);

const stylesheetDir = path.join(REPO, "src", "theme");
const componentDir = path.join(REPO, "src", "components");

const reaching = directives.filter((spec) => {
const target = path.resolve(stylesheetDir, spec);
// `@source` scans a directory recursively, so it reaches the components
// when its target is the component directory or an ancestor of it.
const rel = path.relative(target, componentDir);
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
});

expect(
reaching,
"no @source in src/theme/styles.css reaches src/components, so a consumer's build would " +
"emit none of this package's own utilities. @source resolves relative to the stylesheet: " +
`from src/theme/ the components are at "..". Declared: ${directives.join(", ")}`,
).not.toEqual([]);
});
18 changes: 15 additions & 3 deletions src/theme/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,22 @@

/*
* The package's own components, so a class string that only ever appears
* inside `ui-core` still produces CSS in the consumer's build. Relative to
* this file, which is what `@source` takes.
* inside `ui-core` still produces CSS in the consumer's build.
*
* `@source` resolves **relative to this file**, and this file is in
* `src/theme/`. So the target is `..` — `src/`, which holds `components/` —
* and not `.`, which would be `src/theme/` and contains no component at all.
* A consumer's Tailwind auto-detects its own sources and never looks inside
* `node_modules`, so this directive is the only thing that puts these
* components' class strings in front of the compiler: point it one directory
* too shallow and every consumer builds a stylesheet with no `h-8`, no
* `line-clamp-1` and no variant utility in it, while every check in this
* repository stays green.
*
* `src/gates/design.test.ts` resolves this path and fails if it stops
* reaching the components.
*/
@source ".";
@source "..";

@custom-variant dark (&:is(.dark *));

Expand Down
4 changes: 2 additions & 2 deletions src/theme/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ describe("structure", () => {
});

it("reaches its own package's classes: @source after the imports", () => {
const sourceAt = STYLESHEET.indexOf('@source ".";');
const sourceAt = STYLESHEET.indexOf('@source "..";');
const lastImportAt = STYLESHEET.lastIndexOf("@import");
expect(sourceAt, 'styles.css has no @source ".";').toBeGreaterThan(-1);
expect(sourceAt, 'styles.css has no @source "..";').toBeGreaterThan(-1);
expect(sourceAt).toBeGreaterThan(lastImportAt);
});

Expand Down