@pagepocket/lib 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-type.d.ts +2 -0
- package/dist/content-type.js +36 -0
- package/dist/css-rewrite.d.ts +9 -0
- package/dist/css-rewrite.js +76 -0
- package/dist/download-resources.d.ts +25 -0
- package/dist/download-resources.js +163 -0
- package/dist/hack-html.d.ts +9 -0
- package/dist/hack-html.js +32 -0
- package/dist/hackers/index.d.ts +3 -0
- package/dist/hackers/index.js +22 -0
- package/dist/hackers/preload-fetch.d.ts +2 -0
- package/dist/hackers/preload-fetch.js +56 -0
- package/dist/hackers/preload-xhr.d.ts +2 -0
- package/dist/hackers/preload-xhr.js +59 -0
- package/dist/hackers/replay-beacon.d.ts +2 -0
- package/dist/hackers/replay-beacon.js +21 -0
- package/dist/hackers/replay-dom-rewrite.d.ts +2 -0
- package/dist/hackers/replay-dom-rewrite.js +295 -0
- package/dist/hackers/replay-eventsource.d.ts +2 -0
- package/dist/hackers/replay-eventsource.js +25 -0
- package/dist/hackers/replay-fetch.d.ts +2 -0
- package/dist/hackers/replay-fetch.js +33 -0
- package/dist/hackers/replay-svg-image.d.ts +2 -0
- package/dist/hackers/replay-svg-image.js +89 -0
- package/dist/hackers/replay-websocket.d.ts +2 -0
- package/dist/hackers/replay-websocket.js +26 -0
- package/dist/hackers/replay-xhr.d.ts +2 -0
- package/dist/hackers/replay-xhr.js +91 -0
- package/dist/hackers/types.d.ts +10 -0
- package/dist/hackers/types.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -0
- package/dist/network-records.d.ts +4 -0
- package/dist/network-records.js +83 -0
- package/dist/pagepocket.d.ts +18 -0
- package/dist/pagepocket.js +73 -0
- package/dist/preload.d.ts +1 -0
- package/dist/preload.js +60 -0
- package/dist/replay-script.d.ts +1 -0
- package/dist/replay-script.js +347 -0
- package/dist/resources.d.ts +16 -0
- package/dist/resources.js +82 -0
- package/dist/rewrite-links.d.ts +15 -0
- package/dist/rewrite-links.js +263 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +2 -0
- package/package.json +29 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapLighterceptorRecords = exports.findFaviconDataUrl = exports.toDataUrlFromRecord = void 0;
|
|
4
|
+
const getHeaderValue = (headers, name) => {
|
|
5
|
+
for (const key in headers) {
|
|
6
|
+
if (key.toLowerCase() === name.toLowerCase()) {
|
|
7
|
+
return headers[key];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
12
|
+
const toBase64 = (value) => {
|
|
13
|
+
const bufferConstructor = globalThis.Buffer;
|
|
14
|
+
if (bufferConstructor) {
|
|
15
|
+
return bufferConstructor.from(value, "utf-8").toString("base64");
|
|
16
|
+
}
|
|
17
|
+
if (typeof btoa === "function") {
|
|
18
|
+
return btoa(value);
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
};
|
|
22
|
+
const toDataUrlFromRecord = (record) => {
|
|
23
|
+
if (!record)
|
|
24
|
+
return null;
|
|
25
|
+
const headers = record.responseHeaders || {};
|
|
26
|
+
const contentType = getHeaderValue(headers, "content-type") || "application/octet-stream";
|
|
27
|
+
if (record.responseEncoding === "base64" && record.responseBodyBase64) {
|
|
28
|
+
return `data:${contentType};base64,${record.responseBodyBase64}`;
|
|
29
|
+
}
|
|
30
|
+
if (record.responseBody) {
|
|
31
|
+
const encoded = toBase64(record.responseBody);
|
|
32
|
+
if (!encoded) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return `data:${contentType};base64,${encoded}`;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
exports.toDataUrlFromRecord = toDataUrlFromRecord;
|
|
40
|
+
const findFaviconDataUrl = (records) => {
|
|
41
|
+
for (const record of records) {
|
|
42
|
+
if (!record || !record.url)
|
|
43
|
+
continue;
|
|
44
|
+
const headers = record.responseHeaders || {};
|
|
45
|
+
const contentType = (getHeaderValue(headers, "content-type") || "").toLowerCase();
|
|
46
|
+
const pathname = (() => {
|
|
47
|
+
try {
|
|
48
|
+
return new URL(record.url).pathname;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return record.url;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
const looksLikeFavicon = contentType.includes("icon") || /favicon(\.[a-z0-9]+)?$/i.test(pathname || "");
|
|
55
|
+
if (!looksLikeFavicon)
|
|
56
|
+
continue;
|
|
57
|
+
const dataUrl = (0, exports.toDataUrlFromRecord)(record);
|
|
58
|
+
if (dataUrl)
|
|
59
|
+
return dataUrl;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
exports.findFaviconDataUrl = findFaviconDataUrl;
|
|
64
|
+
const mapLighterceptorRecords = (records) => {
|
|
65
|
+
if (!records)
|
|
66
|
+
return [];
|
|
67
|
+
return records.map((record) => {
|
|
68
|
+
const response = record.response;
|
|
69
|
+
return {
|
|
70
|
+
url: record.url,
|
|
71
|
+
method: record.method,
|
|
72
|
+
status: response?.status,
|
|
73
|
+
statusText: response?.statusText,
|
|
74
|
+
responseHeaders: response?.headers,
|
|
75
|
+
responseBody: response?.bodyEncoding === "text" ? response.body : undefined,
|
|
76
|
+
responseBodyBase64: response?.bodyEncoding === "base64" ? response.body : undefined,
|
|
77
|
+
responseEncoding: response?.bodyEncoding,
|
|
78
|
+
error: record.error,
|
|
79
|
+
timestamp: record.timestamp
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
exports.mapLighterceptorRecords = mapLighterceptorRecords;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SnapshotData } from "./types";
|
|
2
|
+
export type PagePocketOptions = {
|
|
3
|
+
assetsDirName?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
requestsPath?: string;
|
|
6
|
+
};
|
|
7
|
+
type RequestsInput = SnapshotData | string;
|
|
8
|
+
export declare class PagePocket {
|
|
9
|
+
private htmlString;
|
|
10
|
+
private requestsJSON;
|
|
11
|
+
private options;
|
|
12
|
+
resources: SnapshotData["resources"];
|
|
13
|
+
downloadedCount: number;
|
|
14
|
+
failedCount: number;
|
|
15
|
+
constructor(htmlString: string, requestsJSON: RequestsInput, options?: PagePocketOptions);
|
|
16
|
+
put(): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PagePocket = void 0;
|
|
4
|
+
const download_resources_1 = require("./download-resources");
|
|
5
|
+
const hack_html_1 = require("./hack-html");
|
|
6
|
+
const network_records_1 = require("./network-records");
|
|
7
|
+
const resources_1 = require("./resources");
|
|
8
|
+
const rewrite_links_1 = require("./rewrite-links");
|
|
9
|
+
const safeFilename = (input) => {
|
|
10
|
+
const trimmed = input.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return "snapshot";
|
|
13
|
+
}
|
|
14
|
+
return (trimmed
|
|
15
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
16
|
+
.replace(/^_+|_+$/g, "")
|
|
17
|
+
.slice(0, 120) || "snapshot");
|
|
18
|
+
};
|
|
19
|
+
const parseRequestsJson = (requestsJSON) => {
|
|
20
|
+
const snapshot = typeof requestsJSON === "string" ? JSON.parse(requestsJSON) : requestsJSON;
|
|
21
|
+
const rawNetworkRecords = (snapshot.networkRecords || []);
|
|
22
|
+
const mappedNetworkRecords = (0, network_records_1.mapLighterceptorRecords)(rawNetworkRecords);
|
|
23
|
+
return {
|
|
24
|
+
snapshot,
|
|
25
|
+
networkRecords: mappedNetworkRecords
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
class PagePocket {
|
|
29
|
+
constructor(htmlString, requestsJSON, options) {
|
|
30
|
+
this.resources = [];
|
|
31
|
+
this.downloadedCount = 0;
|
|
32
|
+
this.failedCount = 0;
|
|
33
|
+
this.htmlString = htmlString;
|
|
34
|
+
this.requestsJSON = requestsJSON;
|
|
35
|
+
this.options = options ?? {};
|
|
36
|
+
}
|
|
37
|
+
async put() {
|
|
38
|
+
const { snapshot, networkRecords } = parseRequestsJson(this.requestsJSON);
|
|
39
|
+
const safeTitle = safeFilename(snapshot.title || "snapshot");
|
|
40
|
+
const assetsDirName = this.options.assetsDirName ?? `${safeTitle}_files`;
|
|
41
|
+
const baseUrl = this.options.baseUrl ?? snapshot.url ?? "";
|
|
42
|
+
const requestsPath = this.options.requestsPath ?? `${safeTitle}.requests.json`;
|
|
43
|
+
const { $, resourceUrls, srcsetItems } = (0, resources_1.extractResourceUrls)(this.htmlString, baseUrl);
|
|
44
|
+
const downloadResult = await (0, download_resources_1.downloadResources)({
|
|
45
|
+
baseUrl,
|
|
46
|
+
assetsDirName,
|
|
47
|
+
resourceUrls,
|
|
48
|
+
srcsetItems,
|
|
49
|
+
referer: baseUrl
|
|
50
|
+
});
|
|
51
|
+
this.resources = downloadResult.resourceMeta;
|
|
52
|
+
this.downloadedCount = downloadResult.downloadedCount;
|
|
53
|
+
this.failedCount = downloadResult.failedCount;
|
|
54
|
+
await (0, rewrite_links_1.rewriteLinks)({
|
|
55
|
+
$,
|
|
56
|
+
resourceUrls,
|
|
57
|
+
srcsetItems,
|
|
58
|
+
baseUrl,
|
|
59
|
+
assetsDirName,
|
|
60
|
+
resourceMap: downloadResult.resourceMap,
|
|
61
|
+
networkRecords
|
|
62
|
+
});
|
|
63
|
+
const faviconDataUrl = (0, network_records_1.findFaviconDataUrl)(networkRecords);
|
|
64
|
+
(0, hack_html_1.hackHtml)({
|
|
65
|
+
$,
|
|
66
|
+
baseUrl,
|
|
67
|
+
requestsPath,
|
|
68
|
+
faviconDataUrl
|
|
69
|
+
});
|
|
70
|
+
return $.html();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.PagePocket = PagePocket;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildPreloadScript: () => string;
|
package/dist/preload.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildPreloadScript = void 0;
|
|
4
|
+
const hackers_1 = require("./hackers");
|
|
5
|
+
const buildPreloadScript = () => {
|
|
6
|
+
const context = { stage: "preload" };
|
|
7
|
+
const hackerScripts = hackers_1.preloadHackers
|
|
8
|
+
.map((hacker) => ` // hacker:${hacker.id}\n${hacker.build(context)}`)
|
|
9
|
+
.join("\n");
|
|
10
|
+
return `
|
|
11
|
+
(function () {
|
|
12
|
+
if (window.__pagepocketPatched) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(window, "__pagepocketPatched", { value: true });
|
|
16
|
+
|
|
17
|
+
const records = [];
|
|
18
|
+
window.__pagepocketRecords = records;
|
|
19
|
+
window.__pagepocketPendingRequests = 0;
|
|
20
|
+
|
|
21
|
+
const toAbsoluteUrl = (input) => {
|
|
22
|
+
try {
|
|
23
|
+
return new URL(input, window.location.href).toString();
|
|
24
|
+
} catch {
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const normalizeBody = (body) => {
|
|
30
|
+
if (body === undefined || body === null) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
if (typeof body === "string") {
|
|
34
|
+
return body;
|
|
35
|
+
}
|
|
36
|
+
if (body instanceof ArrayBuffer) {
|
|
37
|
+
try {
|
|
38
|
+
return new TextDecoder().decode(body);
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (body instanceof Blob) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
return String(body);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const trackPendingStart = () => {
|
|
50
|
+
window.__pagepocketPendingRequests += 1;
|
|
51
|
+
};
|
|
52
|
+
const trackPendingEnd = () => {
|
|
53
|
+
window.__pagepocketPendingRequests = Math.max(0, window.__pagepocketPendingRequests - 1);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
${hackerScripts}
|
|
57
|
+
})();
|
|
58
|
+
`;
|
|
59
|
+
};
|
|
60
|
+
exports.buildPreloadScript = buildPreloadScript;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildReplayScript: (requestsPath: string, baseUrl: string) => string;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildReplayScript = void 0;
|
|
4
|
+
const hackers_1 = require("./hackers");
|
|
5
|
+
const buildReplayScript = (requestsPath, baseUrl) => {
|
|
6
|
+
const basePayload = JSON.stringify(baseUrl);
|
|
7
|
+
const requestsPayload = JSON.stringify(requestsPath);
|
|
8
|
+
const context = { stage: "replay" };
|
|
9
|
+
const hackerScripts = hackers_1.replayHackers
|
|
10
|
+
.map((hacker) => ` // hacker:${hacker.id}\n${hacker.build(context)}`)
|
|
11
|
+
.join("\n");
|
|
12
|
+
return `
|
|
13
|
+
<script>
|
|
14
|
+
(function(){
|
|
15
|
+
// Load the snapshot metadata before patching runtime APIs.
|
|
16
|
+
const baseUrl = ${basePayload};
|
|
17
|
+
const requestsUrl = ${requestsPayload};
|
|
18
|
+
const __pagepocketOriginalFetch = window.fetch ? window.fetch.bind(window) : null;
|
|
19
|
+
|
|
20
|
+
const loadSnapshot = async () => {
|
|
21
|
+
try {
|
|
22
|
+
if (!__pagepocketOriginalFetch) {
|
|
23
|
+
throw new Error("Fetch is unavailable");
|
|
24
|
+
}
|
|
25
|
+
const response = await __pagepocketOriginalFetch(requestsUrl);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error("Failed to load snapshot metadata");
|
|
28
|
+
}
|
|
29
|
+
return await response.json();
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
url: baseUrl,
|
|
33
|
+
title: "",
|
|
34
|
+
capturedAt: "",
|
|
35
|
+
fetchXhrRecords: [],
|
|
36
|
+
networkRecords: [],
|
|
37
|
+
resources: []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Soften JSON parse failures to avoid halting replay flows.
|
|
43
|
+
const originalResponseJson = Response && Response.prototype && Response.prototype.json;
|
|
44
|
+
if (originalResponseJson) {
|
|
45
|
+
Response.prototype.json = function(...args) {
|
|
46
|
+
try {
|
|
47
|
+
return originalResponseJson.apply(this, args).catch(() => null);
|
|
48
|
+
} catch {
|
|
49
|
+
return Promise.resolve(null);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Guard to reapply patches if overwritten later.
|
|
55
|
+
const ensureReplayPatches = () => {
|
|
56
|
+
try {
|
|
57
|
+
if (!window.fetch.__pagepocketOriginal && typeof __pagepocketOriginalFetch === "function") {
|
|
58
|
+
window.fetch.__pagepocketOriginal = __pagepocketOriginalFetch;
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
try {
|
|
62
|
+
if (!XMLHttpRequest.prototype.send.__pagepocketOriginal) {
|
|
63
|
+
XMLHttpRequest.prototype.send.__pagepocketOriginal = XMLHttpRequest.prototype.send;
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let records = [];
|
|
69
|
+
let networkRecords = [];
|
|
70
|
+
const byKey = new Map();
|
|
71
|
+
|
|
72
|
+
const localResourceSet = new Set();
|
|
73
|
+
const resourceUrlMap = new Map();
|
|
74
|
+
|
|
75
|
+
const normalizeUrl = (input) => {
|
|
76
|
+
try { return new URL(input, baseUrl).toString(); } catch { return input; }
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let baseOrigin = "";
|
|
80
|
+
let baseDir = "";
|
|
81
|
+
try {
|
|
82
|
+
const parsedBase = new URL(baseUrl);
|
|
83
|
+
baseOrigin = parsedBase.origin;
|
|
84
|
+
baseDir = new URL(".", parsedBase).toString().replace(/\\/$/, "");
|
|
85
|
+
} catch {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const expandUrlVariants = (value) => {
|
|
89
|
+
const variants = [];
|
|
90
|
+
if (typeof value === "string") {
|
|
91
|
+
variants.push(value);
|
|
92
|
+
variants.push(normalizeUrl(value));
|
|
93
|
+
if (baseOrigin && value.startsWith("/")) {
|
|
94
|
+
variants.push(baseOrigin + value);
|
|
95
|
+
if (baseDir) variants.push(baseDir + value);
|
|
96
|
+
} else if (baseDir) {
|
|
97
|
+
variants.push(baseDir + (value.startsWith("/") ? value : "/" + value));
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(value, baseUrl);
|
|
101
|
+
const pathWithSearch = (parsed.pathname || "") + (parsed.search || "");
|
|
102
|
+
if (baseOrigin && parsed.origin !== baseOrigin) {
|
|
103
|
+
variants.push(baseOrigin + pathWithSearch);
|
|
104
|
+
if (baseDir) {
|
|
105
|
+
const path = pathWithSearch.startsWith("/") ? pathWithSearch : "/" + pathWithSearch;
|
|
106
|
+
variants.push(baseDir + path);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
return Array.from(new Set(variants.filter(Boolean)));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const normalizeBody = (body) => {
|
|
115
|
+
if (body === undefined || body === null) return "";
|
|
116
|
+
if (typeof body === "string") return body;
|
|
117
|
+
try { return String(body); } catch { return ""; }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Build a stable key so requests with identical method/url/body match the same response.
|
|
121
|
+
const makeKey = (method, url, body) => method.toUpperCase() + " " + normalizeUrl(url) + " " + normalizeBody(body);
|
|
122
|
+
const makeVariantKeys = (method, url, body) => {
|
|
123
|
+
return expandUrlVariants(url).map((variant) => makeKey(method, variant, body));
|
|
124
|
+
};
|
|
125
|
+
const normalizeNetworkRecord = (record) => {
|
|
126
|
+
if (!record || typeof record !== "object") {
|
|
127
|
+
return record;
|
|
128
|
+
}
|
|
129
|
+
if (record.response && record.response.body !== undefined) {
|
|
130
|
+
const response = record.response || {};
|
|
131
|
+
const encoding = response.bodyEncoding || "text";
|
|
132
|
+
return {
|
|
133
|
+
url: record.url,
|
|
134
|
+
method: record.method || "GET",
|
|
135
|
+
requestBody: record.requestBody || "",
|
|
136
|
+
status: response.status,
|
|
137
|
+
statusText: response.statusText,
|
|
138
|
+
responseHeaders: response.headers,
|
|
139
|
+
responseBody: encoding === "text" ? response.body : undefined,
|
|
140
|
+
responseBodyBase64: encoding === "base64" ? response.body : undefined,
|
|
141
|
+
responseEncoding: encoding,
|
|
142
|
+
error: record.error,
|
|
143
|
+
timestamp: record.timestamp
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return record;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const primeLookups = (snapshot) => {
|
|
150
|
+
records = snapshot.fetchXhrRecords || [];
|
|
151
|
+
networkRecords = (snapshot.networkRecords || []).map(normalizeNetworkRecord);
|
|
152
|
+
byKey.clear();
|
|
153
|
+
localResourceSet.clear();
|
|
154
|
+
resourceUrlMap.clear();
|
|
155
|
+
|
|
156
|
+
for (const record of records) {
|
|
157
|
+
if (!record || !record.url || !record.method) continue;
|
|
158
|
+
const keys = makeVariantKeys(record.method, record.url, record.requestBody || "");
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
if (!byKey.has(key)) {
|
|
161
|
+
byKey.set(key, record);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const record of networkRecords) {
|
|
167
|
+
if (!record || !record.url || !record.method) continue;
|
|
168
|
+
const keys = makeVariantKeys(record.method, record.url, record.requestBody || "");
|
|
169
|
+
for (const key of keys) {
|
|
170
|
+
if (!byKey.has(key)) {
|
|
171
|
+
byKey.set(key, record);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Track local resource files and map original URLs to local paths.
|
|
177
|
+
const resourceList = snapshot.resources || [];
|
|
178
|
+
for (const item of resourceList) {
|
|
179
|
+
if (!item || !item.localPath) continue;
|
|
180
|
+
localResourceSet.add(item.localPath);
|
|
181
|
+
localResourceSet.add("./" + item.localPath);
|
|
182
|
+
localResourceSet.add("/" + item.localPath);
|
|
183
|
+
|
|
184
|
+
if (item.url) {
|
|
185
|
+
const variants = expandUrlVariants(item.url);
|
|
186
|
+
for (const variant of variants) {
|
|
187
|
+
resourceUrlMap.set(variant, item.localPath);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
const ready = (async () => {
|
|
195
|
+
// Deserialize the snapshot and prepare lookup tables for offline responses.
|
|
196
|
+
const snapshot = (await loadSnapshot()) || {};
|
|
197
|
+
primeLookups(snapshot);
|
|
198
|
+
return snapshot;
|
|
199
|
+
})();
|
|
200
|
+
|
|
201
|
+
const isLocalResource = (value) => {
|
|
202
|
+
if (!value) return false;
|
|
203
|
+
if (value.startsWith("data:") || value.startsWith("blob:")) return true;
|
|
204
|
+
return localResourceSet.has(value);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Lookup helpers for request records and local assets.
|
|
208
|
+
const findRecord = (method, url, body) => {
|
|
209
|
+
const variants = expandUrlVariants(url);
|
|
210
|
+
for (const variant of variants) {
|
|
211
|
+
const key = makeKey(method, variant, body);
|
|
212
|
+
if (byKey.has(key)) return byKey.get(key);
|
|
213
|
+
}
|
|
214
|
+
for (const variant of variants) {
|
|
215
|
+
const fallbackKey = makeKey(method, variant, "");
|
|
216
|
+
if (byKey.has(fallbackKey)) return byKey.get(fallbackKey);
|
|
217
|
+
}
|
|
218
|
+
for (const variant of variants) {
|
|
219
|
+
const getKey = makeKey("GET", variant, "");
|
|
220
|
+
if (byKey.has(getKey)) return byKey.get(getKey);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const findByUrl = (url) => {
|
|
226
|
+
if (isLocalResource(url)) return null;
|
|
227
|
+
const variants = expandUrlVariants(url);
|
|
228
|
+
for (const variant of variants) {
|
|
229
|
+
const direct = byKey.get(makeKey("GET", variant, ""));
|
|
230
|
+
if (direct) return direct;
|
|
231
|
+
}
|
|
232
|
+
// Attempt a looser match: ignore querystring if needed.
|
|
233
|
+
for (const variant of variants) {
|
|
234
|
+
try {
|
|
235
|
+
const withoutQuery = new URL(variant).origin + new URL(variant).pathname;
|
|
236
|
+
const direct = byKey.get(makeKey("GET", withoutQuery, ""));
|
|
237
|
+
if (direct) return direct;
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const findLocalPath = (url) => {
|
|
244
|
+
if (!url) return null;
|
|
245
|
+
const variants = expandUrlVariants(url);
|
|
246
|
+
for (const variant of variants) {
|
|
247
|
+
const hit = resourceUrlMap.get(variant);
|
|
248
|
+
if (hit) return hit;
|
|
249
|
+
}
|
|
250
|
+
for (const variant of variants) {
|
|
251
|
+
try {
|
|
252
|
+
const withoutQuery = new URL(variant).origin + new URL(variant).pathname;
|
|
253
|
+
const hit = resourceUrlMap.get(withoutQuery);
|
|
254
|
+
if (hit) return hit;
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
// If still not found, fallback to data URLs if present.
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Safe property injection for emulating XHR state transitions.
|
|
262
|
+
const defineProp = (obj, key, value) => {
|
|
263
|
+
try {
|
|
264
|
+
Object.defineProperty(obj, key, { value, configurable: true });
|
|
265
|
+
} catch {}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Base64 helpers for binary payloads.
|
|
269
|
+
const decodeBase64 = (input) => {
|
|
270
|
+
try {
|
|
271
|
+
const binary = atob(input || "");
|
|
272
|
+
const bytes = new Uint8Array(binary.length);
|
|
273
|
+
|
|
274
|
+
Array.from(binary).forEach((char, index) => {
|
|
275
|
+
bytes[index] = char.charCodeAt(0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return bytes;
|
|
279
|
+
} catch {
|
|
280
|
+
return new Uint8Array();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const bytesToBase64 = (bytes) => {
|
|
285
|
+
const binary = Array.from(bytes, (value) => String.fromCharCode(value)).join("");
|
|
286
|
+
return btoa(binary);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const textToBase64 = (text) => {
|
|
290
|
+
try {
|
|
291
|
+
const bytes = new TextEncoder().encode(text || "");
|
|
292
|
+
return bytesToBase64(bytes);
|
|
293
|
+
} catch {
|
|
294
|
+
return btoa(text || "");
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Resolve a content type from recorded response headers.
|
|
299
|
+
const getContentType = (record) => {
|
|
300
|
+
const headers = record.responseHeaders || {};
|
|
301
|
+
for (const key in headers) {
|
|
302
|
+
if (key.toLowerCase() === "content-type") {
|
|
303
|
+
return headers[key] || "application/octet-stream";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return "application/octet-stream";
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Turn a recorded response into a data URL for inline usage.
|
|
310
|
+
const toDataUrl = (record, fallbackType) => {
|
|
311
|
+
if (!record) return "";
|
|
312
|
+
const contentType = getContentType(record) || fallbackType || "application/octet-stream";
|
|
313
|
+
if (record.responseEncoding === "base64" && record.responseBodyBase64) {
|
|
314
|
+
return "data:" + contentType + ";base64," + record.responseBodyBase64;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (record.responseBody) {
|
|
318
|
+
return "data:" + contentType + ";base64," + textToBase64(record.responseBody);
|
|
319
|
+
}
|
|
320
|
+
return "data:" + (fallbackType || "application/octet-stream") + ",";
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Build a real Response object from the recorded payload.
|
|
324
|
+
const responseFromRecord = (record) => {
|
|
325
|
+
const headers = new Headers(record.responseHeaders || {});
|
|
326
|
+
if (record.responseEncoding === "base64" && record.responseBodyBase64) {
|
|
327
|
+
const bytes = decodeBase64(record.responseBodyBase64);
|
|
328
|
+
return new Response(bytes, {
|
|
329
|
+
status: record.status || 200,
|
|
330
|
+
statusText: record.statusText || "OK",
|
|
331
|
+
headers
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const bodyText = record.responseBody || "";
|
|
335
|
+
return new Response(bodyText, {
|
|
336
|
+
status: record.status || 200,
|
|
337
|
+
statusText: record.statusText || "OK",
|
|
338
|
+
headers
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
${hackerScripts}
|
|
343
|
+
})();
|
|
344
|
+
</script>
|
|
345
|
+
`;
|
|
346
|
+
};
|
|
347
|
+
exports.buildReplayScript = buildReplayScript;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
export type ResourceReference = {
|
|
3
|
+
attr: string;
|
|
4
|
+
element: any;
|
|
5
|
+
url: string;
|
|
6
|
+
};
|
|
7
|
+
export type SrcsetReference = {
|
|
8
|
+
element: any;
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const toAbsoluteUrl: (baseUrl: string, resourceUrl: string) => string;
|
|
12
|
+
export declare const extractResourceUrls: (html: string, baseUrl: string) => {
|
|
13
|
+
$: cheerio.CheerioAPI;
|
|
14
|
+
resourceUrls: ResourceReference[];
|
|
15
|
+
srcsetItems: SrcsetReference[];
|
|
16
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extractResourceUrls = exports.toAbsoluteUrl = void 0;
|
|
37
|
+
const cheerio = __importStar(require("cheerio"));
|
|
38
|
+
const toAbsoluteUrl = (baseUrl, resourceUrl) => {
|
|
39
|
+
try {
|
|
40
|
+
return new URL(resourceUrl, baseUrl).toString();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return resourceUrl;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.toAbsoluteUrl = toAbsoluteUrl;
|
|
47
|
+
const extractResourceUrls = (html, baseUrl) => {
|
|
48
|
+
const $ = cheerio.load(html);
|
|
49
|
+
const urls = [];
|
|
50
|
+
const collect = (selector, attr) => {
|
|
51
|
+
$(selector).each((_, element) => {
|
|
52
|
+
const value = $(element).attr(attr);
|
|
53
|
+
if (value) {
|
|
54
|
+
urls.push({ attr, element });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
collect("script[src]", "src");
|
|
59
|
+
collect("link[rel=stylesheet][href]", "href");
|
|
60
|
+
collect("link[rel=icon][href]", "href");
|
|
61
|
+
collect("img[src]", "src");
|
|
62
|
+
collect("source[src]", "src");
|
|
63
|
+
collect("video[src]", "src");
|
|
64
|
+
collect("audio[src]", "src");
|
|
65
|
+
const srcsetItems = [];
|
|
66
|
+
$("img[srcset], source[srcset]").each((_, element) => {
|
|
67
|
+
const value = $(element).attr("srcset");
|
|
68
|
+
if (value) {
|
|
69
|
+
srcsetItems.push({ element, value });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const resourceUrls = urls.map(({ attr, element }) => {
|
|
73
|
+
const value = $(element).attr(attr) || "";
|
|
74
|
+
return {
|
|
75
|
+
attr,
|
|
76
|
+
element,
|
|
77
|
+
url: (0, exports.toAbsoluteUrl)(baseUrl, value)
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
return { $, resourceUrls, srcsetItems };
|
|
81
|
+
};
|
|
82
|
+
exports.extractResourceUrls = extractResourceUrls;
|