@crouton-kit/capture 1.3.7 → 1.3.9
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/bin/capture +32786 -9989
- package/package.json +9 -3
- package/src/capture.ts +86 -163
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -116
- package/src/cdp/bridge/protocol.ts +187 -1
- package/src/cdp/bridge/server.ts +211 -84
- package/src/cdp/bridge/spawn.ts +110 -1
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +35 -4
- package/src/cdp/commands/cdp.ts +266 -69
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +87 -0
- package/src/cdp/commands/measure/check.ts +109 -0
- package/src/cdp/commands/measure/diff.ts +120 -0
- package/src/cdp/commands/measure/explain.ts +133 -0
- package/src/cdp/commands/measure/index.ts +106 -0
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +52 -0
- package/src/cdp/commands/measure/map-layers.ts +59 -0
- package/src/cdp/commands/measure/map-scroll.ts +94 -0
- package/src/cdp/commands/measure/snap.ts +416 -0
- package/src/cdp/commands/measure/sweep.ts +227 -0
- package/src/cdp/commands/motion/index.ts +61 -0
- package/src/cdp/commands/motion/jank.ts +132 -0
- package/src/cdp/commands/motion/mask.ts +92 -0
- package/src/cdp/commands/motion/rec.ts +534 -0
- package/src/cdp/commands/motion/response.ts +76 -0
- package/src/cdp/commands/motion/timeline.ts +152 -0
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/coordinates.ts +158 -0
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +76 -26
- package/src/cdp/domains.ts +75 -0
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1894 -0
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/source-map.ts +309 -0
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +87 -0
- package/src/cdp/types.ts +87 -6
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +526 -0
- package/src/output/diff.ts +111 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +987 -0
- package/src/output/selector.ts +185 -0
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +481 -0
- package/src/session/commands.ts +1237 -368
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +302 -34
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -77
- package/src/tab-session.ts +0 -91
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cascade/winning-declaration + generated-source-header provenance
|
|
3
|
+
* engine, used by both `styles.ts` (per-element computed-style provenance)
|
|
4
|
+
* and `layers.ts` (layer-triggering-property provenance) — ONE
|
|
5
|
+
* implementation of "which declaration wins a property, by what
|
|
6
|
+
* specificity/importance, from where in the authored/generated source"
|
|
7
|
+
* instead of two parallel ones. Each caller supplies its own `nodeId` +
|
|
8
|
+
* `CSS.getMatchedStylesForNode` response, its own computed-value map, and
|
|
9
|
+
* its own property list; this module owns only the cascade math and the
|
|
10
|
+
* source resolution.
|
|
11
|
+
*
|
|
12
|
+
* **Judgment call (inherited from the styles collector's original v1
|
|
13
|
+
* design):** the cascade/specificity model is a simplified v1 for a
|
|
14
|
+
* measurement tool — no `@layer`, no full `:not()`/`:is()` specificity
|
|
15
|
+
* nuance, no cross-origin/user-agent importance ordering beyond a
|
|
16
|
+
* two-tier important/non-important split. Good enough to report "which
|
|
17
|
+
* rule won and why" for the overwhelming majority of real pages; not a
|
|
18
|
+
* spec-perfect cascade resolver. A selector using `:where()` (spec:
|
|
19
|
+
* always zero specificity) or `:is()`/`:matches()` (spec: the specificity
|
|
20
|
+
* of its most specific argument) is scored by this module as an ordinary
|
|
21
|
+
* pseudo-class token instead — every candidate whose matching selector
|
|
22
|
+
* contains either construct is flagged via `winnerApproximate` /
|
|
23
|
+
* `winnerApproximateReason:'selector-specificity-where-is-present'` (the
|
|
24
|
+
* v1 winner is still reported, never silently re-picked).
|
|
25
|
+
*
|
|
26
|
+
* ## Stylesheet header capture — {@link captureStyleSheetHeaders}
|
|
27
|
+
* `CSS.styleSheetAdded` only fires for a stylesheet at the moment CDP's
|
|
28
|
+
* CSS domain transitions from disabled to enabled (proven empirically:
|
|
29
|
+
* re-invoking `CSS.enable` while the domain is ALREADY enabled is a
|
|
30
|
+
* no-op that redelivers nothing — only a `CSS.disable` + `CSS.enable`
|
|
31
|
+
* pair forces Chrome to resend every currently-known stylesheet's
|
|
32
|
+
* header). Both `styles.ts` and `layers.ts` run in the same `baseline`
|
|
33
|
+
* `Promise.all` (see `snapshot.ts`), and `enableDomainsForSnap` already
|
|
34
|
+
* enabled CSS once before either collector starts — so a caller-owned
|
|
35
|
+
* listener registered only after that point would otherwise never see
|
|
36
|
+
* the headers for stylesheets already parsed at that time. Each caller
|
|
37
|
+
* runs `captureStyleSheetHeaders` independently, as the very first thing
|
|
38
|
+
* it does (before any other CSS-domain call), and awaits it before
|
|
39
|
+
* issuing any of its own `CSS.*` calls. Because listener registration is
|
|
40
|
+
* synchronous (before any `await`), and JS never yields to socket I/O
|
|
41
|
+
* mid-way through `Promise.all`'s synchronous dispatch of every baseline
|
|
42
|
+
* collector, both this file's and the other collector's listeners are
|
|
43
|
+
* always attached before either's disable/enable pair reaches Chrome —
|
|
44
|
+
* so neither can miss the other's redelivery burst, and once both
|
|
45
|
+
* callers' own `captureStyleSheetHeaders` calls resolve, the CSS domain
|
|
46
|
+
* is stably enabled for the rest of the snapshot (neither file disables
|
|
47
|
+
* it again).
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { resolveAuthoredSourceLocation, type ResolvedSourceLocation } from '../../source-map.js';
|
|
51
|
+
import { sanitizeString } from '../redaction.js';
|
|
52
|
+
import type { CDPClient } from '../../client.js';
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Output shape
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export interface Specificity {
|
|
59
|
+
a: number;
|
|
60
|
+
b: number;
|
|
61
|
+
c: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CSSRange {
|
|
65
|
+
startLine: number;
|
|
66
|
+
startColumn: number;
|
|
67
|
+
endLine: number;
|
|
68
|
+
endColumn: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface WinningDeclaration {
|
|
72
|
+
property: string;
|
|
73
|
+
/** The COMPUTED style value (`getComputedStyle`) for this property — not the raw declaration text, which can differ (custom properties, relative units, shorthands). */
|
|
74
|
+
value: string | null;
|
|
75
|
+
/** The raw winning declaration's own value text, as authored/serialized by CDP — `undefined` for the no-declaration case. */
|
|
76
|
+
declaredValue?: string;
|
|
77
|
+
/** `'inline'` for the `style` attribute, a selector string for a rule, or `null` when nothing declares this property. */
|
|
78
|
+
selector: string | null;
|
|
79
|
+
/** `"a-b-c"` display form, `null` for inline wins or the no-declaration case. */
|
|
80
|
+
specificity: string | null;
|
|
81
|
+
important?: boolean;
|
|
82
|
+
sourceStyleSheetId?: string;
|
|
83
|
+
/** The real stylesheet `sourceURL` from a `CSS.styleSheetAdded` header, when one was observed for `sourceStyleSheetId` — the human-meaningful counterpart to the opaque id. */
|
|
84
|
+
sourceStyleSheetUrl?: string;
|
|
85
|
+
range?: CSSRange;
|
|
86
|
+
authored?: { file: string; line: number; column: number };
|
|
87
|
+
/** The generated (post-build) CSS location, set for BOTH authored and generated results — the source-map fallback fact preserved even when no map resolves. */
|
|
88
|
+
generated?: { sourceURL: string; line: number; column: number };
|
|
89
|
+
mediaQuery?: string;
|
|
90
|
+
containerQuery?: string;
|
|
91
|
+
/** Present (and `null`) only when no candidate declares this property at all — an explicit "no declaration" fact, not an omission. */
|
|
92
|
+
source?: null;
|
|
93
|
+
/**
|
|
94
|
+
* `true` when the candidate set for this property contains a rule this simplified cascade
|
|
95
|
+
* engine cannot correctly order or place: a cascade `@layer`, an `@scope`, an unresolved
|
|
96
|
+
* `revert`/`revert-layer` keyword, a matching selector using `:where()`/`:is()` (whose real
|
|
97
|
+
* specificity this module's simplified {@link computeSpecificity} does not model), or a
|
|
98
|
+
* declaration whose authored source position could not be traced at all (never the whole-rule
|
|
99
|
+
* or `0:0` fallback pretending to be exact) — the reported `selector`/`declaredValue`/`range`
|
|
100
|
+
* is the simplified model's best guess, NOT a guaranteed spec-accurate winner. Absent (not
|
|
101
|
+
* `false`) when every candidate was fully resolvable, matching every other honesty flag in
|
|
102
|
+
* this module.
|
|
103
|
+
*/
|
|
104
|
+
winnerApproximate?: boolean;
|
|
105
|
+
/** Why `winnerApproximate` is set — `'cascade-layers-present'`, `'cascade-scope-present'`, `'revert-keyword-present'`, `'selector-specificity-where-is-present'`, or `'source-range-unresolved'`. */
|
|
106
|
+
winnerApproximateReason?: string;
|
|
107
|
+
/**
|
|
108
|
+
* `true` (I-4/I-5/I-10) when authored/generated source resolution was ATTEMPTED for this
|
|
109
|
+
* declaration (a real property/rule range and `styleSheetId` existed to seed the lookup) but
|
|
110
|
+
* {@link resolveAuthoredSourceLocation} REJECTED — e.g. a malformed source-map `mappings`
|
|
111
|
+
* string throws synchronously inside the VLQ decoder — rather than resolving normally. This is
|
|
112
|
+
* DISTINCT from a genuine no-authored-source case: `resolveAuthoredSourceLocation` itself
|
|
113
|
+
* degrades a missing/unparseable/unfetchable source map to an honest `GeneratedLocation`
|
|
114
|
+
* (never a rejection) for the ordinary minified/no-source-map-production case, so `authored`
|
|
115
|
+
* absent with this flag ALSO absent still honestly means "no authored source (minified/prod)".
|
|
116
|
+
* `authored` absent WITH this flag set means the opposite: resolution could not complete at
|
|
117
|
+
* all, so whether an authored source exists is genuinely unknown — a failed read, not a
|
|
118
|
+
* negative observation. `generated` is still populated from the already-known ref position
|
|
119
|
+
* (the queried generated location, not something the failed resolution had to compute) so the
|
|
120
|
+
* generated fallback fact is preserved even when resolution itself could not run.
|
|
121
|
+
*/
|
|
122
|
+
sourceResolutionUnavailable?: boolean;
|
|
123
|
+
/** The caught error's message, or `'source-resolution-failed'` when the caught value carries none. */
|
|
124
|
+
sourceResolutionUnavailableReason?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// computeSpecificity — a simplified {a,b,c} selector specificity tuple
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Computes a simplified `{a,b,c}` (id / class-attr-pseudoclass / type-pseudoelement)
|
|
133
|
+
* specificity tuple for a CSS selector string. Strips recognized tokens in
|
|
134
|
+
* cascade-relevant order so overlapping character classes (e.g. `:hover`
|
|
135
|
+
* vs `::before`) aren't double-counted. Does not model `@layer`, full
|
|
136
|
+
* `:not()`/`:is()` specificity delegation, or `:where()` (zero-specificity)
|
|
137
|
+
* — a documented v1 simplification, not a spec-perfect implementation.
|
|
138
|
+
*/
|
|
139
|
+
export function computeSpecificity(selectorText: string): Specificity {
|
|
140
|
+
let s = selectorText;
|
|
141
|
+
let a = 0;
|
|
142
|
+
let b = 0;
|
|
143
|
+
let c = 0;
|
|
144
|
+
|
|
145
|
+
const idMatches = s.match(/#[\w-]+/g);
|
|
146
|
+
a += idMatches ? idMatches.length : 0;
|
|
147
|
+
s = s.replace(/#[\w-]+/g, ' ');
|
|
148
|
+
|
|
149
|
+
const attrMatches = s.match(/\[[^\]]*\]/g);
|
|
150
|
+
b += attrMatches ? attrMatches.length : 0;
|
|
151
|
+
s = s.replace(/\[[^\]]*\]/g, ' ');
|
|
152
|
+
|
|
153
|
+
const classMatches = s.match(/\.[\w-]+/g);
|
|
154
|
+
b += classMatches ? classMatches.length : 0;
|
|
155
|
+
s = s.replace(/\.[\w-]+/g, ' ');
|
|
156
|
+
|
|
157
|
+
const pseudoElementMatches = s.match(/::[\w-]+|:(?:before|after|first-line|first-letter)\b/g);
|
|
158
|
+
c += pseudoElementMatches ? pseudoElementMatches.length : 0;
|
|
159
|
+
s = s.replace(/::[\w-]+|:(?:before|after|first-line|first-letter)\b/g, ' ');
|
|
160
|
+
|
|
161
|
+
const pseudoClassMatches = s.match(/:[\w-]+(\([^)]*\))?/g);
|
|
162
|
+
b += pseudoClassMatches ? pseudoClassMatches.length : 0;
|
|
163
|
+
s = s.replace(/:[\w-]+(\([^)]*\))?/g, ' ');
|
|
164
|
+
|
|
165
|
+
const identMatches = s.match(/[a-zA-Z][a-zA-Z0-9-]*/g);
|
|
166
|
+
c += identMatches ? identMatches.length : 0;
|
|
167
|
+
|
|
168
|
+
return { a, b, c };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function compareSpecificity(x: Specificity, y: Specificity): number {
|
|
172
|
+
return x.a - y.a || x.b - y.b || x.c - y.c;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// resolveNodeIds — shared backend-node-id correlation (used by media.ts too)
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolves each `path` (a `cssPathFromBody`-style `tag:nth-of-type(n) > ...`
|
|
181
|
+
* string rooted at `body`) to its CDP `nodeId` (needed for
|
|
182
|
+
* `CSS.getMatchedStylesForNode`) and `backendNodeId` (the stable cross-file
|
|
183
|
+
* join key), via `DOM.querySelector` + `DOM.describeNode`. Sequential,
|
|
184
|
+
* best-effort per path — a failed lookup pushes `undefined` rather than
|
|
185
|
+
* aborting the batch; one bad path never drops the rest.
|
|
186
|
+
*/
|
|
187
|
+
export async function resolveNodeIds(
|
|
188
|
+
client: CDPClient,
|
|
189
|
+
documentNodeId: number,
|
|
190
|
+
paths: readonly string[],
|
|
191
|
+
): Promise<Array<{ nodeId?: number; backendNodeId?: number } | undefined>> {
|
|
192
|
+
const results: Array<{ nodeId?: number; backendNodeId?: number } | undefined> = [];
|
|
193
|
+
|
|
194
|
+
for (const path of paths) {
|
|
195
|
+
try {
|
|
196
|
+
const queryResult = (await client.send('DOM.querySelector', {
|
|
197
|
+
nodeId: documentNodeId,
|
|
198
|
+
selector: `body > ${path}`,
|
|
199
|
+
})) as { nodeId?: number };
|
|
200
|
+
const nodeId = queryResult.nodeId;
|
|
201
|
+
if (!nodeId) {
|
|
202
|
+
results.push(undefined);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const described = (await client.send('DOM.describeNode', { nodeId })) as {
|
|
207
|
+
node?: { backendNodeId?: number };
|
|
208
|
+
};
|
|
209
|
+
results.push({ nodeId, backendNodeId: described.node?.backendNodeId });
|
|
210
|
+
} catch {
|
|
211
|
+
results.push({ nodeId });
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
results.push(undefined);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Raw CDP response shapes (modeled locally; no CDP protocol package)
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
export interface CDPCSSProperty {
|
|
226
|
+
name: string;
|
|
227
|
+
value: string;
|
|
228
|
+
important?: boolean;
|
|
229
|
+
implicit?: boolean;
|
|
230
|
+
range?: CSSRange;
|
|
231
|
+
/**
|
|
232
|
+
* Present ONLY on a real, literally-authored SHORTHAND declaration (e.g. `margin`) — the
|
|
233
|
+
* individual longhand properties (and their parsed values) it expands to. Real CDP
|
|
234
|
+
* responses give each of these longhand names NO `range` of its own (the longhand text
|
|
235
|
+
* never literally appears in source — only the shorthand's own `range`, on THIS entry,
|
|
236
|
+
* is a real authored position) — see {@link buildRangeByProperty}.
|
|
237
|
+
*/
|
|
238
|
+
longhandProperties?: Array<{ name: string; value: string }>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface CDPSelectorItem {
|
|
242
|
+
text: string;
|
|
243
|
+
range?: CSSRange;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface CDPCSSRule {
|
|
247
|
+
styleSheetId?: string;
|
|
248
|
+
selectorList: { selectors: CDPSelectorItem[]; text: string };
|
|
249
|
+
origin: string;
|
|
250
|
+
style: { cssProperties: CDPCSSProperty[]; styleSheetId?: string; range?: CSSRange };
|
|
251
|
+
media?: Array<{ text: string }>;
|
|
252
|
+
containerQueries?: Array<{ text: string }>;
|
|
253
|
+
/** Non-empty when the rule is nested inside one or more `@layer` blocks — a cascade-layer construct this simplified engine does not order (see {@link detectUnresolvedCascadeConstruct}). */
|
|
254
|
+
layers?: Array<{ text: string }>;
|
|
255
|
+
/** Non-empty when the rule is nested inside an `@scope` block — a construct this simplified engine does not order (see {@link detectUnresolvedCascadeConstruct}). */
|
|
256
|
+
scopes?: Array<{ text: string }>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface CDPMatchedStylesResponse {
|
|
260
|
+
inlineStyle?: { cssProperties: CDPCSSProperty[] };
|
|
261
|
+
matchedCSSRules?: Array<{ rule: CDPCSSRule; matchingSelectors: number[] }>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Cascade winner selection
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
interface Candidate {
|
|
269
|
+
value: string;
|
|
270
|
+
important: boolean;
|
|
271
|
+
selectorText: string;
|
|
272
|
+
specificity: Specificity;
|
|
273
|
+
isInline: boolean;
|
|
274
|
+
rule?: CDPCSSRule;
|
|
275
|
+
range?: CSSRange;
|
|
276
|
+
/**
|
|
277
|
+
* Set when this candidate is only reportable approximately: its rule uses a construct
|
|
278
|
+
* {@link detectUnresolvedCascadeConstruct} identifies as unorderable, its matching selector
|
|
279
|
+
* uses a specificity construct {@link detectSelectorSpecificityApproximation} identifies as
|
|
280
|
+
* unscoreable, or its authored source position could not be traced at all (see `range`'s
|
|
281
|
+
* doc on {@link buildCandidates} — the whole-rule/`0:0` fallback is never reported as exact).
|
|
282
|
+
*/
|
|
283
|
+
approximateReason?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Identifies the ONE class of matched-rule construct this simplified cascade engine cannot
|
|
288
|
+
* correctly order: cascade `@layer`s (layer declaration order, not specificity/source-order,
|
|
289
|
+
* decides the winner across layers), `@scope` (proximity, not specificity/source-order, can
|
|
290
|
+
* decide the winner), and the `revert`/`revert-layer` keywords (which resolve to a DIFFERENT
|
|
291
|
+
* cascade origin/layer entirely, not to "this candidate's value"). Returns the honest reason
|
|
292
|
+
* string for {@link WinningDeclaration.winnerApproximate}, or `undefined` when the candidate is
|
|
293
|
+
* fully within this engine's supported model — recognizing exactly what can be parsed exactly
|
|
294
|
+
* and flagging the rest, never silently guessing (no lenient fallback path).
|
|
295
|
+
*/
|
|
296
|
+
function detectUnresolvedCascadeConstruct(rule: CDPCSSRule, declaredValue: string): string | undefined {
|
|
297
|
+
if (rule.layers && rule.layers.length > 0) return 'cascade-layers-present';
|
|
298
|
+
if (rule.scopes && rule.scopes.length > 0) return 'cascade-scope-present';
|
|
299
|
+
const normalized = declaredValue.trim().toLowerCase();
|
|
300
|
+
if (normalized === 'revert' || normalized === 'revert-layer') return 'revert-keyword-present';
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Identifies the other class of construct this simplified {@link computeSpecificity} cannot
|
|
306
|
+
* score correctly: `:where()` (spec: always zero specificity, regardless of its argument) and
|
|
307
|
+
* `:is()`/`:matches()` (spec: the specificity of its most specific argument) — this module's
|
|
308
|
+
* specificity model treats every pseudo-class token, including these, as a flat `b += 1`
|
|
309
|
+
* contribution, so a selector using either can be scored higher OR lower than its real spec
|
|
310
|
+
* specificity, and the winner picked among competing candidates may not be the true spec
|
|
311
|
+
* winner. Returns the honest reason string for {@link WinningDeclaration.winnerApproximate}
|
|
312
|
+
* (the v1 winner is still reported — this flags it, it never re-picks), or `undefined` when
|
|
313
|
+
* the selector uses neither construct.
|
|
314
|
+
*/
|
|
315
|
+
function detectSelectorSpecificityApproximation(selectorText: string): string | undefined {
|
|
316
|
+
return /:(?:where|is|matches)\(/i.test(selectorText) ? 'selector-specificity-where-is-present' : undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Maps each property name declared (directly or via a shorthand) in `rule` to the source
|
|
321
|
+
* `range` of the declaration that actually sets it: its own literal `range` when directly
|
|
322
|
+
* authored (`color: red;`), or the enclosing SHORTHAND's `range` when the property is only
|
|
323
|
+
* ever expressed through that shorthand's {@link CDPCSSProperty.longhandProperties} (e.g.
|
|
324
|
+
* `margin-top` from `margin: 10px 20px;`, which itself carries no `range` of its own — the
|
|
325
|
+
* literal `margin-top` text never appears in source). Real CDP responses additionally append,
|
|
326
|
+
* after every rule's literally-authored declarations, a flattened restatement of every
|
|
327
|
+
* resolved longhand value with NO `range` of its own — a convenience duplicate, not a new
|
|
328
|
+
* source location, so it is never itself a map source; iterating `cssProperties` in CDP's own
|
|
329
|
+
* delivery order and overwriting on each RANGED match keeps this map's value the LAST
|
|
330
|
+
* (cascade-winning) authored source for that property name, matching the same "later
|
|
331
|
+
* declaration wins" source-order semantics {@link pickWinner} already applies to candidate
|
|
332
|
+
* selection.
|
|
333
|
+
*/
|
|
334
|
+
function buildRangeByProperty(rule: CDPCSSRule): Map<string, CSSRange> {
|
|
335
|
+
const rangeByProperty = new Map<string, CSSRange>();
|
|
336
|
+
for (const p of rule.style.cssProperties) {
|
|
337
|
+
if (!p.range) continue;
|
|
338
|
+
rangeByProperty.set(p.name, p.range);
|
|
339
|
+
if (p.longhandProperties) {
|
|
340
|
+
for (const longhand of p.longhandProperties) rangeByProperty.set(longhand.name, p.range);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return rangeByProperty;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A rule's selector list can have multiple selectors (`.card, #hero`), and CDP reports every selector
|
|
348
|
+
* in the list that matches the node via `matchingSelectors` (indices), not just the primary one. Use
|
|
349
|
+
* the matching selector with the HIGHEST specificity — a rule wins its cascade slot by its best-matching
|
|
350
|
+
* selector, not its first-listed one. Ties keep the first-encountered match (deterministic).
|
|
351
|
+
*/
|
|
352
|
+
function pickHighestSpecificitySelectorText(rule: CDPCSSRule, matchingSelectors: number[] | undefined): string {
|
|
353
|
+
if (!matchingSelectors || matchingSelectors.length === 0) return rule.selectorList.text;
|
|
354
|
+
let bestText: string | undefined;
|
|
355
|
+
let bestSpecificity: Specificity | undefined;
|
|
356
|
+
for (const idx of matchingSelectors) {
|
|
357
|
+
const sel = rule.selectorList.selectors[idx];
|
|
358
|
+
if (!sel) continue;
|
|
359
|
+
const specificity = computeSpecificity(sel.text);
|
|
360
|
+
if (!bestSpecificity || compareSpecificity(specificity, bestSpecificity) > 0) {
|
|
361
|
+
bestSpecificity = specificity;
|
|
362
|
+
bestText = sel.text;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return bestText ?? rule.selectorList.text;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildCandidates(property: string, matched: CDPMatchedStylesResponse): Candidate[] {
|
|
369
|
+
const candidates: Candidate[] = [];
|
|
370
|
+
|
|
371
|
+
for (const entry of matched.matchedCSSRules ?? []) {
|
|
372
|
+
const rule = entry.rule;
|
|
373
|
+
if (rule.origin !== 'regular') continue;
|
|
374
|
+
// CSS importance is resolved BEFORE source order: a rule can declare the same property more than
|
|
375
|
+
// once with different importance (e.g. `color: red !important; color: blue;`), and the `!important`
|
|
376
|
+
// one must win regardless of position. So push every non-implicit same-property declaration as its
|
|
377
|
+
// own candidate, in source order, and let pickWinner's importance partition + source-order tiebreak
|
|
378
|
+
// decide — do NOT collapse to a single "last declaration" here, which would discard importance info.
|
|
379
|
+
const selectorText = pickHighestSpecificitySelectorText(rule, entry.matchingSelectors);
|
|
380
|
+
const specificity = computeSpecificity(selectorText);
|
|
381
|
+
const rangeByProperty = buildRangeByProperty(rule);
|
|
382
|
+
for (const p of rule.style.cssProperties) {
|
|
383
|
+
if (p.name !== property || p.implicit === true) continue;
|
|
384
|
+
// `p.range` covers a literally-authored entry (direct declaration OR a shorthand
|
|
385
|
+
// itself); `rangeByProperty` backfills the accurate authored source for CDP's
|
|
386
|
+
// range-less restatement/synthesized-longhand entries. When NEITHER traces the property's
|
|
387
|
+
// real position, `rule.style.range` is a last-resort SUBSTITUTE (the whole rule's span
|
|
388
|
+
// standing in for one property's position) — that substitution is flagged approximate,
|
|
389
|
+
// never reported as exact. A property with no range anywhere (not even a rule-level one)
|
|
390
|
+
// stays honestly absent instead — an acknowledged gap, not a fabricated claim.
|
|
391
|
+
const resolvedRange = p.range ?? rangeByProperty.get(p.name);
|
|
392
|
+
const usedRuleRangeFallback = resolvedRange === undefined && rule.style.range !== undefined;
|
|
393
|
+
candidates.push({
|
|
394
|
+
value: p.value,
|
|
395
|
+
important: p.important === true,
|
|
396
|
+
selectorText,
|
|
397
|
+
specificity,
|
|
398
|
+
isInline: false,
|
|
399
|
+
rule,
|
|
400
|
+
range: resolvedRange ?? rule.style.range,
|
|
401
|
+
approximateReason:
|
|
402
|
+
detectUnresolvedCascadeConstruct(rule, p.value) ??
|
|
403
|
+
detectSelectorSpecificityApproximation(selectorText) ??
|
|
404
|
+
(usedRuleRangeFallback ? 'source-range-unresolved' : undefined),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const p of matched.inlineStyle?.cssProperties ?? []) {
|
|
410
|
+
if (p.name !== property || p.implicit === true) continue;
|
|
411
|
+
candidates.push({
|
|
412
|
+
value: p.value,
|
|
413
|
+
important: p.important === true,
|
|
414
|
+
selectorText: 'inline',
|
|
415
|
+
specificity: { a: 0, b: 0, c: 0 },
|
|
416
|
+
isInline: true,
|
|
417
|
+
range: p.range,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return candidates;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Picks the winning candidate: important declarations beat non-important
|
|
426
|
+
* ones outright (a two-tier split, not a full important-origin ordering).
|
|
427
|
+
* Within WHICHEVER pool is in play (important or non-important), an
|
|
428
|
+
* inline `style` declaration always wins — the spec treats the style
|
|
429
|
+
* attribute as a higher cascade tier than any selector-based rule, at
|
|
430
|
+
* the SAME importance level, so inline `!important` beats stylesheet
|
|
431
|
+
* `!important` too, not just inline-vs-non-important. Otherwise the
|
|
432
|
+
* highest `{a,b,c}` specificity wins, ties broken by later position in
|
|
433
|
+
* the candidate list (a document-order proxy for "declared later wins").
|
|
434
|
+
*/
|
|
435
|
+
function pickWinner(candidates: Candidate[]): Candidate | undefined {
|
|
436
|
+
if (candidates.length === 0) return undefined;
|
|
437
|
+
|
|
438
|
+
const importantPool = candidates.filter((c) => c.important);
|
|
439
|
+
const nonImportantPool = candidates.filter((c) => !c.important);
|
|
440
|
+
const pool = importantPool.length > 0 ? importantPool : nonImportantPool;
|
|
441
|
+
|
|
442
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
443
|
+
if (pool[i].isInline) return pool[i];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let best: Candidate | undefined;
|
|
447
|
+
for (const candidate of pool) {
|
|
448
|
+
if (!best || compareSpecificity(candidate.specificity, best.specificity) >= 0) {
|
|
449
|
+
best = candidate;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return best;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function noDeclarationRecord(property: string, value: string | null): WinningDeclaration {
|
|
456
|
+
return { property, value, selector: null, specificity: null, source: null };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Resolves the winning declaration (+ authored/generated source, via
|
|
461
|
+
* source-map fallback) for each property in `properties`, given one
|
|
462
|
+
* `CSS.getMatchedStylesForNode` response covering all of them. Shared by
|
|
463
|
+
* `styles.ts` (a bounded per-element property set) and `layers.ts` (the
|
|
464
|
+
* fixed layer-affecting property set).
|
|
465
|
+
*/
|
|
466
|
+
export async function buildWinningDeclarations(
|
|
467
|
+
client: CDPClient,
|
|
468
|
+
matched: CDPMatchedStylesResponse,
|
|
469
|
+
computed: Record<string, string | null>,
|
|
470
|
+
sourceCache: Map<string, Promise<ResolvedSourceLocation>>,
|
|
471
|
+
styleSheetUrls: Map<string, string>,
|
|
472
|
+
properties: readonly string[],
|
|
473
|
+
): Promise<WinningDeclaration[]> {
|
|
474
|
+
const declarations: WinningDeclaration[] = [];
|
|
475
|
+
|
|
476
|
+
for (const property of properties) {
|
|
477
|
+
const candidates = buildCandidates(property, matched);
|
|
478
|
+
const winner = pickWinner(candidates);
|
|
479
|
+
if (!winner) {
|
|
480
|
+
declarations.push(noDeclarationRecord(property, computed[property] ?? null));
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const specificityStr = winner.isInline ? null : `${winner.specificity.a}-${winner.specificity.b}-${winner.specificity.c}`;
|
|
485
|
+
|
|
486
|
+
// Honesty check (I-4/I-10): if ANY candidate in play for this property — not just the
|
|
487
|
+
// reported winner — uses a construct outside the simplified model (a cascade/scope/revert
|
|
488
|
+
// construct, a `:where()`/`:is()` selector this specificity model can't score, or a
|
|
489
|
+
// declaration whose authored source position couldn't be traced), the winner itself may be
|
|
490
|
+
// wrong or its `range` approximate (e.g. a lower-specificity candidate in a higher-priority
|
|
491
|
+
// `@layer` is the REAL winner in real Chrome; see the adversarial cascade-layers fixture
|
|
492
|
+
// this flag exists for). Flag rather than silently claim exactness.
|
|
493
|
+
const winnerApproximateReason = candidates.map((c) => c.approximateReason).find((reason) => reason !== undefined);
|
|
494
|
+
|
|
495
|
+
// Raw sourceURL is used ONLY as an input to source-map resolution below (it must stay
|
|
496
|
+
// byte-exact for relative sourceMappingURL joining). The EMITTED field is sanitized
|
|
497
|
+
// through the shared authority — provenance URLs are page-controlled strings (D1).
|
|
498
|
+
const rawSourceStyleSheetUrl = winner.rule?.styleSheetId ? styleSheetUrls.get(winner.rule.styleSheetId) : undefined;
|
|
499
|
+
const sourceStyleSheetUrl = rawSourceStyleSheetUrl !== undefined ? sanitizeString(rawSourceStyleSheetUrl) : undefined;
|
|
500
|
+
|
|
501
|
+
let authored: { file: string; line: number; column: number } | undefined;
|
|
502
|
+
let generated: { sourceURL: string; line: number; column: number } | undefined;
|
|
503
|
+
let sourceResolutionUnavailable: boolean | undefined;
|
|
504
|
+
let sourceResolutionUnavailableReason: string | undefined;
|
|
505
|
+
// Source resolution requires a REAL property/rule range to seed the lookup — without one
|
|
506
|
+
// there is no authored/generated position to resolve, only a fabricated 0:0 substitute. When
|
|
507
|
+
// `winner.range` is undefined (no declaration range and no `rule.style.range` fallback
|
|
508
|
+
// either), leave `authored`/`generated` honestly undefined rather than defaulting line/column
|
|
509
|
+
// to 0 and reporting a fake location (I-4: honest absence must hold for `authored`/
|
|
510
|
+
// `generated`, not just `range`).
|
|
511
|
+
if (!winner.isInline && winner.rule?.styleSheetId && winner.range) {
|
|
512
|
+
const line = winner.range.startLine;
|
|
513
|
+
const column = winner.range.startColumn;
|
|
514
|
+
const styleSheetId = winner.rule.styleSheetId;
|
|
515
|
+
const key = `${styleSheetId}:${line}:${column}`;
|
|
516
|
+
try {
|
|
517
|
+
let pending = sourceCache.get(key);
|
|
518
|
+
if (!pending) {
|
|
519
|
+
pending = resolveAuthoredSourceLocation(client, {
|
|
520
|
+
styleSheetId,
|
|
521
|
+
sourceURL: rawSourceStyleSheetUrl,
|
|
522
|
+
line,
|
|
523
|
+
column,
|
|
524
|
+
});
|
|
525
|
+
sourceCache.set(key, pending);
|
|
526
|
+
}
|
|
527
|
+
const location = await pending;
|
|
528
|
+
// `generated` is set for BOTH branches: when a source map resolves, `authored` is the
|
|
529
|
+
// mapped location and `generated` is the pre-map (post-build) location it came from; when
|
|
530
|
+
// no map resolves, `generated` alone carries the real generated URL/line/column — the
|
|
531
|
+
// fallback fact that must never be discarded.
|
|
532
|
+
if (location.kind === 'authored') {
|
|
533
|
+
authored = { file: sanitizeString(location.file), line: location.line, column: location.column };
|
|
534
|
+
generated = {
|
|
535
|
+
sourceURL: sanitizeString(location.generated.sourceURL),
|
|
536
|
+
line: location.generated.line,
|
|
537
|
+
column: location.generated.column,
|
|
538
|
+
};
|
|
539
|
+
} else {
|
|
540
|
+
generated = { sourceURL: sanitizeString(location.sourceURL), line: location.line, column: location.column };
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
// Source-map resolution never aborts the WHOLE snapshot for this property, but a REJECTED
|
|
544
|
+
// resolution (e.g. a malformed VLQ `mappings` string throwing inside the decoder) must not
|
|
545
|
+
// be silently indistinguishable from `resolveAuthoredSourceLocation`'s own honest "no
|
|
546
|
+
// authored source" outcome (a genuine minified/no-source-map production build, which it
|
|
547
|
+
// reports as a normal `GeneratedLocation`, never a rejection) — I-4/I-10. Mark the failure
|
|
548
|
+
// explicitly, and preserve the generated (pre-map) fallback fact from the already-known
|
|
549
|
+
// query position — that position was never in question, only the map lookup on top of it.
|
|
550
|
+
sourceResolutionUnavailable = true;
|
|
551
|
+
sourceResolutionUnavailableReason = err instanceof Error ? err.message : 'source-resolution-failed';
|
|
552
|
+
generated = { sourceURL: sanitizeString(rawSourceStyleSheetUrl ?? styleSheetId), line, column };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
declarations.push({
|
|
557
|
+
property,
|
|
558
|
+
value: computed[property] ?? null,
|
|
559
|
+
declaredValue: sanitizeString(winner.value),
|
|
560
|
+
selector: winner.isInline ? 'inline' : sanitizeString(winner.selectorText),
|
|
561
|
+
specificity: specificityStr,
|
|
562
|
+
important: winner.important,
|
|
563
|
+
sourceStyleSheetId: winner.rule?.styleSheetId,
|
|
564
|
+
sourceStyleSheetUrl,
|
|
565
|
+
range: winner.range,
|
|
566
|
+
authored,
|
|
567
|
+
generated,
|
|
568
|
+
mediaQuery: winner.rule?.media?.length ? sanitizeString(winner.rule.media.map((m) => m.text).join(' ; ')) : undefined,
|
|
569
|
+
containerQuery: winner.rule?.containerQueries?.length
|
|
570
|
+
? sanitizeString(winner.rule.containerQueries.map((cq) => cq.text).join(' ; '))
|
|
571
|
+
: undefined,
|
|
572
|
+
winnerApproximate: winnerApproximateReason !== undefined ? true : undefined,
|
|
573
|
+
winnerApproximateReason,
|
|
574
|
+
sourceResolutionUnavailable,
|
|
575
|
+
sourceResolutionUnavailableReason,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return declarations;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ============================================================================
|
|
583
|
+
// captureStyleSheetHeaders — real stylesheet URL/id correlation
|
|
584
|
+
// ============================================================================
|
|
585
|
+
|
|
586
|
+
/** CDP `CSS.styleSheetAdded` header shape (only the two fields this module reads). */
|
|
587
|
+
interface StyleSheetHeader {
|
|
588
|
+
readonly styleSheetId?: string;
|
|
589
|
+
readonly sourceURL?: string;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Registers a `CSS.styleSheetAdded` listener FIRST, then forces Chrome to
|
|
594
|
+
* redeliver a header for every currently-known stylesheet by cycling
|
|
595
|
+
* `CSS.disable` + `CSS.enable` (see this module's doc comment for why a
|
|
596
|
+
* bare re-`enable` is insufficient and why the disable/enable pair is
|
|
597
|
+
* safe here), and returns the populated `styleSheetId -> sourceURL` map
|
|
598
|
+
* plus a `stop()` that removes the listener (any stylesheets added
|
|
599
|
+
* *during* the rest of collection still update the map live, until
|
|
600
|
+
* `stop()` is called), plus an explicit `available`/`reason` fact (I-5):
|
|
601
|
+
* when the client can't deliver events at all, or the disable/enable
|
|
602
|
+
* cycle itself throws, `available` is `false` with a `reason` — an empty
|
|
603
|
+
* `urls` map is still returned (an aborted collector is worse than a
|
|
604
|
+
* degraded one), but callers must propagate the unavailable fact rather
|
|
605
|
+
* than let a resulting missing `sourceStyleSheetUrl`/source read as
|
|
606
|
+
* "genuinely no source" when it is really "couldn't check".
|
|
607
|
+
*/
|
|
608
|
+
export async function captureStyleSheetHeaders(
|
|
609
|
+
client: CDPClient,
|
|
610
|
+
): Promise<{ urls: Map<string, string>; stop: () => void; available: boolean; reason?: string }> {
|
|
611
|
+
const urls = new Map<string, string>();
|
|
612
|
+
if (typeof client.on !== 'function') {
|
|
613
|
+
return { urls, stop: () => {}, available: false, reason: 'client-lacks-event-support' };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const handler = (params: unknown): void => {
|
|
617
|
+
const header = (params as { header?: StyleSheetHeader } | undefined)?.header;
|
|
618
|
+
if (header?.styleSheetId && typeof header.sourceURL === 'string' && header.sourceURL.length > 0) {
|
|
619
|
+
urls.set(header.styleSheetId, header.sourceURL);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
client.on('CSS.styleSheetAdded', handler);
|
|
623
|
+
|
|
624
|
+
const removable = client as unknown as {
|
|
625
|
+
off?: (event: string, handler: (params: unknown) => void) => void;
|
|
626
|
+
removeListener?: (event: string, handler: (params: unknown) => void) => void;
|
|
627
|
+
};
|
|
628
|
+
const stop = (): void => {
|
|
629
|
+
if (typeof removable.off === 'function') removable.off('CSS.styleSheetAdded', handler);
|
|
630
|
+
else if (typeof removable.removeListener === 'function') removable.removeListener('CSS.styleSheetAdded', handler);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
await client.send('CSS.disable');
|
|
635
|
+
await client.send('CSS.enable');
|
|
636
|
+
} catch {
|
|
637
|
+
// Proceed with whatever headers (if any) were captured before the failure, but flag it —
|
|
638
|
+
// an empty `urls` map from here is otherwise indistinguishable from "no stylesheets exist".
|
|
639
|
+
return { urls, stop, available: false, reason: 'stylesheet-header-redelivery-failed' };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return { urls, stop, available: true };
|
|
643
|
+
}
|