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" + ] +}