1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import { __awaiter } from 'tslib';
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const PARSE_TO_PAIRS = /([0-9]+[^0-9]+)/g;
|
17 | const PAIR_SPLIT = /^([0-9]+)([dhmsu]+)$/;
|
18 | function parseDurationToMs(duration) {
|
19 | const matches = [];
|
20 | let array;
|
21 | while ((array = PARSE_TO_PAIRS.exec(duration)) !== null) {
|
22 | matches.push(array[0]);
|
23 | }
|
24 | return matches
|
25 | .map(match => {
|
26 | const res = PAIR_SPLIT.exec(match);
|
27 | if (res === null) {
|
28 | throw new Error(`Not a valid duration: ${match}`);
|
29 | }
|
30 | let factor = 0;
|
31 | switch (res[2]) {
|
32 | case 'd':
|
33 | factor = 86400000;
|
34 | break;
|
35 | case 'h':
|
36 | factor = 3600000;
|
37 | break;
|
38 | case 'm':
|
39 | factor = 60000;
|
40 | break;
|
41 | case 's':
|
42 | factor = 1000;
|
43 | break;
|
44 | case 'u':
|
45 | factor = 1;
|
46 | break;
|
47 | default:
|
48 | throw new Error(`Not a valid duration unit: ${res[2]}`);
|
49 | }
|
50 | return parseInt(res[1]) * factor;
|
51 | })
|
52 | .reduce((total, value) => total + value, 0);
|
53 | }
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | const QUESTION_MARK = '[^/]';
|
63 | const WILD_SINGLE = '[^/]*';
|
64 | const WILD_OPEN = '(?:.+\\/)?';
|
65 | const TO_ESCAPE_BASE = [
|
66 | { replace: /\./g, with: '\\.' },
|
67 | { replace: /\+/g, with: '\\+' },
|
68 | { replace: /\*/g, with: WILD_SINGLE },
|
69 | ];
|
70 | const TO_ESCAPE_WILDCARD_QM = [
|
71 | ...TO_ESCAPE_BASE,
|
72 | { replace: /\?/g, with: QUESTION_MARK },
|
73 | ];
|
74 | const TO_ESCAPE_LITERAL_QM = [
|
75 | ...TO_ESCAPE_BASE,
|
76 | { replace: /\?/g, with: '\\?' },
|
77 | ];
|
78 | function globToRegex(glob, literalQuestionMark = false) {
|
79 | const toEscape = literalQuestionMark ? TO_ESCAPE_LITERAL_QM : TO_ESCAPE_WILDCARD_QM;
|
80 | const segments = glob.split('/').reverse();
|
81 | let regex = '';
|
82 | while (segments.length > 0) {
|
83 | const segment = segments.pop();
|
84 | if (segment === '**') {
|
85 | if (segments.length > 0) {
|
86 | regex += WILD_OPEN;
|
87 | }
|
88 | else {
|
89 | regex += '.*';
|
90 | }
|
91 | }
|
92 | else {
|
93 | const processed = toEscape.reduce((segment, escape) => segment.replace(escape.replace, escape.with), segment);
|
94 | regex += processed;
|
95 | if (segments.length > 0) {
|
96 | regex += '\\/';
|
97 | }
|
98 | }
|
99 | }
|
100 | return regex;
|
101 | }
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | const DEFAULT_NAVIGATION_URLS = [
|
111 | '/**',
|
112 | '!/**/*.*',
|
113 | '!/**/*__*',
|
114 | '!/**/*__*/**',
|
115 | ];
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | class Generator {
|
122 | constructor(fs, baseHref) {
|
123 | this.fs = fs;
|
124 | this.baseHref = baseHref;
|
125 | }
|
126 | process(config) {
|
127 | return __awaiter(this, void 0, void 0, function* () {
|
128 | const unorderedHashTable = {};
|
129 | const assetGroups = yield this.processAssetGroups(config, unorderedHashTable);
|
130 | return {
|
131 | configVersion: 1,
|
132 | timestamp: Date.now(),
|
133 | appData: config.appData,
|
134 | index: joinUrls(this.baseHref, config.index),
|
135 | assetGroups,
|
136 | dataGroups: this.processDataGroups(config),
|
137 | hashTable: withOrderedKeys(unorderedHashTable),
|
138 | navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
|
139 | };
|
140 | });
|
141 | }
|
142 | processAssetGroups(config, hashTable) {
|
143 | return __awaiter(this, void 0, void 0, function* () {
|
144 | const seenMap = new Set();
|
145 | return Promise.all((config.assetGroups || []).map((group) => __awaiter(this, void 0, void 0, function* () {
|
146 | if (group.resources.versionedFiles) {
|
147 | throw new Error(`Asset-group '${group.name}' in 'ngsw-config.json' uses the 'versionedFiles' option, ` +
|
148 | 'which is no longer supported. Use \'files\' instead.');
|
149 | }
|
150 | const fileMatcher = globListToMatcher(group.resources.files || []);
|
151 | const allFiles = yield this.fs.list('/');
|
152 | const matchedFiles = allFiles.filter(fileMatcher).filter(file => !seenMap.has(file)).sort();
|
153 | matchedFiles.forEach(file => seenMap.add(file));
|
154 |
|
155 | yield matchedFiles.reduce((previous, file) => __awaiter(this, void 0, void 0, function* () {
|
156 | yield previous;
|
157 | const hash = yield this.fs.hash(file);
|
158 | hashTable[joinUrls(this.baseHref, file)] = hash;
|
159 | }), Promise.resolve());
|
160 | return {
|
161 | name: group.name,
|
162 | installMode: group.installMode || 'prefetch',
|
163 | updateMode: group.updateMode || group.installMode || 'prefetch',
|
164 | cacheQueryOptions: buildCacheQueryOptions(group.cacheQueryOptions),
|
165 | urls: matchedFiles.map(url => joinUrls(this.baseHref, url)),
|
166 | patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true)),
|
167 | };
|
168 | })));
|
169 | });
|
170 | }
|
171 | processDataGroups(config) {
|
172 | return (config.dataGroups || []).map(group => {
|
173 | return {
|
174 | name: group.name,
|
175 | patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)),
|
176 | strategy: group.cacheConfig.strategy || 'performance',
|
177 | maxSize: group.cacheConfig.maxSize,
|
178 | maxAge: parseDurationToMs(group.cacheConfig.maxAge),
|
179 | timeoutMs: group.cacheConfig.timeout && parseDurationToMs(group.cacheConfig.timeout),
|
180 | cacheQueryOptions: buildCacheQueryOptions(group.cacheQueryOptions),
|
181 | version: group.version !== undefined ? group.version : 1,
|
182 | };
|
183 | });
|
184 | }
|
185 | }
|
186 | function processNavigationUrls(baseHref, urls = DEFAULT_NAVIGATION_URLS) {
|
187 | return urls.map(url => {
|
188 | const positive = !url.startsWith('!');
|
189 | url = positive ? url : url.substr(1);
|
190 | return { positive, regex: `^${urlToRegex(url, baseHref)}$` };
|
191 | });
|
192 | }
|
193 | function globListToMatcher(globs) {
|
194 | const patterns = globs.map(pattern => {
|
195 | if (pattern.startsWith('!')) {
|
196 | return {
|
197 | positive: false,
|
198 | regex: new RegExp('^' + globToRegex(pattern.substr(1)) + '$'),
|
199 | };
|
200 | }
|
201 | else {
|
202 | return {
|
203 | positive: true,
|
204 | regex: new RegExp('^' + globToRegex(pattern) + '$'),
|
205 | };
|
206 | }
|
207 | });
|
208 | return (file) => matches(file, patterns);
|
209 | }
|
210 | function matches(file, patterns) {
|
211 | const res = patterns.reduce((isMatch, pattern) => {
|
212 | if (pattern.positive) {
|
213 | return isMatch || pattern.regex.test(file);
|
214 | }
|
215 | else {
|
216 | return isMatch && !pattern.regex.test(file);
|
217 | }
|
218 | }, false);
|
219 | return res;
|
220 | }
|
221 | function urlToRegex(url, baseHref, literalQuestionMark) {
|
222 | if (!url.startsWith('/') && url.indexOf('://') === -1) {
|
223 |
|
224 |
|
225 |
|
226 | url = joinUrls(baseHref.replace(/^\.(?=\/)/, ''), url);
|
227 | }
|
228 | return globToRegex(url, literalQuestionMark);
|
229 | }
|
230 | function joinUrls(a, b) {
|
231 | if (a.endsWith('/') && b.startsWith('/')) {
|
232 | return a + b.substr(1);
|
233 | }
|
234 | else if (!a.endsWith('/') && !b.startsWith('/')) {
|
235 | return a + '/' + b;
|
236 | }
|
237 | return a + b;
|
238 | }
|
239 | function withOrderedKeys(unorderedObj) {
|
240 | const orderedObj = {};
|
241 | Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]);
|
242 | return orderedObj;
|
243 | }
|
244 | function buildCacheQueryOptions(inOptions) {
|
245 | return Object.assign({ ignoreVary: true }, inOptions);
|
246 | }
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | export { Generator };
|
269 |
|