/** * @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. * * @experimental */ export class Generator { constructor(readonly fs: Filesystem, private baseHref: string) {} async process(config: Config): Promise { const unorderedHashTable = {}; const assetGroups = await this.processAssetGroups(config, unorderedHashTable); return { configVersion: 1, 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 { const seenMap = new Set(); 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(unorderedObj: T): T { const orderedObj = {} as T; Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]); return orderedObj; }