zip-peek 0.2.0 → 0.2.2

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
 
@@ -63,22 +63,22 @@ npm install zip-peek
63
63
  ## Basic Usage
64
64
 
65
65
  ```ts
66
- import { initZipPeek } from 'zip-peek';
66
+ import { initZipPeek } from "zip-peek";
67
67
 
68
68
  const zipPeekInit = await initZipPeek({
69
- workerUrl: new URL('/zipServiceWorker.js', window.location.origin).href,
70
- 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,
71
71
  });
72
72
 
73
73
  if (zipPeekInit.reloaded) {
74
- return;
74
+ return;
75
75
  }
76
76
  ```
77
77
 
78
78
  After initialization, your app can continue building asset URLs under the ZIP URL:
79
79
 
80
80
  ```ts
81
- const zipUrl = 'https://cdn.example.com/packages/zipfile.zip';
81
+ const zipUrl = "https://cdn.example.com/packages/zipfile.zip";
82
82
  const imgUrl = `${zipUrl}/slides/slide-1/image.png`;
83
83
 
84
84
  // This request will work normally and zip-peek will do the magic in the background:
@@ -93,25 +93,25 @@ The browser requests that URL normally. The service worker intercepts it and ser
93
93
 
94
94
  ```ts
95
95
  type InitZipPeekOptions = {
96
- workerUrl: string;
97
- scopeUrl: string;
98
- reloadOnFirstInstall?: boolean;
99
- cacheClearingStrategy?: ZipCacheClearingStrategy;
100
- allowedZipUrls?: string[];
101
- zipAssetCacheName?: string;
102
- assetCacheTtlMs?: number;
103
- 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;
104
104
  };
105
105
 
106
- type ZipCacheClearingStrategy = 'keep-all' | 'keep-allowed-urls' | 'clear-all';
106
+ type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear-all";
107
107
  ```
108
108
 
109
109
  Returns:
110
110
 
111
111
  ```ts
112
112
  type InitZipPeekResult = {
113
- reloaded: boolean;
114
- initialized: boolean;
113
+ reloaded: boolean;
114
+ initialized: boolean;
115
115
  };
116
116
  ```
117
117
 
@@ -124,10 +124,10 @@ The URL where the browser can download the service worker JavaScript file. The s
124
124
  Example:
125
125
 
126
126
  ```ts
127
- workerUrl: new URL('./zipServiceWorker.js', window.location.href).href
127
+ workerUrl: new URL("./zipServiceWorker.js", window.location.href).href;
128
128
  ```
129
129
 
130
- 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:
131
131
 
132
132
  ```text
133
133
  zipServiceWorker.a1b2c3d4.js
@@ -136,12 +136,13 @@ zipServiceWorker.a1b2c3d4.js
136
136
  `scopeUrl`
137
137
 
138
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.
139
140
 
140
141
  Examples:
141
142
 
142
143
  ```ts
143
- scopeUrl: new URL('/', window.location.origin).href
144
- 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;
145
146
  ```
146
147
 
147
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.
@@ -160,17 +161,17 @@ Defaults to `'keep-all'`. Controls which zip-peek cache entries are cleared duri
160
161
 
161
162
  `allowedZipUrls`
162
163
 
163
- 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 rejects matching requests for ZIP URLs outside the list.
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.
164
165
 
165
166
  The allow-list also acts as a warmup list: zip-peek loads and caches each allowed ZIP manifest during initialization.
166
167
 
167
168
  Examples:
168
169
 
169
170
  ```ts
170
- allowedZipUrls: ['https://cdn.example.com/packages/zipfile.zip']
171
+ allowedZipUrls: ["https://cdn.example.com/packages/zipfile.zip"];
171
172
  allowedZipUrls: [
172
- 'https://d3jfe.cloudfront.net/somedir/zipfile.zip?Expires=1798761599&Signature=K2CJW4Z7B3DCM4&Key-Pair-Id=K2CJW4Z7B3DCM4',
173
- ]
173
+ "https://d3jfe.cloudfront.net/somedir/zipfile.zip?Expires=1798761599&Signature=K2CJW4Z7B3DCM4&Key-Pair-Id=K2CJW4Z7B3DCM4",
174
+ ];
174
175
  ```
175
176
 
176
177
  `zipAssetCacheName`
@@ -190,7 +191,7 @@ Controls how long extracted assets remain valid in the Cache API.
190
191
  Default:
191
192
 
192
193
  ```ts
193
- 2 * 60 * 60 * 1000
194
+ 2 * 60 * 60 * 1000; // 2 hours
194
195
  ```
