html-bundler-webpack-plugin
Advanced tools
Comparing version 2.5.1 to 2.6.0
# Change log | ||
## 2.6.0 (2023-08-09) | ||
- feat: add the `css.chunkFilename` option for output filename of non-initial chunk files | ||
- feat: add the `hotUpdate` option to enable/disable live reload in serve/watch mode | ||
- fix: missing an output css file when the same style file is imported in js and linked in html | ||
- chore: add the "hello world" example | ||
- chore: add simple-site example with automatically processing many HTML templates | ||
- chore: add the Handlebars example | ||
- chore: add react-app example, ejected from `create-react-app` (alpha version) | ||
## 2.5.1 (2023-08-04) | ||
@@ -4,0 +13,0 @@ - fix: missing output html file after renaming template file in watch mode when using entry as a path |
{ | ||
"name": "html-bundler-webpack-plugin", | ||
"version": "2.5.1", | ||
"description": "HTML bundler plugin for webpack handels a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.", | ||
"version": "2.6.0", | ||
"description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.", | ||
"keywords": [ | ||
"html", | ||
"webpack", | ||
@@ -10,5 +11,2 @@ "plugin", | ||
"bundler", | ||
"extract", | ||
"inline", | ||
"html", | ||
"template", | ||
@@ -23,6 +21,10 @@ "ejs", | ||
"scss", | ||
"sass", | ||
"css", | ||
"js", | ||
"inline", | ||
"style", | ||
"extract", | ||
"script", | ||
"js", | ||
"javascript", | ||
"svg" | ||
@@ -105,4 +107,4 @@ ], | ||
"devDependencies": { | ||
"@babel/core": "^7.22.9", | ||
"@babel/preset-env": "^7.22.9", | ||
"@babel/core": "^7.22.10", | ||
"@babel/preset-env": "^7.22.10", | ||
"@test-fixtures/js": "0.0.2", | ||
@@ -109,0 +111,0 @@ "@test-fixtures/lorem": "0.0.2", |
@@ -11,4 +11,2 @@ /** | ||
/** | ||
* TODO: test experiments.css | ||
* | ||
* @this {import("webpack").LoaderContext<LoaderOptions>} | ||
@@ -18,2 +16,3 @@ * @param {string} content | ||
const loader = function (content) { | ||
/* istanbul ignore next */ | ||
if (this._compiler.options?.experiments?.css && this._module?.type === 'css') { | ||
@@ -48,3 +47,3 @@ return content; | ||
return '/* extracted by HTMLBundler CSSLoader */ export {};'; | ||
return '/* extracted by HTMLBundler CSSLoader */'; | ||
}; | ||
@@ -51,0 +50,0 @@ |
const Resolver = require('../Resolver'); | ||
const Collection = require('../../Plugin/Collection'); | ||
const PluginService = require('../../Plugin/PluginService'); | ||
const { hmrFile, injectBeforeEndHead } = require('../Utils'); | ||
@@ -41,10 +42,2 @@ const { errorToHtml } = require('../Messages/Exeptions'); | ||
*/ | ||
encodeFile(file) { | ||
return `\\u0027 + \\u0027${file}\\u0027 + \\u0027`; | ||
} | ||
/** | ||
* @param {string} file | ||
* @return {string} | ||
*/ | ||
encodeRequire(file) { | ||
@@ -126,4 +119,4 @@ return `\\u0027 + require(\\u0027${file}\\u0027) + \\u0027`; | ||
export(content, data, issuer) { | ||
if (this.hot) { | ||
// note: it can be tested only manually, because Webpack API no provide `loaderContext.hot` for testing | ||
/* istanbul ignore next: Webpack API no provide `loaderContext.hot` for testing */ | ||
if (this.hot && PluginService.useHotUpdate()) { | ||
content = this.injectHmrFile(content); | ||
@@ -145,5 +138,8 @@ Collection.add({ type: 'script', resource: hmrFile, issuer }); | ||
let content = errorToHtml(error); | ||
content = this.injectHmrFile(content); | ||
Collection.add({ type: 'script', resource: hmrFile, issuer }); | ||
if (PluginService.useHotUpdate()) { | ||
content = this.injectHmrFile(content); | ||
Collection.add({ type: 'script', resource: hmrFile, issuer }); | ||
} | ||
return this.exportCode + "'" + this.decodeReservedChars(content) + "';"; | ||
@@ -150,0 +146,0 @@ } |
@@ -125,2 +125,3 @@ const path = require('path'); | ||
this.beforeResolve = this.beforeResolve.bind(this); | ||
this.afterResolve = this.afterResolve.bind(this); | ||
this.afterCreateModule = this.afterCreateModule.bind(this); | ||
@@ -231,9 +232,7 @@ this.beforeLoader = this.beforeLoader.bind(this); | ||
UrlDependency.init({ | ||
fs, | ||
moduleGraph: compilation.moduleGraph, | ||
}); | ||
UrlDependency.init(fs, compilation); | ||
// resolve modules | ||
normalModuleFactory.hooks.beforeResolve.tap(pluginName, this.beforeResolve); | ||
normalModuleFactory.hooks.afterResolve.tap(pluginName, this.afterResolve); | ||
contextModuleFactory.hooks.alternativeRequests.tap(pluginName, this.filterAlternativeRequests); | ||
@@ -294,2 +293,3 @@ | ||
/* istanbul ignore next: this method is called in watch mode after changes */ | ||
/** | ||
@@ -432,5 +432,3 @@ * Invalidate changed file. | ||
beforeResolve(resolveData) { | ||
const { context, request, contextInfo, dependencyType } = resolveData; | ||
// note: the contextInfo.issuer is the filename w/o a query | ||
const { issuer } = contextInfo; | ||
const { request, dependencyType } = resolveData; | ||
const [file] = request.split('?', 1); | ||
@@ -440,2 +438,3 @@ | ||
/* istanbul ignore next */ | ||
// prevent compilation of renamed or deleted entry point in serve/watch mode | ||
@@ -453,13 +452,23 @@ if (Options.isDynamicEntry() && AssetEntry.isDeletedEntryFile(file)) { | ||
// skip data-URL | ||
if (request.startsWith('data:')) return; | ||
// skip the module loaded via importModule | ||
if (dependencyType === 'loaderImport') return; | ||
if (dependencyType === 'url') { | ||
UrlDependency.resolve(resolveData); | ||
return; | ||
} | ||
} | ||
/** | ||
* Called after the request is resolved. | ||
* | ||
* @param {Object} resolveData | ||
* @return {boolean|undefined} Return undefined to processing, false to ignore dependency. | ||
*/ | ||
afterResolve(resolveData) { | ||
const { request, contextInfo, dependencyType, createData } = resolveData; | ||
const { resource, loaders } = createData; | ||
const [file] = resource.split('?', 1); | ||
// note: the contextInfo.issuer is the filename w/o a query | ||
const { issuer } = contextInfo; | ||
// skip: module loaded via importModule, css url, data-URL | ||
if (dependencyType === 'loaderImport' || dependencyType === 'url' || request.startsWith('data:')) return; | ||
if (issuer) { | ||
@@ -480,9 +489,25 @@ const isIssuerStyle = Options.isStyle(issuer); | ||
if (isIssuerStyle && file.endsWith('.js')) { | ||
const rootIssuer = Collection.findRootIssuer(issuer); | ||
resolveData._isScript = true; | ||
const rootIssuer = Collection.findRootIssuer(issuer); | ||
// return true if the root issuer is a JS (not style and not template), otherwise return false | ||
return rootIssuer != null && !Options.isStyle(rootIssuer) && !Options.isEntry(rootIssuer); | ||
} | ||
// try to detect imported style as resolved resource file, because a request can be a node module w/o an extension | ||
// the issuer can be a style if a scss contains like `@import 'main.css'` | ||
if (!Options.isStyle(issuer) && !Options.isEntry(issuer) && Options.isStyle(file)) { | ||
const rootIssuer = Collection.findRootIssuer(issuer); | ||
Collection.importStyleRootIssuers.add(rootIssuer || issuer); | ||
resolveData._isImportedStyle = true; | ||
if (!createData.request.includes(cssLoader.loader)) { | ||
// the request of an imported style must be different from the request for the same style specified in a html, | ||
// otherwise webpack doesn't apply the added loader for the imported style, | ||
// see the test case js-import-css-same-in-many4 | ||
createData.request = `${cssLoader.loader}!${createData.request}`; | ||
loaders.unshift(cssLoader); | ||
} | ||
} | ||
} | ||
@@ -509,2 +534,3 @@ | ||
module._isStyle = Options.isStyle(resource); | ||
module._isImportedStyle = resolveData._isImportedStyle === true; | ||
module._isLoaderImport = dependencyType === 'loaderImport'; | ||
@@ -518,28 +544,5 @@ module._isDependencyUrl = dependencyType === 'url'; | ||
const { issuer } = resolveData.contextInfo; | ||
const [file] = resource.split('?', 1); | ||
if (!issuer || AssetInline.isDataUrl(rawRequest)) return; | ||
// try to detect imported style as resolved resource file, because a request can be a node module w/o an extension | ||
// the issuer can be a style if a scss contains like `@import 'main.css'` | ||
if (issuer && !Options.isStyle(issuer) && !Options.isEntry(issuer) && Options.isStyle(file)) { | ||
const rootIssuer = Collection.findRootIssuer(issuer); | ||
module._isImportedStyle = true; | ||
Collection.importStyleRootIssuers.add(rootIssuer || issuer); | ||
// check entryId to avoid adding duplicate loaders after changes in serve mode | ||
// add the CSS loader for only styles imported in JavaScript | ||
if (!request.includes(cssLoader)) { | ||
module.loaders.unshift(cssLoader); | ||
// set the correct module type to enable the usage of built-in CSS support together with the bundler plugin | ||
// if (this.compilation.compiler.options?.experiments?.css && type === 'css') { | ||
// module.type = 'javascript/auto'; | ||
// } | ||
} | ||
return; | ||
} | ||
if (type === 'asset/inline' || type === 'asset' || (type === 'asset/source' && AssetInline.isSvgFile(resource))) { | ||
@@ -876,2 +879,3 @@ AssetInline.add(resource, issuer, Options.isEntry(issuer)); | ||
resource: issuer, | ||
useChunkFilename: true, | ||
}); | ||
@@ -951,7 +955,9 @@ const assetFile = inline ? this.getInlineStyleAsseFile(filename, entryFilename) : filename; | ||
* @param {string} resource | ||
* @param {boolean} useChunkFilename | ||
* @return {{isCached: boolean, filename: string}} | ||
*/ | ||
getStyleAsseFile({ name, chunkId, hash, resource }) { | ||
getStyleAsseFile({ name, chunkId, hash, resource, useChunkFilename = false }) { | ||
const { compilation } = this; | ||
const cssOptions = Options.getCss(); | ||
const filenameTemplate = useChunkFilename ? cssOptions.chunkFilename : cssOptions.filename; | ||
@@ -969,3 +975,3 @@ /** @type {PathData} The data to generate an asset path by the filename template. */ | ||
const assetPath = compilation.getAssetPath(cssOptions.filename, pathData); | ||
const assetPath = compilation.getAssetPath(filenameTemplate, pathData); | ||
const outputFilename = Options.resolveOutputFilename(assetPath, cssOptions.outputPath); | ||
@@ -972,0 +978,0 @@ const [sourceFile] = resource.split('?', 1); |
@@ -424,2 +424,3 @@ const fs = require('fs'); | ||
/* istanbul ignore next: this method is called in watch mode after changes */ | ||
/** | ||
@@ -477,2 +478,3 @@ * Add the entry file to compilation. | ||
/* istanbul ignore next: this method is called in watch mode after changes */ | ||
/** | ||
@@ -479,0 +481,0 @@ * Remove the entry file from cache. |
@@ -229,6 +229,6 @@ const path = require('path'); | ||
const walk = (module) => { | ||
const deps = module.dependencies; | ||
const { dependencies } = module; | ||
const result = []; | ||
for (const dependency of deps) { | ||
for (const dependency of dependencies) { | ||
if (!dependency.userRequest) continue; | ||
@@ -239,2 +239,8 @@ | ||
if (!depModule) { | ||
// prevent a potential error in as yet unknown use cases to find the location of the bug | ||
/* istanbul ignore next */ | ||
continue; | ||
} | ||
// use the original NormalModule instead of ConcatenatedModule | ||
@@ -241,0 +247,0 @@ if (!depModule.resource && depModule.rootModule) { |
@@ -16,9 +16,9 @@ const path = require('path'); | ||
* Must be an absolute or a relative by the context path. | ||
* @property {CssOptions=} css The options for embedded plugin module to extract CSS. | ||
* @property {JsOptions=} js The options for embedded plugin module to extract CSS. | ||
* @property {function(string, ResourceInfo, Compilation): string|null =} postprocess The post-process for extracted content from entry. | ||
* @property {CssOptions?} css The options for embedded plugin module to extract CSS. | ||
* @property {JsOptions?} js The options for embedded plugin module to extract CSS. | ||
* @property {function(string, ResourceInfo, Compilation): string|null ?} postprocess The post-process for extracted content from entry. | ||
* @property {function(content: string, {sourceFile: string, assetFile: string})} afterProcess Called after processing all plugins. | ||
* @property {boolean} [extractComments = false] Whether comments should be extracted to a separate file. | ||
* If the original filename is foo.js, then the comments will be stored to foo.js.LICENSE.txt. | ||
* This option enable/disable storing of *.LICENSE.txt file. | ||
* This option enables/disable storing of *.LICENSE.txt file. | ||
* For more flexibility use terser-webpack-plugin https://webpack.js.org/plugins/terser-webpack-plugin/#extractcomments. | ||
@@ -29,4 +29,5 @@ * @property {Object|string} entry The entry points. | ||
* @property {{paths: Array<string>, files: Array<RegExp>, ignore: Array<RegExp>}} watchFiles Paths and files to watch file changes. | ||
* @property {Object=} loaderOptions Options defined in plugin but provided for the loader. | ||
* @property {Array<Object>|boolean=} preload Options to generate preload link tags for assets. | ||
* @property {boolean?} [hotUpdate=true] Whether in serve/watch mode should be added hot-update.js file in html. | ||
* @property {Object?} loaderOptions Options defined in plugin but provided for the loader. | ||
* @property {Array<Object>|boolean?} preload Options to generate preload link tags for assets. | ||
* @property {boolean|Object|'auto'|null} [minify = false] Minify generated HTML. | ||
@@ -41,3 +42,3 @@ * @property {boolean|Object|'auto'|null} [minifyOptions = null] Minification options, it is used for auto minify. | ||
* @property {string|function(PathData, AssetInfo): string} [chunkFilename = '[id].js'] The output filename of non-initial chunk files. | ||
* @property {boolean|string} [`inline` = false] Whether the compiled JS should be inlined. | ||
* @property {boolean|string} [inline = false] Whether the compiled JS should be inlined. | ||
*/ | ||
@@ -49,4 +50,5 @@ | ||
* @property {string|null} [outputPath = options.output.path] The output directory for an asset. | ||
* @property {string|function(PathData, AssetInfo): string} [filename = '[name].js'] The file name of output file. | ||
* @property {boolean|string} [`inline` = false] Whether the compiled CSS should be inlined. | ||
* @property {string|function(PathData, AssetInfo): string} [filename = '[name].css'] The file name of output file. | ||
* @property {string|function(PathData, AssetInfo): string} [chunkFilename = filename] The output filename of non-initial chunk files, e.g., styles imported in js. | ||
* @property {boolean|string} [inline = false] Whether the compiled CSS should be inlined. | ||
*/ | ||
@@ -67,3 +69,3 @@ | ||
chunkFilename: undefined, // used output.chunkFilename | ||
outputPath: null, | ||
outputPath: undefined, | ||
inline: false, | ||
@@ -75,3 +77,4 @@ }; | ||
filename: '[name].css', | ||
outputPath: null, | ||
chunkFilename: undefined, | ||
outputPath: undefined, | ||
inline: false, | ||
@@ -97,2 +100,3 @@ }; | ||
if (!options.watchFiles) this.options.watchFiles = {}; | ||
this.options.hotUpdate = this.options.hotUpdate !== false; | ||
} | ||
@@ -130,2 +134,6 @@ | ||
if (!css.chunkFilename) { | ||
css.chunkFilename = css.filename; | ||
} | ||
js.enabled = this.toBool(js.enabled, true, this.js.enabled); | ||
@@ -132,0 +140,0 @@ js.inline = this.toBool(js.inline, false, this.js.inline); |
@@ -20,3 +20,4 @@ /** | ||
static #used = false; | ||
static #watchMode = false; | ||
static #watchMode; | ||
static #hotUpdate; | ||
static #contextCache = new Set(); | ||
@@ -61,2 +62,3 @@ static dataFiles = new Map(); | ||
this.#watchMode = false; | ||
this.#hotUpdate = pluginOptions.hotUpdate; | ||
this.#options = options; | ||
@@ -134,2 +136,5 @@ this.#loaderOptions = loaderOptions; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
static isWatchMode() { | ||
@@ -139,2 +144,9 @@ return this.#watchMode; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
static useHotUpdate() { | ||
return this.#hotUpdate; | ||
} | ||
static isCached(context) { | ||
@@ -141,0 +153,0 @@ if (this.#contextCache.has(context)) return true; |
@@ -99,3 +99,3 @@ const path = require('path'); | ||
/** | ||
* Resolve the full path of asset source file by raw request and issuer. | ||
* Resolve the full path of asset source file. | ||
* | ||
@@ -106,13 +106,14 @@ * @param {string} rawRequest The raw request of resource. | ||
*/ | ||
static getSourceFile(rawRequest, issuer) { | ||
let sourceFile = this.sourceFiles.get(issuer)?.get(rawRequest); | ||
if (sourceFile) return sourceFile; | ||
static resolveResource(rawRequest, issuer) { | ||
let resource = this.sourceFiles.get(issuer)?.get(rawRequest); | ||
if (resource) return resource; | ||
// normalize request, e.g. the relative `path/to/../to/file` path to absolute `path/to/file` | ||
sourceFile = path.resolve(this.context, rawRequest); | ||
const [file] = sourceFile.split('?', 1); | ||
// normalize request, e.g., the relative `path/to/../to/file` path to absolute `path/to/file` | ||
resource = path.resolve(this.context, rawRequest); | ||
const [file] = resource.split('?', 1); | ||
if (rawRequest.startsWith(this.context) || this.fs.existsSync(file)) { | ||
this.addSourceFile(sourceFile, rawRequest, issuer); | ||
return sourceFile; | ||
this.addSourceFile(resource, rawRequest, issuer); | ||
return resource; | ||
} | ||
@@ -230,3 +231,3 @@ | ||
const resource = this.getSourceFile(rawRequest, issuerFile); | ||
const resource = this.resolveResource(rawRequest, issuerFile); | ||
@@ -233,0 +234,0 @@ // resolve resource |
@@ -5,4 +5,4 @@ const path = require('path'); | ||
class UrlDependency { | ||
static fs = null; | ||
static compilation = null; | ||
static fs; | ||
static moduleGraph; | ||
@@ -13,5 +13,5 @@ /** | ||
*/ | ||
static init({ fs, moduleGraph }) { | ||
static init(fs, compilation) { | ||
this.fs = fs; | ||
this.moduleGraph = moduleGraph; | ||
this.moduleGraph = compilation.moduleGraph; | ||
} | ||
@@ -26,5 +26,7 @@ | ||
const fs = this.fs; | ||
const [file, query] = resolveData.request.split('?'); | ||
const rawRequest = resolveData.request; | ||
const [file, query] = rawRequest.split('?'); | ||
const resource = path.resolve(resolveData.context, file); | ||
if (!fs.existsSync(path.resolve(resolveData.context, file))) { | ||
if (!fs.existsSync(resource)) { | ||
const dependency = resolveData.dependencies[0]; | ||
@@ -35,11 +37,6 @@ const parentModule = this.moduleGraph.getParentModule(dependency); | ||
if (sourceFile != null) { | ||
const resolvedRequest = query ? sourceFile + '?' + query : sourceFile; | ||
const rawRequest = resolveData.request; | ||
const issuer = resolveData.contextInfo.issuer; | ||
resolveData.request = resolvedRequest; | ||
dependency.request = resolvedRequest; | ||
dependency.userRequest = resolvedRequest; | ||
Resolver.addSourceFile(resolvedRequest, rawRequest, issuer); | ||
resolveData.request = query ? sourceFile + '?' + query : sourceFile; | ||
Resolver.addSourceFile(resolveData.request, rawRequest, issuer); | ||
} | ||
@@ -46,0 +43,0 @@ } |
@@ -39,2 +39,3 @@ import { Compiler, Compilation, LoaderContext, WebpackPluginInstance } from 'webpack'; | ||
watchFiles?: WatchFiles; | ||
hotUpdate?: boolean; | ||
verbose?: 'auto' | boolean; | ||
@@ -101,2 +102,3 @@ /** | ||
filename?: FilenameTemplate; | ||
chunkFilename?: FilenameTemplate; | ||
outputPath?: string; | ||
@@ -103,0 +105,0 @@ inline?: 'auto' | boolean; |
Sorry, the diff of this file is too big to display
398988
6863
3861