@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/LICENSE +21 -0
- package/README.md +264 -0
- package/adoption.mjs +138 -0
- package/agentdocs.mjs +246 -0
- package/boundaries.mjs +350 -0
- package/catalogue.mjs +506 -0
- package/cli.mjs +723 -0
- package/clientboundary.mjs +399 -0
- package/compound.mjs +123 -0
- package/docdrift.mjs +439 -0
- package/emit.mjs +862 -0
- package/enforcement.mjs +100 -0
- package/index.mjs +40 -0
- package/markup.mjs +177 -0
- package/migration.mjs +148 -0
- package/normalise.mjs +416 -0
- package/package.json +59 -0
- package/props.mjs +255 -0
- package/ratchet.mjs +290 -0
- package/rules.mjs +258 -0
- package/scan.mjs +291 -0
- package/stylex.mjs +178 -0
- package/tailwind.mjs +238 -0
- package/tokens.mjs +398 -0
- package/upgrade.mjs +344 -0
- package/usage.mjs +245 -0
- package/wiring.mjs +297 -0
- package/workflow.mjs +221 -0
- package/workspace.mjs +318 -0
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
|
+
}
|