195
196
 
196
197
  `logPrefix`
@@ -268,8 +269,8 @@ The registered scope must be allowed by both:
268
269
  Example:
269
270
 
270
271
  ```ts
271
- navigator.serviceWorker.register('/app/zipServiceWorker.a1b2c3d4.js', {
272
- scope: '/app/',
272
+ navigator.serviceWorker.register("/app/zipServiceWorker.a1b2c3d4.js", {
273
+ scope: "/app/",
273
274
  });
274
275
  ```
275
276
 
@@ -300,19 +301,19 @@ Your application must copy this file into its public build output. The browser r
300
301
  When your app emits hashed assets, copy the worker with a content hash and pass the final worker URL to `initZipPeek()`.
301
302
 
302
303
  ```js
303
- const CopyPlugin = require('copy-webpack-plugin');
304
+ const CopyPlugin = require("copy-webpack-plugin");
304
305
 
305
306
  module.exports = {
306
- plugins: [
307
- new CopyPlugin({
308
- patterns: [
309
- {
310
- from: require.resolve('zip-peek/zipServiceWorker.js'),
311
- to: 'zipServiceWorker.[contenthash:8].js',
312
- },
313
- ],
314
- }),
315
- ],
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
+ ],
316
317
  };
317
318
  ```
318
319
 
@@ -326,8 +327,11 @@ For example, after resolving the emitted filename:
326
327
 
327
328
  ```ts
328
329
  await initZipPeek({
329
- workerUrl: new URL(`/assets/${zipServiceWorkerFilename}`, window.location.origin).href,
330
- 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,
331
335
  });
332
336
  ```
333
337
 
@@ -342,19 +346,19 @@ zipServiceWorker.a1b2c3d4.js
342
346
  In development, it is usually simpler to copy the worker without a hash:
343
347
 
344
348
  ```js
345
- const CopyPlugin = require('copy-webpack-plugin');
349
+ const CopyPlugin = require("copy-webpack-plugin");
346
350
 
347
351
  module.exports = {
348
- plugins: [
349
- new CopyPlugin({
350
- patterns: [
351
- {
352
- from: require.resolve('zip-peek/zipServiceWorker.js'),
353
- to: 'zipServiceWorker.js',
354
- },
355
- ],
356
- }),
357
- ],
352
+ plugins: [
353
+ new CopyPlugin({
354
+ patterns: [
355
+ {
356
+ from: require.resolve("zip-peek/zipServiceWorker.js"),
357
+ to: "zipServiceWorker.js",
358
+ },
359
+ ],
360
+ }),
361
+ ],
358
362
  };
359
363
  ```
360
364
 
@@ -362,8 +366,8 @@ Then use:
362
366
 
363
367
  ```ts
364
368
  await initZipPeek({
365
- workerUrl: new URL('/zipServiceWorker.js', window.location.origin).href,
366
- 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,
367
371
  });
368
372
  ```
369
373
 
@@ -386,6 +390,10 @@ Extracted assets expire after two hours by default. Set `assetCacheTtlMs` to cus
386
390
 
387
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.
388
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).
396
+
389
397
  ## Limitations
390
398
 
391
399
  - The ZIP URL must end with `.zip`.
