cssprobe-cli 0.2.12 → 0.2.14
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/AGENTS.md +4 -5
- package/README.md +1 -1
- package/dist/browser/launcher.d.ts +0 -12
- package/dist/browser/launcher.js +1 -123
- package/dist/cli/program.js +22 -21
- package/dist/collector-bundle.js +30 -3
- package/dist/daemon/daemon.js +4 -23
- package/dist/daemon/daemonEntry.js +13 -0
- package/dist/daemon/session.js +10 -0
- package/dist/engine/analyzer.js +4 -6
- package/dist/engine/collector.js +3 -3
- package/dist/engine/renderers/findings.js +1 -1
- package/dist/engine/specificity.d.ts +6 -0
- package/dist/engine/specificity.js +31 -0
- package/dist/installer/skill-template.js +65 -70
- package/dist/utils/socketConnection.d.ts +0 -1
- package/dist/utils/socketConnection.js +0 -14
- package/package.json +1 -1
- package/dist/daemonEntry.js +0 -1468
package/dist/daemonEntry.js
DELETED
|
@@ -1,1468 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
|
|
29
|
-
// package.json
|
|
30
|
-
var require_package = __commonJS({
|
|
31
|
-
"package.json"(exports2, module2) {
|
|
32
|
-
module2.exports = {
|
|
33
|
-
name: "cssprobe-cli",
|
|
34
|
-
version: "0.2.12",
|
|
35
|
-
description: "Runtime CSS probe \u2014 inspect layout, colors, backgrounds, fonts and more in a live browser. AI agents and shell automation.",
|
|
36
|
-
keywords: [
|
|
37
|
-
"css",
|
|
38
|
-
"inspect",
|
|
39
|
-
"layout",
|
|
40
|
-
"dom",
|
|
41
|
-
"forensics",
|
|
42
|
-
"debug",
|
|
43
|
-
"browser",
|
|
44
|
-
"agent",
|
|
45
|
-
"automation"
|
|
46
|
-
],
|
|
47
|
-
homepage: "https://github.com/mack-peng/cssprobe-cli#readme",
|
|
48
|
-
repository: "github:mack-peng/cssprobe-cli",
|
|
49
|
-
license: "MIT",
|
|
50
|
-
author: "strikingly",
|
|
51
|
-
main: "dist/index.js",
|
|
52
|
-
bin: {
|
|
53
|
-
"cssprobe-cli": "./bin/cssprobe-cli.js"
|
|
54
|
-
},
|
|
55
|
-
files: [
|
|
56
|
-
"bin",
|
|
57
|
-
"dist",
|
|
58
|
-
"docs",
|
|
59
|
-
"README.md",
|
|
60
|
-
"LICENSE",
|
|
61
|
-
"AGENTS.md"
|
|
62
|
-
],
|
|
63
|
-
engines: {
|
|
64
|
-
node: ">=18"
|
|
65
|
-
},
|
|
66
|
-
scripts: {
|
|
67
|
-
build: "node scripts/build.js",
|
|
68
|
-
typecheck: "tsc --noEmit",
|
|
69
|
-
test: "npx tsx --test tests/*.test.ts",
|
|
70
|
-
prepublishOnly: "npm run build"
|
|
71
|
-
},
|
|
72
|
-
dependencies: {
|
|
73
|
-
"playwright-core": "^1.48.0",
|
|
74
|
-
zod: "^3.23.0"
|
|
75
|
-
},
|
|
76
|
-
devDependencies: {
|
|
77
|
-
"@types/node": "^22.0.0",
|
|
78
|
-
esbuild: "^0.24.0",
|
|
79
|
-
tsx: "^4.23.0",
|
|
80
|
-
typescript: "^5.5.0"
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// src/daemon/daemon.ts
|
|
87
|
-
var import_fs2 = __toESM(require("fs"));
|
|
88
|
-
var import_net = __toESM(require("net"));
|
|
89
|
-
var import_path2 = __toESM(require("path"));
|
|
90
|
-
|
|
91
|
-
// src/utils/socketConnection.ts
|
|
92
|
-
var SocketConnection = class {
|
|
93
|
-
_socket;
|
|
94
|
-
_pendingBuffers = [];
|
|
95
|
-
onclose;
|
|
96
|
-
onmessage;
|
|
97
|
-
constructor(socket) {
|
|
98
|
-
this._socket = socket;
|
|
99
|
-
socket.on("data", (buffer) => this._onData(buffer));
|
|
100
|
-
socket.on("close", () => {
|
|
101
|
-
this.onclose?.();
|
|
102
|
-
});
|
|
103
|
-
socket.on("error", (e) => console.error(`error: ${e.message}`));
|
|
104
|
-
}
|
|
105
|
-
async send(message) {
|
|
106
|
-
await new Promise((resolve, reject) => {
|
|
107
|
-
this._socket.write(`${JSON.stringify(message)}
|
|
108
|
-
`, (error) => {
|
|
109
|
-
if (error)
|
|
110
|
-
reject(error);
|
|
111
|
-
else
|
|
112
|
-
resolve(void 0);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
close() {
|
|
117
|
-
this._socket.destroy();
|
|
118
|
-
}
|
|
119
|
-
_onData(buffer) {
|
|
120
|
-
let end = buffer.indexOf("\n");
|
|
121
|
-
if (end === -1) {
|
|
122
|
-
this._pendingBuffers.push(buffer);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
this._pendingBuffers.push(buffer.slice(0, end));
|
|
126
|
-
const message = Buffer.concat(this._pendingBuffers).toString();
|
|
127
|
-
this._dispatchMessage(message);
|
|
128
|
-
let start = end + 1;
|
|
129
|
-
end = buffer.indexOf("\n", start);
|
|
130
|
-
while (end !== -1) {
|
|
131
|
-
const message2 = buffer.toString(void 0, start, end);
|
|
132
|
-
this._dispatchMessage(message2);
|
|
133
|
-
start = end + 1;
|
|
134
|
-
end = buffer.indexOf("\n", start);
|
|
135
|
-
}
|
|
136
|
-
this._pendingBuffers = [buffer.slice(start)];
|
|
137
|
-
}
|
|
138
|
-
_dispatchMessage(message) {
|
|
139
|
-
try {
|
|
140
|
-
this.onmessage?.(JSON.parse(message));
|
|
141
|
-
} catch (e) {
|
|
142
|
-
console.error("failed to dispatch message", e);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// src/browser/launcher.ts
|
|
148
|
-
var import_fs = __toESM(require("fs"));
|
|
149
|
-
var import_path = __toESM(require("path"));
|
|
150
|
-
var import_os = __toESM(require("os"));
|
|
151
|
-
var import_playwright_core = require("playwright-core");
|
|
152
|
-
var COLLECTOR_BUNDLE_PATH = import_path.default.join(__dirname, "..", "collector-bundle.js");
|
|
153
|
-
var bundleContent = null;
|
|
154
|
-
function loadBundle() {
|
|
155
|
-
if (bundleContent !== null) return bundleContent;
|
|
156
|
-
if (!import_fs.default.existsSync(COLLECTOR_BUNDLE_PATH)) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`collector bundle not found at ${COLLECTOR_BUNDLE_PATH}. Run "npm run build" first.`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
bundleContent = import_fs.default.readFileSync(COLLECTOR_BUNDLE_PATH, "utf-8");
|
|
162
|
-
return bundleContent;
|
|
163
|
-
}
|
|
164
|
-
function findBrowserExecutable(engine) {
|
|
165
|
-
const cacheDir = import_path.default.join(import_os.default.homedir(), "Library", "Caches", "ms-playwright");
|
|
166
|
-
if (!import_fs.default.existsSync(cacheDir)) return null;
|
|
167
|
-
const platform = process.platform;
|
|
168
|
-
if (engine === "chromium") {
|
|
169
|
-
const dirs = import_fs.default.readdirSync(cacheDir).filter((d) => d.startsWith("chromium-")).sort((a, b) => {
|
|
170
|
-
const na = parseInt(a.split("-")[1]) || 0;
|
|
171
|
-
const nb = parseInt(b.split("-")[1]) || 0;
|
|
172
|
-
return nb - na;
|
|
173
|
-
});
|
|
174
|
-
for (const dir of dirs) {
|
|
175
|
-
if (platform === "darwin") {
|
|
176
|
-
const app = import_path.default.join(cacheDir, dir, "chrome-mac-arm64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
|
177
|
-
if (import_fs.default.existsSync(app)) return app;
|
|
178
|
-
const appIntel = import_path.default.join(cacheDir, dir, "chrome-mac", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
|
179
|
-
if (import_fs.default.existsSync(appIntel)) return appIntel;
|
|
180
|
-
} else if (platform === "linux") {
|
|
181
|
-
const bin = import_path.default.join(cacheDir, dir, "chrome-linux", "chrome");
|
|
182
|
-
if (import_fs.default.existsSync(bin)) return bin;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
var BrowserLauncher = class {
|
|
189
|
-
constructor(options2 = {}) {
|
|
190
|
-
this.options = options2;
|
|
191
|
-
}
|
|
192
|
-
browser = null;
|
|
193
|
-
context = null;
|
|
194
|
-
page = null;
|
|
195
|
-
async open(url) {
|
|
196
|
-
const engine = this.options.browser || "chromium";
|
|
197
|
-
const factory = engine === "firefox" ? import_playwright_core.firefox : engine === "webkit" ? import_playwright_core.webkit : import_playwright_core.chromium;
|
|
198
|
-
const launchOptions = { headless: !this.options.headed };
|
|
199
|
-
const execPath = findBrowserExecutable(engine);
|
|
200
|
-
if (execPath) {
|
|
201
|
-
launchOptions.executablePath = execPath;
|
|
202
|
-
}
|
|
203
|
-
this.browser = await factory.launch(launchOptions);
|
|
204
|
-
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
205
|
-
const contextOptions = { viewport: vp };
|
|
206
|
-
if (this.options.state && import_fs.default.existsSync(this.options.state)) {
|
|
207
|
-
contextOptions.storageState = this.options.state;
|
|
208
|
-
}
|
|
209
|
-
this.context = await this.browser.newContext(contextOptions);
|
|
210
|
-
this.page = await this.context.newPage();
|
|
211
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
212
|
-
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
213
|
-
} else if (url.startsWith("file://")) {
|
|
214
|
-
await this.page.goto(url, { waitUntil: "load", timeout: 3e4 });
|
|
215
|
-
} else {
|
|
216
|
-
const resolved = import_path.default.resolve(url);
|
|
217
|
-
if (!import_fs.default.existsSync(resolved)) {
|
|
218
|
-
throw new Error(`File not found: ${resolved}`);
|
|
219
|
-
}
|
|
220
|
-
await this.page.goto(`file://${resolved}`, { waitUntil: "load", timeout: 3e4 });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
async collect(cfg) {
|
|
224
|
-
if (!this.page) throw new Error("Browser not opened. Call open() first.");
|
|
225
|
-
const bundle = loadBundle();
|
|
226
|
-
await this.page.addScriptTag({ content: bundle });
|
|
227
|
-
const snapshot = await this.page.evaluate((config) => {
|
|
228
|
-
return window.__cssprobe_cli.collect(config);
|
|
229
|
-
}, cfg);
|
|
230
|
-
return snapshot;
|
|
231
|
-
}
|
|
232
|
-
/** Measure the DOM tree depth from a root selector to the deepest leaf. */
|
|
233
|
-
async measureDepth(selector) {
|
|
234
|
-
if (!this.page) throw new Error("Browser not opened. Call open() first.");
|
|
235
|
-
return this.page.evaluate((sel) => {
|
|
236
|
-
const root = document.querySelector(sel);
|
|
237
|
-
if (!root) return 0;
|
|
238
|
-
let maxDepth = 0;
|
|
239
|
-
const walk = (el, depth) => {
|
|
240
|
-
if (depth > maxDepth) maxDepth = depth;
|
|
241
|
-
for (const child of Array.from(el.children)) walk(child, depth + 1);
|
|
242
|
-
};
|
|
243
|
-
walk(root, 0);
|
|
244
|
-
return maxDepth;
|
|
245
|
-
}, selector);
|
|
246
|
-
}
|
|
247
|
-
/** Wait for user to interact with the page. Press Enter to continue. */
|
|
248
|
-
async waitForUser() {
|
|
249
|
-
console.error("");
|
|
250
|
-
console.error(" Browser is open. Perform your actions (login, click buttons, open dialogs, etc.).");
|
|
251
|
-
console.error(" Press Enter here when ready to inspect...");
|
|
252
|
-
console.error("");
|
|
253
|
-
await new Promise((resolve) => {
|
|
254
|
-
process.stdin.once("data", () => resolve());
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
/** Navigate to a new URL in the existing page. */
|
|
258
|
-
async navigate(url) {
|
|
259
|
-
if (!this.page) throw new Error("Browser not opened. Call open() first.");
|
|
260
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
261
|
-
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
262
|
-
} else if (url.startsWith("file://")) {
|
|
263
|
-
await this.page.goto(url, { waitUntil: "load", timeout: 3e4 });
|
|
264
|
-
} else {
|
|
265
|
-
const resolved = import_path.default.resolve(url);
|
|
266
|
-
if (!import_fs.default.existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
267
|
-
await this.page.goto(`file://${resolved}`, { waitUntil: "load", timeout: 3e4 });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/** Scan the page for likely root elements (modals, dialogs, scroll containers). */
|
|
271
|
-
async autoDetectRoot() {
|
|
272
|
-
if (!this.page) throw new Error("Browser not opened. Call open() first.");
|
|
273
|
-
return this.page.evaluate(() => {
|
|
274
|
-
const candidates = [];
|
|
275
|
-
const seen = /* @__PURE__ */ new Set();
|
|
276
|
-
function addSelector(sel, score) {
|
|
277
|
-
if (seen.has(sel)) return;
|
|
278
|
-
seen.add(sel);
|
|
279
|
-
candidates.push({ selector: sel, score });
|
|
280
|
-
}
|
|
281
|
-
for (const el of Array.from(document.querySelectorAll('.s-kit-modal, [class*="dialog"], [class*="modal"], [class*="popup"]'))) {
|
|
282
|
-
const cls = (el.getAttribute("class") || "").split(/\s+/).filter(Boolean).join(".");
|
|
283
|
-
if (cls) addSelector(`.${cls.split(/\s+/)[0]}`, 10);
|
|
284
|
-
}
|
|
285
|
-
for (const el of Array.from(document.querySelectorAll("*"))) {
|
|
286
|
-
const cs = getComputedStyle(el);
|
|
287
|
-
if ((cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1 && el.clientHeight > 100) {
|
|
288
|
-
const tag = el.tagName.toLowerCase();
|
|
289
|
-
const id = el.id ? `#${el.id}` : "";
|
|
290
|
-
const cls = (el.getAttribute("class") || "").split(/\s+/).filter(Boolean).slice(0, 2).join(".");
|
|
291
|
-
const sel = id || (cls ? `${tag}.${cls}` : tag);
|
|
292
|
-
addSelector(sel, 5);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
for (const el of Array.from(document.querySelectorAll('section, [class*="section"], main, [role="main"]'))) {
|
|
296
|
-
const tag = el.tagName.toLowerCase();
|
|
297
|
-
const cls = (el.getAttribute("class") || "").split(/\s+/).filter(Boolean).slice(0, 2).join(".");
|
|
298
|
-
const sel = cls ? `${tag}.${cls}` : tag;
|
|
299
|
-
addSelector(sel, 3);
|
|
300
|
-
}
|
|
301
|
-
return candidates.sort((a, b) => b.score - a.score).slice(0, 5).map((c) => c.selector);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Interactive login flow: opens browser in headed mode, navigates to URL,
|
|
306
|
-
* waits for user to complete login, then saves storage state.
|
|
307
|
-
* Returns the path to the saved state file.
|
|
308
|
-
*/
|
|
309
|
-
async loginAndSave(url, statePath) {
|
|
310
|
-
const engine = this.options.browser || "chromium";
|
|
311
|
-
const factory = engine === "firefox" ? import_playwright_core.firefox : engine === "webkit" ? import_playwright_core.webkit : import_playwright_core.chromium;
|
|
312
|
-
const execPath = findBrowserExecutable(engine);
|
|
313
|
-
const launchOptions = { headless: false };
|
|
314
|
-
if (execPath) launchOptions.executablePath = execPath;
|
|
315
|
-
this.browser = await factory.launch(launchOptions);
|
|
316
|
-
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
317
|
-
this.context = await this.browser.newContext({ viewport: vp });
|
|
318
|
-
this.page = await this.context.newPage();
|
|
319
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
320
|
-
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
321
|
-
} else {
|
|
322
|
-
throw new Error("--login only works with http/https URLs");
|
|
323
|
-
}
|
|
324
|
-
console.error("");
|
|
325
|
-
console.error(" Browser opened. Please complete the login manually.");
|
|
326
|
-
console.error(" The browser will detect when you navigate away from the login page,");
|
|
327
|
-
console.error(" or you can press Enter here when done.");
|
|
328
|
-
console.error("");
|
|
329
|
-
await Promise.race([
|
|
330
|
-
this.page.waitForURL("**/*", { timeout: 3e5 }).catch(() => {
|
|
331
|
-
}),
|
|
332
|
-
new Promise((resolve) => {
|
|
333
|
-
process.stdin.once("data", () => resolve());
|
|
334
|
-
})
|
|
335
|
-
]);
|
|
336
|
-
await this.page.waitForTimeout(1e3);
|
|
337
|
-
const resolvedPath = import_path.default.resolve(statePath);
|
|
338
|
-
const dir = import_path.default.dirname(resolvedPath);
|
|
339
|
-
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
340
|
-
await this.context.storageState({ path: resolvedPath });
|
|
341
|
-
console.error(` State saved to: ${resolvedPath}`);
|
|
342
|
-
return resolvedPath;
|
|
343
|
-
}
|
|
344
|
-
/** Get the current page object. */
|
|
345
|
-
getPage() {
|
|
346
|
-
return this.page;
|
|
347
|
-
}
|
|
348
|
-
async close() {
|
|
349
|
-
if (this.context) await this.context.close().catch(() => {
|
|
350
|
-
});
|
|
351
|
-
if (this.browser) await this.browser.close().catch(() => {
|
|
352
|
-
});
|
|
353
|
-
this.context = null;
|
|
354
|
-
this.browser = null;
|
|
355
|
-
this.page = null;
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
// src/engine/analyzer.ts
|
|
360
|
-
function declaredFor(node, prop) {
|
|
361
|
-
const arr = node.declared && node.declared[prop];
|
|
362
|
-
if (!arr || arr.length === 0) return null;
|
|
363
|
-
return arr.find((d) => d.selector === "inline") || arr[0];
|
|
364
|
-
}
|
|
365
|
-
function confidenceFor(node, props) {
|
|
366
|
-
let sawAccessible = false;
|
|
367
|
-
let sawPercent = false;
|
|
368
|
-
for (const prop of props) {
|
|
369
|
-
const d = declaredFor(node, prop);
|
|
370
|
-
if (!d) continue;
|
|
371
|
-
if (!d.accessible) return "UNVERIFIABLE";
|
|
372
|
-
sawAccessible = true;
|
|
373
|
-
if (d.value.endsWith("%")) sawPercent = true;
|
|
374
|
-
}
|
|
375
|
-
if (!sawAccessible) return "UNVERIFIABLE";
|
|
376
|
-
return sawPercent ? "INDEFINITE" : "DEFINITE";
|
|
377
|
-
}
|
|
378
|
-
function evDeclared(d) {
|
|
379
|
-
return {
|
|
380
|
-
type: "declared",
|
|
381
|
-
property: "height",
|
|
382
|
-
value: d.value,
|
|
383
|
-
source: `${d.selector} (${d.href || "inline"})`,
|
|
384
|
-
accessible: d.accessible
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
function evComputed(node, prop) {
|
|
388
|
-
return {
|
|
389
|
-
type: "computed",
|
|
390
|
-
property: prop,
|
|
391
|
-
value: node.props[prop] ?? ""
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
var ABS_UNIT = /^(0|[1-9]\d*)(\.\d+)?(px|pt|cm|mm|in|vh|vw|vmin|vmax|rem|em)$/;
|
|
395
|
-
function nodeLabel(n) {
|
|
396
|
-
const cls = (n.classes || []).join(".");
|
|
397
|
-
return `${n.tag}.${cls}`;
|
|
398
|
-
}
|
|
399
|
-
function makeFinding(partial) {
|
|
400
|
-
return {
|
|
401
|
-
id: partial.id,
|
|
402
|
-
level: partial.level,
|
|
403
|
-
message: partial.message,
|
|
404
|
-
confidence: partial.confidence ?? "UNVERIFIABLE",
|
|
405
|
-
evidence: partial.evidence ?? [],
|
|
406
|
-
...partial.location ? { location: partial.location } : {}
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
function allAncestorsContentSized(ancestors) {
|
|
410
|
-
return ancestors.length > 0 && ancestors.every((a) => a.shape.heightStrategy === "content");
|
|
411
|
-
}
|
|
412
|
-
function walkTree(root) {
|
|
413
|
-
const scrollNodes = [];
|
|
414
|
-
const constraintNodes = [];
|
|
415
|
-
const flexPairs = [];
|
|
416
|
-
const cbNodes = [];
|
|
417
|
-
const walk = (n, parent) => {
|
|
418
|
-
if (!n) return;
|
|
419
|
-
if (n.flags.hasScrollY || n.props.overflowY === "auto" || n.props.overflowY === "scroll") {
|
|
420
|
-
scrollNodes.push(n);
|
|
421
|
-
}
|
|
422
|
-
const parentHealthy = parent && parent.metrics.clientHeight > 0;
|
|
423
|
-
if (n.flags.overflowsParent && !(n.props.overflowY === "auto" || n.props.overflowY === "scroll") && parentHealthy) {
|
|
424
|
-
constraintNodes.push(n);
|
|
425
|
-
}
|
|
426
|
-
if (parent && (parent.props.display === "flex" || parent.props.display === "inline-flex")) {
|
|
427
|
-
flexPairs.push({ parent, child: n });
|
|
428
|
-
}
|
|
429
|
-
if (n.containingBlockModifiers && n.containingBlockModifiers.length > 0) {
|
|
430
|
-
cbNodes.push(n);
|
|
431
|
-
}
|
|
432
|
-
for (const child of n.children) walk(child, n);
|
|
433
|
-
};
|
|
434
|
-
walk(root, null);
|
|
435
|
-
if (root && root.flags.overflowsParent && !(root.props.overflowY === "auto" || root.props.overflowY === "scroll")) {
|
|
436
|
-
constraintNodes.push(root);
|
|
437
|
-
}
|
|
438
|
-
return { scrollNodes, constraintNodes, flexPairs, cbNodes };
|
|
439
|
-
}
|
|
440
|
-
function analyzeAnchors(snapshot) {
|
|
441
|
-
const findings = [];
|
|
442
|
-
const ancestors = snapshot.ancestors;
|
|
443
|
-
const anchorInfo = ancestors.map((a) => {
|
|
444
|
-
const dh = declaredFor(a, "height");
|
|
445
|
-
const mh = declaredFor(a, "max-height");
|
|
446
|
-
return {
|
|
447
|
-
node: a,
|
|
448
|
-
heightDeclared: dh,
|
|
449
|
-
maxHeightDeclared: mh,
|
|
450
|
-
computedHeight: a.props.height,
|
|
451
|
-
hasAbsolute: !!(dh && ABS_UNIT.test(dh.value)),
|
|
452
|
-
hasPercent: !!(dh && dh.value.endsWith("%"))
|
|
453
|
-
};
|
|
454
|
-
});
|
|
455
|
-
const anchored = anchorInfo.find((x) => x.hasAbsolute);
|
|
456
|
-
const message = anchored ? `chain has absolute-unit height at ${anchored.node.label}: ${anchored.heightDeclared.value}` : "all heights are % or missing \u2192 chain depends on containing block runtime resolution (may resolve to auto)";
|
|
457
|
-
const anchorConf = anchored ? confidenceFor(anchored.node, ["height"]) : (() => {
|
|
458
|
-
const confs = anchorInfo.map((x) => x.heightDeclared ? x.heightDeclared.accessible ? x.hasPercent ? "INDEFINITE" : "DEFINITE" : "UNVERIFIABLE" : "UNVERIFIABLE");
|
|
459
|
-
return confs.includes("UNVERIFIABLE") ? "UNVERIFIABLE" : confs.includes("INDEFINITE") ? "INDEFINITE" : "DEFINITE";
|
|
460
|
-
})();
|
|
461
|
-
findings.push(makeFinding({
|
|
462
|
-
id: anchored ? "anchor-present" : "anchor-missing",
|
|
463
|
-
level: anchored ? "info" : "warning",
|
|
464
|
-
message,
|
|
465
|
-
confidence: anchorConf,
|
|
466
|
-
evidence: anchorInfo.slice(0, 6).flatMap((x) => {
|
|
467
|
-
const ev = [];
|
|
468
|
-
if (x.heightDeclared) ev.push({ type: "declared", property: "height", value: x.heightDeclared.value, source: `${x.heightDeclared.selector} (${x.heightDeclared.href || "inline"})`, accessible: x.heightDeclared.accessible });
|
|
469
|
-
ev.push({ type: "computed", property: "height", value: x.computedHeight });
|
|
470
|
-
return ev;
|
|
471
|
-
}),
|
|
472
|
-
location: anchored ? anchored.node.label : ancestors[ancestors.length - 1]?.label
|
|
473
|
-
}));
|
|
474
|
-
const maxOnly = anchorInfo.filter((x) => !x.heightDeclared && x.maxHeightDeclared);
|
|
475
|
-
if (maxOnly.length) {
|
|
476
|
-
const evs = maxOnly.slice(0, 3).flatMap((x) => x.maxHeightDeclared ? [evDeclared(x.maxHeightDeclared)] : []);
|
|
477
|
-
findings.push(makeFinding({
|
|
478
|
-
id: "max-height-only",
|
|
479
|
-
level: "warning",
|
|
480
|
-
message: `these nodes only declare max-height (cap, not anchor) \u2192 % children still resolve to auto: ${maxOnly.map((x) => x.node.label).join(", ")}`,
|
|
481
|
-
confidence: "DEFINITE",
|
|
482
|
-
evidence: evs,
|
|
483
|
-
location: maxOnly[0].node.label
|
|
484
|
-
}));
|
|
485
|
-
}
|
|
486
|
-
return findings;
|
|
487
|
-
}
|
|
488
|
-
function analyzeScroll(snapshot) {
|
|
489
|
-
const findings = [];
|
|
490
|
-
const { scrollNodes, constraintNodes } = walkTree(snapshot.tree);
|
|
491
|
-
for (const n of constraintNodes.slice(0, 8)) {
|
|
492
|
-
findings.push(makeFinding({
|
|
493
|
-
id: "constraint-candidate",
|
|
494
|
-
level: "warning",
|
|
495
|
-
message: `${nodeLabel(n)} overflows parent but is not a scroll container \u2192 constraint candidate (flex child min-height:auto or parent height unconstrained)`,
|
|
496
|
-
confidence: "DEFINITE",
|
|
497
|
-
evidence: [
|
|
498
|
-
evComputed(n, "overflowY"),
|
|
499
|
-
{ type: "computed", property: "scrollHeight", value: String(n.metrics.scrollHeight) },
|
|
500
|
-
{ type: "computed", property: "clientHeight", value: String(n.metrics.clientHeight) }
|
|
501
|
-
],
|
|
502
|
-
location: nodeLabel(n)
|
|
503
|
-
}));
|
|
504
|
-
}
|
|
505
|
-
for (const n of scrollNodes) {
|
|
506
|
-
const isScrollDeclared = n.props.overflowY === "auto" || n.props.overflowY === "scroll";
|
|
507
|
-
const conf = "DEFINITE";
|
|
508
|
-
let id;
|
|
509
|
-
let message;
|
|
510
|
-
let level;
|
|
511
|
-
if (n.metrics.clientHeight === 0) {
|
|
512
|
-
id = "scroll-collapsed";
|
|
513
|
-
message = `${nodeLabel(n)} height collapsed to 0 \u2192 overflow-y:${n.props.overflowY} has no viewport \u2192 height chain unconstrained (anchor issue)`;
|
|
514
|
-
level = "error";
|
|
515
|
-
} else if (n.flags.scrollable) {
|
|
516
|
-
id = "scrollable";
|
|
517
|
-
message = `${nodeLabel(n)} scrollable (scrollHeight ${n.metrics.scrollHeight} > clientHeight ${n.metrics.clientHeight})`;
|
|
518
|
-
level = "info";
|
|
519
|
-
} else if (!n.flags.hasScrollY && isScrollDeclared && Math.abs(n.metrics.offsetHeight - n.metrics.scrollHeight) <= 1) {
|
|
520
|
-
id = "scroll-content-sized";
|
|
521
|
-
message = `${nodeLabel(n)} content-sized (${n.metrics.offsetHeight}px, scrollHeight==clientHeight) \u2192 overflow-y:auto never triggers \u2192 height chain unconstrained (anchor issue)`;
|
|
522
|
-
level = "warning";
|
|
523
|
-
} else if (!n.flags.hasScrollY) {
|
|
524
|
-
id = "no-overflow";
|
|
525
|
-
message = `${nodeLabel(n)} no overflow, no scroll needed`;
|
|
526
|
-
level = "info";
|
|
527
|
-
} else {
|
|
528
|
-
id = "overflow-clipped";
|
|
529
|
-
message = `${nodeLabel(n)} content overflows (${n.metrics.scrollHeight}>${n.metrics.clientHeight}) clipped by overflow=${n.props.overflowY} \u2192 height chain unconstrained (anchor issue)`;
|
|
530
|
-
level = "warning";
|
|
531
|
-
}
|
|
532
|
-
findings.push(makeFinding({
|
|
533
|
-
id,
|
|
534
|
-
level,
|
|
535
|
-
message,
|
|
536
|
-
confidence: conf,
|
|
537
|
-
evidence: [
|
|
538
|
-
evComputed(n, "overflowY"),
|
|
539
|
-
{ type: "computed", property: "scrollHeight", value: String(n.metrics.scrollHeight) },
|
|
540
|
-
{ type: "computed", property: "clientHeight", value: String(n.metrics.clientHeight) }
|
|
541
|
-
],
|
|
542
|
-
location: nodeLabel(n)
|
|
543
|
-
}));
|
|
544
|
-
}
|
|
545
|
-
return findings;
|
|
546
|
-
}
|
|
547
|
-
function analyzeFlex(snapshot) {
|
|
548
|
-
const findings = [];
|
|
549
|
-
const { flexPairs } = walkTree(snapshot.tree);
|
|
550
|
-
if (flexPairs.length === 0) {
|
|
551
|
-
findings.push(makeFinding({
|
|
552
|
-
id: "no-flex",
|
|
553
|
-
level: "info",
|
|
554
|
-
message: "no flex parent-child relationships found in the tree",
|
|
555
|
-
confidence: "DEFINITE"
|
|
556
|
-
}));
|
|
557
|
-
return findings;
|
|
558
|
-
}
|
|
559
|
-
for (const { parent, child } of flexPairs.slice(0, 10)) {
|
|
560
|
-
const pLabel = nodeLabel(parent);
|
|
561
|
-
const cLabel = nodeLabel(child);
|
|
562
|
-
const pDir = parent.props.flexDirection || "row";
|
|
563
|
-
const cMinH = parent.props.minHeight;
|
|
564
|
-
const cMinW = child.props.minWidth;
|
|
565
|
-
const cFlexGrow = child.props.flexGrow;
|
|
566
|
-
const cFlexShrink = child.props.flexShrink;
|
|
567
|
-
if (pDir === "column") {
|
|
568
|
-
if ((cMinH === "auto" || cMinH === "0px") && child.metrics.scrollHeight > child.metrics.clientHeight + 1) {
|
|
569
|
-
findings.push(makeFinding({
|
|
570
|
-
id: "flex-col-overflow",
|
|
571
|
-
level: "warning",
|
|
572
|
-
message: `${cLabel} is flex-col child with min-height:${cMinH} \u2192 content (${child.metrics.scrollHeight}px) may overflow parent (${parent.metrics.clientHeight}px)`,
|
|
573
|
-
confidence: "DEFINITE",
|
|
574
|
-
evidence: [
|
|
575
|
-
evComputed(parent, "display"),
|
|
576
|
-
evComputed(parent, "flexDirection"),
|
|
577
|
-
evComputed(child, "minHeight"),
|
|
578
|
-
{ type: "computed", property: "scrollHeight", value: String(child.metrics.scrollHeight) },
|
|
579
|
-
{ type: "computed", property: "clientHeight", value: String(child.metrics.clientHeight) }
|
|
580
|
-
],
|
|
581
|
-
location: cLabel
|
|
582
|
-
}));
|
|
583
|
-
}
|
|
584
|
-
if (cFlexGrow === "0" && cFlexShrink === "0" && cMinH === "auto") {
|
|
585
|
-
findings.push(makeFinding({
|
|
586
|
-
id: "flex-zero-auto",
|
|
587
|
-
level: "info",
|
|
588
|
-
message: `${cLabel}: flex:0 0 auto in ${pLabel}(column) \u2192 height is content-driven, not constrained by flex`,
|
|
589
|
-
confidence: "DEFINITE",
|
|
590
|
-
evidence: [evComputed(child, "flexGrow"), evComputed(child, "flexShrink"), evComputed(child, "minHeight")],
|
|
591
|
-
location: cLabel
|
|
592
|
-
}));
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
if ((cMinW === "auto" || cMinW === "0px") && child.metrics.scrollWidth > child.metrics.clientWidth + 1) {
|
|
596
|
-
findings.push(makeFinding({
|
|
597
|
-
id: "flex-row-overflow",
|
|
598
|
-
level: "warning",
|
|
599
|
-
message: `${cLabel} is flex-row child with min-width:${cMinW} \u2192 content (${child.metrics.scrollWidth}px) may overflow parent`,
|
|
600
|
-
confidence: "DEFINITE",
|
|
601
|
-
evidence: [evComputed(parent, "display"), evComputed(child, "minWidth"), evComputed(child, "overflowX")],
|
|
602
|
-
location: cLabel
|
|
603
|
-
}));
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return findings;
|
|
608
|
-
}
|
|
609
|
-
function analyzeContainingBlock(snapshot) {
|
|
610
|
-
const findings = [];
|
|
611
|
-
const { cbNodes } = walkTree(snapshot.tree);
|
|
612
|
-
const ancestorsWithCB = snapshot.ancestors.filter((a) => a.containingBlockModifiers.length > 0);
|
|
613
|
-
const allCB = [
|
|
614
|
-
...cbNodes.slice(0, 5).map((n) => ({ label: nodeLabel(n), mods: n.containingBlockModifiers, node: n })),
|
|
615
|
-
...ancestorsWithCB.map((a) => ({ label: a.label, mods: a.containingBlockModifiers, node: a }))
|
|
616
|
-
];
|
|
617
|
-
if (allCB.length === 0) {
|
|
618
|
-
findings.push(makeFinding({
|
|
619
|
-
id: "no-cb",
|
|
620
|
-
level: "info",
|
|
621
|
-
message: "no containing block modifiers found in the tree",
|
|
622
|
-
confidence: "DEFINITE"
|
|
623
|
-
}));
|
|
624
|
-
return findings;
|
|
625
|
-
}
|
|
626
|
-
const hasFixed = snapshot.ancestors.some((a) => a.shape.role === "fixed") || snapshot.tree && snapshot.tree.props.position === "fixed";
|
|
627
|
-
for (const cb of allCB.slice(0, 5)) {
|
|
628
|
-
const isRootFixed = cb.node.props.position === "fixed";
|
|
629
|
-
findings.push(makeFinding({
|
|
630
|
-
id: "cb-modifier",
|
|
631
|
-
level: isRootFixed ? "error" : "info",
|
|
632
|
-
message: `${cb.label}: CB modifiers: ${cb.mods.join(", ")}${isRootFixed ? " \u2192 fixed/absolute descendants resolve % against this ancestor (containing block hijack)" : ""}`,
|
|
633
|
-
confidence: "DEFINITE",
|
|
634
|
-
evidence: cb.mods.map((m) => ({ type: "declared", property: m.split(":")[0].trim(), value: m.split(":").slice(1).join(":").trim(), accessible: true })),
|
|
635
|
-
location: cb.label
|
|
636
|
-
}));
|
|
637
|
-
}
|
|
638
|
-
if (hasFixed && snapshot.ancestors.every((a) => a.shape.heightStrategy === "content")) {
|
|
639
|
-
findings.push(makeFinding({
|
|
640
|
-
id: "fixed-no-anchor",
|
|
641
|
-
level: "error",
|
|
642
|
-
message: "fixed ancestor + no height anchor \u2192 children % resolve to auto \u2192 content-sized overflow",
|
|
643
|
-
confidence: "DEFINITE"
|
|
644
|
-
}));
|
|
645
|
-
}
|
|
646
|
-
return findings;
|
|
647
|
-
}
|
|
648
|
-
function detectPatterns(snapshot) {
|
|
649
|
-
const findings = [];
|
|
650
|
-
const ancestors = snapshot.ancestors;
|
|
651
|
-
const { scrollNodes } = walkTree(snapshot.tree);
|
|
652
|
-
const anchored = ancestors.some((a) => {
|
|
653
|
-
const dh = declaredFor(a, "height");
|
|
654
|
-
return !!(dh && ABS_UNIT.test(dh.value));
|
|
655
|
-
});
|
|
656
|
-
const hasFlexCol = ancestors.some((a) => a.shape.role === "flex-col");
|
|
657
|
-
const hasFixed = ancestors.some((a) => a.shape.role === "fixed");
|
|
658
|
-
const scrollableExists = scrollNodes.some((n) => n.flags.scrollable);
|
|
659
|
-
if (hasFixed && !anchored) {
|
|
660
|
-
findings.push(makeFinding({
|
|
661
|
-
id: "pattern-fixed-no-anchor",
|
|
662
|
-
level: "info",
|
|
663
|
-
message: "Pattern: fixed ancestor + no height anchor \u2192 children % resolve to auto \u2192 content-sized overflow",
|
|
664
|
-
confidence: "DEFINITE"
|
|
665
|
-
}));
|
|
666
|
-
}
|
|
667
|
-
if (hasFlexCol) {
|
|
668
|
-
const fc = ancestors.find((a) => a.shape.role === "flex-col");
|
|
669
|
-
findings.push(makeFinding({
|
|
670
|
-
id: "pattern-flex-col",
|
|
671
|
-
level: "info",
|
|
672
|
-
message: `Pattern: flex-col at ${fc.label} \u2192 children height governed by flex-grow/shrink/min-height`,
|
|
673
|
-
confidence: "DEFINITE"
|
|
674
|
-
}));
|
|
675
|
-
}
|
|
676
|
-
if (scrollableExists && !anchored) {
|
|
677
|
-
findings.push(makeFinding({
|
|
678
|
-
id: "pattern-scroll-no-anchor",
|
|
679
|
-
level: "info",
|
|
680
|
-
message: "Pattern: scroll container exists but no height anchor above \u2192 overflow:auto never triggers (content-sized)",
|
|
681
|
-
confidence: "DEFINITE"
|
|
682
|
-
}));
|
|
683
|
-
}
|
|
684
|
-
if (allAncestorsContentSized(ancestors)) {
|
|
685
|
-
findings.push(makeFinding({
|
|
686
|
-
id: "pattern-content-sized",
|
|
687
|
-
level: "info",
|
|
688
|
-
message: "Pattern: all ancestors are content-sized \u2192 height entirely depends on content, no constraints propagate",
|
|
689
|
-
confidence: "DEFINITE"
|
|
690
|
-
}));
|
|
691
|
-
}
|
|
692
|
-
return findings;
|
|
693
|
-
}
|
|
694
|
-
function analyze(snapshot) {
|
|
695
|
-
const findings = [
|
|
696
|
-
...analyzeAnchors(snapshot),
|
|
697
|
-
...analyzeScroll(snapshot),
|
|
698
|
-
...analyzeFlex(snapshot),
|
|
699
|
-
...analyzeContainingBlock(snapshot),
|
|
700
|
-
...detectPatterns(snapshot)
|
|
701
|
-
];
|
|
702
|
-
return findings;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// src/engine/renderers/utils.ts
|
|
706
|
-
function nodeLabel2(node) {
|
|
707
|
-
const cls = node.classes.length ? `.${node.classes.join(".")}` : "";
|
|
708
|
-
const id = node.id ? `#${node.id}` : "";
|
|
709
|
-
return `${node.tag}${id}${cls}`;
|
|
710
|
-
}
|
|
711
|
-
function smartLabel(node, maxLen) {
|
|
712
|
-
const full = nodeLabel2(node);
|
|
713
|
-
if (full.length <= maxLen) return full;
|
|
714
|
-
const id = node.id ? `#${node.id}` : "";
|
|
715
|
-
const firstCls = node.classes.length > 0 ? `.${node.classes[0]}` : "";
|
|
716
|
-
const short = `${node.tag}${id}${firstCls}`;
|
|
717
|
-
if (short.length <= maxLen) return short;
|
|
718
|
-
const tagId = `${node.tag}${id}`;
|
|
719
|
-
if (tagId.length <= maxLen) return tagId;
|
|
720
|
-
if (node.tag.length <= maxLen) return node.tag;
|
|
721
|
-
return node.tag.slice(0, Math.max(maxLen - 3, 1)) + "...";
|
|
722
|
-
}
|
|
723
|
-
function isMultiColumn(node) {
|
|
724
|
-
const display = node.props.display;
|
|
725
|
-
return node.shape?.role === "flex-row" || display === "flex" && node.props.flexDirection !== "column" || display === "grid" || display === "inline-grid";
|
|
726
|
-
}
|
|
727
|
-
var BOX = { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", h: "\u2500", v: "\u2502" };
|
|
728
|
-
var CONF_BADGE = {
|
|
729
|
-
DEFINITE: "DEFINITE",
|
|
730
|
-
INDEFINITE: "INDEFINITE",
|
|
731
|
-
UNVERIFIABLE: "UNVERIFIABLE"
|
|
732
|
-
};
|
|
733
|
-
function confidenceSummary(findings) {
|
|
734
|
-
const counts = findings.reduce((acc, f) => {
|
|
735
|
-
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
736
|
-
return acc;
|
|
737
|
-
}, {});
|
|
738
|
-
const parts = [
|
|
739
|
-
`DEFINITE ${counts.DEFINITE || 0}`,
|
|
740
|
-
`INDEFINITE ${counts.INDEFINITE || 0}`,
|
|
741
|
-
`UNVERIFIABLE ${counts.UNVERIFIABLE || 0}`
|
|
742
|
-
];
|
|
743
|
-
return parts.join(" | ");
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// src/engine/renderers/dom-tree.ts
|
|
747
|
-
function renderNode(node, indent) {
|
|
748
|
-
const lines = [];
|
|
749
|
-
if (!node) return lines;
|
|
750
|
-
const cls = node.classes.length ? `.${node.classes.join(".")}` : "";
|
|
751
|
-
const id = node.id ? `#${node.id}` : "";
|
|
752
|
-
const repeat = node.repeat ? ` \xD7${node.repeat}` : "";
|
|
753
|
-
const m = node.metrics;
|
|
754
|
-
const s = node.shape || {};
|
|
755
|
-
const flag = [];
|
|
756
|
-
if (node.flags.overflowsViewport) flag.push("\u26A0viewport-overflow");
|
|
757
|
-
if (node.flags.overflowsParent) flag.push("\u26A0parent-overflow");
|
|
758
|
-
if (m.clientHeight === 0 && m.offsetHeight === 0) flag.push("\u26A0collapsed(0px)");
|
|
759
|
-
if (node.flags.scrollable) flag.push("\u2714scrollable");
|
|
760
|
-
if (node.flags.hasScrollY && !node.flags.scrollable) {
|
|
761
|
-
flag.push(`\u26A0overflow(${m.scrollHeight}>${m.clientHeight})but-overflow=${node.props.overflowY}`);
|
|
762
|
-
}
|
|
763
|
-
if (node.containingBlockModifiers.length) {
|
|
764
|
-
flag.push(`CB:${node.containingBlockModifiers.join(" ")}`);
|
|
765
|
-
}
|
|
766
|
-
const styleHint = node.inlineStyle ? ` style="${node.inlineStyle}"` : "";
|
|
767
|
-
const h = node.props.height;
|
|
768
|
-
const heightStr = h === "auto" ? `auto\u2192${m.offsetHeight}px` : `${h}`;
|
|
769
|
-
const w = node.props.width;
|
|
770
|
-
const widthStr = w === "auto" ? `auto\u2192${m.clientWidth}px` : `${w}`;
|
|
771
|
-
const mhStr = node.declared && node.declared["max-height"] ? ` maxH:${node.declared["max-height"].map((x) => x.value).join(";")}` : "";
|
|
772
|
-
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? " " + s.scrollTag : ""} h:${s.heightStrategy} w:${s.widthStrategy}]` : "";
|
|
773
|
-
lines.push(
|
|
774
|
-
`${" ".repeat(indent)}<${node.tag}${id}${cls}${styleHint}${repeat}> ${shapeStr} [${node.props.position},${node.props.display},h:${heightStr},w:${widthStr}${mhStr}] rect(${m.rect.width}\xD7${m.rect.height}) bottom=${m.rectBottom} scroll=${m.clientHeight}/${m.scrollHeight}` + (flag.length ? ` ${flag.join(" ")}` : "") + (node.text ? ` \u300C${node.text.slice(0, 30)}\u300D` : "")
|
|
775
|
-
);
|
|
776
|
-
for (const child of node.children) {
|
|
777
|
-
lines.push(...renderNode(child, indent + 1));
|
|
778
|
-
}
|
|
779
|
-
return lines;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// src/engine/renderers/ancestors.ts
|
|
783
|
-
function renderAncestors(chain) {
|
|
784
|
-
return chain.map((a) => {
|
|
785
|
-
const dh = a.declared && a.declared.height ? a.declared.height.map((x) => `${x.selector}\u2192${x.value}`).join("; ") : "auto";
|
|
786
|
-
const dw = a.declared && a.declared.width ? a.declared.width.map((x) => `${x.selector}\u2192${x.value}`).join("; ") : "auto";
|
|
787
|
-
const mh = a.declared && a.declared["max-height"] ? a.declared["max-height"].map((x) => `${x.selector}\u2192${x.value}`).join("; ") : null;
|
|
788
|
-
const maxPart = mh ? ` max-height:${mh}` : "";
|
|
789
|
-
const s = a.shape || {};
|
|
790
|
-
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? " " + s.scrollTag : ""} h:${s.heightStrategy} w:${s.widthStrategy}]` : "";
|
|
791
|
-
return `${a.label} ${shapeStr} [${a.props.position},${a.props.display}] h:${dh}${maxPart} w:${dw} \u2192 ${a.metrics.clientWidth}\xD7${a.metrics.offsetHeight}` + (a.containingBlockModifiers.length ? ` CB:${a.containingBlockModifiers.join(" ")}` : "") + (a.inlineStyle ? ` style="${a.inlineStyle}"` : "");
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// src/engine/renderers/sketch.ts
|
|
796
|
-
function buildSketchTree(node, issueLabels) {
|
|
797
|
-
if (!node) return null;
|
|
798
|
-
const label = nodeLabel2(node);
|
|
799
|
-
const s = node.shape || {};
|
|
800
|
-
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : "";
|
|
801
|
-
const flags = [];
|
|
802
|
-
if (node.flags.overflowsParent) flags.push("\u26A0parent-overflow");
|
|
803
|
-
if (node.flags.overflowsViewport) flags.push("\u26A0viewport-overflow");
|
|
804
|
-
if (node.metrics.clientHeight === 0 && node.metrics.offsetHeight === 0) flags.push("\u26A0collapsed");
|
|
805
|
-
if (node.flags.scrollable) flags.push("\u2714scrollable");
|
|
806
|
-
if (node.containingBlockModifiers.length) flags.push(`CB:${node.containingBlockModifiers.join(" ")}`);
|
|
807
|
-
const hasIssue = issueLabels.has(label) || flags.length > 0;
|
|
808
|
-
const children = [];
|
|
809
|
-
let omitted = 0;
|
|
810
|
-
for (const child of node.children) {
|
|
811
|
-
const sketchChild = buildSketchTree(child, issueLabels);
|
|
812
|
-
if (!sketchChild) continue;
|
|
813
|
-
if (sketchChild.hasIssue || hasDescendantIssue(sketchChild)) {
|
|
814
|
-
children.push(sketchChild);
|
|
815
|
-
} else {
|
|
816
|
-
omitted++;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
return { label, shape: shapeStr, flags, children, hasIssue, omitted };
|
|
820
|
-
}
|
|
821
|
-
function hasDescendantIssue(node) {
|
|
822
|
-
if (node.hasIssue) return true;
|
|
823
|
-
return node.children.some((c) => hasDescendantIssue(c));
|
|
824
|
-
}
|
|
825
|
-
function renderSketchNode(node, prefix, isLast) {
|
|
826
|
-
const lines = [];
|
|
827
|
-
const connector = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
828
|
-
const flagStr = node.flags.length ? ` ${node.flags.join(" ")}` : "";
|
|
829
|
-
const issueMark = node.hasIssue && node.flags.length === 0 ? " \u26A0" : "";
|
|
830
|
-
lines.push(`${prefix}${connector}${node.label} ${node.shape}${flagStr}${issueMark}`);
|
|
831
|
-
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
832
|
-
const totalChildren = node.children.length + (node.omitted > 0 ? 1 : 0);
|
|
833
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
834
|
-
lines.push(...renderSketchNode(node.children[i], childPrefix, i === totalChildren - 1));
|
|
835
|
-
}
|
|
836
|
-
if (node.omitted > 0) {
|
|
837
|
-
const omConnector = "\u2514\u2500 ";
|
|
838
|
-
lines.push(`${childPrefix}${omConnector}... (${node.omitted} subtrees omitted)`);
|
|
839
|
-
}
|
|
840
|
-
return lines;
|
|
841
|
-
}
|
|
842
|
-
function renderSketch(tree, findings) {
|
|
843
|
-
if (!tree) return ["(empty tree)"];
|
|
844
|
-
const issueLabels = /* @__PURE__ */ new Set();
|
|
845
|
-
for (const f of findings) {
|
|
846
|
-
if (f.level === "warning" || f.level === "error") {
|
|
847
|
-
if (f.location) issueLabels.add(f.location);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
const sketchRoot = buildSketchTree(tree, issueLabels);
|
|
851
|
-
if (!sketchRoot) return ["(empty tree)"];
|
|
852
|
-
if (!hasDescendantIssue(sketchRoot) && !sketchRoot.hasIssue) {
|
|
853
|
-
return [`${sketchRoot.label} ${sketchRoot.shape} (no issues found in tree)`];
|
|
854
|
-
}
|
|
855
|
-
return renderSketchNode(sketchRoot, "", true);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// src/engine/renderers/layout.ts
|
|
859
|
-
function renderLayout(snapshot, findings) {
|
|
860
|
-
if (!snapshot.tree) return ["(empty tree)"];
|
|
861
|
-
const vpW = snapshot.viewport.width;
|
|
862
|
-
const termWidth = Math.min(Math.max(process.stdout?.columns || 80, 60), 120);
|
|
863
|
-
const indentUnit = 2;
|
|
864
|
-
const maxDepth = 8;
|
|
865
|
-
const boxPad = 4;
|
|
866
|
-
const issueLocations = /* @__PURE__ */ new Set();
|
|
867
|
-
for (const f of findings) {
|
|
868
|
-
if (f.location && (f.level === "warning" || f.level === "error")) issueLocations.add(f.location);
|
|
869
|
-
}
|
|
870
|
-
function boxCharW(nodeW, depth) {
|
|
871
|
-
const usedIndent = depth * indentUnit;
|
|
872
|
-
const available = termWidth - usedIndent - boxPad;
|
|
873
|
-
const ratio = nodeW / vpW;
|
|
874
|
-
return Math.max(Math.round(available * ratio), 14);
|
|
875
|
-
}
|
|
876
|
-
const lines = [];
|
|
877
|
-
function renderBox(node, charW, depth, flat = false) {
|
|
878
|
-
const m = node.metrics.rect;
|
|
879
|
-
const dimStr = `${Math.round(m.width)}\xD7${Math.round(m.height)}`;
|
|
880
|
-
const innerW = Math.max(charW - 2, 1);
|
|
881
|
-
const label = smartLabel(node, innerW - 4);
|
|
882
|
-
const hasIssue = issueLocations.has(label);
|
|
883
|
-
const issueMark = hasIssue ? " \u26A0" : "";
|
|
884
|
-
const repeat = node.repeat && node.repeat > 1 ? node.repeat : 1;
|
|
885
|
-
const boxLines = [];
|
|
886
|
-
if (innerW < 3) {
|
|
887
|
-
boxLines.push(`${BOX.v} ${label.slice(0, Math.max(charW - 3, 0))}... ${dimStr}`);
|
|
888
|
-
return boxLines;
|
|
889
|
-
}
|
|
890
|
-
const labelPart = ` ${label}${repeat > 1 ? ` \xD7${repeat}` : ""} `;
|
|
891
|
-
const dimPart = ` ${dimStr} `;
|
|
892
|
-
const topFillLen = Math.max(0, innerW - labelPart.length - dimPart.length);
|
|
893
|
-
boxLines.push(`${BOX.tl}${BOX.h}${labelPart}${BOX.h.repeat(topFillLen)}${BOX.tr}${dimPart}${issueMark}`);
|
|
894
|
-
const children = node.children.filter((c) => !(c.metrics.rect.width < 1 && c.metrics.rect.height < 1));
|
|
895
|
-
if (children.length === 0) {
|
|
896
|
-
boxLines.push(`${BOX.v}${" ".repeat(innerW)}${BOX.v}`);
|
|
897
|
-
} else if (flat) {
|
|
898
|
-
for (const child of children) {
|
|
899
|
-
const childLabel = nodeLabel2(child);
|
|
900
|
-
const childRepeat = child.repeat && child.repeat > 1 ? ` \xD7${child.repeat}` : "";
|
|
901
|
-
const childDim = `${Math.round(child.metrics.rect.width)}\xD7${Math.round(child.metrics.rect.height)}`;
|
|
902
|
-
const childLine = ` ${childLabel}${childRepeat} ${childDim} `;
|
|
903
|
-
if (childLine.length <= innerW) {
|
|
904
|
-
boxLines.push(`${BOX.v}${childLine}${" ".repeat(innerW - childLine.length)}${BOX.v}`);
|
|
905
|
-
} else {
|
|
906
|
-
boxLines.push(`${BOX.v}${childLine.slice(0, innerW)}${BOX.v}`);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
} else if (isMultiColumn(node)) {
|
|
910
|
-
const expandedCount = children.reduce((sum, c) => sum + (c.repeat && c.repeat > 1 ? c.repeat : 1), 0);
|
|
911
|
-
if (expandedCount > 1) {
|
|
912
|
-
const isGrid = node.props.display === "grid" || node.props.display === "inline-grid";
|
|
913
|
-
const childLines = renderFlexChildren(children, innerW, depth + 1, m.width, isGrid);
|
|
914
|
-
for (const cl of childLines) {
|
|
915
|
-
boxLines.push(`${BOX.v}${cl}${BOX.v}`);
|
|
916
|
-
}
|
|
917
|
-
} else {
|
|
918
|
-
for (const child of children) {
|
|
919
|
-
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
920
|
-
for (const cl of childBox) {
|
|
921
|
-
const line = ` ${cl}`;
|
|
922
|
-
if (line.length <= innerW + 1) {
|
|
923
|
-
boxLines.push(`${BOX.v}${line}${" ".repeat(Math.max(0, innerW - line.length + 1))}${BOX.v}`);
|
|
924
|
-
} else {
|
|
925
|
-
boxLines.push(`${BOX.v}${line.slice(0, innerW)}${BOX.v}`);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
} else {
|
|
931
|
-
for (const child of children) {
|
|
932
|
-
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
933
|
-
for (const cl of childBox) {
|
|
934
|
-
const line = ` ${cl}`;
|
|
935
|
-
if (line.length <= innerW + 1) {
|
|
936
|
-
boxLines.push(`${BOX.v}${line}${" ".repeat(Math.max(0, innerW - line.length + 1))}${BOX.v}`);
|
|
937
|
-
} else {
|
|
938
|
-
boxLines.push(`${BOX.v}${line.slice(0, innerW)}${BOX.v}`);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
boxLines.push(`${BOX.bl}${BOX.h.repeat(innerW)}${BOX.br}`);
|
|
944
|
-
return boxLines;
|
|
945
|
-
}
|
|
946
|
-
function renderFlexChildren(children, availW, depth, parentPxW, isGrid) {
|
|
947
|
-
const expanded = [];
|
|
948
|
-
for (const child of children) {
|
|
949
|
-
const repeat = child.repeat && child.repeat > 1 ? child.repeat : 1;
|
|
950
|
-
for (let r = 0; r < repeat; r++) {
|
|
951
|
-
expanded.push({ ...child, repeat: 1, children: child.children });
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
let rows;
|
|
955
|
-
if (isGrid && expanded.length > 1) {
|
|
956
|
-
const childW = expanded[0].metrics.rect.width;
|
|
957
|
-
const cols = Math.max(1, Math.floor(parentPxW / childW));
|
|
958
|
-
rows = [];
|
|
959
|
-
for (let i = 0; i < expanded.length; i += cols) {
|
|
960
|
-
rows.push(expanded.slice(i, i + cols));
|
|
961
|
-
}
|
|
962
|
-
} else {
|
|
963
|
-
rows = [expanded];
|
|
964
|
-
}
|
|
965
|
-
const allResult = [];
|
|
966
|
-
for (const row of rows) {
|
|
967
|
-
const gap = 1;
|
|
968
|
-
const totalGaps = (row.length - 1) * gap;
|
|
969
|
-
const minPerChild = 10;
|
|
970
|
-
const usableW = Math.max(availW - totalGaps, row.length * minPerChild);
|
|
971
|
-
const childWidths = [];
|
|
972
|
-
let totalUsed = 0;
|
|
973
|
-
for (let i = 0; i < row.length; i++) {
|
|
974
|
-
const ratio = row[i].metrics.rect.width / parentPxW;
|
|
975
|
-
const w = Math.max(Math.round(usableW * ratio), minPerChild);
|
|
976
|
-
childWidths.push(w);
|
|
977
|
-
totalUsed += w;
|
|
978
|
-
}
|
|
979
|
-
if (childWidths.length > 0 && usableW > totalUsed) {
|
|
980
|
-
childWidths[childWidths.length - 1] += usableW - totalUsed;
|
|
981
|
-
}
|
|
982
|
-
const childBoxes = [];
|
|
983
|
-
let maxLines = 0;
|
|
984
|
-
for (let i = 0; i < row.length; i++) {
|
|
985
|
-
const box = renderBox(row[i], childWidths[i], depth, true);
|
|
986
|
-
childBoxes.push(box);
|
|
987
|
-
if (box.length > maxLines) maxLines = box.length;
|
|
988
|
-
}
|
|
989
|
-
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
990
|
-
let merged = "";
|
|
991
|
-
for (let i = 0; i < childBoxes.length; i++) {
|
|
992
|
-
const w = childWidths[i];
|
|
993
|
-
if (i > 0) merged += " ";
|
|
994
|
-
if (lineIdx < childBoxes[i].length) {
|
|
995
|
-
const line = childBoxes[i][lineIdx];
|
|
996
|
-
if (line.length < w) {
|
|
997
|
-
merged += line + " ".repeat(w - line.length);
|
|
998
|
-
} else {
|
|
999
|
-
merged += line.slice(0, w);
|
|
1000
|
-
}
|
|
1001
|
-
} else {
|
|
1002
|
-
merged += " ".repeat(w);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
allResult.push(merged);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return allResult;
|
|
1009
|
-
}
|
|
1010
|
-
function renderEl(node, depth, parentW) {
|
|
1011
|
-
if (depth > maxDepth) {
|
|
1012
|
-
lines.push(" ".repeat(depth) + "... (depth limit)");
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
const m = node.metrics.rect;
|
|
1016
|
-
if (m.width < 1 && m.height < 1 && node.children.length > 0) {
|
|
1017
|
-
for (const child of node.children) renderEl(child, depth, parentW);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (m.width < 1 && m.height < 1) return;
|
|
1021
|
-
const charW = boxCharW(m.width, depth);
|
|
1022
|
-
const indent = " ".repeat(depth);
|
|
1023
|
-
const box = renderBox(node, charW, depth);
|
|
1024
|
-
for (const line of box) {
|
|
1025
|
-
lines.push(`${indent}${line}`);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
renderEl(snapshot.tree, 0, vpW);
|
|
1029
|
-
const scaleStr = `${Math.round(vpW / termWidth)}px/char`;
|
|
1030
|
-
const result = [];
|
|
1031
|
-
result.push(`## Layout (${vpW}\xD7${snapshot.viewport.height} viewport, scale: ${scaleStr})`);
|
|
1032
|
-
result.push("```");
|
|
1033
|
-
result.push(...lines);
|
|
1034
|
-
result.push("```");
|
|
1035
|
-
return result;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// src/engine/renderers/findings.ts
|
|
1039
|
-
function renderFindings(findings) {
|
|
1040
|
-
const lines = [];
|
|
1041
|
-
lines.push("## Findings");
|
|
1042
|
-
if (findings.length === 0) {
|
|
1043
|
-
lines.push("- \u65E0\u5F02\u5E38\u53D1\u73B0");
|
|
1044
|
-
return lines;
|
|
1045
|
-
}
|
|
1046
|
-
for (const f of findings) {
|
|
1047
|
-
const badge = `[${CONF_BADGE[f.confidence]}]`;
|
|
1048
|
-
const loc = f.location ? ` @ ${f.location}` : "";
|
|
1049
|
-
lines.push(`- ${f.level === "error" ? "\u26A0" : f.level === "warning" ? "\u26A0" : "\u2714"} ${f.message} ${badge}${loc}`);
|
|
1050
|
-
for (const ev of f.evidence.slice(0, 3)) {
|
|
1051
|
-
if (ev.type === "computed") {
|
|
1052
|
-
lines.push(` - computed ${ev.property}: ${ev.value}`);
|
|
1053
|
-
} else if (ev.type === "declared") {
|
|
1054
|
-
lines.push(` - declared ${ev.property}: ${ev.value} (${ev.source || "?"})${ev.accessible ? "" : " [inaccessible]"}`);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
return lines;
|
|
1059
|
-
}
|
|
1060
|
-
function renderBriefFindings(findings) {
|
|
1061
|
-
const lines = [];
|
|
1062
|
-
lines.push("## Findings");
|
|
1063
|
-
const important = findings.filter((f) => f.level === "warning" || f.level === "error");
|
|
1064
|
-
if (important.length === 0) {
|
|
1065
|
-
lines.push("- No warnings or errors");
|
|
1066
|
-
return lines;
|
|
1067
|
-
}
|
|
1068
|
-
for (const f of important) {
|
|
1069
|
-
const badge = `[${CONF_BADGE[f.confidence]}]`;
|
|
1070
|
-
const loc = f.location ? ` @ ${f.location}` : "";
|
|
1071
|
-
const icon = f.level === "error" ? "\u26A0" : "\u26A0";
|
|
1072
|
-
lines.push(`- ${icon} ${f.message} ${badge}${loc}`);
|
|
1073
|
-
}
|
|
1074
|
-
return lines;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// src/engine/renderer.ts
|
|
1078
|
-
function renderMarkdown(snapshot, findings, brief = false, layout = false) {
|
|
1079
|
-
const lines = [];
|
|
1080
|
-
const vp = snapshot.viewport ? `${snapshot.viewport.width}\xD7${snapshot.viewport.height}` : "unknown";
|
|
1081
|
-
lines.push("# cssprobe-cli report");
|
|
1082
|
-
lines.push(`viewport: ${vp} | root: ${snapshot.rootSelector} | nodes: ${snapshot.nodeCount}`);
|
|
1083
|
-
lines.push(`confidence: ${confidenceSummary(findings)}`);
|
|
1084
|
-
if (snapshot.error) {
|
|
1085
|
-
lines.push("");
|
|
1086
|
-
lines.push(`\u26A0 ${snapshot.error}`);
|
|
1087
|
-
if (snapshot.candidates && snapshot.candidates.length) {
|
|
1088
|
-
lines.push(`Similar nodes: ${snapshot.candidates.join(", ")}`);
|
|
1089
|
-
}
|
|
1090
|
-
return lines.join("\n");
|
|
1091
|
-
}
|
|
1092
|
-
lines.push("");
|
|
1093
|
-
lines.push(`## Ancestor chain (root\u2192${snapshot.upTo})`);
|
|
1094
|
-
lines.push("```");
|
|
1095
|
-
lines.push(...renderAncestors(snapshot.ancestors));
|
|
1096
|
-
lines.push("```");
|
|
1097
|
-
if (layout) {
|
|
1098
|
-
lines.push("");
|
|
1099
|
-
lines.push(...renderLayout(snapshot, findings));
|
|
1100
|
-
}
|
|
1101
|
-
if (brief) {
|
|
1102
|
-
lines.push("");
|
|
1103
|
-
lines.push("## Tree Sketch (issues only)");
|
|
1104
|
-
lines.push("```");
|
|
1105
|
-
lines.push(...renderSketch(snapshot.tree, findings));
|
|
1106
|
-
lines.push("```");
|
|
1107
|
-
} else if (!layout) {
|
|
1108
|
-
lines.push("");
|
|
1109
|
-
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
1110
|
-
lines.push("```");
|
|
1111
|
-
lines.push(...renderNode(snapshot.tree, 0));
|
|
1112
|
-
lines.push("```");
|
|
1113
|
-
}
|
|
1114
|
-
lines.push("");
|
|
1115
|
-
lines.push(...brief ? renderBriefFindings(findings) : renderFindings(findings));
|
|
1116
|
-
if (snapshot.crossOriginBlocked && snapshot.crossOriginBlocked > 0) {
|
|
1117
|
-
lines.push("");
|
|
1118
|
-
lines.push("## Cross-origin limitation");
|
|
1119
|
-
lines.push(`\u26A0 ${snapshot.crossOriginBlocked} stylesheet(s) blocked by browser security (SecurityError). Declared values from these sheets are marked UNVERIFIABLE \u2014 computed values from getComputedStyle() are still accurate.`);
|
|
1120
|
-
if (snapshot.blockedSheetUrls && snapshot.blockedSheetUrls.length > 0) {
|
|
1121
|
-
lines.push(`Blocked: ${snapshot.blockedSheetUrls.join(", ")}`);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return lines.join("\n");
|
|
1125
|
-
}
|
|
1126
|
-
function renderJSON(snapshot, findings, brief = false) {
|
|
1127
|
-
const counts = findings.reduce((acc, f) => {
|
|
1128
|
-
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
1129
|
-
return acc;
|
|
1130
|
-
}, {});
|
|
1131
|
-
const meta = {
|
|
1132
|
-
rootSelector: snapshot.rootSelector,
|
|
1133
|
-
viewport: snapshot.viewport,
|
|
1134
|
-
nodeCount: snapshot.nodeCount,
|
|
1135
|
-
crossOriginBlocked: snapshot.crossOriginBlocked,
|
|
1136
|
-
blockedSheetUrls: snapshot.blockedSheetUrls
|
|
1137
|
-
};
|
|
1138
|
-
if (brief) {
|
|
1139
|
-
const important = findings.filter((f) => f.level === "warning" || f.level === "error");
|
|
1140
|
-
const briefFindings = important.map((f) => ({
|
|
1141
|
-
id: f.id,
|
|
1142
|
-
level: f.level,
|
|
1143
|
-
message: f.message,
|
|
1144
|
-
confidence: f.confidence,
|
|
1145
|
-
location: f.location,
|
|
1146
|
-
evidence: f.evidence.slice(0, 1)
|
|
1147
|
-
}));
|
|
1148
|
-
return {
|
|
1149
|
-
meta,
|
|
1150
|
-
snapshot: { rootSelector: snapshot.rootSelector, nodeCount: snapshot.nodeCount, downDepth: snapshot.downDepth },
|
|
1151
|
-
findings: briefFindings,
|
|
1152
|
-
summary: {
|
|
1153
|
-
total: findings.length,
|
|
1154
|
-
warnings: important.filter((f) => f.level === "warning").length,
|
|
1155
|
-
errors: important.filter((f) => f.level === "error").length,
|
|
1156
|
-
confidence: { DEFINITE: counts.DEFINITE || 0, INDEFINITE: counts.INDEFINITE || 0, UNVERIFIABLE: counts.UNVERIFIABLE || 0 }
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
return {
|
|
1161
|
-
meta,
|
|
1162
|
-
snapshot: snapshot.error ? { error: snapshot.error, candidates: snapshot.candidates } : snapshot,
|
|
1163
|
-
findings,
|
|
1164
|
-
summary: {
|
|
1165
|
-
total: findings.length,
|
|
1166
|
-
confidence: { DEFINITE: counts.DEFINITE || 0, INDEFINITE: counts.INDEFINITE || 0, UNVERIFIABLE: counts.UNVERIFIABLE || 0 }
|
|
1167
|
-
}
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// src/daemon/daemon.ts
|
|
1172
|
-
var AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1173
|
-
}).constructor;
|
|
1174
|
-
async function startDaemon(sessionName2, clientInfo, options2) {
|
|
1175
|
-
const sessionConfig = createSessionConfig(clientInfo, sessionName2, options2);
|
|
1176
|
-
const { socketPath } = sessionConfig;
|
|
1177
|
-
if (process.platform !== "win32" && await socketExists(socketPath)) {
|
|
1178
|
-
try {
|
|
1179
|
-
await import_fs2.default.promises.unlink(socketPath);
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
throw error;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
const launcher = new BrowserLauncher({
|
|
1185
|
-
browser: options2.browser || "chromium",
|
|
1186
|
-
headed: options2.headed !== false,
|
|
1187
|
-
viewport: options2.viewport,
|
|
1188
|
-
state: options2.state
|
|
1189
|
-
});
|
|
1190
|
-
const server = import_net.default.createServer((socket) => {
|
|
1191
|
-
const connection = new SocketConnection(socket);
|
|
1192
|
-
connection.onmessage = async (message) => {
|
|
1193
|
-
const { id, method, params } = message;
|
|
1194
|
-
try {
|
|
1195
|
-
if (method === "stop") {
|
|
1196
|
-
await deleteSessionFile(sessionConfig);
|
|
1197
|
-
await launcher.close();
|
|
1198
|
-
connection.send({ id, result: "ok" }).catch(() => {
|
|
1199
|
-
});
|
|
1200
|
-
process.exit(0);
|
|
1201
|
-
} else if (method === "run") {
|
|
1202
|
-
const result = await executeCommand(launcher, params.args);
|
|
1203
|
-
await connection.send({ id, result });
|
|
1204
|
-
} else {
|
|
1205
|
-
throw new Error(`Unknown method: ${method}`);
|
|
1206
|
-
}
|
|
1207
|
-
} catch (e) {
|
|
1208
|
-
const error = e.message;
|
|
1209
|
-
connection.send({ id, error }).catch(() => {
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
});
|
|
1214
|
-
await new Promise((resolve, reject) => {
|
|
1215
|
-
server.on("error", reject);
|
|
1216
|
-
server.listen(socketPath, () => resolve());
|
|
1217
|
-
});
|
|
1218
|
-
await saveSessionFile(clientInfo, sessionConfig);
|
|
1219
|
-
console.log(`Daemon listening on ${socketPath}`);
|
|
1220
|
-
return socketPath;
|
|
1221
|
-
}
|
|
1222
|
-
async function executeCommand(launcher, args2) {
|
|
1223
|
-
const command = args2._[0];
|
|
1224
|
-
const params = args2._.slice(1);
|
|
1225
|
-
switch (command) {
|
|
1226
|
-
case "goto": {
|
|
1227
|
-
const url = params[0] || "about:blank";
|
|
1228
|
-
await launcher.open(url);
|
|
1229
|
-
return { text: `Navigated to ${url}` };
|
|
1230
|
-
}
|
|
1231
|
-
case "inspect": {
|
|
1232
|
-
const selector = params[0];
|
|
1233
|
-
if (!selector)
|
|
1234
|
-
throw new Error("Selector is required for inspect command");
|
|
1235
|
-
const snapshot = await launcher.collect({
|
|
1236
|
-
rootSelector: selector,
|
|
1237
|
-
downDepth: args2.depth || 6,
|
|
1238
|
-
maxNodes: 60,
|
|
1239
|
-
upTo: "html"
|
|
1240
|
-
});
|
|
1241
|
-
const findings = analyze(snapshot);
|
|
1242
|
-
if (args2.json) {
|
|
1243
|
-
return { text: JSON.stringify(renderJSON(snapshot, findings, args2.brief), null, 2) };
|
|
1244
|
-
} else {
|
|
1245
|
-
return { text: renderMarkdown(snapshot, findings, args2.brief, args2.layout) };
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
case "tree": {
|
|
1249
|
-
const selector = params[0];
|
|
1250
|
-
if (!selector)
|
|
1251
|
-
throw new Error("Selector is required for tree command");
|
|
1252
|
-
const snapshot = await launcher.collect({
|
|
1253
|
-
rootSelector: selector,
|
|
1254
|
-
downDepth: args2.depth || 6,
|
|
1255
|
-
maxNodes: 60,
|
|
1256
|
-
upTo: "html"
|
|
1257
|
-
});
|
|
1258
|
-
const lines = [];
|
|
1259
|
-
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
1260
|
-
lines.push("```");
|
|
1261
|
-
renderNode2(snapshot.tree, lines, 0);
|
|
1262
|
-
lines.push("```");
|
|
1263
|
-
return { text: lines.join("\n") };
|
|
1264
|
-
}
|
|
1265
|
-
case "layout": {
|
|
1266
|
-
const selector = params[0];
|
|
1267
|
-
if (!selector)
|
|
1268
|
-
throw new Error("Selector is required for layout command");
|
|
1269
|
-
const snapshot = await launcher.collect({
|
|
1270
|
-
rootSelector: selector,
|
|
1271
|
-
downDepth: 6,
|
|
1272
|
-
maxNodes: 60,
|
|
1273
|
-
upTo: "html"
|
|
1274
|
-
});
|
|
1275
|
-
const findings = analyze(snapshot);
|
|
1276
|
-
return { text: renderMarkdown(snapshot, findings, false, true) };
|
|
1277
|
-
}
|
|
1278
|
-
case "findings": {
|
|
1279
|
-
const selector = params[0];
|
|
1280
|
-
if (!selector)
|
|
1281
|
-
throw new Error("Selector is required for findings command");
|
|
1282
|
-
const snapshot = await launcher.collect({
|
|
1283
|
-
rootSelector: selector,
|
|
1284
|
-
downDepth: 6,
|
|
1285
|
-
maxNodes: 60,
|
|
1286
|
-
upTo: "html"
|
|
1287
|
-
});
|
|
1288
|
-
const findings = analyze(snapshot);
|
|
1289
|
-
const important = findings.filter((f) => f.level === "warning" || f.level === "error");
|
|
1290
|
-
if (important.length === 0) {
|
|
1291
|
-
return { text: "No warnings or errors found." };
|
|
1292
|
-
}
|
|
1293
|
-
const lines = important.map((f) => {
|
|
1294
|
-
const icon = f.level === "error" ? "\u26A0" : "\u26A0";
|
|
1295
|
-
return `- ${icon} ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ""}`;
|
|
1296
|
-
});
|
|
1297
|
-
return { text: lines.join("\n") };
|
|
1298
|
-
}
|
|
1299
|
-
case "inject-css": {
|
|
1300
|
-
const css = params[0];
|
|
1301
|
-
if (!css)
|
|
1302
|
-
throw new Error("CSS is required for inject-css command");
|
|
1303
|
-
const page = launcher.getPage();
|
|
1304
|
-
if (!page)
|
|
1305
|
-
throw new Error("Browser is not open. Run: cssprobe-cli open <url>");
|
|
1306
|
-
await page.addStyleTag({ content: css });
|
|
1307
|
-
return { text: JSON.stringify({ success: true }) };
|
|
1308
|
-
}
|
|
1309
|
-
case "screenshot": {
|
|
1310
|
-
const page = launcher.getPage();
|
|
1311
|
-
if (!page)
|
|
1312
|
-
throw new Error("Browser is not open. Run: cssprobe-cli open <url>");
|
|
1313
|
-
const buffer = await page.screenshot({ fullPage: args2.fullPage });
|
|
1314
|
-
const base64 = buffer.toString("base64");
|
|
1315
|
-
return { text: `data:image/png;base64,${base64}` };
|
|
1316
|
-
}
|
|
1317
|
-
case "eval": {
|
|
1318
|
-
const expression = params[0];
|
|
1319
|
-
if (!expression)
|
|
1320
|
-
throw new Error("Expression is required for eval command");
|
|
1321
|
-
const page = launcher.getPage();
|
|
1322
|
-
if (!page)
|
|
1323
|
-
throw new Error("Browser is not open. Run: cssprobe-cli open <url>");
|
|
1324
|
-
const result = await page.evaluate(expression);
|
|
1325
|
-
return { text: JSON.stringify(result, null, 2) };
|
|
1326
|
-
}
|
|
1327
|
-
case "resize": {
|
|
1328
|
-
const width = parseInt(params[0], 10);
|
|
1329
|
-
const height = parseInt(params[1], 10);
|
|
1330
|
-
if (isNaN(width) || isNaN(height))
|
|
1331
|
-
throw new Error("Usage: cssprobe-cli resize <width> <height>");
|
|
1332
|
-
const page = launcher.getPage();
|
|
1333
|
-
if (!page)
|
|
1334
|
-
throw new Error("Browser is not open. Run: cssprobe-cli open <url>");
|
|
1335
|
-
await page.setViewportSize({ width, height });
|
|
1336
|
-
return { text: JSON.stringify({ width, height }) };
|
|
1337
|
-
}
|
|
1338
|
-
case "playwright": {
|
|
1339
|
-
const call = params[0];
|
|
1340
|
-
if (!call)
|
|
1341
|
-
throw new Error("Playwright API call is required");
|
|
1342
|
-
const page = launcher.getPage();
|
|
1343
|
-
if (!page)
|
|
1344
|
-
throw new Error("Browser is not open. Run: cssprobe-cli open <url>");
|
|
1345
|
-
const asyncFunction = new AsyncFunction("page", "browser", `return ${call}`);
|
|
1346
|
-
const browser = page.context().browser();
|
|
1347
|
-
const result = await asyncFunction(page, browser);
|
|
1348
|
-
return { text: JSON.stringify(result, null, 2) };
|
|
1349
|
-
}
|
|
1350
|
-
default:
|
|
1351
|
-
throw new Error(`Unknown command: ${command}`);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
function renderNode2(node, lines, indent) {
|
|
1355
|
-
if (!node) return;
|
|
1356
|
-
const cls = node.classes?.length ? `.${node.classes.join(".")}` : "";
|
|
1357
|
-
const id = node.id ? `#${node.id}` : "";
|
|
1358
|
-
const m = node.metrics;
|
|
1359
|
-
const s = node.shape || {};
|
|
1360
|
-
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : "";
|
|
1361
|
-
const flag = [];
|
|
1362
|
-
if (node.flags?.overflowsParent) flag.push("\u26A0parent-overflow");
|
|
1363
|
-
if (node.flags?.overflowsViewport) flag.push("\u26A0viewport-overflow");
|
|
1364
|
-
if (m?.clientHeight === 0 && m?.offsetHeight === 0) flag.push("\u26A0collapsed");
|
|
1365
|
-
lines.push(`${" ".repeat(indent)}<${node.tag}${id}${cls}> ${shapeStr} [${node.props?.position},${node.props?.display}] rect(${m?.rect?.width}\xD7${m?.rect?.height})${flag.length ? " " + flag.join(" ") : ""}`);
|
|
1366
|
-
for (const child of node.children || []) renderNode2(child, lines, indent + 1);
|
|
1367
|
-
}
|
|
1368
|
-
async function socketExists(socketPath) {
|
|
1369
|
-
try {
|
|
1370
|
-
const stat = await import_fs2.default.promises.stat(socketPath);
|
|
1371
|
-
return stat?.isSocket() || false;
|
|
1372
|
-
} catch {
|
|
1373
|
-
return false;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
function createSessionConfig(clientInfo, sessionName2, options2) {
|
|
1377
|
-
return {
|
|
1378
|
-
name: sessionName2,
|
|
1379
|
-
version: clientInfo.version,
|
|
1380
|
-
timestamp: Date.now(),
|
|
1381
|
-
socketPath: daemonSocketPath(clientInfo, sessionName2),
|
|
1382
|
-
browser: {
|
|
1383
|
-
browserName: options2.browser || "chromium",
|
|
1384
|
-
launchOptions: { headless: options2.headed === false }
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
function daemonSocketPath(clientInfo, sessionName2) {
|
|
1389
|
-
return import_path2.default.join(clientInfo.daemonProfilesDir, `${sessionName2}.sock`);
|
|
1390
|
-
}
|
|
1391
|
-
async function saveSessionFile(clientInfo, sessionConfig) {
|
|
1392
|
-
await import_fs2.default.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true });
|
|
1393
|
-
const sessionFile = import_path2.default.join(clientInfo.daemonProfilesDir, `${sessionConfig.name}.session`);
|
|
1394
|
-
await import_fs2.default.promises.writeFile(sessionFile, JSON.stringify(sessionConfig, null, 2));
|
|
1395
|
-
}
|
|
1396
|
-
async function deleteSessionFile(sessionConfig) {
|
|
1397
|
-
await import_fs2.default.promises.unlink(sessionConfig.socketPath).catch(() => {
|
|
1398
|
-
});
|
|
1399
|
-
const sessionFile = import_path2.default.join(import_path2.default.dirname(sessionConfig.socketPath), `${sessionConfig.name}.session`);
|
|
1400
|
-
await import_fs2.default.promises.rm(sessionFile).catch(() => {
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// src/daemon/session.ts
|
|
1405
|
-
var import_crypto = __toESM(require("crypto"));
|
|
1406
|
-
var import_fs3 = __toESM(require("fs"));
|
|
1407
|
-
var import_os2 = __toESM(require("os"));
|
|
1408
|
-
var import_path3 = __toESM(require("path"));
|
|
1409
|
-
function createClientInfo() {
|
|
1410
|
-
const workspaceDir = findWorkspaceDir(process.cwd());
|
|
1411
|
-
const version = require_package().version;
|
|
1412
|
-
const hash = import_crypto.default.createHash("sha1");
|
|
1413
|
-
hash.update(workspaceDir || __dirname);
|
|
1414
|
-
const workspaceDirHash = hash.digest("hex").substring(0, 16);
|
|
1415
|
-
return {
|
|
1416
|
-
version,
|
|
1417
|
-
workspaceDir,
|
|
1418
|
-
workspaceDirHash,
|
|
1419
|
-
daemonProfilesDir: daemonProfilesDir(workspaceDirHash),
|
|
1420
|
-
homeDir: import_os2.default.homedir()
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
function findWorkspaceDir(startDir) {
|
|
1424
|
-
let dir = startDir;
|
|
1425
|
-
for (let i = 0; i < 10; i++) {
|
|
1426
|
-
if (import_fs3.default.existsSync(import_path3.default.join(dir, ".cssprobe-cli")))
|
|
1427
|
-
return dir;
|
|
1428
|
-
const parentDir = import_path3.default.dirname(dir);
|
|
1429
|
-
if (parentDir === dir)
|
|
1430
|
-
break;
|
|
1431
|
-
dir = parentDir;
|
|
1432
|
-
}
|
|
1433
|
-
return void 0;
|
|
1434
|
-
}
|
|
1435
|
-
var baseDaemonDir = (() => {
|
|
1436
|
-
let localCacheDir;
|
|
1437
|
-
if (process.platform === "linux")
|
|
1438
|
-
localCacheDir = process.env.XDG_CACHE_HOME || import_path3.default.join(import_os2.default.homedir(), ".cache");
|
|
1439
|
-
if (process.platform === "darwin")
|
|
1440
|
-
localCacheDir = import_path3.default.join(import_os2.default.homedir(), "Library", "Caches");
|
|
1441
|
-
if (process.platform === "win32")
|
|
1442
|
-
localCacheDir = process.env.LOCALAPPDATA || import_path3.default.join(import_os2.default.homedir(), "AppData", "Local");
|
|
1443
|
-
if (!localCacheDir)
|
|
1444
|
-
throw new Error("Unsupported platform: " + process.platform);
|
|
1445
|
-
return import_path3.default.join(localCacheDir, "cssprobe-cli", "daemon");
|
|
1446
|
-
})();
|
|
1447
|
-
var daemonProfilesDir = (workspaceDirHash) => {
|
|
1448
|
-
return import_path3.default.join(baseDaemonDir, workspaceDirHash);
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
// src/daemon/daemonEntry.ts
|
|
1452
|
-
var args = process.argv.slice(2);
|
|
1453
|
-
var sessionName = args[0] || "default";
|
|
1454
|
-
var options = {
|
|
1455
|
-
headed: args.includes("--headed"),
|
|
1456
|
-
browser: args.find((a) => a.startsWith("--browser="))?.split("=")[1],
|
|
1457
|
-
state: args.find((a) => a.startsWith("--state="))?.split("=")[1]
|
|
1458
|
-
};
|
|
1459
|
-
async function main() {
|
|
1460
|
-
try {
|
|
1461
|
-
const clientInfo = createClientInfo();
|
|
1462
|
-
await startDaemon(sessionName, clientInfo, options);
|
|
1463
|
-
} catch (e) {
|
|
1464
|
-
console.error("Failed to start daemon:", e.message);
|
|
1465
|
-
process.exit(1);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
main();
|