From b6dec4bf1072509282756e8d83ef6ee447485f3a Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Mon, 21 May 2018 14:42:16 +0200 Subject: [PATCH] feat(hooks): Add a helper for easier hook access --- index.js | 54 +++------------ lib/hooks.js | 135 +++++++++++++++++++++++++++++++++++++ package.json | 1 + index.d.ts => typings.d.ts | 10 ++- 4 files changed, 153 insertions(+), 47 deletions(-) create mode 100644 lib/hooks.js rename index.d.ts => typings.d.ts (91%) diff --git a/index.js b/index.js index f742a476..0f11874b 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ const promisify = require('util.promisify'); // Import types /* eslint-disable */ -/// +/// /* eslint-enable */ /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */ /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */ @@ -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); @@ -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) @@ -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); @@ -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 @@ -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; @@ -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}))); }) @@ -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 => { @@ -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 @@ -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; - }); - }; - } } /** diff --git a/lib/hooks.js b/lib/hooks.js new file mode 100644 index 00000000..20a18f2a --- /dev/null +++ b/lib/hooks.js @@ -0,0 +1,135 @@ +// @ts-check +/* eslint-disable */ +/// +/* 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, + body: Array, + 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 { + (compilation: WebpackCompilation, hookName: T) => HtmlWebpackPluginHooks[T] + } + */ +const getHtmlWebpackPluginHook = (compilation, hookName) => { + const hooks = getHtmlWebpackPluginHooks(compilation); + return /** @type {any} */hooks[hookName]; +}; + +module.exports = { + getHtmlWebpackPluginHooks, + getHtmlWebpackPluginHook +}; diff --git a/package.json b/package.json index e089a534..820d6c4e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/index.d.ts b/typings.d.ts similarity index 91% rename from index.d.ts rename to typings.d.ts index 39bc3626..3d9d31a9 100644 --- a/index.d.ts +++ b/typings.d.ts @@ -2,7 +2,7 @@ /** * The plugin options */ -type HtmlWebpackPluginOptions = { +interface HtmlWebpackPluginOptions { /** * The title to use for the generated HTML document */ @@ -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`. @@ -76,6 +76,12 @@ type HtmlWebpackPluginOptions = { * Enforce self closing tags e.g. */ 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; } /**