dembrandt 0.19.5 → 0.19.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/lib/exit-codes.d.ts +1 -1
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/lib/extractors/context-config.d.ts +70 -0
- package/dist/lib/extractors/context-config.js +96 -0
- package/dist/lib/extractors/context-config.js.map +1 -0
- package/dist/lib/extractors/guard.d.ts +9 -0
- package/dist/lib/extractors/guard.js +10 -0
- package/dist/lib/extractors/guard.js.map +1 -0
- package/dist/lib/extractors/index.d.ts +2 -1
- package/dist/lib/extractors/index.js +38 -55
- package/dist/lib/extractors/index.js.map +1 -1
- package/dist/lib/extractors/logo.js +14 -0
- package/dist/lib/extractors/logo.js.map +1 -1
- package/dist/lib/extractors/platform-colors.d.ts +60 -0
- package/dist/lib/extractors/platform-colors.js +121 -0
- package/dist/lib/extractors/platform-colors.js.map +1 -0
- package/dist/lib/extractors/typography.d.ts +19 -1
- package/dist/lib/extractors/typography.js +61 -1
- package/dist/lib/extractors/typography.js.map +1 -1
- package/dist/lib/formatters/dtcg.js.map +1 -1
- package/dist/lib/formatters/terminal.js +13 -0
- package/dist/lib/formatters/terminal.js.map +1 -1
- package/dist/lib/merger.js +22 -1
- package/dist/lib/merger.js.map +1 -1
- package/dist/lib/types.d.ts +80 -6
- package/dist/package.json +1 -1
- package/dist/test/context-config.test.d.ts +1 -0
- package/dist/test/context-config.test.js +114 -0
- package/dist/test/context-config.test.js.map +1 -0
- package/dist/test/guard.test.d.ts +1 -0
- package/dist/test/guard.test.js +46 -0
- package/dist/test/guard.test.js.map +1 -0
- package/dist/test/merger.test.js +18 -0
- package/dist/test/merger.test.js.map +1 -1
- package/dist/test/platform-colors.test.d.ts +1 -0
- package/dist/test/platform-colors.test.js +173 -0
- package/dist/test/platform-colors.test.js.map +1 -0
- package/dist/test/stealth.test.d.ts +1 -0
- package/dist/test/stealth.test.js +118 -0
- package/dist/test/stealth.test.js.map +1 -0
- package/dist/test/typography.test.d.ts +1 -0
- package/dist/test/typography.test.js +35 -0
- package/dist/test/typography.test.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Or add to your project's `.mcp.json`:
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Available tools include `get_design_tokens`, `get_color_palette`, `get_typography`, `get_component_styles`, `get_surfaces`, `get_spacing`, and `get_brand_identity`, plus drift, report, and job-control tools.
|
|
46
46
|
|
|
47
47
|
Pair with **[dembrandt-skills](https://github.com/dembrandt/dembrandt-skills)** to give your agent UX intelligence on top of extracted tokens — hierarchy, accessibility, interaction states, and a full 6-stage design pipeline orchestrator.
|
|
48
48
|
|
|
@@ -247,7 +247,28 @@ dembrandt https://app.example.com --json-only > baseline.json
|
|
|
247
247
|
dembrandt https://app.example.com --compare baseline.json --html report.html
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
When the change is intended, accept it as the new baseline — `--approve` overwrites the local baseline file and passes instead of failing:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
dembrandt https://app.example.com --compare baseline.json --approve
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Add `--json-only` to a `--compare` run to get the drift report as machine-readable JSON under a `drift` key — `score`, `status`, `summary`, and per-token `changes[]` (each with `category`, `kind`, `before`, `after`, `delta`). A CI gate can render exactly which tokens moved (e.g. in a PR comment) from this instead of parsing the HTML report:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
dembrandt https://app.example.com --compare baseline.json --json-only
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Any CI.** The gate is platform-neutral — it is just the exit code plus the drift JSON, so it drops into any runner:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
dembrandt "$PREVIEW/checkout" --compare base.json --json-only > drift.json
|
|
266
|
+
# exit 1 = drift. Read drift.json (.drift.changes) and surface it however your
|
|
267
|
+
# platform does: a GitLab MR note, an Azure DevOps PR thread, a Jenkins status,
|
|
268
|
+
# a Slack message, or an auto-filed Jira/Linear ticket.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A ready-to-use **GitHub Actions** workflow (preview vs production, per-page PR comment with the exact tokens that changed, run summary, report artifact, host-auth bypass) is in [`examples/drift-gate.yml`](examples/drift-gate.yml) as one full reference. The result-surfacing step (annotations, PR comment) is the only platform-specific part; the extract → compare → branch-on-exit-code core is identical on GitLab CI, Jenkins, and Azure DevOps.
|
|
251
272
|
|
|
252
273
|
### Exit codes
|
|
253
274
|
|
package/dist/lib/exit-codes.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type ErrorCode = "NAVIGATION_TIMEOUT" | "EXTRACTION_FAILED";
|
|
|
23
23
|
* is a generic RUNTIME failure. Reads only `err.message`, so a bare string or a
|
|
24
24
|
* malformed error never throws.
|
|
25
25
|
*/
|
|
26
|
-
export declare function classifyError(err:
|
|
26
|
+
export declare function classifyError(err: unknown): {
|
|
27
27
|
code: ErrorCode;
|
|
28
28
|
exit: number;
|
|
29
29
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../../lib/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAW,CAAC;AAK1E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../../lib/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAW,CAAC;AAK1E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,MAAM,CAAC,GAAG,MAAM,CAAE,GAA6B,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IAChE,IAAI,2EAA2E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing of CLI-supplied browser-context inputs (cookies, headers, screen
|
|
3
|
+
* size, locale) into Playwright context configuration. Split out of the
|
|
4
|
+
* extraction orchestrator so the untrusted-input surface is unit-testable and
|
|
5
|
+
* malformed input degrades to safe defaults instead of producing broken values.
|
|
6
|
+
*
|
|
7
|
+
* Written strict-clean (no implicit any, no null leaks) per the per-module
|
|
8
|
+
* strict ratchet, even though the global tsconfig is strict:false.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtractOptions } from '../types.js';
|
|
11
|
+
export interface ParsedCookie {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly value: string;
|
|
14
|
+
readonly url: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ScreenSize {
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
}
|
|
20
|
+
export type ColorScheme = 'light' | 'dark' | 'no-preference';
|
|
21
|
+
/**
|
|
22
|
+
* The subset of Playwright's BrowserContextOptions that we set. Declared
|
|
23
|
+
* explicitly (rather than `Record<string, any>`) so the config the extraction
|
|
24
|
+
* engine depends on is type-checked at the one call site where `browser` is an
|
|
25
|
+
* untyped handle.
|
|
26
|
+
*/
|
|
27
|
+
export interface ContextOptions {
|
|
28
|
+
viewport: ScreenSize;
|
|
29
|
+
screen: ScreenSize;
|
|
30
|
+
userAgent: string;
|
|
31
|
+
locale: string;
|
|
32
|
+
timezoneId: string;
|
|
33
|
+
extraHTTPHeaders: Record<string, string>;
|
|
34
|
+
colorScheme: ColorScheme;
|
|
35
|
+
permissions?: string[];
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_SCREEN: ScreenSize;
|
|
38
|
+
export declare const DEFAULT_LOCALE = "en-US";
|
|
39
|
+
export declare const DEFAULT_TIMEZONE = "America/New_York";
|
|
40
|
+
export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
|
|
41
|
+
/**
|
|
42
|
+
* Parse a "Name=value; Name2=value2" cookie string. Pairs without an "=" are
|
|
43
|
+
* skipped rather than emitted with a truncated name and the whole token as the
|
|
44
|
+
* value (the previous behavior). Empty names are also dropped.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseCookies(cookie: string | undefined, url: string): ParsedCookie[];
|
|
47
|
+
/**
|
|
48
|
+
* Parse a single "Name: value" header. Returns {} when absent or when no colon
|
|
49
|
+
* is present (an invalid header is ignored rather than guessed at).
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseHeader(header: string | undefined): Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a "WIDTHxHEIGHT" screen size. Falls back to the default when either
|
|
54
|
+
* dimension is missing, non-numeric, or non-positive, so a typo never produces
|
|
55
|
+
* a NaN viewport that silently breaks layout-dependent extraction. Returns a
|
|
56
|
+
* fresh object so callers can never mutate the shared default.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseScreenSize(screenSize: string | undefined): ScreenSize;
|
|
59
|
+
/**
|
|
60
|
+
* Derive the Accept-Language header. An explicit value wins; otherwise it is
|
|
61
|
+
* built from the locale, weighting the locale and its base language ahead of
|
|
62
|
+
* English.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deriveAcceptLanguage(locale: string, explicit?: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Build the Playwright context options from CLI options. Pure: no browser
|
|
67
|
+
* access, no mutation of the input. Cookies and init scripts are applied
|
|
68
|
+
* separately by the caller because they are side effects on a live context.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildContextOptions(options: ExtractOptions, browserName: string): ContextOptions;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const DEFAULT_SCREEN = Object.freeze({ width: 1920, height: 1080 });
|
|
2
|
+
export const DEFAULT_LOCALE = 'en-US';
|
|
3
|
+
export const DEFAULT_TIMEZONE = 'America/New_York';
|
|
4
|
+
export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
|
|
5
|
+
/**
|
|
6
|
+
* Parse a "Name=value; Name2=value2" cookie string. Pairs without an "=" are
|
|
7
|
+
* skipped rather than emitted with a truncated name and the whole token as the
|
|
8
|
+
* value (the previous behavior). Empty names are also dropped.
|
|
9
|
+
*/
|
|
10
|
+
export function parseCookies(cookie, url) {
|
|
11
|
+
if (!cookie)
|
|
12
|
+
return [];
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const raw of cookie.split(';')) {
|
|
15
|
+
const c = raw.trim();
|
|
16
|
+
if (!c)
|
|
17
|
+
continue;
|
|
18
|
+
const eq = c.indexOf('=');
|
|
19
|
+
if (eq < 1)
|
|
20
|
+
continue; // no "=", or "=value" with an empty name
|
|
21
|
+
out.push({ name: c.slice(0, eq).trim(), value: c.slice(eq + 1).trim(), url });
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a single "Name: value" header. Returns {} when absent or when no colon
|
|
27
|
+
* is present (an invalid header is ignored rather than guessed at).
|
|
28
|
+
*/
|
|
29
|
+
export function parseHeader(header) {
|
|
30
|
+
if (!header)
|
|
31
|
+
return {};
|
|
32
|
+
const colon = header.indexOf(':');
|
|
33
|
+
if (colon < 1)
|
|
34
|
+
return {};
|
|
35
|
+
const name = header.slice(0, colon).trim();
|
|
36
|
+
if (!name)
|
|
37
|
+
return {};
|
|
38
|
+
return { [name]: header.slice(colon + 1).trim() };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a "WIDTHxHEIGHT" screen size. Falls back to the default when either
|
|
42
|
+
* dimension is missing, non-numeric, or non-positive, so a typo never produces
|
|
43
|
+
* a NaN viewport that silently breaks layout-dependent extraction. Returns a
|
|
44
|
+
* fresh object so callers can never mutate the shared default.
|
|
45
|
+
*/
|
|
46
|
+
export function parseScreenSize(screenSize) {
|
|
47
|
+
if (!screenSize)
|
|
48
|
+
return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
|
|
49
|
+
const parts = screenSize.split('x');
|
|
50
|
+
if (parts.length !== 2)
|
|
51
|
+
return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
|
|
52
|
+
const width = Number(parts[0]);
|
|
53
|
+
const height = Number(parts[1]);
|
|
54
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
55
|
+
return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
|
|
56
|
+
}
|
|
57
|
+
return { width: Math.round(width), height: Math.round(height) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derive the Accept-Language header. An explicit value wins; otherwise it is
|
|
61
|
+
* built from the locale, weighting the locale and its base language ahead of
|
|
62
|
+
* English.
|
|
63
|
+
*/
|
|
64
|
+
export function deriveAcceptLanguage(locale, explicit) {
|
|
65
|
+
if (explicit)
|
|
66
|
+
return explicit;
|
|
67
|
+
const base = locale.split('-')[0];
|
|
68
|
+
return `${locale},${base};q=0.9,en;q=0.8`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the Playwright context options from CLI options. Pure: no browser
|
|
72
|
+
* access, no mutation of the input. Cookies and init scripts are applied
|
|
73
|
+
* separately by the caller because they are side effects on a live context.
|
|
74
|
+
*/
|
|
75
|
+
export function buildContextOptions(options, browserName) {
|
|
76
|
+
const locale = options.locale || DEFAULT_LOCALE;
|
|
77
|
+
const size = parseScreenSize(options.screenSize);
|
|
78
|
+
const extraHTTPHeaders = {
|
|
79
|
+
'Accept-Language': deriveAcceptLanguage(locale, options.acceptLanguage),
|
|
80
|
+
...parseHeader(options.header),
|
|
81
|
+
};
|
|
82
|
+
const contextOptions = {
|
|
83
|
+
viewport: { width: size.width, height: size.height },
|
|
84
|
+
screen: { width: size.width, height: size.height },
|
|
85
|
+
userAgent: options.userAgent || DEFAULT_USER_AGENT,
|
|
86
|
+
locale,
|
|
87
|
+
timezoneId: options.timezoneId || DEFAULT_TIMEZONE,
|
|
88
|
+
extraHTTPHeaders,
|
|
89
|
+
colorScheme: 'light',
|
|
90
|
+
};
|
|
91
|
+
if (browserName === 'chromium') {
|
|
92
|
+
contextOptions.permissions = ['clipboard-read', 'clipboard-write'];
|
|
93
|
+
}
|
|
94
|
+
return contextOptions;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=context-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-config.js","sourceRoot":"","sources":["../../../lib/extractors/context-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fault isolation for the parallel extractors. Each extractor runs inside
|
|
3
|
+
* guardExtractor so a single throw degrades only its own category — recording
|
|
4
|
+
* { stage, reason } and returning a fallback — instead of rejecting Promise.all
|
|
5
|
+
* and aborting the entire run. Kept in its own module so the isolation contract
|
|
6
|
+
* is unit-testable without a browser.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtractorError } from '../types.js';
|
|
9
|
+
export declare function guardExtractor<T>(stage: string, run: Promise<T>, fallback: T, sink: ExtractorError[]): Promise<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../lib/extractors/guard.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAa,EACb,GAAe,EACf,QAAW,EACX,IAAsB;IAEtB,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/E,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
1
2
|
import type { ExtractOptions, BrandingResult, Spinner } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* @param {string} url
|
|
@@ -6,4 +7,4 @@ import type { ExtractOptions, BrandingResult, Spinner } from '../types.js';
|
|
|
6
7
|
* @param {{ slow?: boolean, darkMode?: boolean, mobile?: boolean, wcag?: boolean, screenshotPath?: string, discoverLinks?: number|null, navigationTimeout?: number, stealth?: boolean, userAgent?: string, locale?: string, timezoneId?: string, acceptLanguage?: string, screenSize?: string }} [options]
|
|
7
8
|
* @returns {Promise<BrandingResult>}
|
|
8
9
|
*/
|
|
9
|
-
export declare function extractBranding(url: string, spinner: Spinner, browser:
|
|
10
|
+
export declare function extractBranding(url: string, spinner: Spinner, browser: Browser, options?: ExtractOptions): Promise<BrandingResult>;
|
|
@@ -10,6 +10,8 @@ import { extractBreakpoints, detectIconSystem, detectFrameworks, extractGradient
|
|
|
10
10
|
import { extractTeach } from './teach.js';
|
|
11
11
|
import { extractWcagPairs } from './colors.js';
|
|
12
12
|
import { SCHEMA_VERSION } from '../version.js';
|
|
13
|
+
import { buildContextOptions, parseCookies, parseScreenSize, DEFAULT_LOCALE } from './context-config.js';
|
|
14
|
+
import { guardExtractor } from './guard.js';
|
|
13
15
|
// Gaussian noise via Box-Muller
|
|
14
16
|
function gaussian(mean = 0, std = 1) {
|
|
15
17
|
let u, v;
|
|
@@ -45,7 +47,7 @@ function velocityProfile(t, overshootProb = 0.3) {
|
|
|
45
47
|
return 0.15 + Math.random() * 0.1; // corrective micro-movements
|
|
46
48
|
}
|
|
47
49
|
// Sleep helper
|
|
48
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
50
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
49
51
|
/**
|
|
50
52
|
* Adaptive readiness: resolve as soon as the page is actually settled — network
|
|
51
53
|
* quiet, web fonts loaded, and the DOM done mutating — instead of always waiting
|
|
@@ -252,49 +254,21 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
252
254
|
const timeoutMultiplier = options.slow ? 3 : 1;
|
|
253
255
|
const timeouts = [];
|
|
254
256
|
const degraded = []; // post-extraction stages that failed but did not abort the run
|
|
257
|
+
const extractorErrors = []; // scoped failures of the parallel extractors
|
|
255
258
|
// Progress lines print only in verbose mode (the main `dembrandt <url>`
|
|
256
259
|
// command). Report commands (drift/init/conformance) pass no verbose flag and
|
|
257
260
|
// stay clean. Warnings are NOT routed through this — they always print.
|
|
258
261
|
const log = (...args) => { if (options.verbose)
|
|
259
262
|
console.log(...args); };
|
|
260
263
|
spinner.text = "Creating browser context...";
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Parse "Name=value; Name2=value2" cookie string into Playwright format
|
|
268
|
-
const parsedCookies = options.cookie
|
|
269
|
-
? options.cookie.split(";").map((c) => c.trim()).filter(Boolean).map((c) => {
|
|
270
|
-
const eq = c.indexOf("=");
|
|
271
|
-
return {
|
|
272
|
-
name: c.slice(0, eq).trim(),
|
|
273
|
-
value: c.slice(eq + 1).trim(),
|
|
274
|
-
url,
|
|
275
|
-
};
|
|
276
|
-
})
|
|
277
|
-
: [];
|
|
278
|
-
const extraHeaders = { "Accept-Language": acceptLanguage };
|
|
279
|
-
if (options.header) {
|
|
280
|
-
const colon = options.header.indexOf(":");
|
|
281
|
-
if (colon > -1) {
|
|
282
|
-
extraHeaders[options.header.slice(0, colon).trim()] = options.header.slice(colon + 1).trim();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const contextOptions = {
|
|
286
|
-
viewport: { width: screenW, height: screenH },
|
|
287
|
-
screen: { width: screenW, height: screenH },
|
|
288
|
-
userAgent: options.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
289
|
-
locale,
|
|
290
|
-
timezoneId,
|
|
291
|
-
extraHTTPHeaders: extraHeaders,
|
|
292
|
-
colorScheme: "light",
|
|
293
|
-
};
|
|
294
|
-
if (browser.browserType().name() === 'chromium') {
|
|
295
|
-
contextOptions.permissions = ["clipboard-read", "clipboard-write"];
|
|
296
|
-
}
|
|
264
|
+
// locale, screenW and screenH are still needed below for the stealth init
|
|
265
|
+
// script; the rest of the context configuration is built purely in
|
|
266
|
+
// context-config.ts and unit-tested there.
|
|
267
|
+
const locale = options.locale || DEFAULT_LOCALE;
|
|
268
|
+
const { width: screenW, height: screenH } = parseScreenSize(options.screenSize);
|
|
269
|
+
const contextOptions = buildContextOptions(options, browser.browserType().name());
|
|
297
270
|
const context = await browser.newContext(contextOptions);
|
|
271
|
+
const parsedCookies = parseCookies(options.cookie, url);
|
|
298
272
|
if (parsedCookies.length > 0) {
|
|
299
273
|
await context.addCookies(parsedCookies);
|
|
300
274
|
}
|
|
@@ -694,27 +668,33 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
694
668
|
log(color.info("\n Extracting design tokens...\n"));
|
|
695
669
|
spinner.start("Analyzing design system (17 parallel tasks)...");
|
|
696
670
|
const [logoResult, colors, typography, spacing, borderRadius, borders, shadows, buttons, inputs, links, badges, breakpoints, iconSystem, frameworks, siteNameRaw, gradients, motion,] = await Promise.all([
|
|
697
|
-
extractLogo(page, url)
|
|
698
|
-
extractColors(page)
|
|
699
|
-
extractTypography(page)
|
|
700
|
-
extractSpacing(page)
|
|
701
|
-
extractBorderRadius(page)
|
|
702
|
-
extractBorders(page)
|
|
703
|
-
extractShadows(page)
|
|
704
|
-
extractButtonStyles(page)
|
|
705
|
-
extractInputStyles(page)
|
|
706
|
-
extractLinkStyles(page)
|
|
707
|
-
extractBadgeStyles(page)
|
|
708
|
-
extractBreakpoints(page)
|
|
709
|
-
detectIconSystem(page)
|
|
710
|
-
detectFrameworks(page)
|
|
711
|
-
extractSiteName(page)
|
|
712
|
-
extractGradients(page)
|
|
713
|
-
extractMotion(page)
|
|
671
|
+
guardExtractor('logo', extractLogo(page, url), { logo: null, instances: [], favicons: [], manifest: null }, extractorErrors),
|
|
672
|
+
guardExtractor('colors', extractColors(page), { semantic: {}, palette: [], cssVariables: [], _raw: [] }, extractorErrors),
|
|
673
|
+
guardExtractor('typography', extractTypography(page), { styles: [], sources: {} }, extractorErrors),
|
|
674
|
+
guardExtractor('spacing', extractSpacing(page), { scaleType: 'unknown', commonValues: [] }, extractorErrors),
|
|
675
|
+
guardExtractor('borderRadius', extractBorderRadius(page), { values: [] }, extractorErrors),
|
|
676
|
+
guardExtractor('borders', extractBorders(page), { combinations: [] }, extractorErrors),
|
|
677
|
+
guardExtractor('shadows', extractShadows(page), [], extractorErrors),
|
|
678
|
+
guardExtractor('buttons', extractButtonStyles(page), [], extractorErrors),
|
|
679
|
+
guardExtractor('inputs', extractInputStyles(page), [], extractorErrors),
|
|
680
|
+
guardExtractor('links', extractLinkStyles(page), [], extractorErrors),
|
|
681
|
+
guardExtractor('badges', extractBadgeStyles(page), { all: [], byVariant: {} }, extractorErrors),
|
|
682
|
+
guardExtractor('breakpoints', extractBreakpoints(page), [], extractorErrors),
|
|
683
|
+
guardExtractor('iconSystem', detectIconSystem(page), [], extractorErrors),
|
|
684
|
+
guardExtractor('frameworks', detectFrameworks(page), [], extractorErrors),
|
|
685
|
+
guardExtractor('siteName', extractSiteName(page), null, extractorErrors),
|
|
686
|
+
guardExtractor('gradients', extractGradients(page), [], extractorErrors),
|
|
687
|
+
guardExtractor('motion', extractMotion(page), { durations: [], easings: [], byContext: {} }, extractorErrors),
|
|
714
688
|
]);
|
|
715
689
|
const { logo, instances: logoInstances, favicons, manifest } = logoResult;
|
|
716
690
|
let siteName = siteNameRaw;
|
|
717
691
|
spinner.stop();
|
|
692
|
+
// Per-extractor failures are fault-isolated above and surface in meta.errors
|
|
693
|
+
// during result assembly. Print them once now, after the spinner stops, so a
|
|
694
|
+
// broken extractor is visible during a verbose run without clobbering it.
|
|
695
|
+
for (const e of extractorErrors) {
|
|
696
|
+
log(color.warning(` ! ${e.stage}: extraction failed (continuing) — ${e.reason}`));
|
|
697
|
+
}
|
|
718
698
|
// Inject manifest theme_color / background_color as high-confidence palette entries
|
|
719
699
|
try {
|
|
720
700
|
if (manifest) {
|
|
@@ -894,7 +874,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
894
874
|
}
|
|
895
875
|
if (['input', 'textarea', 'select', 'button', 'a'].includes(beforeState.tag)) {
|
|
896
876
|
try {
|
|
897
|
-
|
|
877
|
+
// ElementHandle.focus() takes no options; the focus is synchronous.
|
|
878
|
+
await element.focus();
|
|
898
879
|
await page.waitForTimeout(100 * timeoutMultiplier);
|
|
899
880
|
const afterFocus = await element.evaluate(el => {
|
|
900
881
|
function findBg(node) {
|
|
@@ -1244,6 +1225,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
1244
1225
|
}
|
|
1245
1226
|
if (degraded.length)
|
|
1246
1227
|
result.meta.degraded = degraded;
|
|
1228
|
+
if (extractorErrors.length)
|
|
1229
|
+
result.meta.errors = extractorErrors;
|
|
1247
1230
|
return result;
|
|
1248
1231
|
}
|
|
1249
1232
|
catch (error) {
|