From b0398f2f0101c80ee3e4cbffbbff8ec51f76fe82 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Sat, 8 Aug 2026 11:15:31 +0900 Subject: [PATCH] feat(kit): check service worker updates on startup --- angular.json | 1 + package-lock.json | 21 ++++++++ package.json | 1 + projects/kit/app-update/ng-package.json | 6 +++ .../src/lib/kit-app-update.provider.spec.ts | 52 ++++++++++++++++++ .../src/lib/kit-app-update.provider.ts | 53 +++++++++++++++++++ projects/kit/app-update/src/public-api.ts | 2 + projects/kit/package.json | 4 ++ projects/kit/tsconfig.spec.json | 1 + 9 files changed, 141 insertions(+) create mode 100644 projects/kit/app-update/ng-package.json create mode 100644 projects/kit/app-update/src/lib/kit-app-update.provider.spec.ts create mode 100644 projects/kit/app-update/src/lib/kit-app-update.provider.ts create mode 100644 projects/kit/app-update/src/public-api.ts diff --git a/angular.json b/angular.json index bff15f1..34fa3d3 100644 --- a/angular.json +++ b/angular.json @@ -262,6 +262,7 @@ "../theme/src/**/*.spec.ts", "../review/src/**/*.spec.ts", "../live-update/src/**/*.spec.ts", + "../app-update/src/**/*.spec.ts", "../offline/src/**/*.spec.ts", "../auth-firebase/src/**/*.spec.ts", "../auth-firebase/social/src/**/*.spec.ts" diff --git a/package-lock.json b/package-lock.json index d680bc7..704e0ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "@angular/fire": "^21.0.0-rc.0", + "@angular/service-worker": "^21.2.17", "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", @@ -1722,6 +1723,26 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/service-worker": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.2.17.tgz", + "integrity": "sha512-6/uKwxBA3udngVHuIVqD8kdMV1whfym9ESB1UyjoNINx+2zj7A749X89tiV0TW2CvWpUYUE7VxpoapEiv5lvfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", diff --git a/package.json b/package.json index 108b9d4..dc2767b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "@angular/fire": "^21.0.0-rc.0", + "@angular/service-worker": "^21.2.17", "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", diff --git a/projects/kit/app-update/ng-package.json b/projects/kit/app-update/ng-package.json new file mode 100644 index 0000000..d0a2dcd --- /dev/null +++ b/projects/kit/app-update/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/kit/app-update/src/lib/kit-app-update.provider.spec.ts b/projects/kit/app-update/src/lib/kit-app-update.provider.spec.ts new file mode 100644 index 0000000..56261ea --- /dev/null +++ b/projects/kit/app-update/src/lib/kit-app-update.provider.spec.ts @@ -0,0 +1,52 @@ +import { ApplicationRef, provideZonelessChangeDetection } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { SwUpdate } from '@angular/service-worker'; +import { BehaviorSubject } from 'rxjs'; +import { describe, expect, it, vi } from 'vitest'; +import { KitAppUpdateService, provideKitAppUpdate } from './kit-app-update.provider'; + +describe('provideKitAppUpdate', () => { + it('activates a complete update and reloads once after startup becomes stable', async () => { + const stable = new BehaviorSubject(false); + const checkForUpdate = vi.fn().mockResolvedValue(true); + const activateUpdate = vi.fn().mockResolvedValue(true); + const reload = vi.fn(); + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideKitAppUpdate(), + { provide: ApplicationRef, useValue: { isStable: stable } }, + { provide: SwUpdate, useValue: { isEnabled: true, checkForUpdate, activateUpdate } }, + { provide: DOCUMENT, useValue: { location: { reload } } }, + ], + }); + + TestBed.inject(KitAppUpdateService); + expect(checkForUpdate).not.toHaveBeenCalled(); + stable.next(true); + await vi.waitFor(() => expect(reload).toHaveBeenCalledOnce()); + expect(checkForUpdate).toHaveBeenCalledOnce(); + expect(activateUpdate).toHaveBeenCalledOnce(); + }); + + it('does not reload when the installed version is current', async () => { + const checkForUpdate = vi.fn().mockResolvedValue(false); + const activateUpdate = vi.fn(); + const reload = vi.fn(); + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideKitAppUpdate(), + { provide: ApplicationRef, useValue: { isStable: new BehaviorSubject(true) } }, + { provide: SwUpdate, useValue: { isEnabled: true, checkForUpdate, activateUpdate } }, + { provide: DOCUMENT, useValue: { location: { reload } } }, + ], + }); + + TestBed.inject(KitAppUpdateService); + await vi.waitFor(() => expect(checkForUpdate).toHaveBeenCalledOnce()); + expect(activateUpdate).not.toHaveBeenCalled(); + expect(reload).not.toHaveBeenCalled(); + }); +}); diff --git a/projects/kit/app-update/src/lib/kit-app-update.provider.ts b/projects/kit/app-update/src/lib/kit-app-update.provider.ts new file mode 100644 index 0000000..d35850d --- /dev/null +++ b/projects/kit/app-update/src/lib/kit-app-update.provider.ts @@ -0,0 +1,53 @@ +import type { EnvironmentProviders } from '@angular/core'; +import { + ApplicationRef, + Injectable, + inject, + makeEnvironmentProviders, + provideEnvironmentInitializer, +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { SwUpdate } from '@angular/service-worker'; +import { filter, first } from 'rxjs'; + +/** Checks for and activates a complete Angular service-worker update when the application starts. */ +@Injectable({ providedIn: 'root' }) +export class KitAppUpdateService { + readonly #applicationRef = inject(ApplicationRef); + readonly #document = inject(DOCUMENT); + readonly #updates = inject(SwUpdate); + #started = false; + + /** Starts one non-blocking update check after Angular reports the application as stable. */ + start(): void { + if (this.#started || !this.#updates.isEnabled) { + return; + } + this.#started = true; + this.#applicationRef.isStable + .pipe( + filter((stable) => stable), + first(), + ) + .subscribe(() => void this.#activateUpdate()); + } + + async #activateUpdate(): Promise { + try { + if (!(await this.#updates.checkForUpdate())) { + return; + } + await this.#updates.activateUpdate(); + this.#document.location?.reload(); + } catch (error) { + console.error('Angular service-worker update check failed', error); + } + } +} + +/** Provides a startup check that activates and reloads into the latest complete web application version. */ +export function provideKitAppUpdate(): EnvironmentProviders { + return makeEnvironmentProviders([ + provideEnvironmentInitializer(() => inject(KitAppUpdateService).start()), + ]); +} diff --git a/projects/kit/app-update/src/public-api.ts b/projects/kit/app-update/src/public-api.ts new file mode 100644 index 0000000..95efce2 --- /dev/null +++ b/projects/kit/app-update/src/public-api.ts @@ -0,0 +1,2 @@ +/** Automatic startup updates for Angular service-worker web applications. */ +export * from './lib/kit-app-update.provider'; diff --git a/projects/kit/package.json b/projects/kit/package.json index 3ac8ba7..9de901a 100644 --- a/projects/kit/package.json +++ b/projects/kit/package.json @@ -10,6 +10,7 @@ "@angular/core": "^21.0.0", "@angular/forms": "^21.0.0", "@angular/router": "^21.0.0", + "@angular/service-worker": "^21.0.0", "@ionic/angular": "^8.0.0", "@ionic/storage-angular": "^4.0.0", "@capacitor/core": ">=6.0.0 <9.0.0", @@ -30,6 +31,9 @@ "rxjs": "^7.8.0" }, "peerDependenciesMeta": { + "@angular/service-worker": { + "optional": true + }, "firebase": { "optional": true }, diff --git a/projects/kit/tsconfig.spec.json b/projects/kit/tsconfig.spec.json index 7954594..566f4c3 100644 --- a/projects/kit/tsconfig.spec.json +++ b/projects/kit/tsconfig.spec.json @@ -13,6 +13,7 @@ "theme/src/**/*.spec.ts", "review/src/**/*.spec.ts", "live-update/src/**/*.spec.ts", + "app-update/src/**/*.spec.ts", "offline/src/**/*.spec.ts", "auth-firebase/src/**/*.spec.ts", "auth-firebase/social/src/**/*.spec.ts"