@fairfox/polly 0.24.0 → 0.25.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.
Files changed (94) hide show
  1. package/dist/cli/polly.js +21 -1
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/src/actions/error.d.ts +26 -0
  4. package/dist/src/actions/event-delegation.d.ts +48 -0
  5. package/dist/src/actions/form.d.ts +72 -0
  6. package/dist/src/actions/index.d.ts +13 -0
  7. package/dist/src/actions/index.js +525 -0
  8. package/dist/src/actions/index.js.map +15 -0
  9. package/dist/src/actions/overlay.d.ts +26 -0
  10. package/dist/src/actions/registry.d.ts +25 -0
  11. package/dist/src/actions/store.d.ts +26 -0
  12. package/dist/src/actions/testing.d.ts +26 -0
  13. package/dist/src/background/index.js +26 -1
  14. package/dist/src/background/index.js.map +2 -2
  15. package/dist/src/background/message-router.js +26 -1
  16. package/dist/src/background/message-router.js.map +2 -2
  17. package/dist/src/client/index.js +27 -2
  18. package/dist/src/client/index.js.map +3 -3
  19. package/dist/src/elysia/index.js +26 -1
  20. package/dist/src/elysia/index.js.map +2 -2
  21. package/dist/src/index.js +26 -1
  22. package/dist/src/index.js.map +2 -2
  23. package/dist/src/mesh-node.js +26 -1
  24. package/dist/src/mesh-node.js.map +2 -2
  25. package/dist/src/mesh.js +26 -1
  26. package/dist/src/mesh.js.map +2 -2
  27. package/dist/src/peer.js +26 -1
  28. package/dist/src/peer.js.map +2 -2
  29. package/dist/src/polly-ui/ActionForm.d.ts +21 -0
  30. package/dist/src/polly-ui/ActionInput.d.ts +41 -0
  31. package/dist/src/polly-ui/ConfirmDialog.d.ts +24 -0
  32. package/dist/src/polly-ui/Layout.d.ts +51 -0
  33. package/dist/src/polly-ui/Modal.d.ts +52 -0
  34. package/dist/src/polly-ui/OverlayRoot.d.ts +10 -0
  35. package/dist/src/polly-ui/TextInput.d.ts +31 -0
  36. package/dist/src/polly-ui/Toast.d.ts +19 -0
  37. package/dist/src/polly-ui/index.css +319 -0
  38. package/dist/src/polly-ui/index.d.ts +17 -0
  39. package/dist/src/polly-ui/index.js +953 -0
  40. package/dist/src/polly-ui/index.js.map +22 -0
  41. package/dist/src/polly-ui/internal/focus-trap.d.ts +10 -0
  42. package/dist/src/polly-ui/internal/input-base.d.ts +18 -0
  43. package/dist/src/polly-ui/internal/scroll-lock.d.ts +9 -0
  44. package/dist/src/polly-ui/styles.css +70 -0
  45. package/dist/src/polly-ui/theme.css +163 -0
  46. package/dist/src/shared/adapters/index.js +26 -1
  47. package/dist/src/shared/adapters/index.js.map +2 -2
  48. package/dist/src/shared/lib/context-helpers.js +26 -1
  49. package/dist/src/shared/lib/context-helpers.js.map +2 -2
  50. package/dist/src/shared/lib/errors.js +26 -1
  51. package/dist/src/shared/lib/errors.js.map +2 -2
  52. package/dist/src/shared/lib/message-bus.js +26 -1
  53. package/dist/src/shared/lib/message-bus.js.map +2 -2
  54. package/dist/src/shared/lib/resource.js +26 -1
  55. package/dist/src/shared/lib/resource.js.map +2 -2
  56. package/dist/src/shared/lib/state.js +26 -1
  57. package/dist/src/shared/lib/state.js.map +2 -2
  58. package/dist/src/shared/lib/test-helpers.js +26 -1
  59. package/dist/src/shared/lib/test-helpers.js.map +2 -2
  60. package/dist/src/shared/state/app-state.js +26 -1
  61. package/dist/src/shared/state/app-state.js.map +2 -2
  62. package/dist/src/shared/types/messages.js +26 -1
  63. package/dist/src/shared/types/messages.js.map +2 -2
  64. package/dist/tools/quality/src/cli.js +640 -28
  65. package/dist/tools/quality/src/cli.js.map +11 -5
  66. package/dist/tools/quality/src/css/check-layout.d.ts +19 -0
  67. package/dist/tools/quality/src/css/check-quality.d.ts +24 -0
  68. package/dist/tools/quality/src/css/check-unused.d.ts +20 -0
  69. package/dist/tools/quality/src/css/check-vars.d.ts +22 -0
  70. package/dist/tools/quality/src/css/shared.d.ts +33 -0
  71. package/dist/tools/quality/src/index.d.ts +12 -0
  72. package/dist/tools/quality/src/index.js +557 -18
  73. package/dist/tools/quality/src/index.js.map +10 -4
  74. package/dist/tools/quality/src/logger.d.ts +26 -0
  75. package/dist/tools/test/src/adapters/index.js +26 -1
  76. package/dist/tools/test/src/adapters/index.js.map +2 -2
  77. package/dist/tools/test/src/browser/index.js +26 -1
  78. package/dist/tools/test/src/browser/index.js.map +2 -2
  79. package/dist/tools/test/src/browser/run.js +238 -0
  80. package/dist/tools/test/src/browser/run.js.map +11 -0
  81. package/dist/tools/test/src/index.js +26 -1
  82. package/dist/tools/test/src/index.js.map +2 -2
  83. package/dist/tools/test/src/test-utils.js +26 -1
  84. package/dist/tools/test/src/test-utils.js.map +2 -2
  85. package/dist/tools/test/src/visual/compare.d.ts +23 -0
  86. package/dist/tools/test/src/visual/harness.d.ts +53 -0
  87. package/dist/tools/test/src/visual/index.d.ts +12 -0
  88. package/dist/tools/test/src/visual/index.js +13968 -0
  89. package/dist/tools/test/src/visual/index.js.map +41 -0
  90. package/dist/tools/verify/src/cli.js +3 -3
  91. package/dist/tools/verify/src/cli.js.map +1 -1
  92. package/dist/tools/verify/src/config.js +26 -1
  93. package/dist/tools/verify/src/config.js.map +2 -2
  94. package/package.json +21 -2
