cursedbelt 2.3.0 → 2.5.1
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/styles-static.css +41 -19
- package/dist/styles-utilities.css +5074 -0
- package/dist/styles.css +41 -19
- package/package.json +13 -3
- package/scripts/generateUtilityStyles.ts +215 -0
- package/src/styles-static.css +41 -19
- package/src/styles-utilities.css +5074 -0
- package/src/styles.css +41 -19
- package/src/stylesUtilitiesMatches.spec.ts +314 -0
package/src/styles.css
CHANGED
|
@@ -6,32 +6,54 @@
|
|
|
6
6
|
* This `@source` makes Tailwind v4 scan cursedbelt's source files so the utility
|
|
7
7
|
* classes used by its components are generated in the app's stylesheet.
|
|
8
8
|
*
|
|
9
|
-
* ── Two
|
|
9
|
+
* ── Two ways in, and which one an app wants ──────────────────────────────────
|
|
10
10
|
* `@source "."` is the expensive half: it points Tailwind's JIT at cursedbelt's
|
|
11
11
|
* ENTIRE source tree, so the app emits the union of the whole design system
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* whether it renders three components or eighty, and its own build walks 827
|
|
13
|
+
* files it does not own. Everything else in this file is CSS the components
|
|
14
|
+
* genuinely need and no app can do without.
|
|
15
15
|
*
|
|
16
|
-
* A CSS `@import` takes the whole file, so the split is
|
|
16
|
+
* A CSS `@import` takes the whole file, so the split is two more subpaths:
|
|
17
17
|
*
|
|
18
|
-
* cursedbelt/styles.css
|
|
19
|
-
* cursedbelt/styles-static.css
|
|
18
|
+
* cursedbelt/styles.css this file — the real CSS + the `@source` scan
|
|
19
|
+
* cursedbelt/styles-static.css the same file MINUS the `@source` line
|
|
20
|
+
* cursedbelt/styles-utilities.css the utilities that `@source` line would have
|
|
21
|
+
* produced, precompiled at cursedbelt's build
|
|
20
22
|
*
|
|
21
|
-
* The
|
|
22
|
-
* tooltip pointer-events fix, the Progress keyframes, the
|
|
23
|
-
* ambient/glass layer and `tw-animate-css`, and emits none
|
|
24
|
-
* utility classes,
|
|
25
|
-
* sources IT registers. Do not hand-list cursedbelt's component files to get
|
|
26
|
-
* them back: a relative `@source` reaching into this package's source tree is a
|
|
27
|
-
* `file:` dependency wearing a stylesheet's clothes, and it is the thing the
|
|
28
|
-
* previous generation's 29-line-per-app arrangement is remembered for.
|
|
23
|
+
* The second two are a PAIR and neither is usable alone — `styles-static.css` on
|
|
24
|
+
* its own gets the tooltip pointer-events fix, the Progress keyframes, the
|
|
25
|
+
* lightbox bridge, the ambient/glass layer and `tw-animate-css`, and emits none
|
|
26
|
+
* of cursedbelt's utility classes, i.e. an unstyled app:
|
|
29
27
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* @import "tailwindcss";
|
|
29
|
+
* @import "cursedbelt/styles-static.css";
|
|
30
|
+
* @import "cursedbelt/styles-utilities.css";
|
|
31
|
+
* @import "cursedbelt/theme.css";
|
|
32
|
+
*
|
|
33
|
+
* 🔴 Do NOT hand-list cursedbelt's component files to get the utilities back.
|
|
34
|
+
* Tailwind v4 resolves every `@source` to a DIRECTORY and scans it recursively —
|
|
35
|
+
* a file path and a glob are both silently widened to their parent — and
|
|
36
|
+
* `VirtualList`/`VirtualCardGrid` live at the root of `src/react/`, so the finest
|
|
37
|
+
* grain an app can write is the whole design system. A relative `@source` reaching
|
|
38
|
+
* into this package's source tree is also a `file:` dependency wearing a
|
|
39
|
+
* stylesheet's clothes, and it buys nothing: it is the thing the previous
|
|
40
|
+
* generation's 29-line-per-app arrangement is remembered for, and those 29 lines
|
|
41
|
+
* were never file-level scoping either.
|
|
42
|
+
*
|
|
43
|
+
* The pair is not SMALLER than this file — measured, both routes emit the same
|
|
44
|
+
* rules within 54 bytes. What it buys is a build that scans nothing of another
|
|
45
|
+
* package's source tree, and a `styles-static.css` that finally has a correct
|
|
46
|
+
* use. The measurements, and what would actually shrink a consumer, are in the
|
|
47
|
+
* header of `scripts/generateUtilityStyles.ts`.
|
|
48
|
+
*
|
|
49
|
+
* 🔴 This header is in BOTH of the first two files, because one is generated from
|
|
50
|
+
* the other and a test holds them byte-identical apart from the `@source` line.
|
|
51
|
+
* Always edit `src/styles.css`; never `src/styles-static.css`, which
|
|
33
52
|
* `scripts/generateStaticStyles.ts` rewrites on every `bun run build` and
|
|
34
|
-
* `src/stylesStaticMatches.spec.ts` fails on when it is stale.
|
|
53
|
+
* `src/stylesStaticMatches.spec.ts` fails on when it is stale. Editing this file
|
|
54
|
+
* invalidates `src/styles-utilities.css` too — it is compiled against the static
|
|
55
|
+
* variant, `scripts/generateUtilityStyles.ts` rewrites it in the same build step,
|
|
56
|
+
* and `src/stylesUtilitiesMatches.spec.ts` is what fails when it is stale.
|
|
35
57
|
*/
|
|
36
58
|
|
|
37
59
|
/* Animation utilities (animate-in/out, fade/zoom/slide-in-from-*) consumed by the
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `src/styles-utilities.css` is a current, COMPLETE precompile of `src/react/**`
|
|
3
|
+
* (2026-09-14).
|
|
4
|
+
*
|
|
5
|
+
* ## What this exists to prevent
|
|
6
|
+
*
|
|
7
|
+
* The artifact replaces a `@source` scan the consuming app used to run. The scan
|
|
8
|
+
* was self-healing — it read whatever was in `src/react` at the app's build time —
|
|
9
|
+
* and the artifact is not: it is a file, generated once here, and every app that
|
|
10
|
+
* imports it gets exactly what was checked in.
|
|
11
|
+
*
|
|
12
|
+
* Its failure mode is SILENT and it is the worst one available. A stale artifact
|
|
13
|
+
* is missing the utilities a new component uses, so that component renders
|
|
14
|
+
* unstyled in every app; a collapsed scan writes an empty utility layer, so the
|
|
15
|
+
* WHOLE app renders unstyled. Neither is visible to `tsc` (it is CSS), to Biome
|
|
16
|
+
* (nothing is wrong with the file), or to a byte count read in one direction (a
|
|
17
|
+
* smaller stylesheet is the thing the split was asked for).
|
|
18
|
+
*
|
|
19
|
+
* So the assertions below do not compare size. They compile a consuming app BOTH
|
|
20
|
+
* ways — `@import "cursedbelt/styles.css"` (today, scanning) against
|
|
21
|
+
* `styles-static.css` + `styles-utilities.css` with `source(none)` (after
|
|
22
|
+
* adoption, scanning nothing) — and compare the rules that come out: every
|
|
23
|
+
* selector, every declaration, every custom property the declarations reference.
|
|
24
|
+
* `scripts/generateUtilityStyles.ts` owns the WHY and the measurements; this file
|
|
25
|
+
* is the WHETHER.
|
|
26
|
+
*
|
|
27
|
+
* Two differences between the two routes are real, deliberate and normalised for
|
|
28
|
+
* below rather than papered over — each is asserted to STILL BE the only one of
|
|
29
|
+
* its kind, so the normalisation cannot quietly widen into an amnesty:
|
|
30
|
+
*
|
|
31
|
+
* · `var(--x)` on the scanning route vs `var(--x, <literal>)` on the
|
|
32
|
+
* precompiled one, for tokens that reach the compiler through `@reference`.
|
|
33
|
+
* Same variable, same computed value, plus a fallback.
|
|
34
|
+
* · `@theme inline` alias declarations (`--color-card: var(--card)`) that the
|
|
35
|
+
* scanning route emits into `:root` and nothing ever reads — `inline` means
|
|
36
|
+
* the utility itself carries `var(--card)`, so the alias is bookkeeping. Only
|
|
37
|
+
* custom properties something actually `var()`s are compared.
|
|
38
|
+
*
|
|
39
|
+
* `Bun.file` for every read here, not node:fs — the convention the sibling specs
|
|
40
|
+
* (`src/publishShape.spec.ts`, `src/declaredDepsAreImported.spec.ts`) state and
|
|
41
|
+
* the reason they give: a spec that reads through a mockable builtin can pass
|
|
42
|
+
* against files it never opened. The Tailwind compiler this file drives does use
|
|
43
|
+
* node:fs internally, which is unavoidable and is the point — it is resolving the
|
|
44
|
+
* same `@import`s a consumer's bundler will.
|
|
45
|
+
*/
|
|
46
|
+
import { describe, expect, it } from 'bun:test';
|
|
47
|
+
import pkg from '../package.json';
|
|
48
|
+
import {
|
|
49
|
+
CONSUMER_PRECOMPILED,
|
|
50
|
+
CONSUMER_SCANNING,
|
|
51
|
+
MIN_CANDIDATES,
|
|
52
|
+
REPO_ROOT,
|
|
53
|
+
UTILITY_STYLESHEET,
|
|
54
|
+
compileStylesheet,
|
|
55
|
+
readUtilityStylesheet,
|
|
56
|
+
} from '../scripts/generateUtilityStyles';
|
|
57
|
+
|
|
58
|
+
const { expected, onDisk, candidates } = await readUtilityStylesheet();
|
|
59
|
+
|
|
60
|
+
/** The command every staleness failure below is fixed by, as a line the owner can paste. */
|
|
61
|
+
const REGENERATE = `cd ${REPO_ROOT} && bun run styles:utilities`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Flatten a stylesheet into `at-rule ‖ … ‖ selector` → the set of declarations in
|
|
65
|
+
* that block, merging blocks that repeat (both routes emit `@keyframes spin`
|
|
66
|
+
* twice; a consumer's browser does not care, and neither should a comparison).
|
|
67
|
+
*
|
|
68
|
+
* A hand-rolled walk rather than a CSS parser because the only input is Tailwind's
|
|
69
|
+
* own output: well-formed, and the one construct that could confuse brace counting
|
|
70
|
+
* — a `{` inside a string or a comment — is skipped explicitly.
|
|
71
|
+
*/
|
|
72
|
+
const flatten = (css: string): Map<string, Set<string>> => {
|
|
73
|
+
const out = new Map<string, Set<string>>();
|
|
74
|
+
const stack: string[] = [];
|
|
75
|
+
const open: string[][] = [[]];
|
|
76
|
+
let buf = '';
|
|
77
|
+
const push = (raw: string): void => {
|
|
78
|
+
const decl = raw.trim();
|
|
79
|
+
if (decl) open[open.length - 1]?.push(decl);
|
|
80
|
+
};
|
|
81
|
+
for (let i = 0; i < css.length; i++) {
|
|
82
|
+
const ch = css[i];
|
|
83
|
+
if (ch === '/' && css[i + 1] === '*') {
|
|
84
|
+
i = css.indexOf('*/', i) + 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === '"' || ch === "'") {
|
|
88
|
+
const end = css.indexOf(ch, i + 1);
|
|
89
|
+
buf += css.slice(i, end + 1);
|
|
90
|
+
i = end;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === '{') {
|
|
94
|
+
stack.push(buf.trim().replace(/\s+/g, ' '));
|
|
95
|
+
buf = '';
|
|
96
|
+
open.push([]);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === '}') {
|
|
100
|
+
push(buf);
|
|
101
|
+
buf = '';
|
|
102
|
+
const key = stack.join(' ‖ ');
|
|
103
|
+
const set = out.get(key) ?? new Set<string>();
|
|
104
|
+
for (const decl of open.pop() ?? []) set.add(decl);
|
|
105
|
+
out.set(key, set);
|
|
106
|
+
stack.pop();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// At depth 0 a `;` ends a statement at-rule (`@layer a, b;`, `@import …`),
|
|
110
|
+
// which carries no declarations and must not stick to the next selector.
|
|
111
|
+
if (ch === ';') {
|
|
112
|
+
if (stack.length) push(buf);
|
|
113
|
+
buf = '';
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
buf += ch;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** `var(--x, <anything>)` → `var(--x)`, so a fallback reads as the same declaration. */
|
|
122
|
+
const dropFallbacks = (decl: string): string => decl.replace(/var\((\s*--[\w-]+)[^)]*\)/g, 'var($1)');
|
|
123
|
+
|
|
124
|
+
const CUSTOM_PROPERTY = /^(--[\w-]+)\s*:/;
|
|
125
|
+
|
|
126
|
+
/** Every custom property a stylesheet actually reads through `var()`. */
|
|
127
|
+
const referenced = (css: string): Set<string> => {
|
|
128
|
+
const out = new Set<string>();
|
|
129
|
+
for (const m of css.matchAll(/var\(\s*(--[\w-]+)/g)) out.add(m[1] as string);
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Every custom property a stylesheet declares, including `@property` registrations. */
|
|
134
|
+
const declared = (css: string): Set<string> => {
|
|
135
|
+
const out = new Set<string>();
|
|
136
|
+
for (const m of css.matchAll(/(?:^|[;{\s])(--[\w-]+)\s*:/g)) out.add(m[1] as string);
|
|
137
|
+
for (const m of css.matchAll(/@property\s+(--[\w-]+)/g)) out.add(m[1] as string);
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// The two routes, compiled from the SAME candidate list (the classes cursedbelt's
|
|
142
|
+
// components use), so neither side is credited with utilities the other was never
|
|
143
|
+
// asked for. The precompiled route is built with no candidates at all: everything
|
|
144
|
+
// it emits came out of the artifact on disk.
|
|
145
|
+
const scanning = (await compileStylesheet(CONSUMER_SCANNING)).build(candidates);
|
|
146
|
+
const precompiled = (await compileStylesheet(CONSUMER_PRECOMPILED)).build([]);
|
|
147
|
+
const before = flatten(scanning);
|
|
148
|
+
const after = flatten(precompiled);
|
|
149
|
+
/** Hoisted: both loops below ask this per declaration, over 160 KB of CSS. */
|
|
150
|
+
const readByScanning = referenced(scanning);
|
|
151
|
+
|
|
152
|
+
describe('styles-utilities.css is a generated, gated precompile of src/react', () => {
|
|
153
|
+
it('is checked in and byte-identical to what the generator produces', () => {
|
|
154
|
+
expect(onDisk, `${UTILITY_STYLESHEET} is missing. It is generated:\n ${REGENERATE}`).not.toBeNull();
|
|
155
|
+
expect(
|
|
156
|
+
onDisk,
|
|
157
|
+
`${UTILITY_STYLESHEET} is stale — src/react (or src/styles.css, which it is compiled\n` +
|
|
158
|
+
'against) changed and the artifact was not regenerated, so every app importing\n' +
|
|
159
|
+
`\`cursedbelt/styles-utilities.css\` is missing the difference. Run:\n ${REGENERATE}`,
|
|
160
|
+
).toBe(expected);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('was compiled from a scan that did not collapse', () => {
|
|
164
|
+
// The generator throws below its own floor; this is the assertion that says
|
|
165
|
+
// what the floor is FOR, and it fails here rather than at publish time.
|
|
166
|
+
expect(candidates.length).toBeGreaterThanOrEqual(MIN_CANDIDATES);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('has compiled two real consumer stylesheets to compare', () => {
|
|
170
|
+
// Guards every assertion below: two empty strings agree about everything.
|
|
171
|
+
expect(before.size).toBeGreaterThan(500);
|
|
172
|
+
expect(after.size).toBeGreaterThan(500);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('emits every rule the `@source` scan emits, with the same declarations', () => {
|
|
176
|
+
// The core proof, and the reason this spec does not read a byte count: an
|
|
177
|
+
// app that adopts the pair must RENDER the same. A missing key is a selector
|
|
178
|
+
// that vanished; a missing declaration is a property that stopped applying.
|
|
179
|
+
const lostRules: string[] = [];
|
|
180
|
+
const lostDeclarations: string[] = [];
|
|
181
|
+
for (const [key, decls] of before) {
|
|
182
|
+
const mine = after.get(key);
|
|
183
|
+
if (!mine) {
|
|
184
|
+
lostRules.push(key);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const theirs = new Set([...mine].map(dropFallbacks));
|
|
188
|
+
for (const decl of decls) {
|
|
189
|
+
// `@theme inline` aliases nothing reads — see the header.
|
|
190
|
+
const prop = CUSTOM_PROPERTY.exec(decl)?.[1];
|
|
191
|
+
if (prop && !readByScanning.has(prop)) continue;
|
|
192
|
+
if (!theirs.has(dropFallbacks(decl))) lostDeclarations.push(`${key} → ${decl}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
expect(
|
|
196
|
+
lostRules,
|
|
197
|
+
'`styles-static.css` + `styles-utilities.css` emits FEWER rules than\n' +
|
|
198
|
+
'`styles.css` does. Every selector listed here renders unstyled in any app that\n' +
|
|
199
|
+
`adopted the pair. Usually staleness:\n ${REGENERATE}`,
|
|
200
|
+
).toEqual([]);
|
|
201
|
+
expect(
|
|
202
|
+
lostDeclarations,
|
|
203
|
+
'the precompiled pair emits these selectors but not these declarations, so the\n' +
|
|
204
|
+
`affected components render PARTLY styled. Usually staleness:\n ${REGENERATE}`,
|
|
205
|
+
).toEqual([]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('reads no custom property the files beside it do not declare', async () => {
|
|
209
|
+
// The trap the split creates and the scan never could: Tailwind emits only
|
|
210
|
+
// the theme variables its own build finds a use for, so a utility shipped in
|
|
211
|
+
// a prebuilt file can reference a `--text-sm` the consuming app never had a
|
|
212
|
+
// reason to emit — and `font-size: var(--text-sm)` with no `--text-sm` is an
|
|
213
|
+
// invisible nothing.
|
|
214
|
+
//
|
|
215
|
+
// 🔴 Asked of the FILES, not of a compile. Tailwind v4 back-fills: it tracks
|
|
216
|
+
// the `var()`s in whatever CSS it is handed, so the artifact's whole theme
|
|
217
|
+
// layer can be deleted and a compiled consumer still declares everything
|
|
218
|
+
// (verified by deleting it). That back-fill is real and it is why the rule
|
|
219
|
+
// comparison above passes — but it is Tailwind's promise, not this file's,
|
|
220
|
+
// and a `<link>` tag gets none of it. This asks what the artifact and the
|
|
221
|
+
// stylesheets a consumer imports beside it actually WRITE DOWN.
|
|
222
|
+
const artifact = onDisk ?? '';
|
|
223
|
+
// styles-static.css carries exactly one bare specifier, and its `@utility`
|
|
224
|
+
// definitions and `@property` registrations are where the `--tw-enter-*` /
|
|
225
|
+
// `--tw-animation-*` the artifact reads are declared. Pinned so the coupling
|
|
226
|
+
// cannot rot into a silent pass.
|
|
227
|
+
const animateDir = `${REPO_ROOT}/node_modules/tw-animate-css`;
|
|
228
|
+
expect(
|
|
229
|
+
(pkg as { dependencies: Record<string, string> }).dependencies['tw-animate-css'],
|
|
230
|
+
'tw-animate-css is what src/styles.css @imports',
|
|
231
|
+
).toBeDefined();
|
|
232
|
+
// Its own `exports` map, not a guessed filename — a CSS-only package resolves
|
|
233
|
+
// through the `style` condition, which Bun.resolveSync does not follow.
|
|
234
|
+
const animateManifest = (await Bun.file(`${animateDir}/package.json`).json()) as {
|
|
235
|
+
exports: Record<string, { style?: string }>;
|
|
236
|
+
};
|
|
237
|
+
const animateEntry = animateManifest.exports['.']?.style;
|
|
238
|
+
expect(animateEntry, 'tw-animate-css no longer exports a `style` entry').toBeDefined();
|
|
239
|
+
const animateCss = await Bun.file(`${animateDir}/${animateEntry}`).text();
|
|
240
|
+
expect(animateCss.length, 'tw-animate-css moved its stylesheet').toBeGreaterThan(1_000);
|
|
241
|
+
const beside = await Promise.all(
|
|
242
|
+
['src/theme.css', 'src/styles-static.css'].map((f) => Bun.file(`${REPO_ROOT}/${f}`).text()),
|
|
243
|
+
);
|
|
244
|
+
const provided = new Set([artifact, animateCss, ...beside].flatMap((css) => [...declared(css)]));
|
|
245
|
+
// Differential, not an allowlist: whatever the scanning route leaves to the
|
|
246
|
+
// runtime (`--radix-*`/`--reka-*` from the positioning libraries, `--cb-*`
|
|
247
|
+
// set inline by components) is fine here too, and nothing NEW is.
|
|
248
|
+
const declaredByScanning = declared(scanning);
|
|
249
|
+
const known = [...readByScanning].filter((v) => !declaredByScanning.has(v));
|
|
250
|
+
expect(
|
|
251
|
+
[...referenced(artifact)].filter((v) => !provided.has(v) && !known.includes(v)).sort(),
|
|
252
|
+
`${UTILITY_STYLESHEET} reads these custom properties and neither it, src/theme.css,\n` +
|
|
253
|
+
'src/styles-static.css nor tw-animate-css declares them. Under the `@source` scan the\n' +
|
|
254
|
+
"consuming app's own build emitted them; a prebuilt file cannot count on that.\n" +
|
|
255
|
+
`Run:\n ${REGENERATE}`,
|
|
256
|
+
).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('normalised exactly the two differences its header documents, and no more', () => {
|
|
260
|
+
// The two allowances above are the difference between a proof and a rubber
|
|
261
|
+
// stamp, so they are measured rather than trusted: if either grows, this
|
|
262
|
+
// fails and the header has to be rewritten before the pair ships again.
|
|
263
|
+
const fallbackOnly: string[] = [];
|
|
264
|
+
const aliasOnly: string[] = [];
|
|
265
|
+
const read = referenced(scanning);
|
|
266
|
+
for (const [key, decls] of before) {
|
|
267
|
+
const mine = after.get(key);
|
|
268
|
+
if (!mine) continue;
|
|
269
|
+
for (const decl of decls) {
|
|
270
|
+
if (mine.has(decl)) continue;
|
|
271
|
+
const prop = CUSTOM_PROPERTY.exec(decl)?.[1];
|
|
272
|
+
if (prop && !read.has(prop)) aliasOnly.push(`${key} → ${decl}`);
|
|
273
|
+
else fallbackOnly.push(`${key} → ${decl}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// One: `.animate-progress-indeterminate`, whose `@theme` token reaches the
|
|
277
|
+
// artifact's compile through `@reference "./styles-static.css"`.
|
|
278
|
+
expect(fallbackOnly.map((d) => d.split(' → ')[0])).toEqual([
|
|
279
|
+
'@layer utilities ‖ .animate-progress-indeterminate',
|
|
280
|
+
]);
|
|
281
|
+
// Four: the `@theme inline` aliases in src/theme.css that no utility reads.
|
|
282
|
+
// A number, not a list, because which four is an implementation detail of
|
|
283
|
+
// Tailwind's theme tree-shaking; a JUMP here means the allowance is covering
|
|
284
|
+
// something it was not written for.
|
|
285
|
+
expect(aliasOnly.length).toBeLessThanOrEqual(8);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('is exported as a subpath that resolves into a directory `files` ships', () => {
|
|
289
|
+
const exports = (pkg as { exports: Record<string, unknown> }).exports;
|
|
290
|
+
expect(exports['./styles-utilities.css']).toBe(`./${UTILITY_STYLESHEET}`);
|
|
291
|
+
// Out of `src/`, beside the two stylesheets it completes — a trio of
|
|
292
|
+
// app-facing stylesheets served from two different directories is a
|
|
293
|
+
// difference nobody can see in CSS.
|
|
294
|
+
expect(exports['./styles-static.css']).toBe('./src/styles-static.css');
|
|
295
|
+
expect((pkg as { files: string[] }).files).toContain('src');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('resolves as `cursedbelt/styles-utilities.css` through the real resolver', () => {
|
|
299
|
+
// The assertions above read the manifest; this one RESOLVES the specifier the
|
|
300
|
+
// way a consumer's bundler does. An export entry pointing at a path that does
|
|
301
|
+
// not exist satisfies every string comparison in this file and fails here.
|
|
302
|
+
expect(Bun.resolveSync(`${pkg.name}/styles-utilities.css`, REPO_ROOT)).toBe(
|
|
303
|
+
`${REPO_ROOT}/${UTILITY_STYLESHEET}`,
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('is regenerated by the build, so a stale artifact cannot be published', () => {
|
|
308
|
+
// `prepublishOnly` → `verify` → `build`, so the tarball's copy is always the
|
|
309
|
+
// one this repo's current src/react produces.
|
|
310
|
+
expect((pkg as { scripts: Record<string, string> }).scripts.build).toContain(
|
|
311
|
+
'scripts/generateUtilityStyles.ts',
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
});
|