Compare commits

..

8 Commits
5.0.4 ... 5.0.5

Author SHA1 Message Date
9dc310eb50 docs: add changelog for 5.0.5 2017-12-01 14:40:05 -08:00
b26bc9096e release: cut the 5.0.5 release 2017-12-01 14:38:26 -08:00
56c98f7c23 fix(service-worker): use relative path for ngsw.json
Not every application is served from the domain root. The Service
Worker made a bad assumption that it would be, and so requested
/ngsw.json from the domain root.

This change corrects this assumption, and requests ngsw.json without
the leading slash. This causes the request to be interpreted
relative to the SW origin, which will be the application root.
2017-12-01 14:28:40 -08:00
6bf07b4e60 fix(service-worker): send initialization signal from the application
The Service Worker contains a mechanism by which it will postMessage
itself a signal to initialize its caches. Through this mechanism,
initialization happens asynchronously while keeping the SW process
alive.

Unfortunately in Firefox, the SW does not have the ability to
postMessage itself during the activation event. This prevents the
above mechanism from working, and the SW initializes on the next
fetch event, which is often too late.

Therefore, this change has the application wait for SW changes and
tells each new SW to initialize itself. This happens in addition to
the self-signal that the SW attempts to send (as self-signaling is
more reliable). That way even on browsers such as Firefox,
initialization happens eagerly.
2017-12-01 14:28:32 -08:00
a2ff4abddc fix(compiler-cli): propagate ts.SourceFile moduleName into metadata 2017-12-01 14:28:18 -08:00
ee37d4b26d fix(service-worker): don't crash if SW not supported
Currently a bug exists where attempting to inject SwPush crashes the
application if Service Workers are unsupported. This happens because
SwPush doesn't properly detect that navigator.serviceWorker isn't
set.

This change ensures that all passive observation of SwPush and
SwUpdate doesn't cause crashes, and that calling methods to perform
actions on them results in rejected Promises. It's up to applications
to detect when those services are not available, and refrain from
attempting to use them.

To that end, this change also adds an `isSupported` getter to both
services, so users don't have to rely on feature detection directly
with browser APIs. Currently this simply detects whether the SW API
is present, but in the future it will be expanded to detect whether
a particular browser supports specific APIs (such as push
notifications, for example).
2017-12-01 14:27:49 -08:00
f99335bc47 fix(service-worker): allow disabling SW while still using services
Currently, the way to not use the SW is to not install its module.
However, this means that you can't inject any of its services.

This change adds a ServiceWorkerModule.disabled() MWP, that still
registers all of the right providers but acts as if the browser does
not support Service Workers.
2017-12-01 14:27:34 -08:00
445d833b5d fix(build): accidental character in commit-message.json 2017-12-01 11:20:51 -08:00
16 changed files with 153 additions and 28 deletions

View File

