@vite-pwa/workbox-window 8.0.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/dist/index.js ADDED
@@ -0,0 +1,554 @@
1
+ // src/messageSW.ts
2
+ function messageSW(sw, data) {
3
+ return new Promise((resolve) => {
4
+ const messageChannel = new MessageChannel();
5
+ messageChannel.port1.onmessage = (event) => {
6
+ resolve(event.data);
7
+ };
8
+ sw.postMessage(data, [messageChannel.port2]);
9
+ });
10
+ }
11
+
12
+ // src/utils/dontWaitFor.ts
13
+ function dontWaitFor(promise) {
14
+ void promise.then(() => {
15
+ });
16
+ }
17
+
18
+ // src/utils/logger.ts
19
+ var logger = process.env.NODE_ENV === "production" ? null : (() => {
20
+ if (!("__WB_DISABLE_DEV_LOGS" in globalThis))
21
+ self.__WB_DISABLE_DEV_LOGS = false;
22
+ let inGroup = false;
23
+ const methodToColorMap = {
24
+ debug: "#7f8c8d",
25
+ // Gray
26
+ log: "#2ecc71",
27
+ // Green
28
+ warn: "#f39c12",
29
+ // Yellow
30
+ error: "#c0392b",
31
+ // Red
32
+ groupCollapsed: "#3498db",
33
+ // Blue
34
+ groupEnd: null
35
+ // No colored prefix on groupEnd
36
+ };
37
+ const print = function(method, args) {
38
+ if (self.__WB_DISABLE_DEV_LOGS)
39
+ return;
40
+ if (method === "groupCollapsed") {
41
+ if (typeof navigator !== "undefined" && /^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
42
+ console[method](...args);
43
+ return;
44
+ }
45
+ }
46
+ const styles = [
47
+ `background: ${methodToColorMap[method]}`,
48
+ "border-radius: 0.5em",
49
+ "color: white",
50
+ "font-weight: bold",
51
+ "padding: 2px 0.5em"
52
+ ];
53
+ const logPrefix = inGroup ? [] : ["%c@vite-pwa/workbox-window", styles.join(";")];
54
+ console[method](...logPrefix, ...args);
55
+ if (method === "groupCollapsed")
56
+ inGroup = true;
57
+ if (method === "groupEnd")
58
+ inGroup = false;
59
+ };
60
+ const api = {};
61
+ const loggerMethods = Object.keys(methodToColorMap);
62
+ for (const key of loggerMethods) {
63
+ const method = key;
64
+ api[method] = (...args) => {
65
+ print(method, args);
66
+ };
67
+ }
68
+ return api;
69
+ })();
70
+
71
+ // src/utils/Deferred.ts
72
+ var Deferred = class {
73
+ promise;
74
+ resolve;
75
+ reject;
76
+ /**
77
+ * Creates a promise and exposes its resolve and reject functions as methods.
78
+ */
79
+ constructor() {
80
+ this.promise = new Promise((resolve, reject) => {
81
+ this.resolve = resolve;
82
+ this.reject = reject;
83
+ });
84
+ }
85
+ };
86
+
87
+ // src/utils/WorkboxEventTarget.ts
88
+ var WorkboxEventTarget = class {
89
+ _eventListenerRegistry = /* @__PURE__ */ new Map();
90
+ /**
91
+ * @param {string} type
92
+ * @param {Function} listener
93
+ * @private
94
+ */
95
+ addEventListener(type, listener) {
96
+ const foo = this._getEventListenersByType(type);
97
+ foo.add(listener);
98
+ }
99
+ /**
100
+ * @param {string} type
101
+ * @param {Function} listener
102
+ * @private
103
+ */
104
+ removeEventListener(type, listener) {
105
+ this._getEventListenersByType(type).delete(listener);
106
+ }
107
+ /**
108
+ * @param {object} event
109
+ * @private
110
+ */
111
+ dispatchEvent(event) {
112
+ event.target = this;
113
+ const listeners = this._getEventListenersByType(event.type);
114
+ for (const listener of listeners)
115
+ listener(event);
116
+ }
117
+ /**
118
+ * Returns a Set of listeners associated with the passed event type.
119
+ * If no handlers have been registered, an empty Set is returned.
120
+ *
121
+ * @param {string} type The event type.
122
+ * @return {Set<ListenerCallback>} An array of handler functions.
123
+ * @private
124
+ */
125
+ _getEventListenersByType(type) {
126
+ if (!this._eventListenerRegistry.has(type))
127
+ this._eventListenerRegistry.set(type, /* @__PURE__ */ new Set());
128
+ return this._eventListenerRegistry.get(type);
129
+ }
130
+ };
131
+
132
+ // src/utils/urlsMatch.ts
133
+ function urlsMatch(url1, url2) {
134
+ const { href } = location;
135
+ return new URL(url1, href).href === new URL(url2, href).href;
136
+ }
137
+
138
+ // src/utils/WorkboxEvent.ts
139
+ var WorkboxEvent = class {
140
+ constructor(type, props) {
141
+ this.type = type;
142
+ Object.assign(this, props);
143
+ }
144
+ target;
145
+ sw;
146
+ originalEvent;
147
+ isExternal;
148
+ };
149
+
150
+ // src/Workbox.ts
151
+ var WAITING_TIMEOUT_DURATION = 200;
152
+ var REGISTRATION_TIMEOUT_DURATION = 6e4;
153
+ var SKIP_WAITING_MESSAGE = { type: "SKIP_WAITING" };
154
+ var Workbox = class extends WorkboxEventTarget {
155
+ _scriptURL;
156
+ _registerOptions = {};
157
+ _updateFoundCount = 0;
158
+ // Deferreds we can resolve later.
159
+ _swDeferred = new Deferred();
160
+ _activeDeferred = new Deferred();
161
+ _controllingDeferred = new Deferred();
162
+ _registrationTime = 0;
163
+ _isUpdate;
164
+ _compatibleControllingSW;
165
+ _registration;
166
+ _sw;
167
+ _ownSWs = /* @__PURE__ */ new Set();
168
+ _externalSW;
169
+ _waitingTimeout;
170
+ /**
171
+ * Creates a new Workbox instance with a script URL and service worker
172
+ * options. The script URL and options are the same as those used when
173
+ * calling [navigator.serviceWorker.register(scriptURL, options)](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register).
174
+ *
175
+ * @param {string|TrustedScriptURL} scriptURL The service worker script
176
+ * associated with this instance. Using a
177
+ * [`TrustedScriptURL`](https://web.dev/trusted-types/) is supported.
178
+ * @param {object} [registerOptions] The service worker options associated
179
+ * with this instance.
180
+ */
181
+ // eslint-disable-next-line @typescript-eslint/ban-types
182
+ constructor(scriptURL, registerOptions = {}) {
183
+ super();
184
+ this._scriptURL = scriptURL;
185
+ this._registerOptions = registerOptions;
186
+ navigator.serviceWorker.addEventListener("message", this._onMessage);
187
+ }
188
+ /**
189
+ * Registers a service worker for this instances script URL and service
190
+ * worker options. By default this method delays registration until after
191
+ * the window has loaded.
192
+ *
193
+ * @param {object} [options]
194
+ * @param {Function} [options.immediate] Setting this to true will
195
+ * register the service worker immediately, even if the window has
196
+ * not loaded (not recommended).
197
+ */
198
+ async register({ immediate = false } = {}) {
199
+ if (process.env.NODE_ENV !== "production") {
200
+ if (this._registrationTime) {
201
+ logger.error(
202
+ "Cannot re-register a Workbox instance after it has been registered. Create a new instance instead."
203
+ );
204
+ return;
205
+ }
206
+ }
207
+ if (!immediate && document.readyState !== "complete")
208
+ await new Promise((resolve) => window.addEventListener("load", resolve));
209
+ this._isUpdate = Boolean(navigator.serviceWorker.controller);
210
+ this._compatibleControllingSW = this._getControllingSWIfCompatible();
211
+ this._registration = await this._registerScript();
212
+ if (this._compatibleControllingSW) {
213
+ this._sw = this._compatibleControllingSW;
214
+ this._activeDeferred.resolve(this._compatibleControllingSW);
215
+ this._controllingDeferred.resolve(this._compatibleControllingSW);
216
+ this._compatibleControllingSW.addEventListener(
217
+ "statechange",
218
+ this._onStateChange,
219
+ { once: true }
220
+ );
221
+ }
222
+ const waitingSW = this._registration.waiting;
223
+ if (waitingSW && urlsMatch(waitingSW.scriptURL, this._scriptURL.toString())) {
224
+ this._sw = waitingSW;
225
+ dontWaitFor(
226
+ Promise.resolve().then(() => {
227
+ this.dispatchEvent(
228
+ new WorkboxEvent("waiting", {
229
+ sw: waitingSW,
230
+ wasWaitingBeforeRegister: true
231
+ })
232
+ );
233
+ if (process.env.NODE_ENV !== "production") {
234
+ logger.warn(
235
+ "A service worker was already waiting to activate before this script was registered..."
236
+ );
237
+ }
238
+ })
239
+ );
240
+ }
241
+ if (this._sw) {
242
+ this._swDeferred.resolve(this._sw);
243
+ this._ownSWs.add(this._sw);
244
+ }
245
+ if (process.env.NODE_ENV !== "production") {
246
+ logger.log(
247
+ "Successfully registered service worker.",
248
+ this._scriptURL.toString()
249
+ );
250
+ if (navigator.serviceWorker.controller) {
251
+ if (this._compatibleControllingSW) {
252
+ logger.debug(
253
+ "A service worker with the same script URL is already controlling this page."
254
+ );
255
+ } else {
256
+ logger.debug(
257
+ "A service worker with a different script URL is currently controlling the page. The browser is now fetching the new script now..."
258
+ );
259
+ }
260
+ }
261
+ const currentPageIsOutOfScope = () => {
262
+ const scopeURL = new URL(
263
+ this._registerOptions.scope || this._scriptURL.toString(),
264
+ document.baseURI
265
+ );
266
+ const scopeURLBasePath = new URL("./", scopeURL.href).pathname;
267
+ return !location.pathname.startsWith(scopeURLBasePath);
268
+ };
269
+ if (currentPageIsOutOfScope()) {
270
+ logger.warn(
271
+ "The current page is not in scope for the registered service worker. Was this a mistake?"
272
+ );
273
+ }
274
+ }
275
+ this._registration.addEventListener("updatefound", this._onUpdateFound);
276
+ navigator.serviceWorker.addEventListener(
277
+ "controllerchange",
278
+ this._onControllerChange
279
+ );
280
+ return this._registration;
281
+ }
282
+ /**
283
+ * Checks for updates of the registered service worker.
284
+ */
285
+ async update() {
286
+ if (!this._registration) {
287
+ if (process.env.NODE_ENV !== "production") {
288
+ logger.error(
289
+ "Cannot update a Workbox instance without being registered. Register the Workbox instance first."
290
+ );
291
+ }
292
+ return;
293
+ }
294
+ await this._registration.update();
295
+ }
296
+ /**
297
+ * Resolves to the service worker registered by this instance as soon as it
298
+ * is active. If a service worker was already controlling at registration
299
+ * time then it will resolve to that if the script URLs (and optionally
300
+ * script versions) match, otherwise it will wait until an update is found
301
+ * and activates.
302
+ *
303
+ * @return {Promise<ServiceWorker>}
304
+ */
305
+ get active() {
306
+ return this._activeDeferred.promise;
307
+ }
308
+ /**
309
+ * Resolves to the service worker registered by this instance as soon as it
310
+ * is controlling the page. If a service worker was already controlling at
311
+ * registration time then it will resolve to that if the script URLs (and
312
+ * optionally script versions) match, otherwise it will wait until an update
313
+ * is found and starts controlling the page.
314
+ * Note: the first time a service worker is installed it will active but
315
+ * not start controlling the page unless `clients.claim()` is called in the
316
+ * service worker.
317
+ *
318
+ * @return {Promise<ServiceWorker>}
319
+ */
320
+ get controlling() {
321
+ return this._controllingDeferred.promise;
322
+ }
323
+ /**
324
+ * Resolves with a reference to a service worker that matches the script URL
325
+ * of this instance, as soon as it's available.
326
+ *
327
+ * If, at registration time, there's already an active or waiting service
328
+ * worker with a matching script URL, it will be used (with the waiting
329
+ * service worker taking precedence over the active service worker if both
330
+ * match, since the waiting service worker would have been registered more
331
+ * recently).
332
+ * If there's no matching active or waiting service worker at registration
333
+ * time then the promise will not resolve until an update is found and starts
334
+ * installing, at which point the installing service worker is used.
335
+ *
336
+ * @return {Promise<ServiceWorker>}
337
+ */
338
+ getSW() {
339
+ return this._sw !== void 0 ? Promise.resolve(this._sw) : this._swDeferred.promise;
340
+ }
341
+ /**
342
+ * Sends the passed data object to the service worker registered by this
343
+ * instance (via {@link workbox-window.Workbox#getSW}) and resolves
344
+ * with a response (if any).
345
+ *
346
+ * A response can be set in a message handler in the service worker by
347
+ * calling `event.ports[0].postMessage(...)`, which will resolve the promise
348
+ * returned by `messageSW()`. If no response is set, the promise will never
349
+ * resolve.
350
+ *
351
+ * @param {object} data An object to send to the service worker
352
+ * @return {Promise<object>}
353
+ */
354
+ // We might be able to change the 'data' type to Record<string, unknown> in the future.
355
+ async messageSW(data) {
356
+ const sw = await this.getSW();
357
+ return messageSW(sw, data);
358
+ }
359
+ /**
360
+ * Sends a `{type: 'SKIP_WAITING'}` message to the service worker that's
361
+ * currently in the `waiting` state associated with the current registration.
362
+ *
363
+ * If there is no current registration or no service worker is `waiting`,
364
+ * calling this will have no effect.
365
+ */
366
+ messageSkipWaiting() {
367
+ if (this._registration && this._registration.waiting)
368
+ void messageSW(this._registration.waiting, SKIP_WAITING_MESSAGE);
369
+ }
370
+ /**
371
+ * Checks for a service worker already controlling the page and returns
372
+ * it if its script URL matches.
373
+ *
374
+ * @private
375
+ * @return {ServiceWorker|undefined}
376
+ */
377
+ _getControllingSWIfCompatible() {
378
+ const controller = navigator.serviceWorker.controller;
379
+ if (controller && urlsMatch(controller.scriptURL, this._scriptURL.toString()))
380
+ return controller;
381
+ else
382
+ return void 0;
383
+ }
384
+ /**
385
+ * Registers a service worker for this instances script URL and register
386
+ * options and tracks the time registration was complete.
387
+ *
388
+ * @private
389
+ */
390
+ async _registerScript() {
391
+ try {
392
+ const reg = await navigator.serviceWorker.register(
393
+ this._scriptURL,
394
+ this._registerOptions
395
+ );
396
+ this._registrationTime = performance.now();
397
+ return reg;
398
+ } catch (error) {
399
+ if (process.env.NODE_ENV !== "production")
400
+ logger.error(error);
401
+ throw error;
402
+ }
403
+ }
404
+ /**
405
+ * @private
406
+ */
407
+ _onUpdateFound = (originalEvent) => {
408
+ const registration = this._registration;
409
+ const installingSW = registration.installing;
410
+ const updateLikelyTriggeredExternally = !!(this._updateFoundCount > 0 || !urlsMatch(installingSW.scriptURL, this._scriptURL.toString()) || performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION);
411
+ if (updateLikelyTriggeredExternally) {
412
+ this._externalSW = installingSW;
413
+ registration.removeEventListener("updatefound", this._onUpdateFound);
414
+ } else {
415
+ this._sw = installingSW;
416
+ this._ownSWs.add(installingSW);
417
+ this._swDeferred.resolve(installingSW);
418
+ if (process.env.NODE_ENV !== "production") {
419
+ if (navigator.serviceWorker.controller)
420
+ logger.log("Updated service worker found. Installing now...");
421
+ else
422
+ logger.log("Service worker is installing...");
423
+ }
424
+ }
425
+ this.dispatchEvent(
426
+ new WorkboxEvent("installing", {
427
+ sw: installingSW,
428
+ originalEvent,
429
+ isExternal: updateLikelyTriggeredExternally,
430
+ isUpdate: this._isUpdate
431
+ })
432
+ );
433
+ ++this._updateFoundCount;
434
+ installingSW.addEventListener("statechange", this._onStateChange);
435
+ };
436
+ /**
437
+ * @private
438
+ * @param {Event} originalEvent
439
+ */
440
+ _onStateChange = (originalEvent) => {
441
+ const registration = this._registration;
442
+ const sw = originalEvent.target;
443
+ const { state } = sw;
444
+ const isExternal = sw === this._externalSW;
445
+ const eventProps = {
446
+ sw,
447
+ isExternal,
448
+ originalEvent
449
+ };
450
+ if (!isExternal && this._isUpdate)
451
+ eventProps.isUpdate = true;
452
+ this.dispatchEvent(
453
+ new WorkboxEvent(state, eventProps)
454
+ );
455
+ if (state === "installed") {
456
+ this._waitingTimeout = self.setTimeout(() => {
457
+ if (state === "installed" && registration.waiting === sw) {
458
+ this.dispatchEvent(new WorkboxEvent("waiting", eventProps));
459
+ if (process.env.NODE_ENV !== "production") {
460
+ if (isExternal) {
461
+ logger.warn(
462
+ "An external service worker has installed but is waiting for this client to close before activating..."
463
+ );
464
+ } else {
465
+ logger.warn(
466
+ "The service worker has installed but is waiting for existing clients to close before activating..."
467
+ );
468
+ }
469
+ }
470
+ }
471
+ }, WAITING_TIMEOUT_DURATION);
472
+ } else if (state === "activating") {
473
+ clearTimeout(this._waitingTimeout);
474
+ if (!isExternal)
475
+ this._activeDeferred.resolve(sw);
476
+ }
477
+ if (process.env.NODE_ENV !== "production") {
478
+ switch (state) {
479
+ case "installed":
480
+ if (isExternal) {
481
+ logger.warn(
482
+ "An external service worker has installed. You may want to suggest users reload this page."
483
+ );
484
+ } else {
485
+ logger.log("Registered service worker installed.");
486
+ }
487
+ break;
488
+ case "activated":
489
+ if (isExternal) {
490
+ logger.warn("An external service worker has activated.");
491
+ } else {
492
+ logger.log("Registered service worker activated.");
493
+ if (sw !== navigator.serviceWorker.controller) {
494
+ logger.warn(
495
+ "The registered service worker is active but not yet controlling the page. Reload or run `clients.claim()` in the service worker."
496
+ );
497
+ }
498
+ }
499
+ break;
500
+ case "redundant":
501
+ if (sw === this._compatibleControllingSW)
502
+ logger.log("Previously controlling service worker now redundant!");
503
+ else if (!isExternal)
504
+ logger.log("Registered service worker now redundant!");
505
+ break;
506
+ }
507
+ }
508
+ };
509
+ /**
510
+ * @private
511
+ * @param {Event} originalEvent
512
+ */
513
+ _onControllerChange = (originalEvent) => {
514
+ const sw = this._sw;
515
+ const isExternal = sw !== navigator.serviceWorker.controller;
516
+ this.dispatchEvent(
517
+ new WorkboxEvent("controlling", {
518
+ isExternal,
519
+ originalEvent,
520
+ sw,
521
+ isUpdate: this._isUpdate
522
+ })
523
+ );
524
+ if (!isExternal) {
525
+ if (process.env.NODE_ENV !== "production")
526
+ logger.log("Registered service worker now controlling this page.");
527
+ this._controllingDeferred.resolve(sw);
528
+ }
529
+ };
530
+ /**
531
+ * @private
532
+ * @param {Event} originalEvent
533
+ */
534
+ _onMessage = async (originalEvent) => {
535
+ const { data, ports, source } = originalEvent;
536
+ await this.getSW();
537
+ if (this._ownSWs.has(source)) {
538
+ this.dispatchEvent(
539
+ new WorkboxEvent("message", {
540
+ // Can't change type 'any' of data.
541
+ data,
542
+ originalEvent,
543
+ ports,
544
+ sw: source
545
+ })
546
+ );
547
+ }
548
+ };
549
+ };
550
+ export {
551
+ Workbox,
552
+ WorkboxEvent,
553
+ messageSW
554
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@vite-pwa/workbox-window",
3
+ "type": "module",
4
+ "version": "8.0.0",
5
+ "packageManager": "pnpm@8.13.1",
6
+ "description": "Simplifies communications with Workbox packages running in the service worker",
7
+ "author": "Google's Web DevRel Team, Vite PWA's Team",
8
+ "license": "MIT",
9
+ "funding": "https://github.com/sponsors/antfu",
10
+ "homepage": "https://github.com/vite-pwa/workbox-window-es#readme",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/vite-pwa/workbox-window-es.git"
14
+ },
15
+ "bugs": "https://github.com/vite-pwa/workbox-window-es/issues",
16
+ "keywords": [
17
+ "workbox",
18
+ "workboxjs",
19
+ "service worker",
20
+ "sw",
21
+ "window",
22
+ "message",
23
+ "postMessage"
24
+ ],
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "main": "dist/index.cjs",
33
+ "module": "dist/index.js",
34
+ "types": "dist/index.d.ts",
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsup src/index.ts --dts --target esnext --format esm,cjs",
40
+ "lint": "eslint .",
41
+ "lint-fix": "nr lint --fix",
42
+ "prepublishOnly": "npm run build",
43
+ "release": "bumpp && npm publish --access=public"
44
+ },
45
+ "dependencies": {},
46
+ "devDependencies": {
47
+ "@antfu/eslint-config": "^0.43.1",
48
+ "@antfu/ni": "^0.21.12",
49
+ "@types/node": "^18.15.3",
50
+ "@types/trusted-types": "^2.0.7",
51
+ "@typescript-eslint/eslint-plugin": "^6.13.2",
52
+ "bumpp": "^9.2.0",
53
+ "eslint": "^8.55.0",
54
+ "tsup": "^8.0.1",
55
+ "typescript": "^5.3.3"
56
+ }
57
+ }