@pagepocket/cli 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.
Files changed (39) hide show
  1. package/dist/cli.js +135 -0
  2. package/dist/index.js +9 -0
  3. package/dist/lib/content-type.js +36 -0
  4. package/dist/lib/css-rewrite.js +62 -0
  5. package/dist/lib/filename.js +14 -0
  6. package/dist/lib/hackers/capture-network.js +64 -0
  7. package/dist/lib/hackers/index.js +22 -0
  8. package/dist/lib/hackers/preload-fetch.js +56 -0
  9. package/dist/lib/hackers/preload-image.js +61 -0
  10. package/dist/lib/hackers/preload-xhr.js +59 -0
  11. package/dist/lib/hackers/replay-beacon.js +21 -0
  12. package/dist/lib/hackers/replay-dom-rewrite.js +295 -0
  13. package/dist/lib/hackers/replay-eventsource.js +25 -0
  14. package/dist/lib/hackers/replay-fetch.js +33 -0
  15. package/dist/lib/hackers/replay-image.js +48 -0
  16. package/dist/lib/hackers/replay-svg-image.js +89 -0
  17. package/dist/lib/hackers/replay-websocket.js +26 -0
  18. package/dist/lib/hackers/replay-xhr.js +91 -0
  19. package/dist/lib/hackers/types.js +2 -0
  20. package/dist/lib/network-records.js +69 -0
  21. package/dist/lib/replay-script.js +346 -0
  22. package/dist/lib/resources.js +131 -0
  23. package/dist/lib/stages/download.js +61 -0
  24. package/dist/lib/stages/index.js +235 -0
  25. package/dist/lib/stages/intercept.js +23 -0
  26. package/dist/lib/stages/trigger.js +56 -0
  27. package/dist/lib/stages/visit.js +24 -0
  28. package/dist/lib/types.js +2 -0
  29. package/dist/preload.js +60 -0
  30. package/dist/stages/build-snapshot-data.js +14 -0
  31. package/dist/stages/build-snapshot.js +30 -0
  32. package/dist/stages/capture-network.js +19 -0
  33. package/dist/stages/download-resources.js +48 -0
  34. package/dist/stages/fetch-html.js +69 -0
  35. package/dist/stages/prepare-output.js +27 -0
  36. package/dist/stages/rewrite-links.js +145 -0
  37. package/dist/stages/write-snapshot.js +12 -0
  38. package/dist/utils/with-spinner.js +20 -0
  39. package/package.json +44 -0
