Skip to content

Commit

Permalink
feat(service-worker): add support for ignoring specific URLs
Browse files Browse the repository at this point in the history
Normally, the ServiceWorker will redirect navigation requests that don't
match any `asset` or `data` group to the specified index file. Yet,
sometimes it is desirable to configure the SW to ignore specific URLs
(even for navigation requests) and pass them through to the server.

This commit adds support for specifying an optional `ignoredUrlPatterns`
list in `ngsw-config.json`, which contains URLs or simple globs
(currently only recognizing `*` and `**`). Requests matching these URLs
or patterns will not be handled by the SW.

Fixes #20404
  • Loading branch information
gkalpak committed Apr 12, 2018
1 parent 01975ff commit 9ae2575
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 41 deletions.
36 changes: 17 additions & 19 deletions packages/service-worker/config/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export class Generator {
const hashTable = {};
return {
configVersion: 1,
index: joinUrls(this.baseHref, config.index),
appData: config.appData,
index: joinUrls(this.baseHref, config.index),
ignoredUrlPatterns: this.processIgnoredUrlPatterns(config),
assetGroups: await this.processAssetGroups(config, hashTable),
dataGroups: this.processDataGroups(config), hashTable,
};
Expand Down Expand Up @@ -52,15 +53,6 @@ export class Generator {
hashTable[joinUrls(this.baseHref, file)] = hash;
}, Promise.resolve());


// Figure out the patterns.
const patterns = (group.resources.urls || [])
.map(
glob => glob.startsWith('/') || glob.indexOf('://') !== -1 ?
glob :
joinUrls(this.baseHref, glob))
.map(glob => globToRegex(glob));

return {
name: group.name,
installMode: group.installMode || 'prefetch',
Expand All @@ -69,22 +61,16 @@ export class Generator {
.concat(plainFiles)
.concat(versionedFiles)
.map(url => joinUrls(this.baseHref, url)),
patterns,
patterns: (group.resources.urls || []).map(url => this.urlToRegex(url)),
};
}));
}

private processDataGroups(config: Config): Object[] {
return (config.dataGroups || []).map(group => {
const patterns = group.urls
.map(
glob => glob.startsWith('/') || glob.indexOf('://') !== -1 ?
glob :
joinUrls(this.baseHref, glob))
.map(glob => globToRegex(glob));
return {
name: group.name,
patterns,
patterns: group.urls.map(url => this.urlToRegex(url)),
strategy: group.cacheConfig.strategy || 'performance',
maxSize: group.cacheConfig.maxSize,
maxAge: parseDurationToMs(group.cacheConfig.maxAge),
Expand All @@ -93,6 +79,18 @@ export class Generator {
};
});
}

private processIgnoredUrlPatterns(config: Config): string[] {
return (config.ignoredUrlPatterns || []).map(url => `^${this.urlToRegex(url)}$`);
}

private urlToRegex(url: string): string {
if (!url.startsWith('/') && url.indexOf('://') === -1) {
url = joinUrls(this.baseHref, url);
}

return globToRegex(url);
}
}

