zip-peek 0.1.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 +392 -0
- package/dist/ZipStreamManager.d.ts +19 -0
- package/dist/ZipStreamManager.js +181 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +31 -0
- package/dist/registerZipServiceWorker.d.ts +12 -0
- package/dist/registerZipServiceWorker.js +87 -0
- package/dist/zip-utils.d.ts +28 -0
- package/dist/zip-utils.js +100 -0
- package/dist/zipServiceWorker.js +2 -0
- package/dist/zipServiceWorker.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# zip-peek
|
|
2
|
+
|
|
3
|
+
`zip-peek` lets browser applications load files from a remote `.zip` archive by only requesting the needed range of bytes without downloading and extracting the full ZIP file.
|
|
4
|
+
|
|
5
|
+
It registers a service worker that intercepts requests like:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
https://cdn.example.com/packages/session.zip/path/to/asset.png
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The service worker fetches only the byte range needed for the requested ZIP entry, inflates the file when required, caches the extracted response, and returns it to the browser as a normal asset response.
|
|
12
|
+
|
|
13
|
+
## When Is This Package Useful?
|
|
14
|
+
|
|
15
|
+
Use this package when your app receives a base path and later appends folders and file names to fetch assets from that path.
|
|
16
|
+
|
|
17
|
+
For example, your app may normally treat the base path as a directory:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const basePath = 'https://cdn.example.com/packages/session';
|
|
21
|
+
const imageUrl = `${basePath}/slides/slide-1/image.png`;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If the same assets are sometimes delivered as a ZIP file instead:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const basePath = 'https://cdn.example.com/packages/session.zip';
|
|
28
|
+
const imageUrl = `${basePath}/slides/slide-1/image.png`;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`zip-peek` lets the app keep using the same URL-building pattern. The app does not need to know whether the base path points to a normal directory or a ZIP file, and the ZIP-specific work stays inside the service worker.
|
|
32
|
+
|
|
33
|
+
## What It Does
|
|
34
|
+
|
|
35
|
+
The package provides two pieces:
|
|
36
|
+
|
|
37
|
+
- A browser API, `initZipPeek()`, used by your application.
|
|
38
|
+
- A standalone service worker file, `zipServiceWorker.js`, that must be copied into your app's public output and served by your app. You will find a webpack sample below.
|
|
39
|
+
|
|
40
|
+
At runtime, the package:
|
|
41
|
+
|
|
42
|
+
1. Checks whether the provided `zipUrl` points to a `.zip` file.
|
|
43
|
+
2. Registers the ZIP service worker with the configured `workerUrl` and `scopeUrl`.
|
|
44
|
+
3. Reloads once after first service worker install when needed, so the page becomes controlled.
|
|
45
|
+
4. Reads the ZIP central directory using HTTP range requests.
|
|
46
|
+
5. Sends the parsed ZIP manifest to the service worker.
|
|
47
|
+
6. Intercepts asset requests under the ZIP URL.
|
|
48
|
+
7. Fetches only the byte range needed for the requested file.
|
|
49
|
+
8. Inflates deflated files using `fflate`.
|
|
50
|
+
9. Returns the extracted file bytes to the browser request as a normal `Response`.
|
|
51
|
+
10. Caches extracted assets in the browser Cache API.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pnpm add zip-peek
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npm install zip-peek
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Basic Usage
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { initZipPeek } from 'zip-peek';
|
|
69
|
+
|
|
70
|
+
const zipPeekInit = await initZipPeek({
|
|
71
|
+
zipUrl: 'https://cdn.example.com/packages/zipfile.zip',
|
|
72
|
+
workerUrl: new URL('/zipServiceWorker.js', window.location.origin).href,
|
|
73
|
+
scopeUrl: new URL('/', window.location.origin).href,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (zipPeekInit.reloaded) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
After initialization, your app can continue building asset URLs under the ZIP URL:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const zipUrl = 'https://cdn.example.com/packages/zipfile.zip';
|
|
85
|
+
const imgUrl = `${zipUrl}/slides/slide-1/image.png`;
|
|
86
|
+
|
|
87
|
+
// This request will work normally and zip-peek will do the magic in the background:
|
|
88
|
+
fetch(`${imgUrl}`);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The browser requests that URL normally. The service worker intercepts it and serves `slides/slide-1/image.png` from inside the ZIP.
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### `initZipPeek(options)`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
type InitZipPeekOptions = {
|
|
99
|
+
zipUrl: string;
|
|
100
|
+
workerUrl: string;
|
|
101
|
+
scopeUrl: string;
|
|
102
|
+
reloadOnFirstInstall?: boolean;
|
|
103
|
+
purgeOtherZipAssets?: boolean;
|
|
104
|
+
zipAssetCacheName?: string;
|
|
105
|
+
assetCacheTtlMs?: number;
|
|
106
|
+
logPrefix?: string;
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
type InitZipPeekResult = {
|
|
114
|
+
reloaded: boolean;
|
|
115
|
+
initialized: boolean;
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Options
|
|
120
|
+
|
|
121
|
+
`zipUrl`
|
|
122
|
+
|
|
123
|
+
The URL of the ZIP package. This can be absolute or relative, but it must point to a path ending in `.zip`. It can also be a presigned url.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
zipUrl: 'https://cdn.example.com/packages/zipfile.zip'
|
|
129
|
+
zipUrl: 'https://d3jfe.cloudfront.net/somedir/zipfile.zip?Expires=1798761599&Signature=K2CJW4Z7B3DCM4&Key-Pair-Id=K2CJW4Z7B3DCM4'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`workerUrl`
|
|
133
|
+
|
|
134
|
+
The URL where the browser can download the service worker JavaScript file. The service worker file must be served from the same origin as the page.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
workerUrl: new URL('./zipServiceWorker.js', window.location.href).href
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In production, you will usually prefer hashed file to give it a long CacheControl duration:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
zipServiceWorker.a1b2c3d4.js
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`scopeUrl`
|
|
149
|
+
|
|
150
|
+
The service worker scope. The worker can only intercept requests inside this scope.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
scopeUrl: new URL('/', window.location.origin).href
|
|
156
|
+
scopeUrl: new URL('/app/', window.location.origin).href
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Choose the smallest scope that includes the pages and asset requests you want the service worker to intercept. For local development, the scope often differs from production because the app may be served from a different path.
|
|
160
|
+
|
|
161
|
+
`reloadOnFirstInstall`
|
|
162
|
+
|
|
163
|
+
Defaults to `true`. When a service worker is installed for the first time, the current page may not yet be controlled by it. With this option enabled, the package reloads once and returns `{ reloaded: true, initialized: false }`.
|
|
164
|
+
|
|
165
|
+
`purgeOtherZipAssets`
|
|
166
|
+
|
|
167
|
+
Defaults to `true`. Removes cached assets and manifests for other ZIP URLs from this package's Cache API bucket.
|
|
168
|
+
|
|
169
|
+
`zipAssetCacheName`
|
|
170
|
+
|
|
171
|
+
Overrides the browser Cache API bucket used by the service worker for ZIP manifests and extracted assets.
|
|
172
|
+
|
|
173
|
+
Default:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
zip-cache-v1
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`assetCacheTtlMs`
|
|
180
|
+
|
|
181
|
+
Controls how long extracted assets remain valid in the Cache API.
|
|
182
|
+
|
|
183
|
+
Default:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
2 * 60 * 60 * 1000
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`logPrefix`
|
|
190
|
+
|
|
191
|
+
Changes the service worker log prefix.
|
|
192
|
+
|
|
193
|
+
Default:
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
[zipSW]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Required Server Configuration
|
|
200
|
+
|
|
201
|
+
### 1. The ZIP server must support range requests
|
|
202
|
+
|
|
203
|
+
The ZIP package URL must support HTTP byte range requests. The package reads the ZIP central directory and individual files using `Range` headers.
|
|
204
|
+
|
|
205
|
+
The ZIP server should return:
|
|
206
|
+
|
|
207
|
+
```http
|
|
208
|
+
Accept-Ranges: bytes
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
For range requests, it must return:
|
|
212
|
+
|
|
213
|
+
```http
|
|
214
|
+
HTTP/1.1 206 Partial Content
|
|
215
|
+
Content-Range: bytes start-end/total
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
If the ZIP server does not support range requests, zip-peek cannot stream individual files.
|
|
219
|
+
|
|
220
|
+
### 2. The service worker file must be same-origin
|
|
221
|
+
|
|
222
|
+
Browsers require service worker scripts to be served from the same origin as the page.
|
|
223
|
+
|
|
224
|
+
Valid when the page is also served from `https://app.example.com`:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
https://app.example.com/zipServiceWorker.a1b2c3d4.js
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Invalid if the page is on another origin:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
https://static-assets.example.com/zipServiceWorker.a1b2c3d4.js
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 3. Configure `Service-Worker-Allowed`
|
|
237
|
+
|
|
238
|
+
By default, a service worker can only control pages under its own directory. If the worker file is served from a nested folder but needs to control a wider scope, the response for the worker file must include `Service-Worker-Allowed`.
|
|
239
|
+
|
|
240
|
+
For an app under `/app/`, configure:
|
|
241
|
+
|
|
242
|
+
```http
|
|
243
|
+
Service-Worker-Allowed: /app/
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If the worker needs to control the full origin, configure:
|
|
247
|
+
|
|
248
|
+
```http
|
|
249
|
+
Service-Worker-Allowed: /
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The header must be returned on the `zipServiceWorker.js` or `zipServiceWorker.<hash>.js` response itself.
|
|
253
|
+
|
|
254
|
+
### 4. Scope and worker location must match
|
|
255
|
+
|
|
256
|
+
The registered scope must be allowed by both:
|
|
257
|
+
|
|
258
|
+
- the worker file location
|
|
259
|
+
- the `Service-Worker-Allowed` header
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
navigator.serviceWorker.register('/app/zipServiceWorker.a1b2c3d4.js', {
|
|
265
|
+
scope: '/app/',
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
This requires:
|
|
270
|
+
|
|
271
|
+
```http
|
|
272
|
+
Service-Worker-Allowed: /app/
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
or:
|
|
276
|
+
|
|
277
|
+
```http
|
|
278
|
+
Service-Worker-Allowed: /
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Bundler Configuration
|
|
282
|
+
|
|
283
|
+
The package ships the worker as:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
zip-peek/zipServiceWorker.js
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Your application must copy this file into its public build output. The browser registers service workers by URL, so the worker must exist as a real served JavaScript file.
|
|
290
|
+
|
|
291
|
+
### Production with a hashed worker filename
|
|
292
|
+
|
|
293
|
+
When your app emits hashed assets, copy the worker with a content hash and pass the final worker URL to `initZipPeek()`.
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
const CopyPlugin = require('copy-webpack-plugin');
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
plugins: [
|
|
300
|
+
new CopyPlugin({
|
|
301
|
+
patterns: [
|
|
302
|
+
{
|
|
303
|
+
from: require.resolve('zip-peek/zipServiceWorker.js'),
|
|
304
|
+
to: 'zipServiceWorker.[contenthash:8].js',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
}),
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
You then need a way for your runtime code to know the emitted hashed filename. Common approaches include:
|
|
313
|
+
|
|
314
|
+
- generating an asset manifest
|
|
315
|
+
- injecting the filename at build time
|
|
316
|
+
- using your framework or bundler's asset URL mechanism
|
|
317
|
+
|
|
318
|
+
For example, after resolving the emitted filename:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
await initZipPeek({
|
|
322
|
+
zipUrl,
|
|
323
|
+
workerUrl: new URL(`/assets/${zipServiceWorkerFilename}`, window.location.origin).href,
|
|
324
|
+
scopeUrl: new URL('/', window.location.origin).href,
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
where `zipServiceWorkerFilename` is the final emitted file name, such as:
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
zipServiceWorker.a1b2c3d4.js
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Development configuration
|
|
335
|
+
|
|
336
|
+
In development, it is usually simpler to copy the worker without a hash:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
const CopyPlugin = require('copy-webpack-plugin');
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
plugins: [
|
|
343
|
+
new CopyPlugin({
|
|
344
|
+
patterns: [
|
|
345
|
+
{
|
|
346
|
+
from: require.resolve('zip-peek/zipServiceWorker.js'),
|
|
347
|
+
to: 'zipServiceWorker.js',
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Then use:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
await initZipPeek({
|
|
359
|
+
zipUrl,
|
|
360
|
+
workerUrl: new URL('/zipServiceWorker.js', window.location.origin).href,
|
|
361
|
+
scopeUrl: new URL('/', window.location.origin).href,
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
If your app is served from a subpath, adjust both `workerUrl` and `scopeUrl` to match that path.
|
|
366
|
+
|
|
367
|
+
## Caching Behavior
|
|
368
|
+
|
|
369
|
+
The service worker stores:
|
|
370
|
+
|
|
371
|
+
- the parsed ZIP manifest
|
|
372
|
+
- extracted full assets
|
|
373
|
+
|
|
374
|
+
The default cache bucket is:
|
|
375
|
+
|
|
376
|
+
```text
|
|
377
|
+
zip-cache-v1
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Extracted assets expire after two hours by default. Set `assetCacheTtlMs` to customize this.
|
|
381
|
+
|
|
382
|
+
When `purgeOtherZipAssets` is enabled, initializing one ZIP removes cached ZIP assets for other ZIP URLs in the same cache bucket to avoid taking too much cache size.
|
|
383
|
+
|
|
384
|
+
## Limitations
|
|
385
|
+
|
|
386
|
+
- The ZIP URL must end with `.zip`.
|
|
387
|
+
- The ZIP server must support HTTP range requests.
|
|
388
|
+
- The service worker file must be same-origin.
|
|
389
|
+
- The service worker can only intercept requests inside its registered scope.
|
|
390
|
+
- Supported ZIP compression methods are stored (`0`) and deflated (`8`).
|
|
391
|
+
- ZIP64 is not currently supported.
|
|
392
|
+
- The package is browser-only and depends on Service Worker, Cache API, and HTTP range request support.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ZipWorkerConfig = {
|
|
2
|
+
zipAssetCacheName?: string;
|
|
3
|
+
assetCacheTtlMs?: number;
|
|
4
|
+
logPrefix?: string;
|
|
5
|
+
};
|
|
6
|
+
export type ZipStreamManagerOptions = {
|
|
7
|
+
purgeOtherZipAssets?: boolean;
|
|
8
|
+
workerConfig?: ZipWorkerConfig;
|
|
9
|
+
};
|
|
10
|
+
export declare class ZipStreamManager {
|
|
11
|
+
/**
|
|
12
|
+
* Fetches the zip Central Directory and sends the manifest to the
|
|
13
|
+
* service worker, which then serves individual files via HTTP Range
|
|
14
|
+
* requests. The full zip is never downloaded.
|
|
15
|
+
*/
|
|
16
|
+
static init(zipUrl: string, options?: ZipStreamManagerOptions): Promise<void>;
|
|
17
|
+
private static postWorkerConfig;
|
|
18
|
+
private static fetchZipManifest;
|
|
19
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZipStreamManager = void 0;
|
|
4
|
+
const zip_utils_1 = require("./zip-utils");
|
|
5
|
+
class ZipStreamManager {
|
|
6
|
+
/**
|
|
7
|
+
* Fetches the zip Central Directory and sends the manifest to the
|
|
8
|
+
* service worker, which then serves individual files via HTTP Range
|
|
9
|
+
* requests. The full zip is never downloaded.
|
|
10
|
+
*/
|
|
11
|
+
static async init(zipUrl, options = {}) {
|
|
12
|
+
if (!(0, zip_utils_1.isZipPackage)(zipUrl)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
let resolvedZipUrl = '';
|
|
16
|
+
try {
|
|
17
|
+
resolvedZipUrl = new URL(zipUrl, window.location.origin).href;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
resolvedZipUrl = new URL(zipUrl, window.location.href).href;
|
|
21
|
+
}
|
|
22
|
+
let resolveInit;
|
|
23
|
+
const initPromise = new Promise(res => {
|
|
24
|
+
resolveInit = res;
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
void navigator.locks.request(`zip-init-${resolvedZipUrl}`, { ifAvailable: true }, async (lock) => {
|
|
28
|
+
if (!lock) {
|
|
29
|
+
console.log('[ZipStreamManager] Another frame is already processing this zip. Skipping.');
|
|
30
|
+
resolveInit?.();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.postWorkerConfig(options.workerConfig);
|
|
34
|
+
if (options.purgeOtherZipAssets !== false && navigator.serviceWorker.controller) {
|
|
35
|
+
const swZipUrl = (0, zip_utils_1.normalizeZipUrlForSw)(resolvedZipUrl);
|
|
36
|
+
navigator.serviceWorker.controller.postMessage({
|
|
37
|
+
type: 'PURGE_OTHER_ZIP_ASSETS',
|
|
38
|
+
zipUrl: swZipUrl,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
console.log('[ZipStreamManager] Fetching zip manifest for range requests.');
|
|
43
|
+
await this.fetchZipManifest(resolvedZipUrl, zipUrl);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error('[ZipStreamManager] Manifest fetch failed', err);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
resolveInit?.();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error('[ZipStreamManager] Failed to initialize zip stream manager', error);
|
|
55
|
+
resolveInit?.();
|
|
56
|
+
}
|
|
57
|
+
await initPromise;
|
|
58
|
+
}
|
|
59
|
+
static postWorkerConfig(workerConfig) {
|
|
60
|
+
if (!navigator.serviceWorker.controller || !workerConfig) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
navigator.serviceWorker.controller.postMessage({
|
|
64
|
+
type: 'ZIP_SW_CONFIG',
|
|
65
|
+
config: workerConfig,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
static async fetchZipManifest(resolvedZipUrl, zipUrl) {
|
|
69
|
+
const response = await fetch(zipUrl, {
|
|
70
|
+
headers: {
|
|
71
|
+
Range: 'bytes=-65536',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (response.status !== 206) {
|
|
75
|
+
console.warn('[ZipStreamManager] Range requests not supported (status ' + response.status + ').');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const buffer = await response.arrayBuffer();
|
|
79
|
+
const dataView = new DataView(buffer);
|
|
80
|
+
let eocdOffset = -1;
|
|
81
|
+
for (let i = buffer.byteLength - 22; i >= 0; i--) {
|
|
82
|
+
if (dataView.getUint32(i, true) === 0x06054b50) {
|
|
83
|
+
eocdOffset = i;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (eocdOffset === -1) {
|
|
88
|
+
console.warn('[ZipStreamManager] EOCD not found in last 64KB');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const cdOffset = dataView.getUint32(eocdOffset + 16, true);
|
|
92
|
+
const cdSize = dataView.getUint32(eocdOffset + 12, true);
|
|
93
|
+
let cdBuffer = buffer;
|
|
94
|
+
let cdStartInBuf = -1;
|
|
95
|
+
const contentRange = response.headers.get('Content-Range');
|
|
96
|
+
let totalSize = 0;
|
|
97
|
+
if (contentRange) {
|
|
98
|
+
const match = contentRange.match(/\/(\d+)$/);
|
|
99
|
+
if (match)
|
|
100
|
+
totalSize = parseInt(match[1], 10);
|
|
101
|
+
}
|
|
102
|
+
if (totalSize > 0) {
|
|
103
|
+
const rangeStart = totalSize - buffer.byteLength;
|
|
104
|
+
if (cdOffset >= rangeStart) {
|
|
105
|
+
cdStartInBuf = cdOffset - rangeStart;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const cdResponse = await fetch(zipUrl, {
|
|
109
|
+
headers: { Range: `bytes=${cdOffset}-${cdOffset + cdSize - 1}` },
|
|
110
|
+
});
|
|
111
|
+
if (cdResponse.status === 206) {
|
|
112
|
+
cdBuffer = await cdResponse.arrayBuffer();
|
|
113
|
+
cdStartInBuf = 0;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (cdStartInBuf === -1)
|
|
124
|
+
return;
|
|
125
|
+
const files = [];
|
|
126
|
+
const cdDataView = new DataView(cdBuffer);
|
|
127
|
+
let offset = cdStartInBuf;
|
|
128
|
+
while (offset + 46 <= cdBuffer.byteLength) {
|
|
129
|
+
if (cdDataView.getUint32(offset, true) !== 0x02014b50) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const compressionMethod = cdDataView.getUint16(offset + 10, true);
|
|
133
|
+
const compressedSize = cdDataView.getUint32(offset + 20, true);
|
|
134
|
+
const uncompressedSize = cdDataView.getUint32(offset + 24, true);
|
|
135
|
+
const fileNameLength = cdDataView.getUint16(offset + 28, true);
|
|
136
|
+
const extraFieldLength = cdDataView.getUint16(offset + 30, true);
|
|
137
|
+
const fileCommentLength = cdDataView.getUint16(offset + 32, true);
|
|
138
|
+
const localHeaderOffset = cdDataView.getUint32(offset + 42, true);
|
|
139
|
+
if (offset + 46 + fileNameLength > cdBuffer.byteLength) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
const fileNameBytes = new Uint8Array(cdBuffer, offset + 46, fileNameLength);
|
|
143
|
+
const rawName = new TextDecoder().decode(fileNameBytes);
|
|
144
|
+
const fileName = (0, zip_utils_1.normalizeZipEntryPath)(rawName);
|
|
145
|
+
if (fileName !== '') {
|
|
146
|
+
files.push({
|
|
147
|
+
filename: fileName,
|
|
148
|
+
offset: localHeaderOffset,
|
|
149
|
+
compressedSize,
|
|
150
|
+
uncompressedSize,
|
|
151
|
+
compression: compressionMethod,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
offset += 46 + fileNameLength + extraFieldLength + fileCommentLength;
|
|
155
|
+
}
|
|
156
|
+
if (files.length === 0) {
|
|
157
|
+
console.warn('[ZipStreamManager] No files parsed from Central Directory');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
files.sort((a, b) => a.offset - b.offset);
|
|
161
|
+
for (let i = 0; i < files.length; i++) {
|
|
162
|
+
files[i].nextOffset = i < files.length - 1 ? files[i + 1].offset : cdOffset;
|
|
163
|
+
}
|
|
164
|
+
const swZipUrl = (0, zip_utils_1.normalizeZipUrlForSw)(resolvedZipUrl);
|
|
165
|
+
const allPaths = files.map(f => f.filename);
|
|
166
|
+
console.log('[ZipStreamManager] manifest all paths', allPaths);
|
|
167
|
+
console.log('[ZipStreamManager] manifest ready', {
|
|
168
|
+
zipUrl: swZipUrl,
|
|
169
|
+
entryCount: files.length,
|
|
170
|
+
sampleEntries: allPaths.slice(0, 30),
|
|
171
|
+
});
|
|
172
|
+
if (navigator.serviceWorker.controller) {
|
|
173
|
+
navigator.serviceWorker.controller.postMessage({
|
|
174
|
+
type: 'ZIP_MANIFEST',
|
|
175
|
+
zipUrl: swZipUrl,
|
|
176
|
+
files,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.ZipStreamManager = ZipStreamManager;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ZipWorkerConfig } from './ZipStreamManager';
|
|
2
|
+
import { isZipPackage } from './zip-utils';
|
|
3
|
+
export type InitZipPeekOptions = {
|
|
4
|
+
zipUrl: string;
|
|
5
|
+
workerUrl: string;
|
|
6
|
+
scopeUrl: string;
|
|
7
|
+
reloadOnFirstInstall?: boolean;
|
|
8
|
+
purgeOtherZipAssets?: boolean;
|
|
9
|
+
zipAssetCacheName?: string;
|
|
10
|
+
assetCacheTtlMs?: number;
|
|
11
|
+
logPrefix?: string;
|
|
12
|
+
};
|
|
13
|
+
export type InitZipPeekResult = {
|
|
14
|
+
reloaded: boolean;
|
|
15
|
+
initialized: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function initZipPeek({ zipUrl, workerUrl, scopeUrl, reloadOnFirstInstall, purgeOtherZipAssets, zipAssetCacheName, assetCacheTtlMs, logPrefix, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
|
|
18
|
+
export { isZipPackage };
|
|
19
|
+
export type { ZipWorkerConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isZipPackage = void 0;
|
|
4
|
+
exports.initZipPeek = initZipPeek;
|
|
5
|
+
const registerZipServiceWorker_1 = require("./registerZipServiceWorker");
|
|
6
|
+
const ZipStreamManager_1 = require("./ZipStreamManager");
|
|
7
|
+
const zip_utils_1 = require("./zip-utils");
|
|
8
|
+
Object.defineProperty(exports, "isZipPackage", { enumerable: true, get: function () { return zip_utils_1.isZipPackage; } });
|
|
9
|
+
async function initZipPeek({ zipUrl, workerUrl, scopeUrl, reloadOnFirstInstall = true, purgeOtherZipAssets = true, zipAssetCacheName, assetCacheTtlMs, logPrefix, }) {
|
|
10
|
+
if (!(0, zip_utils_1.isZipPackage)(zipUrl)) {
|
|
11
|
+
return { reloaded: false, initialized: false };
|
|
12
|
+
}
|
|
13
|
+
const reloaded = await (0, registerZipServiceWorker_1.ensureZipServiceWorkerRegistered)({
|
|
14
|
+
workerUrl,
|
|
15
|
+
scopeUrl,
|
|
16
|
+
reloadOnFirstInstall,
|
|
17
|
+
});
|
|
18
|
+
if (reloaded) {
|
|
19
|
+
return { reloaded: true, initialized: false };
|
|
20
|
+
}
|
|
21
|
+
const workerConfig = {
|
|
22
|
+
zipAssetCacheName,
|
|
23
|
+
assetCacheTtlMs,
|
|
24
|
+
logPrefix,
|
|
25
|
+
};
|
|
26
|
+
await ZipStreamManager_1.ZipStreamManager.init(zipUrl, {
|
|
27
|
+
purgeOtherZipAssets,
|
|
28
|
+
workerConfig,
|
|
29
|
+
});
|
|
30
|
+
return { reloaded: false, initialized: true };
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type RegisterZipServiceWorkerOptions = {
|
|
2
|
+
workerUrl: string;
|
|
3
|
+
scopeUrl: string;
|
|
4
|
+
reloadOnFirstInstall?: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Registers the zip worker script URL with the caller-provided scope.
|
|
8
|
+
*
|
|
9
|
+
* @returns true if the page is reloading and callers should abort init.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ensureZipServiceWorkerRegistered({ workerUrl, scopeUrl, reloadOnFirstInstall, }: RegisterZipServiceWorkerOptions): Promise<boolean>;
|
|
12
|
+
export {};
|