package/dist/index.d.ts CHANGED
@@ -1,24 +1,6 @@
1
- import { isZipPackage } from './zip-utils';
2
- export type ZipWorkerConfig = {
3
- zipAssetCacheName?: string;
4
- assetCacheTtlMs?: number;
5
- logPrefix?: string;
6
- };
7
- export type ZipCacheClearingStrategy = 'keep-all' | 'keep-allowed-urls' | 'clear-all';
8
- export type InitZipPeekOptions = {
9
- workerUrl: string;
10
- scopeUrl: string;
11
- reloadOnFirstInstall?: boolean;
12
- cacheClearingStrategy?: ZipCacheClearingStrategy;
13
- allowedZipUrls?: string[];
14
- zipAssetCacheName?: string;
15
- assetCacheTtlMs?: number;
16
- logPrefix?: string;
17
- };
18
- export type InitZipPeekResult = {
19
- reloaded: boolean;
20
- initialized: boolean;
21
- };
1
+ import type { InitZipPeekOptions, InitZipPeekResult } from "./types";
2
+ import { isZipPackage } from "./zip-utils";
3
+ export type { InitZipPeekOptions, InitZipPeekResult, ZipCacheClearingStrategy, ZipWorkerConfig, } from "./types";
22
4
  /**
23
5
  * Registers the zip-peek service worker and configures lazy ZIP asset loading.
24
6
  *
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ function normalizeAllowedZipUrls(allowedZipUrls) {
10
10
  return undefined;
11
11
  }
12
12
  if (allowedZipUrls.length === 0) {
13
- throw new Error('zip-peek: allowedZipUrls must contain at least one ZIP URL when provided.');
13
+ throw new Error("zip-peek: allowedZipUrls must contain at least one ZIP URL when provided.");
14
14
  }
15
15
  const normalized = new Set();
16
16
  for (const zipUrl of allowedZipUrls) {
@@ -18,7 +18,7 @@ function normalizeAllowedZipUrls(allowedZipUrls) {
18
18
  if (!(0, zip_utils_1.isZipPackage)(resolvedZipUrl)) {
19
19
  throw new Error(`zip-peek: allowedZipUrls contains a non-ZIP URL: ${zipUrl}`);
20
20
  }
21
- normalized.add((0, zip_utils_1.normalizeZipUrlForSw)(resolvedZipUrl));
21
+ normalized.add((0, zip_utils_1.normalizeZipUrl)(resolvedZipUrl));
22
22
  }
23
23
  return Array.from(normalized);
24
24
  }
@@ -29,19 +29,19 @@ function postWorkerMessage(message) {
29
29
  }
30
30
  return new Promise((resolve, reject) => {
31
31
  const channel = new MessageChannel();
32
- channel.port1.onmessage = event => {
32
+ channel.port1.onmessage = (event) => {
33
33
  const response = event.data;
34
34
  channel.port1.close();
35
35
  if (!response || response.ok) {
36
36
  resolve();
37
37
  }
38
38
  else {
39
- reject(new Error(response.error ?? 'zip-peek: service worker message failed.'));
39
+ reject(new Error(response.error ?? "zip-peek: service worker message failed."));
40
40
  }
41
41
  };
42
42
  channel.port1.onmessageerror = () => {
43
43
  channel.port1.close();
44
- reject(new Error('zip-peek: failed to receive service worker response.'));
44
+ reject(new Error("zip-peek: failed to receive service worker response."));
45
45
  };
46
46
  controller.postMessage(message, [channel.port2]);
47
47
  });
@@ -56,12 +56,14 @@ function postWorkerMessage(message) {
56
56
  * `cacheClearingStrategy` to keep existing cache entries, keep only allowed
57
57
  * ZIP URLs, or clear all zip-peek cache entries before use.
58
58
  */
