html-webpack-plugin 3.1.0 → 4.0.0-alpha.2
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 +17 -0
- package/README.md +113 -36
- package/default_index.ejs +1 -1
- package/index.js +618 -397
- package/lib/chunksorter.js +19 -116
- package/lib/compiler.js +359 -101
- package/lib/errors.js +2 -1
- package/lib/hooks.js +116 -0
- package/lib/html-tags.js +73 -0
- package/lib/loader.js +16 -34
- package/package.json +34 -15
package/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Import types
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
/// <reference path="./typings.d.ts" />
|
|
5
|
+
/* eslint-enable */
|
|
6
|
+
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
|
|
7
|
+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
|
|
1
8
|
'use strict';
|
|
2
9
|
|
|
3
10
|
// use Polyfill for util.promisify in node versions < v8
|
|
@@ -7,18 +14,31 @@ const vm = require('vm');
|
|
|
7
14
|
const fs = require('fs');
|
|
8
15
|
const _ = require('lodash');
|
|
9
16
|
const path = require('path');
|
|
17
|
+
const loaderUtils = require('loader-utils');
|
|
18
|
+
|
|
19
|
+
const { createHtmlTagObject, htmlTagObjectToString } = require('./lib/html-tags');
|
|
20
|
+
|
|
10
21
|
const childCompiler = require('./lib/compiler.js');
|
|
11
22
|
const prettyError = require('./lib/errors.js');
|
|
12
23
|
const chunkSorter = require('./lib/chunksorter.js');
|
|
24
|
+
const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
|
|
13
25
|
|
|
14
26
|
const fsStatAsync = promisify(fs.stat);
|
|
15
27
|
const fsReadFileAsync = promisify(fs.readFile);
|
|
16
28
|
|
|
17
29
|
class HtmlWebpackPlugin {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Partial<HtmlWebpackPluginOptions>} [options]
|
|
32
|
+
*/
|
|
18
33
|
constructor (options) {
|
|
34
|
+
/** @type {Partial<HtmlWebpackPluginOptions>} */
|
|
35
|
+
const userOptions = options || {};
|
|
36
|
+
|
|
19
37
|
// Default options
|
|
20
|
-
|
|
38
|
+
/** @type {HtmlWebpackPluginOptions} */
|
|
39
|
+
const defaultOptions = {
|
|
21
40
|
template: path.join(__dirname, 'default_index.ejs'),
|
|
41
|
+
templateContent: false,
|
|
22
42
|
templateParameters: templateParametersGenerator,
|
|
23
43
|
filename: 'index.html',
|
|
24
44
|
hash: false,
|
|
@@ -30,14 +50,44 @@ class HtmlWebpackPlugin {
|
|
|
30
50
|
showErrors: true,
|
|
31
51
|
chunks: 'all',
|
|
32
52
|
excludeChunks: [],
|
|
53
|
+
chunksSortMode: 'auto',
|
|
54
|
+
meta: {},
|
|
33
55
|
title: 'Webpack App',
|
|
34
56
|
xhtml: false
|
|
35
|
-
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** @type {HtmlWebpackPluginOptions} */
|
|
60
|
+
this.options = Object.assign(defaultOptions, userOptions);
|
|
61
|
+
|
|
62
|
+
// Default metaOptions if no template is provided
|
|
63
|
+
if (!userOptions.template && this.options.templateContent === false && this.options.meta) {
|
|
64
|
+
const defaultMeta = {
|
|
65
|
+
// From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
|
|
66
|
+
viewport: 'width=device-width, initial-scale=1'
|
|
67
|
+
};
|
|
68
|
+
this.options.meta = Object.assign({}, this.options.meta, defaultMeta, userOptions.meta);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Instance variables to keep caching information
|
|
72
|
+
// for multiple builds
|
|
73
|
+
this.childCompilerHash = undefined;
|
|
74
|
+
/**
|
|
75
|
+
* @type {string | undefined}
|
|
76
|
+
*/
|
|
77
|
+
this.childCompilationOutputName = undefined;
|
|
78
|
+
this.assetJson = undefined;
|
|
79
|
+
this.hash = undefined;
|
|
80
|
+
this.version = HtmlWebpackPlugin.version;
|
|
36
81
|
}
|
|
37
82
|
|
|
83
|
+
/**
|
|
84
|
+
* apply is called by the webpack main compiler during the start phase
|
|
85
|
+
* @param {WebpackCompiler} compiler
|
|
86
|
+
*/
|
|
38
87
|
apply (compiler) {
|
|
39
88
|
const self = this;
|
|
40
89
|
let isCompilationCached = false;
|
|
90
|
+
/** @type Promise<string> */
|
|
41
91
|
let compilationPromise;
|
|
42
92
|
|
|
43
93
|
this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
|
|
@@ -49,34 +99,41 @@ class HtmlWebpackPlugin {
|
|
|
49
99
|
this.options.filename = path.relative(compiler.options.output.path, filename);
|
|
50
100
|
}
|
|
51
101
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
// Clear the cache once a new HtmlWebpackPlugin is added
|
|
103
|
+
childCompiler.clearCache(compiler);
|
|
104
|
+
|
|
105
|
+
// Register all HtmlWebpackPlugins instances at the child compiler
|
|
106
|
+
compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
|
|
107
|
+
// Clear the cache if the child compiler is outdated
|
|
108
|
+
if (childCompiler.hasOutDatedTemplateCache(compilation)) {
|
|
109
|
+
childCompiler.clearCache(compiler);
|
|
110
|
+
}
|
|
111
|
+
// Add this instances template to the child compiler
|
|
112
|
+
childCompiler.addTemplateToCompiler(compiler, this.options.template);
|
|
113
|
+
// Add file dependencies of child compiler to parent compiler
|
|
114
|
+
// to keep them watched even if we get the result from the cache
|
|
115
|
+
compilation.hooks.additionalChunkAssets.tap('HtmlWebpackPlugin', () => {
|
|
116
|
+
const childCompilerDependencies = childCompiler.getFileDependencies(compiler);
|
|
117
|
+
childCompilerDependencies.forEach(fileDependency => {
|
|
118
|
+
compilation.compilationDependencies.add(fileDependency);
|
|
119
|
+
});
|
|
63
120
|
});
|
|
64
|
-
}
|
|
121
|
+
});
|
|
65
122
|
|
|
66
|
-
|
|
67
|
-
(compiler.hooks ? compiler.hooks.make.tapAsync.bind(compiler.hooks.make, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'make'))((compilation, callback) => {
|
|
123
|
+
compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
|
|
68
124
|
// Compile the template (queued)
|
|
69
|
-
compilationPromise = childCompiler.compileTemplate(self.options.template,
|
|
125
|
+
compilationPromise = childCompiler.compileTemplate(self.options.template, self.options.filename, compilation)
|
|
70
126
|
.catch(err => {
|
|
71
127
|
compilation.errors.push(prettyError(err, compiler.context).toString());
|
|
72
128
|
return {
|
|
73
129
|
content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
|
|
74
|
-
outputName: self.options.filename
|
|
130
|
+
outputName: self.options.filename,
|
|
131
|
+
hash: ''
|
|
75
132
|
};
|
|
76
133
|
})
|
|
77
134
|
.then(compilationResult => {
|
|
78
135
|
// If the compilation change didnt change the cache is valid
|
|
79
|
-
isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash;
|
|
136
|
+
isCompilationCached = Boolean(compilationResult.hash) && self.childCompilerHash === compilationResult.hash;
|
|
80
137
|
self.childCompilerHash = compilationResult.hash;
|
|
81
138
|
self.childCompilationOutputName = compilationResult.outputName;
|
|
82
139
|
callback();
|
|
@@ -84,155 +141,166 @@ class HtmlWebpackPlugin {
|
|
|
84
141
|
});
|
|
85
142
|
});
|
|
86
143
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
chunks
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
source: false,
|
|
104
|
-
timings: false,
|
|
105
|
-
version: false
|
|
106
|
-
};
|
|
107
|
-
const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
|
|
108
|
-
// Filter chunks (options.chunks and options.excludeCHunks)
|
|
109
|
-
let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
|
|
110
|
-
// Sort chunks
|
|
111
|
-
chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation.chunkGroups);
|
|
112
|
-
// Let plugins alter the chunks and the chunk sorting
|
|
113
|
-
if (compilation.hooks) {
|
|
114
|
-
chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
|
|
115
|
-
} else {
|
|
116
|
-
// Before Webpack 4
|
|
117
|
-
chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
|
|
118
|
-
}
|
|
119
|
-
// Get assets
|
|
120
|
-
const assets = self.htmlWebpackPluginAssets(compilation, chunks);
|
|
121
|
-
// If this is a hot update compilation, move on!
|
|
122
|
-
// This solves a problem where an `index.html` file is generated for hot-update js files
|
|
123
|
-
// It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
|
|
124
|
-
if (self.isHotUpdateCompilation(assets)) {
|
|
125
|
-
return callback();
|
|
126
|
-
}
|
|
144
|
+
compiler.hooks.emit.tapAsync('HtmlWebpackPlugin',
|
|
145
|
+
/**
|
|
146
|
+
* Hook into the webpack emit phase
|
|
147
|
+
* @param {WebpackCompilation} compilation
|
|
148
|
+
* @param {() => void} callback
|
|
149
|
+
*/
|
|
150
|
+
(compilation, callback) => {
|
|
151
|
+
// Get all entry point names for this html file
|
|
152
|
+
const entryNames = Array.from(compilation.entrypoints.keys());
|
|
153
|
+
const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
|
|
154
|
+
const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
|
|
155
|
+
const childCompilationOutputName = self.childCompilationOutputName;
|
|
156
|
+
|
|
157
|
+
if (childCompilationOutputName === undefined) {
|
|
158
|
+
throw new Error('Did not receive child compilation result');
|
|
159
|
+
}
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
// Turn the entry point names into file paths
|
|
162
|
+
const assets = self.htmlWebpackPluginAssets(compilation, childCompilationOutputName, sortedEntryNames);
|
|
163
|
+
|
|
164
|
+
// If this is a hot update compilation, move on!
|
|
165
|
+
// This solves a problem where an `index.html` file is generated for hot-update js files
|
|
166
|
+
// It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
|
|
167
|
+
if (self.isHotUpdateCompilation(assets)) {
|
|
168
|
+
return callback();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If the template and the assets did not change we don't have to emit the html
|
|
172
|
+
const assetJson = JSON.stringify(self.getAssetFiles(assets));
|
|
173
|
+
if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
|
|
174
|
+
return callback();
|
|
175
|
+
} else {
|
|
176
|
+
self.assetJson = assetJson;
|
|
177
|
+
}
|
|
135
178
|
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
// The html-webpack plugin uses a object representation for the html-tags which will be injected
|
|
180
|
+
// to allow altering them more easily
|
|
181
|
+
// Just before they are converted a third-party-plugin author might change the order and content
|
|
182
|
+
const assetsPromise = this.getFaviconPublicPath(this.options.favicon, compilation, assets.publicPath)
|
|
183
|
+
.then((faviconPath) => {
|
|
184
|
+
assets.favicon = faviconPath;
|
|
185
|
+
return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
|
|
186
|
+
assets: assets,
|
|
187
|
+
outputName: childCompilationOutputName,
|
|
188
|
+
plugin: self
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Turn the js and css paths into grouped HtmlTagObjects
|
|
193
|
+
const assetTagGroupsPromise = assetsPromise
|
|
194
|
+
// And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
|
|
195
|
+
.then(({assets}) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
|
|
196
|
+
assetTags: {
|
|
197
|
+
scripts: self.generatedScriptTags(assets.js),
|
|
198
|
+
styles: self.generateStyleTags(assets.css),
|
|
199
|
+
meta: [
|
|
200
|
+
...self.generatedMetaTags(self.options.meta),
|
|
201
|
+
...self.generateFaviconTags(assets.favicon)
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
outputName: childCompilationOutputName,
|
|
205
|
+
plugin: self
|
|
206
|
+
}))
|
|
207
|
+
.then(({assetTags}) => {
|
|
208
|
+
// Inject scripts to body unless it set explictly to head
|
|
209
|
+
const scriptTarget = self.options.inject === 'head' ? 'head' : 'body';
|
|
210
|
+
// Group assets to `head` and `body` tag arrays
|
|
211
|
+
const assetGroups = this.generateAssetGroups(assetTags, scriptTarget);
|
|
212
|
+
// Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
|
|
213
|
+
return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
|
|
214
|
+
headTags: assetGroups.headTags,
|
|
215
|
+
bodyTags: assetGroups.bodyTags,
|
|
216
|
+
outputName: childCompilationOutputName,
|
|
217
|
+
plugin: self
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Turn the compiled tempalte into a nodejs function or into a nodejs string
|
|
222
|
+
const templateEvaluationPromise = compilationPromise
|
|
223
|
+
.then(compiledTemplate => {
|
|
224
|
+
// Allow to use a custom function / string instead
|
|
225
|
+
if (self.options.templateContent !== false) {
|
|
226
|
+
return self.options.templateContent;
|
|
227
|
+
}
|
|
228
|
+
// Once everything is compiled evaluate the html factory
|
|
229
|
+
// and replace it with its content
|
|
230
|
+
return self.evaluateCompilationResult(compilation, compiledTemplate);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
|
|
234
|
+
// Execute the template
|
|
235
|
+
.then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
|
|
236
|
+
? compilationResult
|
|
237
|
+
: self.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
|
|
238
|
+
|
|
239
|
+
const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
|
|
240
|
+
// Allow plugins to change the html before assets are injected
|
|
241
|
+
.then(([assetTags, html]) => {
|
|
242
|
+
const pluginArgs = {html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName};
|
|
243
|
+
return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
|
|
244
|
+
})
|
|
245
|
+
.then(({html, headTags, bodyTags}) => {
|
|
246
|
+
return self.postProcessHtml(html, assets, {headTags, bodyTags});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const emitHtmlPromise = injectedHtmlPromise
|
|
250
|
+
// Allow plugins to change the html after assets are injected
|
|
251
|
+
.then((html) => {
|
|
252
|
+
const pluginArgs = {html, plugin: self, outputName: childCompilationOutputName};
|
|
253
|
+
return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
|
|
254
|
+
.then(result => result.html);
|
|
255
|
+
})
|
|
256
|
+
.catch(err => {
|
|
257
|
+
// In case anything went wrong the promise is resolved
|
|
258
|
+
// with the error message and an error is logged
|
|
259
|
+
compilation.errors.push(prettyError(err, compiler.context).toString());
|
|
260
|
+
// Prevent caching
|
|
261
|
+
self.hash = null;
|
|
262
|
+
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
|
|
263
|
+
})
|
|
264
|
+
.then(html => {
|
|
265
|
+
// Allow to use [contenthash] as placeholder for the html-webpack-plugin name
|
|
266
|
+
// See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
|
|
267
|
+
// From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
|
|
268
|
+
const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
|
|
269
|
+
return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
|
|
147
270
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
plugin: self
|
|
167
|
-
})
|
|
168
|
-
.then(() => compilationResult))
|
|
169
|
-
// Execute the template
|
|
170
|
-
.then(compilationResult => typeof compilationResult !== 'function'
|
|
171
|
-
? compilationResult
|
|
172
|
-
: self.executeTemplate(compilationResult, chunks, assets, compilation))
|
|
173
|
-
// Allow plugins to change the html before assets are injected
|
|
174
|
-
.then(html => {
|
|
175
|
-
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
|
|
176
|
-
return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
|
|
177
|
-
})
|
|
178
|
-
.then(result => {
|
|
179
|
-
const html = result.html;
|
|
180
|
-
const assets = result.assets;
|
|
181
|
-
// Prepare script and link tags
|
|
182
|
-
const assetTags = self.generateAssetTags(assets);
|
|
183
|
-
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
|
|
184
|
-
// Allow plugins to change the assetTag definitions
|
|
185
|
-
return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
|
|
186
|
-
.then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
|
|
187
|
-
.then(html => _.extend(result, {html: html, assets: assets})));
|
|
188
|
-
})
|
|
189
|
-
// Allow plugins to change the html after assets are injected
|
|
190
|
-
.then(result => {
|
|
191
|
-
const html = result.html;
|
|
192
|
-
const assets = result.assets;
|
|
193
|
-
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
|
|
194
|
-
return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, pluginArgs)
|
|
195
|
-
.then(result => result.html);
|
|
196
|
-
})
|
|
197
|
-
.catch(err => {
|
|
198
|
-
// In case anything went wrong the promise is resolved
|
|
199
|
-
// with the error message and an error is logged
|
|
200
|
-
compilation.errors.push(prettyError(err, compiler.context).toString());
|
|
201
|
-
// Prevent caching
|
|
202
|
-
self.hash = null;
|
|
203
|
-
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
|
|
204
|
-
})
|
|
205
|
-
.then(html => {
|
|
206
|
-
// Replace the compilation result with the evaluated html code
|
|
207
|
-
compilation.assets[self.childCompilationOutputName] = {
|
|
208
|
-
source: () => html,
|
|
209
|
-
size: () => html.length
|
|
210
|
-
};
|
|
211
|
-
})
|
|
212
|
-
.then(() => applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
|
|
213
|
-
html: compilation.assets[self.childCompilationOutputName],
|
|
214
|
-
outputName: self.childCompilationOutputName,
|
|
215
|
-
plugin: self
|
|
216
|
-
}).catch(err => {
|
|
217
|
-
console.error(err);
|
|
218
|
-
return null;
|
|
219
|
-
}).then(() => null))
|
|
220
|
-
// Let webpack continue with it
|
|
221
|
-
.then(() => {
|
|
271
|
+
// Add the evaluated html code to the webpack assets
|
|
272
|
+
compilation.assets[finalOutputName] = {
|
|
273
|
+
source: () => html,
|
|
274
|
+
size: () => html.length
|
|
275
|
+
};
|
|
276
|
+
return finalOutputName;
|
|
277
|
+
})
|
|
278
|
+
.then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
|
|
279
|
+
outputName: finalOutputName,
|
|
280
|
+
plugin: self
|
|
281
|
+
}).catch(err => {
|
|
282
|
+
console.error(err);
|
|
283
|
+
return null;
|
|
284
|
+
}).then(() => null));
|
|
285
|
+
|
|
286
|
+
// Once all files are added to the webpack compilation
|
|
287
|
+
// let the webpack compiler continue
|
|
288
|
+
emitHtmlPromise.then(() => {
|
|
222
289
|
callback();
|
|
223
290
|
});
|
|
224
|
-
|
|
291
|
+
});
|
|
225
292
|
}
|
|
226
293
|
|
|
227
294
|
/**
|
|
228
295
|
* Evaluates the child compilation result
|
|
229
|
-
*
|
|
296
|
+
* @param {WebpackCompilation} compilation
|
|
297
|
+
* @param {string} source
|
|
298
|
+
* @returns {Promise<string | (() => string | Promise<string>)>}
|
|
230
299
|
*/
|
|
231
300
|
evaluateCompilationResult (compilation, source) {
|
|
232
301
|
if (!source) {
|
|
233
302
|
return Promise.reject('The child compilation didn\'t provide a result');
|
|
234
303
|
}
|
|
235
|
-
|
|
236
304
|
// The LibraryTemplatePlugin stores the template result in a local variable.
|
|
237
305
|
// To extract the result during the evaluation this part has to be removed.
|
|
238
306
|
source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
|
|
@@ -256,69 +324,101 @@ class HtmlWebpackPlugin {
|
|
|
256
324
|
|
|
257
325
|
/**
|
|
258
326
|
* Generate the template parameters for the template function
|
|
327
|
+
* @param {WebpackCompilation} compilation
|
|
328
|
+
* @param {{
|
|
329
|
+
publicPath: string,
|
|
330
|
+
js: Array<string>,
|
|
331
|
+
css: Array<string>,
|
|
332
|
+
manifest?: string,
|
|
333
|
+
favicon?: string
|
|
334
|
+
}} assets
|
|
335
|
+
* @param {{
|
|
336
|
+
headTags: HtmlTagObject[],
|
|
337
|
+
bodyTags: HtmlTagObject[]
|
|
338
|
+
}} assetTags
|
|
339
|
+
* @returns {{[key: any]: any}}
|
|
259
340
|
*/
|
|
260
|
-
getTemplateParameters (compilation, assets) {
|
|
341
|
+
getTemplateParameters (compilation, assets, assetTags) {
|
|
342
|
+
if (this.options.templateParameters === false) {
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
261
345
|
if (typeof this.options.templateParameters === 'function') {
|
|
262
|
-
return this.options.templateParameters(compilation, assets, this.options);
|
|
346
|
+
return this.options.templateParameters(compilation, assets, assetTags, this.options);
|
|
263
347
|
}
|
|
264
348
|
if (typeof this.options.templateParameters === 'object') {
|
|
265
349
|
return this.options.templateParameters;
|
|
266
350
|
}
|
|
267
|
-
|
|
351
|
+
throw new Error('templateParameters has to be either a function or an object');
|
|
268
352
|
}
|
|
269
353
|
|
|
270
354
|
/**
|
|
271
|
-
*
|
|
355
|
+
* This function renders the actual html by executing the template function
|
|
356
|
+
*
|
|
357
|
+
* @param {(templatePArameters) => string | Promise<string>} templateFunction
|
|
358
|
+
* @param {{
|
|
359
|
+
publicPath: string,
|
|
360
|
+
js: Array<string>,
|
|
361
|
+
css: Array<string>,
|
|
362
|
+
manifest?: string,
|
|
363
|
+
favicon?: string
|
|
364
|
+
}} assets
|
|
365
|
+
* @param {{
|
|
366
|
+
headTags: HtmlTagObject[],
|
|
367
|
+
bodyTags: HtmlTagObject[]
|
|
368
|
+
}} assetTags
|
|
369
|
+
* @param {WebpackCompilation} compilation
|
|
272
370
|
*
|
|
273
|
-
*
|
|
371
|
+
* @returns Promise<string>
|
|
274
372
|
*/
|
|
275
|
-
executeTemplate (templateFunction,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
373
|
+
executeTemplate (templateFunction, assets, assetTags, compilation) {
|
|
374
|
+
// Template processing
|
|
375
|
+
const templateParams = this.getTemplateParameters(compilation, assets, assetTags);
|
|
376
|
+
/** @type {string|Promise<string>} */
|
|
377
|
+
let html = '';
|
|
378
|
+
try {
|
|
379
|
+
html = templateFunction(templateParams);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
compilation.errors.push(new Error('Template execution failed: ' + e));
|
|
382
|
+
return Promise.reject(e);
|
|
383
|
+
}
|
|
384
|
+
// If html is a promise return the promise
|
|
385
|
+
// If html is a string turn it into a promise
|
|
386
|
+
return Promise.resolve().then(() => html);
|
|
289
387
|
}
|
|
290
388
|
|
|
291
389
|
/**
|
|
292
|
-
* Html
|
|
390
|
+
* Html Post processing
|
|
293
391
|
*
|
|
294
|
-
*
|
|
392
|
+
* @param {any} html
|
|
393
|
+
* The input html
|
|
394
|
+
* @param {any} assets
|
|
395
|
+
* @param {{
|
|
396
|
+
headTags: HtmlTagObject[],
|
|
397
|
+
bodyTags: HtmlTagObject[]
|
|
398
|
+
}} assetTags
|
|
399
|
+
* The asset tags to inject
|
|
400
|
+
*
|
|
401
|
+
* @returns {Promise<string>}
|
|
295
402
|
*/
|
|
296
403
|
postProcessHtml (html, assets, assetTags) {
|
|
297
|
-
const self = this;
|
|
298
404
|
if (typeof html !== 'string') {
|
|
299
405
|
return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
|
|
300
406
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
})
|
|
310
|
-
// Minify
|
|
311
|
-
.then(html => {
|
|
312
|
-
if (self.options.minify) {
|
|
313
|
-
const minify = require('html-minifier').minify;
|
|
314
|
-
return minify(html, self.options.minify);
|
|
315
|
-
}
|
|
316
|
-
return html;
|
|
317
|
-
});
|
|
407
|
+
const htmlAfterInjection = this.options.inject
|
|
408
|
+
? this.injectAssetsIntoHtml(html, assets, assetTags)
|
|
409
|
+
: html;
|
|
410
|
+
const htmlAfterMinification = this.options.minify
|
|
411
|
+
? require('html-minifier').minify(htmlAfterInjection, this.options.minify === true ? {} : this.options.minify)
|
|
412
|
+
: htmlAfterInjection;
|
|
413
|
+
return Promise.resolve(htmlAfterMinification);
|
|
318
414
|
}
|
|
319
415
|
|
|
320
416
|
/*
|
|
321
417
|
* Pushes the content of the given filename to the compilation assets
|
|
418
|
+
* @param {string} filename
|
|
419
|
+
* @param {WebpackCompilation} compilation
|
|
420
|
+
*
|
|
421
|
+
* @returns {string} file basename
|
|
322
422
|
*/
|
|
323
423
|
addFileToAssets (filename, compilation) {
|
|
324
424
|
filename = path.resolve(compilation.compiler.context, filename);
|
|
@@ -326,73 +426,50 @@ class HtmlWebpackPlugin {
|
|
|
326
426
|
fsStatAsync(filename),
|
|
327
427
|
fsReadFileAsync(filename)
|
|
328
428
|
])
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (compilation.fileDependencies.add) {
|
|
429
|
+
.then(([size, source]) => {
|
|
430
|
+
return {
|
|
431
|
+
size,
|
|
432
|
+
source
|
|
433
|
+
};
|
|
434
|
+
})
|
|
435
|
+
.catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
|
|
436
|
+
.then(results => {
|
|
437
|
+
const basename = path.basename(filename);
|
|
339
438
|
compilation.fileDependencies.add(filename);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
size: () => results.size.size
|
|
347
|
-
};
|
|
348
|
-
return basename;
|
|
349
|
-
});
|
|
439
|
+
compilation.assets[basename] = {
|
|
440
|
+
source: () => results.source,
|
|
441
|
+
size: () => results.size.size
|
|
442
|
+
};
|
|
443
|
+
return basename;
|
|
444
|
+
});
|
|
350
445
|
}
|
|
351
446
|
|
|
352
447
|
/**
|
|
353
448
|
* Helper to sort chunks
|
|
449
|
+
* @param {string[]} entryNames
|
|
450
|
+
* @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
|
|
451
|
+
* @param {WebpackCompilation} compilation
|
|
354
452
|
*/
|
|
355
|
-
|
|
356
|
-
// Sort mode auto by default:
|
|
357
|
-
if (typeof sortMode === 'undefined') {
|
|
358
|
-
sortMode = 'auto';
|
|
359
|
-
}
|
|
453
|
+
sortEntryChunks (entryNames, sortMode, compilation) {
|
|
360
454
|
// Custom function
|
|
361
455
|
if (typeof sortMode === 'function') {
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
// Disabled sorting:
|
|
365
|
-
if (sortMode === 'none') {
|
|
366
|
-
return chunkSorter.none(chunks);
|
|
367
|
-
}
|
|
368
|
-
if (sortMode === 'manual') {
|
|
369
|
-
return chunkSorter.manual(chunks, this.options.chunks);
|
|
456
|
+
return entryNames.sort(sortMode);
|
|
370
457
|
}
|
|
371
458
|
// Check if the given sort mode is a valid chunkSorter sort mode
|
|
372
459
|
if (typeof chunkSorter[sortMode] !== 'undefined') {
|
|
373
|
-
return chunkSorter[sortMode](
|
|
460
|
+
return chunkSorter[sortMode](entryNames, compilation, this.options);
|
|
374
461
|
}
|
|
375
462
|
throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
|
|
376
463
|
}
|
|
377
464
|
|
|
378
465
|
/**
|
|
379
466
|
* Return all chunks from the compilation result which match the exclude and include filters
|
|
467
|
+
* @param {any} chunks
|
|
468
|
+
* @param {string[]|'all'} includedChunks
|
|
469
|
+
* @param {string[]} excludedChunks
|
|
380
470
|
*/
|
|
381
471
|
filterChunks (chunks, includedChunks, excludedChunks) {
|
|
382
|
-
return chunks.filter(
|
|
383
|
-
const chunkName = chunk.names[0];
|
|
384
|
-
// This chunk doesn't have a name. This script can't handled it.
|
|
385
|
-
if (chunkName === undefined) {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
// Skip if the chunk should be lazy loaded
|
|
389
|
-
if (typeof chunk.isInitial === 'function') {
|
|
390
|
-
if (!chunk.isInitial()) {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
} else if (!chunk.initial) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
472
|
+
return chunks.filter(chunkName => {
|
|
396
473
|
// Skip if the chunks should be filtered and the given chunk was not added explicity
|
|
397
474
|
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
|
|
398
475
|
return false;
|
|
@@ -406,143 +483,316 @@ class HtmlWebpackPlugin {
|
|
|
406
483
|
});
|
|
407
484
|
}
|
|
408
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Check if the given asset object consists only of hot-update.js files
|
|
488
|
+
*
|
|
489
|
+
* @param {{
|
|
490
|
+
publicPath: string,
|
|
491
|
+
js: Array<string>,
|
|
492
|
+
css: Array<string>,
|
|
493
|
+
manifest?: string,
|
|
494
|
+
favicon?: string
|
|
495
|
+
}} assets
|
|
496
|
+
*/
|
|
409
497
|
isHotUpdateCompilation (assets) {
|
|
410
|
-
return assets.js.length && assets.js.every(
|
|
498
|
+
return assets.js.length && assets.js.every((assetPath) => /\.hot-update\.js$/.test(assetPath));
|
|
411
499
|
}
|
|
412
500
|
|
|
413
|
-
|
|
414
|
-
|
|
501
|
+
/**
|
|
502
|
+
* The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
|
|
503
|
+
* for all given entry names
|
|
504
|
+
* @param {WebpackCompilation} compilation
|
|
505
|
+
* @param {string[]} entryNames
|
|
506
|
+
* @returns {{
|
|
507
|
+
publicPath: string,
|
|
508
|
+
js: Array<string>,
|
|
509
|
+
css: Array<string>,
|
|
510
|
+
manifest?: string,
|
|
511
|
+
favicon?: string
|
|
512
|
+
}}
|
|
513
|
+
*/
|
|
514
|
+
htmlWebpackPluginAssets (compilation, childCompilationOutputName, entryNames) {
|
|
415
515
|
const compilationHash = compilation.hash;
|
|
416
516
|
|
|
417
|
-
|
|
517
|
+
/**
|
|
518
|
+
* @type {string} the configured public path to the asset root
|
|
519
|
+
* if a publicPath is set in the current webpack config use it otherwise
|
|
520
|
+
* fallback to a realtive path
|
|
521
|
+
*/
|
|
418
522
|
let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
|
|
419
523
|
// If a hard coded public path exists use it
|
|
420
524
|
? compilation.mainTemplate.getPublicPath({hash: compilationHash})
|
|
421
525
|
// If no public path was set get a relative url path
|
|
422
|
-
: path.relative(path.resolve(compilation.options.output.path, path.dirname(
|
|
526
|
+
: path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
|
|
423
527
|
.split(path.sep).join('/');
|
|
424
528
|
|
|
425
529
|
if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
|
|
426
530
|
publicPath += '/';
|
|
427
531
|
}
|
|
428
532
|
|
|
533
|
+
/**
|
|
534
|
+
* @type {{
|
|
535
|
+
publicPath: string,
|
|
536
|
+
js: Array<string>,
|
|
537
|
+
css: Array<string>,
|
|
538
|
+
manifest?: string,
|
|
539
|
+
favicon?: string
|
|
540
|
+
}}
|
|
541
|
+
*/
|
|
429
542
|
const assets = {
|
|
430
543
|
// The public path
|
|
431
544
|
publicPath: publicPath,
|
|
432
|
-
// Will contain all js & css files by chunk
|
|
433
|
-
chunks: {},
|
|
434
545
|
// Will contain all js files
|
|
435
546
|
js: [],
|
|
436
547
|
// Will contain all css files
|
|
437
548
|
css: [],
|
|
438
549
|
// Will contain the html5 appcache manifest files if it exists
|
|
439
|
-
manifest: Object.keys(compilation.assets).
|
|
550
|
+
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
|
|
551
|
+
// Favicon
|
|
552
|
+
favicon: undefined
|
|
440
553
|
};
|
|
441
554
|
|
|
442
555
|
// Append a hash for cache busting
|
|
443
|
-
if (this.options.hash) {
|
|
444
|
-
assets.manifest =
|
|
445
|
-
assets.favicon = self.appendHash(assets.favicon, compilationHash);
|
|
556
|
+
if (this.options.hash && assets.manifest) {
|
|
557
|
+
assets.manifest = this.appendHash(assets.manifest, compilationHash);
|
|
446
558
|
}
|
|
447
559
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// Prepend the
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile));
|
|
465
|
-
if (js) {
|
|
466
|
-
assets.chunks[chunkName].size = chunk.size;
|
|
467
|
-
assets.chunks[chunkName].entry = js;
|
|
468
|
-
assets.chunks[chunkName].hash = chunk.hash;
|
|
469
|
-
assets.js.push(js);
|
|
470
|
-
}
|
|
560
|
+
// Extract paths to .js and .css files from the current compilation
|
|
561
|
+
const entryPointPublicPathMap = {};
|
|
562
|
+
const extensionRegexp = /\.(css|js)(\?|$)/;
|
|
563
|
+
for (let i = 0; i < entryNames.length; i++) {
|
|
564
|
+
const entryName = entryNames[i];
|
|
565
|
+
const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
|
|
566
|
+
// Prepend the publicPath and append the hash depending on the
|
|
567
|
+
// webpack.output.publicPath and hashOptions
|
|
568
|
+
// E.g. bundle.js -> /bundle.js?hash
|
|
569
|
+
const entryPointPublicPaths = entryPointFiles
|
|
570
|
+
.map(chunkFile => {
|
|
571
|
+
const entryPointPublicPath = publicPath + chunkFile;
|
|
572
|
+
return this.options.hash
|
|
573
|
+
? this.appendHash(entryPointPublicPath, compilationHash)
|
|
574
|
+
: entryPointPublicPath;
|
|
575
|
+
});
|
|
471
576
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
577
|
+
entryPointPublicPaths.forEach((entryPointPublicPath) => {
|
|
578
|
+
const extMatch = extensionRegexp.exec(entryPointPublicPath);
|
|
579
|
+
// Skip if the public path is not a .css or .js file
|
|
580
|
+
if (!extMatch) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// Skip if this file is already known
|
|
584
|
+
// (e.g. because of common chunk optimizations)
|
|
585
|
+
if (entryPointPublicPathMap[entryPointPublicPath]) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
entryPointPublicPathMap[entryPointPublicPath] = true;
|
|
589
|
+
// ext will contain .js or .css
|
|
590
|
+
const ext = extMatch[1];
|
|
591
|
+
assets[ext].push(entryPointPublicPath);
|
|
592
|
+
});
|
|
476
593
|
}
|
|
594
|
+
return assets;
|
|
595
|
+
}
|
|
477
596
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
597
|
+
/**
|
|
598
|
+
* Converts a favicon file from disk to a webpack ressource
|
|
599
|
+
* and returns the url to the ressource
|
|
600
|
+
*
|
|
601
|
+
* @param {string|false} faviconFilePath
|
|
602
|
+
* @param {WebpackCompilation} compilation
|
|
603
|
+
* @parma {string} publicPath
|
|
604
|
+
* @returns {Promise<string|undefined>}
|
|
605
|
+
*/
|
|
606
|
+
getFaviconPublicPath (faviconFilePath, compilation, publicPath) {
|
|
607
|
+
if (!faviconFilePath) {
|
|
608
|
+
return Promise.resolve(undefined);
|
|
609
|
+
}
|
|
610
|
+
return this.addFileToAssets(faviconFilePath, compilation)
|
|
611
|
+
.then((faviconName) => {
|
|
612
|
+
const faviconPath = publicPath + faviconName;
|
|
613
|
+
if (this.options.hash) {
|
|
614
|
+
return this.appendHash(faviconPath, compilation.hash);
|
|
615
|
+
}
|
|
616
|
+
return faviconPath;
|
|
617
|
+
});
|
|
618
|
+
}
|
|
481
619
|
|
|
482
|
-
|
|
620
|
+
/**
|
|
621
|
+
* Generate meta tags
|
|
622
|
+
* @returns {HtmlTagObject[]}
|
|
623
|
+
*/
|
|
624
|
+
getMetaTags () {
|
|
625
|
+
const metaOptions = this.options.meta;
|
|
626
|
+
if (metaOptions === false) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
// Make tags self-closing in case of xhtml
|
|
630
|
+
// Turn { "viewport" : "width=500, initial-scale=1" } into
|
|
631
|
+
// [{ name:"viewport" content:"width=500, initial-scale=1" }]
|
|
632
|
+
const metaTagAttributeObjects = Object.keys(metaOptions)
|
|
633
|
+
.map((metaName) => {
|
|
634
|
+
const metaTagContent = metaOptions[metaName];
|
|
635
|
+
return (typeof metaTagContent === 'string') ? {
|
|
636
|
+
name: metaName,
|
|
637
|
+
content: metaTagContent
|
|
638
|
+
} : metaTagContent;
|
|
639
|
+
})
|
|
640
|
+
.filter((attribute) => attribute !== false);
|
|
641
|
+
// Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
|
|
642
|
+
// the html-webpack-plugin tag structure
|
|
643
|
+
return metaTagAttributeObjects.map((metaTagAttributes) => {
|
|
644
|
+
if (metaTagAttributes === false) {
|
|
645
|
+
throw new Error('Invalid meta tag');
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
tagName: 'meta',
|
|
649
|
+
voidTag: true,
|
|
650
|
+
attributes: metaTagAttributes
|
|
651
|
+
};
|
|
652
|
+
});
|
|
483
653
|
}
|
|
484
654
|
|
|
485
655
|
/**
|
|
486
|
-
*
|
|
656
|
+
* Generate all tags script for the given file paths
|
|
657
|
+
* @param {Array<string>} jsAssets
|
|
658
|
+
* @returns {Array<HtmlTagObject>}
|
|
487
659
|
*/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const scripts = assets.js.map(scriptPath => ({
|
|
660
|
+
generatedScriptTags (jsAssets) {
|
|
661
|
+
return jsAssets.map(scriptAsset => ({
|
|
491
662
|
tagName: 'script',
|
|
492
|
-
|
|
493
|
-
|
|
663
|
+
voidTag: false,
|
|
494
664
|
attributes: {
|
|
495
|
-
|
|
496
|
-
src: scriptPath
|
|
665
|
+
src: scriptAsset
|
|
497
666
|
}
|
|
498
667
|
}));
|
|
499
|
-
|
|
500
|
-
const selfClosingTag = !!this.options.xhtml;
|
|
501
|
-
// Turn css files into link tags
|
|
502
|
-
const styles = assets.css.map(stylePath => ({
|
|
503
|
-
tagName: 'link',
|
|
504
|
-
selfClosingTag: selfClosingTag,
|
|
668
|
+
}
|
|
505
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Generate all style tags for the given file paths
|
|
672
|
+
* @param {Array<string>} cssAssets
|
|
673
|
+
* @returns {Array<HtmlTagObject>}
|
|
674
|
+
*/
|
|
675
|
+
generateStyleTags (cssAssets) {
|
|
676
|
+
return cssAssets.map(styleAsset => ({
|
|
677
|
+
tagName: 'link',
|
|
678
|
+
voidTag: true,
|
|
506
679
|
attributes: {
|
|
507
|
-
href:
|
|
680
|
+
href: styleAsset,
|
|
508
681
|
rel: 'stylesheet'
|
|
509
682
|
}
|
|
510
683
|
}));
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Generate all meta tags for the given meta configuration
|
|
688
|
+
* @param {false | {
|
|
689
|
+
[name: string]: string|false // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
|
|
690
|
+
| {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
|
|
691
|
+
}} metaOptions
|
|
692
|
+
* @returns {Array<HtmlTagObject>}
|
|
693
|
+
*/
|
|
694
|
+
generatedMetaTags (metaOptions) {
|
|
695
|
+
if (metaOptions === false) {
|
|
696
|
+
return [];
|
|
525
697
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
698
|
+
// Make tags self-closing in case of xhtml
|
|
699
|
+
// Turn { "viewport" : "width=500, initial-scale=1" } into
|
|
700
|
+
// [{ name:"viewport" content:"width=500, initial-scale=1" }]
|
|
701
|
+
const metaTagAttributeObjects = Object.keys(metaOptions)
|
|
702
|
+
.map((metaName) => {
|
|
703
|
+
const metaTagContent = metaOptions[metaName];
|
|
704
|
+
return (typeof metaTagContent === 'string') ? {
|
|
705
|
+
name: metaName,
|
|
706
|
+
content: metaTagContent
|
|
707
|
+
} : metaTagContent;
|
|
708
|
+
})
|
|
709
|
+
.filter((attribute) => attribute !== false);
|
|
710
|
+
// Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
|
|
711
|
+
// the html-webpack-plugin tag structure
|
|
712
|
+
return metaTagAttributeObjects.map((metaTagAttributes) => {
|
|
713
|
+
if (metaTagAttributes === false) {
|
|
714
|
+
throw new Error('Invalid meta tag');
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
tagName: 'meta',
|
|
718
|
+
voidTag: true,
|
|
719
|
+
attributes: metaTagAttributes
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Generate a favicon tag for the given file path
|
|
726
|
+
* @param {string| undefined} faviconPath
|
|
727
|
+
* @returns {Array<HtmlTagObject>}
|
|
728
|
+
*/
|
|
729
|
+
generateFaviconTags (faviconPath) {
|
|
730
|
+
if (!faviconPath) {
|
|
731
|
+
return [];
|
|
732
|
+
}
|
|
733
|
+
return [{
|
|
734
|
+
tagName: 'link',
|
|
735
|
+
voidTag: true,
|
|
736
|
+
attributes: {
|
|
737
|
+
rel: 'shortcut icon',
|
|
738
|
+
href: faviconPath
|
|
739
|
+
}
|
|
740
|
+
}];
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Group assets to head and bottom tags
|
|
745
|
+
*
|
|
746
|
+
* @param {{
|
|
747
|
+
scripts: Array<HtmlTagObject>;
|
|
748
|
+
styles: Array<HtmlTagObject>;
|
|
749
|
+
meta: Array<HtmlTagObject>;
|
|
750
|
+
}} assetTags
|
|
751
|
+
* @param {"body" | "head"} scriptTarget
|
|
752
|
+
* @returns {{
|
|
753
|
+
headTags: Array<HtmlTagObject>;
|
|
754
|
+
bodyTags: Array<HtmlTagObject>;
|
|
755
|
+
}}
|
|
756
|
+
*/
|
|
757
|
+
generateAssetGroups (assetTags, scriptTarget) {
|
|
758
|
+
/** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
|
|
759
|
+
const result = {
|
|
760
|
+
headTags: [
|
|
761
|
+
...assetTags.meta,
|
|
762
|
+
...assetTags.styles
|
|
763
|
+
],
|
|
764
|
+
bodyTags: []
|
|
765
|
+
};
|
|
766
|
+
// Add script tags to head or body depending on
|
|
767
|
+
// the htmlPluginOptions
|
|
768
|
+
if (scriptTarget === 'body') {
|
|
769
|
+
result.bodyTags.push(...assetTags.scripts);
|
|
531
770
|
} else {
|
|
532
|
-
|
|
771
|
+
result.headTags.push(...assetTags.scripts);
|
|
533
772
|
}
|
|
534
|
-
return
|
|
773
|
+
return result;
|
|
535
774
|
}
|
|
536
775
|
|
|
537
776
|
/**
|
|
538
777
|
* Injects the assets into the given html string
|
|
778
|
+
*
|
|
779
|
+
* @param {string} html
|
|
780
|
+
* The input html
|
|
781
|
+
* @param {any} assets
|
|
782
|
+
* @param {{
|
|
783
|
+
headTags: HtmlTagObject[],
|
|
784
|
+
bodyTags: HtmlTagObject[]
|
|
785
|
+
}} assetTags
|
|
786
|
+
* The asset tags to inject
|
|
787
|
+
*
|
|
788
|
+
* @returns {string}
|
|
539
789
|
*/
|
|
540
790
|
injectAssetsIntoHtml (html, assets, assetTags) {
|
|
541
791
|
const htmlRegExp = /(<html[^>]*>)/i;
|
|
542
792
|
const headRegExp = /(<\/head\s*>)/i;
|
|
543
793
|
const bodyRegExp = /(<\/body\s*>)/i;
|
|
544
|
-
const body = assetTags.
|
|
545
|
-
const head = assetTags.
|
|
794
|
+
const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
|
|
795
|
+
const head = assetTags.headTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
|
|
546
796
|
|
|
547
797
|
if (body.length) {
|
|
548
798
|
if (bodyRegExp.test(html)) {
|
|
@@ -582,7 +832,10 @@ class HtmlWebpackPlugin {
|
|
|
582
832
|
}
|
|
583
833
|
|
|
584
834
|
/**
|
|
585
|
-
* Appends a cache busting hash
|
|
835
|
+
* Appends a cache busting hash to the query string of the url
|
|
836
|
+
* E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
|
|
837
|
+
* @param {string} url
|
|
838
|
+
* @param {string} hash
|
|
586
839
|
*/
|
|
587
840
|
appendHash (url, hash) {
|
|
588
841
|
if (!url) {
|
|
@@ -591,28 +844,12 @@ class HtmlWebpackPlugin {
|
|
|
591
844
|
return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
|
|
592
845
|
}
|
|
593
846
|
|
|
594
|
-
/**
|
|
595
|
-
* Turn a tag definition into a html string
|
|
596
|
-
*/
|
|
597
|
-
createHtmlTag (tagDefinition) {
|
|
598
|
-
const attributes = Object.keys(tagDefinition.attributes || {})
|
|
599
|
-
.filter(attributeName => tagDefinition.attributes[attributeName] !== false)
|
|
600
|
-
.map(attributeName => {
|
|
601
|
-
if (tagDefinition.attributes[attributeName] === true) {
|
|
602
|
-
return attributeName;
|
|
603
|
-
}
|
|
604
|
-
return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
|
|
605
|
-
});
|
|
606
|
-
// Backport of 3.x void tag definition
|
|
607
|
-
const voidTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag : !tagDefinition.closeTag;
|
|
608
|
-
const selfClosingTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag && this.options.xhtml : tagDefinition.selfClosingTag;
|
|
609
|
-
return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (selfClosingTag ? '/' : '') + '>' +
|
|
610
|
-
(tagDefinition.innerHTML || '') +
|
|
611
|
-
(voidTag ? '' : '</' + tagDefinition.tagName + '>');
|
|
612
|
-
}
|
|
613
|
-
|
|
614
847
|
/**
|
|
615
848
|
* Helper to return the absolute template path with a fallback loader
|
|
849
|
+
* @param {string} template
|
|
850
|
+
* The path to the tempalate e.g. './index.html'
|
|
851
|
+
* @param {string} context
|
|
852
|
+
* The webpack base resolution path for relative paths e.g. process.cwd()
|
|
616
853
|
*/
|
|
617
854
|
getFullTemplatePath (template, context) {
|
|
618
855
|
// If the template doesn't use a loader use the lodash template loader
|
|
@@ -634,75 +871,59 @@ class HtmlWebpackPlugin {
|
|
|
634
871
|
files.sort();
|
|
635
872
|
return files;
|
|
636
873
|
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Helper to promisify compilation.applyPluginsAsyncWaterfall that returns
|
|
640
|
-
* a function that helps to merge given plugin arguments with processed ones
|
|
641
|
-
*/
|
|
642
|
-
applyPluginsAsyncWaterfall (compilation) {
|
|
643
|
-
if (compilation.hooks) {
|
|
644
|
-
return (eventName, requiresResult, pluginArgs) => {
|
|
645
|
-
const ccEventName = trainCaseToCamelCase(eventName);
|
|
646
|
-
if (!compilation.hooks[ccEventName]) {
|
|
647
|
-
compilation.errors.push(
|
|
648
|
-
new Error('No hook found for ' + eventName)
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
return compilation.hooks[ccEventName].promise(pluginArgs);
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Before Webpack 4
|
|
657
|
-
const promisedApplyPluginsAsyncWaterfall = function (name, init) {
|
|
658
|
-
return new Promise((resolve, reject) => {
|
|
659
|
-
const callback = function (err, result) {
|
|
660
|
-
if (err) {
|
|
661
|
-
return reject(err);
|
|
662
|
-
}
|
|
663
|
-
resolve(result);
|
|
664
|
-
};
|
|
665
|
-
compilation.applyPluginsAsyncWaterfall(name, init, callback);
|
|
666
|
-
});
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
return (eventName, requiresResult, pluginArgs) => promisedApplyPluginsAsyncWaterfall(eventName, pluginArgs)
|
|
670
|
-
.then(result => {
|
|
671
|
-
if (requiresResult && !result) {
|
|
672
|
-
compilation.warnings.push(
|
|
673
|
-
new Error('Using ' + eventName + ' without returning a result is deprecated.')
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
return _.extend(pluginArgs, result);
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Takes a string in train case and transforms it to camel case
|
|
683
|
-
*
|
|
684
|
-
* Example: 'hello-my-world' to 'helloMyWorld'
|
|
685
|
-
*
|
|
686
|
-
* @param {string} word
|
|
687
|
-
*/
|
|
688
|
-
function trainCaseToCamelCase (word) {
|
|
689
|
-
return word.replace(/-([\w])/g, (match, p1) => p1.toUpperCase());
|
|
690
874
|
}
|
|
691
875
|
|
|
692
876
|
/**
|
|
693
877
|
* The default for options.templateParameter
|
|
694
878
|
* Generate the template parameters
|
|
879
|
+
*
|
|
880
|
+
* Generate the template parameters for the template function
|
|
881
|
+
* @param {WebpackCompilation} compilation
|
|
882
|
+
* @param {{
|
|
883
|
+
publicPath: string,
|
|
884
|
+
js: Array<string>,
|
|
885
|
+
css: Array<string>,
|
|
886
|
+
manifest?: string,
|
|
887
|
+
favicon?: string
|
|
888
|
+
}} assets
|
|
889
|
+
* @param {{
|
|
890
|
+
headTags: HtmlTagObject[],
|
|
891
|
+
bodyTags: HtmlTagObject[]
|
|
892
|
+
}} assetTags
|
|
893
|
+
* @param {HtmlWebpackPluginOptions} options
|
|
894
|
+
* @returns {HtmlWebpackPluginTemplateParameter}
|
|
695
895
|
*/
|
|
696
|
-
function templateParametersGenerator (compilation, assets, options) {
|
|
896
|
+
function templateParametersGenerator (compilation, assets, assetTags, options) {
|
|
897
|
+
const xhtml = options.xhtml;
|
|
898
|
+
assetTags.headTags.toString = function () {
|
|
899
|
+
return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
|
|
900
|
+
};
|
|
901
|
+
assetTags.bodyTags.toString = function () {
|
|
902
|
+
return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
|
|
903
|
+
};
|
|
697
904
|
return {
|
|
698
905
|
compilation: compilation,
|
|
699
|
-
webpack: compilation.getStats().toJson(),
|
|
700
906
|
webpackConfig: compilation.options,
|
|
701
907
|
htmlWebpackPlugin: {
|
|
908
|
+
tags: assetTags,
|
|
702
909
|
files: assets,
|
|
703
910
|
options: options
|
|
704
911
|
}
|
|
705
912
|
};
|
|
706
913
|
}
|
|
707
914
|
|
|
915
|
+
// Statics:
|
|
916
|
+
/**
|
|
917
|
+
* The major version number of this plugin
|
|
918
|
+
*/
|
|
919
|
+
HtmlWebpackPlugin.version = 4;
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* A static helper to get the hooks for this plugin
|
|
923
|
+
*
|
|
924
|
+
* Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
|
|
925
|
+
*/
|
|
926
|
+
HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
|
|
927
|
+
HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
|
|
928
|
+
|
|
708
929
|
module.exports = HtmlWebpackPlugin;
|