@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.
@@ -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;
@@ -0,0 +1,2 @@
1
+ import type { PageSnapshot } from "./types";
2
+ export declare const createPageSnapshot: (data: Omit<PageSnapshot, "toDirectory" | "toZip">) => PageSnapshot;
@@ -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 FetchRecord = {
2
- kind: "fetch" | "xhr";
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
- requestBody?: string;
6
- status?: number;
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
- responseHeaders?: Record<string, string>;
9
- responseBody?: string;
10
- error?: string;
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 type NetworkRecord = {
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 type CapturedResponseRecord = {
28
- status: number;
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
- title: string;
45
- capturedAt: string;
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
  }
@@ -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>;