| 
									
										
										
										
											2017-09-28 16:18:12 -07: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 18:04:11 +03: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 16:18:12 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Consumes service worker configuration files and processes them into control files. | 
					
						
							|  |  |  |  * | 
					
						
							| 
									
										
										
										
											2018-10-19 12:12:20 +01:00
										 |  |  |  * @publicApi | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |  */ | 
					
						
							|  |  |  | export class Generator { | 
					
						
							|  |  |  |   constructor(readonly fs: Filesystem, private baseHref: string) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async process(config: Config): Promise<Object> { | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  |     const unorderedHashTable = {}; | 
					
						
							|  |  |  |     const assetGroups = await this.processAssetGroups(config, unorderedHashTable); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |     return { | 
					
						
							|  |  |  |       configVersion: 1, | 
					
						
							| 
									
										
										
										
											2019-03-05 15:24:07 +02:00
										 |  |  |       timestamp: Date.now(), | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |       appData: config.appData, | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  |       index: joinUrls(this.baseHref, config.index), assetGroups, | 
					
						
							|  |  |  |       dataGroups: this.processDataGroups(config), | 
					
						
							|  |  |  |       hashTable: withOrderedKeys(unorderedHashTable), | 
					
						
							| 
									
										
										
										
											2018-04-12 18:04:11 +03:00
										 |  |  |       navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls), | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07: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-28 02:01:28 +03: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 16:18:12 -07:00
										 |  |  |       const fileMatcher = globListToMatcher(group.resources.files || []); | 
					
						
							|  |  |  |       const versionedMatcher = globListToMatcher(group.resources.versionedFiles || []); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  |       const allFiles = await this.fs.list('/'); | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const plainFiles = allFiles.filter(fileMatcher).filter(file => !seenMap.has(file)); | 
					
						
							|  |  |  |       plainFiles.forEach(file => seenMap.add(file)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  |       const versionedFiles = allFiles.filter(versionedMatcher).filter(file => !seenMap.has(file)); | 
					
						
							|  |  |  |       versionedFiles.forEach(file => seenMap.add(file)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |       // Add the hashes.
 | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  |       const matchedFiles = [...plainFiles, ...versionedFiles].sort(); | 
					
						
							|  |  |  |       await matchedFiles.reduce(async(previous, file) => { | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |         await previous; | 
					
						
							| 
									
										
										
										
											2017-10-02 15:59:57 -07:00
										 |  |  |         const hash = await this.fs.hash(file); | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07: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-28 02:18:35 +03:00
										 |  |  |         urls: matchedFiles.map(url => joinUrls(this.baseHref, url)), | 
					
						
							| 
									
										
										
										
											2018-05-24 17:51:45 +03:00
										 |  |  |         patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true)), | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07:00
										 |  |  |       }; | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private processDataGroups(config: Config): Object[] { | 
					
						
							|  |  |  |     return (config.dataGroups || []).map(group => { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         name: group.name, | 
					
						
							| 
									
										
										
										
											2018-05-24 17:51:45 +03:00
										 |  |  |         patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)), | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07: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 18:04:11 +03: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 16:18:12 -07: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 17:51:45 +03:00
										 |  |  | function urlToRegex(url: string, baseHref: string, literalQuestionMark?: boolean): string { | 
					
						
							| 
									
										
										
										
											2018-04-12 15:03:20 +03:00
										 |  |  |   if (!url.startsWith('/') && url.indexOf('://') === -1) { | 
					
						
							|  |  |  |     url = joinUrls(baseHref, url); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-24 17:51:45 +03:00
										 |  |  |   return globToRegex(url, literalQuestionMark); | 
					
						
							| 
									
										
										
										
											2018-04-12 15:03:20 +03:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-28 16:18:12 -07: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 15:03:20 +03:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-04-28 02:18:35 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | function withOrderedKeys<T extends{[key: string]: any}>(unorderedObj: T): T { | 
					
						
							|  |  |  |   const orderedObj = {} as T; | 
					
						
							|  |  |  |   Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]); | 
					
						
							|  |  |  |   return orderedObj; | 
					
						
							|  |  |  | } |