@weatherboard/gyde-design 0.3.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/normalise.mjs ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * G-51 — one representation every styling idiom reduces to.
3
+ *
4
+ * WHY THIS EXISTS.
5
+ *
6
+ * Every design-system audit we have was written against one repository's
7
+ * spelling, and each one was correct locally and useless one repository over:
8
+ *
9
+ * System B /\brounded-(lg|xl|2xl|3xl)\b/ Tailwind classes
10
+ * System A /border-radius:\s*(.+)/ CSS declarations
11
+ * System C — (no audit at all)
12
+ *
13
+ * System B's rules would match nothing in System A, which has no Tailwind;
14
+ * System A's would match nothing in System C's cva strings.
15
+ * A rule that matches nothing reports zero and is indistinguishable from a rule
16
+ * that is working — CHARTER §5's failure mode, reached in the rule layer.
17
+ *
18
+ * And it is not only cross-repo. One repository recorded its own `no-rounded-lg`
19
+ * matching one literal spelling, missing every `rounded-xl` / `-2xl` / `-3xl`,
20
+ * and reporting **zero** while 178 real hits existed.
21
+ *
22
+ * THE FIX IS NOT A BIGGER REGEX. It is to stop matching source text. Every
23
+ * idiom below expresses the same four decisions — a radius, a spacing, a type
24
+ * size, a colour — so we parse each into a `Decl` and write the rules once,
25
+ * against that:
26
+ *
27
+ * rounded-lg Tailwind utility → radius 8px
28
+ * border-radius: 8px CSS declaration → radius 8px
29
+ * borderRadius: 8 JS/StyleX object → radius 8px
30
+ * borderRadius: radius.card → radius, tokenised
31
+ * var(--radius-card) custom property → radius, tokenised
32
+ *
33
+ * A rule then asks "is this radius tokenised?", which is the question it was
34
+ * always trying to ask, and gets the same answer in all five spellings.
35
+ *
36
+ * WHAT THIS IS NOT. It is a lexical pass, not a renderer. It cannot know that
37
+ * `p-4` resolved to 16px through a customised Tailwind scale, and it does not
38
+ * try — `scaleHint` carries the assumption so a caller can correct it, and
39
+ * `confidence` marks what was inferred rather than read. G-11 renders the real
40
+ * artifact and is the place to measure computed style; this is what runs on
41
+ * source, at commit time, without a browser.
42
+ */
43
+
44
+ /** Canonical properties. Deliberately few — these are the decisions a token dictionary makes. */
45
+ export const PROP = {
46
+ RADIUS: "radius",
47
+ SPACE: "space",
48
+ TYPE: "type",
49
+ COLOR: "color",
50
+ SHADOW: "shadow",
51
+ FONT: "font",
52
+ BORDER_WIDTH: "borderWidth",
53
+ };
54
+
55
+ /**
56
+ * Tailwind's default scales, as the ASSUMPTION they are.
57
+ *
58
+ * System C runs stock Tailwind 3.4, so these hold there. A project that
59
+ * customises `theme.extend.spacing` makes them wrong, which is why every Decl
60
+ * built from a utility carries `confidence: "assumed"` and `scaleHint`. A rule
61
+ * that needs certainty must check `confidence` rather than trusting the number:
62
+ * being wrong quietly is the thing this module exists to stop.
63
+ */
64
+ export const TAILWIND_SPACING_STEP = 4; // p-4 → 16px
65
+ export const TAILWIND_RADIUS = {
66
+ none: 0, sm: 2, DEFAULT: 4, md: 6, lg: 8, xl: 12, "2xl": 16, "3xl": 24, full: 9999,
67
+ };
68
+ export const TAILWIND_TEXT = {
69
+ xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30,
70
+ "4xl": 36, "5xl": 48, "6xl": 60, "7xl": 72, "8xl": 96, "9xl": 128,
71
+ };
72
+
73
+ /** Values that are never a design decision and must never be reported. */
74
+ const NEUTRAL = new Set([
75
+ "0", "0px", "none", "inherit", "initial", "unset", "revert", "auto", "currentcolor",
76
+ "transparent", "normal",
77
+ ]);
78
+
79
+ /**
80
+ * A single normalised decision.
81
+ *
82
+ * `tokenised` is the field the rules care about: true when the value came from
83
+ * a dictionary (a `var(--x)`, a `tokens.radius.card`, a Tailwind semantic class
84
+ * backed by a CSS variable) rather than being spelled literally at the call site.
85
+ */
86
+ function decl(property, { px = null, token = null, raw, source, confidence = "read", scaleHint = null }) {
87
+ return {
88
+ property,
89
+ px, // comparable number where one could be derived
90
+ token, // the token reference, when the value was tokenised
91
+ tokenised: token !== null,
92
+ neutral: NEUTRAL.has(String(raw).trim().toLowerCase()),
93
+ raw: String(raw).trim(),
94
+ source, // "tailwind" | "css" | "object" | "var"
95
+ confidence, // "read" (literally present) | "assumed" (from a default scale)
96
+ scaleHint, // which assumption was applied, when confidence is "assumed"
97
+ };
98
+ }
99
+
100
+ /** `8px` / `1.5rem` / `8` → px. Returns null for anything not a length. */
101
+ export function toPx(value, { rootFontSize = 16 } = {}) {
102
+ const v = String(value).trim();
103
+ let m = v.match(/^(-?[\d.]+)px$/); if (m) return parseFloat(m[1]);
104
+ m = v.match(/^(-?[\d.]+)(?:rem|em)$/); if (m) return parseFloat(m[1]) * rootFontSize;
105
+ m = v.match(/^(-?[\d.]+)$/); if (m) return parseFloat(m[1]); // unitless JS style
106
+ return null;
107
+ }
108
+
109
+ /** Any reference to a dictionary: `var(--x)`, `tokens.radius.card`, `radius.card`, `$radius.card`. */
110
+ function tokenRef(value) {
111
+ const v = String(value).trim();
112
+ const cssVar = v.match(/var\(\s*(--[\w-]+)/);
113
+ if (cssVar) return cssVar[1];
114
+ // A StyleX/JS member expression into a token object. Deliberately narrow: a
115
+ // bare identifier is not evidence of a dictionary, and treating it as one
116
+ // would make every local variable look tokenised.
117
+ const member = v.match(/^\$?((?:tokens|colors|color|space|spacing|radius|text|type|fonts?|elevation|shadow)(?:\.[A-Za-z_$][\w$]*)+)$/);
118
+ if (member) return member[1];
119
+ return null;
120
+ }
121
+
122
+ /** The CSS properties we map, and to which canonical property. */
123
+ const CSS_PROPERTY = new Map(Object.entries({
124
+ "border-radius": PROP.RADIUS,
125
+ "border-top-left-radius": PROP.RADIUS, "border-top-right-radius": PROP.RADIUS,
126
+ "border-bottom-left-radius": PROP.RADIUS, "border-bottom-right-radius": PROP.RADIUS,
127
+ padding: PROP.SPACE, "padding-top": PROP.SPACE, "padding-right": PROP.SPACE,
128
+ "padding-bottom": PROP.SPACE, "padding-left": PROP.SPACE,
129
+ margin: PROP.SPACE, "margin-top": PROP.SPACE, "margin-right": PROP.SPACE,
130
+ "margin-bottom": PROP.SPACE, "margin-left": PROP.SPACE,
131
+ gap: PROP.SPACE, "row-gap": PROP.SPACE, "column-gap": PROP.SPACE,
132
+ "font-size": PROP.TYPE,
133
+ "font-family": PROP.FONT,
134
+ color: PROP.COLOR, background: PROP.COLOR, "background-color": PROP.COLOR,
135
+ "border-color": PROP.COLOR, "border-top-color": PROP.COLOR, "border-right-color": PROP.COLOR,
136
+ "border-bottom-color": PROP.COLOR, "border-left-color": PROP.COLOR,
137
+ "outline-color": PROP.COLOR, fill: PROP.COLOR, stroke: PROP.COLOR,
138
+ "box-shadow": PROP.SHADOW, "text-shadow": PROP.SHADOW,
139
+ "border-width": PROP.BORDER_WIDTH, "border-top-width": PROP.BORDER_WIDTH,
140
+ }));
141
+
142
+ /** camelCase → kebab-case, so `borderRadius` and `border-radius` reach one lookup. */
143
+ const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
144
+
145
+ /**
146
+ * Shorthands carry several decisions in one value.
147
+ *
148
+ * `padding: 8px 16px` is two spacings, and System A's rule found this
149
+ * the hard way — checking only the first value passed `margin: 0 auto 12px`
150
+ * while the 12px was untokenised. Split on whitespace outside parentheses, so
151
+ * `0 0 0 var(--space-2)` and `1px solid rgb(0 0 0 / 10%)` both survive.
152
+ */
153
+ function splitValues(value) {
154
+ const parts = [];
155
+ let depth = 0, cur = "";
156
+ for (const ch of String(value)) {
157
+ if (ch === "(") depth++;
158
+ if (ch === ")") depth--;
159
+ if (/\s/.test(ch) && depth === 0) { if (cur) parts.push(cur); cur = ""; continue; }
160
+ cur += ch;
161
+ }
162
+ if (cur) parts.push(cur);
163
+ return parts;
164
+ }
165
+
166
+ /** A colour literal: hex, rgb(), hsl(), oklch(), or a CSS named colour we care about. */
167
+ const COLOUR_LITERAL = /^(#[0-9a-f]{3,8}|(rgb|hsl|hwb|lab|lch|oklab|oklch|color)a?\()/i;
168
+ const NAMED_COLOURS = new Set([
169
+ "red", "blue", "green", "yellow", "purple", "pink", "orange", "black", "white",
170
+ "grey", "gray", "teal", "cyan", "indigo", "violet", "brown", "silver", "gold",
171
+ ]);
172
+
173
+ /** One `property: value` pair, from CSS or from a JS/StyleX object. */
174
+ export function normaliseDeclaration(rawProp, rawValue, { source = "css", rootFontSize = 16 } = {}) {
175
+ const prop = kebab(String(rawProp).trim());
176
+ const canonical = CSS_PROPERTY.get(prop);
177
+ if (!canonical) return [];
178
+
179
+ const out = [];
180
+ for (const part of splitValues(rawValue)) {
181
+ const token = tokenRef(part);
182
+ if (token) { out.push(decl(canonical, { token, raw: part, source: source === "css" ? "var" : source })); continue; }
183
+
184
+ if (canonical === PROP.COLOR) {
185
+ const lower = part.toLowerCase();
186
+ if (NEUTRAL.has(lower)) { out.push(decl(canonical, { raw: part, source })); continue; }
187
+ if (COLOUR_LITERAL.test(part) || NAMED_COLOURS.has(lower)) {
188
+ out.push(decl(canonical, { raw: part, source }));
189
+ }
190
+ continue;
191
+ }
192
+
193
+ if (canonical === PROP.SHADOW || canonical === PROP.FONT) {
194
+ out.push(decl(canonical, { raw: part, source }));
195
+ continue;
196
+ }
197
+
198
+ const px = toPx(part, { rootFontSize });
199
+ if (px !== null || NEUTRAL.has(part.toLowerCase())) {
200
+ out.push(decl(canonical, { px, raw: part, source }));
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+
206
+ /**
207
+ * Parse a block of CSS text into declarations, keeping the line number.
208
+ *
209
+ * Declarations are found ANYWHERE on the line, not just at its start. The first
210
+ * version anchored to line-start and silently read nothing from `.a { color: red }`
211
+ * — a whole class of minified, nested and single-line CSS, invisible. That is
212
+ * the module's own failure mode arriving one level down, which is why the
213
+ * fixtures in rules.mjs are deliberately written single-line.
214
+ */
215
+ export function normaliseCss(text, { rootFontSize = 16 } = {}) {
216
+ const out = [];
217
+ text.split("\n").forEach((line, i) => {
218
+ // Strip comments; a `@media`/`@import` prelude declares nothing itself.
219
+ const clean = line.replace(/\/\*.*?\*\//g, "");
220
+
221
+ // A declaration follows the line start, a `{`, or a `;`. Requiring one of
222
+ // those is what keeps `https://x` and `a:hover {` from parsing as values.
223
+ for (const m of clean.matchAll(/(?:^|[{;])\s*(--[\w-]+|[a-zA-Z-]+)\s*:\s*([^;{}]+)/g)) {
224
+ const [, prop, value] = m;
225
+ // A custom property DEFINITION is the dictionary being written, not a use
226
+ // of it. `--color-surface: #0b0d10` in System A's console is the
227
+ // correct way to run a second palette and must not be reported as
228
+ // a bespoke colour, or the mechanism becomes unusable.
229
+ if (prop.startsWith("--")) continue;
230
+ for (const d of normaliseDeclaration(prop, value, { source: "css", rootFontSize })) {
231
+ out.push({ ...d, line: i + 1 });
232
+ }
233
+ }
234
+ });
235
+ return out;
236
+ }
237
+
238
+ /**
239
+ * Tailwind / cva utility classes.
240
+ *
241
+ * System C's whole component layer is `cva("... rounded-md h-10 px-4 ...")`,
242
+ * so without this the normaliser sees an entire design system as opaque strings.
243
+ *
244
+ * Semantic utilities that resolve through a CSS variable — `bg-primary`,
245
+ * `text-muted-foreground`, the shadcn convention — are TOKENISED, which is the
246
+ * subtle and important case. `bg-primary` is a dictionary reference spelled as
247
+ * a class; treating it as a bespoke colour would report a correctly-built
248
+ * shadcn component as 100% violations and the rule would be switched off within
249
+ * a day.
250
+ */
251
+ const SEMANTIC_COLOUR_WORDS = new Set([
252
+ "primary", "secondary", "destructive", "muted", "accent", "popover", "card",
253
+ "background", "foreground", "border", "input", "ring", "success", "warning",
254
+ "danger", "surface", "canvas", "raised", "inset", "faint",
255
+ ]);
256
+
257
+ export function normaliseUtilities(classString, { spacingStep = TAILWIND_SPACING_STEP } = {}) {
258
+ const out = [];
259
+ // Interpolations carry no literal class; System A strips them for
260
+ // the same reason — `${active ? "a" : "b"}` is not a spelling we can read.
261
+ const cleaned = String(classString).replace(/\$\{[^}]*\}/g, " ");
262
+
263
+ for (const raw of cleaned.split(/\s+/)) {
264
+ if (!raw) continue;
265
+ // Drop variant prefixes: `hover:`, `md:`, `dark:`, `focus-visible:`.
266
+ const cls = raw.replace(/^(?:[\w-]+:)+/, "");
267
+ if (!cls) continue;
268
+
269
+ // Arbitrary values are always literal by definition: `rounded-[7px]`.
270
+ const arb = cls.match(/^([\w-]+?)-\[(.+)\]$/);
271
+ if (arb) {
272
+ const [, prefix, value] = arb;
273
+ const canonical = utilityProperty(prefix);
274
+ if (canonical) {
275
+ out.push(decl(canonical, {
276
+ px: toPx(value), raw: cls, source: "tailwind",
277
+ // The value is right there in the class, so this is read, not assumed.
278
+ }));
279
+ }
280
+ continue;
281
+ }
282
+
283
+ let m;
284
+ if ((m = cls.match(/^rounded(?:-(\w+))?$/))) {
285
+ const key = m[1] ?? "DEFAULT";
286
+ if (key in TAILWIND_RADIUS) {
287
+ out.push(decl(PROP.RADIUS, {
288
+ px: TAILWIND_RADIUS[key], raw: cls, source: "tailwind",
289
+ confidence: "assumed", scaleHint: "tailwind default borderRadius scale",
290
+ }));
291
+ } else {
292
+ // `rounded-card` — a project-defined semantic radius, i.e. tokenised.
293
+ out.push(decl(PROP.RADIUS, { token: `radius.${key}`, raw: cls, source: "tailwind" }));
294
+ }
295
+ continue;
296
+ }
297
+
298
+ if ((m = cls.match(/^text-(\w+)$/))) {
299
+ const key = m[1];
300
+ if (key in TAILWIND_TEXT) {
301
+ out.push(decl(PROP.TYPE, {
302
+ px: TAILWIND_TEXT[key], raw: cls, source: "tailwind",
303
+ confidence: "assumed", scaleHint: "tailwind default fontSize scale",
304
+ }));
305
+ } else if (SEMANTIC_COLOUR_WORDS.has(key) || key.endsWith("foreground")) {
306
+ out.push(decl(PROP.COLOR, { token: `color.${key}`, raw: cls, source: "tailwind" }));
307
+ }
308
+ continue;
309
+ }
310
+
311
+ // `bg-red-500` / `text-red-500` / `border-blue-200`: a palette colour chosen
312
+ // at the call site. This is the shape System B's `no-arbitrary-color` targets.
313
+ if ((m = cls.match(/^(bg|text|border|fill|stroke|ring|outline)-([a-z]+)-(\d{2,3})$/))) {
314
+ out.push(decl(PROP.COLOR, { raw: cls, source: "tailwind" }));
315
+ continue;
316
+ }
317
+
318
+ if ((m = cls.match(/^(bg|border|ring|fill|stroke|outline)-([\w-]+)$/))) {
319
+ const word = m[2].replace(/\/\d+$/, "");
320
+ if (SEMANTIC_COLOUR_WORDS.has(word) || word.endsWith("foreground")) {
321
+ out.push(decl(PROP.COLOR, { token: `color.${word}`, raw: cls, source: "tailwind" }));
322
+ } else if (NAMED_COLOURS.has(word) || /^\[?#/.test(word)) {
323
+ out.push(decl(PROP.COLOR, { raw: cls, source: "tailwind" }));
324
+ }
325
+ continue;
326
+ }
327
+
328
+ if ((m = cls.match(/^-?(p|m|gap|space)([xytrbl])?-(\d+(?:\.\d+)?)$/))) {
329
+ out.push(decl(PROP.SPACE, {
330
+ px: parseFloat(m[3]) * spacingStep, raw: cls, source: "tailwind",
331
+ confidence: "assumed", scaleHint: `tailwind spacing step ${spacingStep}px`,
332
+ }));
333
+ continue;
334
+ }
335
+
336
+ if ((m = cls.match(/^shadow(?:-(\w+))?$/))) {
337
+ const key = m[1] ?? "DEFAULT";
338
+ if (key !== "none") out.push(decl(PROP.SHADOW, { raw: cls, source: "tailwind" }));
339
+ continue;
340
+ }
341
+ }
342
+ return out;
343
+ }
344
+
345
+ function utilityProperty(prefix) {
346
+ if (prefix === "rounded") return PROP.RADIUS;
347
+ if (prefix === "text") return PROP.TYPE;
348
+ if (prefix === "shadow") return PROP.SHADOW;
349
+ if (/^-?(p|m|gap|space)[xytrbl]?$/.test(prefix)) return PROP.SPACE;
350
+ if (/^(bg|border|fill|stroke|ring|outline)$/.test(prefix)) return PROP.COLOR;
351
+ return null;
352
+ }
353
+
354
+ /**
355
+ * A source file, whatever it is written in.
356
+ *
357
+ * Walks CSS declarations, JS/StyleX style objects, and class strings in one
358
+ * pass, so a caller never has to know which idiom a repository uses — which is
359
+ * the entire point, since two of our three repositories use more than one.
360
+ */
361
+ export function normaliseSource(text, { filename = "", rootFontSize = 16, spacingStep = TAILWIND_SPACING_STEP } = {}) {
362
+ const isStyleSheet = /\.(css|scss)$/.test(filename);
363
+ if (isStyleSheet) return normaliseCss(text, { rootFontSize });
364
+
365
+ const out = [];
366
+ text.split("\n").forEach((line, i) => {
367
+ const at = (ds) => ds.forEach((d) => out.push({ ...d, line: i + 1 }));
368
+
369
+ /**
370
+ * Ranges of this line already accounted for.
371
+ *
372
+ * The first version ran the `className=` matcher and the bare-string matcher
373
+ * independently, so every class inside a className attribute was counted
374
+ * TWICE — `className="rounded-lg p-4 bg-red-500"` produced six findings
375
+ * instead of three, and every headline number built on Tailwind source was
376
+ * roughly double the truth.
377
+ *
378
+ * That is the exact failure this product exists to prevent, arriving in our
379
+ * own measurement: a number that is confidently wrong, in the direction that
380
+ * makes the tool look more useful. Claiming ranges is the fix, and the
381
+ * double-count test below is what stops it coming back.
382
+ */
383
+ const claimed = [];
384
+ const overlaps = (start, end) => claimed.some(([s, e]) => start < e && end > s);
385
+ const claim = (start, end) => claimed.push([start, end]);
386
+
387
+ // `className="..."` / `class="..."`.
388
+ for (const m of line.matchAll(/(?:className|class)\s*=\s*[{"'`]([^"'`}]*)/g)) {
389
+ claim(m.index, m.index + m[0].length);
390
+ at(normaliseUtilities(m[1], { spacingStep }));
391
+ }
392
+ // A quoted run of three or more utility-looking words. cva() and clsx()
393
+ // arguments are not attached to a className, so without this the whole
394
+ // shadcn variant table is invisible — but anything already claimed above is
395
+ // the same string seen twice.
396
+ for (const m of line.matchAll(/["'`]([a-z0-9-]+(?:\s+[a-z0-9:./[\]-]+){2,})["'`]/gi)) {
397
+ if (overlaps(m.index, m.index + m[0].length)) continue;
398
+ if (/\b(rounded|text-|bg-|p-|px-|py-|m-|gap-|border|shadow|flex|grid|h-|w-)/.test(m[1])) {
399
+ claim(m.index, m.index + m[0].length);
400
+ at(normaliseUtilities(m[1], { spacingStep }));
401
+ }
402
+ }
403
+
404
+ // Style objects: `borderRadius: 8`, `padding: "16px"`, `color: colors.ink`.
405
+ for (const m of line.matchAll(/([A-Za-z][\w$]*)\s*:\s*("[^"]*"|'[^']*'|`[^`]*`|[\w$.()#%-]+)/g)) {
406
+ const value = m[2].replace(/^["'`]|["'`]$/g, "");
407
+ at(normaliseDeclaration(m[1], value, { source: "object", rootFontSize }));
408
+ }
409
+
410
+ // Imperative DOM styling — `el.style.background = "red"`, `setProperty(...)`.
411
+ for (const m of line.matchAll(/\.style\.([A-Za-z][\w$]*)\s*=\s*["'`]([^"'`]*)["'`]/g)) {
412
+ at(normaliseDeclaration(m[1], m[2], { source: "object", rootFontSize }));
413
+ }
414
+ });
415
+ return out;
416
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@weatherboard/gyde-design",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "description": "Scaffolds a design system into a product repository, then keeps auditing it.",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Another-Iteration/gyde.git",
11
+ "directory": "packages/design"
12
+ },
13
+ "bin": {
14
+ "gyde-design": "./cli.mjs"
15
+ },
16
+ "exports": {
17
+ ".": "./index.mjs",
18
+ "./workspace.mjs": "./workspace.mjs",
19
+ "./normalise.mjs": "./normalise.mjs",
20
+ "./rules.mjs": "./rules.mjs",
21
+ "./scan.mjs": "./scan.mjs",
22
+ "./tokens.mjs": "./tokens.mjs",
23
+ "./boundaries.mjs": "./boundaries.mjs",
24
+ "./ratchet.mjs": "./ratchet.mjs",
25
+ "./emit.mjs": "./emit.mjs",
26
+ "./catalogue.mjs": "./catalogue.mjs",
27
+ "./agentdocs.mjs": "./agentdocs.mjs",
28
+ "./upgrade.mjs": "./upgrade.mjs",
29
+ "./usage.mjs": "./usage.mjs",
30
+ "./wiring.mjs": "./wiring.mjs",
31
+ "./clientboundary.mjs": "./clientboundary.mjs",
32
+ "./props.mjs": "./props.mjs",
33
+ "./docdrift.mjs": "./docdrift.mjs",
34
+ "./compound.mjs": "./compound.mjs",
35
+ "./markup.mjs": "./markup.mjs",
36
+ "./adoption.mjs": "./adoption.mjs",
37
+ "./tailwind.mjs": "./tailwind.mjs",
38
+ "./migration.mjs": "./migration.mjs",
39
+ "./stylex.mjs": "./stylex.mjs",
40
+ "./enforcement.mjs": "./enforcement.mjs"
41
+ },
42
+ "files": [
43
+ "*.mjs",
44
+ "!*.test.mjs",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "scripts": {
52
+ "test": "node --test *.test.mjs",
53
+ "prepublishOnly": "npm test && node ./scripts/check-tarball.mjs"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ }
59
+ }