@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.
Files changed (34) hide show
  1. package/dist/core/pagepocket.d.ts +17 -0
  2. package/dist/core/pagepocket.js +25 -1
  3. package/dist/hackers/index.js +3 -1
  4. package/dist/hackers/replay-dom-rewrite/script-part-1.d.ts +1 -1
  5. package/dist/hackers/replay-dom-rewrite/script-part-1.js +40 -51
  6. package/dist/hackers/replay-dom-rewrite/script-part-2.d.ts +1 -1
  7. package/dist/hackers/replay-dom-rewrite/script-part-2.js +74 -44
  8. package/dist/hackers/replay-fetch.js +4 -0
  9. package/dist/hackers/replay-websocket.js +50 -8
  10. package/dist/hackers/replay-worker.d.ts +18 -0
  11. package/dist/hackers/replay-worker.js +242 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/replay/match-api.js +103 -3
  14. package/dist/replay/templates/loader-template.d.ts +15 -0
  15. package/dist/replay/templates/loader-template.js +164 -0
  16. package/dist/replay/templates/match-api-source.d.ts +1 -1
  17. package/dist/replay/templates/match-api-source.js +86 -4
  18. package/dist/replay/templates/replay-script-template.part-2.js +24 -1
  19. package/dist/resource-filter.js +29 -3
  20. package/dist/snapshot-builder/build-snapshot.js +33 -3
  21. package/dist/units/apply-replace-elements-to-file-tree.d.ts +7 -0
  22. package/dist/units/apply-replace-elements-to-file-tree.js +63 -0
  23. package/dist/units/contracts-bridge.d.ts +9 -1
  24. package/dist/units/file-tree-unit.d.ts +28 -0
  25. package/dist/units/file-tree-unit.js +53 -0
  26. package/dist/units/index.d.ts +3 -0
  27. package/dist/units/index.js +2 -0
  28. package/dist/units/internal/runtime.d.ts +5 -2
  29. package/dist/units/internal/runtime.js +14 -0
  30. package/dist/units/runner.d.ts +3 -1
  31. package/dist/units/runner.js +72 -2
  32. package/dist/units/snapshot-unit.d.ts +31 -0
  33. package/dist/units/snapshot-unit.js +58 -0
  34. 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 {};
@@ -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
  }
@@ -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 if (tag !== \"link\") {\n return false;\n }\n\n const rel = String((element.getAttribute && element.getAttribute(\"rel\")) || \"\").toLowerCase();\n if (rel.includes(\"stylesheet\") || rel.includes(\"icon\")) {\n return true;\n }\n\n if (rel.includes(\"preload\") || rel.includes(\"prefetch\") || rel.includes(\"modulepreload\")) {\n return true;\n }\n\n return false;\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 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 const tag = (element.tagName || \"\").toLowerCase();\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 const rel = (element.getAttribute(\"rel\") || \"\").toLowerCase();\n if (href && !href.startsWith(\"data:\") && !href.startsWith(\"blob:\")) {\n if (\n rel.includes(\"stylesheet\") ||\n rel.includes(\"icon\") ||\n rel.includes(\"preload\") ||\n rel.includes(\"prefetch\") ||\n rel.includes(\"modulepreload\")\n ) {\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\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";
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
- if (tag !== "link") {
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 rewriteElement = (element) => {
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
- if (
166
- rel.includes("stylesheet") ||
167
- rel.includes("icon") ||
168
- rel.includes("preload") ||
169
- rel.includes("prefetch") ||
170
- rel.includes("modulepreload")
171
- ) {
172
- const as = (element.getAttribute("as") || "").toLowerCase();
173
- const fallbackType =
174
- rel.includes("stylesheet") || (rel.includes("preload") && as === "style")
175
- ? "text/css"
176
- : undefined;
177
- const next = rewriteResourceUrl(href, {
178
- kind: "attr",
179
- tag,
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, pendingValue));\n return;\n }\n if (attr === \"srcset\") {\n const rewritten = rewriteSrcset(String(value));\n return originalSetAttribute.call(this, name, rewritten);\n }\n\n if (!shouldRewriteAttr(this, attr)) {\n return originalSetAttribute.call(this, name, value);\n }\n\n const tag = (this.tagName || \"\").toLowerCase();\n const rel = (this.getAttribute && this.getAttribute(\"rel\")) || \"\";\n const relLower = rel.toLowerCase();\n const as = String((this.getAttribute && this.getAttribute(\"as\")) || \"\").toLowerCase();\n const fallbackType =\n attr === \"href\" && (relLower.includes(\"stylesheet\") || (relLower.includes(\"preload\") && as === \"style\"))\n ? \"text/css\"\n : undefined;\n\n const next = rewriteResourceUrl(String(value), {\n kind: \"setAttribute\",\n tag,\n attr,\n fallbackType\n });\n\n return originalSetAttribute.call(this, name, next);\n }\n return originalSetAttribute.call(this, name, value);\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 if (\n !relLower.includes(\"stylesheet\") &&\n !relLower.includes(\"icon\") &&\n !relLower.includes(\"preload\") &&\n !relLower.includes(\"prefetch\") &&\n !relLower.includes(\"modulepreload\")\n ) {\n setter.call(this, rawValue);\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 return;\n }\n if (\n !relLower.includes(\"stylesheet\") &&\n !relLower.includes(\"icon\") &&\n !relLower.includes(\"preload\") &&\n !relLower.includes(\"prefetch\") &&\n !relLower.includes(\"modulepreload\")\n ) {\n setter.call(this, rawValue);\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
+ 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(() => originalSetAttribute.call(this, name, pendingValue));
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
- if (!shouldRewriteAttr(this, attr)) {
17
- return originalSetAttribute.call(this, name, value);
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
- const tag = (this.tagName || "").toLowerCase();
21
- const rel = (this.getAttribute && this.getAttribute("rel")) || "";
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 originalSetAttribute.call(this, name, next);
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
- const OriginalWebSocket = window.WebSocket;
7
+ var OriginalWebSocket = window.WebSocket;
8
8
  window.WebSocket = function(url, protocols) {
9
- const socket = {
10
- url,
11
- readyState: 1,
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
- addEventListener: function() {},
15
- removeEventListener: function() {},
16
- dispatchEvent: function() { return false; }
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;