@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
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
// tools/quality/src/css/shared.ts
|
|
20
|
+
import { readdir } from "node:fs/promises";
|
|
21
|
+
import { join, relative } from "node:path";
|
|
22
|
+
|
|
23
|
+
// tools/quality/src/logger.ts
|
|
24
|
+
function defaultLog(message) {
|
|
25
|
+
console.log(message);
|
|
26
|
+
}
|
|
27
|
+
function defaultError(message) {
|
|
28
|
+
console.error(message);
|
|
29
|
+
}
|
|
30
|
+
function defaultInfo(message) {
|
|
31
|
+
console.info(message);
|
|
32
|
+
}
|
|
33
|
+
function defaultWarn(message) {
|
|
34
|
+
console.warn(message);
|
|
35
|
+
}
|
|
36
|
+
var logger = {
|
|
37
|
+
log: defaultLog,
|
|
38
|
+
error: defaultError,
|
|
39
|
+
info: defaultInfo,
|
|
40
|
+
warn: defaultWarn
|
|
41
|
+
};
|
|
42
|
+
function resetLogger() {
|
|
43
|
+
logger.log = defaultLog;
|
|
44
|
+
logger.error = defaultError;
|
|
45
|
+
logger.info = defaultInfo;
|
|
46
|
+
logger.warn = defaultWarn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// tools/quality/src/css/shared.ts
|
|
50
|
+
var DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", "dist-test", "build", "coverage"];
|
|
51
|
+
async function walkDirectory(dir, visit, opts) {
|
|
52
|
+
const skipDirs = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
|
|
53
|
+
const skipFiles = new Set(opts.skipFiles ?? []);
|
|
54
|
+
const rootDir = opts.rootDir;
|
|
55
|
+
async function walk(current) {
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const full = join(current, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
if (skipDirs.has(entry.name))
|
|
66
|
+
continue;
|
|
67
|
+
await walk(full);
|
|
68
|
+
} else if (entry.isFile()) {
|
|
69
|
+
if (skipFiles.has(entry.name))
|
|
70
|
+
continue;
|
|
71
|
+
const rel = relative(rootDir, full);
|
|
72
|
+
await visit(full, rel);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await walk(dir);
|
|
77
|
+
}
|
|
78
|
+
function formatViolations(kind, violations, rootDir) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
if (violations.length === 0) {
|
|
81
|
+
lines.push(`✅ ${kind}: no violations`);
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
lines.push(`❌ ${kind}: ${violations.length} violation(s)`);
|
|
85
|
+
const byFile = new Map;
|
|
86
|
+
for (const v of violations) {
|
|
87
|
+
const bucket = byFile.get(v.file) ?? [];
|
|
88
|
+
bucket.push(v);
|
|
89
|
+
byFile.set(v.file, bucket);
|
|
90
|
+
}
|
|
91
|
+
for (const [file, fileViolations] of byFile) {
|
|
92
|
+
lines.push(` ${relative(rootDir, file)}`);
|
|
93
|
+
for (const v of fileViolations) {
|
|
94
|
+
lines.push(` L${v.line}: ${v.content}`);
|
|
95
|
+
lines.push(` → ${v.suggestion} [${v.rule}]`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
function makeResult(kind, rootDir, violations) {
|
|
101
|
+
return {
|
|
102
|
+
violations,
|
|
103
|
+
print() {
|
|
104
|
+
for (const line of formatViolations(kind, violations, rootDir)) {
|
|
105
|
+
if (line.startsWith("❌")) {
|
|
106
|
+
logger.error(line);
|
|
107
|
+
} else {
|
|
108
|
+
logger.log(line);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function isInsideComment(line) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
return trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("//");
|
|
117
|
+
}
|
|
118
|
+
function isInsideKeyframes(lineNum, allLines) {
|
|
119
|
+
for (let i = lineNum - 1;i >= 0; i -= 1) {
|
|
120
|
+
const l = allLines[i]?.trim() ?? "";
|
|
121
|
+
if (l.startsWith("@keyframes"))
|
|
122
|
+
return true;
|
|
123
|
+
if (l === "}" && i < lineNum - 1)
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// tools/quality/src/css/check-layout.ts
|
|
130
|
+
var CSS_PATTERNS = [
|
|
131
|
+
{ pattern: /display\s*:\s*flex/, kind: "display: flex in CSS" },
|
|
132
|
+
{ pattern: /display\s*:\s*grid/, kind: "display: grid in CSS" }
|
|
133
|
+
];
|
|
134
|
+
var TSX_PATTERNS = [
|
|
135
|
+
{
|
|
136
|
+
pattern: /display\s*:\s*['"]flex['"]/,
|
|
137
|
+
kind: "display: flex in inline style"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
pattern: /display\s*:\s*['"]grid['"]/,
|
|
141
|
+
kind: "display: grid in inline style"
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
var SUPPRESS = "layout-ignore";
|
|
145
|
+
async function checkCssLayout(options) {
|
|
146
|
+
const rootDir = options.rootDir;
|
|
147
|
+
const exempt = options.layoutExemptPaths ?? ["Layout.module.css", "Layout.tsx"];
|
|
148
|
+
const violations = [];
|
|
149
|
+
await walkDirectory(rootDir, async (full) => {
|
|
150
|
+
const isCss = full.endsWith(".module.css");
|
|
151
|
+
const isTsx = full.endsWith(".tsx");
|
|
152
|
+
if (!isCss && !isTsx)
|
|
153
|
+
return;
|
|
154
|
+
if (exempt.some((fragment) => full.includes(fragment)))
|
|
155
|
+
return;
|
|
156
|
+
const patterns = isCss ? CSS_PATTERNS : TSX_PATTERNS;
|
|
157
|
+
const content = await Bun.file(full).text();
|
|
158
|
+
const lines = content.split(`
|
|
159
|
+
`);
|
|
160
|
+
for (let i = 0;i < lines.length; i += 1) {
|
|
161
|
+
const line = lines[i];
|
|
162
|
+
if (!line)
|
|
163
|
+
continue;
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*"))
|
|
166
|
+
continue;
|
|
167
|
+
if (trimmed.includes(SUPPRESS))
|
|
168
|
+
continue;
|
|
169
|
+
const prev = i > 0 ? lines[i - 1]?.trim() ?? "" : "";
|
|
170
|
+
if (prev.includes(SUPPRESS))
|
|
171
|
+
continue;
|
|
172
|
+
for (const rule of patterns) {
|
|
173
|
+
if (rule.pattern.test(line)) {
|
|
174
|
+
violations.push({
|
|
175
|
+
file: full,
|
|
176
|
+
line: i + 1,
|
|
177
|
+
rule: rule.kind,
|
|
178
|
+
content: trimmed,
|
|
179
|
+
suggestion: "Use the <Layout> component instead"
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}, {
|
|
186
|
+
rootDir,
|
|
187
|
+
skipDirs: options.skipDirs
|
|
188
|
+
});
|
|
189
|
+
return makeResult("css-layout", rootDir, violations);
|
|
190
|
+
}
|
|
191
|
+
// tools/quality/src/css/check-quality.ts
|
|
192
|
+
var DEFAULT_RULES = [
|
|
193
|
+
{
|
|
194
|
+
id: "no-hardcoded-color",
|
|
195
|
+
check: (line) => {
|
|
196
|
+
if (isInsideComment(line))
|
|
197
|
+
return null;
|
|
198
|
+
if (line.includes("var("))
|
|
199
|
+
return null;
|
|
200
|
+
if (/\bcolor:\s*(white|black|#[0-9a-fA-F]{3,8})\b/.test(line)) {
|
|
201
|
+
return "Use a semantic colour token (--polly-text, --polly-text-muted, …)";
|
|
202
|
+
}
|
|
203
|
+
if (/background(-color)?:\s*#[0-9a-fA-F]{3,8}/.test(line) && !line.includes("var(")) {
|
|
204
|
+
return "Use a semantic surface token (--polly-surface, --polly-surface-raised, …)";
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "no-hardcoded-rgba",
|
|
211
|
+
check: (line) => {
|
|
212
|
+
if (isInsideComment(line))
|
|
213
|
+
return null;
|
|
214
|
+
if (!line.includes("rgba("))
|
|
215
|
+
return null;
|
|
216
|
+
if (line.includes("var("))
|
|
217
|
+
return null;
|
|
218
|
+
if (line.includes("color-mix("))
|
|
219
|
+
return null;
|
|
220
|
+
if (/background.*rgba/.test(line)) {
|
|
221
|
+
return "Use a semantic surface or overlay token";
|
|
222
|
+
}
|
|
223
|
+
if (/box-shadow.*rgba/.test(line)) {
|
|
224
|
+
return "Use a semantic shadow token";
|
|
225
|
+
}
|
|
226
|
+
return "Use a semantic token instead of raw rgba()";
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "no-hardcoded-z-index",
|
|
231
|
+
check: (line) => {
|
|
232
|
+
const m = line.match(/z-index:\s*(\d+)/);
|
|
233
|
+
if (!m?.[1])
|
|
234
|
+
return null;
|
|
235
|
+
if (line.includes("var("))
|
|
236
|
+
return null;
|
|
237
|
+
if (Number.parseInt(m[1], 10) < 10)
|
|
238
|
+
return null;
|
|
239
|
+
return "Use a --polly-z-* token";
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "no-hardcoded-opacity",
|
|
244
|
+
check: (line) => {
|
|
245
|
+
if (!/opacity:\s*0\.\d/.test(line))
|
|
246
|
+
return null;
|
|
247
|
+
if (line.includes("var("))
|
|
248
|
+
return null;
|
|
249
|
+
const m = line.match(/opacity:\s*(0\.\d+)/);
|
|
250
|
+
if (!m?.[1])
|
|
251
|
+
return null;
|
|
252
|
+
const v = Number.parseFloat(m[1]);
|
|
253
|
+
if (v >= 0.5 && v <= 0.7) {
|
|
254
|
+
return "Use var(--polly-opacity-disabled) for disabled states";
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "no-hardcoded-transition",
|
|
261
|
+
check: (line, lineNum, allLines) => {
|
|
262
|
+
if (isInsideKeyframes(lineNum, allLines))
|
|
263
|
+
return null;
|
|
264
|
+
if (line.includes("var(--polly-motion"))
|
|
265
|
+
return null;
|
|
266
|
+
if (!/(?:transition|animation)/.test(line))
|
|
267
|
+
return null;
|
|
268
|
+
if (/\d+(\.\d+)?(ms|s)\s+(ease|linear|ease-in|ease-out|ease-in-out)/.test(line) && !line.includes("infinite")) {
|
|
269
|
+
return "Use var(--polly-motion-fast|base|slow)";
|
|
270
|
+
}
|
|
271
|
+
if (/\s\d+(\.\d+)?s[;\s,]/.test(line) && !line.includes("infinite")) {
|
|
272
|
+
return "Use var(--polly-motion-fast|base|slow)";
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "no-important",
|
|
279
|
+
check: (line) => {
|
|
280
|
+
if (isInsideComment(line))
|
|
281
|
+
return null;
|
|
282
|
+
if (!line.includes("!important"))
|
|
283
|
+
return null;
|
|
284
|
+
return "Refactor specificity instead of using !important";
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "no-rem-units",
|
|
289
|
+
check: (line, lineNum, allLines) => {
|
|
290
|
+
if (isInsideComment(line))
|
|
291
|
+
return null;
|
|
292
|
+
if (isInsideKeyframes(lineNum, allLines))
|
|
293
|
+
return null;
|
|
294
|
+
if (/:\s*[^;]*\d+(\.\d+)?rem[;\s]/.test(line)) {
|
|
295
|
+
if (line.includes("calc(") || line.includes("translateY(")) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
return "Use --polly-space-* or --polly-text-* tokens instead of rem";
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "no-hardcoded-border-width",
|
|
305
|
+
check: (line) => {
|
|
306
|
+
if (isInsideComment(line))
|
|
307
|
+
return null;
|
|
308
|
+
if (line.includes("var(--polly-border-width"))
|
|
309
|
+
return null;
|
|
310
|
+
const m = line.match(/border(?:-(?:top|right|bottom|left))?:\s*(\d+)px\s+solid/);
|
|
311
|
+
if (!m?.[1])
|
|
312
|
+
return null;
|
|
313
|
+
return "Use a --polly-border-width-* token";
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "no-hardcoded-border-radius",
|
|
318
|
+
check: (line) => {
|
|
319
|
+
if (isInsideComment(line))
|
|
320
|
+
return null;
|
|
321
|
+
if (line.includes("var("))
|
|
322
|
+
return null;
|
|
323
|
+
const m = line.match(/border-radius:\s*(\d+)px/);
|
|
324
|
+
if (!m?.[1])
|
|
325
|
+
return null;
|
|
326
|
+
if (m[1] === "0")
|
|
327
|
+
return null;
|
|
328
|
+
return "Use a --polly-radius-{sm,md,lg} token";
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "no-hardcoded-box-shadow",
|
|
333
|
+
check: (line) => {
|
|
334
|
+
if (isInsideComment(line))
|
|
335
|
+
return null;
|
|
336
|
+
if (!line.includes("box-shadow:"))
|
|
337
|
+
return null;
|
|
338
|
+
if (line.includes("var(--polly-shadow"))
|
|
339
|
+
return null;
|
|
340
|
+
if (line.includes("var(--polly-focus-ring"))
|
|
341
|
+
return null;
|
|
342
|
+
if (line.includes("var("))
|
|
343
|
+
return null;
|
|
344
|
+
if (/box-shadow:\s*none/.test(line))
|
|
345
|
+
return null;
|
|
346
|
+
return "Use var(--polly-shadow-*) or var(--polly-focus-ring)";
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "no-hardcoded-font-weight",
|
|
351
|
+
check: (line) => {
|
|
352
|
+
if (!/font-weight:\s*\d{3}/.test(line))
|
|
353
|
+
return null;
|
|
354
|
+
if (line.includes("var("))
|
|
355
|
+
return null;
|
|
356
|
+
return "Use var(--polly-weight-normal|medium|bold)";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
async function checkCssQuality(options) {
|
|
361
|
+
const rootDir = options.rootDir;
|
|
362
|
+
const extensions = options.extensions ?? [".module.css"];
|
|
363
|
+
const skipFiles = options.skipFiles ?? ["theme.css", "tokens.css"];
|
|
364
|
+
const disabled = new Set(options.disableRules ?? []);
|
|
365
|
+
const rules = DEFAULT_RULES.filter((r) => !disabled.has(r.id));
|
|
366
|
+
const violations = [];
|
|
367
|
+
await walkDirectory(rootDir, async (full) => {
|
|
368
|
+
if (!extensions.some((ext) => full.endsWith(ext)))
|
|
369
|
+
return;
|
|
370
|
+
const content = await Bun.file(full).text();
|
|
371
|
+
const lines = content.split(`
|
|
372
|
+
`);
|
|
373
|
+
const hasFileIgnore = lines[0]?.includes("polly-ignore-all");
|
|
374
|
+
if (hasFileIgnore)
|
|
375
|
+
return;
|
|
376
|
+
for (let i = 0;i < lines.length; i += 1) {
|
|
377
|
+
const line = lines[i];
|
|
378
|
+
if (!line)
|
|
379
|
+
continue;
|
|
380
|
+
const trimmed = line.trim();
|
|
381
|
+
if (trimmed === "" || trimmed === "{" || trimmed === "}")
|
|
382
|
+
continue;
|
|
383
|
+
for (const rule of rules) {
|
|
384
|
+
const suggestion = rule.check(line, i, lines);
|
|
385
|
+
if (suggestion) {
|
|
386
|
+
violations.push({
|
|
387
|
+
file: full,
|
|
388
|
+
line: i + 1,
|
|
389
|
+
rule: rule.id,
|
|
390
|
+
content: trimmed,
|
|
391
|
+
suggestion
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}, {
|
|
397
|
+
rootDir,
|
|
398
|
+
skipDirs: options.skipDirs,
|
|
399
|
+
skipFiles
|
|
400
|
+
});
|
|
401
|
+
return makeResult("css-quality", rootDir, violations);
|
|
402
|
+
}
|
|
403
|
+
// tools/quality/src/css/check-unused.ts
|
|
404
|
+
async function checkCssUnused(options) {
|
|
405
|
+
const rootDir = options.rootDir;
|
|
406
|
+
const alwaysUsed = new Set(options.alwaysUsedClasses ?? []);
|
|
407
|
+
const definitions = [];
|
|
408
|
+
const tsContents = [];
|
|
409
|
+
const cssContents = [];
|
|
410
|
+
await walkDirectory(rootDir, async (full) => {
|
|
411
|
+
if (full.endsWith(".module.css")) {
|
|
412
|
+
const content = await Bun.file(full).text();
|
|
413
|
+
const lines = content.split(`
|
|
414
|
+
`);
|
|
415
|
+
for (let i = 0;i < lines.length; i += 1) {
|
|
416
|
+
const line = lines[i];
|
|
417
|
+
if (!line)
|
|
418
|
+
continue;
|
|
419
|
+
for (const m of line.matchAll(/(?:^|[\s,>+~])\.([\w-]+)/g)) {
|
|
420
|
+
if (m[1]) {
|
|
421
|
+
definitions.push({
|
|
422
|
+
file: full,
|
|
423
|
+
name: m[1],
|
|
424
|
+
line: i + 1,
|
|
425
|
+
type: "class"
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const m of line.matchAll(/^\s+--([\w-]+)\s*:/g)) {
|
|
430
|
+
if (m[1]) {
|
|
431
|
+
definitions.push({
|
|
432
|
+
file: full,
|
|
433
|
+
name: `--${m[1]}`,
|
|
434
|
+
line: i + 1,
|
|
435
|
+
type: "variable"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
cssContents.push({ file: full, content });
|
|
441
|
+
} else if (full.endsWith(".css") && !full.endsWith(".css.d.ts")) {
|
|
442
|
+
const content = await Bun.file(full).text();
|
|
443
|
+
cssContents.push({ file: full, content });
|
|
444
|
+
} else if ((full.endsWith(".ts") || full.endsWith(".tsx")) && !full.endsWith(".css.d.ts")) {
|
|
445
|
+
const content = await Bun.file(full).text();
|
|
446
|
+
tsContents.push({ file: full, content });
|
|
447
|
+
}
|
|
448
|
+
}, { rootDir, skipDirs: options.skipDirs });
|
|
449
|
+
const seen = new Set;
|
|
450
|
+
const uniqueDefs = definitions.filter((d) => {
|
|
451
|
+
const key = `${d.file}:${d.type}:${d.name}`;
|
|
452
|
+
if (seen.has(key))
|
|
453
|
+
return false;
|
|
454
|
+
seen.add(key);
|
|
455
|
+
return true;
|
|
456
|
+
});
|
|
457
|
+
function classReferenced(name) {
|
|
458
|
+
if (alwaysUsed.has(name))
|
|
459
|
+
return true;
|
|
460
|
+
for (const { content } of tsContents) {
|
|
461
|
+
if (content.includes(`.${name}`) || content.includes(`['${name}']`) || content.includes(`["${name}"]`)) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
function variableReferenced(name, defFile) {
|
|
468
|
+
for (const { file, content } of cssContents) {
|
|
469
|
+
if (file === defFile)
|
|
470
|
+
continue;
|
|
471
|
+
if (content.includes(`var(${name})`))
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
const self = cssContents.find((f) => f.file === defFile);
|
|
475
|
+
if (self) {
|
|
476
|
+
const occurrences = self.content.split(name).length - 1;
|
|
477
|
+
if (occurrences > 1)
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
for (const { content } of tsContents) {
|
|
481
|
+
if (content.includes(name))
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const violations = [];
|
|
487
|
+
for (const def of uniqueDefs) {
|
|
488
|
+
if (def.type === "class" && !classReferenced(def.name)) {
|
|
489
|
+
violations.push({
|
|
490
|
+
file: def.file,
|
|
491
|
+
line: def.line,
|
|
492
|
+
rule: "unused-class",
|
|
493
|
+
content: `.${def.name}`,
|
|
494
|
+
suggestion: "Delete the selector or reference the class from a component"
|
|
495
|
+
});
|
|
496
|
+
} else if (def.type === "variable" && !variableReferenced(def.name, def.file)) {
|
|
497
|
+
violations.push({
|
|
498
|
+
file: def.file,
|
|
499
|
+
line: def.line,
|
|
500
|
+
rule: "unused-variable",
|
|
501
|
+
content: def.name,
|
|
502
|
+
suggestion: "Delete the definition or reference the variable"
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return makeResult("css-unused (advisory)", rootDir, violations);
|
|
507
|
+
}
|
|
508
|
+
// tools/quality/src/css/check-vars.ts
|
|
509
|
+
async function checkCssVars(options) {
|
|
510
|
+
const rootDir = options.rootDir;
|
|
511
|
+
const scanExts = options.scanExtensions ?? [".ts", ".tsx", ".css"];
|
|
512
|
+
const dynamic = new Set(options.dynamicVars ?? []);
|
|
513
|
+
const definitions = new Set(dynamic);
|
|
514
|
+
const violations = [];
|
|
515
|
+
await walkDirectory(rootDir, async (full) => {
|
|
516
|
+
if (!full.endsWith(".css"))
|
|
517
|
+
return;
|
|
518
|
+
const content = await Bun.file(full).text();
|
|
519
|
+
for (const m of content.matchAll(/--([\w-]+)\s*:/g)) {
|
|
520
|
+
if (m[1])
|
|
521
|
+
definitions.add(`--${m[1]}`);
|
|
522
|
+
}
|
|
523
|
+
}, { rootDir, skipDirs: options.skipDirs });
|
|
524
|
+
await walkDirectory(rootDir, async (full) => {
|
|
525
|
+
if (full.endsWith(".css.d.ts"))
|
|
526
|
+
return;
|
|
527
|
+
if (!scanExts.some((ext) => full.endsWith(ext)))
|
|
528
|
+
return;
|
|
529
|
+
const content = await Bun.file(full).text();
|
|
530
|
+
const lines = content.split(`
|
|
531
|
+
`);
|
|
532
|
+
for (let i = 0;i < lines.length; i += 1) {
|
|
533
|
+
const line = lines[i];
|
|
534
|
+
if (!line)
|
|
535
|
+
continue;
|
|
536
|
+
for (const m of line.matchAll(/var\(--([\w-]+)\)/g)) {
|
|
537
|
+
const name = `--${m[1]}`;
|
|
538
|
+
if (!definitions.has(name)) {
|
|
539
|
+
violations.push({
|
|
540
|
+
file: full,
|
|
541
|
+
line: i + 1,
|
|
542
|
+
rule: "undefined-var",
|
|
543
|
+
content: line.trim(),
|
|
544
|
+
suggestion: `Define ${name} in a CSS file or add to dynamicVars`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}, { rootDir, skipDirs: options.skipDirs });
|
|
550
|
+
return makeResult("css-vars", rootDir, violations);
|
|
551
|
+
}
|
|
552
|
+
// tools/quality/src/no-as-casting.ts
|
|
553
|
+
import { readFileSync } from "node:fs";
|
|
554
|
+
import { Glob } from "bun";
|
|
555
|
+
function isLineClean(line) {
|
|
556
|
+
if (!line.includes(" as "))
|
|
557
|
+
return true;
|
|
558
|
+
const trimmed = line.trim();
|
|
559
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
if (line.match(/\bas const\b/)) {
|
|
563
|
+
const withoutAsConst = line.replace(/\bas const\b/g, "");
|
|
564
|
+
if (!withoutAsConst.includes(" as "))
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
if (line.includes(" as unknown as ") || line.trimEnd().endsWith("as unknown as")) {
|
|
568
|
+
const withoutEscapeHatch = line.replace(/\bas unknown as\b/g, "");
|
|
569
|
+
if (!withoutEscapeHatch.includes(" as "))
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (line.match(/\b(import|export)\s+.*\s+as\s+\w+/) || line.match(/\b(import|export)\s+\*\s+as\s+\w+/) || line.match(/\b(import|export)\s+type\s+.*\s+as\s+\w+/) || line.match(/^\s*\w+\s+as\s+\w+,?\s*$/) || line.match(/^\s*type\s+\w+\s+as\s+\w+,?\s*$/)) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
if (line.match(/\bas\s*[=:,]/))
|
|
576
|
+
return true;
|
|
577
|
+
if (everyAsInsideString(line))
|
|
578
|
+
return true;
|
|
579
|
+
if (isJsxText(trimmed))
|
|
580
|
+
return true;
|
|
581
|
+
if (isPlainText(trimmed))
|
|
582
|
+
return true;
|
|
583
|
+
const commentIdx = line.indexOf("//");
|
|
584
|
+
if (commentIdx >= 0 && line.indexOf(" as ", commentIdx) >= 0) {
|
|
585
|
+
const beforeComment = line.substring(0, commentIdx);
|
|
586
|
+
if (!beforeComment.includes(" as "))
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (line.match(/"\)\s+as\s+\w+"/))
|
|
590
|
+
return true;
|
|
591
|
+
if (line.includes(" satisfies "))
|
|
592
|
+
return true;
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
function everyAsInsideString(line) {
|
|
596
|
+
let searchFrom = 0;
|
|
597
|
+
while (true) {
|
|
598
|
+
const idx = line.indexOf(" as ", searchFrom);
|
|
599
|
+
if (idx < 0)
|
|
600
|
+
return true;
|
|
601
|
+
const before = line.substring(0, idx);
|
|
602
|
+
const singleQuotes = (before.match(/'/g) ?? []).length;
|
|
603
|
+
const doubleQuotes = (before.match(/"/g) ?? []).length;
|
|
604
|
+
const backticks = (before.match(/`/g) ?? []).length;
|
|
605
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
searchFrom = idx + 4;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function isJsxText(trimmed) {
|
|
612
|
+
if (trimmed.match(/^[^{};=()]*\bas\b[^{};=()]*$/)) {
|
|
613
|
+
if (!trimmed.match(/\bas\s+[A-Z]\w*/) && !trimmed.match(/\bas\s+(string|number|boolean|any|unknown|never)\b/)) {
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
function isPlainText(trimmed) {
|
|
620
|
+
const idx = trimmed.indexOf(" as ");
|
|
621
|
+
if (idx < 0)
|
|
622
|
+
return false;
|
|
623
|
+
const before = trimmed.substring(0, idx);
|
|
624
|
+
return !before.match(/[={}:;(]/) && !before.match(/\b(const|let|var|type|interface|function|return|await)\b/);
|
|
625
|
+
}
|
|
626
|
+
function suggestFix(line) {
|
|
627
|
+
if (line.includes("JSON.parse")) {
|
|
628
|
+
return "Use a validation function or type guard to parse and validate the result.";
|
|
629
|
+
}
|
|
630
|
+
if (line.includes("as HTMLInputElement") || line.includes("as HTMLTextAreaElement") || line.includes("as HTMLButtonElement")) {
|
|
631
|
+
return "Use instanceof: if (el instanceof HTMLInputElement) { el.value ... }";
|
|
632
|
+
}
|
|
633
|
+
if (line.includes("as HTMLElement") || line.includes("as Element")) {
|
|
634
|
+
return "Use instanceof: if (el instanceof HTMLElement) { ... }";
|
|
635
|
+
}
|
|
636
|
+
if (line.includes(".doc()") && line.includes("as ")) {
|
|
637
|
+
return "Type the DocHandle generic: repo.find<MyType>(id) returns DocHandle<MyType>.";
|
|
638
|
+
}
|
|
639
|
+
if (line.includes("Record<string, unknown>") && (line.includes("window") || line.includes("globalThis"))) {
|
|
640
|
+
return "Extract a type guard: function getGlobalProp(name: string): unknown { ... }";
|
|
641
|
+
}
|
|
642
|
+
if (line.includes("Record<string, unknown>")) {
|
|
643
|
+
return "Use a type guard function that narrows the unknown value to the target shape.";
|
|
644
|
+
}
|
|
645
|
+
if (line.includes("as PeerId") || line.includes("as DocumentId")) {
|
|
646
|
+
return "Use the library's branded-type constructor if available, or centralise the cast in a factory.";
|
|
647
|
+
}
|
|
648
|
+
if (line.includes("as string") || line.includes("as number") || line.includes("as boolean")) {
|
|
649
|
+
return "Narrow with typeof: if (typeof x === 'string') { ... }";
|
|
650
|
+
}
|
|
651
|
+
if (line.includes("as any")) {
|
|
652
|
+
return "Replace 'any' with 'unknown' and add a type guard or validation at the boundary.";
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
function isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles) {
|
|
657
|
+
const segments = relative2.split("/");
|
|
658
|
+
if (segments.some((s) => excludeDirs.has(s)))
|
|
659
|
+
return true;
|
|
660
|
+
if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s)))
|
|
661
|
+
return true;
|
|
662
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
663
|
+
return excludeFiles.has(basename) || excludeFiles.has(relative2);
|
|
664
|
+
}
|
|
665
|
+
function findViolations(relative2, content) {
|
|
666
|
+
const results = [];
|
|
667
|
+
const lines = content.split(`
|
|
668
|
+
`);
|
|
669
|
+
let insideTemplate = false;
|
|
670
|
+
for (let i = 0;i < lines.length; i++) {
|
|
671
|
+
const line = lines[i] ?? "";
|
|
672
|
+
const backticks = (line.match(/`/g) ?? []).length;
|
|
673
|
+
const startedInTemplate = insideTemplate;
|
|
674
|
+
if (backticks % 2 === 1)
|
|
675
|
+
insideTemplate = !insideTemplate;
|
|
676
|
+
if (startedInTemplate && backticks === 0 && !line.includes("${"))
|
|
677
|
+
continue;
|
|
678
|
+
if (!isLineClean(line)) {
|
|
679
|
+
results.push({
|
|
680
|
+
file: relative2,
|
|
681
|
+
line: i + 1,
|
|
682
|
+
content: line.trim(),
|
|
683
|
+
advice: suggestFix(line.trim())
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return results;
|
|
688
|
+
}
|
|
689
|
+
function printViolations(violations) {
|
|
690
|
+
if (violations.length === 0) {
|
|
691
|
+
logger.log("[no-as-casting] ✅ No violations found.");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
logger.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
695
|
+
`);
|
|
696
|
+
for (const v of violations) {
|
|
697
|
+
logger.log(` ${v.file}:${v.line}`);
|
|
698
|
+
logger.log(` ${v.content}`);
|
|
699
|
+
if (v.advice)
|
|
700
|
+
logger.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
701
|
+
logger.log("");
|
|
702
|
+
}
|
|
703
|
+
logger.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
|
|
704
|
+
logger.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
|
|
705
|
+
}
|
|
706
|
+
async function checkNoAsCasting(options) {
|
|
707
|
+
const rootDir = options.rootDir;
|
|
708
|
+
const excludeDirs = new Set(options.exclude ?? ["node_modules", "dist", ".git", ".bun"]);
|
|
709
|
+
const excludePackages = new Set(options.excludePackages ?? []);
|
|
710
|
+
const excludeFiles = new Set(options.excludeFiles ?? []);
|
|
711
|
+
const pattern = options.filePatterns ?? "**/*.{ts,tsx}";
|
|
712
|
+
const glob = new Glob(pattern);
|
|
713
|
+
const violations = [];
|
|
714
|
+
for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
715
|
+
const relative2 = file.replace(`${rootDir}/`, "");
|
|
716
|
+
if (isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles))
|
|
717
|
+
continue;
|
|
718
|
+
const content = readFileSync(file, "utf-8");
|
|
719
|
+
violations.push(...findViolations(relative2, content));
|
|
720
|
+
}
|
|
721
|
+
return { violations, print: () => printViolations(violations) };
|
|
722
|
+
}
|
|
723
|
+
export {
|
|
724
|
+
suggestFix,
|
|
725
|
+
resetLogger,
|
|
726
|
+
logger,
|
|
727
|
+
isLineClean,
|
|
728
|
+
checkNoAsCasting,
|
|
729
|
+
checkCssVars,
|
|
730
|
+
checkCssUnused,
|
|
731
|
+
checkCssQuality,
|
|
732
|
+
checkCssLayout
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
//# debugId=99E4430007A4308464756E2164756E21
|