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,253 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Generate `src/styles-areas/*.css` — cursedbelt's utilities, PRECOMPILED PER AREA,
|
|
4
|
+
* so a consuming app pays for the components it renders instead of for the union.
|
|
5
|
+
*
|
|
6
|
+
* ── The number this exists to move ──────────────────────────────────────────
|
|
7
|
+
* `cursedbelt/styles.css` and the `styles-static.css` + `styles-utilities.css`
|
|
8
|
+
* pair emit the same union — the 135,470 B of `src/styles-utilities.css`,
|
|
9
|
+
* measured, whichever route an app takes — because `@source` cannot be aimed at
|
|
10
|
+
* anything finer than a directory and `VirtualList`/`VirtualCardGrid` live at the root of `src/react`.
|
|
11
|
+
* Five apps ship that union today; the heaviest of them draws a 3D orbit scene, a
|
|
12
|
+
* map frame, a lightbox and two lazy document viewers, and none of them renders
|
|
13
|
+
* more than a fraction of the design system.
|
|
14
|
+
*
|
|
15
|
+
* `scripts/styleAreas.ts` is the map and owns the WHY. This file is the compiler:
|
|
16
|
+
* one stylesheet per area, each holding the candidates in that area's import
|
|
17
|
+
* closure MINUS the ones `core` (and whatever the area declares in `needs`)
|
|
18
|
+
* already ships, so importing core plus five areas duplicates nothing. A consumer's stylesheet becomes:
|
|
19
|
+
*
|
|
20
|
+
* @import "tailwindcss";
|
|
21
|
+
* @import "cursedbelt/styles-static.css";
|
|
22
|
+
* @import "cursedbelt/styles-areas/core.css";
|
|
23
|
+
* @import "cursedbelt/styles-areas/data-table.css"; ← only what it renders
|
|
24
|
+
* @import "cursedbelt/theme.css";
|
|
25
|
+
*
|
|
26
|
+
* and it scans nothing of this package's source tree, exactly as the
|
|
27
|
+
* `styles-utilities.css` route already arranged.
|
|
28
|
+
*
|
|
29
|
+
* ── What it is worth, measured on the fixture app ───────────────────────────
|
|
30
|
+
* `demo/fixtures/lean-app` is a real consumer built out of this package's own
|
|
31
|
+
* deep subpaths: a releases console with stat tiles, a chart, a sortable table
|
|
32
|
+
* and a virtualized log. Compiled both ways (tailwindcss 4.3.3, unminified, in
|
|
33
|
+
* this repo, both counts including the app's own utilities and the theme):
|
|
34
|
+
*
|
|
35
|
+
* styles-static.css + styles-utilities.css (the union) 169,517 B
|
|
36
|
+
* styles-static.css + core + charts + data-table
|
|
37
|
+
* + stats + virtual 119,432 B −29.5%
|
|
38
|
+
*
|
|
39
|
+
* Of that, the cursedbelt half falls from 142,119 B to 92,034 B — a 35% cut, and
|
|
40
|
+
* the fixture is not a minimal app: it declares five of the 43 areas. `core`
|
|
41
|
+
* alone is 66,700 B against the 135,470 B union artifact, so the floor for an app
|
|
42
|
+
* that renders only primitives is roughly half of what every app ships today.
|
|
43
|
+
*
|
|
44
|
+
* 🔴 The saving is bounded by how coarse `core` is, NOT by this mechanism. An area
|
|
45
|
+
* can be one file — `scripts/styleAreas.ts` scans a file LIST, so nothing here has
|
|
46
|
+
* a directory's granularity in it — and splitting `core` further is a table edit
|
|
47
|
+
* with a regenerate, not a redesign.
|
|
48
|
+
*
|
|
49
|
+
* `src/fixtureAppCss.spec.ts` is what holds that number down — a baseline that
|
|
50
|
+
* fails when the emitted CSS for the fixture GROWS, which is the check nothing in
|
|
51
|
+
* this package had while three, then four, then five apps paid for the union.
|
|
52
|
+
*
|
|
53
|
+
* ── The trap, and why nothing here is allowed to be hand-written ────────────
|
|
54
|
+
* Every failure this mechanism can have is SILENT in the same way the
|
|
55
|
+
* `styles-utilities.css` split was: a missing area, a stale artifact and a
|
|
56
|
+
* collapsed scan all produce CSS that is smaller, and smaller is what the whole
|
|
57
|
+
* exercise was asked for. So the area map is CLOSURE-derived rather than listed
|
|
58
|
+
* (an area cannot be missing what its own components import),
|
|
59
|
+
* `coverageFault()` fails on a module no area reaches, and
|
|
60
|
+
* `src/stylesAreas.spec.ts` proves core-plus-every-area emits the union rule for
|
|
61
|
+
* rule before any of it is allowed to ship.
|
|
62
|
+
*
|
|
63
|
+
* Usage:
|
|
64
|
+
* bun run styles:areas # write src/styles-areas/*.css
|
|
65
|
+
* bun run styles:areas --check # exit 1 if any is stale, write nothing
|
|
66
|
+
*/
|
|
67
|
+
import { REPO_ROOT, STATIC_STYLESHEET, readStylesheets } from './generateStaticStyles';
|
|
68
|
+
import { UTILITY_INPUT, compileStylesheet } from './generateUtilityStyles';
|
|
69
|
+
import {
|
|
70
|
+
AREA_DIR,
|
|
71
|
+
AREAS,
|
|
72
|
+
type Area,
|
|
73
|
+
CORE,
|
|
74
|
+
areaClosures,
|
|
75
|
+
candidatesIn,
|
|
76
|
+
coverageFault,
|
|
77
|
+
transitiveNeeds,
|
|
78
|
+
} from './styleAreas';
|
|
79
|
+
|
|
80
|
+
export { AREA_DIR, AREAS, CORE };
|
|
81
|
+
|
|
82
|
+
/** The stylesheet an area is written to, relative to the repo root. */
|
|
83
|
+
export const areaFile = (name: string): string => `${AREA_DIR}/${name}.css`;
|
|
84
|
+
|
|
85
|
+
/** The export subpath a consumer imports it by. */
|
|
86
|
+
export const areaSubpath = (name: string): string => `./${AREA_DIR.replace(/^src\//, '')}/${name}.css`;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A floor on `core`'s scan, for the same reason `generateUtilityStyles.ts` has
|
|
90
|
+
* one: a collapsed scan compiles and writes an EMPTY stylesheet, and every app
|
|
91
|
+
* that imported it renders unstyled with nothing red anywhere. 5,170 candidates
|
|
92
|
+
* today; a tripwire, not a ratchet.
|
|
93
|
+
*/
|
|
94
|
+
export const MIN_CORE_CANDIDATES = 1_000;
|
|
95
|
+
|
|
96
|
+
const banner = (area: Area, candidates: number, subtracted: boolean): string => {
|
|
97
|
+
const needs = transitiveNeeds(area.name);
|
|
98
|
+
const imports = [CORE, ...needs, ...(subtracted ? [area.name] : [])]
|
|
99
|
+
.filter((n, i, all) => all.indexOf(n) === i)
|
|
100
|
+
.map((n) => ` * @import "cursedbelt/styles-areas/${n}.css";`)
|
|
101
|
+
.join('\n');
|
|
102
|
+
return `/*
|
|
103
|
+
* cursedbelt — the \`${area.name}\` area. GENERATED BUILD ARTIFACT. Never hand-edit.
|
|
104
|
+
*
|
|
105
|
+
* ${area.why}.
|
|
106
|
+
*
|
|
107
|
+
* ${
|
|
108
|
+
subtracted
|
|
109
|
+
? `Every Tailwind utility the modules in this area's import closure use, MINUS the\n * ones ${[CORE, ...needs].map((n) => `\`styles-areas/${n}.css\``).join(' and ')} already ship${needs.length > 0 ? '' : 's'}. 🔴 Import ALL of these, in\n * this order — this file alone is incomplete:`
|
|
110
|
+
: 'Every Tailwind utility the primitives and their plumbing use. Import it first,\n * before any other area:'
|
|
111
|
+
}
|
|
112
|
+
*
|
|
113
|
+
* @import "tailwindcss";
|
|
114
|
+
* @import "cursedbelt/styles-static.css";
|
|
115
|
+
${imports}
|
|
116
|
+
* @import "cursedbelt/theme.css";
|
|
117
|
+
*
|
|
118
|
+
* An app that imports every area gets exactly what \`cursedbelt/styles-utilities.css\`
|
|
119
|
+
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
120
|
+
* when this file is stale. ${candidates} candidate(s) scanned.
|
|
121
|
+
*
|
|
122
|
+
* Written by scripts/generateAreaStyles.ts (\`bun run styles:areas\`), which
|
|
123
|
+
* \`bun run build\` runs. The map of what is in which area, and why the areas are
|
|
124
|
+
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
125
|
+
*/
|
|
126
|
+
`;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export interface BuiltArea {
|
|
130
|
+
readonly name: string;
|
|
131
|
+
readonly css: string;
|
|
132
|
+
/** Candidates this stylesheet was compiled from, after core was subtracted. */
|
|
133
|
+
readonly candidates: string[];
|
|
134
|
+
/** Every module in the area's closure, repo-relative — what the spec reports on. */
|
|
135
|
+
readonly modules: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compile every area.
|
|
140
|
+
*
|
|
141
|
+
* Refuses to run against a stale `styles-static.css` for the same reason
|
|
142
|
+
* `generateUtilityStyles.ts` does: these are compiled against it through
|
|
143
|
+
* `@reference`, so a stale one bakes in the wrong tokens with nothing to show for
|
|
144
|
+
* it in the output.
|
|
145
|
+
*/
|
|
146
|
+
export const buildAreaStyles = async (): Promise<BuiltArea[]> => {
|
|
147
|
+
const { expected, onDisk } = await readStylesheets();
|
|
148
|
+
if (onDisk !== expected) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`${STATIC_STYLESHEET} is ${onDisk === null ? 'missing' : 'stale'}, and these artifacts are compiled\n` +
|
|
151
|
+
`against it (\`@reference\`), so they would bake in the wrong tokens. Run:\n` +
|
|
152
|
+
` cd ${REPO_ROOT} && bun run styles:static`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const { closures } = await areaClosures();
|
|
156
|
+
const fault = coverageFault(closures);
|
|
157
|
+
if (fault) throw new Error(fault);
|
|
158
|
+
|
|
159
|
+
const coreClosure = closures.get(CORE) as Set<string>;
|
|
160
|
+
const coreCandidates = await candidatesIn(coreClosure);
|
|
161
|
+
if (coreCandidates.size < MIN_CORE_CANDIDATES) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`the scan of \`${CORE}\` found ${coreCandidates.size} candidates (floor ${MIN_CORE_CANDIDATES}).\n` +
|
|
164
|
+
'That is a collapsed scan, not a small core — the stylesheet it would write emits an\n' +
|
|
165
|
+
'empty utility layer and every consuming app renders UNSTYLED.',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Every area's raw candidate set first: `needs` subtracts one area from another,
|
|
170
|
+
// so no area can be compiled until all of them have been scanned.
|
|
171
|
+
const raw = new Map<string, Set<string>>([[CORE, coreCandidates]]);
|
|
172
|
+
for (const area of AREAS) {
|
|
173
|
+
if (area.name === CORE) continue;
|
|
174
|
+
raw.set(area.name, await candidatesIn(closures.get(area.name) as Set<string>));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const built: BuiltArea[] = [];
|
|
178
|
+
for (const area of AREAS) {
|
|
179
|
+
const closure = closures.get(area.name) as Set<string>;
|
|
180
|
+
const needs = transitiveNeeds(area.name);
|
|
181
|
+
const subtract =
|
|
182
|
+
area.name === CORE
|
|
183
|
+
? new Set<string>()
|
|
184
|
+
: new Set([...coreCandidates, ...needs.flatMap((n) => [...(raw.get(n) as Set<string>)])]);
|
|
185
|
+
const only = [...(raw.get(area.name) as Set<string>)].filter((c) => !subtract.has(c)).sort();
|
|
186
|
+
// 🔴 A fresh compile per area. The object `compile()` returns ACCUMULATES the
|
|
187
|
+
// candidates it has been built with, so reusing one would make every area after
|
|
188
|
+
// the first a superset of the ones before it — and each file would still look
|
|
189
|
+
// plausible on its own. Found by measuring; the symptom was areas growing
|
|
190
|
+
// monotonically in the order they were compiled.
|
|
191
|
+
const compiled = await compileStylesheet(UTILITY_INPUT);
|
|
192
|
+
built.push({
|
|
193
|
+
name: area.name,
|
|
194
|
+
css: `${banner(area, only.length, area.name !== CORE)}${compiled.build(only)}`,
|
|
195
|
+
candidates: only,
|
|
196
|
+
modules: [...closure].map((f) => f.replace(`${REPO_ROOT}/`, '')).sort(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return built;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** What each area's stylesheet should contain, and what is on disk right now. */
|
|
203
|
+
export const readAreaStyles = async (): Promise<Array<BuiltArea & { onDisk: string | null }>> => {
|
|
204
|
+
const built = await buildAreaStyles();
|
|
205
|
+
return Promise.all(
|
|
206
|
+
built.map(async (area) => {
|
|
207
|
+
const target = Bun.file(`${REPO_ROOT}/${areaFile(area.name)}`);
|
|
208
|
+
return { ...area, onDisk: (await target.exists()) ? await target.text() : null };
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Area stylesheets on disk that `AREAS` no longer names — a renamed area leaves one behind. */
|
|
214
|
+
export const strayAreaFiles = (): string[] => {
|
|
215
|
+
const known = new Set(AREAS.map((a) => `${a.name}.css`));
|
|
216
|
+
return [...new Bun.Glob('*.css').scanSync({ cwd: `${REPO_ROOT}/${AREA_DIR}`, onlyFiles: true })]
|
|
217
|
+
.filter((f) => !known.has(f))
|
|
218
|
+
.sort();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (import.meta.main) {
|
|
222
|
+
const areas = await readAreaStyles();
|
|
223
|
+
const stale = areas.filter((a) => a.onDisk !== a.css);
|
|
224
|
+
const stray = strayAreaFiles();
|
|
225
|
+
if (process.argv.includes('--check')) {
|
|
226
|
+
if (stale.length === 0 && stray.length === 0) {
|
|
227
|
+
console.log(`✅ ${AREA_DIR}/ is current (${areas.length} areas).`);
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
console.error(
|
|
231
|
+
`❌ ${AREA_DIR}/ is out of date:\n` +
|
|
232
|
+
stale.map((a) => ` ${a.onDisk === null ? 'missing' : 'stale'} ${areaFile(a.name)}`).join('\n') +
|
|
233
|
+
stray.map((f) => `\n no longer an area ${AREA_DIR}/${f}`).join('') +
|
|
234
|
+
`\n Run:\n cd ${REPO_ROOT} && bun run styles:areas`,
|
|
235
|
+
);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
for (const area of stale) await Bun.write(`${REPO_ROOT}/${areaFile(area.name)}`, area.css);
|
|
239
|
+
const total = areas.reduce((sum, a) => sum + a.css.length, 0);
|
|
240
|
+
console.log(
|
|
241
|
+
stale.length === 0
|
|
242
|
+
? `✅ ${AREA_DIR}/ already current — ${areas.length} areas, ${total.toLocaleString()} B in total.`
|
|
243
|
+
: `✅ wrote ${stale.length} of ${areas.length} area stylesheet(s) into ${AREA_DIR}/ ` +
|
|
244
|
+
`(${total.toLocaleString()} B in total; core is ${areas[0]?.css.length.toLocaleString()} B).`,
|
|
245
|
+
);
|
|
246
|
+
if (stray.length > 0) {
|
|
247
|
+
console.error(
|
|
248
|
+
`\n❌ ${AREA_DIR}/ still holds ${stray.length} stylesheet(s) no area names — delete them:\n ` +
|
|
249
|
+
stray.map((f) => `${REPO_ROOT}/${AREA_DIR}/${f}`).join('\n '),
|
|
250
|
+
);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -45,6 +45,68 @@ describe('no-raw-action-button', () => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// Filed 2026-09-15 after three false positives in three repos, all one defect: the tag
|
|
49
|
+
// walker stopped at the first `>` it saw, including one written in the PROSE of a comment
|
|
50
|
+
// inside the attribute list — which made every attribute below that point invisible, opt-out
|
|
51
|
+
// comment included. The third sighting was the fix for the second: a comment explaining this
|
|
52
|
+
// bug, containing a literal `>`, reproduced it.
|
|
53
|
+
describe('opening-tag extraction (comment-aware)', () => {
|
|
54
|
+
test('a prose "<select>" in a comment does not hide the opt-out below it', () => {
|
|
55
|
+
const source = `<button
|
|
56
|
+
// A radio-like list rather than a <select>, so the row is the control.
|
|
57
|
+
// guardrails-ignore no-raw-action-button: presentational row in a radio-like list
|
|
58
|
+
type="button"
|
|
59
|
+
>Move here</button>`;
|
|
60
|
+
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a prose angle bracket does not hide a role="radio" below it', () => {
|
|
64
|
+
const source = `<button
|
|
65
|
+
/* A radio-like list rather than a <select>. */
|
|
66
|
+
role="radio"
|
|
67
|
+
aria-checked={selected}
|
|
68
|
+
>Move here</button>`;
|
|
69
|
+
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('an arrow-function attribute still does not end the tag', () => {
|
|
73
|
+
const source = `<button
|
|
74
|
+
onClick={() => setOpen(true)}
|
|
75
|
+
// guardrails-ignore no-raw-action-button: virtual gamepad key, a real button for AT
|
|
76
|
+
type="button"
|
|
77
|
+
>A</button>`;
|
|
78
|
+
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
79
|
+
// …and the walker still finds the end of that tag, rather than running on.
|
|
80
|
+
expect(check('no-raw-action-button', '<button onClick={() => save()}>Save</button>')).toHaveLength(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a "//" inside an attribute string is a URL, not a comment', () => {
|
|
84
|
+
const source = '<button data-href="https://example.com/x">Go</button><button role="tab">Tab</button>';
|
|
85
|
+
expect(check('no-raw-action-button', source)).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a comment does not extend one tag over the NEXT element\'s ignore', () => {
|
|
89
|
+
const source = `<button
|
|
90
|
+
// A radio-like list rather than a <select>.
|
|
91
|
+
type="button"
|
|
92
|
+
>One</button>
|
|
93
|
+
<button
|
|
94
|
+
// guardrails-ignore no-raw-action-button: presentational row
|
|
95
|
+
type="button"
|
|
96
|
+
>Two</button>`;
|
|
97
|
+
expect(check('no-raw-action-button', source)).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the same defect on <input>: prose above the opt-out', () => {
|
|
101
|
+
const source = `<input
|
|
102
|
+
// A raw input rather than a <select>-backed Field — the native file picker.
|
|
103
|
+
// guardrails-ignore no-raw-inputs: native file picker, no Field equivalent
|
|
104
|
+
type="file"
|
|
105
|
+
/>`;
|
|
106
|
+
expect(check('no-raw-inputs', source)).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
48
110
|
describe('no-raw-overflow', () => {
|
|
49
111
|
test('flags inline style overflow: auto', () => {
|
|
50
112
|
expect(check('no-raw-overflow', '<div style={{ overflow: "auto" }} />')).toHaveLength(1);
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
// run, so these stay few and visible. A malformed/unrecognized-category directive is
|
|
148
148
|
// ignored (its violations still fire) and a warning is printed.
|
|
149
149
|
|
|
150
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
150
|
+
import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
151
151
|
import { basename, extname, join, relative } from "node:path";
|
|
152
152
|
|
|
153
153
|
const IGNORE_DIR_NAMES = new Set(["node_modules", "dist", "dist-deploy", ".git", "build", "coverage"]);
|
|
@@ -179,7 +179,15 @@ interface Rule {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
export function walk(dir: string, out: string[] = []): string[] {
|
|
182
|
-
|
|
182
|
+
// 🔴 `Dirent[]`, never `ReturnType<typeof readdirSync>`. `readdirSync` is OVERLOADED, and
|
|
183
|
+
// `ReturnType` of an overloaded function is the LAST overload — which @types/node 24 made
|
|
184
|
+
// `Dirent<NonSharedBuffer>[]` when `Dirent` became generic, while the call below returns
|
|
185
|
+
// the string-encoding overload's `Dirent<string>[]`. The annotation was always a guess at
|
|
186
|
+
// an overload set somebody else maintains; a release reordered it and the guess became
|
|
187
|
+
// five compile errors that every consumer of `cursedbelt/guardrails` inherited (task 172).
|
|
188
|
+
// Bare `Dirent` is `Dirent<string>` under >=24's default type argument and is the plain
|
|
189
|
+
// non-generic class before that, so this spelling is correct on both.
|
|
190
|
+
let entries: Dirent[];
|
|
183
191
|
try {
|
|
184
192
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
185
193
|
} catch {
|
|
@@ -201,13 +209,69 @@ export function walk(dir: string, out: string[] = []): string[] {
|
|
|
201
209
|
return out;
|
|
202
210
|
}
|
|
203
211
|
|
|
212
|
+
/** Index just past the string/template literal opening at `start`. Quoted attribute values
|
|
213
|
+
* are skipped so a `//` inside one reads as the URL it is (`src="https://…"`) rather than
|
|
214
|
+
* as the start of a comment. `'`/`"` stop at a newline — an unterminated quote must not
|
|
215
|
+
* swallow the rest of the file; a backtick may span lines and nests `${…}`. */
|
|
216
|
+
function skipQuoted(source: string, start: number): number {
|
|
217
|
+
const quote = source[start];
|
|
218
|
+
let i = start + 1;
|
|
219
|
+
if (quote !== "`") {
|
|
220
|
+
while (i < source.length && source[i] !== quote && source[i] !== "\n") {
|
|
221
|
+
i += source[i] === "\\" ? 2 : 1;
|
|
222
|
+
}
|
|
223
|
+
return Math.min(i + 1, source.length);
|
|
224
|
+
}
|
|
225
|
+
let depth = 0;
|
|
226
|
+
while (i < source.length) {
|
|
227
|
+
if (source[i] === "\\") {
|
|
228
|
+
i += 2;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (depth === 0 && source[i] === "`") return i + 1;
|
|
232
|
+
if (source.slice(i, i + 2) === "${") {
|
|
233
|
+
depth++;
|
|
234
|
+
i += 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (depth > 0 && source[i] === "}") depth--;
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
return i;
|
|
241
|
+
}
|
|
242
|
+
|
|
204
243
|
/** Slice from a `<button` (or similar) match to the end of its opening tag, tolerating
|
|
205
|
-
* `{...}` JS expressions in attribute values that may themselves contain
|
|
244
|
+
* `{...}` JS expressions in attribute values that may themselves contain `>`
|
|
245
|
+
* (`onClick={() => …}`, which works because that `>` sits inside braces).
|
|
246
|
+
*
|
|
247
|
+
* JSX permits line and block comments INSIDE an attribute list, and a comment is prose —
|
|
248
|
+
* so it may contain an angle bracket ("a radio-like list rather than a `<select>`").
|
|
249
|
+
* Those are skipped the same way `blankComments` skips them, because a `>` written in prose
|
|
250
|
+
* used to end the tag here and make every attribute below it invisible — the
|
|
251
|
+
* `guardrails-ignore` opt-out included. `blankComments` cannot simply be run first: the
|
|
252
|
+
* opt-out checks read this slice and must see the ORIGINAL comment text (see the
|
|
253
|
+
* no-raw-action-button check below), so the skip only suppresses TERMINATION; the comment
|
|
254
|
+
* stays in what is returned. */
|
|
206
255
|
function extractOpeningTag(source: string, startIndex: number): string {
|
|
207
256
|
let i = startIndex;
|
|
208
257
|
let braceDepth = 0;
|
|
209
258
|
while (i < source.length) {
|
|
259
|
+
const two = source.slice(i, i + 2);
|
|
260
|
+
if (two === "//") {
|
|
261
|
+
const end = source.indexOf("\n", i);
|
|
262
|
+
i = end === -1 ? source.length : end;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (two === "/*") {
|
|
266
|
+
const end = source.indexOf("*/", i + 2);
|
|
267
|
+
i = end === -1 ? source.length : end + 2;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
210
270
|
const ch = source[i];
|
|
271
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
272
|
+
i = skipQuoted(source, i);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
211
275
|
if (ch === "{") braceDepth++;
|
|
212
276
|
else if (ch === "}") braceDepth--;
|
|
213
277
|
else if (ch === ">" && braceDepth <= 0) {
|