refactor(domain): extract source-map envelope walk and disambiguate stringValue (#828)

Move the indexed sections walk shared by the browser fetcher and
the artifact reader into domain/sourceMapEnvelope with the
fetchers stricter offset validation as an option; both validators
keep their leaf strictness. Rename the browser CDP stringValue to
cdpStringValue across its 18 files, fold the fingerprint duplicate
into the static helper, and rename the version-keys shadow to
nonEmptyString.
This commit is contained in:
morluto
2026-10-07 11:23:37 +08:00
committed by GitHub
parent 6dd719eaf6
commit 64ebdf4501
24 changed files with 300 additions and 200 deletions
+3 -14
View File
@@ -4,6 +4,7 @@ import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticA
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../domain/sourceMapContents.js";
import { flattenSourceMapLeaves } from "../domain/sourceMapEnvelope.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
@@ -300,20 +301,8 @@ const unavailableSourceMap = (
const flattenSourceMaps = (
root: unknown,
): Readonly<Record<string, unknown>>[] | undefined => {
if (!isRecord(root) || root.version !== 3) return undefined;
const maps: Readonly<Record<string, unknown>>[] = [];
const pending = [root];
while (pending.length > 0) {
const map = pending.pop();
if (map === undefined || map.version !== 3) return undefined;
if (Array.isArray(map.sections)) {
for (const section of map.sections) {
if (!isRecord(section) || !isRecord(section.map)) return undefined;
pending.push(section.map);
}
} else maps.push(map);
}
return maps;
const leaves = flattenSourceMapLeaves(root);
return leaves === undefined ? undefined : [...leaves];
};
const invalidSourceMap = (
+17 -17
View File
@@ -8,7 +8,7 @@ import {
recordValue,
recordsValue,
requiredRecord,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
@@ -38,7 +38,7 @@ export const captureFrames = (
completeness?.exclude("frames", "invalid_protocol_value"),
)) {
const frame = recordValue(tree.frame);
const frameId = stringValue(frame?.id);
const frameId = cdpStringValue(frame?.id);
const sanitized = allowedSanitizedUrl(frame?.url, allowedOrigins);
if (frame === undefined || frameId === undefined) {
completeness?.exclude("frames", "invalid_protocol_value");
@@ -47,14 +47,14 @@ export const captureFrames = (
if (sanitized === undefined || sanitized.origin === null) {
completeness?.exclude(
"frames",
exclusionReasonForUrl(stringValue(frame.url)),
exclusionReasonForUrl(cdpStringValue(frame.url)),
);
continue;
}
if (maximum !== undefined && items.length >= maximum) continue;
items.push({
frame_id: frameId,
parent_frame_id: stringValue(frame.parentId) ?? null,
parent_frame_id: cdpStringValue(frame.parentId) ?? null,
url: sanitized.url,
origin: sanitized.origin,
});
@@ -64,7 +64,7 @@ export const captureFrames = (
/** Read the current main-frame URL from an untrusted Page.getFrameTree result. */
export const mainFrameUrl = (result: unknown): string | undefined =>
stringValue(
cdpStringValue(
recordValue(recordValue(requiredRecord(result).frameTree)?.frame)?.url,
);
@@ -90,17 +90,17 @@ export const captureResources = (
if (url === undefined || url.origin === null) {
completeness?.exclude(
"resources",
exclusionReasonForUrl(stringValue(resource.url)),
exclusionReasonForUrl(cdpStringValue(resource.url)),
);
continue;
}
const contentSize = numberValue(resource.contentSize);
items.push({
rawUrl: stringValue(resource.url) ?? "",
rawUrl: cdpStringValue(resource.url) ?? "",
url: url.url,
origin: url.origin,
type: stringValue(resource.type) ?? "Other",
mime_type: stringValue(resource.mimeType) ?? "",
type: cdpStringValue(resource.type) ?? "Other",
mime_type: cdpStringValue(resource.mimeType) ?? "",
content_size:
contentSize === undefined ? null : Math.max(0, contentSize),
});
@@ -149,7 +149,7 @@ export const captureDom = (
} => {
const root = requiredRecord(result);
const strings = Array.isArray(root.strings)
? root.strings.map((value) => stringValue(value) ?? "")
? root.strings.map((value) => cdpStringValue(value) ?? "")
: [];
const nodes: WebPageInspection["dom"]["nodes"] = [];
const urls: WebPageInspection["metadata"]["dom_urls"] = [];
@@ -240,14 +240,14 @@ export const captureAccessibility = (
);
const nodeIds = new Set(
all.flatMap((node) => {
const nodeId = stringValue(node.nodeId);
const nodeId = cdpStringValue(node.nodeId);
return nodeId === undefined ? [] : [nodeId];
}),
);
let excludedFields = 0;
const nodes = all.map((node) => {
const captureText = (value: unknown): string | null => {
const raw = stringValue(recordValue(value)?.value);
const raw = cdpStringValue(recordValue(value)?.value);
if (raw === undefined) return null;
if (!options.includeText) {
excludedFields += 1;
@@ -256,8 +256,8 @@ export const captureAccessibility = (
return raw;
};
return {
node_id: stringValue(node.nodeId) ?? "",
parent_id: stringValue(node.parentId) ?? null,
node_id: cdpStringValue(node.nodeId) ?? "",
parent_id: cdpStringValue(node.parentId) ?? null,
role: axText(node.role),
name: captureText(node.name),
description: captureText(node.description),
@@ -272,7 +272,7 @@ export const captureAccessibility = (
(node) =>
Array.isArray(node.childIds) &&
node.childIds.some((childId) => {
const id = stringValue(childId);
const id = cdpStringValue(childId);
return id !== undefined && !nodeIds.has(id);
}),
),
@@ -326,7 +326,7 @@ const accessibilityStates = (
): WebPageInspection["accessibility"]["nodes"][number]["states"] => {
const states = new Map<string, boolean | number | string>();
for (const property of recordsValue(value)) {
const name = stringValue(property.name);
const name = cdpStringValue(property.name);
if (name === undefined || !ACCESSIBILITY_STATE_NAMES.has(name)) continue;
const raw = recordValue(property.value)?.value;
const state =
@@ -345,7 +345,7 @@ const accessibilityStates = (
};
const axText = (value: unknown): string | null =>
stringValue(recordValue(value)?.value) ?? null;
cdpStringValue(recordValue(value)?.value) ?? null;
const indexedString = (strings: readonly string[], index: unknown): string => {
const integer = numberValue(index);
+3 -3
View File
@@ -1,7 +1,7 @@
import { inferJsonShape, type JsonShape } from "../domain/jsonShape.js";
import {
recordValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { decodeBase64, isJsonContentType } from "./CdpCaptureEventHelpers.js";
@@ -14,7 +14,7 @@ export const ingestResponseBodyShape = (
value: unknown,
): void => {
const result = recordValue(value);
const body = stringValue(result?.body);
const body = cdpStringValue(result?.body);
if (body === undefined) {
invalidResponseBodyShape(state, requestId);
return;
@@ -37,7 +37,7 @@ export const requestBodyShape = (
): NetworkState["body_shapes"] => {
if (!state.input.include_json_body_shapes)
return { status: "not_approved", request: null, response: null };
const body = stringValue(request.postData);
const body = cdpStringValue(request.postData);
if (!isJsonContentType(recordValue(request.headers)) || body === undefined)
return { status: "unavailable", request: null, response: null };
const inferred = inferBodyShape(body);
+26 -26
View File
@@ -7,7 +7,7 @@ import {
numberValue,
recordValue,
recordsValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { requestBodyShape } from "./CdpCaptureEventBodyShapes.js";
@@ -30,7 +30,7 @@ export const handleExecutionContextCreated = (
): void => {
const context = recordValue(params.context);
const identifier = numberValue(context?.id);
const frameId = stringValue(recordValue(context?.auxData)?.frameId);
const frameId = cdpStringValue(recordValue(context?.auxData)?.frameId);
if (
identifier === undefined ||
!Number.isSafeInteger(identifier) ||
@@ -53,8 +53,8 @@ export const handleScriptParsed = (
state: CdpCaptureEventsState,
params: UnknownRecord,
): void => {
const scriptId = stringValue(params.scriptId);
const rawUrl = stringValue(params.url) ?? "";
const scriptId = cdpStringValue(params.scriptId);
const rawUrl = cdpStringValue(params.url) ?? "";
const sanitized = allowedSanitizedUrl(rawUrl, state.allowedOrigins);
if (scriptId === undefined) {
state.completeness.exclude("scripts", "invalid_protocol_value");
@@ -70,7 +70,7 @@ export const handleScriptParsed = (
rawUrl,
url: sanitized.url,
origin: sanitized.origin,
hash: stringValue(params.hash) ?? "",
hash: cdpStringValue(params.hash) ?? "",
length: Math.max(
0,
Math.min(
@@ -79,7 +79,7 @@ export const handleScriptParsed = (
),
),
isModule: params.isModule === true,
language: stringValue(params.scriptLanguage) ?? null,
language: cdpStringValue(params.scriptLanguage) ?? null,
sourceMapUrl: sourceMap.sanitized,
sourceMapRawUrl: sourceMap.raw,
executionContextKey: executionContextKey(params.executionContextId),
@@ -92,7 +92,7 @@ const sourceMapForScript = (
scriptUrl: string,
state: CdpCaptureEventsState,
): { readonly sanitized: string | null; readonly raw: string | null } => {
const declaredUrl = stringValue(value);
const declaredUrl = cdpStringValue(value);
if (declaredUrl === undefined || declaredUrl === "")
return { sanitized: null, raw: null };
let rawUrl: string;
@@ -114,7 +114,7 @@ export const handleRequestWillBeSent = (
state: CdpCaptureEventsState,
params: UnknownRecord,
): void => {
const requestId = stringValue(params.requestId);
const requestId = cdpStringValue(params.requestId);
const request = recordValue(params.request);
if (requestId === undefined) {
state.completeness.exclude("network_requests", "invalid_protocol_value");
@@ -129,14 +129,14 @@ export const handleRequestWillBeSent = (
if (sanitized === undefined) {
state.completeness.exclude(
"network_requests",
exclusionReasonForUrl(stringValue(request.url)),
exclusionReasonForUrl(cdpStringValue(request.url)),
);
state.network.delete(requestId);
return;
}
const initiator = recordValue(params.initiator);
const initiatorFrame = initiatorLocation(initiator);
const rawInitiatorUrl = stringValue(initiatorFrame?.url);
const rawInitiatorUrl = cdpStringValue(initiatorFrame?.url);
const initiatorUrl = allowedSanitizedUrl(
rawInitiatorUrl,
state.allowedOrigins,
@@ -150,13 +150,13 @@ export const handleRequestWillBeSent = (
request_id: requestId,
url: sanitized.url,
origin: sanitized.origin ?? "",
method: stringValue(request.method) ?? "GET",
resource_type: stringValue(params.type) ?? null,
method: cdpStringValue(request.method) ?? "GET",
resource_type: cdpStringValue(params.type) ?? null,
status: null,
mime_type: null,
encoded_data_length: null,
initiator: {
type: stringValue(initiator?.type) ?? "other",
type: cdpStringValue(initiator?.type) ?? "other",
url: initiatorUrl?.url ?? null,
line: integerOrNull(initiatorFrame?.lineNumber),
column: integerOrNull(initiatorFrame?.columnNumber),
@@ -169,7 +169,7 @@ export const handleResponseReceived = (
state: CdpCaptureEventsState,
params: UnknownRecord,
): void => {
const requestId = stringValue(params.requestId);
const requestId = cdpStringValue(params.requestId);
if (requestId === undefined) {
state.completeness.exclude("network_requests", "invalid_protocol_value");
return;
@@ -183,7 +183,7 @@ export const handleResponseReceived = (
"network_requests",
response === undefined
? "invalid_protocol_value"
: exclusionReasonForUrl(stringValue(response.url)),
: exclusionReasonForUrl(cdpStringValue(response.url)),
);
state.network.delete(requestId);
return;
@@ -191,7 +191,7 @@ export const handleResponseReceived = (
state.network.set(requestId, {
...current,
status: numberValue(response.status) ?? null,
mime_type: stringValue(response.mimeType) ?? null,
mime_type: cdpStringValue(response.mimeType) ?? null,
});
const metadata = safeResponseMetadata(
requestId,
@@ -207,7 +207,7 @@ export const handleLoadingFinished = (
state: CdpCaptureEventsState,
params: UnknownRecord,
): void => {
const requestId = stringValue(params.requestId);
const requestId = cdpStringValue(params.requestId);
if (requestId === undefined) return;
const current = state.network.get(requestId);
if (current === undefined) return;
@@ -233,16 +233,16 @@ export const handleConsoleAPICalled = (
if (source === undefined) {
state.completeness.exclude(
"console_events",
exclusionReasonForUrl(stringValue(frame.url)),
exclusionReasonForUrl(cdpStringValue(frame.url)),
);
return;
}
const arguments_ = recordsValue(params.args);
state.console.push({
type: stringValue(params.type) ?? "unknown",
type: cdpStringValue(params.type) ?? "unknown",
timestamp: numberValue(params.timestamp) ?? 0,
argument_types: arguments_.map(
(argument) => stringValue(argument.type) ?? "unknown",
(argument) => cdpStringValue(argument.type) ?? "unknown",
),
url: source.url,
line: integerOrNull(frame.lineNumber),
@@ -287,14 +287,14 @@ export const handleWebSocketFrame = (
params: UnknownRecord,
direction: "sent" | "received",
): void => {
const requestId = stringValue(params.requestId);
const requestId = cdpStringValue(params.requestId);
if (
requestId === undefined ||
(!state.network.has(requestId) && !state.allowedWebSockets.has(requestId))
)
return;
const response = recordValue(params.response);
const payload = stringValue(response?.payloadData) ?? "";
const payload = cdpStringValue(response?.payloadData) ?? "";
const opcode = Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0));
const decoded = opcode === 1 ? undefined : decodeBase64(payload);
if (opcode !== 1 && decoded === undefined)
@@ -327,8 +327,8 @@ export const handleWebSocketCreated = (
state: CdpCaptureEventsState,
params: UnknownRecord,
): void => {
const requestId = stringValue(params.requestId);
const rawUrl = stringValue(params.url);
const requestId = cdpStringValue(params.requestId);
const rawUrl = cdpStringValue(params.url);
if (requestId === undefined) {
state.completeness.exclude(
"websocket_connections",
@@ -378,8 +378,8 @@ export const handleFrameNavigated = (
)
)
return;
const rawUrl = stringValue(frame?.url);
const loaderId = stringValue(frame?.loaderId);
const rawUrl = cdpStringValue(frame?.url);
const loaderId = cdpStringValue(frame?.loaderId);
// `frameNavigated` re-fires for a document already reported (same URL and
// loader). Treating that as a navigation aborted a valid capture with
// `target_changed`, which is a false claim that the target moved.
+6 -6
View File
@@ -3,7 +3,7 @@ import {
numberValue,
recordValue,
recordsValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import type { CdpEvent } from "./CdpConnection.js";
@@ -17,10 +17,10 @@ export const isMainFrameNavigation = (
if (params === undefined) return false;
const frameId =
event.method === "Page.frameNavigated"
? stringValue(recordValue(params.frame)?.id)
? cdpStringValue(recordValue(params.frame)?.id)
: event.method === "Page.frameRequestedNavigation" ||
event.method === "Page.navigatedWithinDocument"
? stringValue(params.frameId)
? cdpStringValue(params.frameId)
: undefined;
return frameId === mainFrameId;
};
@@ -38,7 +38,7 @@ export const initiatorLocation = (
if (frame !== undefined) return frame;
stack = recordValue(stack.parent);
}
return stringValue(initiator?.url) === undefined ? undefined : initiator;
return cdpStringValue(initiator?.url) === undefined ? undefined : initiator;
};
export const exclusionReasonForUrl = (
@@ -69,7 +69,7 @@ export const isJsonContentType = (
for (const [name, value] of Object.entries(headers))
if (
name.toLowerCase() === "content-type" &&
isJsonMediaType(stringValue(value))
isJsonMediaType(cdpStringValue(value))
)
return true;
return false;
@@ -85,7 +85,7 @@ export const isJsonMediaType = (value: string | null | undefined): boolean => {
export const consolePrimitive = (
value: UnknownRecord,
): { readonly type: string; readonly text: string } | undefined => {
const type = stringValue(value.type);
const type = cdpStringValue(value.type);
switch (type) {
case "string":
return typeof value.value === "string"
+6 -6
View File
@@ -13,7 +13,7 @@ import {
numberValue,
recordValue,
recordsValue,
stringValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
/** Capture redacted storage metadata for an authorized page and its origin. */
@@ -108,8 +108,8 @@ const capturedCaches = (
values: readonly Record<string, unknown>[],
): CapturedCache[] =>
values.flatMap((cache) => {
const name = stringValue(cache.cacheName);
const id = stringValue(cache.cacheId);
const name = cdpStringValue(cache.cacheName);
const id = cdpStringValue(cache.cacheId);
return name === undefined || id === undefined ? [] : [{ name, id }];
});
@@ -130,8 +130,8 @@ const storageItems = async (
return { items: [], complete: false };
const items = entries.flatMap((entry) => {
if (!Array.isArray(entry)) return [];
const key = stringValue(entry[0]);
const value = stringValue(entry[1]);
const key = cdpStringValue(entry[0]);
const value = cdpStringValue(entry[1]);
return key === undefined || value === undefined ? [] : [{ key, value }];
});
return { items, complete: items.length === entries.length };
@@ -140,7 +140,7 @@ const storageItems = async (
const stringArray = (value: unknown): readonly string[] =>
Array.isArray(value)
? value.flatMap((item) => {
const text = stringValue(item);
const text = cdpStringValue(item);
return text === undefined ? [] : [text];
})
: [];
+20 -16
View File
@@ -9,7 +9,11 @@ import type {
import { compareCodePoints } from "../domain/canonicalOrdering.js";
import { CdpConnection } from "./CdpConnection.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js";
import {
recordValue,
recordsValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
type StorageFingerprint =
WebPageInspection["storage"]["content_fingerprints"][number];
@@ -132,10 +136,10 @@ const addCookies = async (
const cookies = recordsValue(cookieValues);
if (cookies.length !== cookieValues.length) run.state.complete = false;
for (const cookie of cookies) {
const name = stringValue(cookie.name);
const value = stringValue(cookie.value);
const domain = stringValue(cookie.domain);
const path = stringValue(cookie.path);
const name = cdpStringValue(cookie.name);
const value = cdpStringValue(cookie.value);
const domain = cdpStringValue(cookie.domain);
const path = cdpStringValue(cookie.path);
if (
name === undefined ||
value === undefined ||
@@ -176,7 +180,7 @@ const addIndexedDb = async (
run.state.complete = false;
continue;
}
if (stringValue(database.name) !== databaseName) {
if (cdpStringValue(database.name) !== databaseName) {
run.state.complete = false;
continue;
}
@@ -194,7 +198,7 @@ const addIndexedDb = async (
complete: indexedDbSchemaComplete(stores),
});
for (const store of stores) {
const objectStoreName = stringValue(store.name);
const objectStoreName = cdpStringValue(store.name);
if (objectStoreName === undefined) {
run.state.complete = false;
continue;
@@ -209,11 +213,11 @@ const addIndexedDb = async (
};
const indexedDbStoreSchema = (store: Readonly<Record<string, unknown>>) => ({
name: stringValue(store.name) ?? null,
name: cdpStringValue(store.name) ?? null,
key_path: stableValue(store.keyPath),
auto_increment: store.autoIncrement === true,
indexes: recordsValue(store.indexes).map((index) => ({
name: stringValue(index.name) ?? null,
name: cdpStringValue(index.name) ?? null,
key_path: stableValue(index.keyPath),
unique: index.unique === true,
multi_entry: index.multiEntry === true,
@@ -225,14 +229,14 @@ const indexedDbSchemaComplete = (
): boolean =>
stores.every(
(store) =>
stringValue(store.name) !== undefined &&
cdpStringValue(store.name) !== undefined &&
stableValueComplete(store.keyPath) &&
typeof store.autoIncrement === "boolean" &&
Array.isArray(store.indexes) &&
recordsValue(store.indexes).length === store.indexes.length &&
recordsValue(store.indexes).every(
(index) =>
stringValue(index.name) !== undefined &&
cdpStringValue(index.name) !== undefined &&
stableValueComplete(index.keyPath) &&
typeof index.unique === "boolean" &&
typeof index.multiEntry === "boolean",
@@ -358,8 +362,8 @@ const addCacheEntry = async (
cache: CapturedCache,
entry: Readonly<Record<string, unknown>>,
): Promise<void> => {
const requestURL = stringValue(entry.requestURL);
const requestMethod = stringValue(entry.requestMethod);
const requestURL = cdpStringValue(entry.requestURL);
const requestMethod = cdpStringValue(entry.requestMethod);
if (requestURL === undefined || requestMethod === undefined) {
run.state.complete = false;
return;
@@ -370,8 +374,8 @@ const addCacheEntry = async (
}
const headerRecords = recordsValue(entry.requestHeaders);
const requestHeaders = headerRecords.flatMap((header) => {
const name = stringValue(header.name);
const value = stringValue(header.value);
const name = cdpStringValue(header.name);
const value = cdpStringValue(header.value);
return name === undefined || value === undefined ? [] : [{ name, value }];
});
if (
@@ -387,7 +391,7 @@ const addCacheEntry = async (
{ cacheId: cache.id, requestURL, requestHeaders },
run.limitations,
);
const body = stringValue(recordValue(recordValue(raw)?.response)?.body);
const body = cdpStringValue(recordValue(recordValue(raw)?.response)?.body);
const decoded = body === undefined ? null : decodeBase64(body);
const complete = decoded !== null;
if (!complete) run.state.complete = false;
+2 -2
View File
@@ -20,7 +20,7 @@ export const recordsValue = (value: unknown): readonly UnknownRecord[] =>
})
: [];
export const stringValue = (value: unknown): string | undefined =>
export const cdpStringValue = (value: unknown): string | undefined =>
typeof value === "string" ? value : undefined;
export const numberValue = (value: unknown): number | undefined =>
@@ -49,7 +49,7 @@ export const allowedSanitizedUrl = (
value: unknown,
allowedOrigins: ReadonlySet<string>,
): ReturnType<typeof sanitizeBrowserUrl> | undefined => {
const text = stringValue(value);
const text = cdpStringValue(value);
if (text === undefined) return undefined;
const sanitized = sanitizeBrowserUrl(text);
return sanitized.origin !== null && allowedOrigins.has(sanitized.origin)
+10 -10
View File
@@ -14,7 +14,7 @@ import {
recordValue,
recordsValue,
requiredRecord,
stringValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import { walkFrameTrees } from "./CdpCaptureDocuments.js";
import {
@@ -220,9 +220,9 @@ const navigatedFrameId = (event: CdpEvent): string | undefined => {
const params = recordValue(event.params);
if (params === undefined) return undefined;
if (event.method === "Page.frameNavigated")
return stringValue(recordValue(params.frame)?.id);
return cdpStringValue(recordValue(params.frame)?.id);
if (event.method === "Page.navigatedWithinDocument")
return stringValue(params.frameId);
return cdpStringValue(params.frameId);
return undefined;
};
@@ -238,15 +238,15 @@ const captureFrames = async (
completeness.exclude("frames", "invalid_protocol_value"),
)) {
const frame = recordValue(tree.frame);
const frameId = stringValue(frame?.id);
const path = await authorizedElectronFile(stringValue(frame?.url) ?? "");
const frameId = cdpStringValue(frame?.id);
const path = await authorizedElectronFile(cdpStringValue(frame?.url) ?? "");
if (frameId === undefined || path === undefined) {
completeness.exclude("frames", "out_of_target_scope");
continue;
}
frames.push({
frame_id: frameId,
parent_frame_id: stringValue(frame?.parentId) ?? null,
parent_frame_id: cdpStringValue(frame?.parentId) ?? null,
file_path: path,
});
}
@@ -274,7 +274,7 @@ const captureResources = async (
if (tree === undefined) break;
for (const resource of recordsValue(tree.resources)) {
const path = await authorizedElectronFile(
stringValue(resource.url) ?? "",
cdpStringValue(resource.url) ?? "",
);
if (path === undefined) {
completeness.exclude("resources", "out_of_target_scope");
@@ -282,8 +282,8 @@ const captureResources = async (
}
const item = {
file_path: path,
type: stringValue(resource.type) ?? "Other",
mime_type: stringValue(resource.mimeType) ?? "",
type: cdpStringValue(resource.type) ?? "Other",
mime_type: cdpStringValue(resource.mimeType) ?? "",
content_size:
numberValue(resource.contentSize) === undefined
? null
@@ -358,7 +358,7 @@ const captureDom = async (
const mainFilePath = async (result: unknown): Promise<string | undefined> => {
const frameTree = recordValue(requiredRecord(result).frameTree);
const frame = recordValue(frameTree?.frame);
return await authorizedElectronFile(stringValue(frame?.url) ?? "");
return await authorizedElectronFile(cdpStringValue(frame?.url) ?? "");
};
const arrayValue = (value: unknown): readonly unknown[] =>
+10 -6
View File
@@ -1,6 +1,10 @@
import type { CdpEvent } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { numberValue, recordValue, stringValue } from "./CdpCaptureValues.js";
import {
numberValue,
recordValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
export interface ElectronScriptDraft {
readonly scriptId: string;
@@ -35,8 +39,8 @@ export const ingestElectronScriptEvent = (input: {
}
if (input.event.method !== "Debugger.scriptParsed") return;
const value = recordValue(input.event.params);
const scriptId = stringValue(value?.scriptId);
const rawUrl = stringValue(value?.url);
const scriptId = cdpStringValue(value?.scriptId);
const rawUrl = cdpStringValue(value?.url);
if (scriptId === undefined) {
input.completeness.exclude("scripts", "invalid_protocol_value");
return;
@@ -48,10 +52,10 @@ export const ingestElectronScriptEvent = (input: {
input.scripts.push({
scriptId,
rawUrl,
hash: stringValue(value?.hash) ?? "",
hash: cdpStringValue(value?.hash) ?? "",
length: nonnegativeInteger(value?.length),
isModule: value?.isModule === true,
language: stringValue(value?.scriptLanguage) ?? null,
language: cdpStringValue(value?.scriptLanguage) ?? null,
executionContextKey: executionContextKey(value?.executionContextId),
});
};
@@ -63,7 +67,7 @@ const retainExecutionContext = (
const parameters = recordValue(event.params);
const runtimeContext = recordValue(parameters?.context);
const contextKey = executionContextKey(runtimeContext?.id);
const frameId = stringValue(recordValue(runtimeContext?.auxData)?.frameId);
const frameId = cdpStringValue(recordValue(runtimeContext?.auxData)?.frameId);
if (contextKey === null || frameId === undefined) return;
frames.set(contextKey, frameId);
};
+2 -2
View File
@@ -6,7 +6,7 @@ import { canonicalDigest } from "../domain/comparisonSemantics.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { CdpConnection } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { requiredRecord, stringValue } from "./CdpCaptureValues.js";
import { requiredRecord, cdpStringValue } from "./CdpCaptureValues.js";
import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
@@ -94,7 +94,7 @@ const captureScriptSource = async (
input.signal,
),
);
const text = stringValue(result.scriptSource) ?? "";
const text = cdpStringValue(result.scriptSource) ?? "";
return {
source: {
included: true,
+8 -4
View File
@@ -4,7 +4,11 @@ import type {
} from "../domain/electronObservation.js";
import type { CdpConnection } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js";
import {
recordValue,
recordsValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
@@ -35,7 +39,7 @@ export const captureElectronWorkers = async (input: {
}
const workers: ElectronPageInspection["workers"] = [];
for (const target of recordsValue(recordValue(result)?.targetInfos)) {
const type = stringValue(target.type) ?? "";
const type = cdpStringValue(target.type) ?? "";
if (!type.includes("worker")) continue;
const openerTargetId = boundedTargetField(target.openerId);
const parentFrameId = boundedTargetField(target.parentFrameId);
@@ -46,7 +50,7 @@ export const captureElectronWorkers = async (input: {
input.completeness.exclude("workers", "out_of_target_scope");
continue;
}
const path = await authorizedElectronFile(stringValue(target.url) ?? "");
const path = await authorizedElectronFile(cdpStringValue(target.url) ?? "");
if (path === undefined) {
input.completeness.exclude("workers", "out_of_target_scope");
continue;
@@ -71,6 +75,6 @@ export const captureElectronWorkers = async (input: {
};
const boundedTargetField = (value: unknown): string | null => {
const field = stringValue(value);
const field = cdpStringValue(value);
return field === undefined || field === "" ? null : field;
};
+17 -16
View File
@@ -14,7 +14,7 @@ import {
isHttpUrl,
numberValue,
recordValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { mainFrameUrl } from "./CdpCaptureDocuments.js";
@@ -218,7 +218,7 @@ class TimelineCapture {
this.#add({
type: "navigation_requested",
params,
rawUrl: stringValue(params.url) ?? null,
rawUrl: cdpStringValue(params.url) ?? null,
requestId: null,
detail: reason,
});
@@ -227,7 +227,7 @@ class TimelineCapture {
#navigationCommitted(event: CdpEvent, params: UnknownRecord): void {
const frame = recordValue(params.frame);
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
const rawUrl = stringValue(frame?.url);
const rawUrl = cdpStringValue(frame?.url);
const destination = scopedUrl(rawUrl, this.allowedOrigins);
this.#add({
type: this.#pendingReload ? "same_origin_reload" : "navigation_committed",
@@ -247,7 +247,7 @@ class TimelineCapture {
#sameDocument(event: CdpEvent, params: UnknownRecord): void {
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
const rawUrl = stringValue(params.url);
const rawUrl = cdpStringValue(params.url);
const destination = scopedUrl(rawUrl, this.allowedOrigins);
this.#add({
type: "same_document_navigation",
@@ -265,21 +265,21 @@ class TimelineCapture {
}
#redirect(params: UnknownRecord): void {
const resourceType = stringValue(params.type);
const resourceType = cdpStringValue(params.type);
if (resourceType !== undefined && resourceType !== "Document") return;
if (
stringValue(params.frameId) !== this.mainFrameId ||
cdpStringValue(params.frameId) !== this.mainFrameId ||
recordValue(params.redirectResponse) === undefined
)
return;
const request = recordValue(params.request);
const rawUrl = stringValue(request?.url);
const rawUrl = cdpStringValue(request?.url);
const destination = scopedUrl(rawUrl, this.allowedOrigins);
this.#add({
type: "redirect",
params,
rawUrl: rawUrl ?? null,
requestId: stringValue(params.requestId) ?? null,
requestId: cdpStringValue(params.requestId) ?? null,
detail: null,
});
if (destination.scope !== "approved") {
@@ -289,19 +289,19 @@ class TimelineCapture {
}
#loadingFailed(params: UnknownRecord): void {
const frame = stringValue(params.frameId);
const frame = cdpStringValue(params.frameId);
if (frame !== undefined && frame !== this.mainFrameId) return;
this.#add({
type: "load_failed",
params,
rawUrl: null,
requestId: stringValue(params.requestId) ?? null,
requestId: cdpStringValue(params.requestId) ?? null,
detail: safeNetworkError(params.errorText),
});
}
#lifecycle(params: UnknownRecord): void {
if (stringValue(params.frameId) !== this.mainFrameId) return;
if (cdpStringValue(params.frameId) !== this.mainFrameId) return;
this.#add({
type: "lifecycle",
params,
@@ -344,7 +344,7 @@ const observationEventMatches = (
return event.sessionId === sessionId;
return (
sessionId !== undefined &&
stringValue(recordValue(event.params)?.sessionId) === sessionId
cdpStringValue(recordValue(event.params)?.sessionId) === sessionId
);
};
@@ -405,7 +405,7 @@ const captureFinalUrl = async (
};
const frameId = (result: unknown): string | undefined =>
stringValue(
cdpStringValue(
recordValue(recordValue(recordValue(result)?.frameTree)?.frame)?.id,
);
@@ -423,17 +423,18 @@ const scopedUrl = (
: { url: null, scope: "unsupported" };
};
const boundedId = (value: unknown): string | null => stringValue(value) ?? null;
const boundedId = (value: unknown): string | null =>
cdpStringValue(value) ?? null;
const safeDetail = (value: unknown): string | null => {
const detail = stringValue(value)?.toLowerCase();
const detail = cdpStringValue(value)?.toLowerCase();
return detail !== undefined && /^[a-z0-9_.:-]{1,100}$/u.test(detail)
? detail
: null;
};
const safeNetworkError = (value: unknown): string | null => {
const error = stringValue(value);
const error = cdpStringValue(value);
return error !== undefined && /^net::ERR_[A-Z0-9_]{1,100}$/u.test(error)
? error
: null;
+2 -2
View File
@@ -10,7 +10,7 @@ import {
requiredRecord,
sourceExcluded,
sourceResult,
stringValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import type { CaptureContext } from "./CdpPageCapture.js";
import type { WebSourceMapRequest } from "./WebSourceMapFetcher.js";
@@ -115,7 +115,7 @@ const captureScriptSource = async (
context.signal,
),
);
const content = stringValue(result.scriptSource) ?? "";
const content = cdpStringValue(result.scriptSource) ?? "";
return sourceResult(content);
};
+9 -9
View File
@@ -5,7 +5,7 @@ import {
isHttpUrl,
recordValue,
recordsValue,
stringValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import type { CaptureContext } from "./CdpPageCapture.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
@@ -32,19 +32,19 @@ export const captureWorkers = async (
const items: WebPageInspection["workers"] = [];
if (result === undefined) completeness.unavailable("workers");
for (const target of recordsValue(recordValue(result)?.targetInfos)) {
const type = stringValue(target.type) ?? "";
const type = cdpStringValue(target.type) ?? "";
const url = allowedSanitizedUrl(target.url, allowedOrigins);
const relatedToPage =
stringValue(target.openerId) === context.target.id ||
(stringValue(target.parentFrameId) !== undefined &&
frameIds.has(stringValue(target.parentFrameId) ?? ""));
cdpStringValue(target.openerId) === context.target.id ||
(cdpStringValue(target.parentFrameId) !== undefined &&
frameIds.has(cdpStringValue(target.parentFrameId) ?? ""));
if (!type.includes("worker")) continue;
if (!relatedToPage) {
completeness.exclude("workers", "out_of_target_scope");
continue;
}
if (url === undefined) {
const rawUrl = stringValue(target.url);
const rawUrl = cdpStringValue(target.url);
completeness.exclude(
"workers",
rawUrl === undefined || rawUrl === ""
@@ -56,13 +56,13 @@ export const captureWorkers = async (
continue;
}
items.push({
target_id: stringValue(target.targetId) ?? "",
target_id: cdpStringValue(target.targetId) ?? "",
type,
url: url.url,
origin: url.origin,
attached: target.attached === true,
opener_target_id: stringValue(target.openerId) ?? null,
parent_frame_id: stringValue(target.parentFrameId) ?? null,
opener_target_id: cdpStringValue(target.openerId) ?? null,
parent_frame_id: cdpStringValue(target.parentFrameId) ?? null,
});
}
return items;
+2 -2
View File
@@ -2,7 +2,7 @@ import type { WebPageInspection } from "../domain/browserObservation.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import {
numberValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
@@ -42,7 +42,7 @@ export const safeResponseMetadata = (
response: {
request_id: requestId,
url,
mime_type: boundedHeader(stringValue(response.mimeType)),
mime_type: boundedHeader(cdpStringValue(response.mimeType)),
content_length: nonnegativeInteger(headers.get("content-length")),
content_encoding: boundedHeader(headers.get("content-encoding")),
csp,
+5 -5
View File
@@ -11,7 +11,7 @@ import type { CdpConnection, CdpEvent } from "./CdpConnection.js";
import {
allowedSanitizedUrl,
recordValue,
stringValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import { mainFrameUrl } from "./CdpCaptureDocuments.js";
import { captureFrames } from "./CdpCaptureDocuments.js";
@@ -63,7 +63,7 @@ export const captureCdpScreenshot = async (
changed: false,
leftScope: false,
url: beforeUrl,
loaderId: stringValue(initialFrame?.loaderId),
loaderId: cdpStringValue(initialFrame?.loaderId),
};
const removeListener = context.connection.onEvent((event) => {
if (event.sessionId !== context.sessionId) return;
@@ -89,7 +89,7 @@ export const captureCdpScreenshot = async (
} finally {
removeListener();
}
const encoded = stringValue(result?.data);
const encoded = cdpStringValue(result?.data);
const bytes =
encoded === undefined ? undefined : decodeCanonicalBase64(encoded);
if (bytes === undefined)
@@ -149,8 +149,8 @@ const observeScreenshotNavigation = (
event.method === "Page.frameNavigated" &&
state.loaderId !== undefined &&
state.loaderId !== "" &&
stringValue(frame?.loaderId) === state.loaderId &&
stringValue(frame?.url) === state.url
cdpStringValue(frame?.loaderId) === state.loaderId &&
cdpStringValue(frame?.url) === state.url
)
return;
+17 -14
View File
@@ -16,7 +16,7 @@ import {
numberValue,
recordValue,
recordsValue,
stringValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import {
@@ -173,10 +173,13 @@ const initialFrameScope = (
completeness.exclude("webmcp_tools", "invalid_protocol_value"),
)) {
const frame = recordValue(tree.frame);
const frameId = stringValue(frame?.id);
const frameId = cdpStringValue(frame?.id);
const prior = frameId === undefined ? undefined : scoped.get(frameId);
if (frameId !== undefined && prior !== undefined)
scoped.set(frameId, { ...prior, loaderId: stringValue(frame?.loaderId) });
scoped.set(frameId, {
...prior,
loaderId: cdpStringValue(frame?.loaderId),
});
}
return scoped;
};
@@ -203,7 +206,7 @@ const handleFrameDetached = (
params: UnknownRecord,
state: FrameScopeState,
): boolean => {
const frameId = stringValue(params.frameId);
const frameId = cdpStringValue(params.frameId);
if (frameId === undefined) {
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
return true;
@@ -223,12 +226,12 @@ const handleFrameUpdate = (
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
return true;
}
const frameId = stringValue(frame?.id) ?? stringValue(frame?.frameId);
const frameId = cdpStringValue(frame?.id) ?? cdpStringValue(frame?.frameId);
if (frameId === undefined) {
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
return true;
}
const rawUrl = stringValue(frame?.url);
const rawUrl = cdpStringValue(frame?.url);
// Chromium can briefly commit an inherited, empty document while replacing
// a subframe. That intermediate URL says nothing about the frame's eventual
// scope or its registered tools; wait for the next committed URL (or the
@@ -259,7 +262,7 @@ const updateFrameScope = (
return;
}
const previous = state.frames.get(frameId);
const loaderId = stringValue(frame?.loaderId);
const loaderId = cdpStringValue(frame?.loaderId);
if (hasNewDocument(documentCommitted, previous, url.url, loaderId))
removeFrameTools(frameId, state.tools);
if (documentCommitted && state.transientFrames.has(frameId))
@@ -269,7 +272,7 @@ const updateFrameScope = (
origin: url.origin,
...(loaderId === undefined ? {} : { loaderId }),
parentFrameId:
(stringValue(frame?.parentId) ??
(cdpStringValue(frame?.parentId) ??
state.frames.get(frameId)?.parentFrameId ??
"") ||
null,
@@ -364,8 +367,8 @@ const ingestWebMcpEvent = (options: WebMcpIngestOptions): void => {
if (params === undefined) return;
if (event.method === "WebMCP.toolsRemoved") {
for (const removed of recordsValue(params.tools)) {
const name = stringValue(removed.name);
const frameId = stringValue(removed.frameId);
const name = cdpStringValue(removed.name);
const frameId = cdpStringValue(removed.frameId);
if (name === undefined || frameId === undefined) continue;
for (const [key, tool] of tools)
if (tool.name === name && tool.frame_id === frameId) tools.delete(key);
@@ -374,7 +377,7 @@ const ingestWebMcpEvent = (options: WebMcpIngestOptions): void => {
}
if (event.method !== "WebMCP.toolsAdded") return;
for (const declared of recordsValue(params.tools)) {
const frameId = stringValue(declared.frameId);
const frameId = cdpStringValue(declared.frameId);
if (frameId !== undefined && transientFrames.has(frameId)) {
if (!resolvedTransientFrames.has(frameId)) continue;
transientFrames.delete(frameId);
@@ -395,8 +398,8 @@ const normalizeTool = (
>,
completeness: CdpCaptureCompleteness,
): WebMcpDiscovery["tools"]["items"][number] | undefined => {
const frameId = stringValue(value.frameId);
const name = stringValue(value.name);
const frameId = cdpStringValue(value.frameId);
const name = cdpStringValue(value.name);
const frame = frameId === undefined ? undefined : frames.get(frameId);
if (
frameId === undefined ||
@@ -407,7 +410,7 @@ const normalizeTool = (
completeness.exclude("webmcp_tools", "out_of_target_scope");
return undefined;
}
const description = stringValue(value.description) ?? "";
const description = cdpStringValue(value.description) ?? "";
const annotations = recordValue(value.annotations);
return {
tool_key: toolKey(frame.url, frameId, name),
+4 -4
View File
@@ -19,7 +19,7 @@ import { err, ok, type Result } from "../domain/result.js";
import {
numberValue,
recordValue,
stringValue,
cdpStringValue,
delayWithCancellation,
} from "./CdpCaptureValues.js";
import { CdpConnection, type CdpEvent } from "./CdpConnection.js";
@@ -331,7 +331,7 @@ const ingestScript = (
): void => {
state.scriptsObserved += 1;
const value = recordValue(event.params);
const rawUrl = stringValue(value?.url);
const rawUrl = cdpStringValue(value?.url);
if (rawUrl === undefined || rawUrl === "") {
state.invalidScripts += 1;
retainEvent(state, 0);
@@ -340,7 +340,7 @@ const ingestScript = (
const draft: ScriptDraft = {
rawUrl,
executionContextKey: contextKey(value?.executionContextId),
cdpHash: stringValue(value?.hash) ?? null,
cdpHash: cdpStringValue(value?.hash) ?? null,
length: nonnegativeInteger(value?.length),
isModule: value?.isModule === true,
};
@@ -375,7 +375,7 @@ const ingestContext = (
}
const origin =
event.method === "Runtime.executionContextCreated"
? (stringValue(runtimeContext?.origin) ?? null)
? (cdpStringValue(runtimeContext?.origin) ?? null)
: null;
const draft: ContextDraft = {
contextKey: key,
+9 -27
View File
@@ -14,6 +14,10 @@ import type {
import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { safeParseJson } from "../domain/safeJson.js";
import {
flattenSourceMapLeaves,
isVersion3Map,
} from "../domain/sourceMapEnvelope.js";
export interface WebSourceMapRequest {
readonly scriptKey: string;
@@ -278,32 +282,13 @@ const validSourceMapEnvelope = (text: string): boolean => {
const parsedResult = safeParseJson(text);
if (!parsedResult.ok) return false;
const parsed: unknown = parsedResult.value;
if (!isRecord(parsed) || parsed.version !== 3) return false;
if (typeof parsed.mappings === "string") return validSourceMapLeaf(parsed);
if (!Array.isArray(parsed.sections)) return false;
const pending: unknown[] = [...parsed.sections];
while (pending.length > 0) {
const section = pending.pop();
if (
!isRecord(section) ||
!isRecord(section.offset) ||
!validSourceMapOffset(section.offset.line) ||
!validSourceMapOffset(section.offset.column) ||
!("map" in section)
)
return false;
const map = section.map;
if (!isRecord(map) || map.version !== 3) return false;
if (Array.isArray(map.sections))
for (const child of map.sections) pending.push(child);
else if (!validSourceMapLeaf(map)) return false;
}
return true;
if (isVersion3Map(parsed) && typeof parsed.mappings === "string")
return validSourceMapLeaf(parsed);
const leaves = flattenSourceMapLeaves(parsed, { validateOffsets: true });
if (leaves === undefined) return false;
return leaves.every(validSourceMapLeaf);
};
const validSourceMapOffset = (value: unknown): boolean =>
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
const validSourceMapLeaf = (map: Readonly<Record<string, unknown>>): boolean =>
typeof map.mappings === "string" &&
Array.isArray(map.sources) &&
@@ -380,9 +365,6 @@ const sourceMediaType = (source: string | null): string =>
? "text/typescript"
: "text/javascript";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const originalDependency = (
node: t.Node,
): {
@@ -239,9 +239,9 @@ const semanticValue = (
if (node.kind === "ipc-channel")
return properties.resolution === "dynamic"
? null
: (stringValue(properties.channel) ?? firstLabel(node));
: (nonEmptyString(properties.channel) ?? firstLabel(node));
if (node.kind === "context-bridge-api")
return firstLabel(node) ?? stringValue(properties.api_name);
return firstLabel(node) ?? nonEmptyString(properties.api_name);
if (node.kind === "native-export")
return (
joined(
@@ -258,8 +258,8 @@ const semanticValue = (
].includes(node.kind)
)
return joined(properties.declared_path, properties.resolved_path);
if (node.kind === "package") return stringValue(properties.name);
return canonicalPath(node) ?? stringValue(properties.path);
if (node.kind === "package") return nonEmptyString(properties.name);
return canonicalPath(node) ?? nonEmptyString(properties.path);
};
const canonicalPath = (node: ApplicationNode): string | null =>
@@ -286,11 +286,11 @@ const uniqueStringProperty = (
};
const joined = (...values: readonly unknown[]): string | null => {
const strings = values.map(stringValue);
const strings = values.map(nonEmptyString);
return strings.some((value) => value === null) ? null : strings.join("\0");
};
const stringValue = (value: unknown): string | null =>
const nonEmptyString = (value: unknown): string | null =>
typeof value === "string" && value !== "" ? value : null;
const stringList = (value: unknown): string[] =>
+1 -3
View File
@@ -4,6 +4,7 @@ import * as t from "@babel/types";
import { compareCodePoints } from "./canonicalOrdering.js";
import { propertyName } from "./javascriptAstValues.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
/** Static CommonJS export names. */
export interface StaticExports {
@@ -123,6 +124,3 @@ const memberPath = (node: t.Node | null | undefined): string => {
}
return "";
};
const stringValue = (node: t.Node | null | undefined): string | undefined =>
t.isStringLiteral(node) ? node.value : undefined;
+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { flattenSourceMapLeaves, isVersion3Map } from "./sourceMapEnvelope.js";
const leaf = {
version: 3,
mappings: "AAAA",
sources: ["app.ts"],
names: [],
};
describe("source-map envelope walk", () => {
it("accepts version-3 maps and rejects other shapes", () => {
expect(isVersion3Map(leaf)).toBe(true);
expect(isVersion3Map({ version: 2, mappings: "AAAA" })).toBe(false);
expect(isVersion3Map(null)).toBe(false);
expect(isVersion3Map([{ version: 3 }])).toBe(false);
});
it("collects indexed section leaves without validating offsets by default", () => {
const root = {
version: 3,
sections: [
{ offset: { line: 0, column: 0 }, map: leaf },
{ offset: { line: 10, column: 0 }, map: { ...leaf, sources: [] } },
],
};
expect(flattenSourceMapLeaves(root)).toHaveLength(2);
});
it("rejects non-map sections and enforces offsets when requested", () => {
const missing = {
version: 3,
sections: [{ offset: { line: 0, column: 0 } }],
};
expect(flattenSourceMapLeaves(missing)).toBeUndefined();
const badOffset = {
version: 3,
sections: [{ offset: { line: -1, column: 0 }, map: leaf }],
};
expect(
flattenSourceMapLeaves(badOffset, { validateOffsets: true }),
).toBeUndefined();
expect(flattenSourceMapLeaves(badOffset)).toHaveLength(1);
});
it("rejects non-version-3 roots and nested maps", () => {
expect(flattenSourceMapLeaves({ version: 3 })).toHaveLength(1);
expect(flattenSourceMapLeaves({ mappings: "AAAA" })).toBeUndefined();
expect(
flattenSourceMapLeaves({
version: 3,
sections: [{ offset: { line: 0, column: 0 }, map: { version: 2 } }],
}),
).toBeUndefined();
});
});
+58
View File
@@ -0,0 +1,58 @@
/**
* Structural walk over indexed source-map envelopes. Both the browser
* source-map fetcher and the artifact source-map reader accept version-3
* maps with recursively indexed `sections`; this module owns that walk so
* the two validators cannot drift. Leaf content checks (mappings, sources,
* names, contents) stay with the callers, which enforce different
* strictness.
*/
type SourceMapRecord = Readonly<Record<string, unknown>>;
const isRecord = (value: unknown): value is SourceMapRecord =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Whether a parsed value has the version-3 map shape. */
export const isVersion3Map = (value: unknown): value is SourceMapRecord =>
isRecord(value) && value.version === 3;
const isValidOffset = (value: unknown): boolean =>
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
export interface FlattenSourceMapOptions {
/** Require each section offset to carry safe-integer line/column. */
readonly validateOffsets?: boolean;
}
/**
* Collect leaf maps from an indexed source-map envelope, or undefined when
* the envelope is not a version-3 map. A map carrying neither `mappings`
* nor `sections` yields itself as the single leaf; callers decide whether
* that leaf is valid content.
*/
export const flattenSourceMapLeaves = (
root: unknown,
options?: FlattenSourceMapOptions,
): readonly SourceMapRecord[] | undefined => {
if (!isVersion3Map(root)) return undefined;
const leaves: SourceMapRecord[] = [];
const pending: unknown[] = [root];
while (pending.length > 0) {
const map = pending.pop();
if (!isVersion3Map(map)) return undefined;
if (Array.isArray(map.sections)) {
for (const section of map.sections) {
if (!isRecord(section) || !isRecord(section.map)) return undefined;
if (options?.validateOffsets === true) {
if (!isRecord(section.offset)) return undefined;
if (
!isValidOffset(section.offset.line) ||
!isValidOffset(section.offset.column)
)
return undefined;
}
pending.push(section.map);
}
} else leaves.push(map);
}
return leaves;
};