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
17 changes: 17 additions & 0 deletions .changeset/11586-tailwind-source-none-console-runner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/console': patch
'@object-ui/runner': patch
---

The console and runner stylesheets compile only from their declared `@source`
lines. Tailwind's automatic source detection is now off (`source(none)`), as it
already was for `@object-ui/components`.

With detection on, Tailwind also scanned prose in each package's directory, and
`CHANGELOG.md` was part of it. A release writes every changeset body into that
file, so the release itself added utilities to the published CSS that no
component uses. On the 17.7.0 release head, that was 11 rules (about 9 kB) in the
console sheet and 5 in the runner sheet. Both sheets now compile to the same
rules wherever the build runs. The only rules dropped against the previous
build came from prose: one from a docs proposal in the console and two from
the runner's changelog.
58 changes: 47 additions & 11 deletions apps/console/src/index-css-scan-excludes-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,24 @@
*
* ## Why `base` is passed explicitly
*
* This entry opens with a bare `@import 'tailwindcss'`, so Tailwind's automatic
* source detection is ON and rooted at the PROCESS CWD -- vitest runs from the
* repo root while `pnpm --filter @object-ui/console build` runs from this app's
* directory. Passing `base` pins this reading to the directory the build runs
* in, so the test measures the stylesheet users are served rather than the one
* the test runner's CWD happens to produce. `base` governs only the automatic
* root; the `@source` lines always resolve against the entry's own directory.
* `base` is the root of Tailwind's AUTOMATIC source detection, and it defaults
* to the PROCESS CWD. vitest runs from the repo root, while
* `pnpm --filter @object-ui/console build` runs from this app's directory.
* Passing `base` pins this reading to the directory the build runs in. `base`
* governs only the automatic root; the `@source` lines always resolve against
* the entry's own directory.
*
* The entry now opens with `source(none)` (objectui#11586), so detection is off
* and `base` should change nothing. The third test below asserts exactly that:
* it compiles once from this app's directory and once from an empty one and
* requires the same rules. Before the switch, detection scanned this app's
* prose. On the 17.7.0 release head it compiled 11 rules out of the
* `CHANGELOG.md` that `pnpm changeset:version` writes, and one of them was
* components' sentinel, which turned the second test red.
*/
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import postcss from 'postcss';
import tailwindPostcss from '@tailwindcss/postcss';
Expand Down Expand Up @@ -90,9 +98,11 @@ const SENTINEL_OTHER_TREE_VALUE = '3.7331px';
const SENTINEL_OTHER_TREE_HOST =
'packages/components/src/__tests__/index-css-scan-excludes-tests.test.ts';

async function compileConsoleStylesheet(): Promise<{ css: string; selectors: Set<string> }> {
async function compileConsoleStylesheet(
base: string = appRoot,
): Promise<{ css: string; selectors: Set<string> }> {
const source = await readFile(entry, 'utf8');
const result = await postcss([tailwindPostcss({ base: appRoot })]).process(source, {
const result = await postcss([tailwindPostcss({ base })]).process(source, {
from: entry,
});
const selectors = new Set<string>();
Expand Down Expand Up @@ -130,4 +140,30 @@ describe('apps/console/src/index.css @source scan', () => {
expect(selectors.has('.bg-background')).toBe(true);
expect(css).not.toContain(SENTINEL_OTHER_TREE_VALUE);
}, 60_000);

it('compiles the same rules from this app\'s directory as from an empty one — no rule comes from prose beside the source (objectui#11586)', async () => {
// Automatic detection is the only input that depends on `base`. If every
// rule survives a compile rooted at an empty directory, the `@source` lines
// supply all of them, so neither `CHANGELOG.md` nor `docs/` can add a rule.
// A negative test names one token. This one covers any file a future
// release writes.
const empty = await mkdtemp(join(tmpdir(), 'objectui-11586-'));
try {
const { selectors: fromApp } = await compileConsoleStylesheet(appRoot);
const { selectors: fromEmpty } = await compileConsoleStylesheet(empty);

// CONTROL: comparing two empty compiles proves nothing.
expect(fromEmpty.size).toBeGreaterThan(2500);

const onlyFromApp = [...fromApp].filter((s) => !fromEmpty.has(s)).sort();
expect(
onlyFromApp,
'these rules come from Tailwind\'s automatic detection scanning apps/console '
+ '(prose such as CHANGELOG.md or docs/) — keep `source(none)` on the entry',
).toEqual([]);
expect([...fromEmpty].filter((s) => !fromApp.has(s))).toEqual([]);
} finally {
await rm(empty, { recursive: true, force: true });
}
}, 60_000);
});
19 changes: 18 additions & 1 deletion apps/console/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
@import 'tailwindcss';
/*
* `source(none)` turns off Tailwind's AUTOMATIC source detection, so the only
* inputs are the `@source` lines below (objectui#11586). It is the same switch
* `packages/components/src/index.css` took in objectui#9569, and that file's
* header records the mechanism.
*
* Detection roots at the process CWD, which is this app's directory when it
* builds, and it scans every non-ignored file there, prose included.
* `pnpm changeset:version` writes each pending changeset body into
* `CHANGELOG.md`, so on the 17.7.0 release head the console sheet gained 11
* rules (9,450 bytes) compiled from changelog prose. Among them was the
* sentinel of components' own scan test, which the second test in
* `index-css-scan-excludes-tests.test.ts` asserts never reaches this sheet.
* On `main` it gained `.w-100`, out of a proposal under `docs/`.
* Ablated on objectui#11586: every rule that only detection supplied came from
* a `.md` file, and none came from source.
*/
@import 'tailwindcss' source(none);
@import '@object-ui/app-shell/styles.css';

/* Typography plugin — generates the `prose` utilities. plugin-markdown's
Expand Down
39 changes: 35 additions & 4 deletions packages/runner/src/__tests__/published-stylesheet-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
* uses no matter which directory the suite was launched from.
*/
import { describe, expect, it } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import postcss from 'postcss';
import tailwindPostcss from '@tailwindcss/postcss';
Expand Down Expand Up @@ -87,8 +88,8 @@ function classesIn(selector: string): string[] {
* package root is what makes this reading independent of where vitest started
* (from the repo root the same bytes scan the whole monorepo instead).
*/
async function compileClasses(css: string): Promise<Set<string>> {
const result = await postcss([tailwindPostcss({ base: PACKAGE_ROOT })]).process(css, {
async function compileClasses(css: string, base: string = PACKAGE_ROOT): Promise<Set<string>> {
const result = await postcss([tailwindPostcss({ base })]).process(css, {
from: ENTRY,
});
const classes = new Set<string>();
Expand Down Expand Up @@ -177,4 +178,34 @@ describe('@object-ui/runner published stylesheet — source set', () => {
expect(removed.length).toBeGreaterThan(0);
expect([...shipped].filter((cls) => !withTestSources.has(cls))).toEqual([]);
});

it('compiles the same classes from the package root as from an empty directory — no class comes from prose (objectui#11586)', async () => {
// `base` only changes what Tailwind's automatic detection scans. If the
// sheet is the same when detection would root at an empty directory, the
// `@source` lines supply every class, and this package's `CHANGELOG.md`,
// which `pnpm changeset:version` writes changeset prose into, adds none.
// Before `source(none)` it added `flex-shrink-0` and `isolate` on `main`,
// and on the 17.7.0 release head also `paused`, `invert` and `flex-nowrap`.
const empty = mkdtempSync(join(tmpdir(), 'objectui-11586-'));
try {
const fromEmpty = await compileClasses(entryCss, empty);
// CONTROL: comparing two empty compiles proves nothing.
expect(fromEmpty.has('bg-popover')).toBe(true);
expect([...shipped].filter((cls) => !fromEmpty.has(cls)).sort()).toEqual([]);
expect([...fromEmpty].filter((cls) => !shipped.has(cls)).sort()).toEqual([]);
} finally {
rmSync(empty, { recursive: true, force: true });
}
});

it("does not compile a class that only this package's CHANGELOG names", () => {
// CONTROL: the door must still exist. `CHANGELOG.md` is append-only, so the
// entry announcing the `flex-shrink-0` → `shrink-0` rename keeps naming it.
// If this goes red, re-point the probe at a token the file still carries.
// Do not delete it.
expect(readFileSync(resolve(PACKAGE_ROOT, 'CHANGELOG.md'), 'utf8')).toContain('flex-shrink-0');
// The deprecated Tailwind v3 alias. No shipped source emits it, and it was
// in this sheet only because detection read that changelog entry.
expect(shipped.has('flex-shrink-0')).toBe(false);
});
});
27 changes: 19 additions & 8 deletions packages/runner/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
@import 'tailwindcss';
/*
* `source(none)` turns off Tailwind's AUTOMATIC source detection, so the only
* inputs are the `@source` lines below (objectui#11586). It is the same switch
* `packages/components/src/index.css` took in objectui#9569.
*
* Detection roots at `packages/runner`, where `pnpm build` runs, and scans every
* non-ignored file there, `CHANGELOG.md` included. `pnpm changeset:version`
* writes each pending changeset body into that file. On the 17.7.0 release head,
* detection compiled `paused`, `invert` and `flex-nowrap` back into this sheet
* from changelog prose. Those are three of the test-sourced classes the
* `@source not` lines below exist to keep out. On `main` it compiled
* `flex-shrink-0` and `isolate` the same way.
*/
@import 'tailwindcss' source(none);

/* Class-based dark variant: follow `.dark` on <html>/ancestor instead of
the OS `prefers-color-scheme`. Inlined here because @object-ui/runner
Expand All @@ -24,10 +37,9 @@
* That covers runner's own tree and nothing else, so the utilities its
* dependencies need were simply missing from the published bundle: fixing the
* four sibling paths takes it from 228 to 1456 compiled selectors (21 kB ->
* 136 kB). The automatic root is still on (there is no `source(none)` here, and
* that parity question is objectui#8455), which is why the first line below is
* a no-op today and still belongs here: it is the declared intent, and it is
* what keeps runner's own source scanned the day the root moves.
* 136 kB). Detection is now off (`source(none)` above, objectui#11586), so the
* first line below is load-bearing. It is the only thing that scans runner's
* own source.
*/
@source './**/*.{ts,tsx}';
@source '../../components/src/**/*.{ts,tsx}';
Expand All @@ -41,9 +53,8 @@
* not become a published byte. Same two-line spelling as
* `packages/plugin-kanban/src/index.css`, anchored one level up because this
* entry scans four sibling packages instead of only its own tree — `../../`
* bases these on `packages/`, which contains every tree named above AND the
* automatic-detection root, so runner's own tests are covered by the same two
* lines (measured: negations do apply to files the automatic root found).
* bases these on `packages/`, which contains every tree named above, runner's
* own included, so runner's own tests are covered by the same two lines.
*
* Both lines earn their place. The file line removes nine test-sourced classes
* (`flex-grow`, `flex-nowrap`, `h-[125px]`, `h-[400px]`, `invert`, `paused`,
Expand Down
8 changes: 7 additions & 1 deletion packages/types/src/__tests__/base-bind-declared.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,18 @@ describe('`bind` is declared in exactly one place (objectui#6357)', () => {
// have been the wrong repair — it puts a permanent hole in the scan at the
// one path most likely to grow a copy of the pattern.
const PATTERN = 'bind' + '?:';
// Every `CHANGELOG.md` is excluded. `pnpm changeset:version` writes changeset
// prose into them, and a changeset that reports a local declaration being
// REMOVED quotes the pattern. On the 17.7.0 release head that made the
// plugin-dashboard and types CHANGELOGs read as two new declarations
// (objectui#11586). A CHANGELOG declares nothing.
const EXCLUDE_CHANGELOGS = ':!*CHANGELOG.md';

let out: string;
try {
out = execFileSync(
'git',
['grep', '-n', '-F', '--', PATTERN, 'packages'],
['grep', '-n', '-F', '--', PATTERN, 'packages', EXCLUDE_CHANGELOGS],
{ cwd: REPO_ROOT, encoding: 'utf8' },
);
} catch (err: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ const GREP_ENGINE_FLAG = '-P';
/** The scanned tree. `examples/` is in it because an example app is exactly
* where a fourth reader would plausibly appear. */
const SCAN_ROOTS = ['packages', 'apps', 'examples'] as const;
/** Every `CHANGELOG.md` is outside the scanned tree. `pnpm changeset:version`
* writes each pending changeset body into one, and the changeset for a
* retirement names the key it retires. Without this exclusion the release
* commit itself reads as five new readers of the array, which is what turned
* the 17.7.0 release head red (objectui#11586). A CHANGELOG is history, not
* code. The sibling censuses exclude it with the same spelling
* (`eventable-schema-retired-6497.test.ts`). */
const SCAN_EXCLUDES = [':!*CHANGELOG.md'] as const;

/** The engine, for the diagnostics below: a failure that says "the anchor is
* dead" is only actionable if it also says WHICH git read it. */
Expand All @@ -414,7 +422,7 @@ const gitGrepFiles = (args: readonly string[]): string[] => {
let out: string;
let failure: Failure | undefined;
try {
out = execFileSync('git', ['grep', ...args, '--', ...SCAN_ROOTS], {
out = execFileSync('git', ['grep', ...args, '--', ...SCAN_ROOTS, ...SCAN_EXCLUDES], {
cwd: REPO_ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down
21 changes: 21 additions & 0 deletions scripts/markdown-test-inputs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ export const ADJUDICATED = new Map([
'packages/plugin-markdown/src/toc-anchor-parity.test.tsx',
{ reads: ['content/docs/utilities/runner.mdx'] },
],
// objectui#11586. This reads a CHANGELOG as a CONTROL, the same shape as the
// components entry above. The test asserts that the published sheet lacks a
// class only that prose names, and it reads the file to prove the prose still
// names it.
[
'packages/runner/src/__tests__/published-stylesheet-sources.test.ts',
{
reads: ['packages/runner/CHANGELOG.md'],
notRead: ['CHANGELOG.md'],
},
],
[
'packages/types/src/__tests__/action-callback-retired-7068.test.ts',
{
Expand Down Expand Up @@ -522,6 +533,16 @@ export const ADJUDICATED = new Map([
// objectui#10872 batch 4. `taughtActionButton()` extracts the first json fence
// under the quick-start's "### Add Actions" heading -- the taught
// `action:button` node -- and judges it on both zod faces, in a page and alone.
// objectui#11586. Reads no markdown. Its only `.md` literal is the
// `:!*CHANGELOG.md` pathspec that keeps every CHANGELOG OUT of its `git grep`
// census.
[
'packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts',
{
reads: [],
walker: 'not-markdown: lists the zod mirror sources in `packages/types/src/zod`',
},
],
[
'packages/types/src/__tests__/held-public-block-arms-10872.test.ts',
{ reads: ['content/docs/guide/quick-start.md'] },
Expand Down
Loading