@fairfox/polly 0.23.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/README.md +55 -1
- 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 +27 -2
- package/dist/src/elysia/index.js.map +3 -3
- package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
- package/dist/src/index.js +26 -1
- package/dist/src/index.js.map +2 -2
- package/dist/src/mesh-node.d.ts +89 -0
- package/dist/src/mesh-node.js +619 -0
- package/dist/src/mesh-node.js.map +14 -0
- package/dist/src/mesh.d.ts +10 -0
- package/dist/src/mesh.js +951 -24
- package/dist/src/mesh.js.map +17 -9
- package/dist/src/peer.d.ts +1 -0
- package/dist/src/peer.js +130 -84
- package/dist/src/peer.js.map +11 -10
- 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/blob-cache.d.ts +58 -0
- package/dist/src/shared/lib/blob-store-impl.d.ts +33 -0
- package/dist/src/shared/lib/blob-store.d.ts +87 -0
- package/dist/src/shared/lib/blob-transfer.d.ts +58 -0
- 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/crdt-specialised.d.ts +1 -1
- package/dist/src/shared/lib/crdt-state.d.ts +1 -1
- package/dist/src/shared/lib/errors.js +26 -1
- package/dist/src/shared/lib/errors.js.map +2 -2
- package/dist/src/shared/lib/keyring-storage.d.ts +57 -0
- package/dist/src/shared/lib/mesh-client.d.ts +91 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +1 -1
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -0
- package/dist/src/shared/lib/mesh-state.d.ts +1 -1
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +20 -1
- 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/peer-relay-adapter.d.ts +1 -1
- package/dist/src/shared/lib/peer-repo-server.d.ts +1 -1
- package/dist/src/shared/lib/peer-state.d.ts +1 -1
- 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/lib/wasm-init.d.ts +17 -0
- 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 +647 -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 +37 -0
- package/dist/tools/quality/src/index.js +735 -0
- package/dist/tools/quality/src/index.js.map +16 -0
- package/dist/tools/quality/src/logger.d.ts +26 -0
- package/dist/tools/quality/src/no-as-casting.d.ts +44 -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 +42 -3
|
@@ -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,24 +654,31 @@ 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
|
`);
|
|
670
|
+
let insideTemplate = false;
|
|
137
671
|
for (let i = 0;i < lines.length; i++) {
|
|
138
672
|
const line = lines[i] ?? "";
|
|
673
|
+
const backticks = (line.match(/`/g) ?? []).length;
|
|
674
|
+
const startedInTemplate = insideTemplate;
|
|
675
|
+
if (backticks % 2 === 1)
|
|
676
|
+
insideTemplate = !insideTemplate;
|
|
677
|
+
if (startedInTemplate && backticks === 0 && !line.includes("${"))
|
|
678
|
+
continue;
|
|
139
679
|
if (!isLineClean(line)) {
|
|
140
680
|
results.push({
|
|
141
|
-
file:
|
|
681
|
+
file: relative2,
|
|
142
682
|
line: i + 1,
|
|
143
683
|
content: line.trim(),
|
|
144
684
|
advice: suggestFix(line.trim())
|
|
@@ -149,20 +689,20 @@ function findViolations(relative, content) {
|
|
|
149
689
|
}
|
|
150
690
|
function printViolations(violations) {
|
|
151
691
|
if (violations.length === 0) {
|
|
152
|
-
|
|
692
|
+
logger.log("[no-as-casting] ✅ No violations found.");
|
|
153
693
|
return;
|
|
154
694
|
}
|
|
155
|
-
|
|
695
|
+
logger.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
156
696
|
`);
|
|
157
697
|
for (const v of violations) {
|
|
158
|
-
|
|
159
|
-
|
|
698
|
+
logger.log(` ${v.file}:${v.line}`);
|
|
699
|
+
logger.log(` ${v.content}`);
|
|
160
700
|
if (v.advice)
|
|
161
|
-
|
|
162
|
-
|
|
701
|
+
logger.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
702
|
+
logger.log("");
|
|
163
703
|
}
|
|
164
|
-
|
|
165
|
-
|
|
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.');
|
|
166
706
|
}
|
|
167
707
|
async function checkNoAsCasting(options) {
|
|
168
708
|
const rootDir = options.rootDir;
|
|
@@ -173,34 +713,113 @@ async function checkNoAsCasting(options) {
|
|
|
173
713
|
const glob = new Glob(pattern);
|
|
174
714
|
const violations = [];
|
|
175
715
|
for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
176
|
-
const
|
|
177
|
-
if (isFileExcluded(
|
|
716
|
+
const relative2 = file.replace(`${rootDir}/`, "");
|
|
717
|
+
if (isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles))
|
|
178
718
|
continue;
|
|
179
719
|
const content = readFileSync(file, "utf-8");
|
|
180
|
-
violations.push(...findViolations(
|
|
720
|
+
violations.push(...findViolations(relative2, content));
|
|
181
721
|
}
|
|
182
722
|
return { violations, print: () => printViolations(violations) };
|
|
183
723
|
}
|
|
184
|
-
|
|
185
724
|
// tools/quality/src/cli.ts
|
|
186
725
|
var args = process.argv.slice(2);
|
|
187
726
|
function getFlag(name) {
|
|
188
727
|
const idx = args.indexOf(`--${name}`);
|
|
189
728
|
return idx >= 0 ? args[idx + 1] : undefined;
|
|
190
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();
|
|
191
738
|
var rootDir = getFlag("root") ?? process.cwd();
|
|
192
|
-
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
|
+
];
|
|
193
748
|
var excludePackages = getFlag("exclude-packages")?.split(",");
|
|
194
749
|
var excludeFiles = getFlag("exclude-files")?.split(",");
|
|
195
750
|
var filePatterns = getFlag("pattern");
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
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);
|
|
205
824
|
|
|
206
|
-
//# debugId=
|
|
825
|
+
//# debugId=33FEF60F0C8A440B64756E2164756E21
|