cursedbelt 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/components/ListToolbar.d.ts +94 -0
- package/dist/react/components/ListToolbar.d.ts.map +1 -0
- package/dist/react/components/ListToolbar.js +66 -0
- package/dist/react/components/ListToolbar.js.map +1 -0
- package/dist/react/file-tree/FileTree.d.ts +282 -3
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +799 -131
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +151 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
- package/dist/react/file-tree/fileTreeSelection.js +264 -0
- package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
- package/dist/react/file-tree/index.d.ts +1 -0
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +8 -0
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/form.d.ts +3 -0
- package/dist/react/lib/form.d.ts.map +1 -1
- package/dist/react/lib/form.js +43 -5
- package/dist/react/lib/form.js.map +1 -1
- package/dist/react/media/DenseGalleryShell.d.ts +19 -3
- package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
- package/dist/react/media/DenseGalleryShell.js +55 -15
- package/dist/react/media/DenseGalleryShell.js.map +1 -1
- package/dist/react/media/FocusViewer.d.ts.map +1 -1
- package/dist/react/media/FocusViewer.js +14 -14
- package/dist/react/media/FocusViewer.js.map +1 -1
- package/dist/react/media/FrameGrabber.d.ts +23 -0
- package/dist/react/media/FrameGrabber.d.ts.map +1 -1
- package/dist/react/media/FrameGrabber.js +67 -1
- package/dist/react/media/FrameGrabber.js.map +1 -1
- package/dist/react/media/MediaResize.d.ts +51 -19
- package/dist/react/media/MediaResize.d.ts.map +1 -1
- package/dist/react/media/MediaResize.js +20 -13
- package/dist/react/media/MediaResize.js.map +1 -1
- package/dist/react/media/VideoChapterEditor.d.ts +67 -0
- package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
- package/dist/react/media/VideoChapterEditor.js +148 -0
- package/dist/react/media/VideoChapterEditor.js.map +1 -0
- package/dist/react/media/VideoPlayer.d.ts +46 -4
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +124 -11
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/VideoTurner.d.ts +15 -2
- package/dist/react/media/VideoTurner.d.ts.map +1 -1
- package/dist/react/media/VideoTurner.js +29 -2
- package/dist/react/media/VideoTurner.js.map +1 -1
- package/dist/react/media/denseGallery.css +104 -46
- package/dist/react/media/hlsSource.d.ts +57 -0
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +177 -1
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +2 -0
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +6 -0
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/media/mediaResize.css +127 -54
- package/dist/react/media/pictureEditor.css +52 -6
- package/dist/react/media/videoChapterEditor.css +203 -0
- package/dist/react/media/videoChapters.d.ts +117 -0
- package/dist/react/media/videoChapters.d.ts.map +1 -0
- package/dist/react/media/videoChapters.js +95 -0
- package/dist/react/media/videoChapters.js.map +1 -0
- package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
- package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
- package/dist/react/media-gallery/GalleryTable.js +40 -9
- package/dist/react/media-gallery/GalleryTable.js.map +1 -1
- package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +484 -104
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
- package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
- package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
- package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
- package/dist/react/media-gallery/index.d.ts +2 -0
- package/dist/react/media-gallery/index.d.ts.map +1 -1
- package/dist/react/media-gallery/index.js +5 -0
- package/dist/react/media-gallery/index.js.map +1 -1
- package/dist/react/media-gallery/mediaGallery.css +180 -4
- package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
- package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
- package/dist/react/media-gallery/mediaMeta.js +56 -0
- package/dist/react/media-gallery/mediaMeta.js.map +1 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
- package/dist/react/media-gallery/playbackPreferences.js +42 -0
- package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +34 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +29 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
- package/dist/styles-areas/activity-bar.css +30 -0
- package/dist/styles-areas/analytics.css +672 -0
- package/dist/styles-areas/auth.css +154 -0
- package/dist/styles-areas/calendar.css +416 -0
- package/dist/styles-areas/chart-table.css +41 -0
- package/dist/styles-areas/charts.css +89 -0
- package/dist/styles-areas/chat.css +364 -0
- package/dist/styles-areas/clipboard.css +26 -0
- package/dist/styles-areas/code-editor.css +130 -0
- package/dist/styles-areas/companion-link.css +70 -0
- package/dist/styles-areas/core.css +2503 -0
- package/dist/styles-areas/dashboard-grid.css +84 -0
- package/dist/styles-areas/data-table.css +657 -0
- package/dist/styles-areas/deep-link.css +26 -0
- package/dist/styles-areas/diff-viewer.css +30 -0
- package/dist/styles-areas/disk-usage.css +262 -0
- package/dist/styles-areas/emoji.css +154 -0
- package/dist/styles-areas/fields.css +582 -0
- package/dist/styles-areas/file-tree.css +33 -0
- package/dist/styles-areas/filter-rail.css +235 -0
- package/dist/styles-areas/folder-tree.css +219 -0
- package/dist/styles-areas/keep-awake.css +26 -0
- package/dist/styles-areas/layout-engine.css +239 -0
- package/dist/styles-areas/markdown.css +87 -0
- package/dist/styles-areas/master-detail.css +239 -0
- package/dist/styles-areas/master-lock.css +26 -0
- package/dist/styles-areas/media-gallery.css +351 -0
- package/dist/styles-areas/media.css +658 -0
- package/dist/styles-areas/nav.css +488 -0
- package/dist/styles-areas/notifications.css +245 -0
- package/dist/styles-areas/overlays.css +133 -0
- package/dist/styles-areas/palette.css +140 -0
- package/dist/styles-areas/pdf-viewer.css +47 -0
- package/dist/styles-areas/problem-detail.css +97 -0
- package/dist/styles-areas/rich-text.css +167 -0
- package/dist/styles-areas/sharing.css +153 -0
- package/dist/styles-areas/spreadsheet.css +37 -0
- package/dist/styles-areas/stats.css +281 -0
- package/dist/styles-areas/test-report.css +133 -0
- package/dist/styles-areas/virtual.css +30 -0
- package/dist/styles-areas/wizard.css +381 -0
- package/dist/styles-areas/workbench.css +543 -0
- package/dist/styles-areas/workbook-viewer.css +44 -0
- package/dist/styles-static.css +34 -23
- package/dist/styles.css +34 -23
- package/package.json +288 -202
- package/scripts/checkDistExports.ts +67 -0
- package/scripts/cssRules.ts +125 -0
- package/scripts/fixtureAppCss.ts +344 -0
- package/scripts/generateAreaStyles.ts +253 -0
- package/scripts/guardrailsEnforce.spec.ts +62 -0
- package/scripts/guardrailsEnforce.ts +67 -3
- package/scripts/styleAreas.ts +585 -0
- package/scripts/verify.ts +5 -0
- package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
- package/src/docsMatchTheSplit.spec.ts +98 -0
- package/src/fixtureAppCss.spec.ts +177 -0
- package/src/namedSubpathsResolve.spec.ts +35 -0
- package/src/publishShape.spec.ts +67 -0
- package/src/react/components/ListToolbar.spec.tsx +172 -0
- package/src/react/components/ListToolbar.tsx +151 -0
- package/src/react/file-tree/FileTree.spec.tsx +1009 -0
- package/src/react/file-tree/FileTree.tsx +1459 -309
- package/src/react/file-tree/fileTree.css +151 -0
- package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
- package/src/react/file-tree/fileTreeSelection.ts +321 -0
- package/src/react/file-tree/index.ts +21 -0
- package/src/react/index.ts +1 -0
- package/src/react/lib/form.ts +51 -8
- package/src/react/media/DenseGalleryShell.tsx +108 -36
- package/src/react/media/FocusViewer.tsx +15 -8
- package/src/react/media/FrameGrabber.spec.tsx +175 -0
- package/src/react/media/FrameGrabber.tsx +67 -0
- package/src/react/media/MediaResize.spec.tsx +84 -14
- package/src/react/media/MediaResize.tsx +51 -19
- package/src/react/media/VideoChapterEditor.tsx +428 -0
- package/src/react/media/VideoPlayer.spec.tsx +111 -1
- package/src/react/media/VideoPlayer.tsx +158 -11
- package/src/react/media/VideoTurner.spec.tsx +67 -0
- package/src/react/media/VideoTurner.tsx +70 -15
- package/src/react/media/denseGallery.css +104 -46
- package/src/react/media/hlsSource.recovery.spec.ts +213 -0
- package/src/react/media/hlsSource.spec.ts +8 -0
- package/src/react/media/hlsSource.ts +189 -1
- package/src/react/media/index.ts +14 -0
- package/src/react/media/mediaResize.css +127 -54
- package/src/react/media/pictureEditor.css +52 -6
- package/src/react/media/videoChapterEditor.css +203 -0
- package/src/react/media/videoChapters.spec.ts +176 -0
- package/src/react/media/videoChapters.ts +181 -0
- package/src/react/media-gallery/GalleryTable.tsx +93 -3
- package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
- package/src/react/media-gallery/MediaGallery.tsx +813 -95
- package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
- package/src/react/media-gallery/galleryTable.spec.ts +41 -1
- package/src/react/media-gallery/index.ts +7 -0
- package/src/react/media-gallery/mediaGallery.css +180 -4
- package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
- package/src/react/media-gallery/mediaMeta.ts +101 -0
- package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
- package/src/react/media-gallery/playbackPreferences.ts +100 -0
- package/src/react/media-gallery/types.ts +34 -0
- package/src/shippedFilesAreTracked.spec.ts +69 -0
- package/src/styles-areas/activity-bar.css +30 -0
- package/src/styles-areas/analytics.css +672 -0
- package/src/styles-areas/auth.css +154 -0
- package/src/styles-areas/calendar.css +416 -0
- package/src/styles-areas/chart-table.css +41 -0
- package/src/styles-areas/charts.css +89 -0
- package/src/styles-areas/chat.css +364 -0
- package/src/styles-areas/clipboard.css +26 -0
- package/src/styles-areas/code-editor.css +130 -0
- package/src/styles-areas/companion-link.css +70 -0
- package/src/styles-areas/core.css +2503 -0
- package/src/styles-areas/dashboard-grid.css +84 -0
- package/src/styles-areas/data-table.css +657 -0
- package/src/styles-areas/deep-link.css +26 -0
- package/src/styles-areas/diff-viewer.css +30 -0
- package/src/styles-areas/disk-usage.css +262 -0
- package/src/styles-areas/emoji.css +154 -0
- package/src/styles-areas/fields.css +582 -0
- package/src/styles-areas/file-tree.css +33 -0
- package/src/styles-areas/filter-rail.css +235 -0
- package/src/styles-areas/folder-tree.css +219 -0
- package/src/styles-areas/keep-awake.css +26 -0
- package/src/styles-areas/layout-engine.css +239 -0
- package/src/styles-areas/markdown.css +87 -0
- package/src/styles-areas/master-detail.css +239 -0
- package/src/styles-areas/master-lock.css +26 -0
- package/src/styles-areas/media-gallery.css +351 -0
- package/src/styles-areas/media.css +658 -0
- package/src/styles-areas/nav.css +488 -0
- package/src/styles-areas/notifications.css +245 -0
- package/src/styles-areas/overlays.css +133 -0
- package/src/styles-areas/palette.css +140 -0
- package/src/styles-areas/pdf-viewer.css +47 -0
- package/src/styles-areas/problem-detail.css +97 -0
- package/src/styles-areas/rich-text.css +167 -0
- package/src/styles-areas/sharing.css +153 -0
- package/src/styles-areas/spreadsheet.css +37 -0
- package/src/styles-areas/stats.css +281 -0
- package/src/styles-areas/test-report.css +133 -0
- package/src/styles-areas/virtual.css +30 -0
- package/src/styles-areas/wizard.css +381 -0
- package/src/styles-areas/workbench.css +543 -0
- package/src/styles-areas/workbook-viewer.css +44 -0
- package/src/styles-static.css +34 -23
- package/src/styles.css +34 -23
- package/src/stylesAreas.spec.ts +247 -0
- package/src/stylesUtilitiesMatches.spec.ts +6 -77
- package/src/testFilesRunInParallel.spec.ts +79 -0
- package/src/typecheckCachesAreSeparate.spec.ts +128 -0
- package/src/verifyGraph.spec.ts +6 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Every `exports` target that points into `dist/` must EXIST in the build that just ran.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this is a build step and not a spec ────────────────────────────────────────
|
|
6
|
+
* A missing target does not throw. Node and tsc walk the conditions in order and take the
|
|
7
|
+
* first one that matches an ACTIVE condition — but a `types` (or `import`) target whose
|
|
8
|
+
* file is absent makes the resolver fall through to the next condition, which for this
|
|
9
|
+
* package is `bun`/`source`/`default`: the raw `.ts`. So an export that stops being built
|
|
10
|
+
* silently reverts to the exact defect task 172 fixed — a consumer's `tsc` compiling our
|
|
11
|
+
* source again — and every test in this repo stays green while it happens.
|
|
12
|
+
*
|
|
13
|
+
* It has to run inside `build`, against `CURSEDBELT_DIST_DIR`, because `dist/` is the one
|
|
14
|
+
* thing no spec here may read: `build` runs BESIDE `test` and `e2e` in the verify graph
|
|
15
|
+
* (`src/verifyGraph.spec.ts` asserts no spec opens `dist/`), so a spec looking at it would
|
|
16
|
+
* be reading a directory another stage is in the middle of swapping.
|
|
17
|
+
*
|
|
18
|
+
* Declarations are checked the same as runtime files on purpose — a `.d.ts` that was not
|
|
19
|
+
* emitted is not a smaller problem than a `.js` that was not emitted; it is a quieter one.
|
|
20
|
+
*/
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
const root = path.resolve(import.meta.dir, "..");
|
|
24
|
+
const outDir = process.env.CURSEDBELT_DIST_DIR || "dist";
|
|
25
|
+
|
|
26
|
+
const pkg = (await Bun.file(path.join(root, "package.json")).json()) as {
|
|
27
|
+
exports: Record<string, string | Record<string, string>>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** `[subpath, condition, target]` for every export target inside `dist/`. */
|
|
31
|
+
const distTargets: [string, string, string][] = [];
|
|
32
|
+
for (const [subpath, entry] of Object.entries(pkg.exports)) {
|
|
33
|
+
const conditions = typeof entry === "string" ? { default: entry } : entry;
|
|
34
|
+
for (const [condition, target] of Object.entries(conditions)) {
|
|
35
|
+
if (typeof target === "string" && target.startsWith("./dist/")) {
|
|
36
|
+
distTargets.push([subpath, condition, target]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A manifest this script cannot read is a manifest it cannot check; passing silently on
|
|
42
|
+
// zero targets would make the whole step decorative.
|
|
43
|
+
if (distTargets.length === 0) {
|
|
44
|
+
console.error("🔴 no exports target resolves into dist/ — checkDistExports is checking nothing.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const missing: string[] = [];
|
|
49
|
+
for (const [subpath, condition, target] of distTargets) {
|
|
50
|
+
// `./dist/react/index.js` → `<outDir>/react/index.js`, so a staged build (dist.next)
|
|
51
|
+
// is checked where it actually is rather than where it will be after the swap.
|
|
52
|
+
const staged = path.join(root, outDir, target.slice("./dist/".length));
|
|
53
|
+
if (!(await Bun.file(staged).exists())) {
|
|
54
|
+
missing.push(`${subpath} [${condition}] → ${target} (not emitted at ${outDir}/${target.slice(7)})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (missing.length > 0) {
|
|
59
|
+
console.error(
|
|
60
|
+
`\n🔴 the build did not emit ${missing.length} file(s) package.json exports promise:\n ${missing.join("\n ")}\n\n` +
|
|
61
|
+
"A consumer does not get an error for these — the resolver falls through to the next\n" +
|
|
62
|
+
"condition, which is our raw `.ts`, and the consumer's tsc compiles our source instead.\n",
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`✅ all ${distTargets.length} dist export target(s) present in ${outDir}`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two compiled stylesheets by the RULES they emit, not by their bytes.
|
|
3
|
+
*
|
|
4
|
+
* Every split this package has made to its CSS — `styles-static.css`,
|
|
5
|
+
* `styles-utilities.css`, and now `styles-areas/*.css` — can fail in exactly one
|
|
6
|
+
* way that matters: the new route emits FEWER rules than the old one, so some
|
|
7
|
+
* component renders unstyled in the apps that adopted it. That failure is
|
|
8
|
+
* invisible to `tsc` (it is CSS), to Biome (nothing is wrong with the file), and
|
|
9
|
+
* to a byte count read in the only direction anybody reads it, because a smaller
|
|
10
|
+
* stylesheet is what the whole exercise was asked for.
|
|
11
|
+
*
|
|
12
|
+
* So the specs compare rule sets. These helpers were written inside
|
|
13
|
+
* `src/stylesUtilitiesMatches.spec.ts` and live here now because
|
|
14
|
+
* `src/stylesAreas.spec.ts` needs the same comparison against the same two
|
|
15
|
+
* normalisations — a second copy of a CSS flattener is a second copy that can
|
|
16
|
+
* drift from the assertions relying on it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Flatten a stylesheet into `at-rule ‖ … ‖ selector` → the set of declarations in
|
|
21
|
+
* that block, merging blocks that repeat (both routes emit `@keyframes spin`
|
|
22
|
+
* twice; a consumer's browser does not care, and neither should a comparison).
|
|
23
|
+
*
|
|
24
|
+
* A hand-rolled walk rather than a CSS parser because the only input is Tailwind's
|
|
25
|
+
* own output: well-formed, and the one construct that could confuse brace counting
|
|
26
|
+
* — a `{` inside a string or a comment — is skipped explicitly.
|
|
27
|
+
*/
|
|
28
|
+
export const flatten = (css: string): Map<string, Set<string>> => {
|
|
29
|
+
const out = new Map<string, Set<string>>();
|
|
30
|
+
const stack: string[] = [];
|
|
31
|
+
const open: string[][] = [[]];
|
|
32
|
+
let buf = '';
|
|
33
|
+
const push = (raw: string): void => {
|
|
34
|
+
const decl = raw.trim();
|
|
35
|
+
if (decl) open[open.length - 1]?.push(decl);
|
|
36
|
+
};
|
|
37
|
+
for (let i = 0; i < css.length; i++) {
|
|
38
|
+
const ch = css[i];
|
|
39
|
+
if (ch === '/' && css[i + 1] === '*') {
|
|
40
|
+
i = css.indexOf('*/', i) + 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === '"' || ch === "'") {
|
|
44
|
+
const end = css.indexOf(ch, i + 1);
|
|
45
|
+
buf += css.slice(i, end + 1);
|
|
46
|
+
i = end;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === '{') {
|
|
50
|
+
stack.push(buf.trim().replace(/\s+/g, ' '));
|
|
51
|
+
buf = '';
|
|
52
|
+
open.push([]);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '}') {
|
|
56
|
+
push(buf);
|
|
57
|
+
buf = '';
|
|
58
|
+
const key = stack.join(' ‖ ');
|
|
59
|
+
const set = out.get(key) ?? new Set<string>();
|
|
60
|
+
for (const decl of open.pop() ?? []) set.add(decl);
|
|
61
|
+
out.set(key, set);
|
|
62
|
+
stack.pop();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// At depth 0 a `;` ends a statement at-rule (`@layer a, b;`, `@import …`),
|
|
66
|
+
// which carries no declarations and must not stick to the next selector.
|
|
67
|
+
if (ch === ';') {
|
|
68
|
+
if (stack.length) push(buf);
|
|
69
|
+
buf = '';
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
buf += ch;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** `var(--x, <anything>)` → `var(--x)`, so a fallback reads as the same declaration. */
|
|
78
|
+
export const dropFallbacks = (decl: string): string => decl.replace(/var\((\s*--[\w-]+)[^)]*\)/g, 'var($1)');
|
|
79
|
+
|
|
80
|
+
export const CUSTOM_PROPERTY = /^(--[\w-]+)\s*:/;
|
|
81
|
+
|
|
82
|
+
/** Every custom property a stylesheet actually reads through `var()`. */
|
|
83
|
+
export const referenced = (css: string): Set<string> => {
|
|
84
|
+
const out = new Set<string>();
|
|
85
|
+
for (const m of css.matchAll(/var\(\s*(--[\w-]+)/g)) out.add(m[1] as string);
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Every custom property a stylesheet declares, including `@property` registrations. */
|
|
90
|
+
export const declared = (css: string): Set<string> => {
|
|
91
|
+
const out = new Set<string>();
|
|
92
|
+
for (const m of css.matchAll(/(?:^|[;{\s])(--[\w-]+)\s*:/g)) out.add(m[1] as string);
|
|
93
|
+
for (const m of css.matchAll(/@property\s+(--[\w-]+)/g)) out.add(m[1] as string);
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Rules and declarations `before` emits that `after` does not — the whole point.
|
|
99
|
+
*
|
|
100
|
+
* `ignoreProperty` is how a caller excuses the `@theme inline` alias declarations
|
|
101
|
+
* that one route writes into `:root` and nothing ever reads; everything else is a
|
|
102
|
+
* real difference.
|
|
103
|
+
*/
|
|
104
|
+
export const lostRules = (
|
|
105
|
+
before: Map<string, Set<string>>,
|
|
106
|
+
after: Map<string, Set<string>>,
|
|
107
|
+
ignoreProperty: (prop: string) => boolean = () => false,
|
|
108
|
+
): { rules: string[]; declarations: string[] } => {
|
|
109
|
+
const rules: string[] = [];
|
|
110
|
+
const declarations: string[] = [];
|
|
111
|
+
for (const [key, decls] of before) {
|
|
112
|
+
const mine = after.get(key);
|
|
113
|
+
if (!mine) {
|
|
114
|
+
rules.push(key);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const theirs = new Set([...mine].map(dropFallbacks));
|
|
118
|
+
for (const decl of decls) {
|
|
119
|
+
const prop = CUSTOM_PROPERTY.exec(decl)?.[1];
|
|
120
|
+
if (prop && ignoreProperty(prop)) continue;
|
|
121
|
+
if (!theirs.has(dropFallbacks(decl))) declarations.push(`${key} → ${decl}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { rules, declarations };
|
|
125
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `bun scripts/fixtureAppCss.ts [--prune]` — measure what a consuming app emits,
|
|
4
|
+
* and refuse to let it grow.
|
|
5
|
+
*
|
|
6
|
+
* ## What this exists for
|
|
7
|
+
*
|
|
8
|
+
* Three apps shipped the union of this design system before anyone counted it,
|
|
9
|
+
* then four, then five; the fifth arrived with a 173-line `@source` list that
|
|
10
|
+
* could not have worked and a 494 kB entry chunk. Every one of those numbers was
|
|
11
|
+
* measured from the CONSUMER side, in a repo that cannot change the thing causing
|
|
12
|
+
* it, and none of them was measured twice — so nothing here could tell whether the
|
|
13
|
+
* cost was going up or down.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 The check has to live in the package, and it may not reach into `apps/*`:
|
|
16
|
+
* this generation's third rule is that a repo's gate proves that repo. So the
|
|
17
|
+
* package keeps its own consumer — `demo/fixtures/lean-app`, a small releases
|
|
18
|
+
* console built out of cursedbelt's public subpaths — compiles its stylesheet the
|
|
19
|
+
* way an app's bundler will, and records the byte count here.
|
|
20
|
+
*
|
|
21
|
+
* ## The two routes, and why both are compiled every run
|
|
22
|
+
*
|
|
23
|
+
* UNION — `styles-static.css` + `styles-utilities.css`, which is what every app
|
|
24
|
+
* ships today and what `@import "cursedbelt/styles.css"` is equivalent
|
|
25
|
+
* to (measured at 54 bytes' difference; see generateUtilityStyles.ts).
|
|
26
|
+
* AREAS — `styles-static.css` + the areas `demo/fixtures/lean-app/app.css`
|
|
27
|
+
* declares, and no `@source` into this package at all.
|
|
28
|
+
*
|
|
29
|
+
* Both, not just the second, because a ratchet on one number cannot tell a real
|
|
30
|
+
* saving from a design system that shrank. The RATIO is the claim; the absolute
|
|
31
|
+
* area number is the ratchet.
|
|
32
|
+
*
|
|
33
|
+
* ## The ratchet
|
|
34
|
+
*
|
|
35
|
+
* `fixtureAppCss.baseline` records both counts. `--prune` lowers them and REFUSES
|
|
36
|
+
* to raise, the same bargain `publicSurface.baseline` makes: a real increase — a
|
|
37
|
+
* new primitive in `core`, a component the fixture started rendering — is a hand
|
|
38
|
+
* edit in a commit that says why, which is a reviewable moment rather than a
|
|
39
|
+
* number that drifts. `src/fixtureAppCss.spec.ts` is what runs it in the gate, and
|
|
40
|
+
* it asserts the "not unstyled" half too: a stylesheet can always be made smaller
|
|
41
|
+
* by emitting less than the app needs.
|
|
42
|
+
*/
|
|
43
|
+
import { Scanner } from '@tailwindcss/oxide';
|
|
44
|
+
import { REPO_ROOT } from './generateStaticStyles';
|
|
45
|
+
import { compileStylesheet } from './generateUtilityStyles';
|
|
46
|
+
import { AREAS, CORE, REACT_DIR, areaClosures, buildGraph, transitiveNeeds } from './styleAreas';
|
|
47
|
+
|
|
48
|
+
/** The named fixture app. One, deliberately: a second one is a second thing to keep honest. */
|
|
49
|
+
export const FIXTURE = 'demo/fixtures/lean-app';
|
|
50
|
+
export const FIXTURE_DIR = `${REPO_ROOT}/${FIXTURE}`;
|
|
51
|
+
export const FIXTURE_CSS = `${FIXTURE_DIR}/app.css`;
|
|
52
|
+
export const BASELINE = `${REPO_ROOT}/fixtureAppCss.baseline`;
|
|
53
|
+
|
|
54
|
+
/** `@import "cursedbelt/styles-areas/<area>.css";` — the fixture's one knob. */
|
|
55
|
+
const AREA_IMPORT = /@import\s+"cursedbelt\/styles-areas\/([a-z0-9-]+)\.css"/g;
|
|
56
|
+
|
|
57
|
+
/** Areas the fixture's stylesheet declares, in the order it imports them. */
|
|
58
|
+
export const declaredAreas = async (): Promise<string[]> => {
|
|
59
|
+
const css = await Bun.file(FIXTURE_CSS).text();
|
|
60
|
+
return [...css.matchAll(AREA_IMPORT)].map((m) => m[1] as string);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** `from 'cursedbelt/<subpath>'` in the fixture's TSX, with the names it took. */
|
|
64
|
+
export interface FixtureImport {
|
|
65
|
+
/** The export subpath, `.`-prefixed as `package.json` spells it (`./react/charts`). */
|
|
66
|
+
readonly subpath: string;
|
|
67
|
+
/** Named bindings, for a barrel import. Empty for a deep subpath. */
|
|
68
|
+
readonly names: string[];
|
|
69
|
+
readonly file: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const IMPORT_BLOCK = /import\s+(?:type\s+)?(?:\{([^}]*)\}|[\w*\s,]+)\s*from\s*'(cursedbelt(?:\/[^']*)?)'/g;
|
|
73
|
+
|
|
74
|
+
export const fixtureImports = async (): Promise<FixtureImport[]> => {
|
|
75
|
+
const out: FixtureImport[] = [];
|
|
76
|
+
for (const rel of new Bun.Glob('**/*.tsx').scanSync({ cwd: FIXTURE_DIR, onlyFiles: true })) {
|
|
77
|
+
const text = await Bun.file(`${FIXTURE_DIR}/${rel}`).text();
|
|
78
|
+
for (const match of text.matchAll(IMPORT_BLOCK)) {
|
|
79
|
+
const names = (match[1] ?? '')
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((n) => n.trim().replace(/^type\s+/, ''))
|
|
82
|
+
// `A as B` exports A; the local alias is the consumer's business.
|
|
83
|
+
.map((n) => (n.includes(' as ') ? (n.split(/\s+as\s+/)[0] as string) : n))
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
out.push({ subpath: (match[2] as string).replace(/^cursedbelt/, '.'), names, file: `${FIXTURE}/${rel}` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `export { A, type B } from './x'` in the `react` barrel → exported name → module.
|
|
93
|
+
*
|
|
94
|
+
* Only the named form, on purpose. A fixture import the barrel re-exports with
|
|
95
|
+
* `export * from` cannot be attributed to one module by a reader either, and
|
|
96
|
+
* `resolveFixtureModules` fails naming it rather than guessing — an unattributed
|
|
97
|
+
* import is exactly the case where an app would silently under-declare its areas.
|
|
98
|
+
*/
|
|
99
|
+
const NAMED_REEXPORT = /export\s+(?:type\s+)?\{([^}]*)\}\s*from\s*'(\.[^']+)'/g;
|
|
100
|
+
|
|
101
|
+
export const barrelMap = async (): Promise<Map<string, string>> => {
|
|
102
|
+
const text = await Bun.file(`${REACT_DIR}/index.ts`).text();
|
|
103
|
+
const out = new Map<string, string>();
|
|
104
|
+
for (const match of text.matchAll(NAMED_REEXPORT)) {
|
|
105
|
+
const target = match[2] as string;
|
|
106
|
+
for (const raw of (match[1] as string).split(',')) {
|
|
107
|
+
const name = raw.trim().replace(/^type\s+/, '');
|
|
108
|
+
const exported = name.includes(' as ') ? (name.split(/\s+as\s+/)[1] as string) : name;
|
|
109
|
+
if (exported) out.set(exported.trim(), target);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** `./react/charts` → `<repo>/src/react/charts/index.ts`, through the export map's `source` condition. */
|
|
116
|
+
const subpathSource = (subpath: string, exportsMap: Record<string, unknown>): string | null => {
|
|
117
|
+
const entry = exportsMap[subpath];
|
|
118
|
+
const target = typeof entry === 'string' ? entry : (entry as { source?: string } | undefined)?.source;
|
|
119
|
+
return typeof target === 'string' ? `${REPO_ROOT}/${target.replace(/^\.\//, '')}` : null;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export interface FixtureResolution {
|
|
123
|
+
/** Entry modules under `src/react` the fixture's imports land on. */
|
|
124
|
+
readonly entries: Map<string, FixtureImport>;
|
|
125
|
+
/** Imports nothing could attribute to a module — always a failure, never a skip. */
|
|
126
|
+
readonly unresolved: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const resolveFixtureModules = async (exportsMap: Record<string, unknown>): Promise<FixtureResolution> => {
|
|
130
|
+
const barrel = await barrelMap();
|
|
131
|
+
const entries = new Map<string, FixtureImport>();
|
|
132
|
+
const unresolved: string[] = [];
|
|
133
|
+
for (const imp of await fixtureImports()) {
|
|
134
|
+
const source = subpathSource(imp.subpath, exportsMap);
|
|
135
|
+
if (!source) {
|
|
136
|
+
unresolved.push(`${imp.file}: cursedbelt${imp.subpath.slice(1)} is not an export subpath`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// The two barrels re-export the package; attribute each NAME instead, or the
|
|
140
|
+
// fixture would appear to use every module there is.
|
|
141
|
+
if (source === `${REACT_DIR}/index.ts`) {
|
|
142
|
+
for (const name of imp.names) {
|
|
143
|
+
const target = barrel.get(name);
|
|
144
|
+
if (!target) {
|
|
145
|
+
unresolved.push(
|
|
146
|
+
`${imp.file}: \`${name}\` is not re-exported by name from src/react/index.ts, so no area can be ` +
|
|
147
|
+
'attributed to it — import it from its own subpath instead',
|
|
148
|
+
);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const resolved = `${REACT_DIR}/${target.replace(/^\.\//, '')}`;
|
|
152
|
+
for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx']) {
|
|
153
|
+
if (Bun.file(`${resolved}${ext}`).size > 0) {
|
|
154
|
+
entries.set(`${resolved}${ext}`, imp);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
entries.set(source, imp);
|
|
162
|
+
}
|
|
163
|
+
return { entries, unresolved };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Every module under `src/react` the fixture's imports actually reach. */
|
|
167
|
+
export const fixtureClosure = async (exportsMap: Record<string, unknown>): Promise<{
|
|
168
|
+
modules: Set<string>;
|
|
169
|
+
resolution: FixtureResolution;
|
|
170
|
+
}> => {
|
|
171
|
+
const graph = await buildGraph();
|
|
172
|
+
const resolution = await resolveFixtureModules(exportsMap);
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const queue = [...resolution.entries.keys()];
|
|
175
|
+
while (queue.length > 0) {
|
|
176
|
+
const next = queue.pop() as string;
|
|
177
|
+
if (seen.has(next)) continue;
|
|
178
|
+
seen.add(next);
|
|
179
|
+
for (const dep of graph.get(next) ?? []) queue.push(dep);
|
|
180
|
+
}
|
|
181
|
+
return { modules: seen, resolution };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Modules the fixture reaches that no declared area covers.
|
|
186
|
+
*
|
|
187
|
+
* 🔴 This, not the byte count, is the assertion that stops the saving being fake.
|
|
188
|
+
* An app under-declares its areas by adding a component and forgetting the
|
|
189
|
+
* `@import` — and the symptom is a component with no classes, three repos away,
|
|
190
|
+
* on a screen nobody opened during review.
|
|
191
|
+
*/
|
|
192
|
+
export const uncoveredModules = async (
|
|
193
|
+
declared: readonly string[],
|
|
194
|
+
exportsMap: Record<string, unknown>,
|
|
195
|
+
): Promise<string[]> => {
|
|
196
|
+
const { closures } = await areaClosures();
|
|
197
|
+
const covered = new Set<string>();
|
|
198
|
+
for (const name of [CORE, ...declared]) for (const file of closures.get(name) ?? []) covered.add(file);
|
|
199
|
+
const { modules } = await fixtureClosure(exportsMap);
|
|
200
|
+
const owners = (file: string): string[] =>
|
|
201
|
+
AREAS.filter((a) => closures.get(a.name)?.has(file)).map((a) => a.name);
|
|
202
|
+
return [...modules]
|
|
203
|
+
.filter((file) => !covered.has(file))
|
|
204
|
+
.sort()
|
|
205
|
+
.map((file) => `${file.replace(`${REPO_ROOT}/`, '')} — covered by: ${owners(file).join(', ') || 'NO AREA'}`);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Declared areas whose `needs` the fixture's stylesheet does not also import. */
|
|
209
|
+
export const missingNeeds = (declared: readonly string[]): string[] => {
|
|
210
|
+
const have = new Set(declared);
|
|
211
|
+
return declared
|
|
212
|
+
.flatMap((name) => transitiveNeeds(name).filter((need) => !have.has(need)).map((need) => `${name} needs ${need}`))
|
|
213
|
+
.sort();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ── the two compiles ──────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/** Everything is resolved against `src/`, the way `styles.css`'s own `@import`s are. */
|
|
219
|
+
const STYLES_BASE = `${REPO_ROOT}/src`;
|
|
220
|
+
|
|
221
|
+
const buildCss = async (input: string): Promise<string> => {
|
|
222
|
+
const compiled = await compileStylesheet(input);
|
|
223
|
+
return compiled.build(new Scanner({ sources: compiled.sources }).scan());
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** The fixture's own classes, whichever route it takes. */
|
|
227
|
+
const HEAD = `@import "tailwindcss" source(none);\n@source "${FIXTURE_DIR}";\n@import "./styles-static.css";\n`;
|
|
228
|
+
const TAIL = '@import "./theme.css";\n';
|
|
229
|
+
|
|
230
|
+
export const unionRouteCss = (): Promise<string> => buildCss(`${HEAD}@import "./styles-utilities.css";\n${TAIL}`);
|
|
231
|
+
|
|
232
|
+
export const areaRouteCss = (declared: readonly string[]): Promise<string> =>
|
|
233
|
+
buildCss(`${HEAD}${declared.map((a) => `@import "./styles-areas/${a}.css";`).join('\n')}\n${TAIL}`);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* What the fixture NEEDS: its own classes plus the classes of exactly the
|
|
237
|
+
* cursedbelt modules it reaches, and nothing else. The area route must be a
|
|
238
|
+
* superset of this or it is an unstyled app wearing a small byte count.
|
|
239
|
+
*/
|
|
240
|
+
export const requiredCss = async (exportsMap: Record<string, unknown>): Promise<string> => {
|
|
241
|
+
const { modules } = await fixtureClosure(exportsMap);
|
|
242
|
+
const compiled = await compileStylesheet(`${HEAD}${TAIL}`);
|
|
243
|
+
const own = new Scanner({ sources: compiled.sources }).scan();
|
|
244
|
+
const contents = await Promise.all(
|
|
245
|
+
[...modules].sort().map(async (file) => ({
|
|
246
|
+
content: await Bun.file(file).text(),
|
|
247
|
+
extension: file.slice(file.lastIndexOf('.') + 1),
|
|
248
|
+
})),
|
|
249
|
+
);
|
|
250
|
+
const mine = new Scanner({}).scanFiles(contents);
|
|
251
|
+
return compiled.build([...new Set([...own, ...mine])].sort());
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export { STYLES_BASE };
|
|
255
|
+
|
|
256
|
+
// ── the baseline ──────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export interface Baseline {
|
|
259
|
+
readonly header: string;
|
|
260
|
+
readonly counts: Map<string, number>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const ROUTES = ['union', 'areas'] as const;
|
|
264
|
+
|
|
265
|
+
export const parseBaseline = (text: string): Baseline => {
|
|
266
|
+
const header: string[] = [];
|
|
267
|
+
const counts = new Map<string, number>();
|
|
268
|
+
for (const line of text.split('\n')) {
|
|
269
|
+
if (line.startsWith('#') || line.trim() === '') {
|
|
270
|
+
if (counts.size === 0) header.push(line);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const [bytes, route] = line.trim().split(/\s+/);
|
|
274
|
+
if (bytes && route) counts.set(route, Number(bytes));
|
|
275
|
+
}
|
|
276
|
+
return { header: header.join('\n'), counts };
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export const formatBaseline = (header: string, counts: Map<string, number>): string =>
|
|
280
|
+
`${header}\n${ROUTES.map((r) => `${String(counts.get(r) ?? 0).padStart(9)} ${r}`).join('\n')}\n`;
|
|
281
|
+
|
|
282
|
+
export const readBaseline = async (): Promise<Baseline> => {
|
|
283
|
+
const file = Bun.file(BASELINE);
|
|
284
|
+
return (await file.exists()) ? parseBaseline(await file.text()) : { header: '', counts: new Map() };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export interface Measurement {
|
|
288
|
+
readonly declared: string[];
|
|
289
|
+
readonly union: number;
|
|
290
|
+
readonly areas: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const measure = async (): Promise<Measurement> => {
|
|
294
|
+
const declared = await declaredAreas();
|
|
295
|
+
const [union, areas] = await Promise.all([unionRouteCss(), areaRouteCss(declared)]);
|
|
296
|
+
return { declared, union: union.length, areas: areas.length };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (import.meta.main) {
|
|
300
|
+
const { declared, union, areas } = await measure();
|
|
301
|
+
const baseline = await readBaseline();
|
|
302
|
+
const now = new Map<string, number>([
|
|
303
|
+
['union', union],
|
|
304
|
+
['areas', areas],
|
|
305
|
+
]);
|
|
306
|
+
const saved = union - areas;
|
|
307
|
+
console.log(
|
|
308
|
+
`${FIXTURE} — ${declared.length} areas (${declared.join(', ')})\n` +
|
|
309
|
+
` union route ${union.toLocaleString().padStart(9)} B\n` +
|
|
310
|
+
` area route ${areas.toLocaleString().padStart(9)} B (−${saved.toLocaleString()} B, ` +
|
|
311
|
+
`${((saved / union) * 100).toFixed(1)}% smaller)`,
|
|
312
|
+
);
|
|
313
|
+
if (process.argv.includes('--prune')) {
|
|
314
|
+
const next = new Map(baseline.counts);
|
|
315
|
+
let lowered = 0;
|
|
316
|
+
for (const route of ROUTES) {
|
|
317
|
+
const recorded = baseline.counts.get(route);
|
|
318
|
+
const measured = now.get(route) as number;
|
|
319
|
+
if (recorded === undefined || measured < recorded) {
|
|
320
|
+
next.set(route, measured);
|
|
321
|
+
lowered++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (lowered === 0) {
|
|
325
|
+
console.log('\nnothing to prune — the baseline already records the smaller number.');
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
328
|
+
await Bun.write(BASELINE, formatBaseline(baseline.header, next));
|
|
329
|
+
console.log(`\n✅ lowered ${lowered} baseline entry/entries in ${BASELINE}.`);
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
const grown = ROUTES.filter((r) => (now.get(r) as number) > (baseline.counts.get(r) ?? Number.POSITIVE_INFINITY));
|
|
333
|
+
if (grown.length > 0) {
|
|
334
|
+
console.error(
|
|
335
|
+
`\n❌ the fixture app's CSS GREW:\n` +
|
|
336
|
+
grown
|
|
337
|
+
.map((r) => ` ${r}: baseline allows ${baseline.counts.get(r)} B, it emits ${now.get(r)} B now`)
|
|
338
|
+
.join('\n') +
|
|
339
|
+
'\n Raise the number by hand, in a commit that says why — `--prune` only lowers.',
|
|
340
|
+
);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|