diff --git a/aio/README.md b/aio/README.md index a773f7f652..71b778238c 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 57fee90f7a..c801f78c58 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 8eed08fd18..0000000000 --- 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 ba4920c995..89f689ff2d 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", @@ -56,15 +56,13 @@ "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", "~~clean-generated": "node --eval \"require('shelljs').rm('-rf', 'src/generated')\"", "~~build": "ng build", - "post~~build": "yarn build-404-page && yarn sw-manifest && yarn sw-copy" + "post~~build": "yarn build-404-page" }, "engines": { "node": ">=10.9.0 <11.0.0", @@ -84,7 +82,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.2.0", "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 94934ba49a..1d473bcc62 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -9,4 +9,4 @@ } } } -} \ No newline at end of file +} diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index a3e89e24ce..79bf8374e7 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 d56a3a27fc..d80ca8e6f3 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 b79736af07..bfe545e06f 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 432e1c4ab2..3d570e88fe 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 d4ad73c08b..77859b7c66 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 573f64d3e7..065f871708 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 12a48436ea..e99330d663 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 e858990edd..ccbfdc7ddc 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 1eb26655a7..1bc73d4cf7 100644 --- a/aio/src/index.html +++ b/aio/src/index.html @@ -21,7 +21,7 @@ - + diff --git a/aio/src/main.ts b/aio/src/main.ts index 2d2f956888..dc96f6ad56 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 0000000000..8440f5d54d --- /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 005353ee72..d7a7be25fb 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 8dc2d3bc21..b12419d7e2 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"