59
- async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = 'keep-all', allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, }) {
59
+ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = "keep-all", allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, }) {
60
60
  const normalizedAllowedZipUrls = normalizeAllowedZipUrls(allowedZipUrls);
61
- if (cacheClearingStrategy === 'keep-allowed-urls' && !normalizedAllowedZipUrls) {
61
+ if (cacheClearingStrategy === "keep-allowed-urls" &&
62
+ !normalizedAllowedZipUrls) {
62
63
  throw new Error('zip-peek: cacheClearingStrategy "keep-allowed-urls" requires a non-empty allowedZipUrls allow-list.');
63
64
  }
64
- if (!('serviceWorker' in navigator)) {
65
+ if (!("serviceWorker" in navigator)) {
66
+ console.warn("zip-peek: Service Worker API not supported.");
65
67
  return { reloaded: false, initialized: false };
66
68
  }
67
69
  const reloaded = await (0, registerZipServiceWorker_1.ensureZipServiceWorkerRegistered)({
@@ -73,7 +75,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
73
75
  return { reloaded: true, initialized: false };
74
76
  }
75
77
  await postWorkerMessage({
76
- type: 'ZIP_SW_CONFIG',
78
+ type: "ZIP_SW_CONFIG",
77
79
  config: {
78
80
  zipAssetCacheName,
79
81
  assetCacheTtlMs,
@@ -81,12 +83,12 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
81
83
  allowedZipUrls: normalizedAllowedZipUrls ?? null,
82
84
  },
83
85
  });
84
- if (cacheClearingStrategy === 'clear-all') {
86
+ if (cacheClearingStrategy === "clear-all") {
85
87
  await postWorkerMessage({
86
- type: 'CLEAR_ALL_ZIP_ASSETS',
88
+ type: "CLEAR_ALL_ZIP_ASSETS",
87
89
  });
88
90
  await postWorkerMessage({
89
- type: 'ZIP_SW_CONFIG',
91
+ type: "ZIP_SW_CONFIG",
90
92
  config: {
91
93
  zipAssetCacheName,
92
94
  assetCacheTtlMs,
@@ -95,15 +97,16 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
95
97
  },
96
98
  });
97
99
  }
98
- else if (cacheClearingStrategy === 'keep-allowed-urls' && normalizedAllowedZipUrls) {
100
+ else if (cacheClearingStrategy === "keep-allowed-urls" &&
101
+ normalizedAllowedZipUrls) {
99
102
  await postWorkerMessage({
100
- type: 'CLEAR_ZIP_ASSETS_EXCEPT_ALLOWED',
103
+ type: "CLEAR_ZIP_ASSETS_EXCEPT_ALLOWED",
101
104
  allowedZipUrls: normalizedAllowedZipUrls,
102
105
  });
103
106
  }
104
107
  if (normalizedAllowedZipUrls) {
105
108
  await postWorkerMessage({
106
- type: 'PRELOAD_ZIP_MANIFESTS',
109
+ type: "PRELOAD_ZIP_MANIFESTS",
107
110
  allowedZipUrls: normalizedAllowedZipUrls,
108
111
  });
109
112
  }
@@ -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 });
@@ -1,13 +1,17 @@
1
1
  /** True when the URL points at a `.zip` package (path ends with `.zip`). */
2
2
  export declare function isZipPackage(url: string): boolean;
3
+ export type NormalizeZipUrlOptions = {
4
+ /** Remove cache-buster `t` query param (default: true). */
5
+ stripCacheBuster?: boolean;
6
+ /** Drop fragment `#...` (default: true). */
7
+ stripHash?: boolean;
8
+ };
3
9
  /**
4
- * Strip only the `t` cache-buster query param; keep everything else verbatim.
10
+ * Normalize a ZIP package URL for cache keys, allow-lists, and manifests.
5
11
  * Avoids `URL` / `URLSearchParams` so SigV4 values (e.g. `%2F` in credentials)
6
12
  * are not re-encoded.
7
13
  */
8
- export declare function stripTParam(urlString: string): string;
9
- /** Same key the service worker uses for manifests / purge (presigned query kept). */
10
- export declare const normalizeZipUrlForSw: typeof stripTParam;
14
+ export declare function normalizeZipUrl(urlString: string, options?: NormalizeZipUrlOptions): string;
11
15
  /** Align zip entry names with URL paths under `*.zip/…`. */
12
16
  export declare function normalizeZipEntryPath(name: string): string;
13
17
  export type ParsedZipAssetRequest = {
@@ -24,5 +28,5 @@ export type ParsedZipAssetRequest = {
24
28
  * Returns `null` for bare `.zip` GETs (e.g. manifest bootstrap).
25
29
  */
26
30
  export declare function parseZipAssetRequest(urlString: string): ParsedZipAssetRequest | null;
27
- /** Cache key for a zip asset (decoded manifest key + zip URL without `t`). */
31
+ /** Cache key for a zip asset (decoded manifest key + normalized zip URL). */
28
32
  export declare function canonicalZipAssetRequestUrl(zipUrl: string, manifestKey: string): string;
package/dist/zip-utils.js CHANGED
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeZipUrlForSw = void 0;
4
3
  exports.isZipPackage = isZipPackage;
5
- exports.stripTParam = stripTParam;
4
+ exports.normalizeZipUrl = normalizeZipUrl;
6
5
  exports.normalizeZipEntryPath = normalizeZipEntryPath;
7
6
  exports.parseZipAssetRequest = parseZipAssetRequest;
8
7
  exports.canonicalZipAssetRequestUrl = canonicalZipAssetRequestUrl;
@@ -10,41 +9,46 @@ exports.canonicalZipAssetRequestUrl = canonicalZipAssetRequestUrl;
10
9
  function isZipPackage(url) {
11
10
  try {
12
11
  const u = new URL(url);
13
- return u.pathname.endsWith('.zip');
12
+ return u.pathname.endsWith(".zip");
14
13
  }
15
14
  catch {
16
- return url.split('?')[0].endsWith('.zip');
15
+ return url.split("?")[0].endsWith(".zip");
17
16
  }
18
17
  }
19
18
  /**
20
- * Strip only the `t` cache-buster query param; keep everything else verbatim.
19
+ * Normalize a ZIP package URL for cache keys, allow-lists, and manifests.
21
20
  * Avoids `URL` / `URLSearchParams` so SigV4 values (e.g. `%2F` in credentials)
22
21
  * are not re-encoded.
23
22
  */
24
- function stripTParam(urlString) {
25
- const qIdx = urlString.indexOf('?');
23
+ function normalizeZipUrl(urlString, options = {}) {
24
+ const { stripCacheBuster = true, stripHash = true } = options;
25
+ const withoutHash = (url) => (stripHash ? url.split("#")[0] : url);
26
+ if (!stripCacheBuster) {
27
+ return withoutHash(urlString);
28
+ }
29
+ const qIdx = urlString.indexOf("?");
26
30
  if (qIdx === -1)
27
- return urlString.split('#')[0];
28
- const hashIdx = urlString.indexOf('#', qIdx);
31
+ return withoutHash(urlString);
32
+ const hashIdx = urlString.indexOf("#", qIdx);
29
33
  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('=');
34
+ const querySection = hashIdx === -1
35
+ ? urlString.substring(qIdx + 1)
36
+ : urlString.substring(qIdx + 1, hashIdx);
37
+ const hashSection = stripHash || hashIdx === -1 ? "" : urlString.substring(hashIdx);
38
+ const filtered = querySection.split("&").filter((p) => {
39
+ const eqIdx = p.indexOf("=");
34
40
  const k = eqIdx === -1 ? p : p.substring(0, eqIdx);
35
- return k !== 't';
41
+ return k !== "t";
36
42
  });
37
43
  if (filtered.length === 0)
38
44
  return prefix + hashSection;
39
- return prefix + '?' + filtered.join('&') + hashSection;
45
+ return prefix + "?" + filtered.join("&") + hashSection;
40
46
  }
41
- /** Same key the service worker uses for manifests / purge (presigned query kept). */
42
- exports.normalizeZipUrlForSw = stripTParam;
43
47
  /** Align zip entry names with URL paths under `*.zip/…`. */
44
48
  function normalizeZipEntryPath(name) {
45
- let s = name.replace(/\\/g, '/');
46
- while (s.startsWith('/'))
47
- s = s.slice(1);
49
+ let s = name.replace(/\\/g, "/"); // Replace backslashes with forward slashes.
50
+ while (s.startsWith("/"))
51
+ s = s.slice(1); // Remove leading slashes.
48
52
  return s;
49
53
  }
50
54
  function safeDecodeSegment(s) {
@@ -68,33 +72,33 @@ function parseZipAssetRequest(urlString) {
68
72
  const match = /\.zip([/?])/.exec(urlString);
69
73
  if (!match)
70
74
  return null;
71
- const sepIdx = match.index + 4;
75
+ const sepIdx = match.index + ".zip".length;
72
76
  const separator = match[1];
73
77
  const zipUrlBase = urlString.substring(0, sepIdx);
74
- if (separator === '/') {
78
+ if (separator === "/") {
75
79
  const rest = urlString.substring(sepIdx + 1);
76
- const qIdx = rest.indexOf('?');
80
+ const qIdx = rest.indexOf("?");
77
81
  const rawInternal = qIdx === -1 ? rest : rest.substring(0, qIdx);
78
- if (rawInternal === '')
82
+ if (rawInternal === "")
79
83
  return null;
80
84
  return { zipUrl: zipUrlBase, internalPath: safeDecodeSegment(rawInternal) };
81
85
  }
82
86
  const afterZip = urlString.substring(sepIdx);
83
- const slashIdx = afterZip.indexOf('/');
87
+ const slashIdx = afterZip.indexOf("/");
84
88
  if (slashIdx === -1)
85
89
  return null;
86
90
  const zipQueryPart = afterZip.substring(0, slashIdx);
87
91
  const afterSlash = afterZip.substring(slashIdx + 1);
88
- const qIdx = afterSlash.indexOf('?');
92
+ const qIdx = afterSlash.indexOf("?");
89
93
  const rawInternal = qIdx === -1 ? afterSlash : afterSlash.substring(0, qIdx);
90
- if (rawInternal === '')
94
+ if (rawInternal === "")
91
95
  return null;
92
96
  return {
93
97
  zipUrl: zipUrlBase + zipQueryPart,
94
98
  internalPath: safeDecodeSegment(rawInternal),
95
99
  };
96
100
  }
97
- /** Cache key for a zip asset (decoded manifest key + zip URL without `t`). */
101
+ /** Cache key for a zip asset (decoded manifest key + normalized zip URL). */
98
102
  function canonicalZipAssetRequestUrl(zipUrl, manifestKey) {
99
- return zipUrl + '/' + encodeURI(manifestKey);
103
+ return zipUrl + "/" + encodeURIComponent(manifestKey);
100
104
  }