@ -1,3 +1,17 @@
<a name="5.0.5"></a>
## [5.0.5](https://github.com/angular/angular/compare/5.0.4...5.0.5) (2017-12-01)
### Bug Fixes
* **compiler-cli:** propagate ts.SourceFile moduleName into metadata ([a2ff4ab](https://github.com/angular/angular/commit/a2ff4ab))
* **service-worker:** allow disabling SW while still using services ([f99335b](https://github.com/angular/angular/commit/f99335b))
* **service-worker:** don't crash if SW not supported ([ee37d4b](https://github.com/angular/angular/commit/ee37d4b))
* **service-worker:** send initialization signal from the application ([6bf07b4](https://github.com/angular/angular/commit/6bf07b4))
* **service-worker:** use relative path for ngsw.json ([56c98f7](https://github.com/angular/angular/commit/56c98f7))
<a name="5.0.4"></a>
## [5.0.4](https://github.com/angular/angular/compare/5.0.3...5.0.4) (2017-12-01)

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "5.0.4",
"version": "5.0.5",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps",

View File

@ -551,6 +551,7 @@ export class MetadataCollector {
__symbolic: 'module',
version: this.options.version || METADATA_VERSION, metadata
};
if (sourceFile.moduleName) result.importAs = sourceFile.moduleName;
if (exports) result.exports = exports;
return result;
}

View File

@ -112,6 +112,7 @@ function upgradeMetadataWithDtsData(
newMetadata.metadata[prop] = dtsMetadata.metadata[prop];
}
}
if (dtsMetadata['importAs']) newMetadata['importAs'] = dtsMetadata['importAs'];
// Only copy exports from exports from metadata prior to version 3.
// Starting with version 3 the collector began collecting exports and

View File

@ -45,6 +45,7 @@ describe('Collector', () => {
're-exports.ts',
're-exports-2.ts',
'export-as.d.ts',
'named-module.d.ts',
'static-field-reference.ts',
'static-method.ts',
'static-method-call.ts',
@ -101,6 +102,12 @@ describe('Collector', () => {
});
});
it('should preserve module names from TypeScript sources', () => {
const sourceFile = program.getSourceFile('named-module.d.ts');
const metadata = collector.getMetadata(sourceFile);
expect(metadata !['importAs']).toEqual('some-named-module');
});
it('should be able to collect a simple component\'s metadata', () => {
const sourceFile = program.getSourceFile('app/hero-detail.component.ts');
const metadata = collector.getMetadata(sourceFile);
@ -1384,6 +1391,10 @@ const FILES: Directory = {
declare function someFunction(): void;
export { someFunction as SomeFunction };
`,
'named-module.d.ts': `
/// <amd-module name="some-named-module" />
export type SomeType = 'a';
`,
'local-symbol-ref.ts': `
import {Component, Validators} from 'angular2/core';

View File

@ -21,6 +21,7 @@ const globals = {
'rxjs/observable/defer': 'Rx.Observable',
'rxjs/observable/fromEvent': 'Rx.Observable',
'rxjs/observable/merge': 'Rx.Observable',
'rxjs/observable/never': 'Rx.Observable',
'rxjs/observable/of': 'Rx.Observable',
'rxjs/observable/throw': 'Rx.Observable',

View File

@ -24,7 +24,7 @@ import {switchMap as op_switchMap} from 'rxjs/operator/switchMap';
import {take as op_take} from 'rxjs/operator/take';
import {toPromise as op_toPromise} from 'rxjs/operator/toPromise';
const ERR_SW_NOT_SUPPORTED = 'Service workers are not supported by this browser';
export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
export interface Version {
hash: string;
@ -86,9 +86,9 @@ export class NgswCommChannel {
*/
readonly events: Observable<IncomingEvent>;
constructor(serviceWorker: ServiceWorkerContainer|undefined) {
constructor(private serviceWorker: ServiceWorkerContainer|undefined) {
if (!serviceWorker) {
this.worker = this.events = errorObservable(ERR_SW_NOT_SUPPORTED);
this.worker = this.events = this.registration = errorObservable(ERR_SW_NOT_SUPPORTED);
} else {
const controllerChangeEvents =
<Observable<any>>(obs_fromEvent(serviceWorker, 'controllerchange'));
@ -176,4 +176,6 @@ export class NgswCommChannel {
}));
return op_toPromise.call(mapErrorAndValue);
}
get isEnabled(): boolean { return !!this.serviceWorker; }
}

View File

@ -16,14 +16,18 @@ import {NgswCommChannel} from './low_level';
import {SwPush} from './push';
import {SwUpdate} from './update';
export abstract class RegistrationOptions {
scope?: string;
enabled?: boolean;
}
export const SCRIPT = new InjectionToken<string>('NGSW_REGISTER_SCRIPT');
export const OPTS = new InjectionToken<Object>('NGSW_REGISTER_OPTIONS');
export function ngswAppInitializer(
injector: Injector, script: string, options: RegistrationOptions): Function {
const initializer = () => {
const app = injector.get<ApplicationRef>(ApplicationRef);
if (!('serviceWorker' in navigator)) {
if (!('serviceWorker' in navigator) || options.enabled === false) {
return;
}
const onStable =
@ -31,15 +35,24 @@ export function ngswAppInitializer(
const isStable = op_take.call(onStable, 1) as Observable<boolean>;
const whenStable = op_toPromise.call(isStable) as Promise<boolean>;
// Wait for service worker controller changes, and fire an INITIALIZE action when a new SW
// becomes active. This allows the SW to initialize itself even if there is no application
// traffic.
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (navigator.serviceWorker.controller !== null) {
navigator.serviceWorker.controller.postMessage({action: 'INITIALIZE'});
}
});
// Don't return the Promise, as that will block the application until the SW is registered, and
// cause a crash if the SW registration fails.
whenStable.then(() => navigator.serviceWorker.register(script, options));
whenStable.then(() => navigator.serviceWorker.register(script, {scope: options.scope}));
};
return initializer;
}
export function ngswCommChannelFactory(): NgswCommChannel {
return new NgswCommChannel(navigator.serviceWorker);
export function ngswCommChannelFactory(opts: RegistrationOptions): NgswCommChannel {
return new NgswCommChannel(opts.enabled !== false ? navigator.serviceWorker : undefined);
}
/**
@ -49,20 +62,27 @@ export function ngswCommChannelFactory(): NgswCommChannel {
providers: [SwPush, SwUpdate],
})
export class ServiceWorkerModule {
static register(script: string, opts: RegistrationOptions = {}): ModuleWithProviders {
/**
* Register the given Angular Service Worker script.
*
* If `enabled` is set to `false` in the given options, the module will behave as if service
* workers are not supported by the browser, and the service worker will not be registered.
*/
static register(script: string, opts: {scope?: string; enabled?: boolean;} = {}):
ModuleWithProviders {
return {
ngModule: ServiceWorkerModule,
providers: [
{provide: SCRIPT, useValue: script},
{provide: OPTS, useValue: opts},
{provide: NgswCommChannel, useFactory: ngswCommChannelFactory},
{provide: RegistrationOptions, useValue: opts},
{provide: NgswCommChannel, useFactory: ngswCommChannelFactory, deps: [RegistrationOptions]},
{
provide: APP_INITIALIZER,
useFactory: ngswAppInitializer,
deps: [Injector, SCRIPT, OPTS],
deps: [Injector, SCRIPT, RegistrationOptions],
multi: true,
},
],
};
}
}
}

View File

@ -9,15 +9,15 @@
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {merge as obs_merge} from 'rxjs/observable/merge';
import {never as obs_never} from 'rxjs/observable/never';
import {map as op_map} from 'rxjs/operator/map';
import {switchMap as op_switchMap} from 'rxjs/operator/switchMap';
import {take as op_take} from 'rxjs/operator/take';
import {toPromise as op_toPromise} from 'rxjs/operator/toPromise';
import {NgswCommChannel} from './low_level';
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel} from './low_level';
/**
* Subscribe and listen to push notifications from the Service Worker.
@ -34,6 +34,11 @@ export class SwPush {
new Subject<PushSubscription|null>();
constructor(private sw: NgswCommChannel) {
if (!sw.isEnabled) {
this.messages = obs_never();
this.subscription = obs_never();
return;
}
this.messages =
op_map.call(this.sw.eventsOfType('PUSH'), (message: {data: object}) => message.data);
@ -46,7 +51,16 @@ export class SwPush {
this.subscription = obs_merge(workerDrivenSubscriptions, this.subscriptionChanges);
}
/**
* Returns true if the Service Worker is enabled (supported by the browser and enabled via
* ServiceWorkerModule).
*/
get isEnabled(): boolean { return this.sw.isEnabled; }
requestSubscription(options: {serverPublicKey: string}): Promise<PushSubscription> {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const pushOptions: PushSubscriptionOptionsInit = {userVisibleOnly: true};
let key = atob(options.serverPublicKey.replace(/_/g, '/').replace(/-/g, '+'));
let applicationServerKey = new Uint8Array(new ArrayBuffer(key.length));
@ -64,6 +78,9 @@ export class SwPush {
}
unsubscribe(): Promise<void> {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const unsubscribe = op_switchMap.call(this.subscription, (sub: PushSubscription | null) => {
if (sub !== null) {
return sub.unsubscribe().then(success => {

View File

@ -9,9 +9,10 @@
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {defer as obs_defer} from 'rxjs/observable/defer';
import {never as obs_never} from 'rxjs/observable/never';
import {map as op_map} from 'rxjs/operator/map';
import {NgswCommChannel, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
/**
@ -26,16 +27,33 @@ export class SwUpdate {
readonly activated: Observable<UpdateActivatedEvent>;
constructor(private sw: NgswCommChannel) {
if (!sw.isEnabled) {
this.available = obs_never();
this.activated = obs_never();
return;
}
this.available = this.sw.eventsOfType('UPDATE_AVAILABLE');
this.activated = this.sw.eventsOfType('UPDATE_ACTIVATED');
}
/**
* Returns true if the Service Worker is enabled (supported by the browser and enabled via
* ServiceWorkerModule).
*/
get isEnabled(): boolean { return this.sw.isEnabled; }
checkForUpdate(): Promise<void> {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const statusNonce = this.sw.generateNonce();
return this.sw.postMessageWithStatus('CHECK_FOR_UPDATES', {statusNonce}, statusNonce);
}
activateUpdate(): Promise<void> {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const statusNonce = this.sw.generateNonce();
return this.sw.postMessageWithStatus('ACTIVATE_UPDATE', {statusNonce}, statusNonce);
}

View File

@ -71,6 +71,24 @@ export function main() {
});
expect(() => TestBed.get(SwPush)).not.toThrow();
});
describe('with no SW', () => {
beforeEach(() => { comm = new NgswCommChannel(undefined); });
it('can be instantiated', () => { push = new SwPush(comm); });
it('does not crash on subscription to observables', () => {
push = new SwPush(comm);
push.messages.toPromise().catch(err => fail(err));
push.subscription.toPromise().catch(err => fail(err));
});
it('gives an error when registering', done => {
push = new SwPush(comm);
push.requestSubscription({serverPublicKey: 'test'}).catch(err => { done(); });
});
it('gives an error when unsubscribing', done => {
push = new SwPush(comm);
push.unsubscribe().catch(err => { done(); });
});
});
});
describe('SwUpdate', () => {
let update: SwUpdate;
@ -147,6 +165,23 @@ export function main() {
});
expect(() => TestBed.get(SwUpdate)).not.toThrow();
});
describe('with no SW', () => {
beforeEach(() => { comm = new NgswCommChannel(undefined); });
it('can be instantiated', () => { update = new SwUpdate(comm); });
it('does not crash on subscription to observables', () => {
update = new SwUpdate(comm);
update.available.toPromise().catch(err => fail(err));
update.activated.toPromise().catch(err => fail(err));
});
it('gives an error when checking for updates', done => {
update = new SwUpdate(comm);
update.checkForUpdate().catch(err => { done(); });
});
it('gives an error when activating updates', done => {
update = new SwUpdate(comm);
update.activateUpdate().catch(err => { done(); });
});
});
});
});
}

View File

@ -580,8 +580,8 @@ export class Driver implements Debuggable, UpdateSource {
* Retrieve a copy of the latest manifest from the server.
*/
private async fetchLatestManifest(): Promise<Manifest> {
const res = await this.safeFetch(
this.adapter.newRequest('/ngsw.json?ngsw-cache-bust=' + Math.random()));
const res =
await this.safeFetch(this.adapter.newRequest('ngsw.json?ngsw-cache-bust=' + Math.random()));
if (!res.ok) {
if (res.status === 404) {
await this.deleteAllCaches();

View File

@ -158,7 +158,7 @@ export function main() {
expect(await scope.startup(true)).toEqual(true);
await scope.resolveSelfMessages();
await driver.initialized;
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -170,7 +170,7 @@ export function main() {
async_it('initializes prefetched content correctly, after a request kicks it off', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -230,7 +230,7 @@ export function main() {
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
serverUpdate.assertSawRequestFor('/ngsw.json');
serverUpdate.assertSawRequestFor('ngsw.json');
serverUpdate.assertSawRequestFor('/foo.txt');
serverUpdate.assertSawRequestFor('/redirected.txt');
serverUpdate.assertNoOtherRequests();
@ -263,7 +263,7 @@ export function main() {
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
serverUpdate.assertSawRequestFor('/ngsw.json');
serverUpdate.assertSawRequestFor('ngsw.json');
serverUpdate.assertSawRequestFor('/foo.txt');
serverUpdate.assertSawRequestFor('/redirected.txt');
serverUpdate.assertNoOtherRequests();
@ -331,7 +331,7 @@ export function main() {
scope.advance(12000);
await driver.idle.empty;
serverUpdate.assertSawRequestFor('/ngsw.json');
serverUpdate.assertSawRequestFor('ngsw.json');
serverUpdate.assertSawRequestFor('/foo.txt');
serverUpdate.assertSawRequestFor('/redirected.txt');
serverUpdate.assertNoOtherRequests();

View File

@ -73,7 +73,7 @@ export class MockServerStateBuilder {
}
withManifest(manifest: Manifest): MockServerStateBuilder {
this.resources.set('/ngsw.json', new MockResponse(JSON.stringify(manifest)));
this.resources.set('ngsw.json', new MockResponse(JSON.stringify(manifest)));
return this;
}

View File

@ -1,10 +1,14 @@
/** @experimental */
export declare class ServiceWorkerModule {
static register(script: string, opts?: RegistrationOptions): ModuleWithProviders;
static register(script: string, opts?: {
scope?: string;
enabled?: boolean;
}): ModuleWithProviders;
}
/** @experimental */
export declare class SwPush {
readonly isEnabled: boolean;
readonly messages: Observable<object>;
readonly subscription: Observable<PushSubscription | null>;
constructor(sw: NgswCommChannel);
@ -18,6 +22,7 @@ export declare class SwPush {
export declare class SwUpdate {
readonly activated: Observable<UpdateActivatedEvent>;
readonly available: Observable<UpdateAvailableEvent>;
readonly isEnabled: boolean;
constructor(sw: NgswCommChannel);
activateUpdate(): Promise<void>;
checkForUpdate(): Promise<void>;

View File

@ -1,4 +1,4 @@
g{
{
"maxLength": 120,
"types": [
"build",