Skip to content

Commit c0bc0ca

Browse files
committed
support using loader and image in html
1 parent 8a6d3e3 commit c0bc0ca

21 files changed

Lines changed: 524 additions & 37 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,4 @@ If you still don't understand README, you can checkout examples in specWepback w
385385
- v1.1.0 support writing assets in html
386386
- v1.1.2 print chunk name in runtime
387387
- v1.1.3 fix bug for using hot update
388+
- v1.2.0 support using loader and image in html

README_ZH.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,5 @@ plugins: [
385385
- v1.1.0 允许在html中写入资源配置
386386
- v1.1.2 在运行时打印出chunk的名字
387387
- v1.1.3 修复使用热替换时的bug
388+
- v1.2.0 支持对html使用loader以及支持image插入到html中
389+

index.js

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
"use strict";
2+
/**
3+
* @plugin html-res-webpack-plugin
4+
* @author heyli
5+
* @reference: https://github.com/ampedandwired/html-webpack-plugin
6+
*/
27

38
// debug mode
49
const isDebug = false,
510
IS_TO_STR = true;
611

712
const fs = require('fs'),
813
_ = require('lodash'),
14+
vm = require('vm'),
915
path = require('path'),
16+
Promise = require('bluebird'),
1017
minify = require('html-minifier').minify,
18+
childCompiler = require('./libs/compiler'),
1119
utils = require('./libs/utils'),
1220
errors = require('./libs/errors'),
1321
loaderUtils = require('loader-utils');
@@ -23,6 +31,7 @@ function HtmlResWebpackPlugin(options) {
2331
favicon: options.favicon || false,
2432
templateContent: options.templateContent || function(tpl) { return tpl },
2533
cssPublicPath: options.cssPublicPath || null,
34+
entryLog: options.entryLog || false,
2635
}, options);
2736

