angular-cn/packages/service-worker/config/src/generator.ts

158 lines
5.4 KiB
TypeScript
Raw Normal View History

/**
* @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';
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.
];
/**
* Consumes service worker configuration files and processes them into control files.
*
* @publicApi
*/
export class Generator {
constructor(readonly fs: Filesystem, private baseHref: string) {}
async process(config: Config): Promise<Object> {
const unorderedHashTable = {};
const assetGroups = await this.processAssetGroups(config, unorderedHashTable);
return {
configVersion: 1,
timestamp: Date.now(),
appData: config.appData,
index: joinUrls(this.baseHref, config.index), assetGroups,
dataGroups: this.processDataGroups(config),
hashTable: withOrderedKeys(unorderedHashTable),
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
};
}
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) => {
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.');
}
const fileMatcher = globListToMatcher(group.resources.files || []);
const versionedMatcher = globListToMatcher(group.resources.versionedFiles || []);
const allFiles = await this.fs.list('/');
const plainFiles = allFiles.filter(fileMatcher).filter(file => !seenMap.has(file));
plainFiles.forEach(file => seenMap.add(file));
const versionedFiles = allFiles.filter(versionedMatcher).filter(file => !seenMap.has(file));
versionedFiles.forEach(file => seenMap.add(file));
// Add the hashes.
const matchedFiles = [...plainFiles, ...versionedFiles].sort();
await matchedFiles.reduce(async(previous, file) => {
await previous;
const hash = await this.fs.hash(file);
hashTable[joinUrls(this.baseHref, file)] = hash;
}, Promise.resolve());
return {
name: group.name,
installMode: group.installMode || 'prefetch',
updateMode: group.updateMode || group.installMode || 'prefetch',
urls: matchedFiles.map(url => joinUrls(this.baseHref, url)),
patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true)),
};
}));
}
private processDataGroups(config: Config): Object[] {
return (config.dataGroups || []).map(group => {
return {
name: group.name,
patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)),
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,
};
});
}
}
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)}$`};
});
}
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;
}
function urlToRegex(url: string, baseHref: string, literalQuestionMark?: boolean): string {
if (!url.startsWith('/') && url.indexOf('://') === -1) {
url = joinUrls(baseHref, url);
}
return globToRegex(url, literalQuestionMark);
}
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;
}
function withOrderedKeys<T extends{[key: string]: any}>(unorderedObj: T): T {
const orderedObj = {} as{[key: string]: any};
Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]);
return orderedObj as T;
}