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/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`cssprobe-cli` — Runtime CSS probe for layout/scroll/overflow/color/font inspection with session-based browser management. Entrypoint: `bin/cssprobe-cli.js` → `require('../dist/index')`.
|
|
6
6
|
Build: `npm run build` (= `tsc` + `esbuild` collector IIFE bundle + `esbuild` daemon entry + `tsx scripts/generate-help.ts`).
|
|
7
|
-
Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner,
|
|
7
|
+
Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 58 tests).
|
|
8
8
|
Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
|
|
9
9
|
|
|
10
10
|
## Architecture
|
|
@@ -41,7 +41,7 @@ src/
|
|
|
41
41
|
- **Session-based architecture**: Inspired by playwright-cli. `open` starts a daemon process, subsequent commands connect via Unix socket. Single session (default) mode.
|
|
42
42
|
- **Daemon process**: Long-running background process that manages the browser and executes commands. Communicates via Unix domain socket.
|
|
43
43
|
- **Three-layer engine**: `collector.ts` (browser, esbuild IIFE) → `analyzer.ts` (Node, pure functions) → `renderer.ts` (Markdown/JSON). Collector only gathers facts; analyzer does inference with confidence; renderer formats output.
|
|
44
|
-
- **Collector bundling**: `collector.ts` is bundled by esbuild as an IIFE (`format: 'iife', globalName: '
|
|
44
|
+
- **Collector bundling**: `collector.ts` is bundled by esbuild as an IIFE (`format: 'iife', globalName: '__cssprobe_cli'`). The launcher injects it via `page.addScriptTag({ content })`, then calls `window.__cssprobe_cli.collect(cfg)`.
|
|
45
45
|
- **Confidence model**: Every `Finding` carries `confidence: 'DEFINITE' | 'INDEFINITE' | 'UNVERIFIABLE'`. Computed values = DEFINITE. Percent declarations = INDEFINITE. Missing/blocked declarations = UNVERIFIABLE.
|
|
46
46
|
- **Command definition**: `declareCommand({ name, category, description, args?, options? })` — purely declarative, no execution logic.
|
|
47
47
|
- **Command dispatch**: `minimist` parse → `parseCommand()` Zod validate → session command execution via daemon socket.
|
|
@@ -101,11 +101,10 @@ src/
|
|
|
101
101
|
npm run build
|
|
102
102
|
├── 1. tsc # Compile TS → dist/ (includes .d.ts)
|
|
103
103
|
├── 2. esbuild collector.ts # Bundle browser-side code → dist/collector-bundle.js (IIFE)
|
|
104
|
-
|
|
105
|
-
└── 4. tsx generate-help.ts # Zod schemas → dist/help.json
|
|
104
|
+
└── 3. tsx generate-help.ts # Zod schemas → dist/help.json
|
|
106
105
|
```
|
|
107
106
|
|
|
108
|
-
The collector bundle is a self-contained IIFE that exposes `window.
|
|
107
|
+
The collector bundle is a self-contained IIFE that exposes `window.__cssprobe_cli.collect(cfg)`. It runs entirely in the browser context — no Node imports.
|
|
109
108
|
|
|
110
109
|
## Session Mode
|
|
111
110
|
|
package/README.md
CHANGED
|
@@ -318,7 +318,7 @@ cssprobe-cli skill-uninstall
|
|
|
318
318
|
```bash
|
|
319
319
|
npm install
|
|
320
320
|
npm run build # tsc + esbuild collector + daemon entry + generate help.json
|
|
321
|
-
npm test # Run unit tests (
|
|
321
|
+
npm test # Run unit tests (58 tests)
|
|
322
322
|
npx tsc --noEmit # Type check only
|
|
323
323
|
```
|
|
324
324
|
|
|
@@ -19,20 +19,8 @@ export declare class BrowserLauncher {
|
|
|
19
19
|
constructor(options?: LauncherOptions);
|
|
20
20
|
open(url: string): Promise<void>;
|
|
21
21
|
collect(cfg: CollectConfig): Promise<Snapshot>;
|
|
22
|
-
/** Measure the DOM tree depth from a root selector to the deepest leaf. */
|
|
23
|
-
measureDepth(selector: string): Promise<number>;
|
|
24
|
-
/** Wait for user to interact with the page. Press Enter to continue. */
|
|
25
|
-
waitForUser(): Promise<void>;
|
|
26
22
|
/** Navigate to a new URL in the existing page. */
|
|
27
23
|
navigate(url: string): Promise<void>;
|
|
28
|
-
/** Scan the page for likely root elements (modals, dialogs, scroll containers). */
|
|
29
|
-
autoDetectRoot(): Promise<string[]>;
|
|
30
|
-
/**
|
|
31
|
-
* Interactive login flow: opens browser in headed mode, navigates to URL,
|
|
32
|
-
* waits for user to complete login, then saves storage state.
|
|
33
|
-
* Returns the path to the saved state file.
|
|
34
|
-
*/
|
|
35
|
-
loginAndSave(url: string, statePath: string): Promise<string>;
|
|
36
24
|
/** Get the current page object. */
|
|
37
25
|
getPage(): Page | null;
|
|
38
26
|
close(): Promise<void>;
|
package/dist/browser/launcher.js
CHANGED
|
@@ -63,6 +63,7 @@ class BrowserLauncher {
|
|
|
63
63
|
this.options = options;
|
|
64
64
|
}
|
|
65
65
|
async open(url) {
|
|
66
|
+
await this.close();
|
|
66
67
|
const engine = this.options.browser || 'chromium';
|
|
67
68
|
const factory = engine === 'firefox' ? playwright_core_1.firefox :
|
|
68
69
|
engine === 'webkit' ? playwright_core_1.webkit :
|
|
@@ -107,35 +108,6 @@ class BrowserLauncher {
|
|
|
107
108
|
}, cfg);
|
|
108
109
|
return snapshot;
|
|
109
110
|
}
|
|
110
|
-
/** Measure the DOM tree depth from a root selector to the deepest leaf. */
|
|
111
|
-
async measureDepth(selector) {
|
|
112
|
-
if (!this.page)
|
|
113
|
-
throw new Error('Browser not opened. Call open() first.');
|
|
114
|
-
return this.page.evaluate((sel) => {
|
|
115
|
-
const root = document.querySelector(sel);
|
|
116
|
-
if (!root)
|
|
117
|
-
return 0;
|
|
118
|
-
let maxDepth = 0;
|
|
119
|
-
const walk = (el, depth) => {
|
|
120
|
-
if (depth > maxDepth)
|
|
121
|
-
maxDepth = depth;
|
|
122
|
-
for (const child of Array.from(el.children))
|
|
123
|
-
walk(child, depth + 1);
|
|
124
|
-
};
|
|
125
|
-
walk(root, 0);
|
|
126
|
-
return maxDepth;
|
|
127
|
-
}, selector);
|
|
128
|
-
}
|
|
129
|
-
/** Wait for user to interact with the page. Press Enter to continue. */
|
|
130
|
-
async waitForUser() {
|
|
131
|
-
console.error('');
|
|
132
|
-
console.error(' Browser is open. Perform your actions (login, click buttons, open dialogs, etc.).');
|
|
133
|
-
console.error(' Press Enter here when ready to inspect...');
|
|
134
|
-
console.error('');
|
|
135
|
-
await new Promise(resolve => {
|
|
136
|
-
process.stdin.once('data', () => resolve());
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
111
|
/** Navigate to a new URL in the existing page. */
|
|
140
112
|
async navigate(url) {
|
|
141
113
|
if (!this.page)
|
|
@@ -153,100 +125,6 @@ class BrowserLauncher {
|
|
|
153
125
|
await this.page.goto(`file://${resolved}`, { waitUntil: 'load', timeout: 30000 });
|
|
154
126
|
}
|
|
155
127
|
}
|
|
156
|
-
/** Scan the page for likely root elements (modals, dialogs, scroll containers). */
|
|
157
|
-
async autoDetectRoot() {
|
|
158
|
-
if (!this.page)
|
|
159
|
-
throw new Error('Browser not opened. Call open() first.');
|
|
160
|
-
return this.page.evaluate(() => {
|
|
161
|
-
const candidates = [];
|
|
162
|
-
const seen = new Set();
|
|
163
|
-
function addSelector(sel, score) {
|
|
164
|
-
if (seen.has(sel))
|
|
165
|
-
return;
|
|
166
|
-
seen.add(sel);
|
|
167
|
-
candidates.push({ selector: sel, score });
|
|
168
|
-
}
|
|
169
|
-
// Modals / dialogs
|
|
170
|
-
for (const el of Array.from(document.querySelectorAll('.s-kit-modal, [class*="dialog"], [class*="modal"], [class*="popup"]'))) {
|
|
171
|
-
const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).join('.');
|
|
172
|
-
if (cls)
|
|
173
|
-
addSelector(`.${cls.split(/\s+/)[0]}`, 10);
|
|
174
|
-
}
|
|
175
|
-
// Scroll containers (overflow-y: auto/scroll with content overflow)
|
|
176
|
-
for (const el of Array.from(document.querySelectorAll('*'))) {
|
|
177
|
-
const cs = getComputedStyle(el);
|
|
178
|
-
if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') &&
|
|
179
|
-
el.scrollHeight > el.clientHeight + 1 &&
|
|
180
|
-
el.clientHeight > 100) {
|
|
181
|
-
const tag = el.tagName.toLowerCase();
|
|
182
|
-
const id = el.id ? `#${el.id}` : '';
|
|
183
|
-
const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
|
|
184
|
-
const sel = id || (cls ? `${tag}.${cls}` : tag);
|
|
185
|
-
addSelector(sel, 5);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Sections with substantial content
|
|
189
|
-
for (const el of Array.from(document.querySelectorAll('section, [class*="section"], main, [role="main"]'))) {
|
|
190
|
-
const tag = el.tagName.toLowerCase();
|
|
191
|
-
const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
|
|
192
|
-
const sel = cls ? `${tag}.${cls}` : tag;
|
|
193
|
-
addSelector(sel, 3);
|
|
194
|
-
}
|
|
195
|
-
return candidates
|
|
196
|
-
.sort((a, b) => b.score - a.score)
|
|
197
|
-
.slice(0, 5)
|
|
198
|
-
.map(c => c.selector);
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Interactive login flow: opens browser in headed mode, navigates to URL,
|
|
203
|
-
* waits for user to complete login, then saves storage state.
|
|
204
|
-
* Returns the path to the saved state file.
|
|
205
|
-
*/
|
|
206
|
-
async loginAndSave(url, statePath) {
|
|
207
|
-
const engine = this.options.browser || 'chromium';
|
|
208
|
-
const factory = engine === 'firefox' ? playwright_core_1.firefox :
|
|
209
|
-
engine === 'webkit' ? playwright_core_1.webkit :
|
|
210
|
-
playwright_core_1.chromium;
|
|
211
|
-
const execPath = findBrowserExecutable(engine);
|
|
212
|
-
const launchOptions = { headless: false };
|
|
213
|
-
if (execPath)
|
|
214
|
-
launchOptions.executablePath = execPath;
|
|
215
|
-
this.browser = await factory.launch(launchOptions);
|
|
216
|
-
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
217
|
-
this.context = await this.browser.newContext({ viewport: vp });
|
|
218
|
-
this.page = await this.context.newPage();
|
|
219
|
-
// Navigate to the target URL
|
|
220
|
-
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
221
|
-
await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
throw new Error('--login only works with http/https URLs');
|
|
225
|
-
}
|
|
226
|
-
// Wait for user to complete login (detect URL change or navigation)
|
|
227
|
-
console.error('');
|
|
228
|
-
console.error(' Browser opened. Please complete the login manually.');
|
|
229
|
-
console.error(' The browser will detect when you navigate away from the login page,');
|
|
230
|
-
console.error(' or you can press Enter here when done.');
|
|
231
|
-
console.error('');
|
|
232
|
-
// Wait for either URL change or Enter keypress
|
|
233
|
-
await Promise.race([
|
|
234
|
-
this.page.waitForURL('**/*', { timeout: 300000 }).catch(() => { }),
|
|
235
|
-
new Promise(resolve => {
|
|
236
|
-
process.stdin.once('data', () => resolve());
|
|
237
|
-
}),
|
|
238
|
-
]);
|
|
239
|
-
// Small delay to let cookies/localStorage settle
|
|
240
|
-
await this.page.waitForTimeout(1000);
|
|
241
|
-
// Save state
|
|
242
|
-
const resolvedPath = path_1.default.resolve(statePath);
|
|
243
|
-
const dir = path_1.default.dirname(resolvedPath);
|
|
244
|
-
if (!fs_1.default.existsSync(dir))
|
|
245
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
246
|
-
await this.context.storageState({ path: resolvedPath });
|
|
247
|
-
console.error(` State saved to: ${resolvedPath}`);
|
|
248
|
-
return resolvedPath;
|
|
249
|
-
}
|
|
250
128
|
/** Get the current page object. */
|
|
251
129
|
getPage() {
|
|
252
130
|
return this.page;
|
package/dist/cli/program.js
CHANGED
|
@@ -137,13 +137,11 @@ async function handleOpen(command, args, output) {
|
|
|
137
137
|
state,
|
|
138
138
|
_: ['open', url],
|
|
139
139
|
}, 'open');
|
|
140
|
-
// Wait
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await session.run({ _: ['goto', url] });
|
|
146
|
-
}
|
|
140
|
+
// Wait for the daemon to be ready (poll session file instead of fixed delay)
|
|
141
|
+
const session = await waitForSession(5000);
|
|
142
|
+
if (!session)
|
|
143
|
+
throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
|
|
144
|
+
await session.run({ _: ['goto', url] });
|
|
147
145
|
const result = {
|
|
148
146
|
sessionId: 'default',
|
|
149
147
|
pid,
|
|
@@ -164,6 +162,16 @@ async function handleOpen(command, args, output) {
|
|
|
164
162
|
output.error(e instanceof Error ? e.message : String(e));
|
|
165
163
|
}
|
|
166
164
|
}
|
|
165
|
+
async function waitForSession(timeoutMs) {
|
|
166
|
+
const deadline = Date.now() + timeoutMs;
|
|
167
|
+
while (Date.now() < deadline) {
|
|
168
|
+
const session = await (0, session_1.loadSession)();
|
|
169
|
+
if (session && await session.canConnect())
|
|
170
|
+
return session;
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
167
175
|
async function handleClose(output) {
|
|
168
176
|
try {
|
|
169
177
|
const session = await (0, session_1.loadSession)();
|
|
@@ -255,18 +263,11 @@ async function handleSessionCommand(commandName, command, args, output) {
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
// Run command in session
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
catch {
|
|
264
|
-
console.log(result.text);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
console.log(result.text);
|
|
269
|
-
}
|
|
266
|
+
const runArgs = { ...cmdArgs, _: daemonArgs };
|
|
267
|
+
if (args.json)
|
|
268
|
+
runArgs.json = true;
|
|
269
|
+
const result = await session.run(runArgs);
|
|
270
|
+
console.log(result.text);
|
|
270
271
|
}
|
|
271
272
|
catch (e) {
|
|
272
273
|
output.error(e instanceof Error ? e.message : String(e));
|
|
@@ -375,7 +376,7 @@ function parseNetscapeCookies(content) {
|
|
|
375
376
|
const parts = trimmed.split('\t');
|
|
376
377
|
if (parts.length < 7)
|
|
377
378
|
continue;
|
|
378
|
-
const [domain,
|
|
379
|
+
const [domain, httpOnly, path, secure, expires, name, ...valueParts] = parts;
|
|
379
380
|
const value = valueParts.join('\t');
|
|
380
381
|
const expiresNum = parseInt(expires, 10);
|
|
381
382
|
cookies.push({
|
|
@@ -384,7 +385,7 @@ function parseNetscapeCookies(content) {
|
|
|
384
385
|
domain,
|
|
385
386
|
path: path || '/',
|
|
386
387
|
expires: isNaN(expiresNum) || expiresNum === 0 ? -1 : expiresNum,
|
|
387
|
-
httpOnly:
|
|
388
|
+
httpOnly: httpOnly === 'TRUE',
|
|
388
389
|
secure: secure === 'TRUE',
|
|
389
390
|
sameSite: secure === 'TRUE' ? 'None' : 'Lax',
|
|
390
391
|
});
|
package/dist/collector-bundle.js
CHANGED
|
@@ -24,6 +24,34 @@ var __cssprobe_cli = (() => {
|
|
|
24
24
|
classifyShape: () => classifyShape,
|
|
25
25
|
collect: () => collect
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
// src/engine/specificity.ts
|
|
29
|
+
function specificity(selector) {
|
|
30
|
+
if (selector === "inline") return Number.MAX_SAFE_INTEGER;
|
|
31
|
+
let score = 0;
|
|
32
|
+
for (const token of selector.split(/[ >+~]+/)) {
|
|
33
|
+
const ids = (token.match(/#[\w-]+/g) || []).length;
|
|
34
|
+
const attrsClasses = (token.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
|
|
35
|
+
const types = (token.match(/^[\w-]+|::[\w-]+/g) || []).length;
|
|
36
|
+
score += ids * 100 + attrsClasses * 10 + types;
|
|
37
|
+
}
|
|
38
|
+
return score;
|
|
39
|
+
}
|
|
40
|
+
function pickWinning(arr) {
|
|
41
|
+
if (arr.length === 0) return arr[0];
|
|
42
|
+
let winner = arr[0];
|
|
43
|
+
let best = -1;
|
|
44
|
+
for (const d of arr) {
|
|
45
|
+
const s = d.selector === "inline" ? Number.MAX_SAFE_INTEGER : specificity(d.selector);
|
|
46
|
+
if (s >= best) {
|
|
47
|
+
best = s;
|
|
48
|
+
winner = d;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return winner;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/engine/collector.ts
|
|
27
55
|
var WANTED_PROPS = [
|
|
28
56
|
// layout
|
|
29
57
|
"height",
|
|
@@ -149,8 +177,7 @@ var __cssprobe_cli = (() => {
|
|
|
149
177
|
const getDeclared = (prop) => {
|
|
150
178
|
const arr = declared && declared[prop];
|
|
151
179
|
if (!arr || arr.length === 0) return null;
|
|
152
|
-
|
|
153
|
-
return inline ? inline.value : arr[0].value;
|
|
180
|
+
return pickWinning(arr).value;
|
|
154
181
|
};
|
|
155
182
|
const sizeClass = (prop, maxProp) => {
|
|
156
183
|
const val = getDeclared(prop);
|
|
@@ -309,7 +336,7 @@ var __cssprobe_cli = (() => {
|
|
|
309
336
|
tree: null,
|
|
310
337
|
crossOriginBlocked: crossOriginBlocked.count,
|
|
311
338
|
blockedSheetUrls: blockedSheetUrls.slice(0, 5),
|
|
312
|
-
error:
|
|
339
|
+
error: `Not found: ${cfg.rootSelector}`,
|
|
313
340
|
candidates
|
|
314
341
|
};
|
|
315
342
|
}
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ const socketConnection_1 = require("../utils/socketConnection");
|
|
|
16
16
|
const launcher_1 = require("../browser/launcher");
|
|
17
17
|
const analyzer_1 = require("../engine/analyzer");
|
|
18
18
|
const renderer_1 = require("../engine/renderer");
|
|
19
|
+
const dom_tree_1 = require("../engine/renderers/dom-tree");
|
|
19
20
|
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
20
21
|
async function startDaemon(sessionName, clientInfo, options) {
|
|
21
22
|
const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
|
|
@@ -110,7 +111,7 @@ async function executeCommand(launcher, args) {
|
|
|
110
111
|
const lines = [];
|
|
111
112
|
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
112
113
|
lines.push('```');
|
|
113
|
-
renderNode(snapshot.tree,
|
|
114
|
+
lines.push(...(0, dom_tree_1.renderNode)(snapshot.tree, 0));
|
|
114
115
|
lines.push('```');
|
|
115
116
|
return { text: lines.join('\n') };
|
|
116
117
|
}
|
|
@@ -143,8 +144,7 @@ async function executeCommand(launcher, args) {
|
|
|
143
144
|
return { text: 'No warnings or errors found.' };
|
|
144
145
|
}
|
|
145
146
|
const lines = important.map(f => {
|
|
146
|
-
|
|
147
|
-
return `- ${icon} ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ''}`;
|
|
147
|
+
return `- ⚠ ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ''}`;
|
|
148
148
|
});
|
|
149
149
|
return { text: lines.join('\n') };
|
|
150
150
|
}
|
|
@@ -162,7 +162,7 @@ async function executeCommand(launcher, args) {
|
|
|
162
162
|
const page = launcher.getPage();
|
|
163
163
|
if (!page)
|
|
164
164
|
throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
|
|
165
|
-
const buffer = await page.screenshot({ fullPage: args
|
|
165
|
+
const buffer = await page.screenshot({ fullPage: args['full-page'] });
|
|
166
166
|
const base64 = buffer.toString('base64');
|
|
167
167
|
return { text: `data:image/png;base64,${base64}` };
|
|
168
168
|
}
|
|
@@ -204,25 +204,6 @@ async function executeCommand(launcher, args) {
|
|
|
204
204
|
throw new Error(`Unknown command: ${command}`);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
function renderNode(node, lines, indent) {
|
|
208
|
-
if (!node)
|
|
209
|
-
return;
|
|
210
|
-
const cls = node.classes?.length ? `.${node.classes.join('.')}` : '';
|
|
211
|
-
const id = node.id ? `#${node.id}` : '';
|
|
212
|
-
const m = node.metrics;
|
|
213
|
-
const s = node.shape || {};
|
|
214
|
-
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
215
|
-
const flag = [];
|
|
216
|
-
if (node.flags?.overflowsParent)
|
|
217
|
-
flag.push('⚠parent-overflow');
|
|
218
|
-
if (node.flags?.overflowsViewport)
|
|
219
|
-
flag.push('⚠viewport-overflow');
|
|
220
|
-
if (m?.clientHeight === 0 && m?.offsetHeight === 0)
|
|
221
|
-
flag.push('⚠collapsed');
|
|
222
|
-
lines.push(`${' '.repeat(indent)}<${node.tag}${id}${cls}> ${shapeStr} [${node.props?.position},${node.props?.display}] rect(${m?.rect?.width}×${m?.rect?.height})${flag.length ? ' ' + flag.join(' ') : ''}`);
|
|
223
|
-
for (const child of node.children || [])
|
|
224
|
-
renderNode(child, lines, indent + 1);
|
|
225
|
-
}
|
|
226
207
|
async function socketExists(socketPath) {
|
|
227
208
|
try {
|
|
228
209
|
const stat = await fs_1.default.promises.stat(socketPath);
|
|
@@ -13,7 +13,20 @@ const options = {
|
|
|
13
13
|
headed: args.includes('--headed'),
|
|
14
14
|
browser: args.find(a => a.startsWith('--browser='))?.split('=')[1],
|
|
15
15
|
state: args.find(a => a.startsWith('--state='))?.split('=')[1],
|
|
16
|
+
viewport: parseViewportArg(args.find(a => a.startsWith('--viewport='))?.split('=')[1]),
|
|
16
17
|
};
|
|
18
|
+
function parseViewportArg(raw) {
|
|
19
|
+
if (!raw)
|
|
20
|
+
return undefined;
|
|
21
|
+
const m = raw.match(/^(\d+)[xX*](\d+)$/);
|
|
22
|
+
if (!m)
|
|
23
|
+
return undefined;
|
|
24
|
+
const width = Number(m[1]);
|
|
25
|
+
const height = Number(m[2]);
|
|
26
|
+
if (width > 0 && height > 0)
|
|
27
|
+
return { width, height };
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
17
30
|
async function main() {
|
|
18
31
|
try {
|
|
19
32
|
const clientInfo = (0, session_1.createClientInfo)();
|
package/dist/daemon/session.js
CHANGED
|
@@ -50,10 +50,18 @@ class Session {
|
|
|
50
50
|
}
|
|
51
51
|
async _connect() {
|
|
52
52
|
return await new Promise(resolve => {
|
|
53
|
+
let settled = false;
|
|
53
54
|
const socket = net_1.default.createConnection(this.config.socketPath, () => {
|
|
55
|
+
settled = true;
|
|
54
56
|
resolve({ socket });
|
|
55
57
|
});
|
|
56
58
|
socket.on('error', error => {
|
|
59
|
+
if (settled) {
|
|
60
|
+
socket.destroy();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Only clean up the socket file when the daemon is confirmed not listening
|
|
64
|
+
// (ECONNREFUSED / ENOENT). Do not unlink on transient failures of a live socket.
|
|
57
65
|
if (os_1.default.platform() !== 'win32')
|
|
58
66
|
void fs_1.default.promises.unlink(this.config.socketPath).catch(() => { }).then(() => resolve({ error }));
|
|
59
67
|
else
|
|
@@ -86,6 +94,8 @@ class Session {
|
|
|
86
94
|
daemonArgs.push(`--browser=${args.browser}`);
|
|
87
95
|
if (args.state)
|
|
88
96
|
daemonArgs.push(`--state=${args.state}`);
|
|
97
|
+
if (args.viewport)
|
|
98
|
+
daemonArgs.push(`--viewport=${args.viewport.width}x${args.viewport.height}`);
|
|
89
99
|
const child = require('child_process').spawn(process.execPath, daemonArgs, {
|
|
90
100
|
detached: true,
|
|
91
101
|
stdio: ['ignore', 'pipe', err],
|
package/dist/engine/analyzer.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
// No browser or filesystem access — directly unit-testable.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.analyze = analyze;
|
|
6
|
+
const specificity_1 = require("./specificity");
|
|
6
7
|
// ─── Confidence helpers ───
|
|
7
|
-
/** Pick the winning declared value for a property (inline wins, else
|
|
8
|
+
/** Pick the winning declared value for a property (inline wins, else highest specificity). */
|
|
8
9
|
function declaredFor(node, prop) {
|
|
9
10
|
const arr = node.declared && node.declared[prop];
|
|
10
11
|
if (!arr || arr.length === 0)
|
|
11
12
|
return null;
|
|
12
|
-
return
|
|
13
|
+
return (0, specificity_1.pickWinning)(arr);
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Confidence of a declaration-backed claim:
|
|
@@ -66,9 +67,6 @@ function makeFinding(partial) {
|
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
// ─── Ancestors ───
|
|
69
|
-
function findAncestor(ancestors, predicate) {
|
|
70
|
-
return ancestors.find(predicate);
|
|
71
|
-
}
|
|
72
70
|
function allAncestorsContentSized(ancestors) {
|
|
73
71
|
return ancestors.length > 0 && ancestors.every(a => a.shape.heightStrategy === 'content');
|
|
74
72
|
}
|
|
@@ -238,7 +236,7 @@ function analyzeFlex(snapshot) {
|
|
|
238
236
|
const pLabel = nodeLabel(parent);
|
|
239
237
|
const cLabel = nodeLabel(child);
|
|
240
238
|
const pDir = parent.props.flexDirection || 'row';
|
|
241
|
-
const cMinH =
|
|
239
|
+
const cMinH = child.props.minHeight;
|
|
242
240
|
const cMinW = child.props.minWidth;
|
|
243
241
|
const cFlexGrow = child.props.flexGrow;
|
|
244
242
|
const cFlexShrink = child.props.flexShrink;
|
package/dist/engine/collector.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.classifyShape = classifyShape;
|
|
7
7
|
exports.collect = collect;
|
|
8
|
+
const specificity_1 = require("./specificity");
|
|
8
9
|
// ─── Declared-value scanning ───
|
|
9
10
|
const WANTED_PROPS = [
|
|
10
11
|
// layout
|
|
@@ -101,8 +102,7 @@ function classifyShape(cs, declared) {
|
|
|
101
102
|
const arr = declared && declared[prop];
|
|
102
103
|
if (!arr || arr.length === 0)
|
|
103
104
|
return null;
|
|
104
|
-
|
|
105
|
-
return inline ? inline.value : arr[0].value;
|
|
105
|
+
return (0, specificity_1.pickWinning)(arr).value;
|
|
106
106
|
};
|
|
107
107
|
const sizeClass = (prop, maxProp) => {
|
|
108
108
|
const val = getDeclared(prop);
|
|
@@ -256,7 +256,7 @@ function collect(cfg) {
|
|
|
256
256
|
tree: null,
|
|
257
257
|
crossOriginBlocked: crossOriginBlocked.count,
|
|
258
258
|
blockedSheetUrls: blockedSheetUrls.slice(0, 5),
|
|
259
|
-
error:
|
|
259
|
+
error: `Not found: ${cfg.rootSelector}`,
|
|
260
260
|
candidates,
|
|
261
261
|
};
|
|
262
262
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.specificity = specificity;
|
|
4
|
+
exports.pickWinning = pickWinning;
|
|
5
|
+
function specificity(selector) {
|
|
6
|
+
if (selector === 'inline')
|
|
7
|
+
return Number.MAX_SAFE_INTEGER;
|
|
8
|
+
let score = 0;
|
|
9
|
+
for (const token of selector.split(/[ >+~]+/)) {
|
|
10
|
+
const ids = (token.match(/#[\w-]+/g) || []).length;
|
|
11
|
+
const attrsClasses = (token.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
|
|
12
|
+
const types = (token.match(/^[\w-]+|::[\w-]+/g) || []).length;
|
|
13
|
+
score += ids * 100 + attrsClasses * 10 + types;
|
|
14
|
+
}
|
|
15
|
+
return score;
|
|
16
|
+
}
|
|
17
|
+
function pickWinning(arr) {
|
|
18
|
+
if (arr.length === 0)
|
|
19
|
+
return arr[0];
|
|
20
|
+
let winner = arr[0];
|
|
21
|
+
let best = -1;
|
|
22
|
+
for (const d of arr) {
|
|
23
|
+
const s = d.selector === 'inline' ? Number.MAX_SAFE_INTEGER : specificity(d.selector);
|
|
24
|
+
if (s >= best) {
|
|
25
|
+
best = s;
|
|
26
|
+
winner = d;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return winner;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=specificity.js.map
|