zip-peek 0.1.0 → 0.2.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 CHANGED
@@ -17,14 +17,14 @@ Use this package when your app receives a base path and later appends folders an
17
17
  For example, your app may normally treat the base path as a directory:
18
18
 
19
19
  ```ts
20
- const basePath = 'https://cdn.example.com/packages/session';
20
+ const basePath = "https://cdn.example.com/packages/session";
21
21
  const imageUrl = `${basePath}/slides/slide-1/image.png`;
22
22
  ```
23
23
 
24
24
  If the same assets are sometimes delivered as a ZIP file instead:
25
25
 
26
26
  ```ts
27
- const basePath = 'https://cdn.example.com/packages/session.zip';
27
+ const basePath = "https://cdn.example.com/packages/session.zip";
28
28
  const imageUrl = `${basePath}/slides/slide-1/image.png`;
29
29
  ```
30
30
 
@@ -39,16 +39,14 @@ The package provides two pieces:
39
39
 
40
40
  At runtime, the package:
41
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.
42
+ 1. Registers the ZIP service worker with the configured `workerUrl` and `scopeUrl`.
43
+ 2. Reloads once after first service worker install when needed, so the page becomes controlled.
44
+ 3. Intercepts matching `.zip/...` asset requests within the service worker scope.
45
+ 4. Loads and caches the ZIP central directory on first use, or during init when `allowedZipUrls` is provided.
46
+ 5. Fetches only the byte range needed for the requested file.
47
+ 6. Inflates deflated files using `fflate`.
48
+ 7. Returns the extracted file bytes to the browser request as a normal `Response`.
49
+ 8. Caches extracted assets in the browser Cache API.
52
50
 
53
51
  ## Installation
54
52
 
@@ -65,23 +63,22 @@ npm install zip-peek
65
63
  ## Basic Usage
66
64
 
67
65
  ```ts
68
- import { initZipPeek } from 'zip-peek';
66
+ import { initZipPeek } from "zip-peek";
69
67
 
70
68
  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,
69
+ workerUrl: new URL("/zipServiceWorker.js", window.location.origin).href,
70
+ scopeUrl: new URL("/", window.location.origin).href,
74
71
  });
75
72
 
76
73
  if (zipPeekInit.reloaded) {
77
- return;
74
+ return;
78
75
  }
79
76
  ```
80
77
 
81
78
  After initialization, your app can continue building asset URLs under the ZIP URL:
82
79
 
83
80
  ```ts
84
- const zipUrl = 'https://cdn.example.com/packages/zipfile.zip';
81
+ const zipUrl = "https://cdn.example.com/packages/zipfile.zip";
85
82
  const imgUrl = `${zipUrl}/slides/slide-1/image.png`;
86
83
 
87
84
  // This request will work normally and zip-peek will do the magic in the background:
@@ -96,39 +93,30 @@ The browser requests that URL normally. The service worker intercepts it and ser
96
93
 
97
94
  ```ts
98
95
  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;
96
+ workerUrl: string;
97
+ scopeUrl: string;
98
+ reloadOnFirstInstall?: boolean;
99
+ cacheClearingStrategy?: ZipCacheClearingStrategy;
100
+ allowedZipUrls?: string[];
101
+ zipAssetCacheName?: string;
102
+ assetCacheTtlMs?: number;
103
+ logPrefix?: string;
107
104
  };
105
+
106
+ type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear-all";
108
107
  ```
109
108
 
110
109
  Returns:
111
110
 
112
111
  ```ts
113
112
  type InitZipPeekResult = {
114
- reloaded: boolean;
115
- initialized: boolean;
113
+ reloaded: boolean;
114
+ initialized: boolean;
116
115
  };
117
116
  ```
118
117
 
119
118
  ### Options
120
119
 
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
120
  `workerUrl`
133
121
 
134
122
  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.
@@ -136,10 +124,10 @@ The URL where the browser can download the service worker JavaScript file. The s
136
124
  Example:
137
125
 
138
126
  ```ts
