@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.
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/actions/error.d.ts +26 -0
- package/dist/src/actions/event-delegation.d.ts +48 -0
- package/dist/src/actions/form.d.ts +72 -0
- package/dist/src/actions/index.d.ts +13 -0
- package/dist/src/actions/index.js +525 -0
- package/dist/src/actions/index.js.map +15 -0
- package/dist/src/actions/overlay.d.ts +26 -0
- package/dist/src/actions/registry.d.ts +25 -0
- package/dist/src/actions/store.d.ts +26 -0
- package/dist/src/actions/testing.d.ts +26 -0
- package/dist/src/background/index.js +26 -1
- package/dist/src/background/index.js.map +2 -2
- package/dist/src/background/message-router.js +26 -1
- package/dist/src/background/message-router.js.map +2 -2
- package/dist/src/client/index.js +27 -2
- package/dist/src/client/index.js.map +3 -3
- package/dist/src/elysia/index.js +26 -1
- package/dist/src/elysia/index.js.map +2 -2
- package/dist/src/index.js +26 -1
- package/dist/src/index.js.map +2 -2
- package/dist/src/mesh-node.js +26 -1
- package/dist/src/mesh-node.js.map +2 -2
- package/dist/src/mesh.js +26 -1
- package/dist/src/mesh.js.map +2 -2
- package/dist/src/peer.js +26 -1
- package/dist/src/peer.js.map +2 -2
- package/dist/src/polly-ui/ActionForm.d.ts +21 -0
- package/dist/src/polly-ui/ActionInput.d.ts +41 -0
- package/dist/src/polly-ui/ConfirmDialog.d.ts +24 -0
- package/dist/src/polly-ui/Layout.d.ts +51 -0
- package/dist/src/polly-ui/Modal.d.ts +52 -0
- package/dist/src/polly-ui/OverlayRoot.d.ts +10 -0
- package/dist/src/polly-ui/TextInput.d.ts +31 -0
- package/dist/src/polly-ui/Toast.d.ts +19 -0
- package/dist/src/polly-ui/index.css +319 -0
- package/dist/src/polly-ui/index.d.ts +17 -0
- package/dist/src/polly-ui/index.js +953 -0
- package/dist/src/polly-ui/index.js.map +22 -0
- package/dist/src/polly-ui/internal/focus-trap.d.ts +10 -0
- package/dist/src/polly-ui/internal/input-base.d.ts +18 -0
- package/dist/src/polly-ui/internal/scroll-lock.d.ts +9 -0
- package/dist/src/polly-ui/styles.css +70 -0
- package/dist/src/polly-ui/theme.css +163 -0
- package/dist/src/shared/adapters/index.js +26 -1
- package/dist/src/shared/adapters/index.js.map +2 -2
- package/dist/src/shared/lib/context-helpers.js +26 -1
- package/dist/src/shared/lib/context-helpers.js.map +2 -2
- package/dist/src/shared/lib/errors.js +26 -1
- package/dist/src/shared/lib/errors.js.map +2 -2
- package/dist/src/shared/lib/message-bus.js +26 -1
- package/dist/src/shared/lib/message-bus.js.map +2 -2
- package/dist/src/shared/lib/resource.js +26 -1
- package/dist/src/shared/lib/resource.js.map +2 -2
- package/dist/src/shared/lib/state.js +26 -1
- package/dist/src/shared/lib/state.js.map +2 -2
- package/dist/src/shared/lib/test-helpers.js +26 -1
- package/dist/src/shared/lib/test-helpers.js.map +2 -2
- package/dist/src/shared/state/app-state.js +26 -1
- package/dist/src/shared/state/app-state.js.map +2 -2
- package/dist/src/shared/types/messages.js +26 -1
- package/dist/src/shared/types/messages.js.map +2 -2
- package/dist/tools/quality/src/cli.js +640 -28
- package/dist/tools/quality/src/cli.js.map +11 -5
- package/dist/tools/quality/src/css/check-layout.d.ts +19 -0
- package/dist/tools/quality/src/css/check-quality.d.ts +24 -0
- package/dist/tools/quality/src/css/check-unused.d.ts +20 -0
- package/dist/tools/quality/src/css/check-vars.d.ts +22 -0
- package/dist/tools/quality/src/css/shared.d.ts +33 -0
- package/dist/tools/quality/src/index.d.ts +12 -0
- package/dist/tools/quality/src/index.js +557 -18
- package/dist/tools/quality/src/index.js.map +10 -4
- package/dist/tools/quality/src/logger.d.ts +26 -0
- package/dist/tools/test/src/adapters/index.js +26 -1
- package/dist/tools/test/src/adapters/index.js.map +2 -2
- package/dist/tools/test/src/browser/index.js +26 -1
- package/dist/tools/test/src/browser/index.js.map +2 -2
- package/dist/tools/test/src/browser/run.js +238 -0
- package/dist/tools/test/src/browser/run.js.map +11 -0
- package/dist/tools/test/src/index.js +26 -1
- package/dist/tools/test/src/index.js.map +2 -2
- package/dist/tools/test/src/test-utils.js +26 -1
- package/dist/tools/test/src/test-utils.js.map +2 -2
- package/dist/tools/test/src/visual/compare.d.ts +23 -0
- package/dist/tools/test/src/visual/harness.d.ts +53 -0
- package/dist/tools/test/src/visual/index.d.ts +12 -0
- package/dist/tools/test/src/visual/index.js +13968 -0
- package/dist/tools/test/src/visual/index.js.map +41 -0
- package/dist/tools/verify/src/cli.js +3 -3
- package/dist/tools/verify/src/cli.js.map +1 -1
- package/dist/tools/verify/src/config.js +26 -1
- package/dist/tools/verify/src/config.js.map +2 -2
- 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(
|
|
125
|
-
const segments =
|
|
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(
|
|
664
|
+
return excludeFiles.has(basename) || excludeFiles.has(relative2);
|
|
132
665
|
}
|
|
133
|
-
function findViolations(
|
|
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:
|
|
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
|
-
|
|
692
|
+
logger.log("[no-as-casting] ✅ No violations found.");
|
|
160
693
|
return;
|
|
161
694
|
}
|
|
162
|
-
|
|
695
|
+
logger.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
163
696
|
`);
|
|
164
697
|
for (const v of violations) {
|
|
165
|
-
|
|
166
|
-
|
|
698
|
+
logger.log(` ${v.file}:${v.line}`);
|
|
699
|
+
logger.log(` ${v.content}`);
|
|
167
700
|
if (v.advice)
|
|
168
|
-
|
|
169
|
-
|
|
701
|
+
logger.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
702
|
+
logger.log("");
|
|
170
703
|
}
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
184
|
-
if (isFileExcluded(
|
|
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(
|
|
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(",") ?? [
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
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=
|
|
825
|
+
//# debugId=33FEF60F0C8A440B64756E2164756E21
|