2017-09-28 19:18:12 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {parseDurationToMs} from './duration';
|
|
|
|
import {Filesystem} from './filesystem';
|
|
|
|
import {globToRegex} from './glob';
|
|
|
|
import {Config} from './in';
|
|
|
|
|
2018-04-12 11:04:11 -04:00
|
|
|
const DEFAULT_NAVIGATION_URLS = [
|
|
|
|
'/**', // Include all URLs.
|
|
|
|
'!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment).
|
|
|
|
'!/**/*__*', // Exclude URLs containing `__` in the last segment.
|
|
|
|
'!/**/*__*/**', // Exclude URLs containing `__` in any other segment.
|
|
|
|
];
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
/**
|
|
|
|
* Consumes service worker configuration files and processes them into control files.
|
|
|
|
*
|
2018-10-19 07:12:20 -04:00
|
|
|
* @publicApi
|
2017-09-28 19:18:12 -04:00
|
|
|
*/
|
|
|
|
export class Generator {
|
|
|
|
constructor(readonly fs: Filesystem, private baseHref: string) {}
|
|
|
|
|
|
|
|
async process(config: Config): Promise<Object> {
|
2018-04-27 19:18:35 -04:00
|
|
|
const unorderedHashTable = {};
|
|
|
|
const assetGroups = await this.processAssetGroups(config, unorderedHashTable);
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
return {
|
|
|
|
configVersion: 1,
|
2019-03-05 08:24:07 -05:00
|
|
|
timestamp: Date.now(),
|
2017-09-28 19:18:12 -04:00
|
|
|
appData: config.appData,
|
2018-04-27 19:18:35 -04:00
|
|
|
index: joinUrls(this.baseHref, config.index), assetGroups,
|
|
|
|
dataGroups: this.processDataGroups(config),
|
|
|
|
hashTable: withOrderedKeys(unorderedHashTable),
|
2018-04-12 11:04:11 -04:00
|
|
|
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
|
2017-09-28 19:18:12 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private async processAssetGroups(config: Config, hashTable: {[file: string]: string | undefined}):
|
|
|
|
Promise<Object[]> {
|
|
|
|
const seenMap = new Set<string>();
|
|
|
|
return Promise.all((config.assetGroups || []).map(async(group) => {
|
2018-04-27 19:01:28 -04:00
|
|
|
if (group.resources.versionedFiles) {
|
|
|
|
console.warn(
|
|
|
|
`Asset-group '${group.name}' in 'ngsw-config.json' uses the 'versionedFiles' option.\n` +
|
|
|
|
'As of v6 \'versionedFiles\' and \'files\' options have the same behavior. ' +
|
|
|
|
'Use \'files\' instead.');
|
|
|
|
}
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
const fileMatcher = globListToMatcher(group.resources.files || []);
|
|
|
|
const versionedMatcher = globListToMatcher(group.resources.versionedFiles || []);
|
|
|
|
|
2018-04-27 19:18:35 -04:00
|
|
|
const allFiles = await this.fs.list('/');
|
2017-09-28 19:18:12 -04:00
|
|
|
|
|
|
|
const plainFiles = allFiles.filter(fileMatcher).filter(file => !seenMap.has(file));
|
|
|
|
plainFiles.forEach(file => seenMap.add(file));
|
|
|
|
|
2018-04-27 19:18:35 -04:00
|
|
|
const versionedFiles = allFiles.filter(versionedMatcher).filter(file => !seenMap.has(file));
|
|
|
|
versionedFiles.forEach(file => seenMap.add(file));
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
// Add the hashes.
|
2018-04-27 19:18:35 -04:00
|
|
|
const matchedFiles = [...plainFiles, ...versionedFiles].sort();
|
|
|
|
await matchedFiles.reduce(async(previous, file) => {
|
2017-09-28 19:18:12 -04:00
|
|
|
await previous;
|
2017-10-02 18:59:57 -04:00
|
|
|
const hash = await this.fs.hash(file);
|
2017-09-28 19:18:12 -04:00
|
|
|
hashTable[joinUrls(this.baseHref, file)] = hash;
|
|
|
|
}, Promise.resolve());
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: group.name,
|
|
|
|
installMode: group.installMode || 'prefetch',
|
|
|
|
updateMode: group.updateMode || group.installMode || 'prefetch',
|
2018-04-27 19:18:35 -04:00
|
|
|
urls: matchedFiles.map(url => joinUrls(this.baseHref, url)),
|
2018-05-24 10:51:45 -04:00
|
|
|
patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true)),
|
2017-09-28 19:18:12 -04:00
|
|
|
};
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
private processDataGroups(config: Config): Object[] {
|
|
|
|
return (config.dataGroups || []).map(group => {
|
|
|
|
return {
|
|
|
|
name: group.name,
|
2018-05-24 10:51:45 -04:00
|
|
|
patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)),
|
2017-09-28 19:18:12 -04:00
|
|
|
strategy: group.cacheConfig.strategy || 'performance',
|
|
|
|
maxSize: group.cacheConfig.maxSize,
|
|
|
|
maxAge: parseDurationToMs(group.cacheConfig.maxAge),
|
|
|
|
timeoutMs: group.cacheConfig.timeout && parseDurationToMs(group.cacheConfig.timeout),
|
|
|
|
version: group.version !== undefined ? group.version : 1,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-12 11:04:11 -04:00
|
|
|
export function processNavigationUrls(
|
|
|
|
baseHref: string, urls = DEFAULT_NAVIGATION_URLS): {positive: boolean, regex: string}[] {
|
|
|
|
return urls.map(url => {
|
|
|
|
const positive = !url.startsWith('!');
|
|
|
|
url = positive ? url : url.substr(1);
|
|
|
|
return {positive, regex: `^${urlToRegex(url, baseHref)}$`};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
function globListToMatcher(globs: string[]): (file: string) => boolean {
|
|
|
|
const patterns = globs.map(pattern => {
|
|
|
|
if (pattern.startsWith('!')) {
|
|
|
|
return {
|
|
|
|
positive: false,
|
|
|
|
regex: new RegExp('^' + globToRegex(pattern.substr(1)) + '$'),
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
positive: true,
|
|
|
|
regex: new RegExp('^' + globToRegex(pattern) + '$'),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return (file: string) => matches(file, patterns);
|
|
|
|
}
|
|
|
|
|
|
|
|
function matches(file: string, patterns: {positive: boolean, regex: RegExp}[]): boolean {
|
|
|
|
const res = patterns.reduce((isMatch, pattern) => {
|
|
|
|
if (pattern.positive) {
|
|
|
|
return isMatch || pattern.regex.test(file);
|
|
|
|
} else {
|
|
|
|
return isMatch && !pattern.regex.test(file);
|
|
|
|
}
|
|
|
|
}, false);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-05-24 10:51:45 -04:00
|
|
|
function urlToRegex(url: string, baseHref: string, literalQuestionMark?: boolean): string {
|
2018-04-12 08:03:20 -04:00
|
|
|
if (!url.startsWith('/') && url.indexOf('://') === -1) {
|
|
|
|
url = joinUrls(baseHref, url);
|
|
|
|
}
|
|
|
|
|
2018-05-24 10:51:45 -04:00
|
|
|
return globToRegex(url, literalQuestionMark);
|
2018-04-12 08:03:20 -04:00
|
|
|
}
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
function joinUrls(a: string, b: string): string {
|
|
|
|
if (a.endsWith('/') && b.startsWith('/')) {
|
|
|
|
return a + b.substr(1);
|
|
|
|
} else if (!a.endsWith('/') && !b.startsWith('/')) {
|
|
|
|
return a + '/' + b;
|
|
|
|
}
|
|
|
|
return a + b;
|
2018-04-12 08:03:20 -04:00
|
|
|
}
|
2018-04-27 19:18:35 -04:00
|
|
|
|
|
|
|
function withOrderedKeys<T extends{[key: string]: any}>(unorderedObj: T): T {
|
2019-07-17 20:49:16 -04:00
|
|
|
const orderedObj = {} as{[key: string]: any};
|
2018-04-27 19:18:35 -04:00
|
|
|
Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]);
|
2019-07-17 20:49:16 -04:00
|
|
|
return orderedObj as T;
|
2018-04-27 19:18:35 -04:00
|
|
|
}
|