brandrail 0.3.1 → 0.4.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/brandrail.mjs +1177 -130
- package/package.json +3 -2
package/dist/brandrail.mjs
CHANGED
|
@@ -1,8 +1,491 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
4
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
var require2 = createRequire("/");
|
|
10
|
+
var _a;
|
|
11
|
+
var Worker;
|
|
12
|
+
var isMarkedAsUntransferable;
|
|
13
|
+
try {
|
|
14
|
+
_a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
}
|
|
17
|
+
var u8 = Uint8Array;
|
|
18
|
+
var u16 = Uint16Array;
|
|
19
|
+
var i32 = Int32Array;
|
|
20
|
+
var fleb = new u8([
|
|
21
|
+
0,
|
|
22
|
+
0,
|
|
23
|
+
0,
|
|
24
|
+
0,
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
0,
|
|
28
|
+
0,
|
|
29
|
+
1,
|
|
30
|
+
1,
|
|
31
|
+
1,
|
|
32
|
+
1,
|
|
33
|
+
2,
|
|
34
|
+
2,
|
|
35
|
+
2,
|
|
36
|
+
2,
|
|
37
|
+
3,
|
|
38
|
+
3,
|
|
39
|
+
3,
|
|
40
|
+
3,
|
|
41
|
+
4,
|
|
42
|
+
4,
|
|
43
|
+
4,
|
|
44
|
+
4,
|
|
45
|
+
5,
|
|
46
|
+
5,
|
|
47
|
+
5,
|
|
48
|
+
5,
|
|
49
|
+
0,
|
|
50
|
+
/* unused */
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
/* impossible */
|
|
54
|
+
0
|
|
55
|
+
]);
|
|
56
|
+
var fdeb = new u8([
|
|
57
|
+
0,
|
|
58
|
+
0,
|
|
59
|
+
0,
|
|
60
|
+
0,
|
|
61
|
+
1,
|
|
62
|
+
1,
|
|
63
|
+
2,
|
|
64
|
+
2,
|
|
65
|
+
3,
|
|
66
|
+
3,
|
|
67
|
+
4,
|
|
68
|
+
4,
|
|
69
|
+
5,
|
|
70
|
+
5,
|
|
71
|
+
6,
|
|
72
|
+
6,
|
|
73
|
+
7,
|
|
74
|
+
7,
|
|
75
|
+
8,
|
|
76
|
+
8,
|
|
77
|
+
9,
|
|
78
|
+
9,
|
|
79
|
+
10,
|
|
80
|
+
10,
|
|
81
|
+
11,
|
|
82
|
+
11,
|
|
83
|
+
12,
|
|
84
|
+
12,
|
|
85
|
+
13,
|
|
86
|
+
13,
|
|
87
|
+
/* unused */
|
|
88
|
+
0,
|
|
89
|
+
0
|
|
90
|
+
]);
|
|
91
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
92
|
+
var freb = function(eb, start) {
|
|
93
|
+
var b = new u16(31);
|
|
94
|
+
for (var i2 = 0; i2 < 31; ++i2) {
|
|
95
|
+
b[i2] = start += 1 << eb[i2 - 1];
|
|
96
|
+
}
|
|
97
|
+
var r = new i32(b[30]);
|
|
98
|
+
for (var i2 = 1; i2 < 30; ++i2) {
|
|
99
|
+
for (var j = b[i2]; j < b[i2 + 1]; ++j) {
|
|
100
|
+
r[j] = j - b[i2] << 5 | i2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { b, r };
|
|
104
|
+
};
|
|
105
|
+
var _a = freb(fleb, 2);
|
|
106
|
+
var fl = _a.b;
|
|
107
|
+
var revfl = _a.r;
|
|
108
|
+
fl[28] = 258, revfl[258] = 28;
|
|
109
|
+
var _b = freb(fdeb, 0);
|
|
110
|
+
var fd = _b.b;
|
|
111
|
+
var revfd = _b.r;
|
|
112
|
+
var rev = new u16(32768);
|
|
113
|
+
for (i = 0; i < 32768; ++i) {
|
|
114
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
115
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
116
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
117
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
118
|
+
}
|
|
119
|
+
var x;
|
|
120
|
+
var i;
|
|
121
|
+
var hMap = (function(cd, mb, r) {
|
|
122
|
+
var s = cd.length;
|
|
123
|
+
var i2 = 0;
|
|
124
|
+
var l = new u16(mb);
|
|
125
|
+
for (; i2 < s; ++i2) {
|
|
126
|
+
if (cd[i2])
|
|
127
|
+
++l[cd[i2] - 1];
|
|
128
|
+
}
|
|
129
|
+
var le = new u16(mb);
|
|
130
|
+
for (i2 = 1; i2 < mb; ++i2) {
|
|
131
|
+
le[i2] = le[i2 - 1] + l[i2 - 1] << 1;
|
|
132
|
+
}
|
|
133
|
+
var co;
|
|
134
|
+
if (r) {
|
|
135
|
+
co = new u16(1 << mb);
|
|
136
|
+
var rvb = 15 - mb;
|
|
137
|
+
for (i2 = 0; i2 < s; ++i2) {
|
|
138
|
+
if (cd[i2]) {
|
|
139
|
+
var sv = i2 << 4 | cd[i2];
|
|
140
|
+
var r_1 = mb - cd[i2];
|
|
141
|
+
var v = le[cd[i2] - 1]++ << r_1;
|
|
142
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
143
|
+
co[rev[v] >> rvb] = sv;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
co = new u16(s);
|
|
149
|
+
for (i2 = 0; i2 < s; ++i2) {
|
|
150
|
+
if (cd[i2]) {
|
|
151
|
+
co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return co;
|
|
156
|
+
});
|
|
157
|
+
var flt = new u8(288);
|
|
158
|
+
for (i = 0; i < 144; ++i)
|
|
159
|
+
flt[i] = 8;
|
|
160
|
+
var i;
|
|
161
|
+
for (i = 144; i < 256; ++i)
|
|
162
|
+
flt[i] = 9;
|
|
163
|
+
var i;
|
|
164
|
+
for (i = 256; i < 280; ++i)
|
|
165
|
+
flt[i] = 7;
|
|
166
|
+
var i;
|
|
167
|
+
for (i = 280; i < 288; ++i)
|
|
168
|
+
flt[i] = 8;
|
|
169
|
+
var i;
|
|
170
|
+
var fdt = new u8(32);
|
|
171
|
+
for (i = 0; i < 32; ++i)
|
|
172
|
+
fdt[i] = 5;
|
|
173
|
+
var i;
|
|
174
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
175
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
176
|
+
var max = function(a) {
|
|
177
|
+
var m = a[0];
|
|
178
|
+
for (var i2 = 1; i2 < a.length; ++i2) {
|
|
179
|
+
if (a[i2] > m)
|
|
180
|
+
m = a[i2];
|
|
181
|
+
}
|
|
182
|
+
return m;
|
|
183
|
+
};
|
|
184
|
+
var bits = function(d, p, m) {
|
|
185
|
+
var o = p / 8 | 0;
|
|
186
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
187
|
+
};
|
|
188
|
+
var bits16 = function(d, p) {
|
|
189
|
+
var o = p / 8 | 0;
|
|
190
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
191
|
+
};
|
|
192
|
+
var shft = function(p) {
|
|
193
|
+
return (p + 7) / 8 | 0;
|
|
194
|
+
};
|
|
195
|
+
var slc = function(v, s, e) {
|
|
196
|
+
if (s == null || s < 0)
|
|
197
|
+
s = 0;
|
|
198
|
+
if (e == null || e > v.length)
|
|
199
|
+
e = v.length;
|
|
200
|
+
return new u8(v.subarray(s, e));
|
|
201
|
+
};
|
|
202
|
+
var ec = [
|
|
203
|
+
"unexpected EOF",
|
|
204
|
+
"invalid block type",
|
|
205
|
+
"invalid length/literal",
|
|
206
|
+
"invalid distance",
|
|
207
|
+
"stream finished",
|
|
208
|
+
"no stream handler",
|
|
209
|
+
,
|
|
210
|
+
// determined by compression function
|
|
211
|
+
"no callback",
|
|
212
|
+
"invalid UTF-8 data",
|
|
213
|
+
"extra field too long",
|
|
214
|
+
"date not in range 1980-2099",
|
|
215
|
+
"filename too long",
|
|
216
|
+
"stream finishing",
|
|
217
|
+
"invalid zip data"
|
|
218
|
+
// determined by unknown compression method
|
|
219
|
+
];
|
|
220
|
+
var err = function(ind, msg, nt) {
|
|
221
|
+
var e = new Error(msg || ec[ind]);
|
|
222
|
+
e.code = ind;
|
|
223
|
+
if (Error.captureStackTrace)
|
|
224
|
+
Error.captureStackTrace(e, err);
|
|
225
|
+
if (!nt)
|
|
226
|
+
throw e;
|
|
227
|
+
return e;
|
|
228
|
+
};
|
|
229
|
+
var inflt = function(dat, st, buf, dict) {
|
|
230
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
231
|
+
if (!sl || st.f && !st.l)
|
|
232
|
+
return buf || new u8(0);
|
|
233
|
+
var noBuf = !buf;
|
|
234
|
+
var resize = noBuf || st.i != 2;
|
|
235
|
+
var noSt = st.i;
|
|
236
|
+
if (noBuf)
|
|
237
|
+
buf = new u8(sl * 3);
|
|
238
|
+
var cbuf = function(l2) {
|
|
239
|
+
var bl = buf.length;
|
|
240
|
+
if (l2 > bl) {
|
|
241
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
242
|
+
nbuf.set(buf);
|
|
243
|
+
buf = nbuf;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
247
|
+
var tbts = sl * 8;
|
|
248
|
+
do {
|
|
249
|
+
if (!lm) {
|
|
250
|
+
final = bits(dat, pos, 1);
|
|
251
|
+
var type = bits(dat, pos + 1, 3);
|
|
252
|
+
pos += 3;
|
|
253
|
+
if (!type) {
|
|
254
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
255
|
+
if (t > sl) {
|
|
256
|
+
if (noSt)
|
|
257
|
+
err(0);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
if (resize)
|
|
261
|
+
cbuf(bt + l);
|
|
262
|
+
buf.set(dat.subarray(s, t), bt);
|
|
263
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
264
|
+
continue;
|
|
265
|
+
} else if (type == 1)
|
|
266
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
267
|
+
else if (type == 2) {
|
|
268
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
269
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
270
|
+
pos += 14;
|
|
271
|
+
var ldt = new u8(tl);
|
|
272
|
+
var clt = new u8(19);
|
|
273
|
+
for (var i2 = 0; i2 < hcLen; ++i2) {
|
|
274
|
+
clt[clim[i2]] = bits(dat, pos + i2 * 3, 7);
|
|
275
|
+
}
|
|
276
|
+
pos += hcLen * 3;
|
|
277
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
278
|
+
var clm = hMap(clt, clb, 1);
|
|
279
|
+
for (var i2 = 0; i2 < tl; ) {
|
|
280
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
281
|
+
pos += r & 15;
|
|
282
|
+
var s = r >> 4;
|
|
283
|
+
if (s < 16) {
|
|
284
|
+
ldt[i2++] = s;
|
|
285
|
+
} else {
|
|
286
|
+
var c = 0, n = 0;
|
|
287
|
+
if (s == 16)
|
|
288
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i2 - 1];
|
|
289
|
+
else if (s == 17)
|
|
290
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
291
|
+
else if (s == 18)
|
|
292
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
293
|
+
while (n--)
|
|
294
|
+
ldt[i2++] = c;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
298
|
+
lbt = max(lt);
|
|
299
|
+
dbt = max(dt);
|
|
300
|
+
lm = hMap(lt, lbt, 1);
|
|
301
|
+
dm = hMap(dt, dbt, 1);
|
|
302
|
+
} else
|
|
303
|
+
err(1);
|
|
304
|
+
if (pos > tbts) {
|
|
305
|
+
if (noSt)
|
|
306
|
+
err(0);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (resize)
|
|
311
|
+
cbuf(bt + 131072);
|
|
312
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
313
|
+
var lpos = pos;
|
|
314
|
+
for (; ; lpos = pos) {
|
|
315
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
316
|
+
pos += c & 15;
|
|
317
|
+
if (pos > tbts) {
|
|
318
|
+
if (noSt)
|
|
319
|
+
err(0);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (!c)
|
|
323
|
+
err(2);
|
|
324
|
+
if (sym < 256)
|
|
325
|
+
buf[bt++] = sym;
|
|
326
|
+
else if (sym == 256) {
|
|
327
|
+
lpos = pos, lm = null;
|
|
328
|
+
break;
|
|
329
|
+
} else {
|
|
330
|
+
var add = sym - 254;
|
|
331
|
+
if (sym > 264) {
|
|
332
|
+
var i2 = sym - 257, b = fleb[i2];
|
|
333
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i2];
|
|
334
|
+
pos += b;
|
|
335
|
+
}
|
|
336
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
337
|
+
if (!d)
|
|
338
|
+
err(3);
|
|
339
|
+
pos += d & 15;
|
|
340
|
+
var dt = fd[dsym];
|
|
341
|
+
if (dsym > 3) {
|
|
342
|
+
var b = fdeb[dsym];
|
|
343
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
344
|
+
}
|
|
345
|
+
if (pos > tbts) {
|
|
346
|
+
if (noSt)
|
|
347
|
+
err(0);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
if (resize)
|
|
351
|
+
cbuf(bt + 131072);
|
|
352
|
+
var end = bt + add;
|
|
353
|
+
if (bt < dt) {
|
|
354
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
355
|
+
if (shift + bt < 0)
|
|
356
|
+
err(3);
|
|
357
|
+
for (; bt < dend; ++bt)
|
|
358
|
+
buf[bt] = dict[shift + bt];
|
|
359
|
+
}
|
|
360
|
+
for (; bt < end; ++bt)
|
|
361
|
+
buf[bt] = buf[bt - dt];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
365
|
+
if (lm)
|
|
366
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
367
|
+
} while (!final);
|
|
368
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
369
|
+
};
|
|
370
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
371
|
+
var b2 = function(d, b) {
|
|
372
|
+
return d[b] | d[b + 1] << 8;
|
|
373
|
+
};
|
|
374
|
+
var b4 = function(d, b) {
|
|
375
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
376
|
+
};
|
|
377
|
+
var b8 = function(d, b) {
|
|
378
|
+
return b4(d, b) + b4(d, b + 4) * 4294967296;
|
|
379
|
+
};
|
|
380
|
+
function inflateSync(data, opts) {
|
|
381
|
+
return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
|
|
382
|
+
}
|
|
383
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
384
|
+
var tds = 0;
|
|
385
|
+
try {
|
|
386
|
+
td.decode(et, { stream: true });
|
|
387
|
+
tds = 1;
|
|
388
|
+
} catch (e) {
|
|
389
|
+
}
|
|
390
|
+
var dutf8 = function(d) {
|
|
391
|
+
for (var r = "", i2 = 0; ; ) {
|
|
392
|
+
var c = d[i2++];
|
|
393
|
+
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
394
|
+
if (i2 + eb > d.length)
|
|
395
|
+
return { s: r, r: slc(d, i2 - 1) };
|
|
396
|
+
if (!eb)
|
|
397
|
+
r += String.fromCharCode(c);
|
|
398
|
+
else if (eb == 3) {
|
|
399
|
+
c = ((c & 15) << 18 | (d[i2++] & 63) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
|
400
|
+
} else if (eb & 1)
|
|
401
|
+
r += String.fromCharCode((c & 31) << 6 | d[i2++] & 63);
|
|
402
|
+
else
|
|
403
|
+
r += String.fromCharCode((c & 15) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
function strFromU8(dat, latin1) {
|
|
407
|
+
if (latin1) {
|
|
408
|
+
var r = "";
|
|
409
|
+
for (var i2 = 0; i2 < dat.length; i2 += 16384)
|
|
410
|
+
r += String.fromCharCode.apply(null, dat.subarray(i2, i2 + 16384));
|
|
411
|
+
return r;
|
|
412
|
+
} else if (td) {
|
|
413
|
+
return td.decode(dat);
|
|
414
|
+
} else {
|
|
415
|
+
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
416
|
+
if (r.length)
|
|
417
|
+
err(8);
|
|
418
|
+
return s;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
var slzh = function(d, b) {
|
|
422
|
+
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
423
|
+
};
|
|
424
|
+
var zh = function(d, b, z) {
|
|
425
|
+
var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
|
|
426
|
+
var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
427
|
+
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
428
|
+
};
|
|
429
|
+
var z64hs = function(d, b, l, z, sc, su, off) {
|
|
430
|
+
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
431
|
+
var nf = nsc + nsu + noff;
|
|
432
|
+
if (z && nf) {
|
|
433
|
+
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
434
|
+
if (b2(d, b) == 1) {
|
|
435
|
+
return [
|
|
436
|
+
nsc ? b8(d, b + 4 + 8 * nsu) : sc,
|
|
437
|
+
nsu ? b8(d, b + 4) : su,
|
|
438
|
+
noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off,
|
|
439
|
+
1
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (z < 2)
|
|
444
|
+
err(13);
|
|
445
|
+
}
|
|
446
|
+
return [sc, su, off, 0];
|
|
447
|
+
};
|
|
448
|
+
function unzipSync(data, opts) {
|
|
449
|
+
var files = {};
|
|
450
|
+
var e = data.length - 22;
|
|
451
|
+
for (; b4(data, e) != 101010256; --e) {
|
|
452
|
+
if (!e || data.length - e > 65558)
|
|
453
|
+
err(13);
|
|
454
|
+
}
|
|
455
|
+
;
|
|
456
|
+
var c = b2(data, e + 8);
|
|
457
|
+
if (!c)
|
|
458
|
+
return {};
|
|
459
|
+
var o = b4(data, e + 16);
|
|
460
|
+
var z = b4(data, e - 20) == 117853008;
|
|
461
|
+
if (z) {
|
|
462
|
+
var ze = b4(data, e - 12);
|
|
463
|
+
z = b4(data, ze) == 101075792;
|
|
464
|
+
if (z) {
|
|
465
|
+
c = b4(data, ze + 32);
|
|
466
|
+
o = b4(data, ze + 48);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
var fltr = opts && opts.filter;
|
|
470
|
+
for (var i2 = 0; i2 < c; ++i2) {
|
|
471
|
+
var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
472
|
+
o = no;
|
|
473
|
+
if (!fltr || fltr({
|
|
474
|
+
name: fn,
|
|
475
|
+
size: sc,
|
|
476
|
+
originalSize: su,
|
|
477
|
+
compression: c_2
|
|
478
|
+
})) {
|
|
479
|
+
if (!c_2)
|
|
480
|
+
files[fn] = slc(data, b, b + sc);
|
|
481
|
+
else if (c_2 == 8)
|
|
482
|
+
files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
|
|
483
|
+
else
|
|
484
|
+
err(14, "unknown compression type " + c_2);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return files;
|
|
488
|
+
}
|
|
6
489
|
|
|
7
490
|
// ../engine/src/color.ts
|
|
8
491
|
var clamp = (n, lo = 0, hi = 255) => Math.min(hi, Math.max(lo, n));
|
|
@@ -11,7 +494,7 @@ function parseHex(hex) {
|
|
|
11
494
|
if (!/^[0-9a-f]+$/.test(h)) return null;
|
|
12
495
|
if (h.length === 3 || h.length === 4) h = h.split("").map((c) => c + c).join("");
|
|
13
496
|
if (h.length !== 6 && h.length !== 8) return null;
|
|
14
|
-
const n = (
|
|
497
|
+
const n = (i2) => parseInt(h.slice(i2, i2 + 2), 16);
|
|
15
498
|
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) / 255 : 1 };
|
|
16
499
|
}
|
|
17
500
|
function hslToRgb(h, s, l) {
|
|
@@ -30,12 +513,12 @@ function parseColor(input) {
|
|
|
30
513
|
if (!m) return null;
|
|
31
514
|
const parts = m[2].split(/[\s,/]+/).filter(Boolean);
|
|
32
515
|
if (parts.length < 3) return null;
|
|
33
|
-
const num = (p,
|
|
516
|
+
const num = (p, max2) => p.endsWith("%") ? parseFloat(p) / 100 * max2 : parseFloat(p);
|
|
34
517
|
const alpha = parts[3] !== void 0 ? num(parts[3], 1) : 1;
|
|
35
518
|
if (m[1].startsWith("rgb")) {
|
|
36
|
-
const [r2, g2,
|
|
37
|
-
if ([r2, g2,
|
|
38
|
-
return { r: clamp(r2), g: clamp(g2), b: clamp(
|
|
519
|
+
const [r2, g2, b3] = parts.slice(0, 3).map((p) => num(p, 255));
|
|
520
|
+
if ([r2, g2, b3].some((v) => Number.isNaN(v))) return null;
|
|
521
|
+
return { r: clamp(r2), g: clamp(g2), b: clamp(b3), a: alpha };
|
|
39
522
|
}
|
|
40
523
|
const [h, sat, l] = [parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])];
|
|
41
524
|
if ([h, sat, l].some((v) => Number.isNaN(v))) return null;
|
|
@@ -51,11 +534,11 @@ function rgbToLab({ r, g, b }) {
|
|
|
51
534
|
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
52
535
|
};
|
|
53
536
|
const [R, G, B] = [lin(r), lin(g), lin(b)];
|
|
54
|
-
const
|
|
537
|
+
const x2 = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
|
|
55
538
|
const y = R * 0.2126 + G * 0.7152 + B * 0.0722;
|
|
56
539
|
const z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
|
|
57
540
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
58
|
-
return { l: 116 * f(y) - 16, a: 500 * (f(
|
|
541
|
+
return { l: 116 * f(y) - 16, a: 500 * (f(x2) - f(y)), b: 200 * (f(y) - f(z)) };
|
|
59
542
|
}
|
|
60
543
|
function deltaE(a, b) {
|
|
61
544
|
return Math.sqrt((a.l - b.l) ** 2 + (a.a - b.a) ** 2 + (a.b - b.b) ** 2);
|
|
@@ -68,7 +551,7 @@ function contrastRatio(a, b) {
|
|
|
68
551
|
});
|
|
69
552
|
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
70
553
|
};
|
|
71
|
-
const [l1, l2] = [lum(a), lum(b)].sort((
|
|
554
|
+
const [l1, l2] = [lum(a), lum(b)].sort((x2, y) => y - x2);
|
|
72
555
|
return (l1 + 0.05) / (l2 + 0.05);
|
|
73
556
|
}
|
|
74
557
|
|
|
@@ -165,7 +648,7 @@ function normalizeFontFamily(value) {
|
|
|
165
648
|
function isGenericFont(family) {
|
|
166
649
|
return GENERIC_FONTS.has(family) || family.startsWith("var(");
|
|
167
650
|
}
|
|
168
|
-
var uniqSorted = (xs) => [...new Set(xs.map((
|
|
651
|
+
var uniqSorted = (xs) => [...new Set(xs.map((x2) => Math.round(x2 * 100) / 100))].sort((a, b) => a - b);
|
|
169
652
|
function resolveConfig(config) {
|
|
170
653
|
const all = leaves(config.tokens);
|
|
171
654
|
const colors = [];
|
|
@@ -218,13 +701,100 @@ function resolveConfig(config) {
|
|
|
218
701
|
rules: config.rules ?? {},
|
|
219
702
|
ignore: [...DEFAULT_IGNORE, ...config.ignore ?? []],
|
|
220
703
|
forbiddenImports: config.forbiddenImports ?? DEFAULT_FORBIDDEN_IMPORTS,
|
|
221
|
-
colorTolerance: config.colorTolerance ?? 2.5
|
|
704
|
+
colorTolerance: config.colorTolerance ?? 2.5,
|
|
705
|
+
framework: config.framework
|
|
222
706
|
};
|
|
223
707
|
}
|
|
224
708
|
|
|
709
|
+
// ../engine/src/frameworks.ts
|
|
710
|
+
var FRAMEWORKS = ["react", "angular", "vue", "web-components", "html"];
|
|
711
|
+
var FRAMEWORK_LABEL = {
|
|
712
|
+
react: "React",
|
|
713
|
+
angular: "Angular",
|
|
714
|
+
vue: "Vue",
|
|
715
|
+
"web-components": "Web Components",
|
|
716
|
+
html: "Plain HTML"
|
|
717
|
+
};
|
|
718
|
+
var LANG = { react: "tsx", angular: "html", vue: "vue", "web-components": "html", html: "html" };
|
|
719
|
+
var isFramework = (x2) => typeof x2 === "string" && FRAMEWORKS.includes(x2);
|
|
720
|
+
var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/\./g, "-").toLowerCase();
|
|
721
|
+
var prefixOf = (tag) => tag.includes("-") ? `${tag.split("-")[0]}-` : tag;
|
|
722
|
+
function webComponentSetup(target, u, tag) {
|
|
723
|
+
const load = u.importPath ? `Load it once with \`import "${u.importPath}";\` at the app entry.` : "";
|
|
724
|
+
switch (target) {
|
|
725
|
+
case "angular":
|
|
726
|
+
return `Web component: add \`CUSTOM_ELEMENTS_SCHEMA\` to the component's \`schemas\`. ${load}`.trim();
|
|
727
|
+
case "vue":
|
|
728
|
+
return `Web component: set \`compilerOptions.isCustomElement = (tag) => tag.startsWith("${prefixOf(tag)}")\` in the Vue config. ${load}`.trim();
|
|
729
|
+
case "html":
|
|
730
|
+
return u.importPath ? `Web component: load \`${u.importPath}\` once (a bundler import or \`<script type="module">\`).` : void 0;
|
|
731
|
+
default:
|
|
732
|
+
return u.setup ?? (load || void 0);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function resolveUsage(c, framework) {
|
|
736
|
+
if (framework === "react") {
|
|
737
|
+
return {
|
|
738
|
+
framework,
|
|
739
|
+
importLine: `import { ${c.name} } from "${c.importPath}";`,
|
|
740
|
+
tag: c.name,
|
|
741
|
+
example: c.example,
|
|
742
|
+
lang: LANG.react,
|
|
743
|
+
props: c.props
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const own = c.frameworks?.[framework];
|
|
747
|
+
if (own) return fromUsage(c, framework, framework, own);
|
|
748
|
+
const wc = c.frameworks?.["web-components"];
|
|
749
|
+
if (wc) {
|
|
750
|
+
const r = fromUsage(c, framework, "web-components", wc);
|
|
751
|
+
return { ...r, via: "web-components", setup: webComponentSetup(framework, wc, r.tag), importLine: framework === "html" ? void 0 : r.importLine };
|
|
752
|
+
}
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
function fromUsage(c, framework, kind, u) {
|
|
756
|
+
const path = u.importPath?.trim();
|
|
757
|
+
let tag = u.tag?.trim();
|
|
758
|
+
let importLine;
|
|
759
|
+
let setup = u.setup?.trim() || void 0;
|
|
760
|
+
switch (kind) {
|
|
761
|
+
case "angular":
|
|
762
|
+
tag ||= kebab(c.name);
|
|
763
|
+
if (path && u.importName) {
|
|
764
|
+
importLine = `import { ${u.importName} } from "${path}";`;
|
|
765
|
+
setup ??= `Add \`${u.importName}\` to the standalone component's \`imports\` (or the NgModule's).`;
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
case "vue":
|
|
769
|
+
tag ||= u.importName || c.name;
|
|
770
|
+
if (path) importLine = `import { ${u.importName || tag} } from "${path}";`;
|
|
771
|
+
break;
|
|
772
|
+
case "web-components":
|
|
773
|
+
tag ||= kebab(c.name);
|
|
774
|
+
if (path) importLine = `import "${path}";`;
|
|
775
|
+
break;
|
|
776
|
+
case "html":
|
|
777
|
+
tag ||= c.replaces?.[0]?.replace(/\[.*$/, "") || "div";
|
|
778
|
+
if (path && !setup) setup = /\.css(\?|$)/.test(path) ? `Include \`<link rel="stylesheet" href="${path}">\` once.` : /\.m?js(\?|$)/.test(path) ? `Include \`<script type="module" src="${path}"></script>\` once.` : void 0;
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
return { framework, importLine, tag, setup, example: u.example?.trim() || void 0, lang: LANG[kind], props: u.props?.length ? u.props : void 0 };
|
|
782
|
+
}
|
|
783
|
+
var markup = (u) => `<${u.tag}>`;
|
|
784
|
+
function detectFramework(pkg) {
|
|
785
|
+
if (!pkg) return "html";
|
|
786
|
+
const deps = { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
|
|
787
|
+
const has = (name) => name in deps;
|
|
788
|
+
if (has("@angular/core")) return "angular";
|
|
789
|
+
if (has("vue") || has("nuxt")) return "vue";
|
|
790
|
+
if (has("react") || has("next") || has("preact")) return "react";
|
|
791
|
+
if (has("svelte") || has("lit") || has("@stencil/core") || has("@sveltejs/kit")) return "web-components";
|
|
792
|
+
return "html";
|
|
793
|
+
}
|
|
794
|
+
|
|
225
795
|
// ../engine/src/rules.ts
|
|
226
|
-
var MARKUP = ["jsx", "vue", "svelte", "html"];
|
|
227
|
-
var ALL = ["jsx", "vue", "svelte", "html", "css"];
|
|
796
|
+
var MARKUP = ["jsx", "vue", "svelte", "html", "template"];
|
|
797
|
+
var ALL = ["jsx", "vue", "svelte", "html", "template", "css"];
|
|
228
798
|
function* matches(re, text) {
|
|
229
799
|
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
|
230
800
|
let m;
|
|
@@ -233,8 +803,8 @@ function* matches(re, text) {
|
|
|
233
803
|
if (m[0].length === 0) g.lastIndex++;
|
|
234
804
|
}
|
|
235
805
|
}
|
|
236
|
-
var lineBefore = (code, offset,
|
|
237
|
-
const start = Math.max(code.lastIndexOf("\n", offset - 1) + 1, offset -
|
|
806
|
+
var lineBefore = (code, offset, max2 = 80) => {
|
|
807
|
+
const start = Math.max(code.lastIndexOf("\n", offset - 1) + 1, offset - max2);
|
|
238
808
|
return code.slice(start, offset);
|
|
239
809
|
};
|
|
240
810
|
var lineOf = (code, offset) => {
|
|
@@ -244,9 +814,9 @@ var lineOf = (code, offset) => {
|
|
|
244
814
|
};
|
|
245
815
|
var fmt = (n) => `${Math.round(n * 100) / 100}px`;
|
|
246
816
|
function neighbours(scale, v) {
|
|
247
|
-
const lower = [...scale].reverse().find((
|
|
248
|
-
const upper = scale.find((
|
|
249
|
-
const opts = [...new Set([lower, upper].filter((
|
|
817
|
+
const lower = [...scale].reverse().find((x2) => x2 <= v);
|
|
818
|
+
const upper = scale.find((x2) => x2 >= v);
|
|
819
|
+
const opts = [...new Set([lower, upper].filter((x2) => x2 !== void 0))];
|
|
250
820
|
return opts.map(fmt).join(" or ");
|
|
251
821
|
}
|
|
252
822
|
function pxValues(value) {
|
|
@@ -257,6 +827,38 @@ function pxValues(value) {
|
|
|
257
827
|
}
|
|
258
828
|
return out;
|
|
259
829
|
}
|
|
830
|
+
var kebab2 = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
831
|
+
function styleBindings(code) {
|
|
832
|
+
const out = [];
|
|
833
|
+
const withUnit = (v, unit) => unit && /^-?\d*\.?\d+$/.test(v.trim()) ? `${v.trim()}${unit === "%" ? "%" : unit}` : v;
|
|
834
|
+
for (const m of matches(/(['"])([a-zA-Z-]+)(?:\.(px|rem|em|%))?\1\s*:\s*(?:(['"`])([^'"`\n]*)\4|(-?\d*\.?\d+)(?![\w.]))/g, code)) {
|
|
835
|
+
const value = m[5] ?? m[6];
|
|
836
|
+
const offset = m.index + m[0].length - value.length - (m[5] !== void 0 ? 1 : 0);
|
|
837
|
+
out.push({ prop: kebab2(m[2]), value: withUnit(value, m[3]), offset });
|
|
838
|
+
}
|
|
839
|
+
for (const m of matches(/(?<![\w.'"-])((?:padding|margin)(?:Top|Right|Bottom|Left|Inline|Block)(?:Start|End)?|rowGap|columnGap|border(?:Top|Bottom)?(?:Left|Right)?Radius)\s*:\s*(['"`])([^'"`\n]*\d(?:px|rem)[^'"`\n]*)\2/g, code)) {
|
|
840
|
+
out.push({ prop: kebab2(m[1]), value: m[3], offset: m.index + m[0].length - m[3].length - 1 });
|
|
841
|
+
}
|
|
842
|
+
for (const m of matches(/\[style\.([a-zA-Z-]+)(?:\.(px|rem|em|%))?\]\s*=\s*(["'])([\s\S]*?)\3/g, code)) {
|
|
843
|
+
const inner = m[4].trim();
|
|
844
|
+
const lit = /^(['"`])(.*)\1$/.exec(inner)?.[2] ?? (/^-?\d*\.?\d+$/.test(inner) ? inner : null);
|
|
845
|
+
if (lit === null) continue;
|
|
846
|
+
out.push({ prop: kebab2(m[1]), value: withUnit(lit, m[2]), offset: m.index + m[0].length - 1 - m[4].length + m[4].indexOf(inner) });
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
function declValue(v, kind) {
|
|
851
|
+
if (kind === "css") return v;
|
|
852
|
+
const q = v[0];
|
|
853
|
+
if (q === '"' || q === "'" || q === "`") {
|
|
854
|
+
const end = v.indexOf(q, 1);
|
|
855
|
+
return end === -1 ? v : v.slice(0, end + 1);
|
|
856
|
+
}
|
|
857
|
+
const cut = v.search(/["'`>]/);
|
|
858
|
+
return cut === -1 ? v : v.slice(0, cut);
|
|
859
|
+
}
|
|
860
|
+
var SPACING_PROP = /^(?:(?:padding|margin)(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?|gap|row-gap|column-gap)$/;
|
|
861
|
+
var RADIUS_PROP = /^border(?:-(?:top|bottom|start|end)-(?:left|right|start|end))?-radius$/;
|
|
260
862
|
var noHardcodedColor = {
|
|
261
863
|
id: "no-hardcoded-color",
|
|
262
864
|
title: "Hard-coded colour",
|
|
@@ -342,7 +944,7 @@ var spacingScale = {
|
|
|
342
944
|
const ok = (n) => scale.some((s) => Math.abs(s - n) < 0.01);
|
|
343
945
|
for (const m of matches(/(?<![\w-])(padding|margin|gap|row-gap|column-gap)(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?\s*:\s*([^;{}\n]+)/g, code)) {
|
|
344
946
|
const valueStart = m.index + m[0].length - m[2].length;
|
|
345
|
-
for (const v of pxValues(m[2])) {
|
|
947
|
+
for (const v of pxValues(declValue(m[2], kind))) {
|
|
346
948
|
if (!ok(v.n)) out.push({ offset: valueStart + v.index, message: `${m[1]} ${fmt(v.n)} is off the spacing scale`, suggestion: `Use ${neighbours(scale, v.n)}` });
|
|
347
949
|
}
|
|
348
950
|
}
|
|
@@ -355,6 +957,12 @@ var spacingScale = {
|
|
|
355
957
|
const n = Math.abs(parseFloat(m[2])) * (m[3] === "rem" ? 16 : 1);
|
|
356
958
|
if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is off the spacing scale`, suggestion: `Use ${neighbours(scale, n)}` });
|
|
357
959
|
}
|
|
960
|
+
for (const b of styleBindings(code)) {
|
|
961
|
+
if (!SPACING_PROP.test(b.prop)) continue;
|
|
962
|
+
for (const v of pxValues(b.value)) {
|
|
963
|
+
if (!ok(v.n)) out.push({ offset: b.offset + Math.min(v.index, b.value.length), message: `${b.prop} ${fmt(v.n)} is off the spacing scale`, suggestion: `Use ${neighbours(scale, v.n)}` });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
358
966
|
}
|
|
359
967
|
return out;
|
|
360
968
|
}
|
|
@@ -370,7 +978,7 @@ var radiusScale = {
|
|
|
370
978
|
const ok = (n) => n >= 999 || config.radii.some((r) => Math.abs(r - n) < 0.01);
|
|
371
979
|
for (const m of matches(/(?<![\w-])border(?:-(?:top|bottom|start|end)-(?:left|right|start|end))?-radius\s*:\s*([^;{}\n]+)/g, code)) {
|
|
372
980
|
const valueStart = m.index + m[0].length - m[1].length;
|
|
373
|
-
for (const v of pxValues(m[1])) {
|
|
981
|
+
for (const v of pxValues(declValue(m[1], kind))) {
|
|
374
982
|
if (!ok(v.n)) out.push({ offset: valueStart + v.index, message: `border-radius ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), v.n)}` });
|
|
375
983
|
}
|
|
376
984
|
}
|
|
@@ -383,6 +991,12 @@ var radiusScale = {
|
|
|
383
991
|
const n = parseFloat(m[1]);
|
|
384
992
|
if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), n)}` });
|
|
385
993
|
}
|
|
994
|
+
for (const b of styleBindings(code)) {
|
|
995
|
+
if (!RADIUS_PROP.test(b.prop)) continue;
|
|
996
|
+
for (const v of pxValues(b.value)) {
|
|
997
|
+
if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), v.n)}` });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
386
1000
|
}
|
|
387
1001
|
return out;
|
|
388
1002
|
}
|
|
@@ -406,10 +1020,11 @@ var fontFamily = {
|
|
|
406
1020
|
suggestion: `Use var(--font-family-sans) (${allowed[0]})`
|
|
407
1021
|
});
|
|
408
1022
|
};
|
|
409
|
-
for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(m[1], m.index);
|
|
1023
|
+
for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
|
|
410
1024
|
if (kind !== "css") {
|
|
411
1025
|
for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
|
|
412
1026
|
for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
|
|
1027
|
+
for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
|
|
413
1028
|
}
|
|
414
1029
|
return out;
|
|
415
1030
|
}
|
|
@@ -421,7 +1036,7 @@ var preferOfficialComponent = {
|
|
|
421
1036
|
description: "Use the design system component instead of a raw HTML element.",
|
|
422
1037
|
defaultSeverity: "error",
|
|
423
1038
|
kinds: MARKUP,
|
|
424
|
-
check({ code, config }) {
|
|
1039
|
+
check({ code, config, framework }) {
|
|
425
1040
|
if (config.replaces.size === 0) return [];
|
|
426
1041
|
const out = [];
|
|
427
1042
|
const baseTags = [...new Set([...config.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
|
|
@@ -436,16 +1051,28 @@ var preferOfficialComponent = {
|
|
|
436
1051
|
else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config.replaces.get("button") : void 0;
|
|
437
1052
|
}
|
|
438
1053
|
if (!comp) continue;
|
|
1054
|
+
const usage = resolveUsage(comp, framework);
|
|
1055
|
+
if (!usage) continue;
|
|
439
1056
|
if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
|
|
1057
|
+
if (framework !== "react" && implementsTag(code, usage.tag)) continue;
|
|
440
1058
|
out.push({
|
|
441
1059
|
offset: m.index,
|
|
442
1060
|
message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
|
|
443
|
-
suggestion:
|
|
1061
|
+
suggestion: useSuggestion(comp, usage)
|
|
444
1062
|
});
|
|
445
1063
|
}
|
|
446
1064
|
return out;
|
|
447
1065
|
}
|
|
448
1066
|
};
|
|
1067
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1068
|
+
var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
|
|
1069
|
+
function useSuggestion(comp, u) {
|
|
1070
|
+
if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
|
|
1071
|
+
const parts = [`Use <${u.tag}>`];
|
|
1072
|
+
if (u.importLine) parts.push(`(${u.importLine})`);
|
|
1073
|
+
if (u.setup && u.via) parts.push(`\u2014 ${u.setup}`);
|
|
1074
|
+
return parts.join(" ");
|
|
1075
|
+
}
|
|
449
1076
|
var forbiddenImport = {
|
|
450
1077
|
id: "forbidden-import",
|
|
451
1078
|
title: "Other UI library",
|
|
@@ -501,15 +1128,15 @@ function importedComponents(code, components) {
|
|
|
501
1128
|
}
|
|
502
1129
|
return out;
|
|
503
1130
|
}
|
|
504
|
-
function skipBalanced(code,
|
|
505
|
-
const open = code[
|
|
1131
|
+
function skipBalanced(code, i2) {
|
|
1132
|
+
const open = code[i2];
|
|
506
1133
|
if (open === '"' || open === "'") {
|
|
507
|
-
const end = code.indexOf(open,
|
|
1134
|
+
const end = code.indexOf(open, i2 + 1);
|
|
508
1135
|
return end === -1 ? code.length : end + 1;
|
|
509
1136
|
}
|
|
510
1137
|
let depth = 0;
|
|
511
1138
|
let quote = null;
|
|
512
|
-
for (let j =
|
|
1139
|
+
for (let j = i2; j < code.length; j++) {
|
|
513
1140
|
const c = code[j];
|
|
514
1141
|
if (quote) {
|
|
515
1142
|
if (c === "\\") j++;
|
|
@@ -525,38 +1152,121 @@ function skipBalanced(code, i) {
|
|
|
525
1152
|
function parseAttributes(code, start) {
|
|
526
1153
|
const attrs = [];
|
|
527
1154
|
let spread = false;
|
|
528
|
-
let
|
|
529
|
-
while (
|
|
530
|
-
while (/\s/.test(code[
|
|
531
|
-
const c = code[
|
|
1155
|
+
let i2 = start;
|
|
1156
|
+
while (i2 < code.length) {
|
|
1157
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1158
|
+
const c = code[i2];
|
|
532
1159
|
if (c === void 0) return null;
|
|
533
1160
|
if (c === ">") return { attrs, spread, selfClosing: false };
|
|
534
|
-
if (c === "/" && code[
|
|
1161
|
+
if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
|
|
535
1162
|
if (c === "{") {
|
|
536
1163
|
spread = true;
|
|
537
|
-
|
|
1164
|
+
i2 = skipBalanced(code, i2);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
|
|
1168
|
+
if (!name) return null;
|
|
1169
|
+
attrs.push({ name, offset: i2 });
|
|
1170
|
+
i2 += name.length;
|
|
1171
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1172
|
+
if (code[i2] === "=") {
|
|
1173
|
+
i2++;
|
|
1174
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1175
|
+
i2 = skipBalanced(code, i2);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
function parseHtmlAttributes(code, start) {
|
|
1181
|
+
const attrs = [];
|
|
1182
|
+
let i2 = start;
|
|
1183
|
+
while (i2 < code.length) {
|
|
1184
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1185
|
+
const c = code[i2];
|
|
1186
|
+
if (c === void 0) return null;
|
|
1187
|
+
if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
|
|
1188
|
+
if (c === "{" || c === "<" && code[i2 + 1] === "?") {
|
|
1189
|
+
const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
|
|
1190
|
+
const e = code.indexOf(close, i2 + 2);
|
|
1191
|
+
i2 = e === -1 ? code.length : e + close.length;
|
|
538
1192
|
continue;
|
|
539
1193
|
}
|
|
540
|
-
const name = /^[
|
|
1194
|
+
const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
|
|
541
1195
|
if (!name) return null;
|
|
542
|
-
attrs.push({ name, offset:
|
|
543
|
-
|
|
544
|
-
while (/\s/.test(code[
|
|
545
|
-
if (code[
|
|
546
|
-
|
|
547
|
-
while (/\s/.test(code[
|
|
548
|
-
|
|
1196
|
+
attrs.push({ name, offset: i2 });
|
|
1197
|
+
i2 += name.length;
|
|
1198
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1199
|
+
if (code[i2] === "=") {
|
|
1200
|
+
i2++;
|
|
1201
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
1202
|
+
const q = code[i2];
|
|
1203
|
+
if (q === '"' || q === "'") {
|
|
1204
|
+
const e = code.indexOf(q, i2 + 1);
|
|
1205
|
+
i2 = e === -1 ? code.length : e + 1;
|
|
1206
|
+
} else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
|
|
549
1207
|
}
|
|
550
1208
|
}
|
|
551
1209
|
return null;
|
|
552
1210
|
}
|
|
1211
|
+
var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
|
|
1212
|
+
function propName(raw) {
|
|
1213
|
+
let n = raw;
|
|
1214
|
+
if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
|
|
1215
|
+
n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
|
|
1216
|
+
if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
|
|
1217
|
+
n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
|
|
1218
|
+
if (HTML_GLOBAL.test(n.toLowerCase())) return null;
|
|
1219
|
+
return n;
|
|
1220
|
+
}
|
|
1221
|
+
var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
|
|
1222
|
+
function templateProps({ code, config, framework }) {
|
|
1223
|
+
const tagged = [];
|
|
1224
|
+
for (const comp of config.components) {
|
|
1225
|
+
const u = resolveUsage(comp, framework);
|
|
1226
|
+
const props = u?.props;
|
|
1227
|
+
if (u && props?.length) tagged.push({ comp, tag: u.tag, props });
|
|
1228
|
+
}
|
|
1229
|
+
if (!tagged.length) return [];
|
|
1230
|
+
const out = [];
|
|
1231
|
+
const forms = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const t of tagged) {
|
|
1233
|
+
forms.set(t.tag, t);
|
|
1234
|
+
if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
|
|
1235
|
+
}
|
|
1236
|
+
const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
|
|
1237
|
+
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
1238
|
+
const t = forms.get(m[1]);
|
|
1239
|
+
const parsed = parseHtmlAttributes(code, m.index + m[0].length);
|
|
1240
|
+
if (!parsed) continue;
|
|
1241
|
+
const list = t.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
1242
|
+
const passed = [];
|
|
1243
|
+
for (const a of parsed.attrs) {
|
|
1244
|
+
const raw = a.name;
|
|
1245
|
+
if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
|
|
1246
|
+
if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
|
|
1247
|
+
const n = propName(raw);
|
|
1248
|
+
if (!n) continue;
|
|
1249
|
+
passed.push(n);
|
|
1250
|
+
if (t.props.some((p) => sameName(p.name, n))) continue;
|
|
1251
|
+
out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
|
|
1252
|
+
}
|
|
1253
|
+
for (const p of t.props) {
|
|
1254
|
+
if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
|
|
1255
|
+
if (p.name === "children" || p.name === "content") continue;
|
|
1256
|
+
out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return out;
|
|
1260
|
+
}
|
|
553
1261
|
var componentProps = {
|
|
554
1262
|
id: "component-props",
|
|
555
1263
|
title: "Unknown or missing prop",
|
|
556
1264
|
description: "Design system components only accept their documented props, and required props must be passed.",
|
|
557
1265
|
defaultSeverity: "error",
|
|
558
|
-
kinds:
|
|
559
|
-
check(
|
|
1266
|
+
kinds: MARKUP,
|
|
1267
|
+
check(ctx) {
|
|
1268
|
+
if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
|
|
1269
|
+
const { code, config } = ctx;
|
|
560
1270
|
if (!config.components.some((c) => c.props?.length)) return [];
|
|
561
1271
|
const imported = importedComponents(code, config.components);
|
|
562
1272
|
if (imported.size === 0) return [];
|
|
@@ -606,8 +1316,10 @@ var RULES = [
|
|
|
606
1316
|
var RULE_IDS = RULES.map((r) => r.id);
|
|
607
1317
|
|
|
608
1318
|
// ../engine/src/source.ts
|
|
1319
|
+
var TEMPLATE_EXTENSIONS = ["mustache", "hbs", "handlebars", "php", "phtml", "twig", "njk", "liquid"];
|
|
609
1320
|
function fileKind(path) {
|
|
610
1321
|
const ext = path.toLowerCase().split(".").pop() ?? "";
|
|
1322
|
+
if (TEMPLATE_EXTENSIONS.includes(ext)) return "template";
|
|
611
1323
|
if (["tsx", "jsx", "ts", "js", "mjs", "cjs", "mdx"].includes(ext)) return "jsx";
|
|
612
1324
|
if (ext === "vue") return "vue";
|
|
613
1325
|
if (ext === "svelte") return "svelte";
|
|
@@ -615,47 +1327,75 @@ function fileKind(path) {
|
|
|
615
1327
|
if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
|
|
616
1328
|
return "other";
|
|
617
1329
|
}
|
|
618
|
-
var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "css"]);
|
|
1330
|
+
var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
|
|
1331
|
+
var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
|
|
1332
|
+
var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
|
|
1333
|
+
function fileFramework(path, kind, code, project) {
|
|
1334
|
+
const p = path.toLowerCase();
|
|
1335
|
+
switch (kind) {
|
|
1336
|
+
case "vue":
|
|
1337
|
+
return "vue";
|
|
1338
|
+
case "svelte":
|
|
1339
|
+
return "web-components";
|
|
1340
|
+
case "template":
|
|
1341
|
+
return "html";
|
|
1342
|
+
case "css":
|
|
1343
|
+
return project ?? "react";
|
|
1344
|
+
case "html":
|
|
1345
|
+
if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
|
|
1346
|
+
return project === "vue" || project === "web-components" ? project : "html";
|
|
1347
|
+
case "jsx":
|
|
1348
|
+
if (ANGULAR_COMPONENT.test(code)) return "angular";
|
|
1349
|
+
if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
|
|
1350
|
+
return project ?? "react";
|
|
1351
|
+
default:
|
|
1352
|
+
return project ?? "react";
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
619
1355
|
var blank = (s) => s.replace(/[^\n]/g, " ");
|
|
620
1356
|
function stripComments(code, kind) {
|
|
621
1357
|
let out = code;
|
|
622
|
-
if (kind === "html" || kind === "vue" || kind === "svelte") {
|
|
1358
|
+
if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
|
|
623
1359
|
out = out.replace(/<!--[\s\S]*?-->/g, blank);
|
|
624
1360
|
}
|
|
1361
|
+
if (kind === "template") {
|
|
1362
|
+
out = out.replace(/\{\{!--[\s\S]*?--\}\}|\{\{![\s\S]*?\}\}|\{\{--[\s\S]*?--\}\}|\{#[\s\S]*?#\}|\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, blank).replace(/<\?(?:php|=)?[\s\S]*?(?:\?>|$)/g, (block) => block.replace(/\/\*[\s\S]*?\*\/|(?<![:"'])\/\/[^\n]*?(?=\?>|\n|$)|(?<=\s)#[^\n]*?(?=\?>|\n|$)/g, blank));
|
|
1363
|
+
return out;
|
|
1364
|
+
}
|
|
625
1365
|
if (kind === "css") {
|
|
626
1366
|
return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
|
|
627
1367
|
}
|
|
628
1368
|
const chars = out.split("");
|
|
629
|
-
let
|
|
1369
|
+
let i2 = 0;
|
|
630
1370
|
let quote = null;
|
|
631
|
-
while (
|
|
632
|
-
const c = chars[
|
|
633
|
-
const n = chars[
|
|
1371
|
+
while (i2 < chars.length) {
|
|
1372
|
+
const c = chars[i2];
|
|
1373
|
+
const n = chars[i2 + 1];
|
|
634
1374
|
if (quote) {
|
|
635
|
-
if (c === "\\")
|
|
1375
|
+
if (c === "\\") i2 += 2;
|
|
636
1376
|
else {
|
|
637
1377
|
if (c === quote) quote = null;
|
|
638
|
-
|
|
1378
|
+
i2++;
|
|
639
1379
|
}
|
|
640
1380
|
continue;
|
|
641
1381
|
}
|
|
642
1382
|
if (c === '"' || c === "'" || c === "`") {
|
|
643
1383
|
quote = c;
|
|
644
|
-
|
|
1384
|
+
i2++;
|
|
645
1385
|
continue;
|
|
646
1386
|
}
|
|
647
1387
|
if (c === "/" && n === "*") {
|
|
648
|
-
const end = out.indexOf("*/",
|
|
1388
|
+
const end = out.indexOf("*/", i2 + 2);
|
|
649
1389
|
const stop = end === -1 ? chars.length : end + 2;
|
|
650
|
-
for (let j =
|
|
651
|
-
|
|
1390
|
+
for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
|
|
1391
|
+
i2 = stop;
|
|
652
1392
|
continue;
|
|
653
1393
|
}
|
|
654
|
-
if (c === "/" && n === "/" && chars[
|
|
655
|
-
while (
|
|
1394
|
+
if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
|
|
1395
|
+
while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
|
|
656
1396
|
continue;
|
|
657
1397
|
}
|
|
658
|
-
|
|
1398
|
+
i2++;
|
|
659
1399
|
}
|
|
660
1400
|
return chars.join("");
|
|
661
1401
|
}
|
|
@@ -664,7 +1404,7 @@ var LineIndex = class {
|
|
|
664
1404
|
text;
|
|
665
1405
|
constructor(text) {
|
|
666
1406
|
this.text = text;
|
|
667
|
-
for (let
|
|
1407
|
+
for (let i2 = 0; i2 < text.length; i2++) if (text.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
|
|
668
1408
|
}
|
|
669
1409
|
position(offset) {
|
|
670
1410
|
let lo = 0;
|
|
@@ -687,19 +1427,19 @@ var LineIndex = class {
|
|
|
687
1427
|
};
|
|
688
1428
|
function globToRegExp(glob) {
|
|
689
1429
|
let re = "";
|
|
690
|
-
for (let
|
|
691
|
-
const c = glob[
|
|
1430
|
+
for (let i2 = 0; i2 < glob.length; i2++) {
|
|
1431
|
+
const c = glob[i2];
|
|
692
1432
|
if (c === "*") {
|
|
693
|
-
if (glob[
|
|
694
|
-
const slash = glob[
|
|
1433
|
+
if (glob[i2 + 1] === "*") {
|
|
1434
|
+
const slash = glob[i2 + 2] === "/";
|
|
695
1435
|
re += slash ? "(?:.*/)?" : ".*";
|
|
696
|
-
|
|
1436
|
+
i2 += slash ? 2 : 1;
|
|
697
1437
|
} else re += "[^/]*";
|
|
698
1438
|
} else if (c === "?") re += "[^/]";
|
|
699
1439
|
else if (c === "{") {
|
|
700
|
-
const end = glob.indexOf("}",
|
|
701
|
-
re += `(?:${glob.slice(
|
|
702
|
-
|
|
1440
|
+
const end = glob.indexOf("}", i2);
|
|
1441
|
+
re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
1442
|
+
i2 = end;
|
|
703
1443
|
} else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
704
1444
|
}
|
|
705
1445
|
return new RegExp(`^${re}$`);
|
|
@@ -713,7 +1453,7 @@ function makeIgnore(globs) {
|
|
|
713
1453
|
}
|
|
714
1454
|
|
|
715
1455
|
// ../engine/src/check.ts
|
|
716
|
-
var ENGINE_VERSION = "0.
|
|
1456
|
+
var ENGINE_VERSION = "0.4.0";
|
|
717
1457
|
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
718
1458
|
function computeScore(counts, lines) {
|
|
719
1459
|
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
@@ -738,11 +1478,12 @@ function check(files, configInput, opts = {}) {
|
|
|
738
1478
|
const idx = new LineIndex(file.content);
|
|
739
1479
|
linesScanned += only ? only.size : file.content.split("\n").filter((l) => l.trim()).length;
|
|
740
1480
|
const code = stripComments(file.content, kind);
|
|
1481
|
+
const framework = fileFramework(path, kind, code, config.framework);
|
|
741
1482
|
const seen = /* @__PURE__ */ new Set();
|
|
742
1483
|
for (const rule of RULES) {
|
|
743
1484
|
const setting = config.rules[rule.id];
|
|
744
1485
|
if (setting === "off" || !rule.kinds.includes(kind)) continue;
|
|
745
|
-
for (const f of rule.check({ path, kind, code, config })) {
|
|
1486
|
+
for (const f of rule.check({ path, kind, code, config, framework })) {
|
|
746
1487
|
const { line, column } = idx.position(f.offset);
|
|
747
1488
|
if (only && !only.has(line)) continue;
|
|
748
1489
|
const key = `${rule.id}:${line}:${column}`;
|
|
@@ -789,12 +1530,12 @@ var ansi = (on) => {
|
|
|
789
1530
|
return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
|
|
790
1531
|
};
|
|
791
1532
|
var rendered = (r) => r.violations.length > 0 && r.violations.every((v) => v.line === 0);
|
|
792
|
-
function formatPretty(report, { color = true, max = 200 } = {}) {
|
|
1533
|
+
function formatPretty(report, { color = true, max: max2 = 200 } = {}) {
|
|
793
1534
|
const c = ansi(color);
|
|
794
1535
|
const sev = { error: c.red, warning: c.yellow, info: c.cyan };
|
|
795
1536
|
const lines = [];
|
|
796
1537
|
let lastFile = "";
|
|
797
|
-
for (const v of report.violations.slice(0,
|
|
1538
|
+
for (const v of report.violations.slice(0, max2)) {
|
|
798
1539
|
if (v.file !== lastFile) {
|
|
799
1540
|
lines.push("", c.bold(v.file));
|
|
800
1541
|
lastFile = v.file;
|
|
@@ -804,7 +1545,7 @@ function formatPretty(report, { color = true, max = 200 } = {}) {
|
|
|
804
1545
|
if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
|
|
805
1546
|
if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
|
|
806
1547
|
}
|
|
807
|
-
if (report.violations.length >
|
|
1548
|
+
if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
|
|
808
1549
|
const s = report.score;
|
|
809
1550
|
const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
|
|
810
1551
|
lines.push(
|
|
@@ -816,9 +1557,9 @@ function formatPretty(report, { color = true, max = 200 } = {}) {
|
|
|
816
1557
|
}
|
|
817
1558
|
var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
818
1559
|
var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
819
|
-
function formatGithubAnnotations(report,
|
|
1560
|
+
function formatGithubAnnotations(report, max2 = 50) {
|
|
820
1561
|
const level = { error: "error", warning: "warning", info: "notice" };
|
|
821
|
-
return report.violations.slice(0,
|
|
1562
|
+
return report.violations.slice(0, max2).map(
|
|
822
1563
|
(v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
|
|
823
1564
|
v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
|
|
824
1565
|
)}`
|
|
@@ -931,6 +1672,17 @@ function fontWeight(v) {
|
|
|
931
1672
|
return WEIGHTS[slug(v)] ?? (/^\d{3}$/.test(v.trim()) ? v.trim() : v);
|
|
932
1673
|
}
|
|
933
1674
|
function colorValue(v) {
|
|
1675
|
+
if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
|
|
1676
|
+
const c = v;
|
|
1677
|
+
const alpha = typeof c.alpha === "number" ? c.alpha : 1;
|
|
1678
|
+
const withAlpha = (hex2) => alpha < 1 ? `${hex2.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex2.slice(0, 7);
|
|
1679
|
+
if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
|
|
1680
|
+
const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
|
|
1681
|
+
if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
|
|
1682
|
+
return withAlpha(toHex({ r: comps[0] * 255, g: comps[1] * 255, b: comps[2] * 255, a: 1 }));
|
|
1683
|
+
}
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
934
1686
|
if (v && typeof v === "object" && "r" in v) {
|
|
935
1687
|
const c = v;
|
|
936
1688
|
const hex2 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
|
|
@@ -948,7 +1700,14 @@ function colorValue(v) {
|
|
|
948
1700
|
const hex = toHex(rgb);
|
|
949
1701
|
return rgb.a < 1 ? `${hex}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex;
|
|
950
1702
|
}
|
|
951
|
-
var dim = (v) =>
|
|
1703
|
+
var dim = (v) => {
|
|
1704
|
+
if (v && typeof v === "object" && "value" in v) {
|
|
1705
|
+
const d = v;
|
|
1706
|
+
const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
|
|
1707
|
+
return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
|
|
1708
|
+
}
|
|
1709
|
+
return typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
|
|
1710
|
+
};
|
|
952
1711
|
function classify(name, kind, value, opts = {}) {
|
|
953
1712
|
const parts = segs(name);
|
|
954
1713
|
const hay = slug(`${opts.context ?? ""} ${name}`);
|
|
@@ -980,7 +1739,7 @@ function classify(name, kind, value, opts = {}) {
|
|
|
980
1739
|
return null;
|
|
981
1740
|
}
|
|
982
1741
|
const v = dim(value);
|
|
983
|
-
const has = (...scope) => scope.some((
|
|
1742
|
+
const has = (...scope) => scope.some((x2) => scopes.includes(x2));
|
|
984
1743
|
const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
|
|
985
1744
|
if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
|
|
986
1745
|
if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
|
|
@@ -990,7 +1749,7 @@ function classify(name, kind, value, opts = {}) {
|
|
|
990
1749
|
if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
|
|
991
1750
|
if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
|
|
992
1751
|
if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
|
|
993
|
-
if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((
|
|
1752
|
+
if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
|
|
994
1753
|
return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
|
|
995
1754
|
}
|
|
996
1755
|
function rawLeaves(node, prefix, out, studio, inherited) {
|
|
@@ -1051,8 +1810,8 @@ function flattenTokens(doc) {
|
|
|
1051
1810
|
if (!v) continue;
|
|
1052
1811
|
const d = r.dark !== void 0 ? colorValue(r.dark) : null;
|
|
1053
1812
|
out.push({ path: r.path, type: "color", value: v, ...d ? { dark: d } : {}, ...r.description ? { description: r.description } : {} });
|
|
1054
|
-
} else if (typeof r.value === "string" || typeof r.value === "number") {
|
|
1055
|
-
const type = t === "fontfamily" || t === "fontfamilies" ? "fontFamily" : t === "fontweight" || t === "fontweights" ? "fontWeight" : TYPE_KIND[t] === "number" ? "dimension" : t === "number" ? "number" : /^font\.family\./.test(r.path) ? "fontFamily" : typeof r.value === "number" ? "number" : /px$|rem$/.test(String(r.value)) ? "dimension" : "string";
|
|
1813
|
+
} else if (typeof r.value === "string" || typeof r.value === "number" || r.value && typeof r.value === "object" && "unit" in r.value) {
|
|
1814
|
+
const type = typeof r.value === "object" ? "dimension" : t === "fontfamily" || t === "fontfamilies" ? "fontFamily" : t === "fontweight" || t === "fontweights" ? "fontWeight" : TYPE_KIND[t] === "number" ? "dimension" : t === "number" ? "number" : /^font\.family\./.test(r.path) ? "fontFamily" : typeof r.value === "number" ? "number" : /px$|rem$/.test(String(r.value)) ? "dimension" : "string";
|
|
1056
1815
|
out.push({ path: r.path, type, value: type === "dimension" ? dim(r.value) : String(r.value), ...r.description ? { description: r.description } : {} });
|
|
1057
1816
|
}
|
|
1058
1817
|
}
|
|
@@ -1111,8 +1870,8 @@ function buildDtcg(tokens) {
|
|
|
1111
1870
|
for (const t of tokens) {
|
|
1112
1871
|
const parts = t.path.split(".");
|
|
1113
1872
|
let node = tree;
|
|
1114
|
-
parts.forEach((p,
|
|
1115
|
-
if (
|
|
1873
|
+
parts.forEach((p, i2) => {
|
|
1874
|
+
if (i2 === parts.length - 1) {
|
|
1116
1875
|
node[p] = {
|
|
1117
1876
|
$type: t.type,
|
|
1118
1877
|
$value: t.value,
|
|
@@ -1209,7 +1968,7 @@ function fromFigmaVariables(meta, opts = {}) {
|
|
|
1209
1968
|
if (!c) continue;
|
|
1210
1969
|
let darkValue;
|
|
1211
1970
|
if (c.type === "color") {
|
|
1212
|
-
const hasDarkSomewhere = Object.values(cols).some((
|
|
1971
|
+
const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
|
|
1213
1972
|
if (hasDarkSomewhere) {
|
|
1214
1973
|
const d = colorValue(resolve4(v, "dark"));
|
|
1215
1974
|
if (d && d !== c.value) darkValue = d;
|
|
@@ -1243,7 +2002,7 @@ function fromFigmaStyles(styles, opts = {}) {
|
|
|
1243
2002
|
}
|
|
1244
2003
|
}
|
|
1245
2004
|
const fams = [...families.entries()].sort((a, b) => b[1] - a[1]);
|
|
1246
|
-
fams.forEach(([f],
|
|
2005
|
+
fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
|
|
1247
2006
|
const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
|
|
1248
2007
|
return finish("figma-styles", out, notes, opts);
|
|
1249
2008
|
}
|
|
@@ -1261,7 +2020,7 @@ function fromModeObject(obj, opts = {}) {
|
|
|
1261
2020
|
const leafSets = modes.map(([name, v]) => {
|
|
1262
2021
|
const l = [];
|
|
1263
2022
|
plainLeaves(v, [], l);
|
|
1264
|
-
return { name, leaves: l, keys: new Set(l.map((
|
|
2023
|
+
return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
|
|
1265
2024
|
});
|
|
1266
2025
|
const groups = [];
|
|
1267
2026
|
for (const m of leafSets) {
|
|
@@ -1310,7 +2069,7 @@ function fromModeObject(obj, opts = {}) {
|
|
|
1310
2069
|
if (!c) continue;
|
|
1311
2070
|
let d;
|
|
1312
2071
|
if (c.type === "color") {
|
|
1313
|
-
const darkLeaf = dark && dark !== def ? dark.leaves.find((
|
|
2072
|
+
const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
|
|
1314
2073
|
const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
|
|
1315
2074
|
if (dv && dv !== c.value) d = dv;
|
|
1316
2075
|
}
|
|
@@ -1320,12 +2079,125 @@ function fromModeObject(obj, opts = {}) {
|
|
|
1320
2079
|
for (const p of pending) out.push({ ...p.t, referenced: referenced.has(p.key) });
|
|
1321
2080
|
return finish("mode-object", out, notes, opts);
|
|
1322
2081
|
}
|
|
2082
|
+
var figmaExt = (node, key) => {
|
|
2083
|
+
const ext = node.$extensions;
|
|
2084
|
+
if (!ext) return void 0;
|
|
2085
|
+
if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
|
|
2086
|
+
const nested = ext["com.figma"];
|
|
2087
|
+
return nested?.[key];
|
|
2088
|
+
};
|
|
2089
|
+
function fileLeaves(node, prefix, out, inherited) {
|
|
2090
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
2091
|
+
const o = node;
|
|
2092
|
+
const groupType = o.$type ?? inherited;
|
|
2093
|
+
if ("$value" in o) {
|
|
2094
|
+
const alias = figmaExt(o, "aliasData");
|
|
2095
|
+
const scopes = figmaExt(o, "scopes");
|
|
2096
|
+
out.push({
|
|
2097
|
+
path: prefix,
|
|
2098
|
+
type: o.$type ?? groupType,
|
|
2099
|
+
value: o.$value,
|
|
2100
|
+
description: o.$description,
|
|
2101
|
+
scopes: Array.isArray(scopes) ? scopes : void 0,
|
|
2102
|
+
aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
|
|
2103
|
+
hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
|
|
2104
|
+
});
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
for (const [k, v] of Object.entries(o)) {
|
|
2108
|
+
if (k.startsWith("$")) continue;
|
|
2109
|
+
fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
var hasFigmaExt = (node, depth = 0) => {
|
|
2113
|
+
if (!node || typeof node !== "object" || depth > 12) return false;
|
|
2114
|
+
const ext = node.$extensions;
|
|
2115
|
+
if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
|
|
2116
|
+
return Object.values(node).some((v) => hasFigmaExt(v, depth + 1));
|
|
2117
|
+
};
|
|
2118
|
+
function modeOf(file) {
|
|
2119
|
+
const root = file.data ?? {};
|
|
2120
|
+
const parts = file.name.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
2121
|
+
const base = (parts.pop() ?? "tokens").replace(/(\.tokens)?\.json$/i, "");
|
|
2122
|
+
const named = figmaExt(root, "modeName");
|
|
2123
|
+
const split = base.match(/^(.+?)[\s._-]+(light|dark|night|day|default|mode[\s_-]*\d+)$/i);
|
|
2124
|
+
return {
|
|
2125
|
+
collection: parts.join("/") || (split ? split[1] : ""),
|
|
2126
|
+
mode: typeof named === "string" ? named : split ? split[2] : base
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function importTokenFiles(files, opts = {}) {
|
|
2130
|
+
const parsed = files.filter((f) => f.data && typeof f.data === "object");
|
|
2131
|
+
if (!parsed.length) throw new Error("No JSON token files found.");
|
|
2132
|
+
const figma = parsed.some((f) => hasFigmaExt(f.data));
|
|
2133
|
+
if (parsed.length === 1 && !figma) return importTokens(parsed[0].data, opts);
|
|
2134
|
+
const notes = [];
|
|
2135
|
+
const withMode = parsed.map((f) => ({ ...f, ...modeOf(f) }));
|
|
2136
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
2137
|
+
for (const f of withMode) byCollection.set(f.collection, [...byCollection.get(f.collection) ?? [], f]);
|
|
2138
|
+
const light = /* @__PURE__ */ new Map();
|
|
2139
|
+
const darkOverrides = /* @__PURE__ */ new Map();
|
|
2140
|
+
for (const [collection, list] of byCollection) {
|
|
2141
|
+
const dark = list.find((f) => DARK.test(f.mode));
|
|
2142
|
+
const def = list.find((f) => LIGHT.test(f.mode) && !DARK.test(f.mode)) ?? list.find((f) => !DARK.test(f.mode)) ?? list[0];
|
|
2143
|
+
if (list.length > 1) {
|
|
2144
|
+
const label = collection ? `\u201C${collection}\u201D` : "The files";
|
|
2145
|
+
notes.push(`${label}: modes ${list.map((f) => f.mode).join(", ")}; used \u201C${def.mode}\u201D${dark && dark !== def ? ` (and \u201C${dark.mode}\u201D as dark)` : ""}.`);
|
|
2146
|
+
}
|
|
2147
|
+
const read = (f, into) => {
|
|
2148
|
+
const leaves2 = [];
|
|
2149
|
+
fileLeaves(f.data, "", leaves2);
|
|
2150
|
+
for (const l of leaves2) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
|
|
2151
|
+
};
|
|
2152
|
+
read(def, light);
|
|
2153
|
+
if (dark && dark !== def) read(dark, darkOverrides);
|
|
2154
|
+
}
|
|
2155
|
+
const lightRaws = resolveRefs([...light.values()]);
|
|
2156
|
+
const darkRaws = resolveRefs([...light.values()].map((r) => darkOverrides.get(r.path) ?? r));
|
|
2157
|
+
const darkByPath = new Map(darkRaws.map((r) => [r.path, r.value]));
|
|
2158
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2159
|
+
for (const r of [...light.values(), ...darkOverrides.values()]) {
|
|
2160
|
+
if (r.leaf.aliasTarget) referenced.add(r.leaf.aliasTarget);
|
|
2161
|
+
const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
|
|
2162
|
+
if (m) referenced.add(m[1]);
|
|
2163
|
+
}
|
|
2164
|
+
const out = [];
|
|
2165
|
+
let hidden = 0;
|
|
2166
|
+
for (const r of lightRaws) {
|
|
2167
|
+
const leaf = light.get(r.path).leaf;
|
|
2168
|
+
if (leaf.hidden) {
|
|
2169
|
+
hidden++;
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const t = (r.type ?? "").toLowerCase();
|
|
2173
|
+
const kind = t === "color" ? "color" : t === "boolean" ? "boolean" : t === "string" || t === "fontfamily" || t === "fontweight" ? "string" : typeof r.value === "string" && !/^-?[\d.]+(px|rem)?$/.test(r.value) && !colorValue(r.value) ? "string" : colorValue(r.value) && t !== "number" && t !== "dimension" ? "color" : "number";
|
|
2174
|
+
let tok;
|
|
2175
|
+
if (figma) {
|
|
2176
|
+
const value = kind === "number" && r.value && typeof r.value === "object" ? parseFloat(dim(r.value)) : r.value;
|
|
2177
|
+
tok = classify(r.path.replace(/\./g, "/"), kind, value, { scopes: leaf.scopes?.filter((x2) => x2 !== "ALL_SCOPES") });
|
|
2178
|
+
} else {
|
|
2179
|
+
const flat = flattenTokens({ x: { $type: r.type, $value: r.value } })[0];
|
|
2180
|
+
tok = flat ? { path: flat.type === "color" && !r.path.startsWith("color.") ? `color.${r.path}` : r.path, type: flat.type, value: flat.value } : null;
|
|
2181
|
+
}
|
|
2182
|
+
if (!tok) continue;
|
|
2183
|
+
let darkValue;
|
|
2184
|
+
if (tok.type === "color" && darkOverrides.size) {
|
|
2185
|
+
const d = colorValue(darkByPath.get(r.path));
|
|
2186
|
+
if (d && d !== tok.value) darkValue = d;
|
|
2187
|
+
}
|
|
2188
|
+
out.push({ ...tok, ...darkValue ? { dark: darkValue } : {}, ...r.description ? { description: r.description } : {}, referenced: referenced.has(r.path) });
|
|
2189
|
+
}
|
|
2190
|
+
if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
|
|
2191
|
+
return finish(figma ? "figma-export" : "dtcg", out, notes, opts);
|
|
2192
|
+
}
|
|
1323
2193
|
function importTokens(input, opts = {}) {
|
|
1324
2194
|
if (!input || typeof input !== "object") throw new Error("Expected a JSON object.");
|
|
1325
2195
|
const o = input;
|
|
2196
|
+
if (Array.isArray(o.$files)) return importTokenFiles(o.$files, opts);
|
|
1326
2197
|
const meta = o.meta ?? o;
|
|
1327
2198
|
if (meta.variables && meta.variableCollections) return fromFigmaVariables(meta, opts);
|
|
1328
2199
|
if (Array.isArray(o.styles) && o.styles.every((s) => s && "styleType" in s)) return fromFigmaStyles(o.styles, opts);
|
|
2200
|
+
if (hasDollarValue(o) && hasFigmaExt(o)) return importTokenFiles([{ name: "tokens.json", data: o }], opts);
|
|
1329
2201
|
if (hasDollarValue(o) || hasStudioValue(o)) {
|
|
1330
2202
|
const studio = !hasDollarValue(o);
|
|
1331
2203
|
const flat = flattenTokens(o);
|
|
@@ -1368,6 +2240,7 @@ var RULES_START = "<!-- brandrail:start -->";
|
|
|
1368
2240
|
var RULES_END = "<!-- brandrail:end -->";
|
|
1369
2241
|
function generateInstructions(config, opts = {}) {
|
|
1370
2242
|
const r = resolveConfig(config);
|
|
2243
|
+
const fw = opts.framework ?? config.framework ?? "react";
|
|
1371
2244
|
const lines = [];
|
|
1372
2245
|
lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
|
|
1373
2246
|
lines.push("");
|
|
@@ -1375,7 +2248,7 @@ function generateInstructions(config, opts = {}) {
|
|
|
1375
2248
|
`All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
|
|
1376
2249
|
);
|
|
1377
2250
|
lines.push("");
|
|
1378
|
-
const usesKit = r.components.some((c) => c.importPath === KIT_PACKAGE);
|
|
2251
|
+
const usesKit = fw === "react" && r.components.some((c) => c.importPath === KIT_PACKAGE);
|
|
1379
2252
|
if (opts.tokensCss || usesKit) {
|
|
1380
2253
|
lines.push("## Setup");
|
|
1381
2254
|
if (opts.tokensCss) {
|
|
@@ -1387,17 +2260,29 @@ function generateInstructions(config, opts = {}) {
|
|
|
1387
2260
|
lines.push("");
|
|
1388
2261
|
}
|
|
1389
2262
|
if (r.components.length) {
|
|
2263
|
+
const usable = r.components.map((c) => ({ c, u: resolveUsage(c, fw) }));
|
|
2264
|
+
const here = usable.filter((x2) => x2.u);
|
|
2265
|
+
const missing = usable.filter((x2) => !x2.u).map((x2) => x2.c.name);
|
|
1390
2266
|
lines.push("## Components");
|
|
1391
|
-
lines.push("
|
|
1392
|
-
|
|
2267
|
+
if (fw !== "react") lines.push(`This is a${fw === "angular" ? "n" : ""} ${FRAMEWORK_LABEL[fw]} project: use the ${FRAMEWORK_LABEL[fw]} form of each component shown here.`);
|
|
2268
|
+
if (here.length) lines.push("Always use these components. Never re-create them with raw HTML elements:");
|
|
2269
|
+
for (const { c, u } of here) {
|
|
1393
2270
|
const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
|
|
1394
|
-
const props =
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
2271
|
+
const props = u.props?.length ? ` \u2014 props: ${u.props.map((p) => p.required ? `${p.name}*` : p.name).join(", ")}` : "";
|
|
2272
|
+
const how = fw === "react" ? `\`${c.name}\` from \`${c.importPath}\`` : `\`${markup(u)}\`${u.importLine ? ` (\`${u.importLine}\`)` : ""}`;
|
|
2273
|
+
lines.push(`- ${how}${fw === "react" ? "" : ` \u2014 ${c.name}`}${tags.length ? ` (instead of ${tags.join(", ")})` : ""}${props}`);
|
|
2274
|
+
}
|
|
2275
|
+
const setups = [...new Set(here.map((x2) => x2.u.setup).filter(Boolean))];
|
|
2276
|
+
if (setups.length && setups.length <= 3) for (const s of setups) lines.push(`- Setup: ${s}`);
|
|
2277
|
+
else if (setups.length) lines.push("- Each component's setup is in its docs (MCP `get_component_docs`).");
|
|
2278
|
+
if (here.some((x2) => x2.u.props?.length)) {
|
|
1398
2279
|
lines.push("");
|
|
1399
2280
|
lines.push("Pass only the props listed for each component (* = required). Any other prop is a type error and fails the check: if you need something a component doesn't take, look for a related component (often one with a longer name, like AvatarLabel next to Avatar) or compose it with design tokens.");
|
|
1400
2281
|
}
|
|
2282
|
+
if (missing.length) {
|
|
2283
|
+
lines.push("");
|
|
2284
|
+
lines.push(`No ${FRAMEWORK_LABEL[fw]} version yet: ${missing.join(", ")}. Build these with semantic HTML and the design tokens below; don't install another UI library.`);
|
|
2285
|
+
}
|
|
1401
2286
|
lines.push("");
|
|
1402
2287
|
}
|
|
1403
2288
|
if (r.colors.length) {
|
|
@@ -1416,7 +2301,7 @@ function generateInstructions(config, opts = {}) {
|
|
|
1416
2301
|
const radiusVars = named(/^radius\./);
|
|
1417
2302
|
const sizeVars = named(/^font\.size\./);
|
|
1418
2303
|
lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.${spaceVars.length ? ` Prefer the variables: ${spaceVars.join(", ")}.` : ""}`);
|
|
1419
|
-
lines.push(`- Corner radius must be one of: ${r.radii.filter((
|
|
2304
|
+
lines.push(`- Corner radius must be one of: ${r.radii.filter((x2) => x2 > 0 && x2 < 999).map((x2) => `${x2}px`).join(", ")} (or fully rounded).${radiusVars.length ? ` Variables: ${radiusVars.join(", ")}.` : ""}`);
|
|
1420
2305
|
if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
|
|
1421
2306
|
if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
|
|
1422
2307
|
lines.push("");
|
|
@@ -1459,7 +2344,7 @@ function mcpClientConfigs(url) {
|
|
|
1459
2344
|
file: ".mcp.json",
|
|
1460
2345
|
projectFile: true,
|
|
1461
2346
|
content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
|
|
1462
|
-
command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
|
|
2347
|
+
command: `claude mcp add --transport http brandrail "${url}" --header "Authorization: Bearer $BRANDRAIL_KEY"`
|
|
1463
2348
|
},
|
|
1464
2349
|
{
|
|
1465
2350
|
id: "codex",
|
|
@@ -1470,7 +2355,7 @@ function mcpClientConfigs(url) {
|
|
|
1470
2355
|
url = "${url}"
|
|
1471
2356
|
bearer_token_env_var = "BRANDRAIL_KEY"
|
|
1472
2357
|
`,
|
|
1473
|
-
command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
|
|
2358
|
+
command: `codex mcp add brandrail --url "${url}" --bearer-token-env-var BRANDRAIL_KEY`
|
|
1474
2359
|
},
|
|
1475
2360
|
{
|
|
1476
2361
|
id: "cursor",
|
|
@@ -1589,9 +2474,9 @@ function collectRenderedNodes(maxNodes = 3e3) {
|
|
|
1589
2474
|
}
|
|
1590
2475
|
var near = (scale, v, tol = 0.5) => scale.some((s) => Math.abs(s - v) <= tol);
|
|
1591
2476
|
var neighbours2 = (scale, v) => {
|
|
1592
|
-
const lo = [...scale].reverse().find((
|
|
1593
|
-
const hi = scale.find((
|
|
1594
|
-
return [...new Set([lo, hi].filter((
|
|
2477
|
+
const lo = [...scale].reverse().find((x2) => x2 <= v);
|
|
2478
|
+
const hi = scale.find((x2) => x2 >= v);
|
|
2479
|
+
return [...new Set([lo, hi].filter((x2) => x2 !== void 0))].map((x2) => `${x2}px`).join(" or ");
|
|
1595
2480
|
};
|
|
1596
2481
|
function closestToken(config, rgb) {
|
|
1597
2482
|
const lab = rgbToLab(rgb);
|
|
@@ -1657,17 +2542,17 @@ function checkRendered(pages, configInput, opts = {}) {
|
|
|
1657
2542
|
const bg = n.effectiveBackground ? parseColor(n.effectiveBackground) : null;
|
|
1658
2543
|
if (fg && bg && fg.a > 0.05) {
|
|
1659
2544
|
const blended = fg.a < 1 ? { r: fg.r * fg.a + bg.r * (1 - fg.a), g: fg.g * fg.a + bg.g * (1 - fg.a), b: fg.b * fg.a + bg.b * (1 - fg.a), a: 1 } : fg;
|
|
1660
|
-
const
|
|
2545
|
+
const ratio2 = contrastRatio(blended, bg);
|
|
1661
2546
|
const large = n.fontSize >= 24 || n.fontSize >= 18.66 && n.fontWeight >= 700;
|
|
1662
2547
|
const min = large ? 3 : 4.5;
|
|
1663
|
-
if (
|
|
2548
|
+
if (ratio2 < min) {
|
|
1664
2549
|
const pair = `${colorLabel(fg)} on ${toHex(bg)}`;
|
|
1665
2550
|
add(
|
|
1666
2551
|
`contrast:${pair}:${large}`,
|
|
1667
2552
|
{
|
|
1668
2553
|
ruleId: "text-contrast",
|
|
1669
2554
|
severity: "error",
|
|
1670
|
-
message: (c) => `${pair} is ${
|
|
2555
|
+
message: (c) => `${pair} is ${ratio2.toFixed(2)}:1, below ${min}:1${large ? " for large text" : ""} (${c} element${c === 1 ? "" : "s"})`,
|
|
1671
2556
|
suggestion: "Use the Brand Kit's text colours on its background colours, e.g. fg.default on canvas.default",
|
|
1672
2557
|
sample: n.text
|
|
1673
2558
|
},
|
|
@@ -1847,15 +2732,15 @@ var px2 = (v) => {
|
|
|
1847
2732
|
const m = v.trim().match(/^(-?\d*\.?\d+)(px|rem)?$/);
|
|
1848
2733
|
return m ? Number.parseFloat(m[1]) * (m[2] === "rem" ? 16 : 1) : null;
|
|
1849
2734
|
};
|
|
1850
|
-
var nearest2 = (values, v) => values.reduce((best,
|
|
2735
|
+
var nearest2 = (values, v) => values.reduce((best, x2) => Math.abs(x2 - v) < Math.abs(best - v) ? x2 : best, values[0]);
|
|
1851
2736
|
function luminance(hex) {
|
|
1852
2737
|
const n = hex.replace("#", "").slice(0, 6);
|
|
1853
|
-
const [r, g, b] = [0, 2, 4].map((
|
|
2738
|
+
const [r, g, b] = [0, 2, 4].map((i2) => parseInt(n.slice(i2, i2 + 2), 16) / 255).map((c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
1854
2739
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1855
2740
|
}
|
|
1856
2741
|
var contrast = (a, b) => {
|
|
1857
|
-
const [
|
|
1858
|
-
return (
|
|
2742
|
+
const [x2, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
2743
|
+
return (x2 + 0.05) / (y + 0.05);
|
|
1859
2744
|
};
|
|
1860
2745
|
function onColor(bg, candidates) {
|
|
1861
2746
|
return candidates.filter((c) => HEX.test(c)).sort((a, b) => contrast(b, bg) - contrast(a, bg))[0] ?? "#ffffff";
|
|
@@ -1895,7 +2780,7 @@ function scaleVars(s) {
|
|
|
1895
2780
|
function colorVars(colors, dark, font) {
|
|
1896
2781
|
const r = pickColorRoles(colors, dark);
|
|
1897
2782
|
const find = (re) => {
|
|
1898
|
-
const c = colors.find((
|
|
2783
|
+
const c = colors.find((x2) => re.test(x2.path));
|
|
1899
2784
|
return c ? dark ? c.dark ?? c.value : c.value : void 0;
|
|
1900
2785
|
};
|
|
1901
2786
|
const success = find(/success/);
|
|
@@ -1945,8 +2830,126 @@ function kitBridgeCss(tokens, opts = {}) {
|
|
|
1945
2830
|
return [block(sel.scale, scales), block(sel.light, colorVars(colors, false, sc.font)), block(sel.dark, colorVars(colors, true, sc.font))].filter(Boolean).join("\n");
|
|
1946
2831
|
}
|
|
1947
2832
|
|
|
2833
|
+
// ../engine/src/brand-kit-audit.ts
|
|
2834
|
+
var ratio = (a, b) => {
|
|
2835
|
+
const x2 = parseColor(a);
|
|
2836
|
+
const y = parseColor(b);
|
|
2837
|
+
return x2 && y ? Math.round(contrastRatio(x2, y) * 100) / 100 : 21;
|
|
2838
|
+
};
|
|
2839
|
+
var r1 = (n) => `${Math.round(n * 10) / 10}:1`;
|
|
2840
|
+
var STATUS = [
|
|
2841
|
+
{ key: "success", re: /success|positive/, label: "success", usedBy: "success notifications, checks and toggles" },
|
|
2842
|
+
{ key: "warning", re: /warning|attention|caution/, label: "warning", usedBy: "warning notifications and states" },
|
|
2843
|
+
{ key: "danger", re: /danger|error|critical|negative/, label: "danger / error", usedBy: "errors, destructive buttons and invalid fields" }
|
|
2844
|
+
];
|
|
2845
|
+
function auditBrandKit(tokens) {
|
|
2846
|
+
const colors = flattenTokens(tokens).filter((t) => t.type === "color").map((t) => ({ path: t.path, value: t.value, dark: t.dark }));
|
|
2847
|
+
const issues = [];
|
|
2848
|
+
if (!colors.length) return issues;
|
|
2849
|
+
const modes = colors.some((c) => c.dark) ? ["light", "dark"] : ["light"];
|
|
2850
|
+
const light = pickColorRoles(colors, false);
|
|
2851
|
+
for (const mode of modes) {
|
|
2852
|
+
const dark = mode === "dark";
|
|
2853
|
+
const r = pickColorRoles(colors, dark);
|
|
2854
|
+
const where = modes.length > 1 ? ` (${mode} mode)` : "";
|
|
2855
|
+
const paths = (...xs) => xs.filter((x2) => !!x2);
|
|
2856
|
+
const text = ratio(r.fg.value, r.canvas.value);
|
|
2857
|
+
if (text < 4.5) {
|
|
2858
|
+
issues.push({
|
|
2859
|
+
severity: "error",
|
|
2860
|
+
code: "text-contrast",
|
|
2861
|
+
mode,
|
|
2862
|
+
paths: paths(r.fg.path, r.canvas.path),
|
|
2863
|
+
message: `Body text on the page background is ${r1(text)}${where}. It needs at least 4.5:1 to be readable.`
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
const muted = ratio(r.muted.value, r.canvas.value);
|
|
2867
|
+
if (muted < 4.5) {
|
|
2868
|
+
issues.push({
|
|
2869
|
+
severity: muted < 3 ? "error" : "warning",
|
|
2870
|
+
code: "muted-contrast",
|
|
2871
|
+
mode,
|
|
2872
|
+
paths: paths(r.muted.path, r.canvas.path),
|
|
2873
|
+
message: `Secondary text on the page background is ${r1(muted)}${where}. Captions and help text need 4.5:1; darken ${r.muted.path ?? "the muted colour"}.`
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
for (const [role, what] of [
|
|
2877
|
+
["primary", "primary buttons"],
|
|
2878
|
+
["danger", "destructive buttons"]
|
|
2879
|
+
]) {
|
|
2880
|
+
if (!r[role].path) continue;
|
|
2881
|
+
const fill = r[role].value;
|
|
2882
|
+
const best = Math.max(ratio(light.canvas.value, fill), ratio(light.fg.value, fill));
|
|
2883
|
+
if (best < 4.5) {
|
|
2884
|
+
issues.push({
|
|
2885
|
+
severity: best < 3 ? "error" : "warning",
|
|
2886
|
+
code: "on-fill-contrast",
|
|
2887
|
+
mode,
|
|
2888
|
+
paths: [r[role].path],
|
|
2889
|
+
message: `Labels on ${what} are at most ${r1(best)}${where}: neither your background nor your text colour reads well on ${fill}. Use a darker or lighter ${r[role].path} (4.5:1 for text).`
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
for (const [role, what] of [
|
|
2894
|
+
["primary", "ghost buttons, selected items and focus rings"],
|
|
2895
|
+
["accent", "links and highlights"]
|
|
2896
|
+
]) {
|
|
2897
|
+
if (!r[role].path) continue;
|
|
2898
|
+
const vs = ratio(r[role].value, r.canvas.value);
|
|
2899
|
+
if (vs < 4.5) {
|
|
2900
|
+
issues.push({
|
|
2901
|
+
severity: vs < 3 ? "error" : "warning",
|
|
2902
|
+
code: "status-contrast",
|
|
2903
|
+
mode,
|
|
2904
|
+
paths: [r[role].path],
|
|
2905
|
+
message: `${r[role].path} (${r[role].value}) is ${r1(vs)} on the page background${where}. Components use it as text and outlines for ${what}, which need 4.5:1 for text and 3:1 for icons and borders. Darken it, or add a darker variant for text.`
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
for (const s of STATUS) {
|
|
2910
|
+
const c = colors.find((x2) => s.re.test(x2.path));
|
|
2911
|
+
if (!c) continue;
|
|
2912
|
+
const value = dark ? c.dark ?? c.value : c.value;
|
|
2913
|
+
const vs = ratio(value, r.canvas.value);
|
|
2914
|
+
if (vs < 3) {
|
|
2915
|
+
issues.push({
|
|
2916
|
+
severity: "warning",
|
|
2917
|
+
code: "status-contrast",
|
|
2918
|
+
mode,
|
|
2919
|
+
paths: [c.path],
|
|
2920
|
+
message: `${c.path} (${value}) is ${r1(vs)} on the page background${where}: too light for text or icons (3:1 for icons, 4.5:1 for text). It's fine as a fill behind dark text; add a darker variant for text.`
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (!light.primary.path) {
|
|
2926
|
+
issues.push({
|
|
2927
|
+
severity: "warning",
|
|
2928
|
+
code: "missing-color",
|
|
2929
|
+
paths: [],
|
|
2930
|
+
message: "No primary colour (e.g. color.action.primary). Buttons and links fall back to the kit's default green."
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
for (const s of STATUS) {
|
|
2934
|
+
if (colors.some((x2) => s.re.test(x2.path))) continue;
|
|
2935
|
+
issues.push({
|
|
2936
|
+
severity: "warning",
|
|
2937
|
+
code: "missing-color",
|
|
2938
|
+
paths: [],
|
|
2939
|
+
message: `No ${s.label} colour (e.g. color.status.${s.key}). It's used for ${s.usedBy}, which fall back to the kit's own colour instead of your brand.`
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2943
|
+
return issues.filter((i2) => {
|
|
2944
|
+
const key = `${i2.code}:${i2.paths.join(",")}:${i2.message.replace(/ \((light|dark) mode\)/, "").replace(/[\d.]+:1/g, "")}`;
|
|
2945
|
+
if (seen.has(key)) return false;
|
|
2946
|
+
seen.add(key);
|
|
2947
|
+
return true;
|
|
2948
|
+
}).sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
1948
2951
|
// src/api.ts
|
|
1949
|
-
var DEFAULT_API = "https://
|
|
2952
|
+
var DEFAULT_API = "https://www.brandrail.com.au";
|
|
1950
2953
|
function apiBase() {
|
|
1951
2954
|
return (process.env.BRANDRAIL_API_URL ?? DEFAULT_API).replace(/\/$/, "");
|
|
1952
2955
|
}
|
|
@@ -2057,7 +3060,7 @@ function parseChangedLines(diff) {
|
|
|
2057
3060
|
const start = Number(h[1]);
|
|
2058
3061
|
const count = h[2] === void 0 ? 1 : Number(h[2]);
|
|
2059
3062
|
const set = out.get(file);
|
|
2060
|
-
for (let
|
|
3063
|
+
for (let i2 = 0; i2 < count; i2++) set.add(start + i2);
|
|
2061
3064
|
}
|
|
2062
3065
|
}
|
|
2063
3066
|
return out;
|
|
@@ -2070,13 +3073,13 @@ function changedLines(cwd, base) {
|
|
|
2070
3073
|
|
|
2071
3074
|
// src/scan.ts
|
|
2072
3075
|
import { existsSync as existsSync2 } from "node:fs";
|
|
2073
|
-
import { createRequire } from "node:module";
|
|
3076
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2074
3077
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
2075
3078
|
function loadPlaywright(cwd) {
|
|
2076
3079
|
for (const base of [join2(cwd, "package.json"), import.meta.url]) {
|
|
2077
3080
|
for (const name of ["playwright", "playwright-core"]) {
|
|
2078
3081
|
try {
|
|
2079
|
-
return
|
|
3082
|
+
return createRequire2(base)(name);
|
|
2080
3083
|
} catch {
|
|
2081
3084
|
}
|
|
2082
3085
|
}
|
|
@@ -2132,7 +3135,7 @@ Usage
|
|
|
2132
3135
|
brandrail check [paths...] Scan files and print violations
|
|
2133
3136
|
brandrail init Create brandrail.config.json and AI rule files
|
|
2134
3137
|
brandrail scan <url> Check a running page (localhost, staging, behind a login) in a real browser
|
|
2135
|
-
brandrail import <
|
|
3138
|
+
brandrail import <files\u2026> Load tokens into the config: DTCG, Tokens Studio, Figma variables JSON, or Figma's Export modes ZIP
|
|
2136
3139
|
brandrail rules List the rules
|
|
2137
3140
|
|
|
2138
3141
|
Options for check
|
|
@@ -2148,6 +3151,7 @@ Options for init
|
|
|
2148
3151
|
--no-rules Don't write AGENTS.md (Codex), CLAUDE.md, Cursor, Copilot, Windsurf, Gemini rule files
|
|
2149
3152
|
--mcp Also add the Brandrail MCP server to .mcp.json, .cursor/mcp.json, .vscode/mcp.json
|
|
2150
3153
|
and print the Codex / Windsurf config
|
|
3154
|
+
--framework <name> react | angular | vue | web-components | html (default: detected from package.json)
|
|
2151
3155
|
--tokens-css <file> Where to write the CSS variables (default src/brandrail.tokens.css); --no-tokens-css to skip
|
|
2152
3156
|
--force Overwrite an existing brandrail.config.json
|
|
2153
3157
|
|
|
@@ -2171,16 +3175,16 @@ function parseArgs(argv) {
|
|
|
2171
3175
|
const [cmd = "help", ...rest] = argv;
|
|
2172
3176
|
const positional = [];
|
|
2173
3177
|
const flags = {};
|
|
2174
|
-
for (let
|
|
2175
|
-
const a = rest[
|
|
3178
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
3179
|
+
const a = rest[i2];
|
|
2176
3180
|
if (a.startsWith("--no-")) flags[a.slice(5)] = false;
|
|
2177
3181
|
else if (a.startsWith("--")) {
|
|
2178
3182
|
const [k, v] = a.slice(2).split("=", 2);
|
|
2179
|
-
const next = rest[
|
|
3183
|
+
const next = rest[i2 + 1];
|
|
2180
3184
|
if (v !== void 0) flags[k] = v;
|
|
2181
|
-
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css"].includes(k)) {
|
|
3185
|
+
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework"].includes(k)) {
|
|
2182
3186
|
flags[k] = next;
|
|
2183
|
-
|
|
3187
|
+
i2++;
|
|
2184
3188
|
} else flags[k] = true;
|
|
2185
3189
|
} else positional.push(a);
|
|
2186
3190
|
}
|
|
@@ -2280,7 +3284,7 @@ function writeRuleFiles(root, config, org, tokensCss) {
|
|
|
2280
3284
|
if (file.endsWith(".mdc")) {
|
|
2281
3285
|
write(file, () => `---
|
|
2282
3286
|
description: Design system rules from Brandrail
|
|
2283
|
-
globs: ["**/*.{tsx,jsx,vue,svelte,html,css,scss}"]
|
|
3287
|
+
globs: ["**/*.{tsx,jsx,ts,vue,svelte,html,css,scss,php,twig,hbs,mustache}"]
|
|
2284
3288
|
alwaysApply: true
|
|
2285
3289
|
---
|
|
2286
3290
|
|
|
@@ -2300,8 +3304,8 @@ ${md}
|
|
|
2300
3304
|
}
|
|
2301
3305
|
return written;
|
|
2302
3306
|
}
|
|
2303
|
-
function writeMcpConfigs(root) {
|
|
2304
|
-
const url = `${apiBase()}/api/mcp`;
|
|
3307
|
+
function writeMcpConfigs(root, framework) {
|
|
3308
|
+
const url = `${apiBase()}/api/mcp${framework === "react" ? "" : `?framework=${framework}`}`;
|
|
2305
3309
|
for (const c of mcpClientConfigs(url)) {
|
|
2306
3310
|
if (!c.projectFile) continue;
|
|
2307
3311
|
const abs = join3(root, c.file);
|
|
@@ -2315,7 +3319,7 @@ function writeMcpConfigs(root) {
|
|
|
2315
3319
|
for (const [k, v] of Object.entries(ours)) {
|
|
2316
3320
|
if (Array.isArray(v)) {
|
|
2317
3321
|
const prev = Array.isArray(existing[k]) ? existing[k] : [];
|
|
2318
|
-
merged[k] = [...prev.filter((
|
|
3322
|
+
merged[k] = [...prev.filter((x2) => !v.some((y) => y.id === x2.id)), ...v];
|
|
2319
3323
|
} else merged[k] = { ...existing[k] ?? {}, ...v };
|
|
2320
3324
|
}
|
|
2321
3325
|
} catch {
|
|
@@ -2327,7 +3331,7 @@ function writeMcpConfigs(root) {
|
|
|
2327
3331
|
`);
|
|
2328
3332
|
log(`\u2714 added brandrail to ${c.file} (${c.name})`);
|
|
2329
3333
|
}
|
|
2330
|
-
for (const c of mcpClientConfigs(url).filter((
|
|
3334
|
+
for (const c of mcpClientConfigs(url).filter((x2) => !x2.projectFile)) {
|
|
2331
3335
|
log(`
|
|
2332
3336
|
${c.name}: ${c.command ? `run
|
|
2333
3337
|
${c.command}
|
|
@@ -2336,6 +3340,21 @@ ${c.content.replace(/^/gm, " ")}`);
|
|
|
2336
3340
|
}
|
|
2337
3341
|
log("\nSet BRANDRAIL_KEY in your shell (an API key from Dashboard \u2192 Connect) before starting the AI tool.");
|
|
2338
3342
|
}
|
|
3343
|
+
function projectFramework(cwd, config, flags) {
|
|
3344
|
+
const asked = flags.framework;
|
|
3345
|
+
if (typeof asked === "string") {
|
|
3346
|
+
if (!isFramework(asked)) throw new Error(`Unknown --framework "${asked}". Use one of: ${FRAMEWORKS.join(", ")}.`);
|
|
3347
|
+
return asked;
|
|
3348
|
+
}
|
|
3349
|
+
if (config.framework && isFramework(config.framework)) return config.framework;
|
|
3350
|
+
const pkg = join3(cwd, "package.json");
|
|
3351
|
+
if (!existsSync3(pkg)) return detectFramework(null);
|
|
3352
|
+
try {
|
|
3353
|
+
return detectFramework(JSON.parse(readFileSync2(pkg, "utf8")));
|
|
3354
|
+
} catch {
|
|
3355
|
+
return "react";
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
2339
3358
|
async function cmdInit(cwd, flags) {
|
|
2340
3359
|
const target = join3(cwd, CONFIG_FILE);
|
|
2341
3360
|
let config = starterConfig;
|
|
@@ -2360,13 +3379,20 @@ async function cmdInit(cwd, flags) {
|
|
|
2360
3379
|
`);
|
|
2361
3380
|
log(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
|
|
2362
3381
|
}
|
|
3382
|
+
const framework = projectFramework(cwd, config, flags);
|
|
3383
|
+
if (config.framework !== framework) {
|
|
3384
|
+
config = { ...config, framework };
|
|
3385
|
+
writeFileSync(target, `${JSON.stringify(config, null, 2)}
|
|
3386
|
+
`);
|
|
3387
|
+
}
|
|
3388
|
+
log(`\u2714 framework: ${FRAMEWORK_LABEL[framework]}${flags.framework ? "" : " (detected; change with --framework)"}`);
|
|
2363
3389
|
const css = flags["tokens-css"] === false ? void 0 : tokensCssPath(cwd, config, flags);
|
|
2364
3390
|
if (css) {
|
|
2365
3391
|
const kit = writeTokensCss(cwd, css, config, org);
|
|
2366
3392
|
log(`\u2714 wrote ${css} (the Brand Kit as CSS variables${kit ? ", and @brandrail/kit themed from it" : ""} \u2014 import it once at your app entry)`);
|
|
2367
3393
|
}
|
|
2368
3394
|
if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org, css)) log(`\u2714 updated ${f}`);
|
|
2369
|
-
if (flags.mcp) writeMcpConfigs(cwd);
|
|
3395
|
+
if (flags.mcp) writeMcpConfigs(cwd, framework);
|
|
2370
3396
|
log("\nNext: npx brandrail check");
|
|
2371
3397
|
return 0;
|
|
2372
3398
|
}
|
|
@@ -2374,7 +3400,7 @@ async function cmdScan(cwd, positional, flags) {
|
|
|
2374
3400
|
const url = positional[0];
|
|
2375
3401
|
if (!url) throw new Error("Usage: brandrail scan <url> [--pages 3] [--storage-state auth.json] [--upload]");
|
|
2376
3402
|
const { config, source } = await loadConfig(cwd, flags);
|
|
2377
|
-
const num = (k, d,
|
|
3403
|
+
const num = (k, d, max2) => typeof flags[k] === "string" ? Math.min(Math.max(Number(flags[k]) || d, 0), max2) : d;
|
|
2378
3404
|
log(`Scanning ${url} \u2026`);
|
|
2379
3405
|
const { report, pages } = await scanPages(
|
|
2380
3406
|
cwd,
|
|
@@ -2423,11 +3449,31 @@ Brand score ${report.score} is below ${threshold}.`);
|
|
|
2423
3449
|
}
|
|
2424
3450
|
return 0;
|
|
2425
3451
|
}
|
|
3452
|
+
function readTokenInputs(cwd, inputs) {
|
|
3453
|
+
const files = [];
|
|
3454
|
+
const add = (name, text) => files.push({ name, data: JSON.parse(text) });
|
|
3455
|
+
const walk = (abs, rel) => {
|
|
3456
|
+
if (statSync2(abs).isDirectory()) {
|
|
3457
|
+
for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(join3(abs, e), rel ? `${rel}/${e}` : e);
|
|
3458
|
+
} else if (/\.zip$/i.test(abs)) {
|
|
3459
|
+
const entries = unzipSync(new Uint8Array(readFileSync2(abs)), { filter: (e) => /\.json$/i.test(e.name) && !/(^|\/)(__MACOSX|\.)/.test(e.name) });
|
|
3460
|
+
for (const [name, bytes] of Object.entries(entries)) add(name, strFromU8(bytes));
|
|
3461
|
+
} else if (/\.json$/i.test(abs)) {
|
|
3462
|
+
add(rel || basename(abs), readFileSync2(abs, "utf8"));
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
for (const input of inputs) {
|
|
3466
|
+
const abs = resolve3(cwd, input);
|
|
3467
|
+
walk(abs, statSync2(abs).isDirectory() ? "" : basename(abs));
|
|
3468
|
+
}
|
|
3469
|
+
return files;
|
|
3470
|
+
}
|
|
2426
3471
|
function cmdImport(cwd, positional, flags) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
const res =
|
|
3472
|
+
if (!positional.length) throw new Error("Usage: brandrail import <tokens.json | folder | export.zip> [more files\u2026] [--replace] [--primitives]");
|
|
3473
|
+
const files = readTokenInputs(cwd, positional);
|
|
3474
|
+
if (!files.length) throw new Error("No .json token files found.");
|
|
3475
|
+
const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
|
|
3476
|
+
const file = files.length === 1 ? positional[0] : `${files.length} files`;
|
|
2431
3477
|
const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join3(cwd, CONFIG_FILE);
|
|
2432
3478
|
const config = existsSync3(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
|
|
2433
3479
|
const next = flags.replace ? res.tokens : mergeTokens(config.tokens, res.tokens);
|
|
@@ -2436,6 +3482,7 @@ function cmdImport(cwd, positional, flags) {
|
|
|
2436
3482
|
`);
|
|
2437
3483
|
log(`\u2714 ${res.count} tokens from ${file} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
|
|
2438
3484
|
for (const n of res.notes) log(` \xB7 ${n}`);
|
|
3485
|
+
for (const i2 of auditBrandKit(next)) log(` ${i2.severity === "error" ? "\u2716" : "!"} ${i2.message}`);
|
|
2439
3486
|
log(`\u2714 ${relative2(cwd, path) || CONFIG_FILE}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
|
|
2440
3487
|
log("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
|
|
2441
3488
|
return 0;
|
|
@@ -2471,8 +3518,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2471
3518
|
}
|
|
2472
3519
|
main().then(
|
|
2473
3520
|
(code) => process.exit(code),
|
|
2474
|
-
(
|
|
2475
|
-
log(`brandrail: ${
|
|
3521
|
+
(err2) => {
|
|
3522
|
+
log(`brandrail: ${err2.message}`);
|
|
2476
3523
|
process.exit(2);
|
|
2477
3524
|
}
|
|
2478
3525
|
);
|