multiclone 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -2
- package/agents/section-builder.md +37 -0
- package/commands/multiclone.md +15 -0
- package/dist/adapters/cli.js +129 -0
- package/dist/adapters/cli.js.map +1 -0
- package/dist/adapters/cliArgs.js +63 -0
- package/dist/adapters/cliArgs.js.map +1 -0
- package/dist/adapters/mcp.js +83 -0
- package/dist/adapters/mcp.js.map +1 -0
- package/dist/analyze/analyze.js +85 -0
- package/dist/analyze/analyze.js.map +1 -0
- package/dist/analyze/animations.js +340 -0
- package/dist/analyze/animations.js.map +1 -0
- package/dist/analyze/ast.js +182 -0
- package/dist/analyze/ast.js.map +1 -0
- package/dist/analyze/chunks.js +160 -0
- package/dist/analyze/chunks.js.map +1 -0
- package/dist/analyze/jobs/code.js +117 -0
- package/dist/analyze/jobs/code.js.map +1 -0
- package/dist/analyze/jobs/sections.js +356 -0
- package/dist/analyze/jobs/sections.js.map +1 -0
- package/dist/analyze/pretty.js +20 -0
- package/dist/analyze/pretty.js.map +1 -0
- package/dist/analyze/report.js +101 -0
- package/dist/analyze/report.js.map +1 -0
- package/dist/analyze/worker.js +71 -0
- package/dist/analyze/worker.js.map +1 -0
- package/dist/capture/capture.js +340 -0
- package/dist/capture/capture.js.map +1 -0
- package/dist/capture/collectors/a11y.js +92 -0
- package/dist/capture/collectors/a11y.js.map +1 -0
- package/dist/capture/collectors/animations.js +113 -0
- package/dist/capture/collectors/animations.js.map +1 -0
- package/dist/capture/collectors/breakpoints.js +31 -0
- package/dist/capture/collectors/breakpoints.js.map +1 -0
- package/dist/capture/collectors/consent.js +39 -0
- package/dist/capture/collectors/consent.js.map +1 -0
- package/dist/capture/collectors/coverage.js +121 -0
- package/dist/capture/collectors/coverage.js.map +1 -0
- package/dist/capture/collectors/dom.js +16 -0
- package/dist/capture/collectors/dom.js.map +1 -0
- package/dist/capture/collectors/fonts.js +154 -0
- package/dist/capture/collectors/fonts.js.map +1 -0
- package/dist/capture/collectors/frames.js +93 -0
- package/dist/capture/collectors/frames.js.map +1 -0
- package/dist/capture/collectors/index.js +61 -0
- package/dist/capture/collectors/index.js.map +1 -0
- package/dist/capture/collectors/interactions.js +207 -0
- package/dist/capture/collectors/interactions.js.map +1 -0
- package/dist/capture/collectors/intro.js +84 -0
- package/dist/capture/collectors/intro.js.map +1 -0
- package/dist/capture/collectors/listeners.js +84 -0
- package/dist/capture/collectors/listeners.js.map +1 -0
- package/dist/capture/collectors/motion.js +91 -0
- package/dist/capture/collectors/motion.js.map +1 -0
- package/dist/capture/collectors/network.js +288 -0
- package/dist/capture/collectors/network.js.map +1 -0
- package/dist/capture/collectors/probes.js +34 -0
- package/dist/capture/collectors/probes.js.map +1 -0
- package/dist/capture/collectors/runtime.js +38 -0
- package/dist/capture/collectors/runtime.js.map +1 -0
- package/dist/capture/collectors/scripts.js +98 -0
- package/dist/capture/collectors/scripts.js.map +1 -0
- package/dist/capture/collectors/sections.js +17 -0
- package/dist/capture/collectors/sections.js.map +1 -0
- package/dist/capture/collectors/shots.js +58 -0
- package/dist/capture/collectors/shots.js.map +1 -0
- package/dist/capture/collectors/snapshot.js +47 -0
- package/dist/capture/collectors/snapshot.js.map +1 -0
- package/dist/capture/collectors/sourcemaps.js +153 -0
- package/dist/capture/collectors/sourcemaps.js.map +1 -0
- package/dist/capture/collectors/storage.js +72 -0
- package/dist/capture/collectors/storage.js.map +1 -0
- package/dist/capture/context.js +40 -0
- package/dist/capture/context.js.map +1 -0
- package/dist/capture/hooks/candidates.js +104 -0
- package/dist/capture/hooks/candidates.js.map +1 -0
- package/dist/capture/hooks/consent.js +82 -0
- package/dist/capture/hooks/consent.js.map +1 -0
- package/dist/capture/hooks/recorders.js +507 -0
- package/dist/capture/hooks/recorders.js.map +1 -0
- package/dist/capture/hooks/sections.js +62 -0
- package/dist/capture/hooks/sections.js.map +1 -0
- package/dist/capture/hooks/styles.js +86 -0
- package/dist/capture/hooks/styles.js.map +1 -0
- package/dist/capture/probes.js +491 -0
- package/dist/capture/probes.js.map +1 -0
- package/dist/capture/report.js +115 -0
- package/dist/capture/report.js.map +1 -0
- package/dist/compare/compare.js +391 -0
- package/dist/compare/compare.js.map +1 -0
- package/dist/compare/image.js +105 -0
- package/dist/compare/image.js.map +1 -0
- package/dist/compare/probes.js +129 -0
- package/dist/compare/probes.js.map +1 -0
- package/dist/compare/report.js +99 -0
- package/dist/compare/report.js.map +1 -0
- package/dist/compare/session.js +139 -0
- package/dist/compare/session.js.map +1 -0
- package/dist/compare/shoot.js +62 -0
- package/dist/compare/shoot.js.map +1 -0
- package/dist/contracts/catalog.js +29 -0
- package/dist/contracts/catalog.js.map +1 -0
- package/dist/contracts/context.js +19 -0
- package/dist/contracts/context.js.map +1 -0
- package/dist/contracts/fields.js +16 -0
- package/dist/contracts/fields.js.map +1 -0
- package/dist/contracts/tools/analyze.js +38 -0
- package/dist/contracts/tools/analyze.js.map +1 -0
- package/dist/contracts/tools/capture.js +87 -0
- package/dist/contracts/tools/capture.js.map +1 -0
- package/dist/contracts/tools/compare.js +142 -0
- package/dist/contracts/tools/compare.js.map +1 -0
- package/dist/contracts/tools/doctor.js +23 -0
- package/dist/contracts/tools/doctor.js.map +1 -0
- package/dist/contracts/tools/inspect.js +41 -0
- package/dist/contracts/tools/inspect.js.map +1 -0
- package/dist/contracts/tools/method.js +60 -0
- package/dist/contracts/tools/method.js.map +1 -0
- package/dist/contracts/tools/mirror.js +53 -0
- package/dist/contracts/tools/mirror.js.map +1 -0
- package/dist/contracts/tools/project.js +61 -0
- package/dist/contracts/tools/project.js.map +1 -0
- package/dist/contracts/tools/session.js +195 -0
- package/dist/contracts/tools/session.js.map +1 -0
- package/dist/contracts/types.js +20 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/core/cdp.js +146 -0
- package/dist/core/cdp.js.map +1 -0
- package/dist/core/chrome.js +185 -0
- package/dist/core/chrome.js.map +1 -0
- package/dist/core/files.js +31 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/inpage.js +24 -0
- package/dist/core/inpage.js.map +1 -0
- package/dist/core/log.js +10 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/page.js +269 -0
- package/dist/core/page.js.map +1 -0
- package/dist/doctor.js +37 -0
- package/dist/doctor.js.map +1 -0
- package/dist/domain/breakpoints.js +47 -0
- package/dist/domain/breakpoints.js.map +1 -0
- package/dist/domain/budget.js +38 -0
- package/dist/domain/budget.js.map +1 -0
- package/dist/domain/compare.js +138 -0
- package/dist/domain/compare.js.map +1 -0
- package/dist/domain/coverage.js +96 -0
- package/dist/domain/coverage.js.map +1 -0
- package/dist/domain/motion.js +142 -0
- package/dist/domain/motion.js.map +1 -0
- package/dist/domain/paths.js +55 -0
- package/dist/domain/paths.js.map +1 -0
- package/dist/domain/result.js +38 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/domain/routes.js +160 -0
- package/dist/domain/routes.js.map +1 -0
- package/dist/domain/sections.js +94 -0
- package/dist/domain/sections.js.map +1 -0
- package/dist/domain/snapshot.js +268 -0
- package/dist/domain/snapshot.js.map +1 -0
- package/dist/domain/sourcemap.js +95 -0
- package/dist/domain/sourcemap.js.map +1 -0
- package/dist/domain/stack.js +342 -0
- package/dist/domain/stack.js.map +1 -0
- package/dist/domain/tokens.js +183 -0
- package/dist/domain/tokens.js.map +1 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/domain/viewports.js +45 -0
- package/dist/domain/viewports.js.map +1 -0
- package/dist/mirror/rewrite.js +56 -0
- package/dist/mirror/rewrite.js.map +1 -0
- package/dist/mirror/server.js +191 -0
- package/dist/mirror/server.js.map +1 -0
- package/dist/mirror/store.js +86 -0
- package/dist/mirror/store.js.map +1 -0
- package/dist/project/assets.js +69 -0
- package/dist/project/assets.js.map +1 -0
- package/dist/project/draft.js +860 -0
- package/dist/project/draft.js.map +1 -0
- package/dist/project/method.js +154 -0
- package/dist/project/method.js.map +1 -0
- package/dist/project/scaffold.js +142 -0
- package/dist/project/scaffold.js.map +1 -0
- package/dist/session/discover.js +53 -0
- package/dist/session/discover.js.map +1 -0
- package/dist/session/proc.js +113 -0
- package/dist/session/proc.js.map +1 -0
- package/dist/session/progress.js +83 -0
- package/dist/session/progress.js.map +1 -0
- package/dist/session/session.js +450 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/state.js +47 -0
- package/dist/session/state.js.map +1 -0
- package/dist/store/inspect.js +212 -0
- package/dist/store/inspect.js.map +1 -0
- package/dist/store/run.js +112 -0
- package/dist/store/run.js.map +1 -0
- package/dist/store/summary.js +69 -0
- package/dist/store/summary.js.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/install.sh +29 -0
- package/package.json +67 -4
- package/skills/multiclone/SKILL.md +123 -0
- package/skills/multiclone/references/capture-format.md +73 -0
- package/skills/multiclone/references/fidelity.md +54 -0
- package/skills/multiclone/references/quality.md +47 -0
- package/skills/multiclone/references/stacks.md +156 -0
- package/templates/react-vite/.prettierignore +5 -0
- package/templates/react-vite/.prettierrc.json +6 -0
- package/templates/react-vite/eslint.config.js +38 -0
- package/templates/react-vite/gitignore +4 -0
- package/templates/react-vite/index.html +12 -0
- package/templates/react-vite/package.json +36 -0
- package/templates/react-vite/src/app/App.tsx +15 -0
- package/templates/react-vite/src/app/Page.tsx +11 -0
- package/templates/react-vite/src/app/routes.ts +7 -0
- package/templates/react-vite/src/components/README.md +7 -0
- package/templates/react-vite/src/content/README.md +2 -0
- package/templates/react-vite/src/hooks/useActiveSection.ts +37 -0
- package/templates/react-vite/src/hooks/useInView.ts +45 -0
- package/templates/react-vite/src/hooks/useMediaQuery.ts +15 -0
- package/templates/react-vite/src/lib/cn.ts +4 -0
- package/templates/react-vite/src/lib/math.ts +21 -0
- package/templates/react-vite/src/lib/mc.ts +13 -0
- package/templates/react-vite/src/lib/media.ts +4 -0
- package/templates/react-vite/src/main.tsx +13 -0
- package/templates/react-vite/src/providers/SmoothScroll.tsx +24 -0
- package/templates/react-vite/src/styles/base.css +42 -0
- package/templates/react-vite/src/styles/draft.css +1 -0
- package/templates/react-vite/src/styles/fonts.css +1 -0
- package/templates/react-vite/src/styles/index.css +4 -0
- package/templates/react-vite/src/styles/tokens.css +3 -0
- package/templates/react-vite/src/vite-env.d.ts +1 -0
- package/templates/react-vite/tsconfig.app.json +29 -0
- package/templates/react-vite/tsconfig.json +4 -0
- package/templates/react-vite/tsconfig.node.json +19 -0
- package/templates/react-vite/vite.config.ts +37 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inpage.js","sourceRoot":"","sources":["../../src/core/inpage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQlD;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;yBACN,YAAY,CAAC,QAAQ,EAAE;;;;;;;;;;;;;;;;KAgB3C,CAAC"}
|
package/dist/core/log.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Progress goes to stderr so stdout stays clean for JSON output and the MCP transport. */
|
|
2
|
+
let quiet = false;
|
|
3
|
+
export function setQuiet(value) {
|
|
4
|
+
quiet = value;
|
|
5
|
+
}
|
|
6
|
+
export function log(message) {
|
|
7
|
+
if (!quiet)
|
|
8
|
+
process.stderr.write(`${message}\n`);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/core/log.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,IAAI,KAAK,GAAG,KAAK,CAAC;AAElB,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,KAAK,GAAG,KAAK,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { sleep } from './files.js';
|
|
2
|
+
import { IN_PAGE_HELPERS } from './inpage.js';
|
|
3
|
+
/** Scripts multiclone itself evaluates carry these URLs so script capture can ignore them. */
|
|
4
|
+
export const EVAL_SOURCE_URL = '__multiclone_eval__';
|
|
5
|
+
export const INIT_SOURCE_URL = '__multiclone_init__';
|
|
6
|
+
const SETTLE_OBSERVER = `(() => {
|
|
7
|
+
if (window.__mcSettle) { window.__mcSettle.last = performance.now(); return true; }
|
|
8
|
+
const state = { last: performance.now() };
|
|
9
|
+
Object.defineProperty(window, '__mcSettle', { value: state, configurable: true });
|
|
10
|
+
new MutationObserver(() => { state.last = performance.now(); })
|
|
11
|
+
.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
12
|
+
return true;
|
|
13
|
+
})()`;
|
|
14
|
+
const SETTLE_STATE = `(() => {
|
|
15
|
+
const quietMs = performance.now() - (window.__mcSettle ? window.__mcSettle.last : 0);
|
|
16
|
+
let finite = 0;
|
|
17
|
+
try {
|
|
18
|
+
for (const a of document.getAnimations()) {
|
|
19
|
+
if (a.playState !== 'running') continue;
|
|
20
|
+
const timing = a.effect && a.effect.getComputedTiming ? a.effect.getComputedTiming() : null;
|
|
21
|
+
if (timing && Number.isFinite(timing.endTime) && timing.iterations !== Infinity) finite++;
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
return { quietMs, finite };
|
|
25
|
+
})()`;
|
|
26
|
+
export const withInitUrl = (source) => `${source}\n//# sourceURL=${INIT_SOURCE_URL}`;
|
|
27
|
+
const MOBILE_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{v} Mobile Safari/537.36';
|
|
28
|
+
const DESKTOP_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{v} Safari/537.36';
|
|
29
|
+
/** Hides the automation tells most bot checks read first; never throws. */
|
|
30
|
+
export const STEALTH_SCRIPT = `(() => { try {
|
|
31
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', { get: () => false, configurable: true });
|
|
32
|
+
if (!window.chrome) Object.defineProperty(window, 'chrome', { value: { runtime: {} }, configurable: true });
|
|
33
|
+
} catch {} })();`;
|
|
34
|
+
/** A browser tab driven through CDP, with the helpers every multiclone command needs. */
|
|
35
|
+
/** Input events wait for the renderer; this caps how long one may block. */
|
|
36
|
+
const INPUT_TIMEOUT_MS = 5_000;
|
|
37
|
+
const SHOT_TIMEOUT_MS = 20_000;
|
|
38
|
+
export class Page {
|
|
39
|
+
cdp;
|
|
40
|
+
browserVersion;
|
|
41
|
+
constructor(cdp, browserVersion) {
|
|
42
|
+
this.cdp = cdp;
|
|
43
|
+
this.browserVersion = browserVersion;
|
|
44
|
+
}
|
|
45
|
+
async init() {
|
|
46
|
+
await this.cdp.send('Page.enable');
|
|
47
|
+
await this.cdp.send('Runtime.enable');
|
|
48
|
+
await this.addInitScript(STEALTH_SCRIPT);
|
|
49
|
+
}
|
|
50
|
+
/** Real-device metrics and a non-headless user agent, so sites serve what visitors see. */
|
|
51
|
+
async setViewport(viewport) {
|
|
52
|
+
const version = this.browserVersion.split('/')[1] ?? '130.0.0.0';
|
|
53
|
+
await this.cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
54
|
+
width: viewport.width,
|
|
55
|
+
height: viewport.height,
|
|
56
|
+
deviceScaleFactor: 1,
|
|
57
|
+
mobile: viewport.mobile,
|
|
58
|
+
});
|
|
59
|
+
await this.cdp.send('Emulation.setTouchEmulationEnabled', viewport.mobile ? { enabled: true, maxTouchPoints: 5 } : { enabled: false });
|
|
60
|
+
await this.cdp.send('Emulation.setUserAgentOverride', {
|
|
61
|
+
userAgent: (viewport.mobile ? MOBILE_UA : DESKTOP_UA).replace('{v}', version),
|
|
62
|
+
platform: viewport.mobile ? 'Android' : 'MacIntel',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Navigates and waits for load plus a settle period for intro animations and lazy hydration. */
|
|
66
|
+
async goto(url, settleMs = 4000) {
|
|
67
|
+
const loadEvent = this.cdp.waitFor('Page.loadEventFired', 30_000);
|
|
68
|
+
const result = await this.cdp.send('Page.navigate', { url }, 30_000);
|
|
69
|
+
if (result.errorText)
|
|
70
|
+
throw new Error(`Could not open ${url}: ${result.errorText}`);
|
|
71
|
+
// A page that never fires load (endless streaming, a stuck script) is still worth a look.
|
|
72
|
+
const loaded = await loadEvent;
|
|
73
|
+
await sleep(settleMs);
|
|
74
|
+
return { loaded };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Waits until the network is idle, the DOM stops mutating and no finite animation is running,
|
|
78
|
+
* capped at `maxMs`. Infinite loops (marquees, spinners) don't hold it up.
|
|
79
|
+
*/
|
|
80
|
+
async settle(maxMs, signal) {
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
const deadline = started + Math.max(0, maxMs);
|
|
83
|
+
let inflight = 0;
|
|
84
|
+
let lastNetwork = Date.now();
|
|
85
|
+
const offs = [
|
|
86
|
+
this.cdp.on('Network.requestWillBeSent', () => {
|
|
87
|
+
inflight++;
|
|
88
|
+
lastNetwork = Date.now();
|
|
89
|
+
}),
|
|
90
|
+
this.cdp.on('Network.loadingFinished', () => {
|
|
91
|
+
inflight = Math.max(0, inflight - 1);
|
|
92
|
+
lastNetwork = Date.now();
|
|
93
|
+
}),
|
|
94
|
+
this.cdp.on('Network.loadingFailed', () => {
|
|
95
|
+
inflight = Math.max(0, inflight - 1);
|
|
96
|
+
lastNetwork = Date.now();
|
|
97
|
+
}),
|
|
98
|
+
];
|
|
99
|
+
await this.evaluate(SETTLE_OBSERVER).catch(() => undefined);
|
|
100
|
+
try {
|
|
101
|
+
await sleep(Math.min(500, maxMs));
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
const state = await this.evaluate(SETTLE_STATE).catch(() => ({ quietMs: 0, finite: 0 }));
|
|
105
|
+
const networkQuiet = inflight <= 2 && Date.now() - lastNetwork > 500;
|
|
106
|
+
if (networkQuiet && state.quietMs > 600 && state.finite === 0) {
|
|
107
|
+
return { waitedMs: Date.now() - started, reason: 'settled' };
|
|
108
|
+
}
|
|
109
|
+
await sleep(250);
|
|
110
|
+
}
|
|
111
|
+
return { waitedMs: Date.now() - started, reason: 'cap' };
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
for (const off of offs)
|
|
115
|
+
off();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Runs `source` before any page script in every new document of this target. */
|
|
119
|
+
async addInitScript(source) {
|
|
120
|
+
const { identifier } = await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', { source: withInitUrl(source), runImmediately: true });
|
|
121
|
+
return identifier;
|
|
122
|
+
}
|
|
123
|
+
async evaluate(expression) {
|
|
124
|
+
const response = await this.cdp.send('Runtime.evaluate', {
|
|
125
|
+
expression: `${expression}\n//# sourceURL=${EVAL_SOURCE_URL}`,
|
|
126
|
+
returnByValue: true,
|
|
127
|
+
awaitPromise: true,
|
|
128
|
+
});
|
|
129
|
+
if (response.exceptionDetails) {
|
|
130
|
+
const details = response.exceptionDetails;
|
|
131
|
+
throw new Error(`Page script failed: ${details.exception?.description ?? details.text}`);
|
|
132
|
+
}
|
|
133
|
+
return response.result.value;
|
|
134
|
+
}
|
|
135
|
+
/** Runs a self-contained function inside the page; it must not close over Node values. */
|
|
136
|
+
run(fn, arg) {
|
|
137
|
+
return this.evaluate(`(${fn.toString()})(${JSON.stringify(arg)})`);
|
|
138
|
+
}
|
|
139
|
+
/** Like run, plus shared in-page helpers (stable element paths). */
|
|
140
|
+
runWith(fn, arg) {
|
|
141
|
+
return this.evaluate(`(${fn.toString()})(${JSON.stringify(arg)}, ${IN_PAGE_HELPERS})`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whole-page screenshot up to `maxHeight` px, rendered without scrolling (fixed headers appear
|
|
145
|
+
* once at the top; scroll-linked effects show their top-of-page state).
|
|
146
|
+
*/
|
|
147
|
+
async fullPageScreenshot(maxHeight = 16_000) {
|
|
148
|
+
const height = Math.min(await this.pageHeight(), maxHeight);
|
|
149
|
+
const width = await this.evaluate('document.documentElement.clientWidth');
|
|
150
|
+
const { data } = await this.cdp.send('Page.captureScreenshot', {
|
|
151
|
+
format: 'png',
|
|
152
|
+
captureBeyondViewport: true,
|
|
153
|
+
clip: { x: 0, y: 0, width, height, scale: 1 },
|
|
154
|
+
}, 60_000);
|
|
155
|
+
return { png: Buffer.from(data, 'base64'), height };
|
|
156
|
+
}
|
|
157
|
+
/** Viewport screenshot, or a clip in viewport (CSS px) coordinates. */
|
|
158
|
+
async screenshot(clip) {
|
|
159
|
+
const { data } = await this.cdp.send('Page.captureScreenshot', {
|
|
160
|
+
format: 'png',
|
|
161
|
+
...(clip === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: {
|
|
164
|
+
clip: {
|
|
165
|
+
x: Math.max(0, clip.x),
|
|
166
|
+
y: Math.max(0, clip.y),
|
|
167
|
+
width: Math.max(1, clip.width),
|
|
168
|
+
height: Math.max(1, clip.height),
|
|
169
|
+
scale: 1,
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
}, SHOT_TIMEOUT_MS);
|
|
173
|
+
return Buffer.from(data, 'base64');
|
|
174
|
+
}
|
|
175
|
+
scrollY() {
|
|
176
|
+
return this.evaluate('Math.round(window.scrollY)');
|
|
177
|
+
}
|
|
178
|
+
maxScroll() {
|
|
179
|
+
return this.evaluate('Math.max(0, Math.round(document.documentElement.scrollHeight - window.innerHeight))');
|
|
180
|
+
}
|
|
181
|
+
pageHeight() {
|
|
182
|
+
return this.evaluate('Math.round(document.documentElement.scrollHeight)');
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Scrolls with real mouse-wheel events so smooth-scroll libraries (Lenis, Locomotive, GSAP
|
|
186
|
+
* ScrollSmoother) move the way they do for a visitor. A busy renderer can stall input events,
|
|
187
|
+
* so each one gets a short limit and the whole scroll a deadline; then it jumps instead.
|
|
188
|
+
*/
|
|
189
|
+
async scrollTo(target, viewport, deadlineMs = 20_000) {
|
|
190
|
+
const goal = Math.max(0, Math.min(target, await this.maxScroll()));
|
|
191
|
+
const deadline = Date.now() + deadlineMs;
|
|
192
|
+
let stalls = 0;
|
|
193
|
+
let previous = await this.scrollY();
|
|
194
|
+
for (let step = 0; step < 400; step++) {
|
|
195
|
+
const delta = goal - previous;
|
|
196
|
+
if (Math.abs(delta) < 2)
|
|
197
|
+
return previous;
|
|
198
|
+
if (Date.now() > deadline)
|
|
199
|
+
break;
|
|
200
|
+
const sent = await this.cdp
|
|
201
|
+
.send('Input.dispatchMouseEvent', {
|
|
202
|
+
type: 'mouseWheel',
|
|
203
|
+
x: Math.round(viewport.width / 2),
|
|
204
|
+
y: Math.round(viewport.height / 2),
|
|
205
|
+
deltaX: 0,
|
|
206
|
+
deltaY: Math.max(-600, Math.min(600, delta)),
|
|
207
|
+
}, INPUT_TIMEOUT_MS)
|
|
208
|
+
.then(() => true)
|
|
209
|
+
.catch(() => false);
|
|
210
|
+
if (!sent)
|
|
211
|
+
break;
|
|
212
|
+
await sleep(Math.abs(delta) < 600 ? 350 : 80);
|
|
213
|
+
const current = await this.scrollY();
|
|
214
|
+
stalls = Math.abs(current - previous) < 1 ? stalls + 1 : 0;
|
|
215
|
+
previous = current;
|
|
216
|
+
if (stalls >= 8)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
return this.jumpTo(goal);
|
|
220
|
+
}
|
|
221
|
+
/** Instant scroll (no wheel events), waiting for two frames so the next paint is current. */
|
|
222
|
+
async jumpTo(target) {
|
|
223
|
+
return this.evaluate(`new Promise((done) => { const y = ${String(Math.max(0, Math.round(target)))}; const l = window.lenis ?? window.__lenis; try { if (l && typeof l.scrollTo === 'function') l.scrollTo(y, { immediate: true, force: true }); } catch {} window.scrollTo(0, y); const t = setTimeout(() => done(Math.round(window.scrollY)), 500); requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); done(Math.round(window.scrollY)); })); })`);
|
|
224
|
+
}
|
|
225
|
+
/** Moves the pointer; a stalled renderer costs a short wait, never the whole step. */
|
|
226
|
+
async pointer(x, y) {
|
|
227
|
+
return this.cdp
|
|
228
|
+
.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: Math.round(x), y: Math.round(y) }, INPUT_TIMEOUT_MS)
|
|
229
|
+
.then(() => true)
|
|
230
|
+
.catch(() => false);
|
|
231
|
+
}
|
|
232
|
+
/** Walks to the bottom (pages often grow as they lazy-load), then returns to the top. */
|
|
233
|
+
async autoScroll(viewport, budgetMs = 60_000) {
|
|
234
|
+
const started = Date.now();
|
|
235
|
+
let y = 0;
|
|
236
|
+
for (;;) {
|
|
237
|
+
const max = await this.maxScroll();
|
|
238
|
+
if (y >= max || Date.now() - started > budgetMs)
|
|
239
|
+
break;
|
|
240
|
+
y = Math.min(max, y + Math.round(viewport.height * 0.8));
|
|
241
|
+
await this.scrollTo(y, viewport, Math.max(1_000, budgetMs - (Date.now() - started)));
|
|
242
|
+
await sleep(250);
|
|
243
|
+
}
|
|
244
|
+
const height = await this.pageHeight();
|
|
245
|
+
await this.jumpTo(0);
|
|
246
|
+
await sleep(500);
|
|
247
|
+
return height;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Evenly spaced scroll positions across the page, always including the top and the bottom. */
|
|
251
|
+
export function spreadPositions(maxScroll, count) {
|
|
252
|
+
if (count <= 1 || maxScroll <= 0)
|
|
253
|
+
return [0];
|
|
254
|
+
const positions = new Set();
|
|
255
|
+
for (let index = 0; index < count; index++) {
|
|
256
|
+
positions.add(Math.round((maxScroll * index) / (count - 1)));
|
|
257
|
+
}
|
|
258
|
+
return [...positions];
|
|
259
|
+
}
|
|
260
|
+
export function parsePositions(spec) {
|
|
261
|
+
if (!spec)
|
|
262
|
+
return null;
|
|
263
|
+
const values = spec.split(',').map((part) => Number(part.trim()));
|
|
264
|
+
if (values.some((value) => !Number.isFinite(value) || value < 0)) {
|
|
265
|
+
throw new Error(`Bad positions "${spec}". Use comma-separated pixel offsets like 0,900,2400.`);
|
|
266
|
+
}
|
|
267
|
+
return values;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/core/page.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { findChrome, launchBrowser } from './core/chrome.js';
|
|
2
|
+
import { Page } from './core/page.js';
|
|
3
|
+
export async function doctor() {
|
|
4
|
+
const checks = [];
|
|
5
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
6
|
+
checks.push({
|
|
7
|
+
name: 'Node.js ≥ 22.12',
|
|
8
|
+
ok: major > 22 || (major === 22 && minor >= 12),
|
|
9
|
+
detail: process.versions.node,
|
|
10
|
+
});
|
|
11
|
+
const chrome = findChrome();
|
|
12
|
+
checks.push({
|
|
13
|
+
name: 'Chrome / Chromium',
|
|
14
|
+
ok: chrome !== null,
|
|
15
|
+
detail: chrome ?? 'not found — install Chrome or set MULTICLONE_CHROME',
|
|
16
|
+
});
|
|
17
|
+
if (chrome) {
|
|
18
|
+
try {
|
|
19
|
+
const browser = await launchBrowser();
|
|
20
|
+
const page = new Page(browser.cdp, browser.version);
|
|
21
|
+
await page.init();
|
|
22
|
+
const webgl = await page.evaluate("!!document.createElement('canvas').getContext('webgl2')");
|
|
23
|
+
await browser.close();
|
|
24
|
+
checks.push({ name: 'Headless launch', ok: true, detail: browser.version });
|
|
25
|
+
checks.push({
|
|
26
|
+
name: 'WebGL in headless',
|
|
27
|
+
ok: webgl,
|
|
28
|
+
detail: webgl ? 'available' : 'unavailable — WebGL sites will screenshot blank',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
checks.push({ name: 'Headless launch', ok: false, detail: String(error) });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return checks;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAQtC,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,iBAAiB;QACvB,EAAE,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,KAAK,KAAK,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC;QAC/C,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;KAC9B,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,mBAAmB;QACzB,EAAE,EAAE,MAAM,KAAK,IAAI;QACnB,MAAM,EAAE,MAAM,IAAI,qDAAqD;KACxE,CAAC,CAAC;IACH,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC/B,yDAAyD,CAC1D,CAAC;YACF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACtB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5E,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,mBAAmB;gBACzB,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,iDAAiD;aAChF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const toPx = (value, unit) => unit === 'em' || unit === 'rem' ? Math.round(value * 16) : Math.round(value);
|
|
2
|
+
/** Every width used in @media queries (min/max-width and range syntax), most used first. */
|
|
3
|
+
export function extractBreakpoints(css) {
|
|
4
|
+
const counts = new Map();
|
|
5
|
+
const add = (px, kind) => {
|
|
6
|
+
if (!Number.isFinite(px) || px <= 0 || px > 5000)
|
|
7
|
+
return;
|
|
8
|
+
const key = `${kind}|${String(px)}`;
|
|
9
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
10
|
+
};
|
|
11
|
+
for (const media of css.matchAll(/@media([^{]+)\{/g)) {
|
|
12
|
+
const query = media[1] ?? '';
|
|
13
|
+
for (const match of query.matchAll(/(min|max)-width\s*:\s*([\d.]+)(px|em|rem)/g)) {
|
|
14
|
+
add(toPx(Number(match[2]), match[3] ?? 'px'), match[1] === 'min' ? 'min-width' : 'max-width');
|
|
15
|
+
}
|
|
16
|
+
for (const match of query.matchAll(/width\s*(>=|>|<=|<)\s*([\d.]+)(px|em|rem)/g)) {
|
|
17
|
+
add(toPx(Number(match[2]), match[3] ?? 'px'), match[1]?.startsWith('>') ? 'min-width' : 'max-width');
|
|
18
|
+
}
|
|
19
|
+
for (const match of query.matchAll(/([\d.]+)(px|em|rem)\s*(<=|<|>=|>)\s*width/g)) {
|
|
20
|
+
add(toPx(Number(match[1]), match[2] ?? 'px'), match[3]?.startsWith('<') ? 'min-width' : 'max-width');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...counts]
|
|
24
|
+
.map(([key, uses]) => {
|
|
25
|
+
const [kind, px] = key.split('|');
|
|
26
|
+
return {
|
|
27
|
+
px: Number(px),
|
|
28
|
+
kind: kind === 'min-width' ? 'min-width' : 'max-width',
|
|
29
|
+
uses,
|
|
30
|
+
};
|
|
31
|
+
})
|
|
32
|
+
.sort((a, b) => b.uses - a.uses || a.px - b.px);
|
|
33
|
+
}
|
|
34
|
+
/** Major breakpoints worth an extra capture: used at least `minUses` times, merged within 40px. */
|
|
35
|
+
export function majorBreakpoints(points, minUses = 3) {
|
|
36
|
+
const widths = points
|
|
37
|
+
.filter((point) => point.uses >= minUses)
|
|
38
|
+
.map((point) => (point.kind === 'max-width' ? point.px : point.px));
|
|
39
|
+
const merged = [];
|
|
40
|
+
for (const width of [...new Set(widths)].sort((a, b) => a - b)) {
|
|
41
|
+
const last = merged.at(-1);
|
|
42
|
+
if (last === undefined || width - last > 40)
|
|
43
|
+
merged.push(width);
|
|
44
|
+
}
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=breakpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breakpoints.js","sourceRoot":"","sources":["../../src/domain/breakpoints.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ByteBudget {
|
|
2
|
+
name;
|
|
3
|
+
limits;
|
|
4
|
+
#used = 0;
|
|
5
|
+
#omitted = [];
|
|
6
|
+
constructor(name, limits) {
|
|
7
|
+
this.name = name;
|
|
8
|
+
this.limits = limits;
|
|
9
|
+
}
|
|
10
|
+
/** Reserve bytes for one item; false means skip it (the omission is already recorded). */
|
|
11
|
+
take(what, bytes) {
|
|
12
|
+
if (bytes > this.limits.itemBytes) {
|
|
13
|
+
this.#omitted.push({ what, bytes, reason: 'item-limit' });
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (this.#used + bytes > this.limits.totalBytes) {
|
|
17
|
+
this.#omitted.push({ what, bytes, reason: 'total-limit' });
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
this.#used += bytes;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
get used() {
|
|
24
|
+
return this.#used;
|
|
25
|
+
}
|
|
26
|
+
report() {
|
|
27
|
+
return { name: this.name, used: this.#used, limits: this.limits, omitted: [...this.#omitted] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const MB = 1024 * 1024;
|
|
31
|
+
export const DEFAULT_BUDGETS = {
|
|
32
|
+
network: { itemBytes: 200 * MB, totalBytes: 2048 * MB },
|
|
33
|
+
scripts: { itemBytes: 32 * MB, totalBytes: 256 * MB },
|
|
34
|
+
sourcemaps: { itemBytes: 64 * MB, totalBytes: 256 * MB },
|
|
35
|
+
websocket: { itemBytes: 1 * MB, totalBytes: 16 * MB },
|
|
36
|
+
runtime: { itemBytes: 8 * MB, totalBytes: 64 * MB },
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/domain/budget.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,UAAU;IAKV;IACA;IALX,KAAK,GAAG,CAAC,CAAC;IACD,QAAQ,GAAqB,EAAE,CAAC;IAEzC,YACW,IAAY,EACZ,MAAoB;QADpB,SAAI,GAAJ,IAAI,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAc;IAC5B,CAAC;IAEJ,0FAA0F;IAC1F,IAAI,CAAC,IAAY,EAAE,KAAa;QAC9B,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;YAC1D,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YAChD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;YAC3D,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;IACjG,CAAC;CACF;AAED,MAAM,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvB,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,EAAE,SAAS,EAAE,GAAG,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,GAAG,EAAE,EAAE;IACvD,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,GAAG,GAAG,EAAE,EAAE;IACrD,UAAU,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,GAAG,GAAG,EAAE,EAAE;IACxD,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,EAAE;IACrD,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,EAAE;CACJ,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Pure scoring and alignment helpers for compare. */
|
|
2
|
+
const norm = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
3
|
+
/**
|
|
4
|
+
* Aligns sections by exact key first, then by normalised heading/text prefix; remaining sections
|
|
5
|
+
* stay unpaired (reported as missing or extra, never silently dropped).
|
|
6
|
+
*/
|
|
7
|
+
export function alignSections(original, clone) {
|
|
8
|
+
const pairs = [];
|
|
9
|
+
const usedClone = new Set();
|
|
10
|
+
const byKey = new Map();
|
|
11
|
+
clone.forEach((section, index) => {
|
|
12
|
+
if (!byKey.has(section.key))
|
|
13
|
+
byKey.set(section.key, index);
|
|
14
|
+
});
|
|
15
|
+
const pending = [];
|
|
16
|
+
for (const section of original) {
|
|
17
|
+
const index = byKey.get(section.key);
|
|
18
|
+
if (index !== undefined && !usedClone.has(index)) {
|
|
19
|
+
usedClone.add(index);
|
|
20
|
+
pairs.push({
|
|
21
|
+
key: section.key,
|
|
22
|
+
label: section.label,
|
|
23
|
+
original: section,
|
|
24
|
+
clone: clone[index] ?? null,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
pending.push(section);
|
|
29
|
+
}
|
|
30
|
+
for (const section of pending) {
|
|
31
|
+
const anchor = norm(section.label || section.text).slice(0, 40);
|
|
32
|
+
let found = -1;
|
|
33
|
+
if (anchor.length >= 4) {
|
|
34
|
+
found = clone.findIndex((candidate, index) => !usedClone.has(index) &&
|
|
35
|
+
(norm(candidate.label).startsWith(anchor) ||
|
|
36
|
+
norm(candidate.text).slice(0, 200).includes(anchor)));
|
|
37
|
+
}
|
|
38
|
+
if (found >= 0) {
|
|
39
|
+
usedClone.add(found);
|
|
40
|
+
pairs.push({
|
|
41
|
+
key: section.key,
|
|
42
|
+
label: section.label,
|
|
43
|
+
original: section,
|
|
44
|
+
clone: clone[found] ?? null,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
pairs.push({ key: section.key, label: section.label, original: section, clone: null });
|
|
49
|
+
}
|
|
50
|
+
clone.forEach((section, index) => {
|
|
51
|
+
if (!usedClone.has(index))
|
|
52
|
+
pairs.push({ key: section.key, label: section.label, original: null, clone: section });
|
|
53
|
+
});
|
|
54
|
+
const order = (pair) => pair.original?.top ?? (pair.clone?.top ?? 0) + 0.5;
|
|
55
|
+
return pairs.sort((a, b) => order(a) - order(b));
|
|
56
|
+
}
|
|
57
|
+
/** Word-level Jaccard similarity of two texts (0–1); 1 when both are empty. */
|
|
58
|
+
export function textSimilarity(a, b) {
|
|
59
|
+
const words = (text) => new Set(norm(text)
|
|
60
|
+
.split(' ')
|
|
61
|
+
.filter((word) => word.length > 1));
|
|
62
|
+
const left = words(a);
|
|
63
|
+
const right = words(b);
|
|
64
|
+
if (!left.size && !right.size)
|
|
65
|
+
return 1;
|
|
66
|
+
let shared = 0;
|
|
67
|
+
for (const word of left)
|
|
68
|
+
if (right.has(word))
|
|
69
|
+
shared++;
|
|
70
|
+
return shared / (left.size + right.size - shared);
|
|
71
|
+
}
|
|
72
|
+
/** 1 when heights match, falling linearly to 0 at a 50% difference. */
|
|
73
|
+
export function heightSimilarity(a, b) {
|
|
74
|
+
if (a <= 0 && b <= 0)
|
|
75
|
+
return 1;
|
|
76
|
+
const ratio = Math.abs(a - b) / Math.max(a, b, 1);
|
|
77
|
+
return Math.max(0, 1 - ratio * 2);
|
|
78
|
+
}
|
|
79
|
+
/** Numbers inside a computed CSS value (matrix(...), rgb(...), px). */
|
|
80
|
+
export function cssNumbers(value) {
|
|
81
|
+
return [...value.matchAll(/-?\d*\.?\d+(?:e-?\d+)?/gi)].map((match) => Number(match[0]));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalised error between two computed values of the same property: 0 identical, 1 completely
|
|
85
|
+
* different. Numeric values compare component-wise relative to their magnitude; others by string.
|
|
86
|
+
*/
|
|
87
|
+
export function valueError(a, b) {
|
|
88
|
+
if (a === b)
|
|
89
|
+
return 0;
|
|
90
|
+
const x = cssNumbers(a);
|
|
91
|
+
const y = cssNumbers(b);
|
|
92
|
+
if (!x.length || x.length !== y.length)
|
|
93
|
+
return 1;
|
|
94
|
+
let total = 0;
|
|
95
|
+
for (let index = 0; index < x.length; index++) {
|
|
96
|
+
const p = x[index] ?? 0;
|
|
97
|
+
const q = y[index] ?? 0;
|
|
98
|
+
const scale = Math.max(1, Math.abs(p), Math.abs(q));
|
|
99
|
+
total += Math.min(1, Math.abs(p - q) / scale);
|
|
100
|
+
}
|
|
101
|
+
return total / x.length;
|
|
102
|
+
}
|
|
103
|
+
/** Value of a step curve `[[y, value]…]` at `y` (the last change at or before y). */
|
|
104
|
+
export function curveAt(curve, y) {
|
|
105
|
+
let value = curve[0]?.[1];
|
|
106
|
+
for (const [at, next] of curve) {
|
|
107
|
+
if (at > y)
|
|
108
|
+
break;
|
|
109
|
+
value = next;
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
/** Mean error of a clone curve sampled at the original's change points. */
|
|
114
|
+
export function curveError(original, sampled) {
|
|
115
|
+
if (!original.length)
|
|
116
|
+
return 0;
|
|
117
|
+
let total = 0;
|
|
118
|
+
for (const [y, value] of original) {
|
|
119
|
+
const other = curveAt(sampled, y);
|
|
120
|
+
total += other === undefined ? 1 : valueError(value, other);
|
|
121
|
+
}
|
|
122
|
+
return total / original.length;
|
|
123
|
+
}
|
|
124
|
+
/** Weighted section score out of 100; missing sections score 0, extra sections cost a little. */
|
|
125
|
+
export function sectionScore(parts) {
|
|
126
|
+
const pixel = parts.pixel ?? 0.5;
|
|
127
|
+
return (Math.round((pixel * 0.55 + parts.height * 0.2 + parts.text * 0.15 + parts.fonts * 0.1) * 1000) /
|
|
128
|
+
10);
|
|
129
|
+
}
|
|
130
|
+
/** The headline score can't reach 100 while anything wasn't compared. */
|
|
131
|
+
export function cappedScore(raw, completeness) {
|
|
132
|
+
const incomplete = completeness.framesSkipped +
|
|
133
|
+
completeness.sectionsMissing +
|
|
134
|
+
completeness.sectionsExtra +
|
|
135
|
+
completeness.dimensionMismatches;
|
|
136
|
+
return incomplete > 0 ? Math.min(raw, 99) : raw;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/domain/compare.ts"],"names":[],"mappings":"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"}
|