diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 00000000..39bc3626
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,105 @@
+
+/**
+ * The plugin options
+ */
+type HtmlWebpackPluginOptions = {
+ /**
+ * The title to use for the generated HTML document
+ */
+ title: string,
+ /**
+ * `webpack` require path to the template.
+ * @see https://github.com/jantimon/html-webpack-plugin/blob/master/docs/template-option.md
+ */
+ template: string,
+ /**
+ *
+ */
+ templateContent: string | (() => string),
+ /**
+ * Allows to overwrite the parameters used in the template
+ */
+ templateParameters:
+ false // Pass an empty object to the template function
+ | ((compilation: any, assets, options: HtmlWebpackPluginOptions) => {})
+ | Object
+ /**
+ * The file to write the HTML to.
+ * Defaults to `index.html`.
+ * Supports subdirectories eg: `assets/admin.html`
+ */
+ filename: string,
+ /**
+ * If `true` then append a unique `webpack` compilation hash to all included scripts and CSS files.
+ * This is useful for cache busting
+ */
+ hash: boolean,
+ /**
+ * Inject all assets into the given `template` or `templateContent`.
+ */
+ inject: false // Don't inject scripts
+ | true // Inject scripts into body
+ | 'body' // Inject scripts into body
+ | 'head' // Inject scripts into head
+ /**
+ * Path to the favicon icon
+ */
+ favicon: false | string,
+ /**
+ * HTML Minification options
+ * @https://github.com/kangax/html-minifier#options-quick-reference
+ */
+ minify: boolean | {},
+ cache: boolean,
+ /**
+ * Render errors into the HTML page
+ */
+ showErrors: boolean,
+ /**
+ * List all entries which should be injected
+ */
+ chunks: 'all' | string[],
+ /**
+ * List all entries which should not be injeccted
+ */
+ excludeChunks: string[],
+ chunksSortMode: 'auto' | 'manual' | (((entryNameA: string, entryNameB: string) => number)),
+ /**
+ * Inject meta tags
+ */
+ meta: false // Disable injection
+ | {
+ [name: string]: string // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
+ | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
+ },
+ /**
+ * Enforce self closing tags e.g.
+ */
+ xhtml: boolean
+}
+
+/**
+ * A tag element according to the htmlWebpackPlugin object notation
+ */
+interface HtmlTagObject {
+ /**
+ * Attributes of the html tag
+ * E.g. `{'disabled': true, 'value': 'demo'}`
+ */
+ attributes: {
+ [attributeName: string]: string|boolean
+ },
+ /**
+ * Wether this html must not contain innerHTML
+ * @see https://www.w3.org/TR/html5/syntax.html#void-elements
+ */
+ voidTag: boolean,
+ /**
+ * The tag name e.g. `'div'`
+ */
+ tagName: string,
+ /**
+ * Inner HTML The
+ */
+ innerHTML?: string
+}
diff --git a/index.js b/index.js
index 4349d30f..8ebcd545 100644
--- a/index.js
+++ b/index.js
@@ -1,8 +1,16 @@
+// @ts-check
'use strict';
// use Polyfill for util.promisify in node versions < v8
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 */
+
const vm = require('vm');
const fs = require('fs');
const _ = require('lodash');
@@ -20,10 +28,17 @@ const fsStatAsync = promisify(fs.stat);
const fsReadFileAsync = promisify(fs.readFile);
class HtmlWebpackPlugin {
+ /**
+ * @param {Partial} options
+ */
constructor (options) {
// Default options
+ /**
+ * @type {HtmlWebpackPluginOptions}
+ */
this.options = _.extend({
template: path.join(__dirname, 'default_index.ejs'),
+ templateContent: undefined,
templateParameters: templateParametersGenerator,
filename: 'index.html',
hash: false,
@@ -40,8 +55,18 @@ class HtmlWebpackPlugin {
title: 'Webpack App',
xhtml: false
}, options);
+ // Instance variables to keep caching information
+ // for multiple builds
+ this.childCompilerHash = undefined;
+ this.childCompilationOutputName = undefined;
+ this.assetJson = undefined;
+ this.hash = undefined;
}
+ /**
+ * apply is called by the webpack main compiler during the start phase
+ * @param {WebpackCompiler} compiler
+ */
apply (compiler) {
const self = this;
let isCompilationCached = false;
@@ -56,8 +81,12 @@ class HtmlWebpackPlugin {
this.options.filename = path.relative(compiler.options.output.path, filename);
}
- // setup hooks for webpack 4
+ // 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']);
@@ -86,7 +115,13 @@ class HtmlWebpackPlugin {
});
});
- compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
+ compiler.hooks.emit.tapAsync('HtmlWebpackPlugin',
+ /**
+ * Hook into the webpack emit phase
+ * @param {WebpackCompilation} compilation
+ * @param {() => void} callback
+ */
+ (compilation, callback) => {
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
// Get chunks info as json
// Note: we're excluding stuff that we don't need to improve toJson serialization speed.
@@ -175,7 +210,7 @@ class HtmlWebpackPlugin {
const html = result.html;
const assets = result.assets;
// Prepare script and link tags
- const assetTags = self.generateHtmlTags(assets);
+ const assetTags = self.generateHtmlTagObjects(assets);
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
// Allow plugins to change the assetTag definitions
return applyPluginsAsyncWaterfall('htmlWebpackPluginAlterAssetTags', true, pluginArgs)
@@ -252,6 +287,8 @@ class HtmlWebpackPlugin {
/**
* Generate the template parameters for the template function
+ * @param {WebpackCompilation} compilation
+ *
*/
getTemplateParameters (compilation, assets) {
if (typeof this.options.templateParameters === 'function') {
@@ -266,22 +303,21 @@ class HtmlWebpackPlugin {
/**
* Html post processing
*
- * Returns a promise
+ * @returns Promise
*/
executeTemplate (templateFunction, chunks, assets, compilation) {
- return Promise.resolve()
- // Template processing
- .then(() => {
- const templateParams = this.getTemplateParameters(compilation, assets);
- let html = '';
- try {
- html = templateFunction(templateParams);
- } catch (e) {
- compilation.errors.push(new Error('Template execution failed: ' + e));
- return Promise.reject(e);
- }
- return html;
- });
+ // Template processing
+ const templateParams = this.getTemplateParameters(compilation, assets);
+ let html = '';
+ try {
+ html = templateFunction(templateParams);
+ } catch (e) {
+ compilation.errors.push(new Error('Template execution failed: ' + e));
+ return Promise.reject(e);
+ }
+ // If html is a promise return the promise
+ // If html is a string turn it into a promise
+ return Promise.resolve().then(() => html);
}
/**
@@ -290,24 +326,23 @@ class HtmlWebpackPlugin {
* Returns a promise
*/
postProcessHtml (html, assets, assetTags) {
- const self = this;
if (typeof html !== 'string') {
return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
}
return Promise.resolve()
// Inject
.then(() => {
- if (self.options.inject) {
- return self.injectAssetsIntoHtml(html, assets, assetTags);
+ if (this.options.inject) {
+ return this.injectAssetsIntoHtml(html, assets, assetTags);
} else {
return html;
}
})
// Minify
.then(html => {
- if (self.options.minify) {
+ if (this.options.minify) {
const minify = require('html-minifier').minify;
- return minify(html, self.options.minify);
+ return minify(html, this.options.minify === true ? {} : this.options.minify);
}
return html;
});
@@ -315,6 +350,8 @@ class HtmlWebpackPlugin {
/*
* Pushes the content of the given filename to the compilation assets
+ * @param {string} filename
+ * @param {WebpackCompilation} compilation
*/
addFileToAssets (filename, compilation) {
filename = path.resolve(compilation.compiler.context, filename);
@@ -357,6 +394,9 @@ class HtmlWebpackPlugin {
/**
* Return all chunks from the compilation result which match the exclude and include filters
+ * @param {any} chunks
+ * @param {string[]|'all'} includedChunks
+ * @param {string[]} excludedChunks
*/
filterChunks (chunks, includedChunks, excludedChunks) {
return chunks.filter(chunk => {
@@ -391,7 +431,6 @@ class HtmlWebpackPlugin {
}
htmlWebpackPluginAssets (compilation, chunks) {
- const self = this;
const compilationHash = compilation.hash;
// Use the configured public path or build a relative path
@@ -399,7 +438,7 @@ class HtmlWebpackPlugin {
// If a hard coded public path exists use it
? compilation.mainTemplate.getPublicPath({hash: compilationHash})
// If no public path was set get a relative url path
- : path.relative(path.resolve(compilation.options.output.path, path.dirname(self.childCompilationOutputName)), compilation.options.output.path)
+ : path.relative(path.resolve(compilation.options.output.path, path.dirname(this.childCompilationOutputName)), compilation.options.output.path)
.split(path.sep).join('/');
if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
@@ -421,8 +460,8 @@ class HtmlWebpackPlugin {
// Append a hash for cache busting
if (this.options.hash) {
- assets.manifest = self.appendHash(assets.manifest, compilationHash);
- assets.favicon = self.appendHash(assets.favicon, compilationHash);
+ assets.manifest = this.appendHash(assets.manifest, compilationHash);
+ assets.favicon = this.appendHash(assets.favicon, compilationHash);
}
for (let i = 0; i < chunks.length; i++) {
@@ -436,7 +475,7 @@ class HtmlWebpackPlugin {
// Append a hash for cache busting
if (this.options.hash) {
- chunkFiles = chunkFiles.map(chunkFile => self.appendHash(chunkFile, compilationHash));
+ chunkFiles = chunkFiles.map(chunkFile => this.appendHash(chunkFile, compilationHash));
}
// Webpack outputs an array for each chunk when using sourcemaps
@@ -464,9 +503,11 @@ class HtmlWebpackPlugin {
/**
* Generate meta tags
+ * @returns {HtmlTagObject[]}
*/
getMetaTags () {
- if (this.options.meta === false) {
+ const metaOptions = this.options.meta;
+ if (metaOptions === false) {
return [];
}
// Make tags self-closing in case of xhtml
@@ -493,7 +534,19 @@ class HtmlWebpackPlugin {
}
/**
- * Injects the assets into the given html string
+ * Turns the given asset information into tag object representations
+ * which is seperated into head and body
+ *
+ * @param {{
+ js: {entryName: string, path: string}[],
+ css: {entryName: string, path: string}[],
+ favicon?: string
+ }} assets
+ *
+ * @returns {{
+ head: HtmlTagObject[],
+ body: HtmlTagObject[]
+ }}
*/
generateHtmlTags (assets) {
// Turn script files into script tags
@@ -546,6 +599,17 @@ class HtmlWebpackPlugin {
/**
* Injects the assets into the given html string
+ *
+ * @param {string} html
+ * @param {any} assets
+ * The input html
+ * @param {{
+ head: HtmlTagObject[],
+ body: HtmlTagObject[]
+ }} assetTags
+ * The asset tags to inject
+ *
+ * @returns {string}
*/
injectAssetsIntoHtml (html, assets, assetTags) {
const htmlRegExp = /(]*>)/i;
@@ -592,7 +656,10 @@ class HtmlWebpackPlugin {
}
/**
- * Appends a cache busting hash
+ * Appends a cache busting hash to the query string of the url
+ * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
+ * @param {string} url
+ * @param {string} hash
*/
appendHash (url, hash) {
if (!url) {
@@ -603,6 +670,10 @@ class HtmlWebpackPlugin {
/**
* Helper to return the absolute template path with a fallback loader
+ * @param {string} template
+ * The path to the tempalate e.g. './index.html'
+ * @param {string} context
+ * The webpack base resolution path for relative paths e.g. process.cwd()
*/
getFullTemplatePath (template, context) {
// If the template doesn't use a loader use the lodash template loader
@@ -628,6 +699,9 @@ class HtmlWebpackPlugin {
/**
* 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) => {
diff --git a/lib/chunksorter.js b/lib/chunksorter.js
index d9b87cd4..8bcbead3 100644
--- a/lib/chunksorter.js
+++ b/lib/chunksorter.js
@@ -1,5 +1,11 @@
+// @ts-check
'use strict';
+// Import webpack types using commonjs
+// As we use only the type we have to prevent warnings about unused varaibles
+/* eslint-disable */
+const WebpackCompilation = require('webpack/lib/Compilation');
+/* eslint-enable */
/**
* Performs identity mapping (no-sort).
* @param {Array} chunks the chunks to sort
diff --git a/lib/errors.js b/lib/errors.js
index 2b946dad..c6cae4c8 100644
--- a/lib/errors.js
+++ b/lib/errors.js
@@ -1,8 +1,9 @@
+// @ts-nocheck
'use strict';
const PrettyError = require('pretty-error');
const prettyError = new PrettyError();
prettyError.withoutColors();
-prettyError.skipPackage(['html-plugin-evaluation']);
+prettyError.skipPackage('html-plugin-evaluation');
prettyError.skipNodeFiles();
prettyError.skip(function (traceLine) {
return traceLine.path === 'html-plugin-evaluation';
diff --git a/lib/loader.js b/lib/loader.js
index 9340e157..3fecc64b 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -1,6 +1,6 @@
/* This loader renders the template with underscore if no other loader was found */
+// @ts-nocheck
'use strict';
-
const _ = require('lodash');
const loaderUtils = require('loader-utils');
diff --git a/package.json b/package.json
index bb8f232e..9ffae619 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
],
"scripts": {
"pretest": "semistandard",
+ "posttest": "tsc --pretty",
"commit": "git-cz",
"build-examples": "node examples/build-examples.js",
"test": "jasmine",
@@ -23,6 +24,7 @@
]
},
"devDependencies": {
+ "@types/node": "10.1.1",
"appcache-webpack-plugin": "^1.3.0",
"commitizen": "2.9.6",
"css-loader": "^0.26.1",
@@ -39,9 +41,10 @@
"semistandard": "8.0.0",
"standard-version": "^4.3.0",
"style-loader": "^0.13.1",
+ "typescript": "2.9.0-dev.20180518",
"underscore-template-loader": "^0.7.3",
"url-loader": "^0.5.7",
- "webpack": "^4.0.0",
+ "webpack": "4.8.3",
"webpack-cli": "2.0.12",
"webpack-recompilation-simulator": "^1.3.0"
},
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..2ac4ae0e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "allowJs": true, /* Allow javascript files to be compiled. */
+ "checkJs": true, /* Report errors in .js files. */
+ "noEmit": true, /* Do not emit outputs. */
+ "lib": ["es2017"],
+
+ /* Strict Type-Checking Options */
+ "strict": false, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ },
+ "types": ["node"],
+ "exclude": [
+ "node_modules",
+ "spec",
+ "examples",
+ "dist"
+ ]
+}