-
Notifications
You must be signed in to change notification settings - Fork 798
/
inject-manifest.ts
435 lines (398 loc) 路 15.8 KB
/
inject-manifest.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {escapeRegExp} from 'workbox-build/build/lib/escape-regexp';
import {replaceAndUpdateSourceMap} from 'workbox-build/build/lib/replace-and-update-source-map';
import {validateWebpackInjectManifestOptions} from 'workbox-build/build/lib/validate-options';
import prettyBytes from 'pretty-bytes';
import stringify from 'fast-json-stable-stringify';
import upath from 'upath';
import webpack from 'webpack';
//import {CommonConfig} from './types';
import {getManifestEntriesFromCompilation} from './lib/get-manifest-entries-from-compilation';
import {getSourcemapAssetName} from './lib/get-sourcemap-asset-name';
import {relativeToOutputPath} from './lib/relative-to-output-path';
import {WebpackInjectManifestOptions} from 'workbox-build';
// Used to keep track of swDest files written by *any* instance of this plugin.
// See https://github.com/GoogleChrome/workbox/issues/2181
const _generatedAssetNames = new Set<string>();
// SingleEntryPlugin in v4 was renamed to EntryPlugin in v5.
const SingleEntryPlugin = webpack.EntryPlugin || webpack.SingleEntryPlugin;
// webpack v4/v5 compatibility:
// https://github.com/webpack/webpack/issues/11425#issuecomment-686607633
const {RawSource} = webpack.sources || require('webpack-sources');
/**
* This class supports compiling a service worker file provided via `swSrc`,
* and injecting into that service worker a list of URLs and revision
* information for precaching based on the webpack asset pipeline.
*
* Use an instance of `InjectManifest` in the
* [`plugins` array](https://webpack.js.org/concepts/plugins/#usage) of a
* webpack config.
*
* @memberof module:workbox-webpack-plugin
*/
class InjectManifest {
private config: WebpackInjectManifestOptions;
private alreadyCalled: boolean;
// eslint-disable-next-line jsdoc/newline-after-description
/**
* Creates an instance of InjectManifest.
*
* @param {Object} config The configuration to use.
*
* @param {string} config.swSrc An existing service worker file that will be
* compiled and have a precache manifest injected into it.
*
* @param {Array<module:workbox-build.ManifestEntry>} [config.additionalManifestEntries]
* A list of entries to be precached, in addition to any entries that are
* generated as part of the build configuration.
*
* @param {Array<string>} [config.chunks] One or more chunk names whose corresponding
* output files should be included in the precache manifest.
*
* @param {boolean} [config.compileSrc=true] When `true` (the default), the
* `swSrc` file will be compiled by webpack. When `false`, compilation will
* not occur (and `webpackCompilationPlugins` can't be used.) Set to `false`
* if you want to inject the manifest into, e.g., a JSON file.
*
* @param {RegExp} [config.dontCacheBustURLsMatching] Assets that match this will be
* assumed to be uniquely versioned via their URL, and exempted from the normal
* HTTP cache-busting that's done when populating the precache. (As of Workbox
* v6, this option is usually not needed, as each
* [asset's metadata](https://github.com/webpack/webpack/issues/9038) is used
* to determine whether it's immutable or not.)
*
* @param {Array<string|RegExp>} [config.exclude=[/\.map$/, /^manifest.*\.js$]]
* One or more specifiers used to exclude assets from the precache manifest.
* This is interpreted following
* [the same rules](https://webpack.js.org/configuration/module/#condition)
* as `webpack`'s standard `exclude` option.
*
* @param {Array<string>} [config.excludeChunks] One or more chunk names whose
* corresponding output files should be excluded from the precache manifest.
*
* @param {Array<string|RegExp>} [config.include]
* One or more specifiers used to include assets in the precache manifest.
* This is interpreted following
* [the same rules](https://webpack.js.org/configuration/module/#condition)
* as `webpack`'s standard `include` option.
*
* @param {string} [config.injectionPoint='self.__WB_MANIFEST'] The string to
* find inside of the `swSrc` file. Once found, it will be replaced by the
* generated precache manifest.
*
* @param {Array<module:workbox-build.ManifestTransform>} [config.manifestTransforms]
* One or more functions which will be applied sequentially against the
* generated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are
* also specified, their corresponding transformations will be applied first.
*
* @param {number} [config.maximumFileSizeToCacheInBytes=2097152] This value can be
* used to determine the maximum size of files that will be precached. This
* prevents you from inadvertently precaching very large files that might have
* accidentally matched one of your patterns.
*
* @param {string} [config.mode] If set to 'production', then an optimized service
* worker bundle that excludes debugging info will be produced. If not explicitly
* configured here, the `mode` value configured in the current `webpack`
* compilation will be used.
*
* @param {object<string, string>} [config.modifyURLPrefix] A mapping of prefixes
* that, if present in an entry in the precache manifest, will be replaced with
* the corresponding value. This can be used to, for example, remove or add a
* path prefix from a manifest entry if your web hosting setup doesn't match
* your local filesystem setup. As an alternative with more flexibility, you can
* use the `manifestTransforms` option and provide a function that modifies the
* entries in the manifest using whatever logic you provide.
*
* @param {string} [config.swDest] The asset name of the
* service worker file that will be created by this plugin. If omitted, the
* name will be based on the `swSrc` name.
*
* @param {Array<Object>} [config.webpackCompilationPlugins] Optional `webpack`
* plugins that will be used when compiling the `swSrc` input file.
*/
constructor(config: WebpackInjectManifestOptions) {
this.config = config;
this.alreadyCalled = false;
}
/**
* @param {Object} [compiler] default compiler object passed from webpack
*
* @private
*/
propagateWebpackConfig(compiler: webpack.Compiler): void {
// Because this.config is listed last, properties that are already set
// there take precedence over derived properties from the compiler.
this.config = Object.assign(
{
mode: compiler.options.mode,
// Use swSrc with a hardcoded .js extension, in case swSrc is a .ts file.
swDest: upath.parse(this.config.swSrc).name + '.js',
},
this.config,
);
}
/**
* @param {Object} [compiler] default compiler object passed from webpack
*
* @private
*/
apply(compiler: webpack.Compiler): void {
this.propagateWebpackConfig(compiler);
compiler.hooks.make.tapPromise(this.constructor.name, (compilation) =>
this.handleMake(compilation, compiler).catch(
(error: webpack.WebpackError) => {
compilation.errors.push(error);
},
),
);
// webpack v4/v5 compatibility:
// https://github.com/webpack/webpack/issues/11425#issuecomment-690387207
if (webpack.version?.startsWith('4.')) {
compiler.hooks.emit.tapPromise(this.constructor.name, (compilation) =>
this.addAssets(compilation).catch((error: webpack.WebpackError) => {
compilation.errors.push(error);
}),
);
} else {
const {PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER} = webpack.Compilation;
// Specifically hook into thisCompilation, as per
// https://github.com/webpack/webpack/issues/11425#issuecomment-690547848
compiler.hooks.thisCompilation.tap(
this.constructor.name,
(compilation) => {
compilation.hooks.processAssets.tapPromise(
{
name: this.constructor.name,
// TODO(jeffposnick): This may need to change eventually.
// See https://github.com/webpack/webpack/issues/11822#issuecomment-726184972
stage: PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER - 10,
},
() =>
this.addAssets(compilation).catch(
(error: webpack.WebpackError) => {
compilation.errors.push(error);
},
),
);
},
);
}
}
/**
* @param {Object} compilation The webpack compilation.
* @param {Object} parentCompiler The webpack parent compiler.
*
* @private
*/
async performChildCompilation(
compilation: webpack.Compilation,
parentCompiler: webpack.Compiler,
): Promise<void> {
const outputOptions = {
path: parentCompiler.options.output.path,
filename: this.config.swDest,
};
const childCompiler = compilation.createChildCompiler(
this.constructor.name,
outputOptions,
[],
);
childCompiler.context = parentCompiler.context;
childCompiler.inputFileSystem = parentCompiler.inputFileSystem;
childCompiler.outputFileSystem = parentCompiler.outputFileSystem;
if (Array.isArray(this.config.webpackCompilationPlugins)) {
for (const plugin of this.config.webpackCompilationPlugins) {
plugin.apply(childCompiler); //eslint-disable-line
}
}
new SingleEntryPlugin(
parentCompiler.context,
this.config.swSrc,
this.constructor.name,
).apply(childCompiler);
await new Promise<void>((resolve, reject) => {
childCompiler.runAsChild((error, _entries, childCompilation) => {
if (error) {
reject(error);
} else {
compilation.warnings = compilation.warnings.concat(
childCompilation?.warnings ?? [],
);
compilation.errors = compilation.errors.concat(
childCompilation?.errors ?? [],
);
resolve();
}
});
});
}
/**
* @param {Object} compilation The webpack compilation.
* @param {Object} parentCompiler The webpack parent compiler.
*
* @private
*/
addSrcToAssets(
compilation: webpack.Compilation,
parentCompiler: webpack.Compiler,
): void {
// eslint-disable-next-line
const source = (parentCompiler.inputFileSystem as any).readFileSync(
this.config.swSrc,
);
compilation.emitAsset(this.config.swDest!, new RawSource(source));
}
/**
* @param {Object} compilation The webpack compilation.
* @param {Object} parentCompiler The webpack parent compiler.
*
* @private
*/
async handleMake(
compilation: webpack.Compilation,
parentCompiler: webpack.Compiler,
): Promise<void> {
try {
this.config = validateWebpackInjectManifestOptions(this.config);
} catch (error) {
// eslint-disable-next-line
if (error instanceof Error) {
throw new Error(
`Please check your ${this.constructor.name} plugin ` +
// eslint-disable-next-line
`configuration:\n${error.message}`,
);
}
}
this.config.swDest = relativeToOutputPath(compilation, this.config.swDest!);
_generatedAssetNames.add(this.config.swDest);
if (this.config.compileSrc) {
await this.performChildCompilation(compilation, parentCompiler);
} else {
this.addSrcToAssets(compilation, parentCompiler);
// This used to be a fatal error, but just warn at runtime because we
// can't validate it easily.
if (
Array.isArray(this.config.webpackCompilationPlugins) &&
this.config.webpackCompilationPlugins.length > 0
) {
compilation.warnings.push(
new Error(
'compileSrc is false, so the ' +
'webpackCompilationPlugins option will be ignored.',
) as webpack.WebpackError,
);
}
}
}
/**
* @param {Object} compilation The webpack compilation.
*
* @private
*/
async addAssets(compilation: webpack.Compilation): Promise<void> {
// See https://github.com/GoogleChrome/workbox/issues/1790
if (this.alreadyCalled) {
const warningMessage =
`${this.constructor.name} has been called ` +
`multiple times, perhaps due to running webpack in --watch mode. The ` +
`precache manifest generated after the first call may be inaccurate! ` +
`Please see https://github.com/GoogleChrome/workbox/issues/1790 for ` +
`more information.`;
if (
!compilation.warnings.some(
(warning) =>
warning instanceof Error && warning.message === warningMessage,
)
) {
compilation.warnings.push(
new Error(warningMessage) as webpack.WebpackError,
);
}
} else {
this.alreadyCalled = true;
}
const config = Object.assign({}, this.config);
// Ensure that we don't precache any of the assets generated by *any*
// instance of this plugin.
config.exclude!.push((name: string) => _generatedAssetNames.has(name));
// See https://webpack.js.org/contribute/plugin-patterns/#monitoring-the-watch-graph
const absoluteSwSrc = upath.resolve(this.config.swSrc);
compilation.fileDependencies.add(absoluteSwSrc);
const swAsset = compilation.getAsset(config.swDest!);
const swAssetString = swAsset!.source.source().toString();
const globalRegexp = new RegExp(escapeRegExp(config.injectionPoint!), 'g');
const injectionResults = swAssetString.match(globalRegexp);
if (!injectionResults) {
throw new Error(
`Can't find ${config.injectionPoint ?? ''} in your SW source.`,
);
}
if (injectionResults.length !== 1) {
throw new Error(
`Multiple instances of ${config.injectionPoint ?? ''} were ` +
`found in your SW source. Include it only once. For more info, see ` +
`https://github.com/GoogleChrome/workbox/issues/2681`,
);
}
const {size, sortedEntries} = await getManifestEntriesFromCompilation(
compilation,
config,
);
let manifestString = stringify(sortedEntries);
if (
this.config.compileSrc &&
// See https://github.com/GoogleChrome/workbox/issues/2729
// (TODO: Switch to ?. once our linter supports it.)
!(
compilation.options &&
compilation.options.devtool === 'eval-cheap-source-map' &&
compilation.options.optimization &&
compilation.options.optimization.minimize
)
) {
// See https://github.com/GoogleChrome/workbox/issues/2263
manifestString = manifestString.replace(/"/g, `'`);
}
const sourcemapAssetName = getSourcemapAssetName(
compilation,
swAssetString,
config.swDest!,
);
if (sourcemapAssetName) {
_generatedAssetNames.add(sourcemapAssetName);
const sourcemapAsset = compilation.getAsset(sourcemapAssetName);
const {source, map} = await replaceAndUpdateSourceMap({
// eslint-disable-next-line
jsFilename: config.swDest!,
// eslint-disable-next-line
originalMap: JSON.parse(sourcemapAsset!.source.source().toString()),
originalSource: swAssetString,
replaceString: manifestString,
searchString: config.injectionPoint!,
});
compilation.updateAsset(sourcemapAssetName, new RawSource(map));
compilation.updateAsset(config.swDest!, new RawSource(source));
} else {
// If there's no sourcemap associated with swDest, a simple string
// replacement will suffice.
compilation.updateAsset(
config.swDest!,
new RawSource(
swAssetString.replace(config.injectionPoint!, manifestString),
),
);
}
if (compilation.getLogger) {
const logger = compilation.getLogger(this.constructor.name);
logger.info(`The service worker at ${config.swDest ?? ''} will precache
${sortedEntries.length} URLs, totaling ${prettyBytes(size)}.`);
}
}
}
export {InjectManifest};