@@ -17,6 +17,539 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
+ // tools/quality/src/css/shared.ts
21
+ import { readdir } from "node:fs/promises";
22
+ import { join, relative } from "node:path";
23
+
24
+ // tools/quality/src/logger.ts
25
+ function defaultLog(message) {
26
+ console.log(message);
27
+ }
28
+ function defaultError(message) {
29
+ console.error(message);
30
+ }
31
+ function defaultInfo(message) {
32
+ console.info(message);
33
+ }
34
+ function defaultWarn(message) {
35
+ console.warn(message);
36
+ }
37
+ var logger = {
38
+ log: defaultLog,
39
+ error: defaultError,
40
+ info: defaultInfo,
41
+ warn: defaultWarn
42
+ };
43
+ function resetLogger() {
44
+ logger.log = defaultLog;
45
+ logger.error = defaultError;
46
+ logger.info = defaultInfo;
47
+ logger.warn = defaultWarn;
48
+ }
49
+
50
+ // tools/quality/src/css/shared.ts
51
+ var DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", "dist-test", "build", "coverage"];
52
+ async function walkDirectory(dir, visit, opts) {
53
+ const skipDirs = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
54
+ const skipFiles = new Set(opts.skipFiles ?? []);
55
+ const rootDir = opts.rootDir;
56
+ async function walk(current) {
57
+ let entries;
58
+ try {
59
+ entries = await readdir(current, { withFileTypes: true });
60
+ } catch {
61
+ return;
62
+ }
63
+ for (const entry of entries) {
64
+ const full = join(current, entry.name);
65
+ if (entry.isDirectory()) {
66
+ if (skipDirs.has(entry.name))
67
+ continue;
68
+ await walk(full);
69
+ } else if (entry.isFile()) {
70
+ if (skipFiles.has(entry.name))
71
+ continue;
72
+ const rel = relative(rootDir, full);
73
+ await visit(full, rel);
74
+ }
75
+ }
76
+ }
77
+ await walk(dir);
78
+ }
79
+ function formatViolations(kind, violations, rootDir) {
80
+ const lines = [];
81
+ if (violations.length === 0) {
82
+ lines.push(`✅ ${kind}: no violations`);
83
+ return lines;
84
+ }
85
+ lines.push(`❌ ${kind}: ${violations.length} violation(s)`);
86
+ const byFile = new Map;
87
+ for (const v of violations) {
88
+ const bucket = byFile.get(v.file) ?? [];
89
+ bucket.push(v);
90
+ byFile.set(v.file, bucket);
91
+ }
92
+ for (const [file, fileViolations] of byFile) {
93
+ lines.push(` ${relative(rootDir, file)}`);
94
+ for (const v of fileViolations) {
95
+ lines.push(` L${v.line}: ${v.content}`);
96
+ lines.push(` → ${v.suggestion} [${v.rule}]`);
97
+ }
98
+ }
99
+ return lines;
100
+ }
101
+ function makeResult(kind, rootDir, violations) {
102
+ return {
103
+ violations,
104
+ print() {
105
+ for (const line of formatViolations(kind, violations, rootDir)) {
106
+ if (line.startsWith("❌")) {
107
+ logger.error(line);
108
+ } else {
109
+ logger.log(line);
110
+ }
111
+ }
112
+ }
113
+ };
114
+ }
115
+ function isInsideComment(line) {
116
+ const trimmed = line.trim();
117
+ return trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("//");
118
+ }
119
+ function isInsideKeyframes(lineNum, allLines) {
120
+ for (let i = lineNum - 1;i >= 0; i -= 1) {
121
+ const l = allLines[i]?.trim() ?? "";
122
+ if (l.startsWith("@keyframes"))
123
+ return true;
124
+ if (l === "}" && i < lineNum - 1)
125
+ return false;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ // tools/quality/src/css/check-layout.ts
131
+ var CSS_PATTERNS = [
132
+ { pattern: /display\s*:\s*flex/, kind: "display: flex in CSS" },
133
+ { pattern: /display\s*:\s*grid/, kind: "display: grid in CSS" }
134
+ ];
135
+ var TSX_PATTERNS = [
136
+ {
137
+ pattern: /display\s*:\s*['"]flex['"]/,
138
+ kind: "display: flex in inline style"
139
+ },
140
+ {
141
+ pattern: /display\s*:\s*['"]grid['"]/,
142
+ kind: "display: grid in inline style"
143
+ }
144
+ ];
145
+ var SUPPRESS = "layout-ignore";
146
+ async function checkCssLayout(options) {
147
+ const rootDir = options.rootDir;
148
+ const exempt = options.layoutExemptPaths ?? ["Layout.module.css", "Layout.tsx"];
149
+ const violations = [];
150
+ await walkDirectory(rootDir, async (full) => {
151
+ const isCss = full.endsWith(".module.css");
152
+ const isTsx = full.endsWith(".tsx");
153
+ if (!isCss && !isTsx)
154
+ return;
155
+ if (exempt.some((fragment) => full.includes(fragment)))
156
+ return;
157
+ const patterns = isCss ? CSS_PATTERNS : TSX_PATTERNS;
158
+ const content = await Bun.file(full).text();
159
+ const lines = content.split(`
160
+ `);
161
+ for (let i = 0;i < lines.length; i += 1) {
162
+ const line = lines[i];
163
+ if (!line)
164
+ continue;
165
+ const trimmed = line.trim();
166
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*"))
167
+ continue;
168
+ if (trimmed.includes(SUPPRESS))
169
+ continue;
170
+ const prev = i > 0 ? lines[i - 1]?.trim() ?? "" : "";
171
+ if (prev.includes(SUPPRESS))
172
+ continue;
173
+ for (const rule of patterns) {
174
+ if (rule.pattern.test(line)) {
175
+ violations.push({
176
+ file: full,
177
+ line: i + 1,
178
+ rule: rule.kind,
179
+ content: trimmed,
180
+ suggestion: "Use the <Layout> component instead"
181
+ });
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ }, {
187
+ rootDir,
188
+ skipDirs: options.skipDirs
189
+ });
190
+ return makeResult("css-layout", rootDir, violations);
191
+ }
192
+ // tools/quality/src/css/check-quality.ts
193
+ var DEFAULT_RULES = [
194
+ {
195
+ id: "no-hardcoded-color",
196
+ check: (line) => {
197
+ if (isInsideComment(line))
198
+ return null;
199
+ if (line.includes("var("))
200
+ return null;
201
+ if (/\bcolor:\s*(white|black|#[0-9a-fA-F]{3,8})\b/.test(line)) {
202
+ return "Use a semantic colour token (--polly-text, --polly-text-muted, …)";
203
+ }
204
+ if (/background(-color)?:\s*#[0-9a-fA-F]{3,8}/.test(line) && !line.includes("var(")) {
205
+ return "Use a semantic surface token (--polly-surface, --polly-surface-raised, …)";
206
+ }
207
+ return null;
208
+ }
209
+ },
210
+ {
211
+ id: "no-hardcoded-rgba",
212
+ check: (line) => {
213
+ if (isInsideComment(line))
214
+ return null;
215
+ if (!line.includes("rgba("))
216
+ return null;
217
+ if (line.includes("var("))
218
+ return null;
219
+ if (line.includes("color-mix("))
220
+ return null;
221
+ if (/background.*rgba/.test(line)) {
222
+ return "Use a semantic surface or overlay token";
223
+ }
224
+ if (/box-shadow.*rgba/.test(line)) {
225
+ return "Use a semantic shadow token";
226
+ }
227
+ return "Use a semantic token instead of raw rgba()";
228
+ }
229
+ },
230
+ {
231
+ id: "no-hardcoded-z-index",
232
+ check: (line) => {
233
+ const m = line.match(/z-index:\s*(\d+)/);
234
+ if (!m?.[1])
235
+ return null;
236
+ if (line.includes("var("))
237
+ return null;
238
+ if (Number.parseInt(m[1], 10) < 10)
239
+ return null;
240
+ return "Use a --polly-z-* token";
241
+ }
242
+ },
243
+ {
244
+ id: "no-hardcoded-opacity",
245
+ check: (line) => {
246
+ if (!/opacity:\s*0\.\d/.test(line))
247
+ return null;
248
+ if (line.includes("var("))
249
+ return null;
250
+ const m = line.match(/opacity:\s*(0\.\d+)/);
251
+ if (!m?.[1])
252
+ return null;
253
+ const v = Number.parseFloat(m[1]);
254
+ if (v >= 0.5 && v <= 0.7) {
255
+ return "Use var(--polly-opacity-disabled) for disabled states";
256
+ }
257
+ return null;
258
+ }
259
+ },
260
+ {
261
+ id: "no-hardcoded-transition",
262
+ check: (line, lineNum, allLines) => {
263
+ if (isInsideKeyframes(lineNum, allLines))
264
+ return null;
265
+ if (line.includes("var(--polly-motion"))
266
+ return null;
267
+ if (!/(?:transition|animation)/.test(line))
268
+ return null;
269
+ if (/\d+(\.\d+)?(ms|s)\s+(ease|linear|ease-in|ease-out|ease-in-out)/.test(line) && !line.includes("infinite")) {
270
+ return "Use var(--polly-motion-fast|base|slow)";
271
+ }
272
+ if (/\s\d+(\.\d+)?s[;\s,]/.test(line) && !line.includes("infinite")) {
273
+ return "Use var(--polly-motion-fast|base|slow)";
274
+ }
275
+ return null;
276
+ }
277
+ },
278
+ {
279
+ id: "no-important",
280
+ check: (line) => {
281
+ if (isInsideComment(line))
282
+ return null;
283
+ if (!line.includes("!important"))
284
+ return null;
285
+ return "Refactor specificity instead of using !important";
286
+ }
287
+ },
288
+ {
289
+ id: "no-rem-units",
290
+ check: (line, lineNum, allLines) => {
291
+ if (isInsideComment(line))
292
+ return null;
293
+ if (isInsideKeyframes(lineNum, allLines))
294
+ return null;
295
+ if (/:\s*[^;]*\d+(\.\d+)?rem[;\s]/.test(line)) {
296
+ if (line.includes("calc(") || line.includes("translateY(")) {
297
+ return null;
298
+ }
299
+ return "Use --polly-space-* or --polly-text-* tokens instead of rem";
300
+ }
301
+ return null;
302
+ }
303
+ },
304
+ {
305
+ id: "no-hardcoded-border-width",
306
+ check: (line) => {
307
+ if (isInsideComment(line))
308
+ return null;
309
+ if (line.includes("var(--polly-border-width"))
310
+ return null;
311
+ const m = line.match(/border(?:-(?:top|right|bottom|left))?:\s*(\d+)px\s+solid/);
312
+ if (!m?.[1])
313
+ return null;
314
+ return "Use a --polly-border-width-* token";
315
+ }
316
+ },
317
+ {
318
+ id: "no-hardcoded-border-radius",
319
+ check: (line) => {
320
+ if (isInsideComment(line))
321
+ return null;
322
+ if (line.includes("var("))
323
+ return null;
324
+ const m = line.match(/border-radius:\s*(\d+)px/);
325
+ if (!m?.[1])
326
+ return null;
327
+ if (m[1] === "0")
328
+ return null;
329
+ return "Use a --polly-radius-{sm,md,lg} token";
330
+ }
331
+ },
332
+ {
333
+ id: "no-hardcoded-box-shadow",
334
+ check: (line) => {
335
+ if (isInsideComment(line))
336
+ return null;
337
+ if (!line.includes("box-shadow:"))
338
+ return null;
339
+ if (line.includes("var(--polly-shadow"))
340
+ return null;
341
+ if (line.includes("var(--polly-focus-ring"))
342
+ return null;
343
+ if (line.includes("var("))
344
+ return null;
345
+ if (/box-shadow:\s*none/.test(line))
346
+ return null;
347
+ return "Use var(--polly-shadow-*) or var(--polly-focus-ring)";
348
+ }
349
+ },
350
+ {
351
+ id: "no-hardcoded-font-weight",
352
+ check: (line) => {
353
+ if (!/font-weight:\s*\d{3}/.test(line))
354
+ return null;
355
+ if (line.includes("var("))
356
+ return null;
357
+ return "Use var(--polly-weight-normal|medium|bold)";
358
+ }
359
+ }
360
+ ];
361
+ async function checkCssQuality(options) {
362
+ const rootDir = options.rootDir;
363
+ const extensions = options.extensions ?? [".module.css"];
364
+ const skipFiles = options.skipFiles ?? ["theme.css", "tokens.css"];
365
+ const disabled = new Set(options.disableRules ?? []);
366
+ const rules = DEFAULT_RULES.filter((r) => !disabled.has(r.id));
367
+ const violations = [];
368
+ await walkDirectory(rootDir, async (full) => {
369
+ if (!extensions.some((ext) => full.endsWith(ext)))
370
+ return;
371
+ const content = await Bun.file(full).text();
372
+ const lines = content.split(`
373
+ `);
374
+ const hasFileIgnore = lines[0]?.includes("polly-ignore-all");
375
+ if (hasFileIgnore)
376
+ return;
377
+ for (let i = 0;i < lines.length; i += 1) {
378
+ const line = lines[i];
379
+ if (!line)
380
+ continue;
381
+ const trimmed = line.trim();
382
+ if (trimmed === "" || trimmed === "{" || trimmed === "}")
383
+ continue;
384
+ for (const rule of rules) {
385
+ const suggestion = rule.check(line, i, lines);
386
+ if (suggestion) {
387
+ violations.push({
388
+ file: full,
389
+ line: i + 1,
390
+ rule: rule.id,
391
+ content: trimmed,
392
+ suggestion
393
+ });
394
+ }
395
+ }
396
+ }
397
+ }, {
398
+ rootDir,
399
+ skipDirs: options.skipDirs,
400
+ skipFiles
401
+ });
402
+ return makeResult("css-quality", rootDir, violations);
403
+ }
404
+ // tools/quality/src/css/check-unused.ts
405
+ async function checkCssUnused(options) {
406
+ const rootDir = options.rootDir;
407
+ const alwaysUsed = new Set(options.alwaysUsedClasses ?? []);
408
+ const definitions = [];
409
+ const tsContents = [];
410
+ const cssContents = [];
411
+ await walkDirectory(rootDir, async (full) => {
412
+ if (full.endsWith(".module.css")) {
413
+ const content = await Bun.file(full).text();
414
+ const lines = content.split(`
415
+ `);
416
+ for (let i = 0;i < lines.length; i += 1) {
417
+ const line = lines[i];
418
+ if (!line)
419
+ continue;
420
+ for (const m of line.matchAll(/(?:^|[\s,>+~])\.([\w-]+)/g)) {
421
+ if (m[1]) {
422
+ definitions.push({
423
+ file: full,
424
+ name: m[1],
425
+ line: i + 1,
426
+ type: "class"
427
+ });
428
+ }
429
+ }
430
+ for (const m of line.matchAll(/^\s+--([\w-]+)\s*:/g)) {
431
+ if (m[1]) {
432
+ definitions.push({
433
+ file: full,
434
+ name: `--${m[1]}`,
435
+ line: i + 1,
436
+ type: "variable"
437
+ });
438
+ }
439
+ }
440
+ }
441
+ cssContents.push({ file: full, content });
442
+ } else if (full.endsWith(".css") && !full.endsWith(".css.d.ts")) {
443
+ const content = await Bun.file(full).text();
444
+ cssContents.push({ file: full, content });
445
+ } else if ((full.endsWith(".ts") || full.endsWith(".tsx")) && !full.endsWith(".css.d.ts")) {
446
+ const content = await Bun.file(full).text();
447
+ tsContents.push({ file: full, content });
448
+ }
449
+ }, { rootDir, skipDirs: options.skipDirs });
450
+ const seen = new Set;
451
+ const uniqueDefs = definitions.filter((d) => {
452
+ const key = `${d.file}:${d.type}:${d.name}`;
453
+ if (seen.has(key))
454
+ return false;
455
+ seen.add(key);
456
+ return true;
457
+ });
458
+ function classReferenced(name) {
459
+ if (alwaysUsed.has(name))
460
+ return true;
461
+ for (const { content } of tsContents) {
462
+ if (content.includes(`.${name}`) || content.includes(`['${name}']`) || content.includes(`["${name}"]`)) {
463
+ return true;
464
+ }
465
+ }
466
+ return false;
467
+ }
468
+ function variableReferenced(name, defFile) {
469
+ for (const { file, content } of cssContents) {
470
+ if (file === defFile)
471
+ continue;
472
+ if (content.includes(`var(${name})`))
473
+ return true;
474
+ }
475
+ const self = cssContents.find((f) => f.file === defFile);
476
+ if (self) {
477
+ const occurrences = self.content.split(name).length - 1;
478
+ if (occurrences > 1)
479
+ return true;
480
+ }
481
+ for (const { content } of tsContents) {
482
+ if (content.includes(name))
483
+ return true;
484
+ }
485
+ return false;
486
+ }
487
+ const violations = [];
488
+ for (const def of uniqueDefs) {
489
+ if (def.type === "class" && !classReferenced(def.name)) {
490
+ violations.push({
491
+ file: def.file,
492
+ line: def.line,
493
+ rule: "unused-class",
494
+ content: `.${def.name}`,
495
+ suggestion: "Delete the selector or reference the class from a component"
496
+ });
497
+ } else if (def.type === "variable" && !variableReferenced(def.name, def.file)) {
498
+ violations.push({
499
+ file: def.file,
500
+ line: def.line,
501
+ rule: "unused-variable",
502
+ content: def.name,
503
+ suggestion: "Delete the definition or reference the variable"
504
+ });
505
+ }
506
+ }
507
+ return makeResult("css-unused (advisory)", rootDir, violations);
508
+ }
509
+ // tools/quality/src/css/check-vars.ts
510
+ async function checkCssVars(options) {
511
+ const rootDir = options.rootDir;
512
+ const scanExts = options.scanExtensions ?? [".ts", ".tsx", ".css"];
513
+ const dynamic = new Set(options.dynamicVars ?? []);
514
+ const definitions = new Set(dynamic);
515
+ const violations = [];
516
+ await walkDirectory(rootDir, async (full) => {
517
+ if (!full.endsWith(".css"))
518
+ return;
519
+ const content = await Bun.file(full).text();
520
+ for (const m of content.matchAll(/--([\w-]+)\s*:/g)) {
521
+ if (m[1])
522
+ definitions.add(`--${m[1]}`);
523
+ }
524
+ }, { rootDir, skipDirs: options.skipDirs });
525
+ await walkDirectory(rootDir, async (full) => {
526
+ if (full.endsWith(".css.d.ts"))
527
+ return;
528
+ if (!scanExts.some((ext) => full.endsWith(ext)))
529
+ return;
530
+ const content = await Bun.file(full).text();
531
+ const lines = content.split(`
532
+ `);
533
+ for (let i = 0;i < lines.length; i += 1) {
534
+ const line = lines[i];
535
+ if (!line)
536
+ continue;
537
+ for (const m of line.matchAll(/var\(--([\w-]+)\)/g)) {
538
+ const name = `--${m[1]}`;
539
+ if (!definitions.has(name)) {
540
+ violations.push({
541
+ file: full,
542
+ line: i + 1,
543
+ rule: "undefined-var",
544
+ content: line.trim(),
545
+ suggestion: `Define ${name} in a CSS file or add to dynamicVars`
546
+ });
547
+ }
548
+ }
549
+ }
550
+ }, { rootDir, skipDirs: options.skipDirs });
551
+ return makeResult("css-vars", rootDir, violations);
552
+ }
20
553
  // tools/quality/src/no-as-casting.ts
21
554
  import { readFileSync } from "node:fs";
22
555
  import { Glob } from "bun";
@@ -121,16 +654,16 @@ function suggestFix(line) {
121
654
  }
122
655
  return;
123
656
  }
124
- function isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles) {
125
- const segments = relative.split("/");
657
+ function isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles) {
658
+ const segments = relative2.split("/");
126
659
  if (segments.some((s) => excludeDirs.has(s)))
127
660
  return true;
128
661
  if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s)))
129
662
  return true;
130
663
  const basename = segments[segments.length - 1] ?? "";
131
- return excludeFiles.has(basename) || excludeFiles.has(relative);
664
+ return excludeFiles.has(basename) || excludeFiles.has(relative2);
132
665
  }
133
- function findViolations(relative, content) {
666
+ function findViolations(relative2, content) {
134
667
  const results = [];
135
668
  const lines = content.split(`
136
669
  `);
@@ -145,7 +678,7 @@ function findViolations(relative, content) {
145
678
  continue;
146
679
  if (!isLineClean(line)) {
147
680
  results.push({
148
- file: relative,
681
+ file: relative2,
149
682
  line: i + 1,
150
683
  content: line.trim(),
151
684
  advice: suggestFix(line.trim())
@@ -156,20 +689,20 @@ function findViolations(relative, content) {
156
689
  }
157
690
  function printViolations(violations) {
158
691
  if (violations.length === 0) {
159
- console.log("[no-as-casting] ✅ No violations found.");
692
+ logger.log("[no-as-casting] ✅ No violations found.");
160
693
  return;
161
694
  }
162
- console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
695
+ logger.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
163
696
  `);
164
697
  for (const v of violations) {
165
- console.log(` ${v.file}:${v.line}`);
166
- console.log(` ${v.content}`);
698
+ logger.log(` ${v.file}:${v.line}`);
699
+ logger.log(` ${v.content}`);
167
700
  if (v.advice)
168
- console.log(` \uD83D\uDCA1 ${v.advice}`);
169
- console.log();
701
+ logger.log(` \uD83D\uDCA1 ${v.advice}`);
702
+ logger.log("");
170
703
  }
171
- console.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
172
- console.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
704
+ logger.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
705
+ logger.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
173
706
  }
174
707
  async function checkNoAsCasting(options) {
175
708
  const rootDir = options.rootDir;
@@ -180,34 +713,113 @@ async function checkNoAsCasting(options) {
180
713
  const glob = new Glob(pattern);
181
714
  const violations = [];
182
715
  for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
183
- const relative = file.replace(`${rootDir}/`, "");
184
- if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles))
716
+ const relative2 = file.replace(`${rootDir}/`, "");
717
+ if (isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles))
185
718
  continue;
186
719
  const content = readFileSync(file, "utf-8");
187
- violations.push(...findViolations(relative, content));
720
+ violations.push(...findViolations(relative2, content));
188
721
  }
189
722
  return { violations, print: () => printViolations(violations) };
190
723
  }
191
-
192
724
  // tools/quality/src/cli.ts
193
725
  var args = process.argv.slice(2);
194
726
  function getFlag(name) {
195
727
  const idx = args.indexOf(`--${name}`);
196
728
  return idx >= 0 ? args[idx + 1] : undefined;
197
729
  }
730
+ function getSubcommand() {
731
+ for (const arg of args) {
732
+ if (!arg.startsWith("--"))
733
+ return arg;
734
+ }
735
+ return "all";
736
+ }
737
+ var subcommand = getSubcommand();
198
738
  var rootDir = getFlag("root") ?? process.cwd();
199
- var exclude = getFlag("exclude")?.split(",") ?? ["node_modules", "dist", ".git", ".bun"];
739
+ var exclude = getFlag("exclude")?.split(",") ?? [
740
+ "node_modules",
741
+ "dist",
742
+ ".git",
743
+ ".bun",
744
+ "dist-test",
745
+ "build",
746
+ "coverage"
747
+ ];
200
748
  var excludePackages = getFlag("exclude-packages")?.split(",");
201
749
  var excludeFiles = getFlag("exclude-files")?.split(",");
202
750
  var filePatterns = getFlag("pattern");
203
- var result = await checkNoAsCasting({
204
- rootDir,
205
- exclude,
206
- ...excludePackages ? { excludePackages } : {},
207
- ...excludeFiles ? { excludeFiles } : {},
208
- ...filePatterns ? { filePatterns } : {}
209
- });
210
- result.print();
211
- process.exit(result.violations.length > 0 ? 1 : 0);
751
+ async function runNoAsCasting() {
752
+ const result = await checkNoAsCasting({
753
+ rootDir,
754
+ exclude,
755
+ ...excludePackages ? { excludePackages } : {},
756
+ ...excludeFiles ? { excludeFiles } : {},
757
+ ...filePatterns ? { filePatterns } : {}
758
+ });
759
+ result.print();
760
+ return result.violations.length > 0 ? 1 : 0;
761
+ }
762
+ async function runCssQuality() {
763
+ const r = await checkCssQuality({ rootDir, skipDirs: exclude });
764
+ r.print();
765
+ return r.violations.length > 0 ? 1 : 0;
766
+ }
767
+ async function runCssLayout() {
768
+ const r = await checkCssLayout({ rootDir, skipDirs: exclude });
769
+ r.print();
770
+ return r.violations.length > 0 ? 1 : 0;
771
+ }
772
+ async function runCssVars() {
773
+ const r = await checkCssVars({ rootDir, skipDirs: exclude });
774
+ r.print();
775
+ return r.violations.length > 0 ? 1 : 0;
776
+ }
777
+ async function runCssUnused() {
778
+ const r = await checkCssUnused({ rootDir, skipDirs: exclude });
779
+ r.print();
780
+ return 0;
781
+ }
782
+ async function runCssAll() {
783
+ const results = [
784
+ await runCssQuality(),
785
+ await runCssLayout(),
786
+ await runCssVars(),
787
+ await runCssUnused()
788
+ ];
789
+ return results.some((code) => code !== 0) ? 1 : 0;
790
+ }
791
+ async function runAll() {
792
+ const results = [await runNoAsCasting(), await runCssAll()];
793
+ return results.some((code) => code !== 0) ? 1 : 0;
794
+ }
795
+ var exitCode = 0;
796
+ switch (subcommand) {
797
+ case "no-as-casting":
798
+ exitCode = await runNoAsCasting();
799
+ break;
800
+ case "css":
801
+ exitCode = await runCssAll();
802
+ break;
803
+ case "css-quality":
804
+ exitCode = await runCssQuality();
805
+ break;
806
+ case "css-layout":
807
+ exitCode = await runCssLayout();
808
+ break;
809
+ case "css-vars":
810
+ exitCode = await runCssVars();
811
+ break;
812
+ case "css-unused":
813
+ exitCode = await runCssUnused();
814
+ break;
815
+ case "all":
816
+ exitCode = await runAll();
817
+ break;
818
+ default:
819
+ logger.error(`Unknown quality subcommand: ${subcommand}`);
820
+ logger.error("Expected one of: no-as-casting, css, css-quality, css-layout, css-vars, css-unused");
821
+ exitCode = 2;
822
+ }
823
+ process.exit(exitCode);
212
824
 
213
- //# debugId=D4F70D746137C24364756E2164756E21
825
+ //# debugId=33FEF60F0C8A440B64756E2164756E21