@pagepocket/lib 0.4.2 → 0.5.1
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 +66 -17
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +29 -0
- package/dist/content-store.d.ts +21 -0
- package/dist/content-store.js +96 -0
- package/dist/css-rewrite.d.ts +3 -4
- package/dist/css-rewrite.js +48 -49
- package/dist/hack-html.d.ts +1 -1
- package/dist/hack-html.js +1 -1
- package/dist/hackers/replay-xhr.js +26 -10
- package/dist/index.d.ts +8 -3
- package/dist/index.js +19 -5
- package/dist/network-store.d.ts +51 -0
- package/dist/network-store.js +159 -0
- package/dist/pagepocket.d.ts +6 -20
- package/dist/pagepocket.js +96 -70
- package/dist/path-resolver.d.ts +5 -0
- package/dist/path-resolver.js +92 -0
- package/dist/replay-script.d.ts +11 -1
- package/dist/replay-script.js +156 -173
- package/dist/resource-filter.d.ts +2 -0
- package/dist/resource-filter.js +34 -0
- package/dist/rewrite-links.d.ts +12 -14
- package/dist/rewrite-links.js +185 -197
- package/dist/snapshot-builder.d.ts +15 -0
- package/dist/snapshot-builder.js +275 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +12 -0
- package/dist/types.d.ts +181 -38
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +109 -0
- package/dist/writers.d.ts +3 -0
- package/dist/writers.js +175 -0
- package/package.json +2 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSnapshot = void 0;
|
|
4
|
+
const path_resolver_1 = require("./path-resolver");
|
|
5
|
+
const snapshot_1 = require("./snapshot");
|
|
6
|
+
const css_rewrite_1 = require("./css-rewrite");
|
|
7
|
+
const rewrite_links_1 = require("./rewrite-links");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
const streamToUint8Array = async (stream) => {
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let total = 0;
|
|
13
|
+
while (true) {
|
|
14
|
+
const result = await reader.read();
|
|
15
|
+
if (result.done)
|
|
16
|
+
break;
|
|
17
|
+
if (result.value) {
|
|
18
|
+
chunks.push(result.value);
|
|
19
|
+
total += result.value.byteLength;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const output = new Uint8Array(total);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
for (const chunk of chunks) {
|
|
25
|
+
output.set(chunk, offset);
|
|
26
|
+
offset += chunk.byteLength;
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
};
|
|
30
|
+
const docDirFromUrl = (url) => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(url);
|
|
33
|
+
const clean = (0, utils_1.sanitizePosixPath)(parsed.pathname || "");
|
|
34
|
+
if (!clean) {
|
|
35
|
+
return "root";
|
|
36
|
+
}
|
|
37
|
+
return clean;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "root";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const groupResources = (input) => {
|
|
44
|
+
const documents = input.resources.filter((resource) => resource.request.resourceType === "document");
|
|
45
|
+
const hasFrameId = input.resources.some((resource) => !!resource.request.frameId);
|
|
46
|
+
const primaryDoc = documents.find((doc) => doc.request.url === input.entryUrl) ?? documents[0];
|
|
47
|
+
if (!hasFrameId) {
|
|
48
|
+
if (documents.length > 1) {
|
|
49
|
+
input.warnings.push("Multiple documents captured without frameId; using the first document.");
|
|
50
|
+
}
|
|
51
|
+
const primaryGroup = {
|
|
52
|
+
id: primaryDoc?.request.requestId ?? "root",
|
|
53
|
+
url: primaryDoc?.request.url ?? input.entryUrl,
|
|
54
|
+
resources: [],
|
|
55
|
+
apiEntries: []
|
|
56
|
+
};
|
|
57
|
+
for (const resource of input.resources) {
|
|
58
|
+
if (resource.request.resourceType === "document" && resource !== primaryDoc) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
primaryGroup.resources.push(resource);
|
|
62
|
+
if (resource.request.resourceType === "document") {
|
|
63
|
+
primaryGroup.docResource = resource;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const apiEntry of input.apiEntries) {
|
|
67
|
+
primaryGroup.apiEntries.push(apiEntry);
|
|
68
|
+
}
|
|
69
|
+
return [primaryGroup];
|
|
70
|
+
}
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const doc of documents) {
|
|
73
|
+
const id = doc.request.frameId ?? doc.request.requestId;
|
|
74
|
+
groups.set(id, {
|
|
75
|
+
id,
|
|
76
|
+
url: doc.request.url,
|
|
77
|
+
resources: [doc],
|
|
78
|
+
apiEntries: [],
|
|
79
|
+
docResource: doc
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const primaryGroup = primaryDoc
|
|
83
|
+
? groups.get(primaryDoc.request.frameId ?? primaryDoc.request.requestId) ?? null
|
|
84
|
+
: null;
|
|
85
|
+
const groupByUrl = new Map();
|
|
86
|
+
for (const group of groups.values()) {
|
|
87
|
+
groupByUrl.set(group.url, group);
|
|
88
|
+
}
|
|
89
|
+
for (const resource of input.resources) {
|
|
90
|
+
if (resource.request.resourceType === "document") {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const frameId = resource.request.frameId;
|
|
94
|
+
const byFrame = frameId ? groups.get(frameId) : undefined;
|
|
95
|
+
const byInitiator = resource.request.initiator?.url ? groupByUrl.get(resource.request.initiator.url) : undefined;
|
|
96
|
+
const target = byFrame ?? byInitiator ?? primaryGroup ?? Array.from(groups.values())[0];
|
|
97
|
+
if (target) {
|
|
98
|
+
target.resources.push(resource);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const entry of input.apiEntries) {
|
|
102
|
+
const frameId = entry.request.frameId;
|
|
103
|
+
const byFrame = frameId ? groups.get(frameId) : undefined;
|
|
104
|
+
const byInitiator = entry.request.initiator?.url ? groupByUrl.get(entry.request.initiator.url) : undefined;
|
|
105
|
+
const target = byFrame ?? byInitiator ?? primaryGroup ?? Array.from(groups.values())[0];
|
|
106
|
+
if (target) {
|
|
107
|
+
target.apiEntries.push(entry);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Array.from(groups.values());
|
|
111
|
+
};
|
|
112
|
+
const buildApiSnapshot = (url, createdAt, entries) => ({
|
|
113
|
+
version: "1.0",
|
|
114
|
+
url,
|
|
115
|
+
createdAt,
|
|
116
|
+
records: entries.map((entry) => entry.record)
|
|
117
|
+
});
|
|
118
|
+
const buildSnapshot = async (input) => {
|
|
119
|
+
const warnings = input.warnings;
|
|
120
|
+
const groups = groupResources({
|
|
121
|
+
entryUrl: input.entryUrl,
|
|
122
|
+
resources: input.resources,
|
|
123
|
+
apiEntries: input.apiEntries,
|
|
124
|
+
warnings
|
|
125
|
+
});
|
|
126
|
+
const multiDoc = groups.length > 1;
|
|
127
|
+
const files = [];
|
|
128
|
+
let entryPath = "";
|
|
129
|
+
let title;
|
|
130
|
+
for (const group of groups) {
|
|
131
|
+
const docDir = multiDoc ? docDirFromUrl(group.url) : "";
|
|
132
|
+
const baseResolver = input.pathResolver ?? (0, path_resolver_1.createDefaultPathResolver)();
|
|
133
|
+
const resolver = multiDoc ? (0, path_resolver_1.withPrefixPathResolver)(baseResolver, docDir) : baseResolver;
|
|
134
|
+
const urlToPath = new Map();
|
|
135
|
+
for (const resource of group.resources) {
|
|
136
|
+
const path = resolver.resolve({
|
|
137
|
+
url: resource.request.url,
|
|
138
|
+
resourceType: resource.request.resourceType,
|
|
139
|
+
mimeType: resource.mimeType,
|
|
140
|
+
suggestedFilename: undefined,
|
|
141
|
+
isCrossOrigin: (0, path_resolver_1.resolveCrossOrigin)(resource.request.url, group.url),
|
|
142
|
+
entryUrl: group.url
|
|
143
|
+
});
|
|
144
|
+
urlToPath.set(resource.request.url, path);
|
|
145
|
+
}
|
|
146
|
+
const resolve = (absoluteUrl) => urlToPath.get(absoluteUrl) ?? null;
|
|
147
|
+
const apiPath = (0, utils_1.ensureLeadingSlash)(multiDoc ? `${(0, utils_1.sanitizePosixPath)(docDir)}/api.json` : "/api.json");
|
|
148
|
+
for (const resource of group.resources) {
|
|
149
|
+
if (resource.request.resourceType === "document") {
|
|
150
|
+
const path = urlToPath.get(resource.request.url) ?? "/index.html";
|
|
151
|
+
const stream = await input.contentStore.open(resource.contentRef);
|
|
152
|
+
const bytes = await streamToUint8Array(stream);
|
|
153
|
+
const decoded = (0, utils_1.decodeUtf8)(bytes) ?? "";
|
|
154
|
+
let html = decoded;
|
|
155
|
+
const rewritten = await (0, rewrite_links_1.rewriteEntryHtml)({
|
|
156
|
+
html,
|
|
157
|
+
entryUrl: group.url,
|
|
158
|
+
apiPath,
|
|
159
|
+
resolve,
|
|
160
|
+
rewriteLinks: input.rewriteEntry
|
|
161
|
+
});
|
|
162
|
+
html = rewritten.html;
|
|
163
|
+
if (!title) {
|
|
164
|
+
title = rewritten.title;
|
|
165
|
+
}
|
|
166
|
+
const encoded = new TextEncoder().encode(html);
|
|
167
|
+
const contentRef = await input.contentStore.put({ kind: "buffer", data: encoded }, { url: resource.request.url, mimeType: resource.mimeType, sizeHint: encoded.byteLength });
|
|
168
|
+
files.push({
|
|
169
|
+
path,
|
|
170
|
+
mimeType: resource.mimeType ?? "text/html",
|
|
171
|
+
size: encoded.byteLength,
|
|
172
|
+
source: contentRef,
|
|
173
|
+
originalUrl: resource.request.url,
|
|
174
|
+
resourceType: resource.request.resourceType,
|
|
175
|
+
headers: resource.response.headers
|
|
176
|
+
});
|
|
177
|
+
if (resource.request.url === input.entryUrl || !entryPath) {
|
|
178
|
+
entryPath = path;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let contentRef = resource.contentRef;
|
|
183
|
+
let size = resource.size;
|
|
184
|
+
if (resource.request.resourceType === "stylesheet" && input.rewriteCSS) {
|
|
185
|
+
const stream = await input.contentStore.open(resource.contentRef);
|
|
186
|
+
const bytes = await streamToUint8Array(stream);
|
|
187
|
+
const decoded = (0, utils_1.decodeUtf8)(bytes);
|
|
188
|
+
if (decoded !== null) {
|
|
189
|
+
const rewritten = await (0, css_rewrite_1.rewriteCssText)({
|
|
190
|
+
cssText: decoded,
|
|
191
|
+
cssUrl: resource.request.url,
|
|
192
|
+
resolveUrl: resolve
|
|
193
|
+
});
|
|
194
|
+
if (rewritten !== decoded) {
|
|
195
|
+
const encoded = new TextEncoder().encode(rewritten);
|
|
196
|
+
contentRef = await input.contentStore.put({ kind: "buffer", data: encoded }, {
|
|
197
|
+
url: resource.request.url,
|
|
198
|
+
mimeType: resource.mimeType,
|
|
199
|
+
sizeHint: encoded.byteLength
|
|
200
|
+
});
|
|
201
|
+
size = encoded.byteLength;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (resource.request.resourceType === "script") {
|
|
206
|
+
const stream = await input.contentStore.open(contentRef);
|
|
207
|
+
const bytes = await streamToUint8Array(stream);
|
|
208
|
+
const decoded = (0, utils_1.decodeUtf8)(bytes);
|
|
209
|
+
if (decoded !== null) {
|
|
210
|
+
const rewritten = await (0, rewrite_links_1.rewriteJsText)(decoded, resolve, resource.request.url);
|
|
211
|
+
if (rewritten !== decoded) {
|
|
212
|
+
const encoded = new TextEncoder().encode(rewritten);
|
|
213
|
+
contentRef = await input.contentStore.put({ kind: "buffer", data: encoded }, {
|
|
214
|
+
url: resource.request.url,
|
|
215
|
+
mimeType: resource.mimeType,
|
|
216
|
+
sizeHint: encoded.byteLength
|
|
217
|
+
});
|
|
218
|
+
size = encoded.byteLength;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const path = urlToPath.get(resource.request.url) ??
|
|
223
|
+
resolver.resolve({
|
|
224
|
+
url: resource.request.url,
|
|
225
|
+
resourceType: resource.request.resourceType,
|
|
226
|
+
mimeType: resource.mimeType,
|
|
227
|
+
suggestedFilename: undefined,
|
|
228
|
+
isCrossOrigin: (0, path_resolver_1.resolveCrossOrigin)(resource.request.url, group.url),
|
|
229
|
+
entryUrl: group.url
|
|
230
|
+
});
|
|
231
|
+
files.push({
|
|
232
|
+
path,
|
|
233
|
+
mimeType: resource.mimeType,
|
|
234
|
+
size,
|
|
235
|
+
source: contentRef,
|
|
236
|
+
originalUrl: resource.request.url,
|
|
237
|
+
resourceType: resource.request.resourceType,
|
|
238
|
+
headers: resource.response.headers
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const apiSnapshot = buildApiSnapshot(group.url, input.createdAt, group.apiEntries);
|
|
242
|
+
const apiBytes = new TextEncoder().encode(JSON.stringify(apiSnapshot, null, 2));
|
|
243
|
+
const apiRef = await input.contentStore.put({ kind: "buffer", data: apiBytes }, { url: apiPath, mimeType: "application/json", sizeHint: apiBytes.byteLength });
|
|
244
|
+
files.push({
|
|
245
|
+
path: apiPath,
|
|
246
|
+
mimeType: "application/json",
|
|
247
|
+
size: apiBytes.byteLength,
|
|
248
|
+
source: apiRef,
|
|
249
|
+
originalUrl: apiPath
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const totalBytes = files.reduce((sum, file) => sum + (file.size ?? 0), 0);
|
|
253
|
+
const totalFiles = files.length;
|
|
254
|
+
const snapshotUrl = input.entryUrl || groups[0]?.url || "";
|
|
255
|
+
return (0, snapshot_1.createPageSnapshot)({
|
|
256
|
+
version: "1.0",
|
|
257
|
+
createdAt: input.createdAt,
|
|
258
|
+
url: snapshotUrl,
|
|
259
|
+
title,
|
|
260
|
+
entry: entryPath || "/index.html",
|
|
261
|
+
files,
|
|
262
|
+
meta: {
|
|
263
|
+
totalBytes,
|
|
264
|
+
totalFiles,
|
|
265
|
+
warnings: warnings.length ? warnings : undefined
|
|
266
|
+
},
|
|
267
|
+
content: {
|
|
268
|
+
open: (ref) => input.contentStore.open(ref),
|
|
269
|
+
dispose: async () => {
|
|
270
|
+
await input.contentStore.dispose?.();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
exports.buildSnapshot = buildSnapshot;
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPageSnapshot = void 0;
|
|
4
|
+
const writers_1 = require("./writers");
|
|
5
|
+
const createPageSnapshot = (data) => {
|
|
6
|
+
return {
|
|
7
|
+
...data,
|
|
8
|
+
toDirectory: (outDir, options) => (0, writers_1.writeToFS)(data, outDir, options),
|
|
9
|
+
toZip: (options) => (0, writers_1.toZip)(data, options)
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
exports.createPageSnapshot = createPageSnapshot;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,20 +1,188 @@
|
|
|
1
|
-
export type
|
|
2
|
-
|
|
1
|
+
export type ResourceType = "document" | "stylesheet" | "script" | "image" | "font" | "media" | "xhr" | "fetch" | "other" | (string & {});
|
|
2
|
+
export type BodySource = {
|
|
3
|
+
kind: "buffer";
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "stream";
|
|
7
|
+
stream: ReadableStream<Uint8Array>;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "late";
|
|
10
|
+
read: () => Promise<Uint8Array>;
|
|
11
|
+
};
|
|
12
|
+
export interface NetworkRequestEvent {
|
|
13
|
+
type: "request";
|
|
14
|
+
requestId: string;
|
|
3
15
|
url: string;
|
|
4
16
|
method: string;
|
|
5
|
-
|
|
6
|
-
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
frameId?: string;
|
|
19
|
+
resourceType?: ResourceType;
|
|
20
|
+
initiator?: {
|
|
21
|
+
type?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
};
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
export interface NetworkResponseEvent {
|
|
27
|
+
type: "response";
|
|
28
|
+
requestId: string;
|
|
29
|
+
url: string;
|
|
30
|
+
status: number;
|
|
7
31
|
statusText?: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
mimeType?: string;
|
|
34
|
+
fromDiskCache?: boolean;
|
|
35
|
+
fromServiceWorker?: boolean;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
body?: BodySource;
|
|
38
|
+
}
|
|
39
|
+
export interface NetworkRequestFailedEvent {
|
|
40
|
+
type: "failed";
|
|
41
|
+
requestId: string;
|
|
42
|
+
url: string;
|
|
43
|
+
errorText: string;
|
|
11
44
|
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
export type NetworkEvent = NetworkRequestEvent | NetworkResponseEvent | NetworkRequestFailedEvent;
|
|
47
|
+
export interface NetworkEventHandlers {
|
|
48
|
+
onEvent(event: NetworkEvent): void;
|
|
49
|
+
onError?(error: Error): void;
|
|
50
|
+
onLog?(msg: string, meta?: unknown): void;
|
|
51
|
+
}
|
|
52
|
+
export interface InterceptorCapabilities {
|
|
53
|
+
canGetResponseBody: boolean;
|
|
54
|
+
canStreamResponseBody: boolean;
|
|
55
|
+
canGetRequestBody: boolean;
|
|
56
|
+
providesResourceType: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type InterceptTarget = {
|
|
59
|
+
kind: "url";
|
|
60
|
+
url: string;
|
|
61
|
+
} | {
|
|
62
|
+
kind: "puppeteer-page";
|
|
63
|
+
page: unknown;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "cdp-tab";
|
|
66
|
+
tabId: number;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "cdp-session";
|
|
69
|
+
session: unknown;
|
|
70
|
+
};
|
|
71
|
+
export type InterceptOptions = Record<string, unknown>;
|
|
72
|
+
export type NavigateOptions = Record<string, unknown>;
|
|
73
|
+
export interface InterceptSession {
|
|
74
|
+
navigate?(url: string, options?: NavigateOptions): Promise<void>;
|
|
75
|
+
stop(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export interface NetworkInterceptorAdapter {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly capabilities: InterceptorCapabilities;
|
|
80
|
+
start(target: InterceptTarget, handlers: NetworkEventHandlers, options?: InterceptOptions): Promise<InterceptSession>;
|
|
81
|
+
}
|
|
82
|
+
export interface PathResolver {
|
|
83
|
+
resolve(input: {
|
|
84
|
+
url: string;
|
|
85
|
+
resourceType?: ResourceType;
|
|
86
|
+
mimeType?: string;
|
|
87
|
+
suggestedFilename?: string;
|
|
88
|
+
isCrossOrigin: boolean;
|
|
89
|
+
entryUrl: string;
|
|
90
|
+
}): string;
|
|
91
|
+
}
|
|
92
|
+
export interface ResourceFilter {
|
|
93
|
+
shouldSave(req: NetworkRequestEvent, res?: NetworkResponseEvent): boolean;
|
|
94
|
+
}
|
|
95
|
+
export type ContentRef = {
|
|
96
|
+
kind: "memory";
|
|
97
|
+
data: Uint8Array;
|
|
98
|
+
} | {
|
|
99
|
+
kind: "store-ref";
|
|
100
|
+
id: string;
|
|
12
101
|
};
|
|
13
|
-
export
|
|
102
|
+
export interface ContentStore {
|
|
103
|
+
name: string;
|
|
104
|
+
put(body: BodySource, meta: {
|
|
105
|
+
url: string;
|
|
106
|
+
mimeType?: string;
|
|
107
|
+
sizeHint?: number;
|
|
108
|
+
}): Promise<ContentRef>;
|
|
109
|
+
open(ref: ContentRef): Promise<ReadableStream<Uint8Array>>;
|
|
110
|
+
dispose?(): Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
export interface ContentStoreHandle {
|
|
113
|
+
open(ref: ContentRef): Promise<ReadableStream<Uint8Array>>;
|
|
114
|
+
dispose?(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
export interface CompletionContext {
|
|
117
|
+
now(): number;
|
|
118
|
+
getStats(): {
|
|
119
|
+
inflightRequests: number;
|
|
120
|
+
lastNetworkTs: number;
|
|
121
|
+
totalRequests: number;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export interface CompletionStrategy {
|
|
125
|
+
wait(ctx: CompletionContext): Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
export interface PagePocketOptions {
|
|
128
|
+
}
|
|
129
|
+
export interface CaptureOptions {
|
|
130
|
+
interceptor: NetworkInterceptorAdapter;
|
|
131
|
+
completion?: CompletionStrategy | CompletionStrategy[];
|
|
132
|
+
filter?: ResourceFilter;
|
|
133
|
+
pathResolver?: PathResolver;
|
|
134
|
+
contentStore?: ContentStore;
|
|
135
|
+
rewriteEntry?: boolean;
|
|
136
|
+
rewriteCSS?: boolean;
|
|
137
|
+
limits?: {
|
|
138
|
+
maxTotalBytes?: number;
|
|
139
|
+
maxSingleResourceBytes?: number;
|
|
140
|
+
maxResources?: number;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export interface SnapshotFile {
|
|
144
|
+
path: string;
|
|
145
|
+
mimeType?: string;
|
|
146
|
+
size?: number;
|
|
147
|
+
source: ContentRef;
|
|
148
|
+
originalUrl?: string;
|
|
149
|
+
resourceType?: ResourceType;
|
|
150
|
+
headers?: Record<string, string>;
|
|
151
|
+
}
|
|
152
|
+
export interface PageSnapshot {
|
|
153
|
+
version: "1.0";
|
|
154
|
+
createdAt: number;
|
|
155
|
+
url: string;
|
|
156
|
+
title?: string;
|
|
157
|
+
entry: string;
|
|
158
|
+
files: SnapshotFile[];
|
|
159
|
+
meta?: {
|
|
160
|
+
totalBytes?: number;
|
|
161
|
+
totalFiles?: number;
|
|
162
|
+
warnings?: string[];
|
|
163
|
+
};
|
|
164
|
+
content: ContentStoreHandle;
|
|
165
|
+
toDirectory(outDir: string, options?: WriteFSOptions): Promise<WriteResult>;
|
|
166
|
+
toZip(options?: ZipOptions): Promise<Uint8Array | Blob>;
|
|
167
|
+
}
|
|
168
|
+
export interface WriteFSOptions {
|
|
169
|
+
clearCache?: boolean;
|
|
170
|
+
}
|
|
171
|
+
export interface WriteResult {
|
|
172
|
+
filesWritten: number;
|
|
173
|
+
totalBytes: number;
|
|
174
|
+
}
|
|
175
|
+
export interface ZipOptions {
|
|
176
|
+
asBlob?: boolean;
|
|
177
|
+
clearCache?: boolean;
|
|
178
|
+
}
|
|
179
|
+
export interface ApiRecord {
|
|
14
180
|
url: string;
|
|
15
181
|
method: string;
|
|
16
182
|
requestHeaders?: Record<string, string>;
|
|
17
183
|
requestBody?: string;
|
|
184
|
+
requestBodyBase64?: string;
|
|
185
|
+
requestEncoding?: "text" | "base64";
|
|
18
186
|
status?: number;
|
|
19
187
|
statusText?: string;
|
|
20
188
|
responseHeaders?: Record<string, string>;
|
|
@@ -23,35 +191,10 @@ export type NetworkRecord = {
|
|
|
23
191
|
responseEncoding?: "text" | "base64";
|
|
24
192
|
error?: string;
|
|
25
193
|
timestamp: number;
|
|
26
|
-
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
statusText: string;
|
|
30
|
-
headers: Record<string, string>;
|
|
31
|
-
body: string;
|
|
32
|
-
bodyEncoding: "text" | "base64";
|
|
33
|
-
};
|
|
34
|
-
export type CapturedNetworkRecord = {
|
|
35
|
-
url: string;
|
|
36
|
-
source?: string;
|
|
37
|
-
method: string;
|
|
38
|
-
timestamp: number;
|
|
39
|
-
response?: CapturedResponseRecord;
|
|
40
|
-
error?: string;
|
|
41
|
-
};
|
|
42
|
-
export type SnapshotData = {
|
|
194
|
+
}
|
|
195
|
+
export interface ApiSnapshot {
|
|
196
|
+
version: "1.0";
|
|
43
197
|
url: string;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fetchXhrRecords: FetchRecord[];
|
|
47
|
-
networkRecords: CapturedNetworkRecord[];
|
|
48
|
-
resources: Array<{
|
|
49
|
-
url: string;
|
|
50
|
-
localPath: string;
|
|
51
|
-
contentType?: string | null;
|
|
52
|
-
size?: number;
|
|
53
|
-
}>;
|
|
54
|
-
};
|
|
55
|
-
export interface NetworkInterceptorAdapter {
|
|
56
|
-
run(url: string): Promise<SnapshotData>;
|
|
198
|
+
createdAt: number;
|
|
199
|
+
records: ApiRecord[];
|
|
57
200
|
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BodySource } from "./types";
|
|
2
|
+
export declare const sleep: (ms: number) => Promise<unknown>;
|
|
3
|
+
export declare const hashString: (value: string) => string;
|
|
4
|
+
export declare const stripLeadingSlash: (value: string) => string;
|
|
5
|
+
export declare const ensureLeadingSlash: (value: string) => string;
|
|
6
|
+
export declare const sanitizePosixPath: (value: string) => string;
|
|
7
|
+
export declare const bytesToBase64: (bytes: Uint8Array) => string;
|
|
8
|
+
export declare const decodeUtf8: (bytes: Uint8Array) => string | null;
|
|
9
|
+
export declare const isUtf8Text: (bytes: Uint8Array, mimeType?: string) => boolean;
|
|
10
|
+
export declare const toUint8Array: (body: BodySource) => Promise<Uint8Array>;
|
|
11
|
+
export declare const bodyToTextOrBase64: (bytes: Uint8Array, mimeType?: string) => {
|
|
12
|
+
encoding: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
base64?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
encoding: "base64";
|
|
17
|
+
base64: string;
|
|
18
|
+
text?: undefined;
|
|
19
|
+
};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bodyToTextOrBase64 = exports.toUint8Array = exports.isUtf8Text = exports.decodeUtf8 = exports.bytesToBase64 = exports.sanitizePosixPath = exports.ensureLeadingSlash = exports.stripLeadingSlash = exports.hashString = exports.sleep = void 0;
|
|
4
|
+
const content_type_1 = require("./content-type");
|
|
5
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
6
|
+
const FNV_PRIME = 0x01000193;
|
|
7
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
exports.sleep = sleep;
|
|
9
|
+
const hashString = (value) => {
|
|
10
|
+
let hash = FNV_OFFSET;
|
|
11
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
12
|
+
hash ^= value.charCodeAt(i);
|
|
13
|
+
hash = (hash * FNV_PRIME) >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return hash.toString(16).padStart(8, "0");
|
|
16
|
+
};
|
|
17
|
+
exports.hashString = hashString;
|
|
18
|
+
const stripLeadingSlash = (value) => value.replace(/^\/+/, "");
|
|
19
|
+
exports.stripLeadingSlash = stripLeadingSlash;
|
|
20
|
+
const ensureLeadingSlash = (value) => value.startsWith("/") ? value : `/${value}`;
|
|
21
|
+
exports.ensureLeadingSlash = ensureLeadingSlash;
|
|
22
|
+
const sanitizePosixPath = (value) => {
|
|
23
|
+
const parts = value.split("/").filter(Boolean);
|
|
24
|
+
const clean = [];
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
if (part === ".")
|
|
27
|
+
continue;
|
|
28
|
+
if (part === "..")
|
|
29
|
+
continue;
|
|
30
|
+
clean.push(part);
|
|
31
|
+
}
|
|
32
|
+
return clean.join("/");
|
|
33
|
+
};
|
|
34
|
+
exports.sanitizePosixPath = sanitizePosixPath;
|
|
35
|
+
const getGlobalBuffer = () => {
|
|
36
|
+
return globalThis.Buffer;
|
|
37
|
+
};
|
|
38
|
+
const bytesToBase64 = (bytes) => {
|
|
39
|
+
const BufferCtor = getGlobalBuffer();
|
|
40
|
+
if (BufferCtor) {
|
|
41
|
+
return BufferCtor.from(bytes).toString("base64");
|
|
42
|
+
}
|
|
43
|
+
let binary = "";
|
|
44
|
+
const chunkSize = 0x8000;
|
|
45
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
46
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
47
|
+
binary += String.fromCharCode(...chunk);
|
|
48
|
+
}
|
|
49
|
+
return btoa(binary);
|
|
50
|
+
};
|
|
51
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
52
|
+
const decodeUtf8 = (bytes) => {
|
|
53
|
+
try {
|
|
54
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
55
|
+
return decoder.decode(bytes);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.decodeUtf8 = decodeUtf8;
|
|
62
|
+
const isUtf8Text = (bytes, mimeType) => {
|
|
63
|
+
const decoded = (0, exports.decodeUtf8)(bytes);
|
|
64
|
+
if (decoded === null) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (mimeType) {
|
|
68
|
+
return (0, content_type_1.isTextResponse)(mimeType);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
exports.isUtf8Text = isUtf8Text;
|
|
73
|
+
const toUint8Array = async (body) => {
|
|
74
|
+
if (body.kind === "buffer") {
|
|
75
|
+
return body.data;
|
|
76
|
+
}
|
|
77
|
+
if (body.kind === "late") {
|
|
78
|
+
return body.read();
|
|
79
|
+
}
|
|
80
|
+
const reader = body.stream.getReader();
|
|
81
|
+
const chunks = [];
|
|
82
|
+
let total = 0;
|
|
83
|
+
while (true) {
|
|
84
|
+
const result = await reader.read();
|
|
85
|
+
if (result.done)
|
|
86
|
+
break;
|
|
87
|
+
const value = result.value;
|
|
88
|
+
if (value) {
|
|
89
|
+
chunks.push(value);
|
|
90
|
+
total += value.byteLength;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const output = new Uint8Array(total);
|
|
94
|
+
let offset = 0;
|
|
95
|
+
for (const chunk of chunks) {
|
|
96
|
+
output.set(chunk, offset);
|
|
97
|
+
offset += chunk.byteLength;
|
|
98
|
+
}
|
|
99
|
+
return output;
|
|
100
|
+
};
|
|
101
|
+
exports.toUint8Array = toUint8Array;
|
|
102
|
+
const bodyToTextOrBase64 = (bytes, mimeType) => {
|
|
103
|
+
if ((0, exports.isUtf8Text)(bytes, mimeType)) {
|
|
104
|
+
const text = (0, exports.decodeUtf8)(bytes) ?? "";
|
|
105
|
+
return { encoding: "text", text };
|
|
106
|
+
}
|
|
107
|
+
return { encoding: "base64", base64: (0, exports.bytesToBase64)(bytes) };
|
|
108
|
+
};
|
|
109
|
+
exports.bodyToTextOrBase64 = bodyToTextOrBase64;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PageSnapshot, WriteFSOptions, WriteResult, ZipOptions } from "./types";
|
|
2
|
+
export declare const writeToFS: (snapshot: PageSnapshot, outDir: string, options?: WriteFSOptions) => Promise<WriteResult>;
|
|
3
|
+
export declare const toZip: (snapshot: PageSnapshot, options?: ZipOptions) => Promise<Uint8Array | Blob>;
|