function globListToMatcher(globs: string[]): (file: string) => boolean {
Expand Down Expand Up @@ -130,4 +128,4 @@ function joinUrls(a: string, b: string): string {
return a + '/' + b;
}
return a + b;
}
}
3 changes: 2 additions & 1 deletion packages/service-worker/config/src/in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type Duration = string;
export interface Config {
appData?: {};
index: string;
ignoredUrlPatterns?: string[];
assetGroups?: AssetGroup[];
dataGroups?: DataGroup[];
}
Expand Down Expand Up @@ -52,4 +53,4 @@ export interface DataGroup {
cacheConfig: {
maxSize: number; maxAge: Duration; timeout?: Duration; strategy?: 'freshness' | 'performance';
};
}
}
52 changes: 32 additions & 20 deletions packages/service-worker/config/test/generator_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ import {MockFilesystem} from '../testing/mock';
});
const gen = new Generator(fs, '/test');
const res = gen.process({
index: '/index.html',
appData: {
test: true,
},
index: '/index.html',
ignoredUrlPatterns: [
'/ignored/absolute/**',
'/ignored/some/url?with+escaped+chars',
'ignored/relative/*.txt',
'http://example.com/ignored',
],
assetGroups: [{
name: 'test',
resources: {
Expand Down Expand Up @@ -56,36 +62,42 @@ import {MockFilesystem} from '../testing/mock';
});
res.then(config => {
expect(config).toEqual({
'configVersion': 1,
'index': '/test/index.html',
'appData': {
'test': true,
configVersion: 1,
appData: {
test: true,
},
'assetGroups': [{
'name': 'test',
'installMode': 'prefetch',
'updateMode': 'prefetch',
'urls': [
index: '/test/index.html',
ignoredUrlPatterns: [
'^\\/ignored\\/absolute\\/.*',
'^\\/ignored\\/some\\/url\\?with\\+escaped\\+chars',
'^\\/test\\/ignored\\/relative\\/[^\\/]+\\.txt',
'^http:\\/\\/example\\.com\\/ignored',
],
assetGroups: [{
name: 'test',
installMode: 'prefetch',
updateMode: 'prefetch',
urls: [
'/test/index.html',
'/test/foo/test.html',
'/test/test.txt',
],
'patterns': [
patterns: [
'\\/absolute\\/.*',
'\\/some\\/url\\?with\\+escaped\\+chars',
'\\/test\\/relative\\/[^\\/]+\\.txt',
]
}],
'dataGroups': [{
'name': 'other',
'patterns': ['\\/api\\/.*', '\\/test\\/relapi\\/.*'],
'strategy': 'performance',
'maxSize': 100,
'maxAge': 259200000,
'timeoutMs': 60000,
'version': 1,
dataGroups: [{
name: 'other',
patterns: ['\\/api\\/.*', '\\/test\\/relapi\\/.*'],
strategy: 'performance',
maxSize: 100,
maxAge: 259200000,
timeoutMs: 60000,
version: 1,
}],
'hashTable': {
hashTable: {
'/test/test.txt': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643',
'/test/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
'/test/foo/test.html': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643'
Expand Down
16 changes: 16 additions & 0 deletions packages/service-worker/worker/src/app-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export class AppVersion implements UpdateSource {
*/
private dataGroups: DataGroup[];

/**
* Requests to URLs that match any RegExp in this list will not be handled.
*/
private ignoredUrlPatterns: RegExp[];

/**
* Tracks whether the manifest has encountered any inconsistencies.
*/
Expand Down Expand Up @@ -79,6 +84,10 @@ export class AppVersion implements UpdateSource {
config => new DataGroup(
this.scope, this.adapter, config, this.database,
`ngsw:${config.version}:data`));

// Create a RegExp for each `ignoredUrlPattern` declared in the manifest.
this.ignoredUrlPatterns =
(manifest.ignoredUrlPatterns || []).map(pattern => new RegExp(pattern));
}

/**
Expand Down Expand Up @@ -107,6 +116,13 @@ export class AppVersion implements UpdateSource {
}

async handleFetch(req: Request, context: Context): Promise<Response|null> {
// If the request URL matches any of the `ignoredUrlPatterns`, return `null`.
const urlPrefix = this.scope.registration.scope.replace(/\/$/, '');
const relativeUrl = req.url.startsWith(urlPrefix) ? req.url.substr(urlPrefix.length) : req.url;
if (this.ignoredUrlPatterns.some(pattern => pattern.test(relativeUrl))) {
return null;
}

// Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the
// request,
// it will return `null`. Thus, the first non-null response is the SW's answer to the request.
Expand Down
3 changes: 2 additions & 1 deletion packages/service-worker/worker/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Manifest {
configVersion: number;
appData?: {[key: string]: string};
index: string;
ignoredUrlPatterns?: string[];
assetGroups?: AssetGroupConfig[];
dataGroups?: DataGroupConfig[];
hashTable: {[url: string]: string};
Expand All @@ -40,4 +41,4 @@ export interface DataGroupConfig {

export function hashManifest(manifest: Manifest): ManifestHash {
return sha1(JSON.stringify(manifest));
}
}
41 changes: 41 additions & 0 deletions packages/service-worker/worker/test/happy_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const dist =
.addFile('/qux.txt', 'this is qux')
.addFile('/quux.txt', 'this is quux')
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/ignored/file', 'this is not handled by the SW')
.addUnhashedFile('/ignored/dir/file', 'this is not handled by the SW either')
.build();

const distUpdate =
Expand Down Expand Up @@ -60,6 +62,10 @@ const manifest: Manifest = {
version: 'original',
},
index: '/foo.txt',
ignoredUrlPatterns: [
'^\\/ignored\\/file',
'^\\/ignored\\/dir\\/.*',
],
assetGroups: [
{
name: 'assets',
Expand Down Expand Up @@ -680,6 +686,41 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
})).toBeNull();
server.assertSawRequestFor('/baz');
});

async_it(
'does not redirect to index on a request that exctly matches `ignoredUrlPatterns`',
async() => {
const navInit = {
headers: {Accept: 'text/plain, text/html, text/css'},
mode: 'navigate',
};
const navRequest = (url: string) => makeRequest(scope, url, undefined, navInit);

expect(await navRequest('/ignored/file')).toBe('this is not handled by the SW');
server.assertSawRequestFor('/ignored/file');

expect(await navRequest('/ignored/dir/file'))
.toBe('this is not handled by the SW either');
server.assertSawRequestFor('/ignored/dir/file');

expect(await navRequest('/ignored/directory/file')).toBe('this is foo');
server.assertNoOtherRequests();
});

async_it('strips registration scope before checking `ignoredUrlPatterns`', async() => {
const navInit = {
headers: {Accept: 'text/plain, text/html, text/css'},
mode: 'navigate',
};
const navRequest = (url: string) => makeRequest(scope, url, undefined, navInit);

expect(await navRequest('http://localhost/ignored/file'))
.toBe('this is not handled by the SW');
server.assertSawRequestFor('/ignored/file');

expect(await navRequest('http://localhost.dev/ignored/file')).toBe('this is foo');
server.assertNoOtherRequests();
});
});

describe('bugs', () => {
Expand Down

0 comments on commit 9ae2575

Please sign in to comment.