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
48 changes: 48 additions & 0 deletions .changeset/8446-components-css-excludes-test-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@object-ui/components': minor
---

Stop scanning this package's own TEST files into the published stylesheet
(objectui#8446).

**The bloat was the mild half — this was a false-green generator.** Tailwind v4
scans source *text*, not an import graph, and `src/index.css` declared
`@source '../src/**/*.{ts,tsx}'` with no exclusion. All 243 test files under
`src/**/__tests__/` were therefore sources for the published `dist/index.css`,
so a class-shaped token written as a test's *expected value* compiled a real
utility into the shipped bundle — meaning a test could create the production
utility it was asserting on. Measured on #8435: `.\32 xl\:grid-cols-6` was
present in the sheet with the *unfixed* renderer on disk, sourced entirely from
assertion strings. Anyone reading "the class is in the stylesheet" as evidence
the renderer worked would have been reading their own test back.

Two `@source not` lines are added, matching the spelling already used by
`fields`, `plugin-grid` and `plugin-kanban`.

**Published bundle change — eight rules are removed**, measured by compiling
`src/index.css` through this package's own postcss + `@tailwindcss/postcss`
pipeline before and after, from the directory `pnpm build` runs in:

.flex-grow .flex-nowrap .h-[125px] .isolate
.paused .shrink .text-green-500 .w-[250px]

Nothing is added. Every one of the eight was named **only** by a test file:
`.h-[125px]` / `.w-[250px]` are `<Skeleton>` fixture sizes in
`snapshot-critical.test.tsx`, and `.flex-grow` came from a *prose sentence in a
JSDoc comment* that mentions the CSS property `flex-grow: 50`. No non-test
source in the package names any of them.

**Who could notice.** A consumer running their own Tailwind build (as every
example app here does) generates utilities from their own markup and is
unaffected. Authoring these classes in runtime page metadata was already
unsupported — the 2026-06-30 amendment to ADR-0080 under ADR-0065 states that a
utility class in page source "produces CSS only if that exact class happens to
already appear in objectui's own source", and `os validate` warns
`page-source-className-tailwind`. That incidental "happens to appear" is exactly
what is being removed. The narrow case that *can* regress is an app which
imports only the prebuilt `@object-ui/components/style.css`, runs no Tailwind of
its own, and hand-writes one of the eight in its own JSX; `.isolate`,
`.shrink`, `.flex-nowrap` and `.text-green-500` are plausible there. Scored
`minor` for that reason, not `patch`. `.flex-grow` and `.shrink` additionally
have surviving canonical spellings — `.grow` and `.shrink-0` remain in the
sheet, and this repo already migrated `flex-grow-N` → `grow-N` deliberately.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* `src/index.css` must not scan this package's own TEST files — objectui#8446.
*
* ## What was wrong
*
* Tailwind v4 scans source TEXT, not an import graph. `src/index.css` declared
* `@source '../src/**' + '/*.{ts,tsx}'` with no exclusion, so every one of the
* 243 test files under `src/**' + '/__tests__/` was a source for the PUBLISHED
* `dist/index.css`. A class-shaped token written as a test's expected value —
* even one sitting in a prose comment — therefore compiled a real utility into
* the shipped bundle, which means a test could create the very production
* utility it was asserting on. Measured on #8435: `.\32 xl\:grid-cols-6` was
* in the sheet with the UNFIXED renderer, sourced entirely from assertion
* strings.
*
* ## The instrument
*
* A sentinel token that exists nowhere else in the repository, written in THIS
* file — which lives under `__tests__/` and is therefore exactly the kind of
* file the exclusion must keep out of the scan. If the exclusions are removed,
* this file becomes a source, the sentinel compiles, and the negative
* assertion below goes red. The probe is live only because it is self-hosted:
* it does not depend on any other test continuing to name a fixture class.
*
* ## Why the positive assertion is not optional
*
* "No test-sourced rules" is satisfied by a stylesheet compiled from NOTHING —
* an implementation strictly worse than the fix (delete the `@source` line
* outright) would pass a negative-only test. The positive half pins a
* production-sourced utility and a floor on the rule count, so an empty or
* gutted sheet fails.
*
* ## Why `base` is passed explicitly
*
* `src/index.css` opens with a bare `@import 'tailwindcss'`, so Tailwind's
* automatic source detection is ON and resolves against the PROCESS CWD. Vitest
* runs from the repo root while `pnpm build` runs from this package directory,
* and the two produce different stylesheets from the same bytes (measured:
* 3430 rules vs 1385). Pinning `base` to the package root makes this reading
* reproduce the artifact the BUILD produces, from either working directory.
* The CWD dependence itself is out of scope here and reported separately.
*/
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import postcss from 'postcss';
import tailwindPostcss from '@tailwindcss/postcss';
import { describe, expect, it } from 'vitest';

const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const entry = resolve(packageRoot, 'src/index.css');

/**
* Deliberately absent from every other file in the repository, and an
* arbitrary-value utility so no production source can ever legitimately name
* it. Its ONLY occurrence is this line, in a file under `__tests__/`.
*/
const SENTINEL = 'mt-[3.7331px]';

/**
* The part of the sentinel that Tailwind's selector escaping leaves alone
* (`.mt-\[3\.7331px\]` escapes the brackets and the dot, never the digits).
* Asserting on THIS rather than on a hand-escaped selector is what keeps the
* negative assertion from passing for the wrong reason.
*/
const SENTINEL_VALUE = '3.7331px';

async function compilePublishedStylesheet(): Promise<{
css: string;
selectors: Set<string>;
}> {
const source = await readFile(entry, 'utf8');
const result = await postcss([tailwindPostcss({ base: packageRoot })]).process(source, {
from: entry,
});
const selectors = new Set<string>();
postcss.parse(result.css, { from: entry }).walkRules((rule) => {
selectors.add(rule.selector);
});
return { css: result.css, selectors };
}

describe('packages/components/src/index.css @source scan', () => {
it('does not compile tokens that only test files name, and still compiles shipped ones', async () => {
const { css, selectors } = await compilePublishedStylesheet();

// POSITIVE — a sheet compiled from nothing must not pass.
expect(selectors.has('.flex-col')).toBe(true);
expect(selectors.size).toBeGreaterThan(800);

// NEGATIVE — this file is scanned only if the exclusions are gone.
expect(SENTINEL).toContain(SENTINEL_VALUE);
expect(css).not.toContain(SENTINEL_VALUE);
}, 60_000);
});
18 changes: 17 additions & 1 deletion packages/components/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@
centralized app-shell styles directly from their own index.css. */
@custom-variant dark (&:where(.dark, .dark *));

/* Scan sources for Tailwind classes */
/* Scan sources for Tailwind classes.
*
* Only SHIPPED source. Tailwind v4 scans source TEXT, so any class-shaped
* token in a file that is scanned compiles a real rule into the published
* `dist/index.css` — including a token that only ever appears as a test's
* EXPECTED value. Without the exclusions below a test could therefore create
* the very production utility it asserts on, and a stylesheet reading would
* stop being evidence about the renderer (objectui#8446, hit live on #8435).
*
* The directory exclusion is the load-bearing one here: every test file in
* this package lives under a `__tests__/` directory, and two of them
* (`test-utils.tsx`, `page-header-action-ids.dist.spec.tsx`) are not named
* `*.test.*`. The filename exclusion carries no files today; it is kept to
* match the siblings (`fields`, `plugin-grid`, `plugin-kanban`) so a future
* co-located test is covered by construction rather than by luck. */
@source '../src/**/*.{ts,tsx}';
@source not './**/*.test.{ts,tsx}';
@source not './**/__tests__/**';

/* Tailwind plugin for animations */
@plugin 'tailwindcss-animate';
Expand Down
Loading