html-webpack-plugin 4.0.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +66 -28
- package/index.js +64 -72
- package/lib/cached-child-compiler.js +379 -0
- package/lib/child-compiler.js +183 -0
- package/lib/file-watcher-api.js +14 -0
- package/lib/html-tags.js +24 -0
- package/lib/webpack4/file-watcher-api.js +64 -0
- package/lib/webpack5/file-watcher-api.js +71 -0
- package/package.json +8 -7
- package/typings.d.ts +1 -0
- package/lib/compiler.js +0 -356
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file
|
|
4
|
+
* Helper plugin manages the cached state of the child compilation
|
|
5
|
+
*
|
|
6
|
+
* To optimize performance the child compilation is running asyncronously.
|
|
7
|
+
* Therefore it needs to be started in the compiler.make phase and ends after
|
|
8
|
+
* the compilation.afterCompile phase.
|
|
9
|
+
*
|
|
10
|
+
* To prevent bugs from blocked hooks there is no promise or event based api
|
|
11
|
+
* for this plugin.
|
|
12
|
+
*
|
|
13
|
+
* Example usage:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
const childCompilerPlugin = new PersistentChildCompilerPlugin();
|
|
17
|
+
childCompilerPlugin.addEntry('./src/index.js');
|
|
18
|
+
compiler.hooks.afterCompile.tapAsync('MyPlugin', (compilation, callback) => {
|
|
19
|
+
console.log(childCompilerPlugin.getCompilationResult()['./src/index.js']));
|
|
20
|
+
return true;
|
|
21
|
+
});
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Import types
|
|
26
|
+
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
|
|
27
|
+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
|
|
28
|
+
/** @typedef {{hash: string, entry: any, content: string }} ChildCompilationResultEntry */
|
|
29
|
+
/** @typedef {import("./webpack4/file-watcher-api").Snapshot} Snapshot */
|
|
30
|
+
/** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */
|
|
31
|
+
/** @typedef {{
|
|
32
|
+
dependencies: FileDependencies,
|
|
33
|
+
compiledEntries: {[entryName: string]: ChildCompilationResultEntry}
|
|
34
|
+
} | {
|
|
35
|
+
dependencies: FileDependencies,
|
|
36
|
+
error: Error
|
|
37
|
+
}} ChildCompilationResult */
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const { HtmlWebpackChildCompiler } = require('./child-compiler');
|
|
41
|
+
const fileWatcherApi = require('./file-watcher-api');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This plugin is a singleton for performance reasons.
|
|
45
|
+
* To keep track if a plugin does already exist for the compiler they are cached
|
|
46
|
+
* in this map
|
|
47
|
+
* @type {WeakMap<WebpackCompiler, PersistentChildCompilerSingletonPlugin>}}
|
|
48
|
+
*/
|
|
49
|
+
const compilerMap = new WeakMap();
|
|
50
|
+
|
|
51
|
+
class CachedChildCompilation {
|
|
52
|
+
/**
|
|
53
|
+
* @param {WebpackCompiler} compiler
|
|
54
|
+
*/
|
|
55
|
+
constructor (compiler) {
|
|
56
|
+
/**
|
|
57
|
+
* @private
|
|
58
|
+
* @type {WebpackCompiler}
|
|
59
|
+
*/
|
|
60
|
+
this.compiler = compiler;
|
|
61
|
+
// Create a singlton instance for the compiler
|
|
62
|
+
// if there is none
|
|
63
|
+
if (compilerMap.has(compiler)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const persistentChildCompilerSingletonPlugin = new PersistentChildCompilerSingletonPlugin();
|
|
67
|
+
compilerMap.set(compiler, persistentChildCompilerSingletonPlugin);
|
|
68
|
+
persistentChildCompilerSingletonPlugin.apply(compiler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* apply is called by the webpack main compiler during the start phase
|
|
73
|
+
* @param {string} entry
|
|
74
|
+
*/
|
|
75
|
+
addEntry (entry) {
|
|
76
|
+
const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
|
|
77
|
+
if (!persistentChildCompilerSingletonPlugin) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'PersistentChildCompilerSingletonPlugin instance not found.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
persistentChildCompilerSingletonPlugin.addEntry(entry);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getCompilationResult () {
|
|
86
|
+
const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
|
|
87
|
+
if (!persistentChildCompilerSingletonPlugin) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'PersistentChildCompilerSingletonPlugin instance not found.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return persistentChildCompilerSingletonPlugin.getLatestResult();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns the result for the given entry
|
|
97
|
+
* @param {string} entry
|
|
98
|
+
* @returns {
|
|
99
|
+
| { mainCompilationHash: string, error: Error }
|
|
100
|
+
| { mainCompilationHash: string, compiledEntry: ChildCompilationResultEntry }
|
|
101
|
+
}
|
|
102
|
+
*/
|
|
103
|
+
getCompilationEntryResult (entry) {
|
|
104
|
+
const latestResult = this.getCompilationResult();
|
|
105
|
+
const compilationResult = latestResult.compilationResult;
|
|
106
|
+
return 'error' in compilationResult ? {
|
|
107
|
+
mainCompilationHash: latestResult.mainCompilationHash,
|
|
108
|
+
error: compilationResult.error
|
|
109
|
+
} : {
|
|
110
|
+
mainCompilationHash: latestResult.mainCompilationHash,
|
|
111
|
+
compiledEntry: compilationResult.compiledEntries[entry]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class PersistentChildCompilerSingletonPlugin {
|
|
117
|
+
constructor () {
|
|
118
|
+
/**
|
|
119
|
+
* @private
|
|
120
|
+
* @type {
|
|
121
|
+
| {
|
|
122
|
+
isCompiling: false,
|
|
123
|
+
isVerifyingCache: false,
|
|
124
|
+
entries: string[],
|
|
125
|
+
compiledEntries: string[],
|
|
126
|
+
mainCompilationHash: string,
|
|
127
|
+
compilationResult: ChildCompilationResult
|
|
128
|
+
}
|
|
129
|
+
| Readonly<{
|
|
130
|
+
isCompiling: false,
|
|
131
|
+
isVerifyingCache: true,
|
|
132
|
+
entries: string[],
|
|
133
|
+
previousEntries: string[],
|
|
134
|
+
previousResult: ChildCompilationResult
|
|
135
|
+
}>
|
|
136
|
+
| Readonly <{
|
|
137
|
+
isVerifyingCache: false,
|
|
138
|
+
isCompiling: true,
|
|
139
|
+
entries: string[],
|
|
140
|
+
}>
|
|
141
|
+
} the internal compilation state */
|
|
142
|
+
this.compilationState = {
|
|
143
|
+
isCompiling: false,
|
|
144
|
+
isVerifyingCache: false,
|
|
145
|
+
entries: [],
|
|
146
|
+
compiledEntries: [],
|
|
147
|
+
mainCompilationHash: 'initial',
|
|
148
|
+
compilationResult: {
|
|
149
|
+
dependencies: {
|
|
150
|
+
fileDependencies: [],
|
|
151
|
+
contextDependencies: [],
|
|
152
|
+
missingDependencies: []
|
|
153
|
+
},
|
|
154
|
+
compiledEntries: {}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* apply is called by the webpack main compiler during the start phase
|
|
161
|
+
* @param {WebpackCompiler} compiler
|
|
162
|
+
*/
|
|
163
|
+
apply (compiler) {
|
|
164
|
+
/** @type Promise<ChildCompilationResult> */
|
|
165
|
+
let childCompilationResultPromise = Promise.resolve({
|
|
166
|
+
dependencies: {
|
|
167
|
+
fileDependencies: [],
|
|
168
|
+
contextDependencies: [],
|
|
169
|
+
missingDependencies: []
|
|
170
|
+
},
|
|
171
|
+
compiledEntries: {}
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* The main compilation hash which will only be updated
|
|
175
|
+
* if the childCompiler changes
|
|
176
|
+
*/
|
|
177
|
+
let mainCompilationHashOfLastChildRecompile = '';
|
|
178
|
+
/** @typedef{Snapshot|undefined} */
|
|
179
|
+
let previousFileSystemSnapshot;
|
|
180
|
+
let compilationStartTime = new Date().getTime();
|
|
181
|
+
|
|
182
|
+
compiler.hooks.make.tapAsync(
|
|
183
|
+
'PersistentChildCompilerSingletonPlugin',
|
|
184
|
+
(mainCompilation, callback) => {
|
|
185
|
+
if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
|
|
186
|
+
return callback(new Error('Child compilation has already started'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Update the time to the current compile start time
|
|
190
|
+
compilationStartTime = new Date().getTime();
|
|
191
|
+
|
|
192
|
+
// The compilation starts - adding new templates is now not possible anymore
|
|
193
|
+
this.compilationState = {
|
|
194
|
+
isCompiling: false,
|
|
195
|
+
isVerifyingCache: true,
|
|
196
|
+
previousEntries: this.compilationState.compiledEntries,
|
|
197
|
+
previousResult: this.compilationState.compilationResult,
|
|
198
|
+
entries: this.compilationState.entries
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Validate cache:
|
|
202
|
+
const isCacheValidPromise = this.isCacheValid(previousFileSystemSnapshot, mainCompilation);
|
|
203
|
+
|
|
204
|
+
let cachedResult = childCompilationResultPromise;
|
|
205
|
+
childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => {
|
|
206
|
+
// Reuse cache
|
|
207
|
+
if (isCacheValid) {
|
|
208
|
+
return cachedResult;
|
|
209
|
+
}
|
|
210
|
+
// Start the compilation
|
|
211
|
+
const compiledEntriesPromise = this.compileEntries(
|
|
212
|
+
mainCompilation,
|
|
213
|
+
this.compilationState.entries
|
|
214
|
+
);
|
|
215
|
+
// Update snapshot as soon as we know the filedependencies
|
|
216
|
+
// this might possibly cause bugs if files were changed inbetween
|
|
217
|
+
// compilation start and snapshot creation
|
|
218
|
+
compiledEntriesPromise.then((childCompilationResult) => {
|
|
219
|
+
return fileWatcherApi.createSnapshot(childCompilationResult.dependencies, mainCompilation, compilationStartTime);
|
|
220
|
+
}).then((snapshot) => {
|
|
221
|
+
previousFileSystemSnapshot = snapshot;
|
|
222
|
+
});
|
|
223
|
+
return compiledEntriesPromise;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Add files to compilation which needs to be watched:
|
|
227
|
+
mainCompilation.hooks.optimizeTree.tapAsync(
|
|
228
|
+
'PersistentChildCompilerSingletonPlugin',
|
|
229
|
+
(chunks, modules, callback) => {
|
|
230
|
+
const handleCompilationDonePromise = childCompilationResultPromise.then(
|
|
231
|
+
childCompilationResult => {
|
|
232
|
+
this.watchFiles(
|
|
233
|
+
mainCompilation,
|
|
234
|
+
childCompilationResult.dependencies
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
handleCompilationDonePromise.then(() => callback(null, chunks, modules), callback);
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Store the final compilation once the main compilation hash is known
|
|
242
|
+
mainCompilation.hooks.additionalAssets.tapAsync(
|
|
243
|
+
'PersistentChildCompilerSingletonPlugin',
|
|
244
|
+
(callback) => {
|
|
245
|
+
const didRecompilePromise = Promise.all([childCompilationResultPromise, cachedResult]).then(
|
|
246
|
+
([childCompilationResult, cachedResult]) => {
|
|
247
|
+
// Update if childCompilation changed
|
|
248
|
+
return (cachedResult !== childCompilationResult);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const handleCompilationDonePromise = Promise.all([childCompilationResultPromise, didRecompilePromise]).then(
|
|
253
|
+
([childCompilationResult, didRecompile]) => {
|
|
254
|
+
// Update hash and snapshot if childCompilation changed
|
|
255
|
+
if (didRecompile) {
|
|
256
|
+
mainCompilationHashOfLastChildRecompile = mainCompilation.hash;
|
|
257
|
+
}
|
|
258
|
+
this.compilationState = {
|
|
259
|
+
isCompiling: false,
|
|
260
|
+
isVerifyingCache: false,
|
|
261
|
+
entries: this.compilationState.entries,
|
|
262
|
+
compiledEntries: this.compilationState.entries,
|
|
263
|
+
compilationResult: childCompilationResult,
|
|
264
|
+
mainCompilationHash: mainCompilationHashOfLastChildRecompile
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
handleCompilationDonePromise.then(() => callback(null), callback);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Continue compilation:
|
|
272
|
+
callback(null);
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Add a new entry to the next compile run
|
|
279
|
+
* @param {string} entry
|
|
280
|
+
*/
|
|
281
|
+
addEntry (entry) {
|
|
282
|
+
if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
'The child compiler has already started to compile. ' +
|
|
285
|
+
"Please add entries before the main compiler 'make' phase has started or " +
|
|
286
|
+
'after the compilation is done.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (this.compilationState.entries.indexOf(entry) === -1) {
|
|
290
|
+
this.compilationState.entries = [...this.compilationState.entries, entry];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getLatestResult () {
|
|
295
|
+
if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
'The child compiler is not done compiling. ' +
|
|
298
|
+
"Please access the result after the compiler 'make' phase has started or " +
|
|
299
|
+
'after the compilation is done.'
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
mainCompilationHash: this.compilationState.mainCompilationHash,
|
|
304
|
+
compilationResult: this.compilationState.compilationResult
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Verify that the cache is still valid
|
|
310
|
+
* @private
|
|
311
|
+
* @param {Snapshot | undefined} snapshot
|
|
312
|
+
* @param {WebpackCompilation} mainCompilation
|
|
313
|
+
* @returns {Promise<boolean>}
|
|
314
|
+
*/
|
|
315
|
+
isCacheValid (snapshot, mainCompilation) {
|
|
316
|
+
if (!this.compilationState.isVerifyingCache) {
|
|
317
|
+
return Promise.reject(new Error('Cache validation can only be done right before the compilation starts'));
|
|
318
|
+
}
|
|
319
|
+
// If there are no entries we don't need a new child compilation
|
|
320
|
+
if (this.compilationState.entries.length === 0) {
|
|
321
|
+
return Promise.resolve(true);
|
|
322
|
+
}
|
|
323
|
+
// If there are new entries the cache is invalid
|
|
324
|
+
if (this.compilationState.entries !== this.compilationState.previousEntries) {
|
|
325
|
+
return Promise.resolve(false);
|
|
326
|
+
}
|
|
327
|
+
// Mark the cache as invalid if there is no snapshot
|
|
328
|
+
if (!snapshot) {
|
|
329
|
+
return Promise.resolve(false);
|
|
330
|
+
}
|
|
331
|
+
return fileWatcherApi.isSnapShotValid(snapshot, mainCompilation);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Start to compile all templates
|
|
336
|
+
*
|
|
337
|
+
* @private
|
|
338
|
+
* @param {WebpackCompilation} mainCompilation
|
|
339
|
+
* @param {string[]} entries
|
|
340
|
+
* @returns {Promise<ChildCompilationResult>}
|
|
341
|
+
*/
|
|
342
|
+
compileEntries (mainCompilation, entries) {
|
|
343
|
+
const compiler = new HtmlWebpackChildCompiler(entries);
|
|
344
|
+
return compiler.compileTemplates(mainCompilation).then((result) => {
|
|
345
|
+
return {
|
|
346
|
+
// The compiled sources to render the content
|
|
347
|
+
compiledEntries: result,
|
|
348
|
+
// The file dependencies to find out if a
|
|
349
|
+
// recompilation is required
|
|
350
|
+
dependencies: compiler.fileDependencies,
|
|
351
|
+
// The main compilation hash can be used to find out
|
|
352
|
+
// if this compilation was done during the current compilation
|
|
353
|
+
mainCompilationHash: mainCompilation.hash
|
|
354
|
+
};
|
|
355
|
+
}, error => ({
|
|
356
|
+
// The compiled sources to render the content
|
|
357
|
+
error,
|
|
358
|
+
// The file dependencies to find out if a
|
|
359
|
+
// recompilation is required
|
|
360
|
+
dependencies: compiler.fileDependencies,
|
|
361
|
+
// The main compilation hash can be used to find out
|
|
362
|
+
// if this compilation was done during the current compilation
|
|
363
|
+
mainCompilationHash: mainCompilation.hash
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @private
|
|
369
|
+
* @param {WebpackCompilation} mainCompilation
|
|
370
|
+
* @param {FileDependencies} files
|
|
371
|
+
*/
|
|
372
|
+
watchFiles (mainCompilation, files) {
|
|
373
|
+
fileWatcherApi.watchFiles(mainCompilation, files);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
CachedChildCompilation
|
|
379
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
|
|
3
|
+
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
|
|
4
|
+
/** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */
|
|
5
|
+
'use strict';
|
|
6
|
+
/**
|
|
7
|
+
* @file
|
|
8
|
+
* This file uses webpack to compile a template with a child compiler.
|
|
9
|
+
*
|
|
10
|
+
* [TEMPLATE] -> [JAVASCRIPT]
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
|
|
15
|
+
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
|
|
16
|
+
const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
|
|
17
|
+
const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
|
|
18
|
+
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The HtmlWebpackChildCompiler is a helper to allow resusing one childCompiler
|
|
22
|
+
* for multile HtmlWebpackPlugin instances to improve the compilation performance.
|
|
23
|
+
*/
|
|
24
|
+
class HtmlWebpackChildCompiler {
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param {string[]} templates
|
|
28
|
+
*/
|
|
29
|
+
constructor (templates) {
|
|
30
|
+
/**
|
|
31
|
+
* @type {string[]} templateIds
|
|
32
|
+
* The template array will allow us to keep track which input generated which output
|
|
33
|
+
*/
|
|
34
|
+
this.templates = templates;
|
|
35
|
+
/**
|
|
36
|
+
* @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
|
|
37
|
+
*/
|
|
38
|
+
this.compilationPromise; // eslint-disable-line
|
|
39
|
+
/**
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
this.compilationStartedTimestamp; // eslint-disable-line
|
|
43
|
+
/**
|
|
44
|
+
* @type {number}
|
|
45
|
+
*/
|
|
46
|
+
this.compilationEndedTimestamp; // eslint-disable-line
|
|
47
|
+
/**
|
|
48
|
+
* All file dependencies of the child compiler
|
|
49
|
+
* @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
|
|
50
|
+
*/
|
|
51
|
+
this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if the childCompiler is currently compiling
|
|
56
|
+
* @retuns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
isCompiling () {
|
|
59
|
+
return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns true if the childCOmpiler is done compiling
|
|
64
|
+
*/
|
|
65
|
+
didCompile () {
|
|
66
|
+
return this.compilationEndedTimestamp !== undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function will start the template compilation
|
|
71
|
+
* once it is started no more templates can be added
|
|
72
|
+
*
|
|
73
|
+
* @param {WebpackCompilation} mainCompilation
|
|
74
|
+
* @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
|
|
75
|
+
*/
|
|
76
|
+
compileTemplates (mainCompilation) {
|
|
77
|
+
// To prevent multiple compilations for the same template
|
|
78
|
+
// the compilation is cached in a promise.
|
|
79
|
+
// If it already exists return
|
|
80
|
+
if (this.compilationPromise) {
|
|
81
|
+
return this.compilationPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The entry file is just an empty helper as the dynamic template
|
|
85
|
+
// require is added in "loader.js"
|
|
86
|
+
const outputOptions = {
|
|
87
|
+
filename: '__child-[name]',
|
|
88
|
+
publicPath: mainCompilation.outputOptions.publicPath
|
|
89
|
+
};
|
|
90
|
+
const compilerName = 'HtmlWebpackCompiler';
|
|
91
|
+
// Create an additional child compiler which takes the template
|
|
92
|
+
// and turns it into an Node.JS html factory.
|
|
93
|
+
// This allows us to use loaders during the compilation
|
|
94
|
+
const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
|
|
95
|
+
// The file path context which webpack uses to resolve all relative files to
|
|
96
|
+
childCompiler.context = mainCompilation.compiler.context;
|
|
97
|
+
// Compile the template to nodejs javascript
|
|
98
|
+
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
|
|
99
|
+
new NodeTargetPlugin().apply(childCompiler);
|
|
100
|
+
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
|
|
101
|
+
new LoaderTargetPlugin('node').apply(childCompiler);
|
|
102
|
+
|
|
103
|
+
// Add all templates
|
|
104
|
+
this.templates.forEach((template, index) => {
|
|
105
|
+
new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.compilationStartedTimestamp = new Date().getTime();
|
|
109
|
+
this.compilationPromise = new Promise((resolve, reject) => {
|
|
110
|
+
childCompiler.runAsChild((err, entries, childCompilation) => {
|
|
111
|
+
// Extract templates
|
|
112
|
+
const compiledTemplates = entries
|
|
113
|
+
? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
|
|
114
|
+
: [];
|
|
115
|
+
// Extract file dependencies
|
|
116
|
+
if (entries) {
|
|
117
|
+
this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
|
|
118
|
+
}
|
|
119
|
+
// Reject the promise if the childCompilation contains error
|
|
120
|
+
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
|
|
121
|
+
const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
|
|
122
|
+
reject(new Error('Child compilation failed:\n' + errorDetails));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Reject if the error object contains errors
|
|
126
|
+
if (err) {
|
|
127
|
+
reject(err);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
|
|
132
|
+
*/
|
|
133
|
+
const result = {};
|
|
134
|
+
compiledTemplates.forEach((templateSource, entryIndex) => {
|
|
135
|
+
// The compiledTemplates are generated from the entries added in
|
|
136
|
+
// the addTemplate function.
|
|
137
|
+
// Therefore the array index of this.templates should be the as entryIndex.
|
|
138
|
+
result[this.templates[entryIndex]] = {
|
|
139
|
+
content: templateSource,
|
|
140
|
+
hash: childCompilation.hash,
|
|
141
|
+
entry: entries[entryIndex]
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
this.compilationEndedTimestamp = new Date().getTime();
|
|
145
|
+
resolve(result);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return this.compilationPromise;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The webpack child compilation will create files as a side effect.
|
|
155
|
+
* This function will extract them and clean them up so they won't be written to disk.
|
|
156
|
+
*
|
|
157
|
+
* Returns the source code of the compiled templates as string
|
|
158
|
+
*
|
|
159
|
+
* @returns Array<string>
|
|
160
|
+
*/
|
|
161
|
+
function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
|
|
162
|
+
const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
|
|
163
|
+
return mainCompilation.mainTemplate.getAssetPath(filename, {
|
|
164
|
+
hash: childCompilation.hash,
|
|
165
|
+
chunk: entryChunk,
|
|
166
|
+
name: `HtmlWebpackPlugin_${index}`
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
helperAssetNames.forEach((helperFileName) => {
|
|
171
|
+
delete mainCompilation.assets[helperFileName];
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const helperContents = helperAssetNames.map((helperFileName) => {
|
|
175
|
+
return childCompilation.assets[helperFileName].source();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return helperContents;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
HtmlWebpackChildCompiler
|
|
183
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* To use the availble webpack core api
|
|
4
|
+
* we have to use different child compilers
|
|
5
|
+
* depending on the used webpack version
|
|
6
|
+
*/
|
|
7
|
+
const webpackMajorVersion = Number(require('webpack/package.json').version.split('.')[0]);
|
|
8
|
+
|
|
9
|
+
// Typescript hack to test only the webpack 4 code
|
|
10
|
+
/** @type {import('./webpack4/file-watcher-api')} */
|
|
11
|
+
module.exports = webpackMajorVersion === 4
|
|
12
|
+
? require('./webpack4/file-watcher-api.js')
|
|
13
|
+
// Hack to ignore './webpack5/file-watcher-api.js' from typescript:
|
|
14
|
+
: require('./webpack' + 5 + '/file-watcher-api.js');
|
package/lib/html-tags.js
CHANGED
|
@@ -65,7 +65,31 @@ function createHtmlTagObject (tagName, attributes, innerHTML) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The `HtmlTagArray Array with a custom `.toString()` method.
|
|
70
|
+
*
|
|
71
|
+
* This allows the following:
|
|
72
|
+
* ```
|
|
73
|
+
* const tags = HtmlTagArray.from([tag1, tag2]);
|
|
74
|
+
* const scriptTags = tags.filter((tag) => tag.tagName === 'script');
|
|
75
|
+
* const html = scriptTags.toString();
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* Or inside a string literal:
|
|
79
|
+
* ```
|
|
80
|
+
* const tags = HtmlTagArray.from([tag1, tag2]);
|
|
81
|
+
* const html = `<html><body>${tags.filter((tag) => tag.tagName === 'script')}</body></html>`;
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
*/
|
|
85
|
+
class HtmlTagArray extends Array {
|
|
86
|
+
toString () {
|
|
87
|
+
return this.join('');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
module.exports = {
|
|
92
|
+
HtmlTagArray: HtmlTagArray,
|
|
69
93
|
createHtmlTagObject: createHtmlTagObject,
|
|
70
94
|
htmlTagObjectToString: htmlTagObjectToString
|
|
71
95
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
|
|
2
|
+
/** @typedef {{timestamp: number, fileDependencies: string[]}} Snapshot */
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
|
|
8
|
+
* @param {WebpackCompilation} compilation
|
|
9
|
+
* @param {number} startTime
|
|
10
|
+
*/
|
|
11
|
+
function createSnapshot (fileDependencies, compilation, startTime) {
|
|
12
|
+
const flatDependencies = [];
|
|
13
|
+
Object.keys(fileDependencies).forEach((depencyTypes) => {
|
|
14
|
+
fileDependencies[depencyTypes].forEach(fileDependency => {
|
|
15
|
+
flatDependencies.push(fileDependency);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
fileDependencies: flatDependencies,
|
|
20
|
+
timestamp: startTime
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the files inside this snapshot
|
|
26
|
+
* have not been changed
|
|
27
|
+
*
|
|
28
|
+
* @param {Snapshot} snapshot
|
|
29
|
+
* @param {WebpackCompilation} compilation
|
|
30
|
+
* @returns {Promise<boolean>}
|
|
31
|
+
*/
|
|
32
|
+
function isSnapShotValid (snapshot, compilation) {
|
|
33
|
+
// Check if any dependent file was changed after the last compilation
|
|
34
|
+
const fileTimestamps = compilation.fileTimestamps;
|
|
35
|
+
const isCacheOutOfDate = snapshot.fileDependencies.some((fileDependency) => {
|
|
36
|
+
const timestamp = fileTimestamps.get(fileDependency);
|
|
37
|
+
// If the timestamp is not known the file is new
|
|
38
|
+
// If the timestamp is larger then the file has changed
|
|
39
|
+
// Otherwise the file is still the same
|
|
40
|
+
return !timestamp || timestamp > snapshot.timestamp;
|
|
41
|
+
});
|
|
42
|
+
return Promise.resolve(!isCacheOutOfDate);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ensure that the files keep watched for changes
|
|
47
|
+
* and will trigger a recompile
|
|
48
|
+
*
|
|
49
|
+
* @param {WebpackCompilation} mainCompilation
|
|
50
|
+
* @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
|
|
51
|
+
*/
|
|
52
|
+
function watchFiles (mainCompilation, fileDependencies) {
|
|
53
|
+
Object.keys(fileDependencies).forEach((depencyTypes) => {
|
|
54
|
+
fileDependencies[depencyTypes].forEach(fileDependency => {
|
|
55
|
+
mainCompilation.compilationDependencies.add(fileDependency);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
createSnapshot,
|
|
62
|
+
isSnapShotValid,
|
|
63
|
+
watchFiles
|
|
64
|
+
};
|