bebraw
2/8/2017 - 1:48 PM

HtmlPlugin.js

"use strict";

const Mustache = require("mustache");
const path = require("path");

/**
 * This plugin is used to generate an html file from a mustache template.
 * @param  {object} options
 *     - enabled {boolean} whether plugin is enabled
 *     - outputFile {string} the relative path to the html file result
 *     - templateFile {string} the absolute path to the html file result
 *     - templateVars: {object} mustache view
 *     - assetMatchers {object map str -> Regex} an object where each key K points to a regular expression that
 *       is used to match asset files output by webpack during the build. We then set:
 *       templateVars.assets[K] = <matched urls, made relative to the html file> {undefined | string | string[]}
 */
var HtmlPlugin = module.exports = function(options) {
    this.options = Object.assign({
        enabled: true,
        templateVars: {},
        assetMatchers: {}
    }, options);

    if (typeof this.options.outputFile !== "string") {
        throw new Error("HtmlPlugin: options.outputFile of type <string> is required!");
    }

    this.options.outputFile = this.options.outputFile.replace(/\\/g, '/');

    if (typeof this.options.templateFile !== "string") {
        throw new Error("HtmlPlugin: options.templateFile of type <string> is required!");
    }
};


HtmlPlugin.prototype.apply = function(compiler) {
    var options = this.options;
    if (options.enabled === false) {
        return;
    }

    compiler.plugin('emit', function(compilation, callback) {
        compilation.fileDependencies.push(options.templateFile); // note: those get deduped internally

        Promise.resolve().then(function() {
            return new Promise(function(resolve, reject) {
                compiler.inputFileSystem.stat(options.templateFile, function(err, statInfo) {
                    // Check template file exists
                    if(err) {
                        return reject(err);
                    }

                    if (this._old_template_mtime && (statInfo.mtime === this._old_template_mtime)) {
                        // use cached version
                        return resolve(this._outputFileAsset);
                    }

                    compiler.inputFileSystem.readFile(options.templateFile, function(err, templateContent) {
                        if(err) {
                            return reject(err);
                        }

                        // TODO: generate relative paths
                        var assetUrls = Object.keys(compilation.assets);
                        var assets = Object.keys(options.assetMatchers).reduce((assetsMap, assetKey) => {
                            var assetMatcher = options.assetMatchers[assetKey];
                            var matchedUrls = assetUrls.filter(url => assetMatcher.test(url))
                                                       .map(url => path.relative(path.dirname(options.outputFile), url).replace(/\\/g, '/'));
                            assetsMap[assetKey] = matchedUrls.length > 1 ? matchedUrls : matchedUrls[0];
                            return assetsMap;
                        }, {});

                        var outputContent;
                        try {
                            outputContent = Mustache.render(templateContent.toString(), Object.assign({}, options.templateVars, {
                                assets: assets
                            }));
                        } catch(e) {
                            e.message = "Cannot render mustache template: " + e.message;
                            return reject(e);
                        }

                        this._old_template_mtime = statInfo.mtime;
                        return resolve({
                            _content: outputContent,
                            source: function() {
                                return this._content;
                            },
                            size: function() {
                                return this._content.length;
                            }
                        });
                    }.bind(this));

                }.bind(this));
            }.bind(this));
        }.bind(this)).then(function(assetSource) {
            this._outputFileAsset = assetSource;
            compilation.assets[options.outputFile] = assetSource;
            callback();
        }.bind(this)).catch(function(err) {
            callback(err);
        });
    }.bind(this));
};