@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,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayDomRewriter = void 0;
|
|
4
|
+
exports.replayDomRewriter = {
|
|
5
|
+
id: "replay-dom-rewriter",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
// Placeholder data URLs for missing resources.
|
|
9
|
+
const transparentGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
10
|
+
const emptyScript = "data:text/javascript,/*pagepocket-missing*/";
|
|
11
|
+
const emptyStyle = "data:text/css,/*pagepocket-missing*/";
|
|
12
|
+
|
|
13
|
+
let readyResolved = false;
|
|
14
|
+
if (ready && typeof ready.then === "function") {
|
|
15
|
+
ready.then(() => {
|
|
16
|
+
readyResolved = true;
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
19
|
+
readyResolved = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const onReady = (callback) => {
|
|
23
|
+
if (readyResolved) {
|
|
24
|
+
callback();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (ready && typeof ready.then === "function") {
|
|
28
|
+
ready.then(callback);
|
|
29
|
+
} else {
|
|
30
|
+
callback();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Rewrite srcset values to local files only (avoid data: URLs in srcset).
|
|
35
|
+
const rewriteSrcset = (value) => {
|
|
36
|
+
if (!value) return value;
|
|
37
|
+
return value.split(",").map((part) => {
|
|
38
|
+
const trimmed = part.trim();
|
|
39
|
+
if (!trimmed) return trimmed;
|
|
40
|
+
const pieces = trimmed.split(/\\s+/, 2);
|
|
41
|
+
const url = pieces[0];
|
|
42
|
+
const descriptor = pieces[1];
|
|
43
|
+
if (isLocalResource(url)) return trimmed;
|
|
44
|
+
const localPath = findLocalPath(url);
|
|
45
|
+
if (localPath) {
|
|
46
|
+
return descriptor ? localPath + " " + descriptor : localPath;
|
|
47
|
+
}
|
|
48
|
+
return trimmed;
|
|
49
|
+
}).join(", ");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Rewrite element attributes to local files or data URLs.
|
|
53
|
+
const rewriteElement = (element) => {
|
|
54
|
+
if (!element || !element.getAttribute) return;
|
|
55
|
+
if (!readyResolved) {
|
|
56
|
+
onReady(() => rewriteElement(element));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const tag = (element.tagName || "").toLowerCase();
|
|
60
|
+
if (tag === "img" || tag === "source" || tag === "video" || tag === "audio" || tag === "script" || tag === "iframe") {
|
|
61
|
+
const src = element.getAttribute("src");
|
|
62
|
+
if (src && !isLocalResource(src) && !src.startsWith("data:") && !src.startsWith("blob:")) {
|
|
63
|
+
const localPath = findLocalPath(src);
|
|
64
|
+
if (localPath) {
|
|
65
|
+
element.setAttribute("src", localPath);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const record = findByUrl(src);
|
|
69
|
+
const fallback = tag === "script" ? emptyScript : transparentGif;
|
|
70
|
+
element.setAttribute("src", record ? toDataUrl(record) : fallback);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (tag === "link") {
|
|
75
|
+
const href = element.getAttribute("href");
|
|
76
|
+
const rel = (element.getAttribute("rel") || "").toLowerCase();
|
|
77
|
+
if (href && !isLocalResource(href) && !href.startsWith("data:") && !href.startsWith("blob:")) {
|
|
78
|
+
const localPath = findLocalPath(href);
|
|
79
|
+
if (localPath) {
|
|
80
|
+
element.setAttribute("href", localPath);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const record = findByUrl(href);
|
|
84
|
+
const fallback = rel === "stylesheet" ? emptyStyle : emptyStyle;
|
|
85
|
+
element.setAttribute("href", record ? toDataUrl(record, "text/css") : fallback);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const srcset = element.getAttribute("srcset");
|
|
90
|
+
if (srcset) {
|
|
91
|
+
element.setAttribute("srcset", rewriteSrcset(srcset));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Intercept DOM attribute writes to keep resources local.
|
|
96
|
+
const originalSetAttribute = Element.prototype.setAttribute;
|
|
97
|
+
Element.prototype.setAttribute = function(name, value) {
|
|
98
|
+
const attr = String(name).toLowerCase();
|
|
99
|
+
if (attr === "src" || attr === "href" || attr === "srcset") {
|
|
100
|
+
if (!readyResolved) {
|
|
101
|
+
const pendingValue = String(value);
|
|
102
|
+
onReady(() => originalSetAttribute.call(this, name, pendingValue));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (attr === "srcset") {
|
|
106
|
+
const rewritten = rewriteSrcset(String(value));
|
|
107
|
+
return originalSetAttribute.call(this, name, rewritten);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tag = (this.tagName || "").toLowerCase();
|
|
111
|
+
const rel = (this.getAttribute && this.getAttribute("rel")) || "";
|
|
112
|
+
const relLower = rel.toLowerCase();
|
|
113
|
+
|
|
114
|
+
if (isLocalResource(String(value))) {
|
|
115
|
+
return originalSetAttribute.call(this, name, value);
|
|
116
|
+
}
|
|
117
|
+
const localPath = findLocalPath(String(value));
|
|
118
|
+
if (localPath) {
|
|
119
|
+
return originalSetAttribute.call(this, name, localPath);
|
|
120
|
+
}
|
|
121
|
+
const record = findByUrl(String(value));
|
|
122
|
+
if (record) {
|
|
123
|
+
const fallbackType = attr === "href" && relLower.includes("stylesheet") ? "text/css" : undefined;
|
|
124
|
+
const dataUrl = toDataUrl(record, fallbackType);
|
|
125
|
+
return originalSetAttribute.call(this, name, dataUrl);
|
|
126
|
+
}
|
|
127
|
+
if (attr === "src") {
|
|
128
|
+
const fallback = tag === "script" ? emptyScript : transparentGif;
|
|
129
|
+
return originalSetAttribute.call(this, name, fallback);
|
|
130
|
+
}
|
|
131
|
+
if (attr === "href") {
|
|
132
|
+
const fallback = relLower === "stylesheet" ? emptyStyle : emptyStyle;
|
|
133
|
+
return originalSetAttribute.call(this, name, fallback);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return originalSetAttribute.call(this, name, value);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Patch property setters (e.g. img.src) so direct assignments are rewritten.
|
|
140
|
+
const patchProperty = (proto, prop, handler) => {
|
|
141
|
+
try {
|
|
142
|
+
const desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
143
|
+
if (!desc || !desc.set) return;
|
|
144
|
+
Object.defineProperty(proto, prop, {
|
|
145
|
+
configurable: true,
|
|
146
|
+
get: desc.get,
|
|
147
|
+
set: function(value) {
|
|
148
|
+
return handler.call(this, value, desc.set);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
} catch {}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
patchProperty(HTMLImageElement.prototype, "src", function(value, setter) {
|
|
155
|
+
const rawValue = String(value);
|
|
156
|
+
if (!readyResolved) {
|
|
157
|
+
onReady(() => {
|
|
158
|
+
if (isLocalResource(rawValue)) {
|
|
159
|
+
setter.call(this, rawValue);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const localPath = findLocalPath(rawValue);
|
|
163
|
+
if (localPath) {
|
|
164
|
+
setter.call(this, localPath);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const record = findByUrl(rawValue);
|
|
168
|
+
const next = record ? toDataUrl(record) : transparentGif;
|
|
169
|
+
setter.call(this, next);
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (isLocalResource(rawValue)) {
|
|
174
|
+
setter.call(this, rawValue);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const localPath = findLocalPath(rawValue);
|
|
178
|
+
if (localPath) {
|
|
179
|
+
setter.call(this, localPath);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const record = findByUrl(rawValue);
|
|
183
|
+
const next = record ? toDataUrl(record) : transparentGif;
|
|
184
|
+
setter.call(this, next);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
patchProperty(HTMLScriptElement.prototype, "src", function(value, setter) {
|
|
188
|
+
const rawValue = String(value);
|
|
189
|
+
if (!readyResolved) {
|
|
190
|
+
onReady(() => {
|
|
191
|
+
if (isLocalResource(rawValue)) {
|
|
192
|
+
setter.call(this, rawValue);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const localPath = findLocalPath(rawValue);
|
|
196
|
+
if (localPath) {
|
|
197
|
+
setter.call(this, localPath);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const record = findByUrl(rawValue);
|
|
201
|
+
const next = record ? toDataUrl(record) : emptyScript;
|
|
202
|
+
setter.call(this, next);
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isLocalResource(rawValue)) {
|
|
207
|
+
setter.call(this, rawValue);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const localPath = findLocalPath(rawValue);
|
|
211
|
+
if (localPath) {
|
|
212
|
+
setter.call(this, localPath);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const record = findByUrl(rawValue);
|
|
216
|
+
const next = record ? toDataUrl(record) : emptyScript;
|
|
217
|
+
setter.call(this, next);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
patchProperty(HTMLLinkElement.prototype, "href", function(value, setter) {
|
|
221
|
+
const rawValue = String(value);
|
|
222
|
+
const rel = (this.getAttribute && this.getAttribute("rel")) || "";
|
|
223
|
+
const relLower = rel.toLowerCase();
|
|
224
|
+
if (!readyResolved) {
|
|
225
|
+
onReady(() => {
|
|
226
|
+
if (isLocalResource(rawValue)) {
|
|
227
|
+
setter.call(this, rawValue);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const localPath = findLocalPath(rawValue);
|
|
231
|
+
if (localPath) {
|
|
232
|
+
setter.call(this, localPath);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const record = findByUrl(rawValue);
|
|
236
|
+
const next = record ? toDataUrl(record, relLower.includes("stylesheet") ? "text/css" : undefined) : emptyStyle;
|
|
237
|
+
setter.call(this, next);
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (isLocalResource(rawValue)) {
|
|
242
|
+
setter.call(this, rawValue);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const localPath = findLocalPath(rawValue);
|
|
246
|
+
if (localPath) {
|
|
247
|
+
setter.call(this, localPath);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const record = findByUrl(rawValue);
|
|
251
|
+
const next = record ? toDataUrl(record, relLower.includes("stylesheet") ? "text/css" : undefined) : emptyStyle;
|
|
252
|
+
setter.call(this, next);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
patchProperty(HTMLImageElement.prototype, "srcset", function(value, setter) {
|
|
256
|
+
const rawValue = String(value);
|
|
257
|
+
if (!readyResolved) {
|
|
258
|
+
onReady(() => {
|
|
259
|
+
const next = rewriteSrcset(rawValue);
|
|
260
|
+
setter.call(this, next);
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const next = rewriteSrcset(rawValue);
|
|
265
|
+
setter.call(this, next);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Observe DOM mutations and rewrite any new elements or attributes.
|
|
269
|
+
const observer = new MutationObserver((mutations) => {
|
|
270
|
+
for (const mutation of mutations) {
|
|
271
|
+
if (mutation.type === "attributes" && mutation.target) {
|
|
272
|
+
rewriteElement(mutation.target);
|
|
273
|
+
}
|
|
274
|
+
if (mutation.type === "childList") {
|
|
275
|
+
mutation.addedNodes.forEach((node) => {
|
|
276
|
+
if (node && node.nodeType === 1) {
|
|
277
|
+
rewriteElement(node);
|
|
278
|
+
const descendants = node.querySelectorAll ? node.querySelectorAll("img,source,video,audio,script,link,iframe") : [];
|
|
279
|
+
descendants.forEach((el) => rewriteElement(el));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
observer.observe(document.documentElement, {
|
|
287
|
+
attributes: true,
|
|
288
|
+
childList: true,
|
|
289
|
+
subtree: true,
|
|
290
|
+
attributeFilter: ["src", "href", "srcset"]
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
document.querySelectorAll("img,source,video,audio,script,link,iframe").forEach((el) => rewriteElement(el));
|
|
294
|
+
`
|
|
295
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayEventSourceStub = void 0;
|
|
4
|
+
exports.replayEventSourceStub = {
|
|
5
|
+
id: "replay-eventsource-stub",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
// Stub EventSource to prevent live network connections.
|
|
9
|
+
if (window.EventSource) {
|
|
10
|
+
const OriginalEventSource = window.EventSource;
|
|
11
|
+
window.EventSource = function(url) {
|
|
12
|
+
const source = {
|
|
13
|
+
url,
|
|
14
|
+
readyState: 1,
|
|
15
|
+
close: function() {},
|
|
16
|
+
addEventListener: function() {},
|
|
17
|
+
removeEventListener: function() {},
|
|
18
|
+
dispatchEvent: function() { return false; }
|
|
19
|
+
};
|
|
20
|
+
return source;
|
|
21
|
+
};
|
|
22
|
+
window.EventSource.__pagepocketOriginal = OriginalEventSource;
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayFetchResponder = void 0;
|
|
4
|
+
exports.replayFetchResponder = {
|
|
5
|
+
id: "replay-fetch-responder",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
// Patch fetch to serve from recorded network data.
|
|
9
|
+
const originalFetch = (typeof __pagepocketOriginalFetch === "function")
|
|
10
|
+
? __pagepocketOriginalFetch
|
|
11
|
+
: window.fetch.bind(window);
|
|
12
|
+
window.fetch = async (input, init = {}) => {
|
|
13
|
+
if (ready && typeof ready.then === "function") {
|
|
14
|
+
await ready;
|
|
15
|
+
}
|
|
16
|
+
const url = typeof input === "string" ? input : input.url;
|
|
17
|
+
const method = (init && init.method) || (typeof input === "string" ? "GET" : input.method || "GET");
|
|
18
|
+
const body = init && init.body;
|
|
19
|
+
try {
|
|
20
|
+
const record = findRecord(method, url, body);
|
|
21
|
+
if (record) {
|
|
22
|
+
return responseFromRecord(record);
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.warn("pagepocket fetch replay fallback", { url, method, err });
|
|
26
|
+
}
|
|
27
|
+
return new Response("", { status: 404, statusText: "Not Found" });
|
|
28
|
+
};
|
|
29
|
+
window.fetch.__pagepocketOriginal = originalFetch;
|
|
30
|
+
ensureReplayPatches && ensureReplayPatches();
|
|
31
|
+
|
|
32
|
+
`
|
|
33
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replaySvgImageRewriter = void 0;
|
|
4
|
+
exports.replaySvgImageRewriter = {
|
|
5
|
+
id: "replay-svg-image-rewriter",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
(function(){
|
|
9
|
+
const xlinkNs = "http://www.w3.org/1999/xlink";
|
|
10
|
+
const transparentGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
11
|
+
|
|
12
|
+
let readyResolved = false;
|
|
13
|
+
if (ready && typeof ready.then === "function") {
|
|
14
|
+
ready.then(() => {
|
|
15
|
+
readyResolved = true;
|
|
16
|
+
});
|
|
17
|
+
} else {
|
|
18
|
+
readyResolved = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const onReady = (callback) => {
|
|
22
|
+
if (readyResolved) {
|
|
23
|
+
callback();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (ready && typeof ready.then === "function") {
|
|
27
|
+
ready.then(callback);
|
|
28
|
+
} else {
|
|
29
|
+
callback();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const resolveHref = (value) => {
|
|
34
|
+
if (!value) return null;
|
|
35
|
+
if (isLocalResource(value)) return value;
|
|
36
|
+
const localPath = findLocalPath(value);
|
|
37
|
+
if (localPath) return localPath;
|
|
38
|
+
const record = findByUrl(value);
|
|
39
|
+
if (record) return toDataUrl(record);
|
|
40
|
+
return transparentGif;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const rewriteImage = (el) => {
|
|
44
|
+
if (!el || !el.getAttribute) return;
|
|
45
|
+
const href = el.getAttribute("href") || (el.getAttributeNS && el.getAttributeNS(xlinkNs, "href"));
|
|
46
|
+
if (!href) return;
|
|
47
|
+
if (href.startsWith("data:") || href.startsWith("blob:") || isLocalResource(href)) return;
|
|
48
|
+
if (!readyResolved) {
|
|
49
|
+
try { el.setAttributeNS(xlinkNs, "href", transparentGif); } catch {}
|
|
50
|
+
try { el.setAttribute("href", transparentGif); } catch {}
|
|
51
|
+
onReady(() => rewriteImage(el));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const next = resolveHref(href);
|
|
55
|
+
if (!next) return;
|
|
56
|
+
try { el.setAttributeNS(xlinkNs, "href", next); } catch {}
|
|
57
|
+
try { el.setAttribute("href", next); } catch {}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const observer = new MutationObserver((mutations) => {
|
|
61
|
+
for (const mutation of mutations) {
|
|
62
|
+
if (mutation.type === "attributes" && mutation.target && mutation.target.tagName && mutation.target.tagName.toLowerCase() === "image") {
|
|
63
|
+
rewriteImage(mutation.target);
|
|
64
|
+
}
|
|
65
|
+
if (mutation.type === "childList") {
|
|
66
|
+
mutation.addedNodes.forEach((node) => {
|
|
67
|
+
if (node && node.nodeType === 1) {
|
|
68
|
+
if ((node.tagName || "").toLowerCase() === "image") {
|
|
69
|
+
rewriteImage(node);
|
|
70
|
+
}
|
|
71
|
+
const descendants = node.querySelectorAll ? node.querySelectorAll("image") : [];
|
|
72
|
+
descendants.forEach((img) => rewriteImage(img));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
observer.observe(document.documentElement, {
|
|
80
|
+
attributes: true,
|
|
81
|
+
childList: true,
|
|
82
|
+
subtree: true,
|
|
83
|
+
attributeFilter: ["href", "xlink:href"]
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
document.querySelectorAll("image").forEach((el) => rewriteImage(el));
|
|
87
|
+
})();
|
|
88
|
+
`
|
|
89
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayWebSocketStub = void 0;
|
|
4
|
+
exports.replayWebSocketStub = {
|
|
5
|
+
id: "replay-websocket-stub",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
// Stub WebSocket to prevent live network connections.
|
|
9
|
+
if (window.WebSocket) {
|
|
10
|
+
const OriginalWebSocket = window.WebSocket;
|
|
11
|
+
window.WebSocket = function(url, protocols) {
|
|
12
|
+
const socket = {
|
|
13
|
+
url,
|
|
14
|
+
readyState: 1,
|
|
15
|
+
send: function() {},
|
|
16
|
+
close: function() {},
|
|
17
|
+
addEventListener: function() {},
|
|
18
|
+
removeEventListener: function() {},
|
|
19
|
+
dispatchEvent: function() { return false; }
|
|
20
|
+
};
|
|
21
|
+
return socket;
|
|
22
|
+
};
|
|
23
|
+
window.WebSocket.__pagepocketOriginal = OriginalWebSocket;
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replayXhrResponder = void 0;
|
|
4
|
+
exports.replayXhrResponder = {
|
|
5
|
+
id: "replay-xhr-responder",
|
|
6
|
+
stage: "replay",
|
|
7
|
+
build: () => `
|
|
8
|
+
// Patch XHR so app code sees consistent responses offline.
|
|
9
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
10
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
11
|
+
const waitForReady = () => {
|
|
12
|
+
if (ready && typeof ready.then === "function") {
|
|
13
|
+
return ready;
|
|
14
|
+
}
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
};
|
|
17
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
18
|
+
this.__pagepocketMethod = method;
|
|
19
|
+
this.__pagepocketUrl = url;
|
|
20
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
21
|
+
};
|
|
22
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
23
|
+
const method = this.__pagepocketMethod || "GET";
|
|
24
|
+
const url = this.__pagepocketUrl || "";
|
|
25
|
+
const xhr = this;
|
|
26
|
+
waitForReady().then(() => {
|
|
27
|
+
let record = null;
|
|
28
|
+
try {
|
|
29
|
+
record = findRecord(method, url, body);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.warn("pagepocket xhr replay fallback", { url, method, err });
|
|
32
|
+
}
|
|
33
|
+
if (record) {
|
|
34
|
+
const responseText = record.responseBody || "";
|
|
35
|
+
const status = record.status || 200;
|
|
36
|
+
const statusText = record.statusText || "OK";
|
|
37
|
+
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
defineProp(xhr, "readyState", 4);
|
|
40
|
+
defineProp(xhr, "status", status);
|
|
41
|
+
defineProp(xhr, "statusText", statusText);
|
|
42
|
+
if (xhr.responseType === "arraybuffer" && record.responseBodyBase64) {
|
|
43
|
+
const bytes = decodeBase64(record.responseBodyBase64);
|
|
44
|
+
defineProp(xhr, "response", bytes.buffer);
|
|
45
|
+
defineProp(xhr, "responseText", "");
|
|
46
|
+
} else if (xhr.responseType === "blob" && record.responseBodyBase64) {
|
|
47
|
+
const bytes = decodeBase64(record.responseBodyBase64);
|
|
48
|
+
defineProp(xhr, "response", new Blob([bytes]));
|
|
49
|
+
defineProp(xhr, "responseText", "");
|
|
50
|
+
} else {
|
|
51
|
+
defineProp(xhr, "response", responseText);
|
|
52
|
+
defineProp(xhr, "responseText", responseText);
|
|
53
|
+
}
|
|
54
|
+
if (typeof xhr.onreadystatechange === "function") xhr.onreadystatechange();
|
|
55
|
+
if (typeof xhr.onload === "function") xhr.onload(new Event("load"));
|
|
56
|
+
if (typeof xhr.onloadend === "function") xhr.onloadend(new Event("loadend"));
|
|
57
|
+
if (xhr.dispatchEvent) {
|
|
58
|
+
xhr.dispatchEvent(new Event("readystatechange"));
|
|
59
|
+
xhr.dispatchEvent(new Event("load"));
|
|
60
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
61
|
+
}
|
|
62
|
+
}, 0);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const status = 404;
|
|
67
|
+
const statusText = "Not Found";
|
|
68
|
+
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
defineProp(xhr, "readyState", 4);
|
|
71
|
+
defineProp(xhr, "status", status);
|
|
72
|
+
defineProp(xhr, "statusText", statusText);
|
|
73
|
+
defineProp(xhr, "response", "");
|
|
74
|
+
defineProp(xhr, "responseText", "");
|
|
75
|
+
if (typeof xhr.onreadystatechange === "function") xhr.onreadystatechange();
|
|
76
|
+
if (typeof xhr.onload === "function") xhr.onload(new Event("load"));
|
|
77
|
+
if (typeof xhr.onloadend === "function") xhr.onloadend(new Event("loadend"));
|
|
78
|
+
if (xhr.dispatchEvent) {
|
|
79
|
+
xhr.dispatchEvent(new Event("readystatechange"));
|
|
80
|
+
xhr.dispatchEvent(new Event("load"));
|
|
81
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
82
|
+
}
|
|
83
|
+
}, 0);
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
};
|
|
87
|
+
XMLHttpRequest.prototype.open.__pagepocketOriginal = originalOpen;
|
|
88
|
+
XMLHttpRequest.prototype.send.__pagepocketOriginal = originalSend;
|
|
89
|
+
ensureReplayPatches && ensureReplayPatches();
|
|
90
|
+
`
|
|
91
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type HackerStage = "preload" | "replay";
|
|
2
|
+
export type HackerContext = {
|
|
3
|
+
stage: HackerStage;
|
|
4
|
+
};
|
|
5
|
+
export type ScriptHacker = {
|
|
6
|
+
id: string;
|
|
7
|
+
stage: "preload" | "replay";
|
|
8
|
+
build: (context: HackerContext) => string;
|
|
9
|
+
};
|
|
10
|
+
export type HackerModule = ScriptHacker;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PagePocket } from "./pagepocket";
|
|
2
|
+
export type { PagePocketOptions } from "./pagepocket";
|
|
3
|
+
export type { FetchRecord, NetworkRecord, LighterceptorNetworkRecord, SnapshotData } from "./types";
|
|
4
|
+
export { buildReplayScript } from "./replay-script";
|
|
5
|
+
export { buildPreloadScript } from "./preload";
|
|
6
|
+
export { findFaviconDataUrl, mapLighterceptorRecords, toDataUrlFromRecord } from "./network-records";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toDataUrlFromRecord = exports.mapLighterceptorRecords = exports.findFaviconDataUrl = exports.buildPreloadScript = exports.buildReplayScript = exports.PagePocket = void 0;
|
|
4
|
+
var pagepocket_1 = require("./pagepocket");
|
|
5
|
+
Object.defineProperty(exports, "PagePocket", { enumerable: true, get: function () { return pagepocket_1.PagePocket; } });
|
|
6
|
+
var replay_script_1 = require("./replay-script");
|
|
7
|
+
Object.defineProperty(exports, "buildReplayScript", { enumerable: true, get: function () { return replay_script_1.buildReplayScript; } });
|
|
8
|
+
var preload_1 = require("./preload");
|
|
9
|
+
Object.defineProperty(exports, "buildPreloadScript", { enumerable: true, get: function () { return preload_1.buildPreloadScript; } });
|
|
10
|
+
var network_records_1 = require("./network-records");
|
|
11
|
+
Object.defineProperty(exports, "findFaviconDataUrl", { enumerable: true, get: function () { return network_records_1.findFaviconDataUrl; } });
|
|
12
|
+
Object.defineProperty(exports, "mapLighterceptorRecords", { enumerable: true, get: function () { return network_records_1.mapLighterceptorRecords; } });
|
|
13
|
+
Object.defineProperty(exports, "toDataUrlFromRecord", { enumerable: true, get: function () { return network_records_1.toDataUrlFromRecord; } });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LighterceptorNetworkRecord, NetworkRecord } from "./types";
|
|
2
|
+
export declare const toDataUrlFromRecord: (record: NetworkRecord) => string | null;
|
|
3
|
+
export declare const findFaviconDataUrl: (records: NetworkRecord[]) => string | null;
|
|
4
|
+
export declare const mapLighterceptorRecords: (records: LighterceptorNetworkRecord[] | undefined) => NetworkRecord[];
|