@pagepocket/lib 0.12.0 → 0.14.5
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/core/pagepocket.d.ts +17 -0
- package/dist/core/pagepocket.js +25 -1
- package/dist/hackers/index.js +3 -1
- package/dist/hackers/replay-dom-rewrite/script-part-1.d.ts +1 -1
- package/dist/hackers/replay-dom-rewrite/script-part-1.js +40 -51
- package/dist/hackers/replay-dom-rewrite/script-part-2.d.ts +1 -1
- package/dist/hackers/replay-dom-rewrite/script-part-2.js +74 -44
- package/dist/hackers/replay-fetch.js +4 -0
- package/dist/hackers/replay-websocket.js +50 -8
- package/dist/hackers/replay-worker.d.ts +18 -0
- package/dist/hackers/replay-worker.js +242 -0
- package/dist/index.d.ts +1 -0
- package/dist/replay/match-api.js +103 -3
- package/dist/replay/templates/loader-template.d.ts +15 -0
- package/dist/replay/templates/loader-template.js +164 -0
- package/dist/replay/templates/match-api-source.d.ts +1 -1
- package/dist/replay/templates/match-api-source.js +86 -4
- package/dist/replay/templates/replay-script-template.part-2.js +24 -1
- package/dist/resource-filter.js +29 -3
- package/dist/snapshot-builder/build-snapshot.js +33 -3
- package/dist/units/apply-replace-elements-to-file-tree.d.ts +7 -0
- package/dist/units/apply-replace-elements-to-file-tree.js +63 -0
- package/dist/units/contracts-bridge.d.ts +9 -1
- package/dist/units/file-tree-unit.d.ts +28 -0
- package/dist/units/file-tree-unit.js +53 -0
- package/dist/units/index.d.ts +3 -0
- package/dist/units/index.js +2 -0
- package/dist/units/internal/runtime.d.ts +5 -2
- package/dist/units/internal/runtime.js +14 -0
- package/dist/units/runner.d.ts +3 -1
- package/dist/units/runner.js +72 -2
- package/dist/units/snapshot-unit.d.ts +31 -0
- package/dist/units/snapshot-unit.js +58 -0
- package/package.json +4 -4
|
@@ -13,12 +13,27 @@ export type CaptureTarget = {
|
|
|
13
13
|
baseUrl: string;
|
|
14
14
|
url?: string;
|
|
15
15
|
};
|
|
16
|
+
import type { ProgressEvent } from "@pagepocket/contracts";
|
|
16
17
|
import type { PagePocketOptions } from "../types.js";
|
|
17
18
|
import type { CaptureResult as PagePocketCaptureResult, Plugin as V3Plugin, Unit as V3Unit } from "../units/contracts-bridge.js";
|
|
18
19
|
import type { CaptureOptions } from "../units/types.js";
|
|
20
|
+
export type CaptureEventMap = {
|
|
21
|
+
"unit:start": Extract<ProgressEvent, {
|
|
22
|
+
type: "unit:start";
|
|
23
|
+
}>;
|
|
24
|
+
"unit:end": Extract<ProgressEvent, {
|
|
25
|
+
type: "unit:end";
|
|
26
|
+
}>;
|
|
27
|
+
"unit:log": Extract<ProgressEvent, {
|
|
28
|
+
type: "unit:log";
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
type CaptureEventName = keyof CaptureEventMap;
|
|
32
|
+
type CaptureEventListener<K extends CaptureEventName> = (event: CaptureEventMap[K]) => void;
|
|
19
33
|
export declare class PagePocket {
|
|
20
34
|
private target;
|
|
21
35
|
private options;
|
|
36
|
+
private listeners;
|
|
22
37
|
private constructor();
|
|
23
38
|
static fromURL(url: string, options?: PagePocketOptions): PagePocket;
|
|
24
39
|
static fromPuppeteerPage(page: unknown, options?: PagePocketOptions): PagePocket;
|
|
@@ -32,8 +47,10 @@ export declare class PagePocket {
|
|
|
32
47
|
url?: string;
|
|
33
48
|
serialize?: (doc: unknown) => string;
|
|
34
49
|
} & PagePocketOptions): PagePocket;
|
|
50
|
+
on<K extends CaptureEventName>(event: K, listener: CaptureEventListener<K>): this;
|
|
35
51
|
capture(options: {
|
|
36
52
|
units: V3Unit[];
|
|
37
53
|
plugins?: V3Plugin[];
|
|
38
54
|
} & CaptureOptions): Promise<PagePocketCaptureResult>;
|
|
39
55
|
}
|
|
56
|
+
export {};
|
package/dist/core/pagepocket.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runCapture } from "../units/index.js";
|
|
2
2
|
export class PagePocket {
|
|
3
3
|
constructor(target, options) {
|
|
4
|
+
this.listeners = new Map();
|
|
4
5
|
this.target = target;
|
|
5
6
|
this.options = options ?? {};
|
|
6
7
|
}
|
|
@@ -32,6 +33,16 @@ export class PagePocket {
|
|
|
32
33
|
const { baseUrl, url, serialize: _serialize, ...rest } = options;
|
|
33
34
|
return new PagePocket({ kind: "html", htmlString, baseUrl, ...(url ? { url } : {}) }, rest);
|
|
34
35
|
}
|
|
36
|
+
on(event, listener) {
|
|
37
|
+
const existing = this.listeners.get(event);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.push(listener);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.listeners.set(event, [listener]);
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
35
46
|
async capture(options) {
|
|
36
47
|
const entry = this.target.kind === "url"
|
|
37
48
|
? { kind: "url", url: this.target.url }
|
|
@@ -45,12 +56,25 @@ export class PagePocket {
|
|
|
45
56
|
htmlString: this.target.htmlString,
|
|
46
57
|
...(this.target.url ? { url: this.target.url } : {})
|
|
47
58
|
};
|
|
59
|
+
const hasListeners = this.listeners.size > 0;
|
|
60
|
+
const onProgress = hasListeners
|
|
61
|
+
? (event) => {
|
|
62
|
+
const eventListeners = this.listeners.get(event.type);
|
|
63
|
+
if (!eventListeners) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const listener of eventListeners) {
|
|
67
|
+
listener(event);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
: undefined;
|
|
48
71
|
const result = await runCapture({
|
|
49
72
|
entry,
|
|
50
73
|
pocketOptions: this.options,
|
|
51
74
|
options,
|
|
52
75
|
units: options.units,
|
|
53
|
-
plugins: options.plugins
|
|
76
|
+
plugins: options.plugins,
|
|
77
|
+
onProgress
|
|
54
78
|
});
|
|
55
79
|
return result;
|
|
56
80
|
}
|
package/dist/hackers/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { replayFetchResponder } from "./replay-fetch.js";
|
|
|
9
9
|
import { replayHistoryPath } from "./replay-history-path.js";
|
|
10
10
|
import { replaySvgImageRewriter } from "./replay-svg-image.js";
|
|
11
11
|
import { replayWebSocketStub } from "./replay-websocket.js";
|
|
12
|
+
import { replayWorkerStub } from "./replay-worker.js";
|
|
12
13
|
import { replayXhrResponder } from "./replay-xhr.js";
|
|
13
14
|
export const preloadHackers = [preloadFetchRecorder, preloadXhrRecorder];
|
|
14
15
|
export const replayHackers = [
|
|
@@ -21,5 +22,6 @@ export const replayHackers = [
|
|
|
21
22
|
replaySvgImageRewriter,
|
|
22
23
|
replayBeaconStub,
|
|
23
24
|
replayWebSocketStub,
|
|
24
|
-
replayEventSourceStub
|
|
25
|
+
replayEventSourceStub,
|
|
26
|
+
replayWorkerStub
|
|
25
27
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const replayDomRewriteScriptPart1 = "\n const transparentGif = \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==\";\n const emptyScript = \"data:text/javascript,/*pagepocket-missing*/\";\n const emptyStyle = \"data:text/css,/*pagepocket-missing*/\";\n\n let readyResolved = false;\n if (ready && typeof ready.then === \"function\") {\n ready.then(() => {\n readyResolved = true;\n });\n } else {\n readyResolved = true;\n }\n\n const onReady = (callback) => {\n if (readyResolved) {\n callback();\n return;\n }\n if (ready && typeof ready.then === \"function\") {\n ready.then(callback);\n } else {\n callback();\n }\n };\n\n const rewritten = new WeakMap();\n\n const shouldRewriteAttr = (element, attr) => {\n try {\n const tag = String((element && element.tagName) || \"\").toLowerCase();\n const name = String(attr || \"\").toLowerCase();\n\n if (!tag || !name) {\n return false;\n }\n\n if (tag === \"a\" && name === \"href\") {\n return false;\n }\n\n if (name === \"src\") {\n return (\n tag === \"img\" ||\n tag === \"source\" ||\n tag === \"video\" ||\n tag === \"audio\" ||\n tag === \"script\" ||\n tag === \"iframe\" ||\n tag === \"object\" ||\n tag === \"embed\" ||\n tag === \"track\"\n );\n }\n\n if (name === \"href\") {\n
|
|
1
|
+
export declare const replayDomRewriteScriptPart1 = "\n const transparentGif = \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==\";\n const emptyScript = \"data:text/javascript,/*pagepocket-missing*/\";\n const emptyStyle = \"data:text/css,/*pagepocket-missing*/\";\n\n let readyResolved = false;\n if (ready && typeof ready.then === \"function\") {\n ready.then(() => {\n readyResolved = true;\n });\n } else {\n readyResolved = true;\n }\n\n const onReady = (callback) => {\n if (readyResolved) {\n callback();\n return;\n }\n if (ready && typeof ready.then === \"function\") {\n ready.then(callback);\n } else {\n callback();\n }\n };\n\n const rewritten = new WeakMap();\n\n const shouldRewriteAttr = (element, attr) => {\n try {\n const tag = String((element && element.tagName) || \"\").toLowerCase();\n const name = String(attr || \"\").toLowerCase();\n\n if (!tag || !name) {\n return false;\n }\n\n if (tag === \"a\" && name === \"href\") {\n return false;\n }\n\n if (name === \"src\") {\n return (\n tag === \"img\" ||\n tag === \"source\" ||\n tag === \"video\" ||\n tag === \"audio\" ||\n tag === \"script\" ||\n tag === \"iframe\" ||\n tag === \"object\" ||\n tag === \"embed\" ||\n tag === \"track\"\n );\n }\n\n if (name === \"href\") {\n return tag === \"link\";\n }\n\n if (name === \"srcset\") {\n return tag === \"img\" || tag === \"source\";\n }\n\n return false;\n } catch {\n return false;\n }\n };\n\n const rewriteSrcset = (value) => {\n if (!value) return value;\n\n try {\n const trimmed = String(value || \"\").trim();\n const hasFetchTransform = trimmed.includes(\"/image/fetch/\");\n const hasEncodedUrlTail = trimmed.includes(\"https%3A%2F%2F\");\n const hasCommaTokens =\n trimmed.includes(\",w_\") ||\n trimmed.includes(\", w_\") ||\n trimmed.includes(\",h_\") ||\n trimmed.includes(\", h_\") ||\n trimmed.includes(\",c_\") ||\n trimmed.includes(\", c_\");\n\n if (hasFetchTransform && hasEncodedUrlTail && hasCommaTokens) {\n return \"\";\n }\n } catch {}\n\n return value\n .split(\",\")\n .map((part) => {\n const trimmed = part.trim();\n if (!trimmed) return trimmed;\n const pieces = trimmed.split(/\\s+/, 2);\n const url = pieces[0];\n const descriptor = pieces[1];\n if (isLocalResource(url)) return trimmed;\n const localPath = findLocalPath(url);\n if (localPath) {\n return descriptor ? localPath + \" \" + descriptor : localPath;\n }\n return trimmed;\n })\n .join(\",\");\n };\n\n const applyRewriteToElement = (element) => {\n if (!element || !element.getAttribute) return;\n const currentSrc = element.getAttribute(\"src\");\n const currentHref = element.getAttribute(\"href\");\n const currentSrcset = element.getAttribute(\"srcset\");\n const tag = (element.tagName || \"\").toLowerCase();\n\n if (\n tag === \"img\" ||\n tag === \"source\" ||\n tag === \"video\" ||\n tag === \"audio\" ||\n tag === \"script\" ||\n tag === \"iframe\" ||\n tag === \"object\" ||\n tag === \"embed\" ||\n tag === \"track\"\n ) {\n const src = currentSrc;\n if (src && !src.startsWith(\"data:\") && !src.startsWith(\"blob:\")) {\n const next = rewriteResourceUrl(src, { kind: \"attr\", tag, attr: \"src\" });\n if (next && next !== src) {\n element.setAttribute(\"src\", next);\n return;\n }\n }\n }\n\n if (tag === \"link\") {\n const href = currentHref;\n if (href && !href.startsWith(\"data:\") && !href.startsWith(\"blob:\")) {\n const rel = (element.getAttribute(\"rel\") || \"\").toLowerCase();\n const as = (element.getAttribute(\"as\") || \"\").toLowerCase();\n const fallbackType =\n rel.includes(\"stylesheet\") || (rel.includes(\"preload\") && as === \"style\")\n ? \"text/css\"\n : undefined;\n const next = rewriteResourceUrl(href, {\n kind: \"attr\",\n tag,\n attr: \"href\",\n fallbackType\n });\n if (next && next !== href) {\n element.setAttribute(\"href\", next);\n return;\n }\n }\n }\n\n const srcset = currentSrcset;\n if (srcset) {\n element.setAttribute(\"srcset\", rewriteSrcset(srcset));\n }\n\n rewritten.set(element, {\n src: element.getAttribute(\"src\"),\n href: element.getAttribute(\"href\"),\n srcset: element.getAttribute(\"srcset\")\n });\n };\n\n const rewriteElement = (element) => {\n if (!element || !element.getAttribute) return;\n if (!readyResolved) {\n onReady(() => rewriteElement(element));\n return;\n }\n const prev = rewritten.get(element);\n const currentSrc = element.getAttribute(\"src\");\n const currentHref = element.getAttribute(\"href\");\n const currentSrcset = element.getAttribute(\"srcset\");\n if (\n prev &&\n prev.src === currentSrc &&\n prev.href === currentHref &&\n prev.srcset === currentSrcset\n ) {\n return;\n }\n\n applyRewriteToElement(element);\n };\n";
|
|
@@ -54,20 +54,7 @@ export const replayDomRewriteScriptPart1 = `
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (name === "href") {
|
|
57
|
-
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const rel = String((element.getAttribute && element.getAttribute("rel")) || "").toLowerCase();
|
|
62
|
-
if (rel.includes("stylesheet") || rel.includes("icon")) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (rel.includes("preload") || rel.includes("prefetch") || rel.includes("modulepreload")) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return false;
|
|
57
|
+
return tag === "link";
|
|
71
58
|
}
|
|
72
59
|
|
|
73
60
|
if (name === "srcset") {
|
|
@@ -118,25 +105,13 @@ export const replayDomRewriteScriptPart1 = `
|
|
|
118
105
|
.join(",");
|
|
119
106
|
};
|
|
120
107
|
|
|
121
|
-
const
|
|
108
|
+
const applyRewriteToElement = (element) => {
|
|
122
109
|
if (!element || !element.getAttribute) return;
|
|
123
|
-
if (!readyResolved) {
|
|
124
|
-
onReady(() => rewriteElement(element));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const prev = rewritten.get(element);
|
|
128
110
|
const currentSrc = element.getAttribute("src");
|
|
129
111
|
const currentHref = element.getAttribute("href");
|
|
130
112
|
const currentSrcset = element.getAttribute("srcset");
|
|
131
|
-
if (
|
|
132
|
-
prev &&
|
|
133
|
-
prev.src === currentSrc &&
|
|
134
|
-
prev.href === currentHref &&
|
|
135
|
-
prev.srcset === currentSrcset
|
|
136
|
-
) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
113
|
const tag = (element.tagName || "").toLowerCase();
|
|
114
|
+
|
|
140
115
|
if (
|
|
141
116
|
tag === "img" ||
|
|
142
117
|
tag === "source" ||
|
|
@@ -160,30 +135,22 @@ export const replayDomRewriteScriptPart1 = `
|
|
|
160
135
|
|
|
161
136
|
if (tag === "link") {
|
|
162
137
|
const href = currentHref;
|
|
163
|
-
const rel = (element.getAttribute("rel") || "").toLowerCase();
|
|
164
138
|
if (href && !href.startsWith("data:") && !href.startsWith("blob:")) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
rel.includes("preload")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
attr: "href",
|
|
181
|
-
fallbackType
|
|
182
|
-
});
|
|
183
|
-
if (next && next !== href) {
|
|
184
|
-
element.setAttribute("href", next);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
139
|
+
const rel = (element.getAttribute("rel") || "").toLowerCase();
|
|
140
|
+
const as = (element.getAttribute("as") || "").toLowerCase();
|
|
141
|
+
const fallbackType =
|
|
142
|
+
rel.includes("stylesheet") || (rel.includes("preload") && as === "style")
|
|
143
|
+
? "text/css"
|
|
144
|
+
: undefined;
|
|
145
|
+
const next = rewriteResourceUrl(href, {
|
|
146
|
+
kind: "attr",
|
|
147
|
+
tag,
|
|
148
|
+
attr: "href",
|
|
149
|
+
fallbackType
|
|
150
|
+
});
|
|
151
|
+
if (next && next !== href) {
|
|
152
|
+
element.setAttribute("href", next);
|
|
153
|
+
return;
|
|
187
154
|
}
|
|
188
155
|
}
|
|
189
156
|
}
|
|
@@ -199,4 +166,26 @@ export const replayDomRewriteScriptPart1 = `
|
|
|
199
166
|
srcset: element.getAttribute("srcset")
|
|
200
167
|
});
|
|
201
168
|
};
|
|
169
|
+
|
|
170
|
+
const rewriteElement = (element) => {
|
|
171
|
+
if (!element || !element.getAttribute) return;
|
|
172
|
+
if (!readyResolved) {
|
|
173
|
+
onReady(() => rewriteElement(element));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const prev = rewritten.get(element);
|
|
177
|
+
const currentSrc = element.getAttribute("src");
|
|
178
|
+
const currentHref = element.getAttribute("href");
|
|
179
|
+
const currentSrcset = element.getAttribute("srcset");
|
|
180
|
+
if (
|
|
181
|
+
prev &&
|
|
182
|
+
prev.src === currentSrc &&
|
|
183
|
+
prev.href === currentHref &&
|
|
184
|
+
prev.srcset === currentSrcset
|
|
185
|
+
) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
applyRewriteToElement(element);
|
|
190
|
+
};
|
|
202
191
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const replayDomRewriteScriptPart2 = "\n const originalSetAttribute = Element.prototype.setAttribute;\n Element.prototype.setAttribute = function(name, value) {\n const attr = String(name).toLowerCase();\n if (attr === \"src\" || attr === \"href\" || attr === \"srcset\") {\n if (!readyResolved) {\n const pendingValue = String(value);\n onReady(() => originalSetAttribute.call(this, name,
|
|
1
|
+
export declare const replayDomRewriteScriptPart2 = "\n const rewriteAttrValue = (element, attr, rawValue) => {\n if (attr === \"srcset\") {\n return rewriteSrcset(rawValue);\n }\n\n if (!shouldRewriteAttr(element, attr)) {\n return rawValue;\n }\n\n const tag = (element.tagName || \"\").toLowerCase();\n const rel = (element.getAttribute && element.getAttribute(\"rel\")) || \"\";\n const relLower = rel.toLowerCase();\n const as = String((element.getAttribute && element.getAttribute(\"as\")) || \"\").toLowerCase();\n const fallbackType =\n attr === \"href\" && (relLower.includes(\"stylesheet\") || (relLower.includes(\"preload\") && as === \"style\"))\n ? \"text/css\"\n : undefined;\n\n return rewriteResourceUrl(rawValue, {\n kind: \"setAttribute\",\n tag,\n attr,\n fallbackType\n });\n };\n\n const originalSetAttribute = Element.prototype.setAttribute;\n Element.prototype.setAttribute = function(name, value) {\n const attr = String(name).toLowerCase();\n if (attr === \"src\" || attr === \"href\" || attr === \"srcset\") {\n if (!readyResolved) {\n const pendingValue = String(value);\n onReady(() => {\n const next = rewriteAttrValue(this, attr, pendingValue);\n originalSetAttribute.call(this, name, next);\n });\n return;\n }\n\n const next = rewriteAttrValue(this, attr, String(value));\n return originalSetAttribute.call(this, name, next);\n }\n return originalSetAttribute.call(this, name, value);\n };\n\n const rewriteHtmlChunk = (chunk) => {\n const html = String(chunk || \"\");\n if (!html) {\n return html;\n }\n\n try {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n\n template.content\n .querySelectorAll(\"img,source,video,audio,script,link,iframe,object,embed,track\")\n .forEach((element) => {\n applyRewriteToElement(element);\n });\n\n template.content.querySelectorAll(\"img[srcset],source[srcset]\").forEach((element) => {\n applyRewriteToElement(element);\n });\n\n return template.innerHTML;\n } catch {\n return html;\n }\n };\n\n const originalDocumentWrite = typeof document.write === \"function\" ? document.write.bind(document) : null;\n const originalDocumentWriteln =\n typeof document.writeln === \"function\" ? document.writeln.bind(document) : null;\n\n if (originalDocumentWrite) {\n document.write = function(...args) {\n const rewrittenArgs = args.map((value) => rewriteHtmlChunk(value));\n return originalDocumentWrite(...rewrittenArgs);\n };\n }\n\n if (originalDocumentWriteln) {\n document.writeln = function(...args) {\n const rewrittenArgs = args.map((value) => rewriteHtmlChunk(value));\n return originalDocumentWriteln(...rewrittenArgs);\n };\n }\n\n const patchProperty = (proto, prop, handler) => {\n try {\n const desc = Object.getOwnPropertyDescriptor(proto, prop);\n if (!desc || !desc.set) return;\n Object.defineProperty(proto, prop, {\n configurable: true,\n get: desc.get,\n set: function(value) {\n return handler.call(this, value, desc.set);\n }\n });\n } catch {}\n };\n\n patchProperty(HTMLImageElement.prototype, \"src\", function(value, setter) {\n const rawValue = String(value);\n if (!readyResolved) {\n onReady(() => {\n const next = rewriteResourceUrl(rawValue, { kind: \"setter\", tag: \"img\", attr: \"src\" });\n setter.call(this, next);\n });\n return;\n }\n const next = rewriteResourceUrl(rawValue, { kind: \"setter\", tag: \"img\", attr: \"src\" });\n setter.call(this, next);\n });\n\n patchProperty(HTMLScriptElement.prototype, \"src\", function(value, setter) {\n const rawValue = String(value);\n if (!readyResolved) {\n onReady(() => {\n const next = rewriteResourceUrl(rawValue, { kind: \"setter\", tag: \"script\", attr: \"src\" });\n setter.call(this, next);\n });\n return;\n }\n const next = rewriteResourceUrl(rawValue, { kind: \"setter\", tag: \"script\", attr: \"src\" });\n setter.call(this, next);\n });\n\n patchProperty(HTMLLinkElement.prototype, \"href\", function(value, setter) {\n const rawValue = String(value);\n const rel = (this.getAttribute && this.getAttribute(\"rel\")) || \"\";\n const relLower = rel.toLowerCase();\n const as = String((this.getAttribute && this.getAttribute(\"as\")) || \"\").toLowerCase();\n if (!readyResolved) {\n onReady(() => {\n const fallbackType =\n relLower.includes(\"stylesheet\") || (relLower.includes(\"preload\") && as === \"style\")\n ? \"text/css\"\n : undefined;\n const next = rewriteResourceUrl(rawValue, {\n kind: \"setter\",\n tag: \"link\",\n attr: \"href\",\n fallbackType\n });\n setter.call(this, next);\n });\n return;\n }\n const fallbackType =\n relLower.includes(\"stylesheet\") || (relLower.includes(\"preload\") && as === \"style\")\n ? \"text/css\"\n : undefined;\n const next = rewriteResourceUrl(rawValue, {\n kind: \"setter\",\n tag: \"link\",\n attr: \"href\",\n fallbackType\n });\n setter.call(this, next);\n });\n\n patchProperty(HTMLImageElement.prototype, \"srcset\", function(value, setter) {\n const rawValue = String(value);\n if (!readyResolved) {\n onReady(() => {\n const next = rewriteSrcset(rawValue);\n setter.call(this, next);\n });\n return;\n }\n const next = rewriteSrcset(rawValue);\n setter.call(this, next);\n });\n\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\" && mutation.target) {\n rewriteElement(mutation.target);\n }\n if (mutation.type === \"childList\") {\n mutation.addedNodes.forEach((node) => {\n if (node && node.nodeType === 1) {\n rewriteElement(node);\n }\n });\n }\n }\n });\n\n observer.observe(document.documentElement, {\n attributes: true,\n childList: true,\n subtree: true,\n attributeFilter: [\"src\", \"href\", \"srcset\", \"rel\", \"as\"]\n });\n\n document\n .querySelectorAll(\"img,source,video,audio,script,link,iframe\")\n .forEach((el) => rewriteElement(el));\n";
|
|
@@ -1,43 +1,93 @@
|
|
|
1
1
|
export const replayDomRewriteScriptPart2 = `
|
|
2
|
+
const rewriteAttrValue = (element, attr, rawValue) => {
|
|
3
|
+
if (attr === "srcset") {
|
|
4
|
+
return rewriteSrcset(rawValue);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!shouldRewriteAttr(element, attr)) {
|
|
8
|
+
return rawValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const tag = (element.tagName || "").toLowerCase();
|
|
12
|
+
const rel = (element.getAttribute && element.getAttribute("rel")) || "";
|
|
13
|
+
const relLower = rel.toLowerCase();
|
|
14
|
+
const as = String((element.getAttribute && element.getAttribute("as")) || "").toLowerCase();
|
|
15
|
+
const fallbackType =
|
|
16
|
+
attr === "href" && (relLower.includes("stylesheet") || (relLower.includes("preload") && as === "style"))
|
|
17
|
+
? "text/css"
|
|
18
|
+
: undefined;
|
|
19
|
+
|
|
20
|
+
return rewriteResourceUrl(rawValue, {
|
|
21
|
+
kind: "setAttribute",
|
|
22
|
+
tag,
|
|
23
|
+
attr,
|
|
24
|
+
fallbackType
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
2
28
|
const originalSetAttribute = Element.prototype.setAttribute;
|
|
3
29
|
Element.prototype.setAttribute = function(name, value) {
|
|
4
30
|
const attr = String(name).toLowerCase();
|
|
5
31
|
if (attr === "src" || attr === "href" || attr === "srcset") {
|
|
6
32
|
if (!readyResolved) {
|
|
7
33
|
const pendingValue = String(value);
|
|
8
|
-
onReady(() =>
|
|
34
|
+
onReady(() => {
|
|
35
|
+
const next = rewriteAttrValue(this, attr, pendingValue);
|
|
36
|
+
originalSetAttribute.call(this, name, next);
|
|
37
|
+
});
|
|
9
38
|
return;
|
|
10
39
|
}
|
|
11
|
-
if (attr === "srcset") {
|
|
12
|
-
const rewritten = rewriteSrcset(String(value));
|
|
13
|
-
return originalSetAttribute.call(this, name, rewritten);
|
|
14
|
-
}
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
const next = rewriteAttrValue(this, attr, String(value));
|
|
42
|
+
return originalSetAttribute.call(this, name, next);
|
|
43
|
+
}
|
|
44
|
+
return originalSetAttribute.call(this, name, value);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const rewriteHtmlChunk = (chunk) => {
|
|
48
|
+
const html = String(chunk || "");
|
|
49
|
+
if (!html) {
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const template = document.createElement("template");
|
|
55
|
+
template.innerHTML = html;
|
|
56
|
+
|
|
57
|
+
template.content
|
|
58
|
+
.querySelectorAll("img,source,video,audio,script,link,iframe,object,embed,track")
|
|
59
|
+
.forEach((element) => {
|
|
60
|
+
applyRewriteToElement(element);
|
|
61
|
+
});
|
|
19
62
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const relLower = rel.toLowerCase();
|
|
23
|
-
const as = String((this.getAttribute && this.getAttribute("as")) || "").toLowerCase();
|
|
24
|
-
const fallbackType =
|
|
25
|
-
attr === "href" && (relLower.includes("stylesheet") || (relLower.includes("preload") && as === "style"))
|
|
26
|
-
? "text/css"
|
|
27
|
-
: undefined;
|
|
28
|
-
|
|
29
|
-
const next = rewriteResourceUrl(String(value), {
|
|
30
|
-
kind: "setAttribute",
|
|
31
|
-
tag,
|
|
32
|
-
attr,
|
|
33
|
-
fallbackType
|
|
63
|
+
template.content.querySelectorAll("img[srcset],source[srcset]").forEach((element) => {
|
|
64
|
+
applyRewriteToElement(element);
|
|
34
65
|
});
|
|
35
66
|
|
|
36
|
-
return
|
|
67
|
+
return template.innerHTML;
|
|
68
|
+
} catch {
|
|
69
|
+
return html;
|
|
37
70
|
}
|
|
38
|
-
return originalSetAttribute.call(this, name, value);
|
|
39
71
|
};
|
|
40
72
|
|
|
73
|
+
const originalDocumentWrite = typeof document.write === "function" ? document.write.bind(document) : null;
|
|
74
|
+
const originalDocumentWriteln =
|
|
75
|
+
typeof document.writeln === "function" ? document.writeln.bind(document) : null;
|
|
76
|
+
|
|
77
|
+
if (originalDocumentWrite) {
|
|
78
|
+
document.write = function(...args) {
|
|
79
|
+
const rewrittenArgs = args.map((value) => rewriteHtmlChunk(value));
|
|
80
|
+
return originalDocumentWrite(...rewrittenArgs);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (originalDocumentWriteln) {
|
|
85
|
+
document.writeln = function(...args) {
|
|
86
|
+
const rewrittenArgs = args.map((value) => rewriteHtmlChunk(value));
|
|
87
|
+
return originalDocumentWriteln(...rewrittenArgs);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
41
91
|
const patchProperty = (proto, prop, handler) => {
|
|
42
92
|
try {
|
|
43
93
|
const desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
@@ -85,16 +135,6 @@ export const replayDomRewriteScriptPart2 = `
|
|
|
85
135
|
const as = String((this.getAttribute && this.getAttribute("as")) || "").toLowerCase();
|
|
86
136
|
if (!readyResolved) {
|
|
87
137
|
onReady(() => {
|
|
88
|
-
if (
|
|
89
|
-
!relLower.includes("stylesheet") &&
|
|
90
|
-
!relLower.includes("icon") &&
|
|
91
|
-
!relLower.includes("preload") &&
|
|
92
|
-
!relLower.includes("prefetch") &&
|
|
93
|
-
!relLower.includes("modulepreload")
|
|
94
|
-
) {
|
|
95
|
-
setter.call(this, rawValue);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
138
|
const fallbackType =
|
|
99
139
|
relLower.includes("stylesheet") || (relLower.includes("preload") && as === "style")
|
|
100
140
|
? "text/css"
|
|
@@ -109,16 +149,6 @@ export const replayDomRewriteScriptPart2 = `
|
|
|
109
149
|
});
|
|
110
150
|
return;
|
|
111
151
|
}
|
|
112
|
-
if (
|
|
113
|
-
!relLower.includes("stylesheet") &&
|
|
114
|
-
!relLower.includes("icon") &&
|
|
115
|
-
!relLower.includes("preload") &&
|
|
116
|
-
!relLower.includes("prefetch") &&
|
|
117
|
-
!relLower.includes("modulepreload")
|
|
118
|
-
) {
|
|
119
|
-
setter.call(this, rawValue);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
152
|
const fallbackType =
|
|
123
153
|
relLower.includes("stylesheet") || (relLower.includes("preload") && as === "style")
|
|
124
154
|
? "text/css"
|
|
@@ -11,6 +11,10 @@ export const replayFetchResponder = {
|
|
|
11
11
|
await ready;
|
|
12
12
|
}
|
|
13
13
|
const url = typeof input === "string" ? input : input.url;
|
|
14
|
+
// Let blob: and data: URLs pass through to the original fetch.
|
|
15
|
+
if (url && (url.startsWith("blob:") || url.startsWith("data:"))) {
|
|
16
|
+
return originalFetch(input, init);
|
|
17
|
+
}
|
|
14
18
|
const method = (init && init.method) || (typeof input === "string" ? "GET" : input.method || "GET");
|
|
15
19
|
const body = init && init.body;
|
|
16
20
|
try {
|
|
@@ -4,19 +4,61 @@ export const replayWebSocketStub = {
|
|
|
4
4
|
build: () => `
|
|
5
5
|
// Stub WebSocket to prevent live network connections.
|
|
6
6
|
if (window.WebSocket) {
|
|
7
|
-
|
|
7
|
+
var OriginalWebSocket = window.WebSocket;
|
|
8
8
|
window.WebSocket = function(url, protocols) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
var listeners = {};
|
|
10
|
+
var socket = {
|
|
11
|
+
url: url,
|
|
12
|
+
protocol: typeof protocols === 'string' ? protocols : (Array.isArray(protocols) ? protocols[0] || '' : ''),
|
|
13
|
+
readyState: 0,
|
|
14
|
+
bufferedAmount: 0,
|
|
15
|
+
extensions: '',
|
|
16
|
+
binaryType: 'blob',
|
|
17
|
+
onopen: null,
|
|
18
|
+
onclose: null,
|
|
19
|
+
onerror: null,
|
|
20
|
+
onmessage: null,
|
|
21
|
+
CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3,
|
|
12
22
|
send: function() {},
|
|
13
|
-
close: function() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
close: function() {
|
|
24
|
+
if (socket.readyState >= 2) return;
|
|
25
|
+
socket.readyState = 3;
|
|
26
|
+
var evt = new Event('close');
|
|
27
|
+
try { evt.code = 1000; evt.reason = ''; evt.wasClean = true; } catch(e) {}
|
|
28
|
+
if (typeof socket.onclose === 'function') socket.onclose(evt);
|
|
29
|
+
var fns = listeners['close'] || [];
|
|
30
|
+
for (var i = 0; i < fns.length; i++) { try { fns[i](evt); } catch(e) {} }
|
|
31
|
+
},
|
|
32
|
+
addEventListener: function(type, fn) {
|
|
33
|
+
if (!listeners[type]) listeners[type] = [];
|
|
34
|
+
listeners[type].push(fn);
|
|
35
|
+
},
|
|
36
|
+
removeEventListener: function(type, fn) {
|
|
37
|
+
if (!listeners[type]) return;
|
|
38
|
+
listeners[type] = listeners[type].filter(function(f) { return f !== fn; });
|
|
39
|
+
},
|
|
40
|
+
dispatchEvent: function(evt) {
|
|
41
|
+
var fns = listeners[evt.type] || [];
|
|
42
|
+
for (var i = 0; i < fns.length; i++) { try { fns[i](evt); } catch(e) {} }
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
17
45
|
};
|
|
46
|
+
|
|
47
|
+
// Simulate async open — the SPA often waits for onopen before hydrating.
|
|
48
|
+
setTimeout(function() {
|
|
49
|
+
socket.readyState = 1;
|
|
50
|
+
var evt = new Event('open');
|
|
51
|
+
if (typeof socket.onopen === 'function') socket.onopen(evt);
|
|
52
|
+
var fns = listeners['open'] || [];
|
|
53
|
+
for (var i = 0; i < fns.length; i++) { try { fns[i](evt); } catch(e) {} }
|
|
54
|
+
}, 0);
|
|
55
|
+
|
|
18
56
|
return socket;
|
|
19
57
|
};
|
|
58
|
+
window.WebSocket.CONNECTING = 0;
|
|
59
|
+
window.WebSocket.OPEN = 1;
|
|
60
|
+
window.WebSocket.CLOSING = 2;
|
|
61
|
+
window.WebSocket.CLOSED = 3;
|
|
20
62
|
window.WebSocket.__pagepocketOriginal = OriginalWebSocket;
|
|
21
63
|
}
|
|
22
64
|
`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScriptHacker } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Intercept `new Worker(url|blob)` during replay so that worker-internal
|
|
4
|
+
* `fetch` / `XHR` requests are served from the saved api.json snapshot.
|
|
5
|
+
*
|
|
6
|
+
* Strategy: create a **real** Worker (preserving the full Worker global scope)
|
|
7
|
+
* but prepend the replay fetch/XHR patches and the api.json records to the
|
|
8
|
+
* worker script source. This avoids the impossible task of perfectly
|
|
9
|
+
* emulating the Worker global on the main thread.
|
|
10
|
+
*
|
|
11
|
+
* The injected preamble inside the worker:
|
|
12
|
+
* 1. Defines a minimal `matchAPI` + `findRecord` using the serialised records.
|
|
13
|
+
* 2. Monkey-patches `self.fetch` and `self.XMLHttpRequest.prototype` so
|
|
14
|
+
* network requests hit the saved records instead of the (offline) network.
|
|
15
|
+
* 3. Hands off to the original worker script, which now runs in a genuine
|
|
16
|
+
* Worker thread with all native APIs intact.
|
|
17
|
+
*/
|
|
18
|
+
export declare const replayWorkerStub: ScriptHacker;
|