Skip to content

Commit

Permalink
feat(hooks): Add a helper for easier hook access
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed Jun 3, 2018
1 parent 342867e commit b6dec4b
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 47 deletions.
54 changes: 9 additions & 45 deletions index.js
Expand Up @@ -6,7 +6,7 @@ const promisify = require('util.promisify');

// Import types
/* eslint-disable */
/// <reference path="./index.d.ts" />
/// <reference path="./typings.d.ts" />
/* eslint-enable */
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
Expand All @@ -15,14 +15,14 @@ const vm = require('vm');
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const SyncWaterfallHook = require('tapable').SyncWaterfallHook;
const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;

const htmlTagObjectToString = require('./lib/html-tags').htmlTagObjectToString;

const childCompiler = require('./lib/compiler.js');
const prettyError = require('./lib/errors.js');
const chunkSorter = require('./lib/chunksorter.js');
const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
const getHtmlWebpackPluginHook = require('./lib/hooks.js').getHtmlWebpackPluginHook;

const fsStatAsync = promisify(fs.stat);
const fsReadFileAsync = promisify(fs.readFile);
Expand Down Expand Up @@ -86,18 +86,7 @@ class HtmlWebpackPlugin {
}

// setup hooks for third party plugins
compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', compilation => {
// Setup the hooks only once
if (compilation.hooks.htmlWebpackPluginAlterChunks) {
return;
}
compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook(['chunks', 'objectWithPluginRef']);
compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
});
compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', getHtmlWebpackPluginHooks);

compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
// Compile the template (queued)
Expand Down Expand Up @@ -126,7 +115,6 @@ class HtmlWebpackPlugin {
* @param {() => void} callback
*/
(compilation, callback) => {
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
// Get all entry point names for this html file
const entryNames = Array.from(compilation.entrypoints.keys());
const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
Expand Down Expand Up @@ -176,7 +164,7 @@ class HtmlWebpackPlugin {
})
// Allow plugins to make changes to the assets before invoking the template
// This only makes sense to use if `inject` is `false`
.then(compilationResult => applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlGeneration', false, {
.then(compilationResult => getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginBeforeHtmlGeneration').promise({
assets: assets,
outputName: self.childCompilationOutputName,
plugin: self
Expand All @@ -189,7 +177,7 @@ class HtmlWebpackPlugin {
// Allow plugins to change the html before assets are injected
.then(html => {
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
return applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlProcessing', true, pluginArgs);
return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginBeforeHtmlProcessing').promise(pluginArgs);
})
.then(result => {
const html = result.html;
Expand All @@ -198,7 +186,7 @@ class HtmlWebpackPlugin {
const assetTags = self.generateHtmlTagObjects(assets);
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, outputName: self.childCompilationOutputName};
// Allow plugins to change the assetTag definitions
return applyPluginsAsyncWaterfall('htmlWebpackPluginAlterAssetTags', true, pluginArgs)
return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAlterAssetTags').promise(pluginArgs)
.then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
.then(html => _.extend(result, {html: html, assets: assets})));
})
Expand All @@ -207,7 +195,7 @@ class HtmlWebpackPlugin {
const html = result.html;
const assets = result.assets;
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
return applyPluginsAsyncWaterfall('htmlWebpackPluginAfterHtmlProcessing', true, pluginArgs)
return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAfterHtmlProcessing').promise(pluginArgs)
.then(result => result.html);
})
.catch(err => {
Expand All @@ -225,7 +213,7 @@ class HtmlWebpackPlugin {
size: () => html.length
};
})
.then(() => applyPluginsAsyncWaterfall('htmlWebpackPluginAfterEmit', false, {
.then(() => getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAfterEmit').promise({
html: compilation.assets[self.childCompilationOutputName],
outputName: self.childCompilationOutputName,
plugin: self
Expand Down Expand Up @@ -684,30 +672,6 @@ class HtmlWebpackPlugin {
files.sort();
return files;
}

/**
* Helper to promisify compilation.applyPluginsAsyncWaterfall that returns
* a function that helps to merge given plugin arguments with processed ones
*
* @param {WebpackCompilation} compilation
*
*/
applyPluginsAsyncWaterfall (compilation) {
return (eventName, requiresResult, pluginArgs) => {
if (!compilation.hooks[eventName]) {
compilation.errors.push(
new Error('No hook found for ' + eventName)
);
}
return compilation.hooks[eventName].promise(pluginArgs)
.then(result => {
if (requiresResult && !result) {
throw new Error('Using ' + eventName + ' did not return a result.');
}
return result;
});
};
}
}

/**
Expand Down
135 changes: 135 additions & 0 deletions lib/hooks.js
@@ -0,0 +1,135 @@
// @ts-check
/* eslint-disable */
/// <reference path="../typings.d.ts" />
/* eslint-enable */
'use strict';
/**
* This file provides access to all public htmlWebpackPlugin hooks
*
* Usage:
* ```js
* const getHtmlWebpackPluginHooks = require('html-webpack-plugin/lib/hooks').getHtmlWebpackPluginHooks;
*
* compiler.hooks.compilation.tap('YOUR_PLUGIN_NAME', (compilation) => {
* const htmlWebpackPluginHooks = getHtmlWebpackPluginHooks(compilation);
* htmlWebpackPluginHooks.htmlWebpackPluginAfterEmit.tap('YOUR_PLUGIN_NAME', (pluginArgs) => {
* // your code
* });
* });
* ```
*/

/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
/** @typedef {import("../index.js")} HtmlWebpackPlugin */

const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;

// The following typedef holds the API definition for all available hooks
// to allow easier access when using ts-check or typescript inside plugins
/** @typedef {{
htmlWebpackPluginBeforeHtmlGeneration:
AsyncSeriesWaterfallHook<{
assets: {
publicPath: string,
js: Array<{entryName: string, path: string}>,
css: Array<{entryName: string, path: string}>,
manifest: string,
},
outputName: string,
plugin: HtmlWebpackPlugin
}>,
htmlWebpackPluginBeforeHtmlProcessing:
AsyncSeriesWaterfallHook<{
html: string,
assets: {
publicPath: string,
js: Array<{entryName: string, path: string}>,
css: Array<{entryName: string, path: string}>,
manifest: string,
},
outputName: string,
plugin: HtmlWebpackPlugin,
}>,
htmlWebpackPluginAfterHtmlProcessing:
AsyncSeriesWaterfallHook<{
html: string,
assets: {
publicPath: string,
js: Array<{entryName: string, path: string}>,
css: Array<{entryName: string, path: string}>,
manifest: string,
},
outputName: string,
plugin: HtmlWebpackPlugin,
}>,
htmlWebpackPluginAlterAssetTags:
AsyncSeriesWaterfallHook<{
head: Array<HtmlTagObject>,
body: Array<HtmlTagObject>,
outputName: string,
plugin: HtmlWebpackPlugin
}>,
htmlWebpackPluginAfterEmit:
AsyncSeriesWaterfallHook<{
html: string,
outputName: string,
plugin: HtmlWebpackPlugin
}>,
}} HtmlWebpackPluginHooks
*/

/**
* Returns all public hooks of the html webpack plugin for the given compilation
*
* @param {WebpackCompilation} compilation
* @returns {HtmlWebpackPluginHooks}
*/
function getHtmlWebpackPluginHooks (compilation) {
/** @type {HtmlWebpackPluginHooks} */
const hooks = compilation.hooks;
// Setup the hooks only once
if (!hooks.htmlWebpackPluginAfterEmit) {
attachHooksToCompilation(compilation);
}
return {
htmlWebpackPluginBeforeHtmlGeneration: hooks.htmlWebpackPluginBeforeHtmlGeneration,
htmlWebpackPluginBeforeHtmlProcessing: hooks.htmlWebpackPluginBeforeHtmlProcessing,
htmlWebpackPluginAlterAssetTags: hooks.htmlWebpackPluginAlterAssetTags,
htmlWebpackPluginAfterHtmlProcessing: hooks.htmlWebpackPluginAfterHtmlProcessing,
htmlWebpackPluginAfterEmit: hooks.htmlWebpackPluginAfterEmit
};
}

/**
* Add hooks to the webpack compilation object to allow foreign plugins to
* extend the HtmlWebpackPlugin
*
* @param {WebpackCompilation} compilation
*/
function attachHooksToCompilation (compilation) {
/** @type {HtmlWebpackPluginHooks} */
const hooks = compilation.hooks;
hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
}

/**
* Small workaround helper to work around https://github.com/Microsoft/TypeScript/issues/1178
* Returns the hook of the given name
*
* @type {
<T extends keyof HtmlWebpackPluginHooks>(compilation: WebpackCompilation, hookName: T) => HtmlWebpackPluginHooks[T]
}
*/
const getHtmlWebpackPluginHook = (compilation, hookName) => {
const hooks = getHtmlWebpackPluginHooks(compilation);
return /** @type {any} */hooks[hookName];
};

module.exports = {
getHtmlWebpackPluginHooks,
getHtmlWebpackPluginHook
};
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -49,6 +49,7 @@
"webpack-recompilation-simulator": "^1.3.0"
},
"dependencies": {
"@types/tapable": "1.0.2",
"html-minifier": "^3.2.3",
"loader-utils": "^1.1.0",
"lodash": "^4.17.10",
Expand Down
10 changes: 8 additions & 2 deletions index.d.ts → typings.d.ts
Expand Up @@ -2,7 +2,7 @@
/**
* The plugin options
*/
type HtmlWebpackPluginOptions = {
interface HtmlWebpackPluginOptions {
/**
* The title to use for the generated HTML document
*/
Expand All @@ -22,7 +22,7 @@ type HtmlWebpackPluginOptions = {
templateParameters:
false // Pass an empty object to the template function
| ((compilation: any, assets, options: HtmlWebpackPluginOptions) => {})
| Object
| {[option: string]: any}
/**
* The file to write the HTML to.
* Defaults to `index.html`.
Expand Down Expand Up @@ -76,6 +76,12 @@ type HtmlWebpackPluginOptions = {
* Enforce self closing tags e.g. <link />
*/
xhtml: boolean

/**
* In addition to the options actually used by this plugin, you can use this hash to pass arbitrary data through
* to your template.
*/
[option: string]: any;
}

/**
Expand Down

0 comments on commit b6dec4b

Please sign in to comment.