139
- workerUrl: new URL('./zipServiceWorker.js', window.location.href).href
127
+ workerUrl: new URL("./zipServiceWorker.js", window.location.href).href;
140
128
  ```
141
129
 
142
- In production, you will usually prefer hashed file to give it a long CacheControl duration:
130
+ In production, you will usually prefer hashed file to give it a long `Cache-Control` duration:
143
131
 
144
132
  ```text
145
133
  zipServiceWorker.a1b2c3d4.js
@@ -148,12 +136,13 @@ zipServiceWorker.a1b2c3d4.js
148
136
  `scopeUrl`
149
137
 
150
138
  The service worker scope. The worker can only intercept requests inside this scope.
139
+ See [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#scope) for more details.
151
140
 
152
141
  Examples:
153
142
 
154
143
  ```ts
155
- scopeUrl: new URL('/', window.location.origin).href
156
- scopeUrl: new URL('/app/', window.location.origin).href
144
+ scopeUrl: new URL("/", window.location.origin).href;
145
+ scopeUrl: new URL("/app/", window.location.origin).href;
157
146
  ```
158
147
 
159
148
  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.
@@ -162,9 +151,28 @@ Choose the smallest scope that includes the pages and asset requests you want th
162
151
 
163
152
  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
153
 
165
- `purgeOtherZipAssets`
154
+ `cacheClearingStrategy`
155
+
156
+ Defaults to `'keep-all'`. Controls which zip-peek cache entries are cleared during initialization.
157
+
158
+ - `'keep-all'`: do not clear cached ZIP manifests or extracted assets.
159
+ - `'keep-allowed-urls'`: requires non-empty `allowedZipUrls`; clears cached ZIP data for URLs outside the allow-list.
160
+ - `'clear-all'`: clears all zip-peek cached manifests and extracted assets for a fresh start.
166
161
 
167
- Defaults to `true`. Removes cached assets and manifests for other ZIP URLs from this package's Cache API bucket.
162
+ `allowedZipUrls`
163
+
164
+ Restricts zip-peek to a known set of ZIP package URLs. When omitted, zip-peek lazily serves any matching `.zip/...` request inside the service worker scope. When provided, the array must contain at least one ZIP URL, and zip-peek transparently passes matching requests for ZIP URLs outside the list.
165
+
166
+ The allow-list also acts as a warmup list: zip-peek loads and caches each allowed ZIP manifest during initialization.
167
+
168
+ Examples:
169
+
170
+ ```ts
171
+ allowedZipUrls: ["https://cdn.example.com/packages/zipfile.zip"];
172
+ allowedZipUrls: [
173
+ "https://d3jfe.cloudfront.net/somedir/zipfile.zip?Expires=1798761599&Signature=K2CJW4Z7B3DCM4&Key-Pair-Id=K2CJW4Z7B3DCM4",
174
+ ];
175
+ ```
168
176
 
169
177
  `zipAssetCacheName`
170
178
 
@@ -183,7 +191,7 @@ Controls how long extracted assets remain valid in the Cache API.
183
191
  Default:
184
192
 
185
193
  ```ts
186
- 2 * 60 * 60 * 1000
194
+ 2 * 60 * 60 * 1000; // 2 hours
187
195
  ```
188
196
 
189
197
  `logPrefix`
@@ -261,8 +269,8 @@ The registered scope must be allowed by both:
261
269
  Example:
262
270
 
263
271
  ```ts
264
- navigator.serviceWorker.register('/app/zipServiceWorker.a1b2c3d4.js', {
265
- scope: '/app/',
272
+ navigator.serviceWorker.register("/app/zipServiceWorker.a1b2c3d4.js", {
273
+ scope: "/app/",
266
274
  });
267
275
  ```
268
276
 
@@ -293,19 +301,19 @@ Your application must copy this file into its public build output. The browser r
293
301
  When your app emits hashed assets, copy the worker with a content hash and pass the final worker URL to `initZipPeek()`.
294
302
 
295
303
  ```js
296
- const CopyPlugin = require('copy-webpack-plugin');
304
+ const CopyPlugin = require("copy-webpack-plugin");
297
305
 
298
306
  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
- ],
307
+ plugins: [
308
+ new CopyPlugin({
309
+ patterns: [
310
+ {
311
+ from: require.resolve("zip-peek/zipServiceWorker.js"),
312
+ to: "zipServiceWorker.[contenthash:8].js",
313
+ },
314
+ ],
315
+ }),
316
+ ],
309
317
  };
310
318
  ```
311
319
 
@@ -319,9 +327,11 @@ For example, after resolving the emitted filename:
319
327
 
320
328
  ```ts
321
329
  await initZipPeek({
322
- zipUrl,
323
- workerUrl: new URL(`/assets/${zipServiceWorkerFilename}`, window.location.origin).href,
324
- scopeUrl: new URL('/', window.location.origin).href,
330
+ workerUrl: new URL(
331
+ `/assets/${zipServiceWorkerFilename}`,
332
+ window.location.origin,
333
+ ).href,
334
+ scopeUrl: new URL("/", window.location.origin).href,
325
335
  });
326
336
  ```
327
337
 
@@ -336,19 +346,19 @@ zipServiceWorker.a1b2c3d4.js
336
346
  In development, it is usually simpler to copy the worker without a hash:
337
347
 
338
348
  ```js
339
- const CopyPlugin = require('copy-webpack-plugin');
349
+ const CopyPlugin = require("copy-webpack-plugin");
340
350
 
341
351
  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
+ plugins: [
353
+ new CopyPlugin({
354
+ patterns: [
355
+ {
356
+ from: require.resolve("zip-peek/zipServiceWorker.js"),
357
+ to: "zipServiceWorker.js",
358
+ },
359
+ ],
360
+ }),
361
+ ],
352
362
  };
353
363
  ```
354
364
 
@@ -356,9 +366,8 @@ Then use:
356
366
 
357
367
  ```ts
358
368
  await initZipPeek({
359
- zipUrl,
360
- workerUrl: new URL('/zipServiceWorker.js', window.location.origin).href,
361
- scopeUrl: new URL('/', window.location.origin).href,
369
+ workerUrl: new URL("/zipServiceWorker.js", window.location.origin).href,
370
+ scopeUrl: new URL("/", window.location.origin).href,
362
371
  });
363
372
  ```
364
373
 
@@ -379,7 +388,11 @@ zip-cache-v1
379
388
 
380
389
  Extracted assets expire after two hours by default. Set `assetCacheTtlMs` to customize this.
381
390
 
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.
391
+ Use `cacheClearingStrategy` to control initialization-time cleanup. `keep-all` leaves existing entries untouched, `keep-allowed-urls` removes cached ZIP data outside `allowedZipUrls`, and `clear-all` removes all zip-peek cached ZIP data before warming any allowed manifests.
392
+
393
+ ## Internal Flow
394
+
395
+ For a detailed explanation of how the service worker intercepts requests, parses ZIP files, and handles caching, see the [Zip Service Worker Flow documentation](./docs/ZIP_SERVICE_WORKER_FLOW.md).
383
396
 
384
397
  ## Limitations
385
398
 
package/dist/index.d.ts CHANGED
@@ -1,19 +1,15 @@
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>;
1
+ import type { InitZipPeekOptions, InitZipPeekResult } from "./types";
2
+ import { isZipPackage } from "./zip-utils";
3
+ export type { InitZipPeekOptions, InitZipPeekResult, ZipCacheClearingStrategy, ZipWorkerConfig, } from "./types";
4
+ /**
5
+ * Registers the zip-peek service worker and configures lazy ZIP asset loading.
6
+ *
7
+ * By default, zip-peek serves any matching `.zip/...` request within the
8
+ * service worker scope by loading the ZIP manifest on first use. Pass
9
+ * `allowedZipUrls` to restrict serving to a known, non-empty set of ZIP
10
+ * package URLs and warm their manifests during initialization. Use
11
+ * `cacheClearingStrategy` to keep existing cache entries, keep only allowed
12
+ * ZIP URLs, or clear all zip-peek cache entries before use.
13
+ */
14
+ export declare function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall, cacheClearingStrategy, allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
18
15
  export { isZipPackage };
19
- export type { ZipWorkerConfig };
package/dist/index.js CHANGED
@@ -3,11 +3,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isZipPackage = void 0;
4
4
  exports.initZipPeek = initZipPeek;
5
5
  const registerZipServiceWorker_1 = require("./registerZipServiceWorker");
6
- const ZipStreamManager_1 = require("./ZipStreamManager");
7
6
  const zip_utils_1 = require("./zip-utils");
8
7
  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)) {
8
+ function normalizeAllowedZipUrls(allowedZipUrls) {
9
+ if (allowedZipUrls === undefined) {
10
+ return undefined;
11
+ }
12
+ if (allowedZipUrls.length === 0) {
13
+ throw new Error("zip-peek: allowedZipUrls must contain at least one ZIP URL when provided.");
14
+ }
15
+ const normalized = new Set();
16
+ for (const zipUrl of allowedZipUrls) {
17
+ const resolvedZipUrl = new URL(zipUrl, window.location.href).href;
18
+ if (!(0, zip_utils_1.isZipPackage)(resolvedZipUrl)) {
19
+ throw new Error(`zip-peek: allowedZipUrls contains a non-ZIP URL: ${zipUrl}`);
20
+ }
21
+ normalized.add((0, zip_utils_1.normalizeZipUrlForSw)(resolvedZipUrl));
22
+ }
23
+ return Array.from(normalized);
24
+ }
25
+ function postWorkerMessage(message) {
26
+ const controller = navigator.serviceWorker.controller;
27
+ if (!controller) {
28
+ return Promise.resolve();
29
+ }
30
+ return new Promise((resolve, reject) => {
31
+ const channel = new MessageChannel();
32
+ channel.port1.onmessage = (event) => {
33
+ const response = event.data;
34
+ channel.port1.close();
35
+ if (!response || response.ok) {
36
+ resolve();
37
+ }
38
+ else {
39
+ reject(new Error(response.error ?? "zip-peek: service worker message failed."));
40
+ }
41
+ };
42
+ channel.port1.onmessageerror = () => {
43
+ channel.port1.close();
44
+ reject(new Error("zip-peek: failed to receive service worker response."));
45
+ };
46
+ controller.postMessage(message, [channel.port2]);
47
+ });
48
+ }
49
+ /**
50
+ * Registers the zip-peek service worker and configures lazy ZIP asset loading.
51
+ *
52
+ * By default, zip-peek serves any matching `.zip/...` request within the
53
+ * service worker scope by loading the ZIP manifest on first use. Pass
54
+ * `allowedZipUrls` to restrict serving to a known, non-empty set of ZIP
55
+ * package URLs and warm their manifests during initialization. Use
56
+ * `cacheClearingStrategy` to keep existing cache entries, keep only allowed
57
+ * ZIP URLs, or clear all zip-peek cache entries before use.
58
+ */
59
+ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = "keep-all", allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, }) {
60
+ const normalizedAllowedZipUrls = normalizeAllowedZipUrls(allowedZipUrls);
61
+ if (cacheClearingStrategy === "keep-allowed-urls" &&
62
+ !normalizedAllowedZipUrls) {
63
+ throw new Error('zip-peek: cacheClearingStrategy "keep-allowed-urls" requires a non-empty allowedZipUrls allow-list.');
64
+ }
65
+ if (!("serviceWorker" in navigator)) {
66
+ console.warn("zip-peek: Service Worker API not supported.");
11
67
  return { reloaded: false, initialized: false };
12
68
  }
13
69
  const reloaded = await (0, registerZipServiceWorker_1.ensureZipServiceWorkerRegistered)({
@@ -18,14 +74,41 @@ async function initZipPeek({ zipUrl, workerUrl, scopeUrl, reloadOnFirstInstall =
18
74
  if (reloaded) {
19
75
  return { reloaded: true, initialized: false };
20
76
  }
21
- const workerConfig = {
22
- zipAssetCacheName,
23
- assetCacheTtlMs,
24
- logPrefix,
25
- };
26
- await ZipStreamManager_1.ZipStreamManager.init(zipUrl, {
27
- purgeOtherZipAssets,
28
- workerConfig,
77
+ await postWorkerMessage({
78
+ type: "ZIP_SW_CONFIG",
79
+ config: {
80
+ zipAssetCacheName,
81
+ assetCacheTtlMs,
82
+ logPrefix,
83
+ allowedZipUrls: normalizedAllowedZipUrls ?? null,
84
+ },
29
85
  });
86
+ if (cacheClearingStrategy === "clear-all") {
87
+ await postWorkerMessage({
88
+ type: "CLEAR_ALL_ZIP_ASSETS",
89
+ });
90
+ await postWorkerMessage({
91
+ type: "ZIP_SW_CONFIG",
92
+ config: {
93
+ zipAssetCacheName,
94
+ assetCacheTtlMs,
95
+ logPrefix,
96
+ allowedZipUrls: normalizedAllowedZipUrls ?? null,
97
+ },
98
+ });
99
+ }
100
+ else if (cacheClearingStrategy === "keep-allowed-urls" &&
101
+ normalizedAllowedZipUrls) {
102
+ await postWorkerMessage({
103
+ type: "CLEAR_ZIP_ASSETS_EXCEPT_ALLOWED",
104
+ allowedZipUrls: normalizedAllowedZipUrls,
105
+ });
106
+ }
107
+ if (normalizedAllowedZipUrls) {
108
+ await postWorkerMessage({
109
+ type: "PRELOAD_ZIP_MANIFESTS",
110
+ allowedZipUrls: normalizedAllowedZipUrls,
111
+ });
112
+ }
30
113
  return { reloaded: false, initialized: true };
31
114
  }
@@ -0,0 +1,24 @@
1
+ export type ZipWorkerConfig = {
2
+ zipAssetCacheName?: string;
3
+ assetCacheTtlMs?: number;
4
+ logPrefix?: string;
5
+ };
6
+ export type ZipCacheClearingStrategy = 'keep-all' | 'keep-allowed-urls' | 'clear-all';
7
+ export type InitZipPeekOptions = {
8
+ workerUrl: string;
9
+ scopeUrl: string;
10
+ reloadOnFirstInstall?: boolean;
11
+ cacheClearingStrategy?: ZipCacheClearingStrategy;
12
+ allowedZipUrls?: string[];
13
+ zipAssetCacheName?: string;
14
+ assetCacheTtlMs?: number;
15
+ logPrefix?: string;
16
+ };
17
+ export type InitZipPeekResult = {
18
+ reloaded: boolean;
19
+ initialized: boolean;
20
+ };
21
+ export type WorkerMessageResponse = {
22
+ ok: boolean;
23
+ error?: string;
24
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/zip-utils.js CHANGED
@@ -10,10 +10,10 @@ exports.canonicalZipAssetRequestUrl = canonicalZipAssetRequestUrl;
10
10
  function isZipPackage(url) {
11
11
  try {
12
12
  const u = new URL(url);
13
- return u.pathname.endsWith('.zip');
13
+ return u.pathname.endsWith(".zip");
14
14
  }
15
15
  catch {
16
- return url.split('?')[0].endsWith('.zip');
16
+ return url.split("?")[0].endsWith(".zip");
17
17
  }
18
18
  }
19
19
  /**
@@ -22,29 +22,31 @@ function isZipPackage(url) {
22
22
  * are not re-encoded.
23
23
  */
24
24
  function stripTParam(urlString) {
25
- const qIdx = urlString.indexOf('?');
25
+ const qIdx = urlString.indexOf("?");
26
26
  if (qIdx === -1)
27
- return urlString.split('#')[0];
28
- const hashIdx = urlString.indexOf('#', qIdx);
27
+ return urlString.split("#")[0];
28
+ const hashIdx = urlString.indexOf("#", qIdx);
29
29
  const prefix = urlString.substring(0, qIdx);
30
- const querySection = hashIdx === -1 ? urlString.substring(qIdx + 1) : urlString.substring(qIdx + 1, hashIdx);
31
- const hashSection = hashIdx === -1 ? '' : urlString.substring(hashIdx);
32
- const filtered = querySection.split('&').filter(p => {
33
- const eqIdx = p.indexOf('=');
30
+ const querySection = hashIdx === -1
31
+ ? urlString.substring(qIdx + 1)
32
+ : urlString.substring(qIdx + 1, hashIdx);
33
+ const hashSection = hashIdx === -1 ? "" : urlString.substring(hashIdx);
34
+ const filtered = querySection.split("&").filter((p) => {
35
+ const eqIdx = p.indexOf("=");
34
36
  const k = eqIdx === -1 ? p : p.substring(0, eqIdx);
35
- return k !== 't';
37
+ return k !== "t";
36
38
  });
37
39
  if (filtered.length === 0)
38
40
  return prefix + hashSection;
39
- return prefix + '?' + filtered.join('&') + hashSection;
41
+ return prefix + "?" + filtered.join("&") + hashSection;
40
42
  }
41
43
  /** Same key the service worker uses for manifests / purge (presigned query kept). */
42
44
  exports.normalizeZipUrlForSw = stripTParam;
43
45
  /** Align zip entry names with URL paths under `*.zip/…`. */
44
46
  function normalizeZipEntryPath(name) {
45
- let s = name.replace(/\\/g, '/');
46
- while (s.startsWith('/'))
47
- s = s.slice(1);
47
+ let s = name.replace(/\\/g, "/"); // Replace backslashes with forward slashes.
48
+ while (s.startsWith("/"))
49
+ s = s.slice(1); // Remove leading slashes.
48
50
  return s;
49
51
  }
50
52
  function safeDecodeSegment(s) {
@@ -68,26 +70,26 @@ function parseZipAssetRequest(urlString) {
68
70
  const match = /\.zip([/?])/.exec(urlString);
69
71
  if (!match)
70
72
  return null;
71
- const sepIdx = match.index + 4;
73
+ const sepIdx = match.index + ".zip".length;
72
74
  const separator = match[1];
73
75
  const zipUrlBase = urlString.substring(0, sepIdx);
74
- if (separator === '/') {
76
+ if (separator === "/") {
75
77
  const rest = urlString.substring(sepIdx + 1);
76
- const qIdx = rest.indexOf('?');
78
+ const qIdx = rest.indexOf("?");
77
79
  const rawInternal = qIdx === -1 ? rest : rest.substring(0, qIdx);
78
- if (rawInternal === '')
80
+ if (rawInternal === "")
79
81
  return null;
80
82
  return { zipUrl: zipUrlBase, internalPath: safeDecodeSegment(rawInternal) };
81
83
  }
82
84
  const afterZip = urlString.substring(sepIdx);
83
- const slashIdx = afterZip.indexOf('/');
85
+ const slashIdx = afterZip.indexOf("/");
84
86
  if (slashIdx === -1)
85
87
  return null;
86
88
  const zipQueryPart = afterZip.substring(0, slashIdx);
87
89
  const afterSlash = afterZip.substring(slashIdx + 1);
88
- const qIdx = afterSlash.indexOf('?');
90
+ const qIdx = afterSlash.indexOf("?");
89
91
  const rawInternal = qIdx === -1 ? afterSlash : afterSlash.substring(0, qIdx);
90
- if (rawInternal === '')
92
+ if (rawInternal === "")
91
93
  return null;
92
94
  return {
93
95
  zipUrl: zipUrlBase + zipQueryPart,
@@ -96,5 +98,5 @@ function parseZipAssetRequest(urlString) {
96
98
  }
97
99
  /** Cache key for a zip asset (decoded manifest key + zip URL without `t`). */
98
100
  function canonicalZipAssetRequestUrl(zipUrl, manifestKey) {
99
- return zipUrl + '/' + encodeURI(manifestKey);
101
+ return zipUrl + "/" + encodeURIComponent(manifestKey);
100
102
  }