package/dist/cli.js ADDED
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ const core_1 = require("@oclif/core");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const lib_1 = require("@pagepocket/lib");
10
+ const with_spinner_1 = require("./utils/with-spinner");
11
+ const build_snapshot_data_1 = require("./stages/build-snapshot-data");
12
+ const capture_network_1 = require("./stages/capture-network");
13
+ const fetch_html_1 = require("./stages/fetch-html");
14
+ const prepare_output_1 = require("./stages/prepare-output");
15
+ const write_snapshot_1 = require("./stages/write-snapshot");
16
+ class PagepocketCommand extends core_1.Command {
17
+ async run() {
18
+ const { args, flags } = await this.parse(PagepocketCommand);
19
+ const targetUrl = args.url;
20
+ const outputFlag = flags.output ? flags.output.trim() : undefined;
21
+ const fetchTimeoutMs = Number(process.env.PAGEPOCKET_FETCH_TIMEOUT_MS || "60000");
22
+ const headersOverride = (() => {
23
+ const raw = process.env.PAGEPOCKET_FETCH_HEADERS;
24
+ if (!raw) {
25
+ return undefined;
26
+ }
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ const headers = {};
30
+ for (const [key, value] of Object.entries(parsed)) {
31
+ if (value === undefined || value === null) {
32
+ continue;
33
+ }
34
+ headers[key] = String(value);
35
+ }
36
+ return headers;
37
+ }
38
+ catch {
39
+ throw new Error("Invalid PAGEPOCKET_FETCH_HEADERS JSON.");
40
+ }
41
+ })();
42
+ const fetchXhrRecords = [];
43
+ const fetched = await (0, with_spinner_1.withSpinner)(async () => (0, fetch_html_1.fetchHtml)(targetUrl, fetchTimeoutMs, headersOverride), "Fetching the target HTML");
44
+ const networkStage = await (async () => {
45
+ try {
46
+ return await (0, with_spinner_1.withSpinner)(async () => (0, capture_network_1.captureNetwork)(targetUrl, fetched.title), "Capturing network requests with lighterceptor");
47
+ }
48
+ catch {
49
+ return {
50
+ networkRecords: [],
51
+ lighterceptorNetworkRecords: [],
52
+ capturedTitle: undefined,
53
+ title: fetched.title
54
+ };
55
+ }
56
+ })();
57
+ const outputPaths = await (0, with_spinner_1.withSpinner)(async () => (0, prepare_output_1.prepareOutputPaths)(networkStage.title, outputFlag), "Preparing output paths");
58
+ const downloadStage = await (0, with_spinner_1.withSpinner)(async () => {
59
+ const originalCwd = process.cwd();
60
+ const shouldRestoreCwd = outputPaths.baseDir !== originalCwd;
61
+ try {
62
+ if (shouldRestoreCwd) {
63
+ process.chdir(outputPaths.baseDir);
64
+ }
65
+ const seedSnapshot = {
66
+ url: targetUrl,
67
+ title: networkStage.title,
68
+ capturedAt: new Date().toISOString(),
69
+ fetchXhrRecords,
70
+ networkRecords: networkStage.lighterceptorNetworkRecords,
71
+ resources: []
72
+ };
73
+ const pagepocket = new lib_1.PagePocket(fetched.html, seedSnapshot, {
74
+ assetsDirName: outputPaths.assetsDirName,
75
+ baseUrl: targetUrl,
76
+ requestsPath: node_path_1.default.basename(outputPaths.outputRequestsPath)
77
+ });
78
+ const snapshotHtml = await pagepocket.put();
79
+ return {
80
+ snapshotHtml,
81
+ resourceMeta: pagepocket.resources,
82
+ downloadedCount: pagepocket.downloadedCount,
83
+ failedCount: pagepocket.failedCount
84
+ };
85
+ }
86
+ finally {
87
+ if (shouldRestoreCwd) {
88
+ try {
89
+ process.chdir(originalCwd);
90
+ }
91
+ catch {
92
+ // Ignore restore errors to preserve original failure.
93
+ }
94
+ }
95
+ }
96
+ }, "Downloading resources");
97
+ const snapshotData = await (0, with_spinner_1.withSpinner)(async () => (0, build_snapshot_data_1.buildSnapshotData)({
98
+ targetUrl,
99
+ title: networkStage.title,
100
+ fetchXhrRecords,
101
+ lighterceptorNetworkRecords: networkStage.lighterceptorNetworkRecords,
102
+ resources: downloadStage.resourceMeta
103
+ }), "Preparing snapshot HTML");
104
+ await (0, with_spinner_1.withSpinner)(async () => {
105
+ await (0, write_snapshot_1.writeSnapshotFiles)({
106
+ outputRequestsPath: outputPaths.outputRequestsPath,
107
+ outputHtmlPath: outputPaths.outputHtmlPath,
108
+ snapshotData,
109
+ snapshotHtml: downloadStage.snapshotHtml
110
+ });
111
+ }, "Writing snapshot files");
112
+ this.log(chalk_1.default.green("All done! Snapshot created."));
113
+ this.log(`HTML saved to ${chalk_1.default.cyan(outputPaths.outputHtmlPath)}`);
114
+ this.log(`Requests saved to ${chalk_1.default.cyan(outputPaths.outputRequestsPath)}`);
115
+ this.log(`Resources saved to ${chalk_1.default.cyan(outputPaths.resourcesDir)}`);
116
+ process.exit();
117
+ }
118
+ }
119
+ PagepocketCommand.description = "Save a snapshot of a web page.";
120
+ PagepocketCommand.args = {
121
+ url: core_1.Args.string({
122
+ description: "URL to snapshot",
123
+ required: true
124
+ })
125
+ };
126
+ PagepocketCommand.flags = {
127
+ help: core_1.Flags.help({
128
+ char: "h"
129
+ }),
130
+ output: core_1.Flags.string({
131
+ char: "o",
132
+ description: "Output path for the snapshot HTML file"
133
+ })
134
+ };
135
+ exports.default = PagepocketCommand;
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ (0, core_1.run)().catch((error) => {
5
+ const message = error && typeof error.message === "string" ? error.message : String(error);
6
+ console.error(message);
7
+ const exitCode = error && typeof error.exitCode === "number" ? error.exitCode : 1;
8
+ process.exit(exitCode);
9
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTextResponse = exports.extensionFromContentType = void 0;
4
+ const extensionFromContentType = (contentType) => {
5
+ if (!contentType) {
6
+ return "";
7
+ }
8
+ if (contentType.includes("text/css"))
9
+ return ".css";
10
+ if (contentType.includes("javascript"))
11
+ return ".js";
12
+ if (contentType.includes("image/png"))
13
+ return ".png";
14
+ if (contentType.includes("image/jpeg"))
15
+ return ".jpg";
16
+ if (contentType.includes("image/gif"))
17
+ return ".gif";
18
+ if (contentType.includes("image/svg"))
19
+ return ".svg";
20
+ if (contentType.includes("font/woff2"))
21
+ return ".woff2";
22
+ if (contentType.includes("font/woff"))
23
+ return ".woff";
24
+ return "";
25
+ };
26
+ exports.extensionFromContentType = extensionFromContentType;
27
+ const isTextResponse = (contentType) => {
28
+ const lowered = contentType.toLowerCase();
29
+ return (lowered.startsWith("text/") ||
30
+ lowered.includes("json") ||
31
+ lowered.includes("javascript") ||
32
+ lowered.includes("xml") ||
33
+ lowered.includes("svg") ||
34
+ lowered.includes("html"));
35
+ };
36
+ exports.isTextResponse = isTextResponse;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rewriteCssUrls = exports.buildDataUrlMap = void 0;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const getHeaderValue = (headers, name) => {
9
+ // Normalize header names to find the requested value.
10
+ for (const key in headers) {
11
+ if (key.toLowerCase() === name.toLowerCase()) {
12
+ return headers[key];
13
+ }
14
+ }
15
+ return undefined;
16
+ };
17
+ const buildDataUrlMap = (records) => {
18
+ // Build a lookup of recorded binary responses so CSS url() can be inlined.
19
+ const map = new Map();
20
+ for (const record of records) {
21
+ if (!record || !record.url || !record.responseBodyBase64) {
22
+ continue;
23
+ }
24
+ const headers = record.responseHeaders || {};
25
+ const contentType = getHeaderValue(headers, "content-type") || "application/octet-stream";
26
+ map.set(record.url, `data:${contentType};base64,${record.responseBodyBase64}`);
27
+ }
28
+ return map;
29
+ };
30
+ exports.buildDataUrlMap = buildDataUrlMap;
31
+ const rewriteCssUrls = async (filePath, cssUrl, dataUrlMap) => {
32
+ // Replace external URLs in CSS with data URLs to preserve rendering offline.
33
+ const css = await promises_1.default.readFile(filePath, "utf-8");
34
+ const urlPattern = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
35
+ const rewritten = css.replace(urlPattern, (match, quote, rawUrl) => {
36
+ const trimmed = String(rawUrl || "").trim();
37
+ if (!trimmed || trimmed.startsWith("data:") || trimmed.startsWith("blob:")) {
38
+ return match;
39
+ }
40
+ const absolute = (() => {
41
+ try {
42
+ return new URL(trimmed, cssUrl).toString();
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ })();
48
+ if (!absolute) {
49
+ return match;
50
+ }
51
+ const dataUrl = dataUrlMap.get(absolute);
52
+ if (!dataUrl) {
53
+ return match;
54
+ }
55
+ const safeQuote = quote || "";
56
+ return `url(${safeQuote}${dataUrl}${safeQuote})`;
57
+ });
58
+ if (rewritten !== css) {
59
+ await promises_1.default.writeFile(filePath, rewritten, "utf-8");
60
+ }
61
+ };
62
+ exports.rewriteCssUrls = rewriteCssUrls;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeFilename = void 0;
4
+ const safeFilename = (input) => {
5
+ const trimmed = input.trim();
6
+ if (!trimmed) {
7
+ return "snapshot";
8
+ }
9
+ return (trimmed
10
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
11
+ .replace(/^_+|_+$/g, "")
12
+ .slice(0, 120) || "snapshot");
13
+ };
14
+ exports.safeFilename = safeFilename;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.captureNetworkRecorder = void 0;
4
+ const content_type_1 = require("../content-type");
5
+ exports.captureNetworkRecorder = {
6
+ id: "capture-network-recorder",
7
+ stage: "capture",
8
+ apply: async ({ page, networkRecords }) => {
9
+ await page.setRequestInterception(true);
10
+ page.on("request", (request) => {
11
+ request.continue().catch(() => undefined);
12
+ });
13
+ page.on("response", async (response) => {
14
+ const request = response.request();
15
+ const url = response.url();
16
+ const headers = response.headers();
17
+ const requestHeaders = request.headers();
18
+ const requestBody = request.postData() || "";
19
+ // Capture the response body while preserving encoding for replay.
20
+ const { responseBody, responseBodyBase64, responseEncoding, error } = await (async () => {
21
+ try {
22
+ const buffer = await response.buffer();
23
+ const contentType = headers["content-type"] || "";
24
+ if ((0, content_type_1.isTextResponse)(contentType)) {
25
+ return {
26
+ responseBody: buffer.toString("utf-8"),
27
+ responseBodyBase64: undefined,
28
+ responseEncoding: "text",
29
+ error: undefined
30
+ };
31
+ }
32
+ return {
33
+ responseBody: undefined,
34
+ responseBodyBase64: buffer.toString("base64"),
35
+ responseEncoding: "base64",
36
+ error: undefined
37
+ };
38
+ }
39
+ catch (err) {
40
+ return {
41
+ responseBody: undefined,
42
+ responseBodyBase64: undefined,
43
+ responseEncoding: undefined,
44
+ error: String(err)
45
+ };
46
+ }
47
+ })();
48
+ networkRecords.push({
49
+ url,
50
+ method: request.method(),
51
+ requestHeaders,
52
+ requestBody,
53
+ status: response.status(),
54
+ statusText: response.statusText(),
55
+ responseHeaders: headers,
56
+ responseBody,
57
+ responseBodyBase64,
58
+ responseEncoding,
59
+ error,
60
+ timestamp: Date.now()
61
+ });
62
+ });
63
+ }
64
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replayHackers = exports.preloadHackers = void 0;
4
+ const preload_fetch_1 = require("./preload-fetch");
5
+ const preload_xhr_1 = require("./preload-xhr");
6
+ const replay_beacon_1 = require("./replay-beacon");
7
+ const replay_dom_rewrite_1 = require("./replay-dom-rewrite");
8
+ const replay_eventsource_1 = require("./replay-eventsource");
9
+ const replay_fetch_1 = require("./replay-fetch");
10
+ const replay_svg_image_1 = require("./replay-svg-image");
11
+ const replay_websocket_1 = require("./replay-websocket");
12
+ const replay_xhr_1 = require("./replay-xhr");
13
+ exports.preloadHackers = [preload_fetch_1.preloadFetchRecorder, preload_xhr_1.preloadXhrRecorder];
14
+ exports.replayHackers = [
15
+ replay_fetch_1.replayFetchResponder,
16
+ replay_xhr_1.replayXhrResponder,
17
+ replay_dom_rewrite_1.replayDomRewriter,
18
+ replay_svg_image_1.replaySvgImageRewriter,
19
+ replay_beacon_1.replayBeaconStub,
20
+ replay_websocket_1.replayWebSocketStub,
21
+ replay_eventsource_1.replayEventSourceStub
22
+ ];
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preloadFetchRecorder = void 0;
4
+ exports.preloadFetchRecorder = {
5
+ id: "preload-fetch-recorder",
6
+ stage: "preload",
7
+ build: () => `
8
+ // Record fetch calls and responses for replay.
9
+ const originalFetch = window.fetch.bind(window);
10
+ const recordFetch = async (input, init) => {
11
+ trackPendingStart();
12
+ const url = typeof input === "string" ? toAbsoluteUrl(input) : toAbsoluteUrl(input.url);
13
+ const method = init && init.method ? init.method : (typeof input === "string" ? "GET" : input.method || "GET");
14
+ const requestBody = normalizeBody(init && init.body ? init.body : (typeof input === "string" ? undefined : input.body));
15
+
16
+ try {
17
+ const response = await originalFetch(input, init);
18
+ const clone = response.clone();
19
+ const responseBody = await clone.text();
20
+ const headers = {};
21
+ response.headers.forEach((value, key) => {
22
+ headers[key] = value;
23
+ });
24
+ records.push({
25
+ kind: "fetch",
26
+ url,
27
+ method,
28
+ requestBody,
29
+ status: response.status,
30
+ statusText: response.statusText,
31
+ responseHeaders: headers,
32
+ responseBody,
33
+ timestamp: Date.now()
34
+ });
35
+ return response;
36
+ } catch (error) {
37
+ records.push({
38
+ kind: "fetch",
39
+ url,
40
+ method,
41
+ requestBody,
42
+ error: String(error),
43
+ timestamp: Date.now()
44
+ });
45
+ throw error;
46
+ } finally {
47
+ trackPendingEnd();
48
+ }
49
+ };
50
+
51
+ window.fetch = (input, init) => {
52
+ return recordFetch(input, init);
53
+ };
54
+ window.fetch.__pagepocketOriginal = originalFetch;
55
+ `
56
+ };
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preloadImageRecorder = void 0;
4
+ exports.preloadImageRecorder = {
5
+ id: "preload-image-recorder",
6
+ stage: "preload",
7
+ build: () => `
8
+ // Intercept image src assignments and record them without triggering network requests.
9
+ const NativeImage = window.Image;
10
+ const placeholderSrc =
11
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
12
+ const nativeDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
13
+ const nativeSet = nativeDescriptor && nativeDescriptor.set ? nativeDescriptor.set : null;
14
+ const nativeGet = nativeDescriptor && nativeDescriptor.get ? nativeDescriptor.get : null;
15
+
16
+ const recordImage = (input) => {
17
+ if (!input) return;
18
+ const url = toAbsoluteUrl(input);
19
+ records.push({
20
+ kind: "image",
21
+ url,
22
+ method: "GET",
23
+ requestBody: "",
24
+ timestamp: Date.now()
25
+ });
26
+ };
27
+
28
+ const shouldBypass = (value) => {
29
+ const raw = String(value || "");
30
+ if (!raw) return true;
31
+ return raw.startsWith("data:") || raw.startsWith("blob:");
32
+ };
33
+
34
+ Object.defineProperty(HTMLImageElement.prototype, "src", {
35
+ set(value) {
36
+ console.info(value)
37
+
38
+ if (shouldBypass(value)) {
39
+ if (nativeSet) nativeSet.call(this, value);
40
+ return;
41
+ }
42
+ const url = toAbsoluteUrl(value);
43
+ this.__webechoImageSrc = url;
44
+ recordImage(url);
45
+ if (nativeSet) nativeSet.call(this, placeholderSrc);
46
+ },
47
+ get() {
48
+ if (this.__webechoImageSrc) return this.__webechoImageSrc;
49
+ return nativeGet ? nativeGet.call(this) : "";
50
+ },
51
+ configurable: true,
52
+ enumerable: true
53
+ });
54
+
55
+ window.Image = function(...args) {
56
+ return new NativeImage(...args);
57
+ };
58
+ window.Image.prototype = NativeImage.prototype;
59
+ Object.defineProperty(window.Image, "name", { value: "Image" });
60
+ `
61
+ };
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preloadXhrRecorder = void 0;
4
+ exports.preloadXhrRecorder = {
5
+ id: "preload-xhr-recorder",
6
+ stage: "preload",
7
+ build: () => `
8
+ // Record XHR calls and responses for replay.
9
+ const originalOpen = XMLHttpRequest.prototype.open;
10
+ const originalSend = XMLHttpRequest.prototype.send;
11
+
12
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
13
+ this.__pagepocketMethod = method;
14
+ this.__pagepocketUrl = toAbsoluteUrl(url);
15
+ return originalOpen.call(this, method, url, ...rest);
16
+ };
17
+
18
+ XMLHttpRequest.prototype.send = function(body) {
19
+ const xhr = this;
20
+ const requestBody = normalizeBody(body);
21
+
22
+ const onLoadEnd = () => {
23
+ const responseHeadersRaw = xhr.getAllResponseHeaders();
24
+ const headers = {};
25
+ responseHeadersRaw
26
+ .trim()
27
+ .split(/\\r?\\n/)
28
+ .filter(Boolean)
29
+ .forEach((line) => {
30
+ const index = line.indexOf(":");
31
+ if (index > -1) {
32
+ const key = line.slice(0, index).trim().toLowerCase();
33
+ const value = line.slice(index + 1).trim();
34
+ headers[key] = value;
35
+ }
36
+ });
37
+
38
+ records.push({
39
+ kind: "xhr",
40
+ url: xhr.__pagepocketUrl || "",
41
+ method: xhr.__pagepocketMethod || "GET",
42
+ requestBody,
43
+ status: xhr.status,
44
+ statusText: xhr.statusText,
45
+ responseHeaders: headers,
46
+ responseBody: xhr.responseText || "",
47
+ timestamp: Date.now()
48
+ });
49
+
50
+ xhr.removeEventListener("loadend", onLoadEnd);
51
+ trackPendingEnd();
52
+ };
53
+
54
+ trackPendingStart();
55
+ xhr.addEventListener("loadend", onLoadEnd);
56
+ return originalSend.call(xhr, body);
57
+ };
58
+ `
59
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replayBeaconStub = void 0;
4
+ exports.replayBeaconStub = {
5
+ id: "replay-beacon-stub",
6
+ stage: "replay",
7
+ build: () => `
8
+ // Stub beacon calls so analytics doesn't leak outside the snapshot.
9
+ if (navigator.sendBeacon) {
10
+ const originalBeacon = navigator.sendBeacon.bind(navigator);
11
+ navigator.sendBeacon = (url, data) => {
12
+ const record = findRecord("POST", url, data);
13
+ if (record) {
14
+ return true;
15
+ }
16
+ return true;
17
+ };
18
+ navigator.sendBeacon.__pagepocketOriginal = originalBeacon;
19
+ }
20
+ `
21
+ };