From be24f9f0cb522c2626dff07ddca0045b0a60b806 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 28 Nov 2017 10:13:52 -0800 Subject: [PATCH] feat(docs-infra): Convert AIO to use the new Service Worker 5.0.0. (#19795) AIO is currently using a beta version of @angular/service-worker. Since that was implemented, the SW has been rewritten and released as part of Angular 5.0.0. This commit updates AIO to use the latest implementation, with an appropriate configuration file that caches the various AIO assets in useful ways. PR Close #19795 --- aio/README.md | 11 +- .../marketing/home/{ => _unused}/ng-atl.png | Bin aio/firebase.json | 2 +- aio/ngsw-manifest.json | 26 --- aio/package.json | 8 +- aio/scripts/_payload-limits.json | 4 +- aio/src/app/app.module.ts | 4 + .../code/pretty-printer.service.ts | 2 + .../app/custom-elements/toc/toc.component.ts | 1 + aio/src/app/documents/document.service.ts | 1 + aio/src/app/search/search-worker.js | 2 +- aio/src/app/sw-updates/sw-updates.module.ts | 4 - .../app/sw-updates/sw-updates.service.spec.ts | 184 +++++++++++------- aio/src/app/sw-updates/sw-updates.service.ts | 78 ++++---- aio/src/index.html | 2 +- aio/src/main.ts | 13 +- aio/src/ngsw-config.json | 75 +++++++ .../unit/testServiceWorkerRoutes.spec.ts | 3 +- aio/yarn.lock | 15 +- 19 files changed, 254 insertions(+), 181 deletions(-) rename aio/content/images/marketing/home/{ => _unused}/ng-atl.png (100%) delete mode 100644 aio/ngsw-manifest.json create mode 100644 aio/src/ngsw-config.json diff --git a/aio/README.md b/aio/README.md index a773f7f652c..71b778238c5 100644 --- a/aio/README.md +++ b/aio/README.md @@ -56,14 +56,9 @@ It's necessary to remove the temporary files, because otherwise they're displaye ## Using ServiceWorker locally -Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which -would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible -with webpack serving the files from memory). - -If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/` -with `yarn http-server dist -p 4200`. - -For more details see #16745. +Running `yarn start` (even when explicitly targeting production mode) does not set up the +ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then +serve the files in `dist/` with `yarn http-server dist -p 4200`. ## Guide to authoring diff --git a/aio/content/images/marketing/home/ng-atl.png b/aio/content/images/marketing/home/_unused/ng-atl.png similarity index 100% rename from aio/content/images/marketing/home/ng-atl.png rename to aio/content/images/marketing/home/_unused/ng-atl.png diff --git a/aio/firebase.json b/aio/firebase.json index 57fee90f7a5..c801f78c58d 100644 --- a/aio/firebase.json +++ b/aio/firebase.json @@ -9,7 +9,7 @@ ////////////////////////////////////////////////////////////////////////////////////////////// // README: // Redirects must also be handled by the ServiceWorker. If you add a redirect rule here, - // make sure the routing RegExp in `ngsw-manifest.json` is updated accordingly. + // make sure it is compatible with the configuration in `ngsw-config.json`. ////////////////////////////////////////////////////////////////////////////////////////////// // A random bad indexed page that used `api/api` diff --git a/aio/ngsw-manifest.json b/aio/ngsw-manifest.json deleted file mode 100644 index 8eed08fd187..00000000000 --- a/aio/ngsw-manifest.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "external": { - "urls": [ - {"url": "https://fonts.googleapis.com/css?family=Droid+Sans+Mono"}, - {"url": "https://fonts.gstatic.com/s/droidsansmono/v7/ns-m2xQYezAtqh7ai59hJYdJ2JT0J65PSe7wdxAnx_I.woff2"}, - {"url": "https://fonts.googleapis.com/icon?family=Material+Icons"}, - {"url": "https://fonts.gstatic.com/s/materialicons/v22/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2"}, - {"url": "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"} - ] - }, - "static.ignore": [ - "\\.js\\.map$", - "^(?:/|\\\\)generated(?:/|\\\\)(?:docs(?:/|\\\\)(?!api(?:/|\\\\)api-list\\.json).*|images(?:/|\\\\)(?!marketing(?:/|\\\\)).*|live-examples|zips)(?:/|\\\\)" - ], - "static.versioned": [ - "\\.[0-9a-z]{20}\\." - ], - "routing": { - "index": "/index.html", - "routes": { - "^(?!/styleguide|/docs/.|(?:/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref)|learning-angular|webpack)|/news)(?:\\.html|/)?$|/testing|/api/(?:.+/[^/]+-|platform-browser/AnimationDriver|testing/|api/|animate/|(?:common/(?:NgModel|Control|MaxLengthValidator))|(?:[^/]+/)?(?:NgFor(?:$|-)|AnimationStateDeclarationMetadata|CORE_DIRECTIVES|PLATFORM_PIPES|DirectiveMetadata|HTTP_PROVIDERS))|.*/stackblitz(?:\\.html)?(?:\\?.*)?$|.*\\.[^\/.]+$)": { - "match": "regex" - } - } - } -} diff --git a/aio/package.json b/aio/package.json index 7b06930fc30..0d7237f6c8e 100644 --- a/aio/package.json +++ b/aio/package.json @@ -8,7 +8,7 @@ "scripts": { "preinstall": "node ../tools/yarn/check-yarn.js", "postinstall": "node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", - "aio-use-local": "node tools/ng-packages-installer overwrite . --debug --ignore-packages @angular/service-worker", + "aio-use-local": "node tools/ng-packages-installer overwrite . --debug", "aio-use-npm": "node tools/ng-packages-installer restore .", "aio-check-local": "node tools/ng-packages-installer check .", "ng": "yarn check-env && ng", @@ -55,14 +55,12 @@ "boilerplate:test": "node tools/examples/test.js", "generate-stackblitz": "node ./tools/stackblitz-builder/generateStackblitz", "generate-zips": "node ./tools/example-zipper/generateZips", - "sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json", - "sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/", "build-404-page": "node scripts/build-404-page", "build-ie-polyfills": "yarn webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production", "update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG", "~~check-env": "node scripts/check-environment", "~~build": "ng build", - "post~~build": "yarn build-404-page && yarn sw-manifest && yarn sw-copy" + "post~~build": "yarn build-404-page" }, "engines": { "node": ">=8.9.1 <9.0.0", @@ -82,7 +80,7 @@ "@angular/platform-browser-dynamic": "6.0.0", "@angular/platform-server": "6.0.0", "@angular/router": "6.0.0", - "@angular/service-worker": "^1.0.0-beta.16", + "@angular/service-worker": "6.0.0", "@webcomponents/custom-elements": "^1.0.8", "classlist.js": "^1.1.20150312", "core-js": "^2.4.1", diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index b02773aeef4..932d5df02e7 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -2,8 +2,8 @@ "aio": { "master": { "uncompressed": { - "runtime": 2768, - "main": 475855, + "runtime": 2712, + "main": 458226, "polyfills": 38453, "prettify": 14913 } diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index a3e89e24ce2..79bf8374e75 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ServiceWorkerModule } from '@angular/service-worker'; import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; @@ -40,6 +41,8 @@ import { CustomElementsModule } from 'app/custom-elements/custom-elements.module import { SharedModule } from 'app/shared/shared.module'; import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module'; +import {environment} from '../environments/environment'; + // These are the hardcoded inline svg sources to be used by the `` component export const svgIconProviders = [ { @@ -99,6 +102,7 @@ export const svgIconProviders = [ MatToolbarModule, SwUpdatesModule, SharedModule, + ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}), ], declarations: [ AppComponent, diff --git a/aio/src/app/custom-elements/code/pretty-printer.service.ts b/aio/src/app/custom-elements/code/pretty-printer.service.ts index d56a3a27fc5..d80ca8e6f3b 100644 --- a/aio/src/app/custom-elements/code/pretty-printer.service.ts +++ b/aio/src/app/custom-elements/code/pretty-printer.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { from as fromPromise, Observable } from 'rxjs'; import { first, map, share } from 'rxjs/operators'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/share'; import { Logger } from 'app/shared/logger.service'; diff --git a/aio/src/app/custom-elements/toc/toc.component.ts b/aio/src/app/custom-elements/toc/toc.component.ts index b79736af078..bfe545e06ff 100644 --- a/aio/src/app/custom-elements/toc/toc.component.ts +++ b/aio/src/app/custom-elements/toc/toc.component.ts @@ -1,6 +1,7 @@ import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import { asapScheduler as asap, combineLatest, Subject } from 'rxjs'; import { startWith, subscribeOn, takeUntil } from 'rxjs/operators'; +import 'rxjs/add/operator/startWith'; import { ScrollService } from 'app/shared/scroll.service'; import { TocItem, TocService } from 'app/shared/toc.service'; diff --git a/aio/src/app/documents/document.service.ts b/aio/src/app/documents/document.service.ts index 432e1c4ab2d..3d570e88fe8 100644 --- a/aio/src/app/documents/document.service.ts +++ b/aio/src/app/documents/document.service.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { AsyncSubject, Observable, of } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; +import 'rxjs/add/operator/do'; import { DocumentContents } from './document-contents'; export { DocumentContents } from './document-contents'; diff --git a/aio/src/app/search/search-worker.js b/aio/src/app/search/search-worker.js index 0fc98c1ff88..503a75aaa80 100644 --- a/aio/src/app/search/search-worker.js +++ b/aio/src/app/search/search-worker.js @@ -5,7 +5,7 @@ var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json'; -// NOTE: This needs to be kept in sync with `ngsw-manifest.json`. +// NOTE: This needs to be kept in sync with `ngsw-config.json`. importScripts('/assets/js/lunr.min.js'); var index; diff --git a/aio/src/app/sw-updates/sw-updates.module.ts b/aio/src/app/sw-updates/sw-updates.module.ts index 573f64d3e74..065f871708c 100644 --- a/aio/src/app/sw-updates/sw-updates.module.ts +++ b/aio/src/app/sw-updates/sw-updates.module.ts @@ -1,13 +1,9 @@ import { NgModule } from '@angular/core'; -import { ServiceWorkerModule } from '@angular/service-worker'; import { SwUpdatesService } from './sw-updates.service'; @NgModule({ - imports: [ - ServiceWorkerModule - ], providers: [ SwUpdatesService ] diff --git a/aio/src/app/sw-updates/sw-updates.service.spec.ts b/aio/src/app/sw-updates/sw-updates.service.spec.ts index 12a48436ea7..e99330d663f 100644 --- a/aio/src/app/sw-updates/sw-updates.service.spec.ts +++ b/aio/src/app/sw-updates/sw-updates.service.spec.ts @@ -1,41 +1,41 @@ import { ApplicationRef, ReflectiveInjector } from '@angular/core'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { NgServiceWorker } from '@angular/service-worker'; +import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing'; +import { SwUpdate } from '@angular/service-worker'; import { Subject } from 'rxjs'; -import { take } from 'rxjs/operators'; import { Logger } from 'app/shared/logger.service'; import { SwUpdatesService } from './sw-updates.service'; + describe('SwUpdatesService', () => { let injector: ReflectiveInjector; let appRef: MockApplicationRef; let service: SwUpdatesService; - let sw: MockNgServiceWorker; + let swu: MockSwUpdate; let checkInterval: number; // Helpers // NOTE: - // Because `SwUpdatesService` uses the `debounceTime` operator, it needs to be instantiated and + // Because `SwUpdatesService` uses the `interval` operator, it needs to be instantiated and // destroyed inside the `fakeAsync` zone (when `fakeAsync` is used for the test). Thus, we can't // run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper // to call them inside each test's zone. - const setup = () => { + const setup = (isSwUpdateEnabled: boolean) => { injector = ReflectiveInjector.resolveAndCreate([ { provide: ApplicationRef, useClass: MockApplicationRef }, { provide: Logger, useClass: MockLogger }, - { provide: NgServiceWorker, useClass: MockNgServiceWorker }, + { provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) }, SwUpdatesService ]); appRef = injector.get(ApplicationRef); service = injector.get(SwUpdatesService); - sw = injector.get(NgServiceWorker); + swu = injector.get(SwUpdate); checkInterval = (service as any).checkInterval; }; const tearDown = () => service.ngOnDestroy(); - const run = (specFn: VoidFunction) => () => { - setup(); + const run = (specFn: VoidFunction, isSwUpdateEnabled = true) => () => { + setup(isSwUpdateEnabled); specFn(); tearDown(); }; @@ -46,109 +46,153 @@ describe('SwUpdatesService', () => { })); it('should start checking for updates when instantiated (once the app stabilizes)', run(() => { - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).not.toHaveBeenCalled(); appRef.isStable.next(false); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).not.toHaveBeenCalled(); appRef.isStable.next(true); - expect(sw.checkForUpdate).toHaveBeenCalled(); + expect(swu.checkForUpdate).toHaveBeenCalled(); })); - it('should schedule a new check if there is no update available', fakeAsync(run(() => { + it('should periodically check for updates', fakeAsync(run(() => { appRef.isStable.next(true); - sw.checkForUpdate.calls.reset(); - - sw.$$checkForUpdateSubj.next(false); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + swu.checkForUpdate.calls.reset(); tick(checkInterval); - expect(sw.checkForUpdate).toHaveBeenCalled(); - expect(sw.activateUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(1); + + tick(checkInterval); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(2); + + appRef.isStable.next(false); + + tick(checkInterval); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(3); + + discardPeriodicTasks(); }))); - it('should activate new updates immediately', fakeAsync(run(() => { + it('should activate available updates immediately', fakeAsync(run(() => { appRef.isStable.next(true); - sw.checkForUpdate.calls.reset(); + expect(swu.activateUpdate).not.toHaveBeenCalled(); - sw.$$checkForUpdateSubj.next(true); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); - - tick(checkInterval); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); - expect(sw.activateUpdate).toHaveBeenCalled(); + swu.$$availableSubj.next({available: {hash: 'foo'}}); + expect(swu.activateUpdate).toHaveBeenCalled(); }))); - it('should not pass a specific version to `NgServiceWorker.activateUpdate()`', fakeAsync(run(() => { + it('should keep periodically checking for updates even after one is available/activated', fakeAsync(run(() => { appRef.isStable.next(true); - sw.$$checkForUpdateSubj.next(true); - tick(checkInterval); - - expect(sw.activateUpdate).toHaveBeenCalledWith(null); - }))); - - it('should schedule a new check after activating the update', fakeAsync(run(() => { - appRef.isStable.next(true); - sw.checkForUpdate.calls.reset(); - sw.$$checkForUpdateSubj.next(true); + swu.checkForUpdate.calls.reset(); tick(checkInterval); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(1); - sw.$$activateUpdateSubj.next(); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + swu.$$availableSubj.next({available: {hash: 'foo'}}); tick(checkInterval); - expect(sw.checkForUpdate).toHaveBeenCalled(); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(2); + + tick(checkInterval); + expect(swu.checkForUpdate).toHaveBeenCalledTimes(3); + + discardPeriodicTasks(); }))); it('should emit on `updateActivated` when an update has been activated', run(() => { const activatedVersions: (string|undefined)[] = []; service.updateActivated.subscribe(v => activatedVersions.push(v)); - sw.$$updatesSubj.next({type: 'pending', version: 'foo'}); - sw.$$updatesSubj.next({type: 'activation', version: 'bar'}); - sw.$$updatesSubj.next({type: 'pending', version: 'baz'}); - sw.$$updatesSubj.next({type: 'activation', version: 'qux'}); + swu.$$availableSubj.next({available: {hash: 'foo'}}); + swu.$$activatedSubj.next({current: {hash: 'bar'}}); + swu.$$availableSubj.next({available: {hash: 'baz'}}); + swu.$$activatedSubj.next({current: {hash: 'qux'}}); expect(activatedVersions).toEqual(['bar', 'qux']); })); + describe('when `SwUpdate` is not enabled', () => { + const runDeactivated = (specFn: VoidFunction) => run(specFn, false); + + it('should not check for updates', fakeAsync(runDeactivated(() => { + appRef.isStable.next(true); + + tick(checkInterval); + tick(checkInterval); + + swu.$$availableSubj.next({available: {hash: 'foo'}}); + swu.$$activatedSubj.next({current: {hash: 'bar'}}); + + tick(checkInterval); + tick(checkInterval); + + expect(swu.checkForUpdate).not.toHaveBeenCalled(); + }))); + + it('should not activate available updates', fakeAsync(runDeactivated(() => { + swu.$$availableSubj.next({available: {hash: 'foo'}}); + expect(swu.activateUpdate).not.toHaveBeenCalled(); + }))); + + it('should never emit on `updateActivated`', runDeactivated(() => { + const activatedVersions: (string|undefined)[] = []; + service.updateActivated.subscribe(v => activatedVersions.push(v)); + + swu.$$availableSubj.next({available: {hash: 'foo'}}); + swu.$$activatedSubj.next({current: {hash: 'bar'}}); + swu.$$availableSubj.next({available: {hash: 'baz'}}); + swu.$$activatedSubj.next({current: {hash: 'qux'}}); + + expect(activatedVersions).toEqual([]); + })); + }); + describe('when destroyed', () => { it('should not schedule a new check for update (after current check)', fakeAsync(run(() => { appRef.isStable.next(true); - sw.checkForUpdate.calls.reset(); + expect(swu.checkForUpdate).toHaveBeenCalled(); service.ngOnDestroy(); - sw.$$checkForUpdateSubj.next(false); + swu.checkForUpdate.calls.reset(); + + tick(checkInterval); tick(checkInterval); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).not.toHaveBeenCalled(); }))); it('should not schedule a new check for update (after activating an update)', fakeAsync(run(() => { appRef.isStable.next(true); - sw.checkForUpdate.calls.reset(); - - sw.$$checkForUpdateSubj.next(true); - expect(sw.activateUpdate).toHaveBeenCalled(); + expect(swu.checkForUpdate).toHaveBeenCalled(); service.ngOnDestroy(); - sw.$$activateUpdateSubj.next(); + swu.checkForUpdate.calls.reset(); + + swu.$$availableSubj.next({available: {hash: 'foo'}}); + swu.$$activatedSubj.next({current: {hash: 'baz'}}); + + tick(checkInterval); tick(checkInterval); - expect(sw.checkForUpdate).not.toHaveBeenCalled(); + expect(swu.checkForUpdate).not.toHaveBeenCalled(); + }))); + + it('should not activate available updates', fakeAsync(run(() => { + service.ngOnDestroy(); + swu.$$availableSubj.next({available: {hash: 'foo'}}); + + expect(swu.activateUpdate).not.toHaveBeenCalled(); }))); it('should stop emitting on `updateActivated`', run(() => { const activatedVersions: (string|undefined)[] = []; service.updateActivated.subscribe(v => activatedVersions.push(v)); - sw.$$updatesSubj.next({type: 'pending', version: 'foo'}); - sw.$$updatesSubj.next({type: 'activation', version: 'bar'}); + swu.$$availableSubj.next({available: {hash: 'foo'}}); + swu.$$activatedSubj.next({current: {hash: 'bar'}}); service.ngOnDestroy(); - sw.$$updatesSubj.next({type: 'pending', version: 'baz'}); - sw.$$updatesSubj.next({type: 'activation', version: 'qux'}); + swu.$$availableSubj.next({available: {hash: 'baz'}}); + swu.$$activatedSubj.next({current: {hash: 'qux'}}); expect(activatedVersions).toEqual(['bar']); })); @@ -164,16 +208,18 @@ class MockLogger { log = jasmine.createSpy('MockLogger.log'); } -class MockNgServiceWorker { - $$activateUpdateSubj = new Subject(); - $$checkForUpdateSubj = new Subject(); - $$updatesSubj = new Subject<{type: string, version: string}>(); +class MockSwUpdate { + $$availableSubj = new Subject<{available: {hash: string}}>(); + $$activatedSubj = new Subject<{current: {hash: string}}>(); - updates = this.$$updatesSubj.asObservable(); + available = this.$$availableSubj.asObservable(); + activated = this.$$activatedSubj.asObservable(); - activateUpdate = jasmine.createSpy('MockNgServiceWorker.activateUpdate') - .and.callFake(() => this.$$activateUpdateSubj.pipe(take(1))); + activateUpdate = jasmine.createSpy('MockSwUpdate.activateUpdate') + .and.callFake(() => Promise.resolve()); - checkForUpdate = jasmine.createSpy('MockNgServiceWorker.checkForUpdate') - .and.callFake(() => this.$$checkForUpdateSubj.pipe(take(1))); + checkForUpdate = jasmine.createSpy('MockSwUpdate.checkForUpdate') + .and.callFake(() => Promise.resolve()); + + constructor(public isEnabled: boolean) {} } diff --git a/aio/src/app/sw-updates/sw-updates.service.ts b/aio/src/app/sw-updates/sw-updates.service.ts index e858990eddf..ccbfdc7ddcc 100644 --- a/aio/src/app/sw-updates/sw-updates.service.ts +++ b/aio/src/app/sw-updates/sw-updates.service.ts @@ -1,7 +1,7 @@ import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; -import { NgServiceWorker } from '@angular/service-worker'; -import { concat, Subject } from 'rxjs'; -import { debounceTime, defaultIfEmpty, filter, first, map, startWith, takeUntil, tap } from 'rxjs/operators'; +import { SwUpdate } from '@angular/service-worker'; +import { concat, interval, NEVER, Observable, Subject } from 'rxjs'; +import { first, map, takeUntil, tap } from 'rxjs/operators'; import { Logger } from 'app/shared/logger.service'; @@ -11,63 +11,55 @@ import { Logger } from 'app/shared/logger.service'; * * @description * 1. Checks for available ServiceWorker updates once instantiated. - * 2. As long as there is no update available, re-checks every 6 hours. - * 3. As soon as an update is detected, it activates the update and notifies interested parties. - * 4. It continues to check for available updates. + * 2. Re-checks every 6 hours. + * 3. Whenever an update is available, it activates the update. * * @property * `updateActivated` {Observable} - Emit the version hash whenever an update is activated. */ @Injectable() export class SwUpdatesService implements OnDestroy { - private checkInterval = 1000 * 60 * 60 * 6; // 6 hours + private checkInterval = 1000 * 60 * 60 * 6; // 6 hours private onDestroy = new Subject(); - private checkForUpdateSubj = new Subject(); - updateActivated = this.sw.updates.pipe( - takeUntil(this.onDestroy), - tap(evt => this.log(`Update event: ${JSON.stringify(evt)}`)), - filter(({type}) => type === 'activation'), - map(({version}) => version), - ); + updateActivated: Observable; - constructor(appRef: ApplicationRef, private logger: Logger, private sw: NgServiceWorker) { - const appIsStable$ = appRef.isStable.pipe(first(v => v)); - const checkForUpdates$ = this.checkForUpdateSubj.pipe(debounceTime(this.checkInterval), startWith(undefined)); + constructor(appRef: ApplicationRef, private logger: Logger, private swu: SwUpdate) { + if (!swu.isEnabled) { + this.updateActivated = NEVER.pipe(takeUntil(this.onDestroy)); + return; + } - concat(appIsStable$, checkForUpdates$) - .pipe(takeUntil(this.onDestroy)) - .subscribe(() => this.checkForUpdate()); + // Periodically check for updates (after the app is stabilized). + const appIsStable = appRef.isStable.pipe(first(v => v)); + concat(appIsStable, interval(this.checkInterval)) + .pipe( + tap(() => this.log('Checking for update...')), + takeUntil(this.onDestroy), + ) + .subscribe(() => this.swu.checkForUpdate()); + + // Activate available updates. + this.swu.available + .pipe( + tap(evt => this.log(`Update available: ${JSON.stringify(evt)}`)), + takeUntil(this.onDestroy), + ) + .subscribe(() => this.swu.activateUpdate()); + + // Notify about activated updates. + this.updateActivated = this.swu.activated.pipe( + tap(evt => this.log(`Update activated: ${JSON.stringify(evt)}`)), + map(evt => evt.current.hash), + takeUntil(this.onDestroy), + ); } ngOnDestroy() { this.onDestroy.next(); } - private activateUpdate() { - this.log('Activating update...'); - this.sw.activateUpdate(null as any) // expects a non-null string - .subscribe(() => this.scheduleCheckForUpdate()); - } - - private checkForUpdate() { - this.log('Checking for update...'); - this.sw.checkForUpdate() - .pipe( - // Temp workaround for https://github.com/angular/mobile-toolkit/pull/137. - // TODO (gkalpak): Remove once #137 is fixed. - defaultIfEmpty(false), - first(), - tap(v => this.log(`Update available: ${v}`)), - ) - .subscribe(v => v ? this.activateUpdate() : this.scheduleCheckForUpdate()); - } - private log(message: string) { const timestamp = (new Date).toISOString(); this.logger.log(`[SwUpdates - ${timestamp}]: ${message}`); } - - private scheduleCheckForUpdate() { - this.checkForUpdateSubj.next(); - } } diff --git a/aio/src/index.html b/aio/src/index.html index 99565e7800b..465363d33ce 100644 --- a/aio/src/index.html +++ b/aio/src/index.html @@ -20,7 +20,7 @@ - + diff --git a/aio/src/main.ts b/aio/src/main.ts index 2d2f9568880..dc96f6ad562 100644 --- a/aio/src/main.ts +++ b/aio/src/main.ts @@ -1,6 +1,5 @@ -import { enableProdMode, ApplicationRef } from '@angular/core'; +import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { first } from 'rxjs/operators'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; @@ -9,11 +8,5 @@ if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { - if (environment.production && 'serviceWorker' in (navigator as any)) { - const appRef: ApplicationRef = ref.injector.get(ApplicationRef); - appRef.isStable.pipe(first(v => v)).subscribe(() => { - (navigator as any).serviceWorker.register('/worker-basic.min.js'); - }); - } -}); +platformBrowserDynamic().bootstrapModule(AppModule); + diff --git a/aio/src/ngsw-config.json b/aio/src/ngsw-config.json new file mode 100644 index 00000000000..8440f5d54da --- /dev/null +++ b/aio/src/ngsw-config.json @@ -0,0 +1,75 @@ +{ + "index": "/index.html", + "assetGroups": [ + { + "name": "app-shell", + "installMode": "prefetch", + "updateMode": "prefetch", + "resources": { + "files": [ + "/index.html", + "/pwa-manifest.json", + "/app/search/search-worker.js", + "/assets/images/favicons/favicon.ico", + "/assets/js/*.js" + ], + "urls": [ + "https://fonts.googleapis.com/**", + "https://fonts.gstatic.com/s/**", + "https://maxcdn.bootstrapcdn.com/**" + ], + "versionedFiles": [ + "/*.bundle.css", + "/*.bundle.js", + "/*.chunk.js" + ] + } + }, { + "name": "assets-eager", + "installMode": "prefetch", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/images/**", + "/generated/images/marketing/**", + "!/assets/images/favicons/**", + "!/**/_unused/**" + ] + } + }, { + "name": "assets-lazy", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/images/favicons/**", + "/generated/ie-polyfills.min.js", + "!/**/_unused/**" + ] + } + }, { + "name": "docs-index", + "installMode": "prefetch", + "updateMode": "prefetch", + "resources": { + "files": [ + "/generated/*.json", + "/generated/docs/*.json", + "/generated/docs/api/api-list.json", + "/generated/docs/app/search-data.json" + ] + } + }, { + "name": "docs-lazy", + "installMode": "lazy", + "updateMode": "lazy", + "resources": { + "files": [ + "/generated/docs/**/*.json", + "/generated/images/**", + "!/**/_unused/**" + ] + } + } + ] +} diff --git a/aio/tests/deployment/unit/testServiceWorkerRoutes.spec.ts b/aio/tests/deployment/unit/testServiceWorkerRoutes.spec.ts index 005353ee72b..d7a7be25fbd 100644 --- a/aio/tests/deployment/unit/testServiceWorkerRoutes.spec.ts +++ b/aio/tests/deployment/unit/testServiceWorkerRoutes.spec.ts @@ -1,6 +1,7 @@ import { loadLegacyUrls, loadLocalSitemapUrls, loadSWRoutes } from '../shared/helpers'; -describe('service-worker routes', () => { +// NOTE: The new `@angular/service-worker` does not support configurable routes. +xdescribe('service-worker routes', () => { loadLocalSitemapUrls().forEach(url => { it('should process URLs in the Sitemap', () => { diff --git a/aio/yarn.lock b/aio/yarn.lock index a330c4a25d7..1d80cbda098 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -200,12 +200,11 @@ dependencies: tslib "^1.9.0" -"@angular/service-worker@^1.0.0-beta.16": - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-1.0.0-beta.16.tgz#cb4fcd1d5b311195136fd284bcf2dbb870544d64" +"@angular/service-worker@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-6.0.0.tgz#35a187554d33e05911544080fafc281ff1b322e0" dependencies: - base64-js "^1.1.2" - jshashes "^1.0.5" + tslib "^1.9.0" "@google-cloud/common@^0.13.0": version "0.13.6" @@ -1514,7 +1513,7 @@ base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" -base64-js@^1.0.2, base64-js@^1.1.2: +base64-js@^1.0.2: version "1.2.3" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" @@ -5998,10 +5997,6 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" -jshashes@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/jshashes/-/jshashes-1.0.7.tgz#bed8c97a0e9632fd0513916f55f76dd5486be59f" - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"