@pagepocket/lib 0.6.3 → 0.7.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/README.md +265 -3
- package/dist/builtin-blacklist.d.ts +3 -0
- package/dist/builtin-blacklist.js +6 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +18 -0
- package/dist/hackers/index.js +2 -0
- package/dist/hackers/replay-css-proxy.d.ts +2 -0
- package/dist/hackers/replay-css-proxy.js +206 -0
- package/dist/hackers/replay-dom-rewrite.js +57 -26
- package/dist/index.d.ts +3 -1
- package/dist/index.js +18 -1
- package/dist/inflight-tracker.d.ts +19 -0
- package/dist/inflight-tracker.js +48 -0
- package/dist/pagepocket.d.ts +3 -1
- package/dist/pagepocket.js +150 -35
- package/dist/replace-elements.d.ts +9 -0
- package/dist/replace-elements.js +258 -0
- package/dist/replay-script.js +286 -2
- package/dist/resource-proxy.d.ts +34 -0
- package/dist/resource-proxy.js +284 -0
- package/dist/rewrite-links.d.ts +8 -0
- package/dist/rewrite-links.js +8 -0
- package/dist/snapshot-builder.d.ts +2 -1
- package/dist/snapshot-builder.js +51 -1
- package/dist/types.d.ts +88 -1
- package/dist/writers.d.ts +2 -2
- package/dist/writers.js +56 -4
- package/package.json +3 -3
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyReplaceElements = void 0;
|
|
4
|
+
const defaultApply = {
|
|
5
|
+
scope: "document",
|
|
6
|
+
limit: "all",
|
|
7
|
+
onReplaced: "stop"
|
|
8
|
+
};
|
|
9
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null;
|
|
10
|
+
const normalizeApply = (apply) => ({
|
|
11
|
+
scope: apply?.scope ?? defaultApply.scope,
|
|
12
|
+
limit: apply?.limit ?? defaultApply.limit,
|
|
13
|
+
onReplaced: apply?.onReplaced ?? defaultApply.onReplaced
|
|
14
|
+
});
|
|
15
|
+
const normalizeMatchToSelector = (match) => {
|
|
16
|
+
if (typeof match === "string") {
|
|
17
|
+
return { selector: match, filter: null };
|
|
18
|
+
}
|
|
19
|
+
return { selector: match.selector ?? "*", filter: match };
|
|
20
|
+
};
|
|
21
|
+
const getNodeName = (el) => {
|
|
22
|
+
if (!el || typeof el !== "object")
|
|
23
|
+
return null;
|
|
24
|
+
const name = el.name;
|
|
25
|
+
return typeof name === "string" ? name : null;
|
|
26
|
+
};
|
|
27
|
+
const getNodeType = (el) => {
|
|
28
|
+
if (!el || typeof el !== "object")
|
|
29
|
+
return null;
|
|
30
|
+
const type = el.type;
|
|
31
|
+
return typeof type === "string" ? type : null;
|
|
32
|
+
};
|
|
33
|
+
const tagNameMatches = (el, expected) => {
|
|
34
|
+
if (!expected)
|
|
35
|
+
return true;
|
|
36
|
+
const name = getNodeName(el);
|
|
37
|
+
if (!name)
|
|
38
|
+
return false;
|
|
39
|
+
return name.toLowerCase() === expected.toLowerCase();
|
|
40
|
+
};
|
|
41
|
+
const idMatches = (_$, $el, expected) => {
|
|
42
|
+
if (!expected)
|
|
43
|
+
return true;
|
|
44
|
+
return ($el.attr("id") || "") === expected;
|
|
45
|
+
};
|
|
46
|
+
const attrsMatch = ($el, attrs) => {
|
|
47
|
+
if (!attrs)
|
|
48
|
+
return true;
|
|
49
|
+
for (const [name, expected] of Object.entries(attrs)) {
|
|
50
|
+
const actual = $el.attr(name);
|
|
51
|
+
if (expected === true) {
|
|
52
|
+
if (actual === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (typeof expected === "string") {
|
|
57
|
+
if (actual !== expected)
|
|
58
|
+
return false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (expected instanceof RegExp) {
|
|
62
|
+
if (actual === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
if (!expected.test(actual))
|
|
65
|
+
return false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
const elementMatchesFilter = ($, $el, filter) => {
|
|
72
|
+
if (!filter || typeof filter === "string")
|
|
73
|
+
return true;
|
|
74
|
+
const el = $el.get(0);
|
|
75
|
+
if (!el)
|
|
76
|
+
return false;
|
|
77
|
+
if (!tagNameMatches(el, filter.tagName))
|
|
78
|
+
return false;
|
|
79
|
+
if (!idMatches($, $el, filter.id))
|
|
80
|
+
return false;
|
|
81
|
+
if (!attrsMatch($el, filter.attrs))
|
|
82
|
+
return false;
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
const isHtmlElement = ($, $el) => {
|
|
86
|
+
if (!$el || !$el.length)
|
|
87
|
+
return false;
|
|
88
|
+
if (!$el.closest("html").length)
|
|
89
|
+
return false;
|
|
90
|
+
const el = $el.get(0);
|
|
91
|
+
if (!el)
|
|
92
|
+
return false;
|
|
93
|
+
// Cheerio nodes for tags have a "type" of "tag".
|
|
94
|
+
const type = getNodeType(el);
|
|
95
|
+
if (type && type !== "tag")
|
|
96
|
+
return false;
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
const applyReplaceAction = ($, $el, action) => {
|
|
100
|
+
switch (action.type) {
|
|
101
|
+
case "replaceWithHtml": {
|
|
102
|
+
$el.replaceWith(action.html);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
case "replaceWithElement": {
|
|
106
|
+
const next = $(`<${action.tagName}></${action.tagName}>`);
|
|
107
|
+
if (action.attrs) {
|
|
108
|
+
for (const [key, value] of Object.entries(action.attrs)) {
|
|
109
|
+
if (value === null) {
|
|
110
|
+
next.removeAttr(key);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
next.attr(key, value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (action.html !== undefined) {
|
|
117
|
+
next.html(action.html);
|
|
118
|
+
}
|
|
119
|
+
else if (action.textContent !== undefined) {
|
|
120
|
+
next.text(action.textContent);
|
|
121
|
+
}
|
|
122
|
+
$el.replaceWith(next);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
case "renameTag": {
|
|
126
|
+
const to = action.to;
|
|
127
|
+
const keepAttributes = action.keepAttributes !== false;
|
|
128
|
+
const keepChildren = action.keepChildren !== false;
|
|
129
|
+
const next = $(`<${to}></${to}>`);
|
|
130
|
+
if (keepAttributes) {
|
|
131
|
+
const attrs = ($el.attr() ?? {});
|
|
132
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
133
|
+
next.attr(key, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (keepChildren) {
|
|
137
|
+
next.append($el.contents());
|
|
138
|
+
}
|
|
139
|
+
$el.replaceWith(next);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "remove": {
|
|
143
|
+
$el.remove();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
default: {
|
|
147
|
+
const exhaustive = action;
|
|
148
|
+
throw new Error(`Unknown replace action: ${String(exhaustive?.type ?? "")}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const normalizeReplaceElementsConfig = (replaceElements) => {
|
|
153
|
+
const items = [];
|
|
154
|
+
replaceElements.forEach((item, ruleIndex) => {
|
|
155
|
+
if (typeof item === "function") {
|
|
156
|
+
items.push({
|
|
157
|
+
kind: "fn",
|
|
158
|
+
ruleIndex,
|
|
159
|
+
query: "*",
|
|
160
|
+
run: item,
|
|
161
|
+
apply: normalizeApply(undefined)
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!isPlainObject(item)) {
|
|
166
|
+
throw new Error("replaceElements item must be a rule object or function");
|
|
167
|
+
}
|
|
168
|
+
if ("run" in item && typeof item.run === "function") {
|
|
169
|
+
const fnItem = item;
|
|
170
|
+
items.push({
|
|
171
|
+
kind: "fn",
|
|
172
|
+
ruleIndex,
|
|
173
|
+
query: fnItem.query,
|
|
174
|
+
run: fnItem.run,
|
|
175
|
+
apply: normalizeApply(fnItem.apply)
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const rule = item;
|
|
180
|
+
items.push({ kind: "rule", ruleIndex, rule, apply: normalizeApply(rule.apply) });
|
|
181
|
+
});
|
|
182
|
+
return items;
|
|
183
|
+
};
|
|
184
|
+
const runFnRuleOnSelection = async (input) => {
|
|
185
|
+
const { $, item } = input;
|
|
186
|
+
const selection = $(item.query).toArray();
|
|
187
|
+
const limit = item.apply.limit;
|
|
188
|
+
const max = limit === "all" ? selection.length : Math.max(0, limit);
|
|
189
|
+
for (let i = 0; i < selection.length && i < max; i += 1) {
|
|
190
|
+
const el = selection[i];
|
|
191
|
+
const $el = $(el);
|
|
192
|
+
if (!isHtmlElement($, $el)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const ctx = {
|
|
196
|
+
$,
|
|
197
|
+
$el,
|
|
198
|
+
url: input.url,
|
|
199
|
+
entryUrl: input.entryUrl,
|
|
200
|
+
ruleIndex: item.ruleIndex,
|
|
201
|
+
matchIndex: i
|
|
202
|
+
};
|
|
203
|
+
const result = await item.run(ctx);
|
|
204
|
+
if (!result) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const actions = Array.isArray(result) ? result : [result];
|
|
208
|
+
for (const action of actions) {
|
|
209
|
+
applyReplaceAction($, $el, action);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const applyReplaceElements = async (input) => {
|
|
214
|
+
if (!input.replaceElements || input.replaceElements.length === 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const items = normalizeReplaceElementsConfig(input.replaceElements);
|
|
218
|
+
const active = items.filter((it) => it.apply.scope === "allFrames" || input.isEntryDocument);
|
|
219
|
+
const replacedByIndex = new WeakSet();
|
|
220
|
+
for (const item of active) {
|
|
221
|
+
if (item.kind === "fn") {
|
|
222
|
+
// Function rule uses its own query to select targets.
|
|
223
|
+
// Note: "onReplaced" semantics are best-effort for function rules.
|
|
224
|
+
await runFnRuleOnSelection({
|
|
225
|
+
$: input.$,
|
|
226
|
+
entryUrl: input.entryUrl,
|
|
227
|
+
url: input.url,
|
|
228
|
+
item
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// Object rules: we can respect onReplaced by skipping elements already replaced.
|
|
233
|
+
const { selector, filter } = normalizeMatchToSelector(item.rule.match);
|
|
234
|
+
const selection = input.$(selector).toArray();
|
|
235
|
+
const limit = item.apply.limit;
|
|
236
|
+
const max = limit === "all" ? selection.length : Math.max(0, limit);
|
|
237
|
+
let replacedCount = 0;
|
|
238
|
+
for (let i = 0; i < selection.length && replacedCount < max; i += 1) {
|
|
239
|
+
const el = selection[i];
|
|
240
|
+
if (item.apply.onReplaced === "stop" && replacedByIndex.has(el)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const $el = input.$(selection[i]);
|
|
244
|
+
if (!isHtmlElement(input.$, $el)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!elementMatchesFilter(input.$, $el, filter)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
applyReplaceAction(input.$, $el, item.rule.replace);
|
|
251
|
+
replacedCount += 1;
|
|
252
|
+
if (item.apply.onReplaced === "stop") {
|
|
253
|
+
replacedByIndex.add(el);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
exports.applyReplaceElements = applyReplaceElements;
|
package/dist/replay-script.js
CHANGED
|
@@ -147,13 +147,280 @@ const buildReplayScript = (apiPath, baseUrl) => {
|
|
|
147
147
|
const hackerScripts = hackers_1.replayHackers
|
|
148
148
|
.map((hacker) => ` // hacker:${hacker.id}\n${hacker.build(context)}`)
|
|
149
149
|
.join("\n");
|
|
150
|
+
const resourceProxyScript = `
|
|
151
|
+
const __pagepocketResourceProxy = (() => {
|
|
152
|
+
const stripHash = (value) => {
|
|
153
|
+
const input = String(value || "");
|
|
154
|
+
const index = input.indexOf("#");
|
|
155
|
+
return index === -1 ? input : input.slice(0, index);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const stripTrailingSlash = (value) => {
|
|
159
|
+
const input = String(value || "");
|
|
160
|
+
if (!input || input === "/") return input;
|
|
161
|
+
return input.endsWith("/") ? input.slice(0, -1) : input;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const looksAlreadyEscapedForStaticServers = (value) => {
|
|
165
|
+
return /%25[0-9a-fA-F]{2}/.test(String(value || ""));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const escapePercentForStaticServersOnce = (value) => {
|
|
169
|
+
const input = String(value || "");
|
|
170
|
+
if (!input) return input;
|
|
171
|
+
if (looksAlreadyEscapedForStaticServers(input)) return input;
|
|
172
|
+
return input.split("%").join("%25");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const isLikelyHostname = (value) => {
|
|
176
|
+
const input = String(value || "");
|
|
177
|
+
if (!input) return false;
|
|
178
|
+
if (input === "localhost") return true;
|
|
179
|
+
return input.includes(".");
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const encodeEmbeddedUrlTailIfPresent = (pathname) => {
|
|
183
|
+
const raw = String(pathname || "");
|
|
184
|
+
if (!raw.includes("/http")) return null;
|
|
185
|
+
const parts = raw.split("/");
|
|
186
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
187
|
+
const scheme = parts[i];
|
|
188
|
+
if (scheme !== "http:" && scheme !== "https:") continue;
|
|
189
|
+
const hasDoubleSlash = parts[i + 1] === "";
|
|
190
|
+
const host = parts[i + 2] || "";
|
|
191
|
+
if (!hasDoubleSlash || !isLikelyHostname(host)) continue;
|
|
192
|
+
const embedded = scheme + "//" + parts.slice(i + 2).join("/");
|
|
193
|
+
const encoded = encodeURIComponent(embedded);
|
|
194
|
+
const nextParts = parts.slice(0, i).concat(encoded);
|
|
195
|
+
const rebuilt = nextParts.join("/") || "/";
|
|
196
|
+
return rebuilt.startsWith("/") ? rebuilt : "/" + rebuilt;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const makePathnameVariants = (pathname) => {
|
|
202
|
+
const variants = new Set();
|
|
203
|
+
const push = (value) => {
|
|
204
|
+
if (!value) return;
|
|
205
|
+
variants.add(value);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const raw = String(pathname || "");
|
|
209
|
+
push(raw);
|
|
210
|
+
push(stripTrailingSlash(raw));
|
|
211
|
+
|
|
212
|
+
const encodedTail = encodeEmbeddedUrlTailIfPresent(raw);
|
|
213
|
+
if (encodedTail && encodedTail !== raw) {
|
|
214
|
+
push(encodedTail);
|
|
215
|
+
push(stripTrailingSlash(encodedTail));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return Array.from(variants);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const toUrlOrNull = (value, base) => {
|
|
222
|
+
try {
|
|
223
|
+
return new URL(String(value || ""), String(base || ""));
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const getBasename = (pathname) => {
|
|
230
|
+
const clean = String(pathname || "").split("?")[0] || "";
|
|
231
|
+
const parts = clean.split("/").filter(Boolean);
|
|
232
|
+
return parts[parts.length - 1] || "";
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const makeSuffixes = (pathname) => {
|
|
236
|
+
const parts = String(pathname || "").split("/").filter(Boolean);
|
|
237
|
+
const out = [];
|
|
238
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
239
|
+
out.push({ key: "/" + parts.slice(i).join("/"), depth: parts.length - i });
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const toArray = (value) => {
|
|
245
|
+
if (!value) return [];
|
|
246
|
+
return Array.isArray(value) ? value : [value];
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const addMulti = (map, key, value) => {
|
|
250
|
+
const existing = map.get(key);
|
|
251
|
+
if (!existing) {
|
|
252
|
+
map.set(key, value);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(existing)) {
|
|
256
|
+
existing.push(value);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
map.set(key, [existing, value]);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const uniqByPath = (items) => {
|
|
263
|
+
const seen = new Set();
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const item of items || []) {
|
|
266
|
+
const p = item && item.path;
|
|
267
|
+
if (!p || seen.has(p)) continue;
|
|
268
|
+
seen.add(p);
|
|
269
|
+
out.push(item);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const chooseUnique = (items, baseUrl, depth) => {
|
|
275
|
+
const unique = uniqByPath(items);
|
|
276
|
+
if (unique.length === 0) return null;
|
|
277
|
+
if (unique.length === 1) return unique[0];
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const base = new URL(String(baseUrl || ""));
|
|
281
|
+
const sameOrigin = unique.filter((i) => i && i.origin === base.origin);
|
|
282
|
+
if (sameOrigin.length === 1) {
|
|
283
|
+
return sameOrigin[0];
|
|
284
|
+
}
|
|
285
|
+
if (sameOrigin.length > 1) {
|
|
286
|
+
items = sameOrigin;
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
|
|
290
|
+
if (depth < 2) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const buildIndex = (snapshot) => {
|
|
298
|
+
const byExactUrl = new Map();
|
|
299
|
+
const byPathnameWithSearch = new Map();
|
|
300
|
+
const byPathname = new Map();
|
|
301
|
+
const byBasename = new Map();
|
|
302
|
+
|
|
303
|
+
for (const item of (snapshot && snapshot.items) || []) {
|
|
304
|
+
if (!item || !item.url || !item.path) continue;
|
|
305
|
+
let parsed = null;
|
|
306
|
+
try {
|
|
307
|
+
parsed = new URL(item.url);
|
|
308
|
+
} catch {
|
|
309
|
+
parsed = null;
|
|
310
|
+
}
|
|
311
|
+
if (!parsed) continue;
|
|
312
|
+
|
|
313
|
+
const pathname = parsed.pathname || "/";
|
|
314
|
+
const pathnameWithSearch = pathname + (parsed.search || "");
|
|
315
|
+
const basename = getBasename(pathname);
|
|
316
|
+
|
|
317
|
+
const indexed = {
|
|
318
|
+
url: parsed.toString(),
|
|
319
|
+
origin: parsed.origin,
|
|
320
|
+
pathname,
|
|
321
|
+
pathnameWithSearch,
|
|
322
|
+
basename,
|
|
323
|
+
path: item.path,
|
|
324
|
+
resourceType: item.resourceType,
|
|
325
|
+
mimeType: item.mimeType,
|
|
326
|
+
size: item.size
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (!byExactUrl.has(indexed.url)) {
|
|
330
|
+
byExactUrl.set(indexed.url, indexed);
|
|
331
|
+
}
|
|
332
|
+
addMulti(byPathnameWithSearch, pathnameWithSearch, indexed);
|
|
333
|
+
addMulti(byPathname, pathname, indexed);
|
|
334
|
+
if (basename) {
|
|
335
|
+
addMulti(byBasename, basename, indexed);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { byExactUrl, byPathnameWithSearch, byPathname, byBasename };
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const resolveToLocalPath = (requestUrl, baseUrl, index) => {
|
|
343
|
+
if (!requestUrl) return null;
|
|
344
|
+
const abs = toUrlOrNull(String(requestUrl), baseUrl);
|
|
345
|
+
if (!abs) return null;
|
|
346
|
+
|
|
347
|
+
const absString = abs.toString();
|
|
348
|
+
const exact = index && index.byExactUrl && index.byExactUrl.get(absString);
|
|
349
|
+
if (exact) return escapePercentForStaticServersOnce(exact.path);
|
|
350
|
+
|
|
351
|
+
const withoutHash = stripHash(absString);
|
|
352
|
+
if (withoutHash !== absString) {
|
|
353
|
+
const found = index.byExactUrl.get(withoutHash);
|
|
354
|
+
if (found) return escapePercentForStaticServersOnce(found.path);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const pathname = abs.pathname || "/";
|
|
358
|
+
const pathnameVariants = makePathnameVariants(pathname);
|
|
359
|
+
const search = abs.search || "";
|
|
360
|
+
const pathnameWithSearchVariants = pathnameVariants.map((p) => String(p) + search);
|
|
361
|
+
|
|
362
|
+
for (const key of pathnameWithSearchVariants) {
|
|
363
|
+
const items = toArray(index.byPathnameWithSearch.get(key));
|
|
364
|
+
const chosen = chooseUnique(items, baseUrl, 99);
|
|
365
|
+
if (chosen) return escapePercentForStaticServersOnce(chosen.path);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const key of pathnameVariants) {
|
|
369
|
+
const items = toArray(index.byPathname.get(key));
|
|
370
|
+
const chosen = chooseUnique(items, baseUrl, 99);
|
|
371
|
+
if (chosen) return escapePercentForStaticServersOnce(chosen.path);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const variant of pathnameVariants) {
|
|
375
|
+
for (const suffix of makeSuffixes(variant)) {
|
|
376
|
+
const items = toArray(index.byPathname.get(suffix.key));
|
|
377
|
+
const chosen = chooseUnique(items, baseUrl, suffix.depth);
|
|
378
|
+
if (chosen) return escapePercentForStaticServersOnce(chosen.path);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const basename = getBasename(pathname);
|
|
383
|
+
if (basename) {
|
|
384
|
+
const items = toArray(index.byBasename.get(basename));
|
|
385
|
+
const chosen = chooseUnique(items, baseUrl, 1);
|
|
386
|
+
if (chosen) return escapePercentForStaticServersOnce(chosen.path);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return { buildIndex, resolveToLocalPath };
|
|
393
|
+
})();
|
|
394
|
+
`;
|
|
150
395
|
return `
|
|
151
396
|
<script>
|
|
152
397
|
(function(){
|
|
153
398
|
const baseUrl = ${basePayload};
|
|
154
399
|
const apiUrl = ${apiPayload};
|
|
400
|
+
const resourcesPathUrl = "/resources_path.json";
|
|
155
401
|
const __pagepocketOriginalFetch = window.fetch ? window.fetch.bind(window) : null;
|
|
156
402
|
|
|
403
|
+
${resourceProxyScript}
|
|
404
|
+
|
|
405
|
+
const loadResourcesPath = async () => {
|
|
406
|
+
try {
|
|
407
|
+
if (!__pagepocketOriginalFetch) {
|
|
408
|
+
throw new Error("Fetch is unavailable");
|
|
409
|
+
}
|
|
410
|
+
const response = await __pagepocketOriginalFetch(resourcesPathUrl);
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
throw new Error("Failed to load resources_path.json");
|
|
413
|
+
}
|
|
414
|
+
const json = await response.json();
|
|
415
|
+
if (!json || json.version !== "1.0" || !Array.isArray(json.items)) {
|
|
416
|
+
throw new Error("Invalid resources_path.json");
|
|
417
|
+
}
|
|
418
|
+
return json;
|
|
419
|
+
} catch {
|
|
420
|
+
return { version: "1.0", createdAt: 0, items: [] };
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
157
424
|
const loadApiSnapshot = async () => {
|
|
158
425
|
try {
|
|
159
426
|
if (!__pagepocketOriginalFetch) {
|
|
@@ -196,6 +463,8 @@ const buildReplayScript = (apiPath, baseUrl) => {
|
|
|
196
463
|
let records = [];
|
|
197
464
|
const byKey = new Map();
|
|
198
465
|
|
|
466
|
+
let resourceIndex = null;
|
|
467
|
+
|
|
199
468
|
const normalizeUrl = (input) => {
|
|
200
469
|
try { return new URL(input, baseUrl).toString(); } catch { return input; }
|
|
201
470
|
};
|
|
@@ -226,7 +495,10 @@ const buildReplayScript = (apiPath, baseUrl) => {
|
|
|
226
495
|
};
|
|
227
496
|
|
|
228
497
|
const ready = (async () => {
|
|
229
|
-
const
|
|
498
|
+
const [apiSnapshot, resourcesPath] = await Promise.all([loadApiSnapshot(), loadResourcesPath()]);
|
|
499
|
+
resourceIndex = __pagepocketResourceProxy.buildIndex(resourcesPath || {});
|
|
500
|
+
|
|
501
|
+
const snapshot = apiSnapshot || {};
|
|
230
502
|
primeLookups(snapshot);
|
|
231
503
|
return snapshot;
|
|
232
504
|
})();
|
|
@@ -253,7 +525,19 @@ const buildReplayScript = (apiPath, baseUrl) => {
|
|
|
253
525
|
return record;
|
|
254
526
|
};
|
|
255
527
|
|
|
256
|
-
const findLocalPath = () =>
|
|
528
|
+
const findLocalPath = (url) => {
|
|
529
|
+
try {
|
|
530
|
+
if (!url || isLocalResource(String(url))) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (!resourceIndex) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
return __pagepocketResourceProxy.resolveToLocalPath(String(url), baseUrl, resourceIndex);
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
257
541
|
|
|
258
542
|
const defineProp = (obj, key, value) => {
|
|
259
543
|
try {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResourceType } from "./types";
|
|
2
|
+
export type ResourcesPathSnapshotItem = {
|
|
3
|
+
url: string;
|
|
4
|
+
path: string;
|
|
5
|
+
resourceType?: ResourceType;
|
|
6
|
+
mimeType?: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
};
|
|
9
|
+
export type ResourcesPathSnapshot = {
|
|
10
|
+
version: "1.0";
|
|
11
|
+
createdAt: number;
|
|
12
|
+
items: ResourcesPathSnapshotItem[];
|
|
13
|
+
};
|
|
14
|
+
type IndexedItem = ResourcesPathSnapshotItem & {
|
|
15
|
+
parsed: URL;
|
|
16
|
+
pathname: string;
|
|
17
|
+
pathnameWithSearch: string;
|
|
18
|
+
basename: string;
|
|
19
|
+
};
|
|
20
|
+
type MultiMap<K, V> = Map<K, V | V[]>;
|
|
21
|
+
export type ResourceProxyIndex = {
|
|
22
|
+
byExactUrl: Map<string, IndexedItem>;
|
|
23
|
+
byPathnameWithSearch: MultiMap<string, IndexedItem>;
|
|
24
|
+
byPathname: MultiMap<string, IndexedItem>;
|
|
25
|
+
byBasename: MultiMap<string, IndexedItem>;
|
|
26
|
+
};
|
|
27
|
+
export declare const buildResourceProxyIndex: (snapshot: ResourcesPathSnapshot) => ResourceProxyIndex;
|
|
28
|
+
type ResolveOptions = {
|
|
29
|
+
requestUrl: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
index: ResourceProxyIndex;
|
|
32
|
+
};
|
|
33
|
+
export declare const resolveToLocalPath: (options: ResolveOptions) => string | undefined;
|
|
34
|
+
export {};
|