2837
this.logChunkName = true;
@@ -65,41 +74,63 @@ HtmlResWebpackPlugin.prototype.checkRequiredOptions = function(options) {
6574
*/
6675
HtmlResWebpackPlugin.prototype.apply = function(compiler, callback) {
6776

68-
compiler.plugin("make", function(compilation, callback) {
77+
// format: loader!fiename
78+
this.options.templateLoaderName = this.getFullTemplatePath(this.options.template);
79+
80+
var compilationPromise = null;
81+
82+
compiler.plugin("make", (compilation, callback) => {
6983
isDebug && console.log("==================make================");
84+
85+
compilationPromise = childCompiler.compileTemplate(this.options.templateLoaderName, compiler.context, this.options.filename, compilation);
86+
7087
callback();
7188
});
7289

7390

7491
// right after emit, files will be generated
7592
compiler.plugin("emit", (compilation, callback) => {
7693
isDebug && console.log("===================emit===============");
77-
// return basename, ie, /xxx/xxx.html return xxx.html
78-
this.options.htmlFileName = this.addFileToWebpackAsset(compilation, this.options.template, utils.getBaseName(this.options.template, this.options.filename), IS_TO_STR);
79-
80-
// inject favicon
81-
if (this.options.favicon) {
82-
this.options.faviconFileName = this.addFileToWebpackAsset(compilation, this.options.template, utils.getBaseName(this.options.favicon, null));
83-
}
8494

85-
// webpack options
86-
this.webpackOptions = compilation.options;
87-
88-
if (this.options.mode === 'default') {
89-
this.buildStats(compilation);
90-
// start injecting resource into html
91-
this.injectAssets(compilation);
92-
}
93-
else if (this.options.mode === 'html') {
94-
this.buildStatsHtmlMode(compilation);
95-
// process
96-
this.processAssets(compilation);
97-
}
98-
99-
// compress html content
100-
this.options.htmlMinify && this.compressHtml(compilation);
101-
102-
callback();
95+
Promise.resolve()
96+
.then(() => {
97+
return compilationPromise;
98+
})
99+
.then((compiledTemplate) => {
100+
return this.evaluateCompilationResult(compilation, compiledTemplate.content);
101+
}).
102+
then((compiledResult) => {
103+
104+
// return basename, ie, /xxx/xxx.html return xxx.html
105+
this.options.htmlFileName = this.addFileToWebpackAsset(compilation, this.options.template, utils.getBaseName(this.options.template, this.options.filename), IS_TO_STR, compiledResult);
106+
107+
// inject favicon
108+
if (this.options.favicon) {
109+
this.options.faviconFileName = this.addFileToWebpackAsset(compilation, this.options.template, utils.getBaseName(this.options.favicon, null));
110+
}
111+
112+
// webpack options
113+
this.webpackOptions = compilation.options;
114+
115+
if (this.options.mode === 'default') {
116+
this.buildStats(compilation);
117+
// start injecting resource into html
118+
this.injectAssets(compilation);
119+
}
120+
else if (this.options.mode === 'html') {
121+
this.buildStatsHtmlMode(compilation);
122+
// process
123+
this.processAssets(compilation);
124+
}
125+
126+
// compress html content
127+
this.options.htmlMinify && this.compressHtml(compilation);
128+
129+
callback();
130+
131+
// console.log(compiledResult);
132+
133+
});
103134
});
104135

105136
};
@@ -126,12 +157,52 @@ HtmlResWebpackPlugin.prototype.buildStatsHtmlMode = function(compilation) {
126157
}
127158

128159
this.logChunkName = false;
129-
console.log("=====html-res-webapck-plugin=====");
130-
Object.keys(this.stats.assets).map((chunk, key) => {
131-
console.log("chunk" + (key + 1) + ": " + chunk);
160+
this.printChunkName(this.stats.assets);
161+
162+
};
163+
164+
HtmlResWebpackPlugin.prototype.printChunkName = function(assets) {
165+
166+
let assetsArray = Object.keys(assets);
167+
168+
if (!assetsArray.length || !this.options.entryLog) {
169+
return;
170+
}
171+
172+
utils.alert('=====html-res-webapck-plugin=====');
173+
utils.alert('assets used like:');
174+
utils.alert('<link rel="stylesheet" href="' + assetsArray[0] + '">');
175+
utils.alert('<script src="' + assetsArray[0] + '"></script>');
176+
177+
assetsArray.map((chunk, key) => {
178+
utils.info("chunk" + (key + 1) + ": " + chunk);
132179
});
133180
};
134181

182+
HtmlResWebpackPlugin.prototype.evaluateCompilationResult = function(compilation, source) {
183+
if (!source) {
184+
return Promise.reject('The child compilation didn\'t provide a result');
185+
}
186+
187+
// The LibraryTemplatePlugin stores the template result in a local variable.
188+
// To extract the result during the evaluation this part has to be removed.
189+
source = source.replace('var HTML_RES_WEBPACK_PLUGIN_RESULT =', '');
190+
var template = this.options.templateLoaderName.replace(/^.+!/, '').replace(/\?.+$/, '');
191+
var vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
192+
var vmScript = new vm.Script(source, {filename: template});
193+
// console.log(vmScript);
194+
// Evaluate code and cast to string
195+
var newSource;
196+
try {
197+
newSource = vmScript.runInContext(vmContext);
198+
} catch (e) {
199+
return Promise.reject(e);
200+
}
201+
return typeof newSource === 'string' || typeof newSource === 'function'
202+
? Promise.resolve(newSource)
203+
: Promise.reject('The loader "' + this.options.templateLoaderName + '" didn\'t return html.');
204+
};
205+
135206
/**
136207
* find resources related the html
137208
* @param {[type]} compilation [description]
@@ -383,13 +454,13 @@ HtmlResWebpackPlugin.prototype.inlineRes = function(compilation, chunk, file, fi
383454
* @param {[type]} template [description]
384455
* @param {Boolean} isToStr [description]
385456
*/
386-
HtmlResWebpackPlugin.prototype.addFileToWebpackAsset = function(compilation, template, basename, isToStr) {
457+
HtmlResWebpackPlugin.prototype.addFileToWebpackAsset = function(compilation, template, basename, isToStr, source) {
387458
var filename = path.resolve(template);
388459

389460
compilation.fileDependencies.push(filename);
390461
compilation.assets[basename] = {
391462
source: () => {
392-
let fileContent = (isToStr) ? fs.readFileSync(filename).toString() : fs.readFileSync(filename);
463+
let fileContent = (isToStr) ? source : fs.readFileSync(filename);
393464
return fileContent;
394465
},
395466
size: () => {
@@ -400,6 +471,23 @@ HtmlResWebpackPlugin.prototype.addFileToWebpackAsset = function(compilation, tem
400471
return basename;
401472
};
402473

474+
/**
475+
* Helper to return the absolute template path with a fallback loader
476+
*/
477+
HtmlResWebpackPlugin.prototype.getFullTemplatePath = function (template, context) {
478+
// If the template doesn't use a loader use the lodash template loader
479+
if (template.indexOf('!') === -1) {
480+
template = require.resolve('./libs/loader.js') + '!' + template;
481+
}
482+
// Resolve template path
483+
484+
return template.replace(
485+
/([!])([^\/\\][^!\?]+|[^\/\\!?])($|\?.+$)/,
486+
function (match, prefix, filepath, postfix) {
487+
return prefix + path.resolve(filepath) + postfix;
488+
});
489+
};
490+
403491
/**
404492
* compress html files
405493
* @param {[type]} compilation [description]

libs/compiler.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
var _ = require('lodash');
4+
var path = require('path');
5+
var Promise = require('bluebird');
6+
var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
7+
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
8+
var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
9+
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
10+
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
11+
12+
13+
module.exports.compileTemplate = function compileTemplate(template, context, outputFilename, compilation) {
14+
var outputOptions = {
15+
filename: outputFilename,
16+
publicPath: compilation.outputOptions.publicPath
17+
};
18+
19+
// console.log(outputOptions);
20+
// Store the result of the parent compilation before we start the child compilation
21+
var assetsBeforeCompilation = _.assign({}, compilation.assets[outputOptions.filename]);
22+
// Create an additional child compiler which takes the template
23+
// and turns it into an Node.JS html factory.
24+
// This allows us to use loaders during the compilation
25+
26+
var compilerName = getCompilerName(context, outputFilename);
27+
var childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
28+
childCompiler.context = context;
29+
childCompiler.apply(
30+
new NodeTemplatePlugin(outputOptions),
31+
new NodeTargetPlugin(),
32+
new LibraryTemplatePlugin('HTML_RES_WEBPACK_PLUGIN_RESULT', 'var'),
33+
new SingleEntryPlugin(this.context, template),
34+
new LoaderTargetPlugin('node')
35+
);
36+
37+
// Fix for "Uncaught TypeError: __webpack_require__(...) is not a function"
38+
// Hot module replacement requires that every child compiler has its own
39+
// cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179
40+
childCompiler.plugin('compilation', function (compilation) {
41+
if (compilation.cache) {
42+
if (!compilation.cache[compilerName]) {
43+
compilation.cache[compilerName] = {};
44+
}
45+
compilation.cache = compilation.cache[compilerName];
46+
}
47+
});
48+
49+
50+
// Compile and return a promise
51+
return new Promise(function (resolve, reject) {
52+
childCompiler.runAsChild(function (err, entries, childCompilation) {
53+
// Resolve / reject the promise
54+
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
55+
var errorDetails = childCompilation.errors.map(function (error) {
56+
return error.message + (error.error ? ':\n' + error.error : '');
57+
}).join('\n');
58+
reject(new Error('Child compilation failed:\n' + errorDetails));
59+
}
60+
else if (err) {
61+
reject(err);
62+
}
63+
else {
64+
// Replace [hash] placeholders in filename
65+
var outputName = compilation.mainTemplate.applyPluginsWaterfall('asset-path', outputOptions.filename, {
66+
hash: childCompilation.hash,
67+
chunk: entries[0]
68+
});
69+
70+
// console.log(childCompilation.assets[outputName].source());
71+
// Restore the parent compilation to the state like it
72+
// was before the child compilation
73+
74+
compilation.assets[outputName] = assetsBeforeCompilation[outputName];
75+
76+
// console.log(childCompilation.assets[outputName].source());
77+
78+
if (assetsBeforeCompilation[outputName] === undefined) {
79+
// If it wasn't there - delete it
80+
delete compilation.assets[outputName];
81+
}
82+
resolve({
83+
// Hash of the template entry point
84+
hash: entries[0].hash,
85+
// Output name
86+
outputName: outputName,
87+
// Compiled code
88+
content: childCompilation.assets[outputName].source()
89+
});
90+
}
91+
});
92+
});
93+
94+
};
95+
96+
/**
97+
* Returns the child compiler name e.g. 'html-webpack-plugin for "index.html"'
98+
*/
99+
function getCompilerName (context, filename) {
100+
var absolutePath = path.resolve(context, filename);
101+
var relativePath = path.relative(context, absolutePath);
102+
return 'html-webpack-plugin for "' + (absolutePath.length < relativePath.length ? absolutePath : relativePath) + '"';
103+
}

libs/loader.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* This loader renders the template with underscore if no other loader was found */
2+
'use strict';
3+
4+
var _ = require('lodash');
5+
var loaderUtils = require('loader-utils');
6+
7+
module.exports = function (source) {
8+
if (this.cacheable) {
9+
this.cacheable();
10+
}
11+
var allLoadersButThisOne = this.loaders.filter(function (loader) {
12+
// Loader API changed from `loader.module` to `loader.normal` in Webpack 2.
13+
return (loader.module || loader.normal) !== module.exports;
14+
});
15+
// This loader shouldn't kick in if there is any other loader
16+
if (allLoadersButThisOne.length > 0) {
17+
return source;
18+
}
19+
// Skip .js files
20+
if (/\.js$/.test(this.request)) {
21+
return source;
22+
}
23+
24+
// The following part renders the tempalte with lodash as aminimalistic loader
25+
//
26+
// Get templating options
27+
var options = loaderUtils.parseQuery(this.query);
28+
// Webpack 2 does not allow with() statements, which lodash templates use to unwrap
29+
// the parameters passed to the compiled template inside the scope. We therefore
30+
// need to unwrap them ourselves here. This is essentially what lodash does internally
31+
// To tell lodash it should not use with we set a variable
32+
var template = _.template(source, _.defaults(options, { variable: 'data' }));
33+
// All templateVariables which should be available
34+
// @see HtmlWebpackPlugin.prototype.executeTemplate
35+
var templateVariables = [
36+
'compilation',
37+
'webpack',
38+
'webpackConfig',
39+
'htmlWebpackPlugin'
40+
];
41+
return 'var _ = require(' + loaderUtils.stringifyRequest(this, require.resolve('lodash')) + ');' +
42+
'module.exports = function (templateParams) {' +
43+
// Declare the template variables in the outer scope of the
44+
// lodash template to unwrap them
45+
templateVariables.map(function (variableName) {
46+
return 'var ' + variableName + ' = templateParams.' + variableName;
47+
}).join(';') + ';' +
48+
// Execute the lodash template
49+
'return (' + template.source + ')();' +
50+
'}';
51+
};

0 commit comments

Comments
 (0)