cursedbelt 3.0.2 → 4.1.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 +130 -32
- 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/paths.ts +106 -0
- 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/markdown/MarkdownDoc.spec.tsx +1 -1
- 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,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AREA MAP — which of cursedbelt's own modules a consuming app has to pay for,
|
|
3
|
+
* and which it can decline.
|
|
4
|
+
*
|
|
5
|
+
* ── What this is for ────────────────────────────────────────────────────────
|
|
6
|
+
* `cursedbelt/styles.css` ends in `@source "."`, so every app that imports it
|
|
7
|
+
* emits the union of the whole design system — the 135,470 B of utilities in
|
|
8
|
+
* `src/styles-utilities.css` — whether it renders three components or eighty. `styles-utilities.css` precompiled that
|
|
9
|
+
* union so the app stops scanning this package's tree, but it is the SAME union —
|
|
10
|
+
* measured at 54 bytes' difference (see `scripts/generateUtilityStyles.ts`).
|
|
11
|
+
*
|
|
12
|
+
* The reason no consumer could do better is that Tailwind v4 resolves every
|
|
13
|
+
* `@source` to a DIRECTORY and scans it recursively — a file path and a glob are
|
|
14
|
+
* both silently widened to their parent — so the finest grain an app can ask for
|
|
15
|
+
* is `src/react`, whose recursive contents ARE the design system. flix measured
|
|
16
|
+
* the ceiling from the consumer side and found no move available to it; the third
|
|
17
|
+
* and fourth and fifth consumers each re-derived the same dead end, one of them
|
|
18
|
+
* with a 173-line `@source` list that could never have worked.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 The package is not stuck where the consumer is, and the lever is NOT its
|
|
21
|
+
* directory layout. `@tailwindcss/oxide`'s `Scanner.scanFiles()` takes a list of
|
|
22
|
+
* FILE CONTENTS, so cursedbelt can scan an arbitrary set of its own modules —
|
|
23
|
+
* per file, at any grain it likes — and ship the result as CSS. Moving
|
|
24
|
+
* `VirtualList`/`VirtualCardGrid` off the root of `src/react` into a `virtual/`
|
|
25
|
+
* directory (the breaking export change the task costed) buys exactly nothing
|
|
26
|
+
* that this does not, because nothing here asks Tailwind to walk a directory.
|
|
27
|
+
* The layout stays; the granularity comes from the file list.
|
|
28
|
+
*
|
|
29
|
+
* ── How an area is defined ──────────────────────────────────────────────────
|
|
30
|
+
* An area is a NAME and a set of ROOT modules. Its file set is the transitive
|
|
31
|
+
* import closure of those roots inside `src/react`, so an area is self-contained
|
|
32
|
+
* by construction: it can never be missing the CSS of something its own
|
|
33
|
+
* components render. That is the property a hand-listed area would get wrong on
|
|
34
|
+
* its second edit, and the failure would be invisible — a component that renders
|
|
35
|
+
* unstyled only in the apps that declined the area it silently needed.
|
|
36
|
+
*
|
|
37
|
+
* `core` is the area every consumer imports: the primitives, the theme plumbing
|
|
38
|
+
* and everything they reach. Every other area's stylesheet carries only what core
|
|
39
|
+
* — and whatever it declares in `needs` — does not already, so importing core plus
|
|
40
|
+
* five areas duplicates nothing.
|
|
41
|
+
*
|
|
42
|
+
* ── The two gates ───────────────────────────────────────────────────────────
|
|
43
|
+
* 1. COVERAGE — every non-spec module under `src/react` is in some area's
|
|
44
|
+
* closure. A new component that no area reaches would be in no stylesheet at
|
|
45
|
+
* all, i.e. unstyled in every app that adopted this. `coverageFault()` names
|
|
46
|
+
* the orphans; `src/stylesAreas.spec.ts` fails on them.
|
|
47
|
+
* 2. COMPLETENESS — core plus every area emits exactly the rules the union
|
|
48
|
+
* (`styles-utilities.css`) emits. Same assertion shape as
|
|
49
|
+
* `src/stylesUtilitiesMatches.spec.ts`, and for the same reason: an area split
|
|
50
|
+
* that loses a rule is invisible to `tsc`, to Biome, and to a byte count read
|
|
51
|
+
* in the direction everybody reads it.
|
|
52
|
+
*/
|
|
53
|
+
import { Scanner } from '@tailwindcss/oxide';
|
|
54
|
+
import { REPO_ROOT } from './generateStaticStyles';
|
|
55
|
+
|
|
56
|
+
/** Where every path below is relative to. */
|
|
57
|
+
export const REACT_DIR = `${REPO_ROOT}/src/react`;
|
|
58
|
+
|
|
59
|
+
/** Directory the generated per-area stylesheets are written to, relative to the repo root. */
|
|
60
|
+
export const AREA_DIR = 'src/styles-areas';
|
|
61
|
+
|
|
62
|
+
export interface Area {
|
|
63
|
+
/** File name (`<name>.css`) and the subpath a consumer imports. */
|
|
64
|
+
readonly name: string;
|
|
65
|
+
/** Why this is a separable purchase — an area nobody can explain is an area nobody can decline. */
|
|
66
|
+
readonly why: string;
|
|
67
|
+
/**
|
|
68
|
+
* Root modules, relative to `src/react`. A directory means its `index.ts`; a
|
|
69
|
+
* directory with no index means every module directly inside it. The area is
|
|
70
|
+
* the transitive closure of these, so this list is entry points, not contents.
|
|
71
|
+
*/
|
|
72
|
+
readonly roots: readonly string[];
|
|
73
|
+
/**
|
|
74
|
+
* Modules included WITHOUT walking their imports.
|
|
75
|
+
*
|
|
76
|
+
* For `src/react/index.ts` and nothing else so far: the barrel re-exports the
|
|
77
|
+
* entire package, so expanding it would pull every area into core and collapse
|
|
78
|
+
* this whole file back into the union it exists to break up. It renders nothing
|
|
79
|
+
* itself, so its own candidates are all it can contribute — but it is still a
|
|
80
|
+
* module under `src/react`, and coverage is deliberately the strict "every
|
|
81
|
+
* module is in some area" rather than a candidate comparison, because two areas
|
|
82
|
+
* can share a class without sharing the component that uses it.
|
|
83
|
+
*/
|
|
84
|
+
readonly leaves?: readonly string[];
|
|
85
|
+
/**
|
|
86
|
+
* Areas this one COMPOSES, and therefore subtracts as well as core.
|
|
87
|
+
*
|
|
88
|
+
* An area's stylesheet is its closure's candidates minus core's, so two areas
|
|
89
|
+
* whose closures overlap each ship the overlap and an app importing both pays
|
|
90
|
+
* for it twice. That is fine for the incidental overlap between, say, `chat` and
|
|
91
|
+
* `notifications` — a few hundred bytes — and it is not fine for a composite:
|
|
92
|
+
* `ChartTablePanel` renders a chart AND a DataTable, so without this it would
|
|
93
|
+
* carry a second copy of both, and `charts` would drag the whole DataTable into
|
|
94
|
+
* every app that draws a line chart.
|
|
95
|
+
*
|
|
96
|
+
* 🔴 A consumer MUST import an area's `needs` alongside it, which is what the
|
|
97
|
+
* generated banner says and what `src/fixtureAppCss.spec.ts` checks of the
|
|
98
|
+
* fixture's own stylesheet. `needs` is transitive and may not cycle.
|
|
99
|
+
*/
|
|
100
|
+
readonly needs?: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 🔴 `core` FIRST and always — the rest of this file subtracts it.
|
|
105
|
+
*
|
|
106
|
+
* What is in it: the primitives an app cannot render a screen without, plus the
|
|
107
|
+
* plumbing every one of them imports (hooks, lib, styling, icons, portal, slots,
|
|
108
|
+
* the colour-scheme and scale providers). What is deliberately NOT: anything a
|
|
109
|
+
* whole app can decline — charts, media, the spreadsheet, the calendar, the
|
|
110
|
+
* editors, the viewers, the gallery. Measured, that line puts `core.css` at
|
|
111
|
+
* 66,700 B against the union artifact's 135,470 — so an app that renders only
|
|
112
|
+
* primitives ships roughly half of what every app ships today, and an app that
|
|
113
|
+
* renders four families ships a little more than that.
|
|
114
|
+
*/
|
|
115
|
+
export const AREAS: readonly Area[] = [
|
|
116
|
+
{
|
|
117
|
+
name: 'core',
|
|
118
|
+
why: 'the primitives and the plumbing every screen renders; every consumer imports this one',
|
|
119
|
+
roots: [
|
|
120
|
+
'styling.ts',
|
|
121
|
+
'icons.tsx',
|
|
122
|
+
'compat.ts',
|
|
123
|
+
'authSignal.ts',
|
|
124
|
+
'sessionAuth.ts',
|
|
125
|
+
'createApiHooks.ts',
|
|
126
|
+
'usePagedList.ts',
|
|
127
|
+
'confirm.tsx',
|
|
128
|
+
'prompt.tsx',
|
|
129
|
+
'Toast.tsx',
|
|
130
|
+
'colorScheme',
|
|
131
|
+
'constants',
|
|
132
|
+
'layout',
|
|
133
|
+
'namespace',
|
|
134
|
+
'portal',
|
|
135
|
+
'slots',
|
|
136
|
+
'uiScale',
|
|
137
|
+
'hooks',
|
|
138
|
+
'lib',
|
|
139
|
+
'components/Accordion.tsx',
|
|
140
|
+
'components/ActionBar.tsx',
|
|
141
|
+
'components/ActionGroup.tsx',
|
|
142
|
+
'components/Alert.tsx',
|
|
143
|
+
'components/AsyncPanel.tsx',
|
|
144
|
+
'components/Avatar.tsx',
|
|
145
|
+
'components/AvatarStack.tsx',
|
|
146
|
+
'components/Badge.tsx',
|
|
147
|
+
'components/Button.tsx',
|
|
148
|
+
'components/ButtonGroup.tsx',
|
|
149
|
+
'components/ButtonLink.tsx',
|
|
150
|
+
'components/CardTapTarget.tsx',
|
|
151
|
+
'components/Checkbox.tsx',
|
|
152
|
+
'components/ChoiceMenu.tsx',
|
|
153
|
+
'components/Collapsible.tsx',
|
|
154
|
+
'components/ConfirmDialog.tsx',
|
|
155
|
+
'components/CopyButton.tsx',
|
|
156
|
+
'components/CopyableText.tsx',
|
|
157
|
+
'components/DeleteButton.tsx',
|
|
158
|
+
'components/DisclosureButton.tsx',
|
|
159
|
+
'components/DismissButton.tsx',
|
|
160
|
+
'components/DropdownMenu.tsx',
|
|
161
|
+
'components/EmptyState.tsx',
|
|
162
|
+
'components/ErrorBoundary.tsx',
|
|
163
|
+
'components/ErrorCard.tsx',
|
|
164
|
+
'components/Field.tsx',
|
|
165
|
+
'components/FieldLabel.tsx',
|
|
166
|
+
'components/Grid.tsx',
|
|
167
|
+
'components/HoverCard.tsx',
|
|
168
|
+
'components/IconButton.tsx',
|
|
169
|
+
'components/InfoHint.tsx',
|
|
170
|
+
'components/Input.tsx',
|
|
171
|
+
'components/Modal.tsx',
|
|
172
|
+
'components/OnlineDot.tsx',
|
|
173
|
+
'components/Page.tsx',
|
|
174
|
+
'components/PanelSeparator.tsx',
|
|
175
|
+
'components/PathText.tsx',
|
|
176
|
+
'components/Popover.tsx',
|
|
177
|
+
'components/Progress.tsx',
|
|
178
|
+
'components/QueryBoundary.tsx',
|
|
179
|
+
'components/ScrollArea.tsx',
|
|
180
|
+
'components/Section.tsx',
|
|
181
|
+
'components/SegmentedControl.tsx',
|
|
182
|
+
'components/SelectField.tsx',
|
|
183
|
+
'components/Skeleton.tsx',
|
|
184
|
+
'components/Slider.tsx',
|
|
185
|
+
'components/Spinner.tsx',
|
|
186
|
+
'components/SplitButton.tsx',
|
|
187
|
+
'components/StatusPill.tsx',
|
|
188
|
+
'components/Surface.tsx',
|
|
189
|
+
'components/Switch.tsx',
|
|
190
|
+
'components/Tabs.tsx',
|
|
191
|
+
'components/Tooltip.tsx',
|
|
192
|
+
'components/TooltipImpl.tsx',
|
|
193
|
+
'components/TruncatedText.tsx',
|
|
194
|
+
'components/dialog.tsx',
|
|
195
|
+
'components/layout.tsx',
|
|
196
|
+
'components/revealOnHover.ts',
|
|
197
|
+
],
|
|
198
|
+
leaves: ['index.ts'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'activity-bar',
|
|
202
|
+
why: 'the `cursedbelt/react/activity-bar` rail — app chrome most apps do not mount',
|
|
203
|
+
roots: ['activity-bar'],
|
|
204
|
+
},
|
|
205
|
+
{ name: 'analytics', why: 'the analytics panel and its drilldown', roots: ['analytics'] },
|
|
206
|
+
{
|
|
207
|
+
name: 'auth',
|
|
208
|
+
why: 'the sign-in / register surfaces and the aurora front door — one screen, and never in the app shell',
|
|
209
|
+
roots: ['components/auth', 'AuthGate.tsx'],
|
|
210
|
+
},
|
|
211
|
+
{ name: 'calendar', why: 'the date-picker family: a calendar grid is a whole layout nothing else shares', roots: ['components/calendar'] },
|
|
212
|
+
{ name: 'charts', why: 'the recharts wrappers — an app with no dashboard renders none of them', roots: ['charts'] },
|
|
213
|
+
{
|
|
214
|
+
name: 'chart-table',
|
|
215
|
+
why: 'ChartTablePanel, the chart-with-its-own-data-table composite — 14,838 B of it is the DataTable and the charts it already needs',
|
|
216
|
+
roots: ['components/ChartTablePanel.tsx'],
|
|
217
|
+
needs: ['charts', 'data-table'],
|
|
218
|
+
},
|
|
219
|
+
{ name: 'chat', why: 'the chat transcript, composer and message list', roots: ['chat'] },
|
|
220
|
+
{ name: 'clipboard', why: 'the clipboard-paste surfaces', roots: ['clipboard'] },
|
|
221
|
+
{ name: 'code-editor', why: 'the CodeMirror host and its chrome (an optional peer already)', roots: ['code-editor'] },
|
|
222
|
+
{ name: 'companion-link', why: 'the companion-device pairing flow', roots: ['companion-link'] },
|
|
223
|
+
{ name: 'dashboard-grid', why: 'the draggable widget grid', roots: ['dashboard-grid'] },
|
|
224
|
+
{ name: 'data-table', why: 'the DataTable — the largest single component in the package', roots: ['components/DataTable'] },
|
|
225
|
+
{ name: 'deep-link', why: 'the deep-link resolver surfaces', roots: ['deep-link'] },
|
|
226
|
+
{ name: 'diff-viewer', why: 'the side-by-side diff view', roots: ['diff-viewer'] },
|
|
227
|
+
{ name: 'disk-usage', why: 'the disk-usage treemap and its legend', roots: ['disk-usage'] },
|
|
228
|
+
{ name: 'emoji', why: 'the emoji picker — a whole third-party grid', roots: ['emoji', 'components/EmojiField.tsx', 'components/ReactionBar.tsx', 'components/ReactionPicker.tsx'] },
|
|
229
|
+
{
|
|
230
|
+
name: 'fields',
|
|
231
|
+
why: 'the long tail of form controls beyond Input/Select/Checkbox/Switch — an app renders a handful, never all',
|
|
232
|
+
roots: [
|
|
233
|
+
'components/Autocomplete.tsx',
|
|
234
|
+
'components/FieldInput.tsx',
|
|
235
|
+
'components/AutoGrowTextarea.tsx',
|
|
236
|
+
'components/CodeArea.tsx',
|
|
237
|
+
'components/CodeChip.tsx',
|
|
238
|
+
'components/CodeFieldInput.tsx',
|
|
239
|
+
'components/ColorField.tsx',
|
|
240
|
+
'components/ComboboxField.tsx',
|
|
241
|
+
'components/EnvCompare.tsx',
|
|
242
|
+
'components/EnvEditor.tsx',
|
|
243
|
+
'components/InlineEditableText.tsx',
|
|
244
|
+
'components/JsonEditor.tsx',
|
|
245
|
+
'components/MultiSelectField.tsx',
|
|
246
|
+
'components/NativeSelect.tsx',
|
|
247
|
+
'components/OtpInput.tsx',
|
|
248
|
+
'components/RatingField.tsx',
|
|
249
|
+
'components/RenderField.tsx',
|
|
250
|
+
'components/SecretInput.tsx',
|
|
251
|
+
'components/SliderField.tsx',
|
|
252
|
+
'components/StarRating.tsx',
|
|
253
|
+
'components/SwatchPicker.tsx',
|
|
254
|
+
'components/TagStrip.tsx',
|
|
255
|
+
'components/TagsInput.tsx',
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
{ name: 'file-tree', why: 'the flat (non-nested) file list and its row chrome', roots: ['file-tree'] },
|
|
259
|
+
{ name: 'filter-rail', why: 'the faceted filter rail', roots: ['filter-rail'] },
|
|
260
|
+
{ name: 'folder-tree', why: 'the nested folder tree', roots: ['folder-tree'] },
|
|
261
|
+
{ name: 'keep-awake', why: 'the keep-awake toggle and its badge', roots: ['keep-awake'] },
|
|
262
|
+
{ name: 'layout-engine', why: 'the persisted pane/grid layout engine — apps with a fixed layout render none of it', roots: ['layoutEngine'] },
|
|
263
|
+
{ name: 'markdown', why: 'the rendered-markdown document', roots: ['markdown', 'markdown/MarkdownDoc.tsx'] },
|
|
264
|
+
{ name: 'master-detail', why: 'the master/detail shell', roots: ['master-detail'] },
|
|
265
|
+
{ name: 'master-lock', why: 'the master-lock gate', roots: ['master-lock'] },
|
|
266
|
+
{
|
|
267
|
+
name: 'media',
|
|
268
|
+
why: 'the video/audio player, the upload client and the player strip',
|
|
269
|
+
roots: ['media', 'media/FrameGrabber.tsx', 'media/poster.ts'],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'media-gallery',
|
|
273
|
+
why: 'the photo album, the lightbox and the gallery chrome',
|
|
274
|
+
// `MediaMetaEditor` is named explicitly for the same reason `media/FrameGrabber.tsx` is
|
|
275
|
+
// two entries up: the gallery reaches it through `React.lazy(() => import(…))`, and the
|
|
276
|
+
// closure walk reads static specifiers only — so a lazily-imported dialog is an orphan
|
|
277
|
+
// unless it is its own root. The coverage gate is what says so, by name, at build time.
|
|
278
|
+
roots: ['media-gallery', 'media-gallery/MediaMetaEditor.tsx', 'components/Lightbox.tsx'],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'nav',
|
|
282
|
+
why: 'the AppShell chrome — the command palette, the nav rail, the place bar and the app menus',
|
|
283
|
+
roots: ['nav', 'nav/BreadcrumbMenus.tsx', 'nav/SideNavCustomize.tsx', 'components/AppShell.tsx'],
|
|
284
|
+
},
|
|
285
|
+
{ name: 'notifications', why: 'the notification centre and its feed', roots: ['notifications'] },
|
|
286
|
+
{
|
|
287
|
+
name: 'overlays',
|
|
288
|
+
why: 'the dialogs and drawers beyond the plain Modal/Confirm pair',
|
|
289
|
+
roots: [
|
|
290
|
+
'components/Drawer.tsx',
|
|
291
|
+
'components/DiscardConfirmDialog.tsx',
|
|
292
|
+
'components/ImpactDialog.tsx',
|
|
293
|
+
'components/PromptDialog.tsx',
|
|
294
|
+
'components/ElsewhereNotice.tsx',
|
|
295
|
+
'components/AddItemsMenu.tsx',
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
{ name: 'palette', why: 'the theme/colour palette editor', roots: ['palette'] },
|
|
299
|
+
{ name: 'pdf-viewer', why: 'the lazy PDF viewer (an optional peer already)', roots: ['pdf-viewer'] },
|
|
300
|
+
{ name: 'problem-detail', why: 'the RFC-7807 problem renderer', roots: ['problem-detail'] },
|
|
301
|
+
{ name: 'rich-text', why: 'the TipTap editor and its toolbar', roots: ['rich-text'] },
|
|
302
|
+
{ name: 'sharing', why: 'the share sheet and its permission rows', roots: ['sharing', 'components/sharing'] },
|
|
303
|
+
{ name: 'spreadsheet', why: 'the glide-data-grid host (an optional peer already)', roots: ['spreadsheet'] },
|
|
304
|
+
{
|
|
305
|
+
name: 'stats',
|
|
306
|
+
why: 'the KPI/meter/timeline family — dashboards render them, forms and lists never do',
|
|
307
|
+
roots: [
|
|
308
|
+
'components/EventTimeline.tsx',
|
|
309
|
+
'components/FindingsList.tsx',
|
|
310
|
+
'components/HudPill.tsx',
|
|
311
|
+
'components/LogViewer.tsx',
|
|
312
|
+
'components/MeterList.tsx',
|
|
313
|
+
'components/Sparkline.tsx',
|
|
314
|
+
'components/StatStrip.tsx',
|
|
315
|
+
'components/StatTile.tsx',
|
|
316
|
+
'components/StatTileGrid.tsx',
|
|
317
|
+
'components/ThresholdMeter.tsx',
|
|
318
|
+
'components/BuildFreshness.tsx',
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
{ name: 'test-report', why: 'the test-report reader', roots: ['testReport'] },
|
|
322
|
+
{
|
|
323
|
+
name: 'virtual',
|
|
324
|
+
why: 'VirtualList and VirtualCardGrid — at the ROOT of src/react, and separable anyway because nothing about a stylesheet cares where a module lives',
|
|
325
|
+
roots: ['VirtualList.tsx', 'VirtualCardGrid.tsx'],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'workbench',
|
|
329
|
+
why: 'the resizable/split/panelled work surfaces and the drag-and-drop affordances',
|
|
330
|
+
roots: [
|
|
331
|
+
'components/PanelGrid.tsx',
|
|
332
|
+
'components/ResponsiveWorkbench.tsx',
|
|
333
|
+
'components/SplitPane.tsx',
|
|
334
|
+
'components/WidgetHeader.tsx',
|
|
335
|
+
'components/DragHandle.tsx',
|
|
336
|
+
'components/DropIndicator.tsx',
|
|
337
|
+
'components/DropSlot.tsx',
|
|
338
|
+
'components/SelectableRow.tsx',
|
|
339
|
+
'components/TreeView.tsx',
|
|
340
|
+
'components/PluginCard.tsx',
|
|
341
|
+
'components/IsolatedFrame.tsx',
|
|
342
|
+
'components/ScrollToTopButton.tsx',
|
|
343
|
+
'components/ChipFilterStrip.tsx',
|
|
344
|
+
// Beside the chips it renders, not in `core`: `ListToolbar`'s only non-core
|
|
345
|
+
// import IS `ChipFilterStrip`, so an app that mounts the toolbar already buys
|
|
346
|
+
// this area — and putting it in core would drag the chips into every consumer.
|
|
347
|
+
'components/ListToolbar.tsx',
|
|
348
|
+
'components/CommentThread.tsx',
|
|
349
|
+
'components/ConnectionHealer.tsx',
|
|
350
|
+
'components/ExternalLink.tsx',
|
|
351
|
+
'components/FileActions.tsx',
|
|
352
|
+
'components/FileDropZone.tsx',
|
|
353
|
+
'components/FileLocation.tsx',
|
|
354
|
+
'components/FileTypeIcon.tsx',
|
|
355
|
+
'components/InheritBadge.tsx',
|
|
356
|
+
'components/AmbientBackdrop.tsx',
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
{ name: 'wizard', why: 'the multi-step Wizard and its step chrome', roots: ['components/Wizard'] },
|
|
360
|
+
{ name: 'workbook-viewer', why: 'the lazy XLSX viewer (an optional peer already)', roots: ['workbook-viewer'] },
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
export const CORE = 'core';
|
|
364
|
+
|
|
365
|
+
/** A module the graph walk can reach. Absolute paths, so nothing here is relative to a cwd. */
|
|
366
|
+
const SOURCE = /\.(ts|tsx)$/;
|
|
367
|
+
const SPEC = /\.spec\.(ts|tsx)$/;
|
|
368
|
+
|
|
369
|
+
/** Every non-spec module under `src/react` — the set coverage is measured against. */
|
|
370
|
+
export const allModules = (): string[] =>
|
|
371
|
+
[...new Bun.Glob('**/*.{ts,tsx}').scanSync({ cwd: REACT_DIR, onlyFiles: true })]
|
|
372
|
+
.filter((rel) => !SPEC.test(rel))
|
|
373
|
+
.map((rel) => `${REACT_DIR}/${rel}`)
|
|
374
|
+
.sort();
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Everything under `src/react` that is NOT a module an area can contain: the
|
|
378
|
+
* `*.spec.tsx` fixtures, the hand-written `.css` files, the `.json` corpora.
|
|
379
|
+
*
|
|
380
|
+
* 🔴 They are in no area, and they are the ONE deliberate difference between the
|
|
381
|
+
* areas and the union. `styles-utilities.css` is compiled from a `@source` scan of
|
|
382
|
+
* the `src/react` DIRECTORY, and a directory scan reads every file type it finds —
|
|
383
|
+
* so the union every app ships today emits `.antialiased` because `fileTree.css`
|
|
384
|
+
* writes `-webkit-font-smoothing: antialiased`, and `.slide-in-from-left` because
|
|
385
|
+
* a spec asserts on the string. Nothing renders any of it, so the areas correctly
|
|
386
|
+
* drop them; `src/stylesAreas.spec.ts` asserts they are the ONLY thing the split
|
|
387
|
+
* drops, which is what keeps "correct" from widening into "whatever went missing".
|
|
388
|
+
*/
|
|
389
|
+
export const nonModuleFiles = (): string[] => {
|
|
390
|
+
const modules = new Set(allModules());
|
|
391
|
+
return [...new Bun.Glob('**/*').scanSync({ cwd: REACT_DIR, onlyFiles: true })]
|
|
392
|
+
.map((rel) => `${REACT_DIR}/${rel}`)
|
|
393
|
+
.filter((file) => !modules.has(file))
|
|
394
|
+
.sort();
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Relative import specifiers in a module — `from '…'`, bare `import '…'` and
|
|
399
|
+
* `import('…')` alike.
|
|
400
|
+
*
|
|
401
|
+
* A regex and not the TypeScript AST on purpose: this runs over 700 modules on
|
|
402
|
+
* every build, the only thing it has to understand is a string literal after a
|
|
403
|
+
* keyword, and a miss cannot go unnoticed — a module nothing reaches is an orphan,
|
|
404
|
+
* and `coverageFault()` fails the build naming it.
|
|
405
|
+
*/
|
|
406
|
+
const SPECIFIER = /(?:\bfrom\s*|\bimport\s*|\brequire\s*\(\s*)['"](\.[^'"]*)['"]/g;
|
|
407
|
+
|
|
408
|
+
/** `./x` → the module it resolves to, or null when it is CSS, an asset, or missing. */
|
|
409
|
+
const resolveRelative = (fromFile: string, specifier: string): string | null => {
|
|
410
|
+
const dir = fromFile.slice(0, fromFile.lastIndexOf('/'));
|
|
411
|
+
const parts = `${dir}/${specifier}`.split('/');
|
|
412
|
+
const stack: string[] = [];
|
|
413
|
+
for (const part of parts) {
|
|
414
|
+
if (part === '.' || part === '') continue;
|
|
415
|
+
if (part === '..') stack.pop();
|
|
416
|
+
else stack.push(part);
|
|
417
|
+
}
|
|
418
|
+
const base = `/${stack.join('/')}`;
|
|
419
|
+
if (SOURCE.test(base)) return base;
|
|
420
|
+
for (const candidate of [`${base}.ts`, `${base}.tsx`, `${base}/index.ts`, `${base}/index.tsx`]) {
|
|
421
|
+
// biome-ignore lint/nursery/noAwaitInLoop: synchronous existence check, no await here
|
|
422
|
+
if (Bun.file(candidate).size > 0) return candidate;
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The import graph of `src/react`, as `module → modules it imports`.
|
|
429
|
+
*
|
|
430
|
+
* Built once and shared: the closure of 45 areas over 700 modules is 45 walks of
|
|
431
|
+
* the same graph, and reading every file 45 times is the difference between a
|
|
432
|
+
* generator that runs in a second and one nobody runs.
|
|
433
|
+
*/
|
|
434
|
+
export const buildGraph = async (): Promise<Map<string, string[]>> => {
|
|
435
|
+
const modules = allModules();
|
|
436
|
+
const graph = new Map<string, string[]>();
|
|
437
|
+
await Promise.all(
|
|
438
|
+
modules.map(async (file) => {
|
|
439
|
+
const text = await Bun.file(file).text();
|
|
440
|
+
const out = new Set<string>();
|
|
441
|
+
for (const match of text.matchAll(SPECIFIER)) {
|
|
442
|
+
const target = resolveRelative(file, match[1] as string);
|
|
443
|
+
// Outside `src/react` is outside every area — the utility scan never sees it.
|
|
444
|
+
if (target && target.startsWith(`${REACT_DIR}/`) && !SPEC.test(target)) out.add(target);
|
|
445
|
+
}
|
|
446
|
+
graph.set(file, [...out]);
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
return graph;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/** A root, as written in `AREAS`, expanded to the module(s) it names. */
|
|
453
|
+
export const expandRoot = (root: string): string[] => {
|
|
454
|
+
const path = `${REACT_DIR}/${root}`;
|
|
455
|
+
if (SOURCE.test(root)) return [path];
|
|
456
|
+
for (const index of [`${path}/index.ts`, `${path}/index.tsx`]) {
|
|
457
|
+
if (Bun.file(index).size > 0) return [index];
|
|
458
|
+
}
|
|
459
|
+
// A directory with no barrel (`components/calendar`, `components/auth`): every
|
|
460
|
+
// module directly inside it is a root, because nothing else names them.
|
|
461
|
+
const inside = [...new Bun.Glob('**/*.{ts,tsx}').scanSync({ cwd: path, onlyFiles: true })]
|
|
462
|
+
.filter((rel) => !SPEC.test(rel))
|
|
463
|
+
.map((rel) => `${path}/${rel}`);
|
|
464
|
+
if (inside.length === 0) throw new Error(`area root "${root}" names nothing under ${REACT_DIR}`);
|
|
465
|
+
return inside;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/** Transitive closure of an area's roots through the import graph. */
|
|
469
|
+
export const closureOf = (area: Area, graph: Map<string, string[]>): Set<string> => {
|
|
470
|
+
const seen = new Set<string>();
|
|
471
|
+
const queue = area.roots.flatMap(expandRoot);
|
|
472
|
+
for (const leaf of area.leaves ?? []) seen.add(`${REACT_DIR}/${leaf}`);
|
|
473
|
+
while (queue.length > 0) {
|
|
474
|
+
const next = queue.pop() as string;
|
|
475
|
+
if (seen.has(next)) continue;
|
|
476
|
+
seen.add(next);
|
|
477
|
+
for (const dep of graph.get(next) ?? []) queue.push(dep);
|
|
478
|
+
}
|
|
479
|
+
return seen;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export interface AreaClosures {
|
|
483
|
+
readonly graph: Map<string, string[]>;
|
|
484
|
+
/** Area name → every module in its closure, core included where it overlaps. */
|
|
485
|
+
readonly closures: Map<string, Set<string>>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export const areaClosures = async (areas: readonly Area[] = AREAS): Promise<AreaClosures> => {
|
|
489
|
+
const graph = await buildGraph();
|
|
490
|
+
const closures = new Map<string, Set<string>>();
|
|
491
|
+
for (const area of areas) closures.set(area.name, closureOf(area, graph));
|
|
492
|
+
return { graph, closures };
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 🔴 COVERAGE. Modules no area's closure reaches, and area names that collide.
|
|
497
|
+
*
|
|
498
|
+
* An orphan module's utilities are in NO stylesheet, so the component renders
|
|
499
|
+
* unstyled in every app that adopted the areas — and nothing else in this repo can
|
|
500
|
+
* see it, because the module still typechecks, still lints, and is still in the
|
|
501
|
+
* union `styles-utilities.css` that apps on the old route import. This is the
|
|
502
|
+
* whole reason the area map is allowed to be a hand-written table: the table can
|
|
503
|
+
* be wrong, and this says so by name.
|
|
504
|
+
*/
|
|
505
|
+
export const coverageFault = (closures: Map<string, Set<string>>, areas: readonly Area[] = AREAS): string | null => {
|
|
506
|
+
if (areas[0]?.name !== CORE) return `the first area must be \`${CORE}\`, which every other area subtracts`;
|
|
507
|
+
const names = new Set(areas.map((a) => a.name));
|
|
508
|
+
if (names.size !== areas.length) return 'two areas share a name, so one stylesheet would overwrite the other';
|
|
509
|
+
const needsFault = needsGraphFault(areas);
|
|
510
|
+
if (needsFault) return needsFault;
|
|
511
|
+
const covered = new Set<string>();
|
|
512
|
+
for (const files of closures.values()) for (const file of files) covered.add(file);
|
|
513
|
+
const orphans = allModules().filter((file) => !covered.has(file));
|
|
514
|
+
if (orphans.length === 0) return null;
|
|
515
|
+
return (
|
|
516
|
+
`${orphans.length} module(s) under src/react are in no area, so their utility classes are in no\n` +
|
|
517
|
+
'stylesheet and every component they render is UNSTYLED in an app that imports the areas.\n' +
|
|
518
|
+
`Add each to an existing area's \`roots\` in scripts/styleAreas.ts, or give it an area:\n ` +
|
|
519
|
+
orphans.map((f) => f.replace(`${REPO_ROOT}/`, '')).join('\n ')
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 🔴 `needs` names areas that exist, never itself, and never a cycle.
|
|
525
|
+
*
|
|
526
|
+
* A dangling name would silently subtract nothing — the area would ship the
|
|
527
|
+
* overlap it was written to drop, which is only a size regression. A CYCLE is the
|
|
528
|
+
* one that corrupts: two areas each subtracting the other means a candidate that
|
|
529
|
+
* is in both is emitted by neither, and every component using it renders unstyled
|
|
530
|
+
* in an app that imported both. Checked before anything is compiled.
|
|
531
|
+
*/
|
|
532
|
+
export const needsGraphFault = (areas: readonly Area[] = AREAS): string | null => {
|
|
533
|
+
const byName = new Map(areas.map((a) => [a.name, a]));
|
|
534
|
+
for (const area of areas) {
|
|
535
|
+
if (area.name === CORE && (area.needs ?? []).length > 0) return `\`${CORE}\` may not \`need\` another area — everything subtracts it`;
|
|
536
|
+
for (const need of area.needs ?? []) {
|
|
537
|
+
if (need === area.name) return `area \`${area.name}\` needs itself`;
|
|
538
|
+
if (!byName.has(need)) return `area \`${area.name}\` needs \`${need}\`, which is not an area`;
|
|
539
|
+
if (need === CORE) return `area \`${area.name}\` needs \`${CORE}\`, which every area already subtracts`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Kahn: anything left when nothing more resolves is in a cycle.
|
|
543
|
+
const done = new Set<string>([CORE]);
|
|
544
|
+
for (;;) {
|
|
545
|
+
const ready = areas.filter((a) => !done.has(a.name) && (a.needs ?? []).every((n) => done.has(n)));
|
|
546
|
+
if (ready.length === 0) break;
|
|
547
|
+
for (const a of ready) done.add(a.name);
|
|
548
|
+
}
|
|
549
|
+
const stuck = areas.filter((a) => !done.has(a.name)).map((a) => a.name);
|
|
550
|
+
return stuck.length > 0 ? `a cycle in \`needs\`, so a shared candidate would be in NO area: ${stuck.join(', ')}` : null;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/** `needs`, transitively — the areas a consumer must import alongside this one. */
|
|
554
|
+
export const transitiveNeeds = (name: string, areas: readonly Area[] = AREAS): string[] => {
|
|
555
|
+
const byName = new Map(areas.map((a) => [a.name, a]));
|
|
556
|
+
const out = new Set<string>();
|
|
557
|
+
const queue = [...(byName.get(name)?.needs ?? [])];
|
|
558
|
+
while (queue.length > 0) {
|
|
559
|
+
const next = queue.pop() as string;
|
|
560
|
+
if (out.has(next)) continue;
|
|
561
|
+
out.add(next);
|
|
562
|
+
for (const more of byName.get(next)?.needs ?? []) queue.push(more);
|
|
563
|
+
}
|
|
564
|
+
return [...out].sort();
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Tailwind candidates in a set of modules.
|
|
569
|
+
*
|
|
570
|
+
* 🔴 A FRESH `Scanner` every call. Oxide's scanner remembers what it has already
|
|
571
|
+
* been shown and returns only what is new, so one shared instance would credit
|
|
572
|
+
* every shared candidate to whichever area happened to be scanned first — and the
|
|
573
|
+
* areas scanned later would ship stylesheets missing the utilities they use. Cost
|
|
574
|
+
* an hour to find while measuring this; the symptom was `media-gallery` reporting
|
|
575
|
+
* 591 candidates and 127 bytes.
|
|
576
|
+
*/
|
|
577
|
+
export const candidatesIn = async (files: Iterable<string>): Promise<Set<string>> => {
|
|
578
|
+
const contents = await Promise.all(
|
|
579
|
+
[...files].sort().map(async (file) => ({
|
|
580
|
+
content: await Bun.file(file).text(),
|
|
581
|
+
extension: file.slice(file.lastIndexOf('.') + 1),
|
|
582
|
+
})),
|
|
583
|
+
);
|
|
584
|
+
return new Set(new Scanner({}).scanFiles(contents));
|
|
585
|
+
};
|
package/scripts/verify.ts
CHANGED
|
@@ -76,6 +76,11 @@ export const STAGES: readonly Stage[] = [
|
|
|
76
76
|
{ script: "paths" },
|
|
77
77
|
{ script: "typecheck:root" },
|
|
78
78
|
{ script: "typecheck:specs" },
|
|
79
|
+
// 🔴 Added 2026-09-16 (task 172). `tsconfig.json` includes `src` only, so the eleven
|
|
80
|
+
// files under `scripts/` were compiled by nothing — including `guardrailsEnforce.ts`,
|
|
81
|
+
// which package.json EXPORTS as `cursedbelt/guardrails`. Four apps were typechecking a
|
|
82
|
+
// file this repo's own gate had never looked at, and inheriting its errors.
|
|
83
|
+
{ script: "typecheck:scripts" },
|
|
79
84
|
{ script: "demo:check" },
|
|
80
85
|
{ script: "styles" },
|
|
81
86
|
{
|