From 3244e6319e49e1dd9326189d711ec1314c26fab7 Mon Sep 17 00:00:00 2001 From: biodiscus Date: Fri, 16 Aug 2024 14:30:48 +0200 Subject: [PATCH] chore: code refactoring, invisible improvements, test this version with your projects --- CHANGELOG.md | 14 +- README.md | 10 +- package.json | 6 +- src/Plugin/AssetCompiler.js | 191 +++++++++--------- test/integration.test.js | 1 + .../webpack.config.js | 4 +- 6 files changed, 111 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a6ec0ff..f245677c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +## 4.0.0-beta.3 (2024-08-16) + +- chore: code refactoring, invisible improvements, test this version with your projects + ## 4.0.0-beta.2 (2024-08-15) - fix: ERROR in RealContentHashPlugin in serv/watch mode after adding new import file @@ -12,10 +16,12 @@ ### BREAKING CHANGES -- Drop supporting for Node.js < `v18`.\ - The plugin works on the Node.js >= `v14.21.xx`, but we can't test the plugin with outdated Node.js versions, - because many actual dev dependencies requires current LTS Node.js >= v18.x. - Up-to-date versions of dependencies are very important because they contain `security updates`. +- Minimum supported Node.js version `18+`.\ + The plugin may works on the Node.js >= `16.20.0`, but we can't test the plugin with outdated Node.js versions. + GitHub CI test works only on Node.js >= 18. + Many actual dev dependencies requires Node.js >= 18. + +- Minimum supported Webpack version `5.81+`. - The plugin `option` property is not static anymore: diff --git a/README.md b/README.md index ca8d6550..ae06f6bb 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ All source file paths in dependencies will be resolved and auto-replaced with co - Generates the [integrity](#option-integrity) attribute in the `link` and `script` tags. - Generates the [favicons](#favicons-bundler-plugin) of different sizes for various platforms. - You can create **own plugin** using the [Plugin Hooks](#plugin-hooks-and-callbacks). -- Over 550 [tests](https://github.com/webdiscus/html-bundler-webpack-plugin/tree/master/test). +- Over 600 [tests](https://github.com/webdiscus/html-bundler-webpack-plugin/tree/master/test). See the [full list of features](#features). @@ -146,9 +146,9 @@ See [how the plugin works under the hood](#plugin-hooks-and-callbacks). - **Simplify Webpack config** using one powerful plugin instead of many [different plugins and loaders](#list-of-plugins). -- **Start from HTML**, not from JS. Define an **HTML** template file as an **entry point**. +- **Start from HTML**, not from JS. Define an HTML template file as an **entry point**. -- Specify script and style **source files** directly in an **HTML** template, +- Specify script and style **source files** directly in a template, and you no longer need to define them in Webpack entry or import styles in JavaScript. - Use **any template engine** without additional plugins and loaders. @@ -169,7 +169,7 @@ If you have discovered a bug or have a feature suggestion, feel free to create a ## 🔆 What's New in v4 -- **NEW** added supports the [multiple configurations](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations) +- **NEW** added supports the [multiple configurations](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations). ## 🔆 What's New in v3 @@ -186,7 +186,7 @@ If you have discovered a bug or have a feature suggestion, feel free to create a - **NEW** added importing style files in JavaScript. - **NEW** added support the [integrity](#option-integrity). -- **NEW** you can add/delete/rename a template file in the [entry path](#option-entry-path) without restarting Webpack +- **NEW** you can add/delete/rename a template file in the [entry path](#option-entry-path) without restarting Webpack. For full release notes see the [changelog](https://github.com/webdiscus/html-bundler-webpack-plugin/blob/master/CHANGELOG.md). diff --git a/package.json b/package.json index 65309c34..672051a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-bundler-webpack-plugin", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "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", @@ -89,7 +89,7 @@ } }, "engines": { - "node": ">=14.21.0" + "node": ">=16.20.0" }, "peerDependencies": { "ejs": ">=3.1.9", @@ -103,7 +103,7 @@ "prismjs": ">=1.29.0", "pug": ">=3.0.2", "twig": ">=1.17.1", - "webpack": ">=5.32.0" + "webpack": ">=5.81.0" }, "peerDependenciesMeta": { "favicons": { diff --git a/src/Plugin/AssetCompiler.js b/src/Plugin/AssetCompiler.js index a0322644..263dfe57 100644 --- a/src/Plugin/AssetCompiler.js +++ b/src/Plugin/AssetCompiler.js @@ -9,6 +9,13 @@ const AssetParser = require('webpack/lib/asset/AssetParser'); const AssetGenerator = require('webpack/lib/asset/AssetGenerator'); //const JavascriptParser = require('webpack/lib/javascript/JavascriptParser'); //const JavascriptGenerator = require('webpack/lib/javascript/JavascriptGenerator'); +const { + JAVASCRIPT_MODULE_TYPE_AUTO, + ASSET_MODULE_TYPE, + ASSET_MODULE_TYPE_INLINE, + ASSET_MODULE_TYPE_RESOURCE, + ASSET_MODULE_TYPE_SOURCE, +} = require('webpack/lib//ModuleTypeConstants'); const Config = require('../Common/Config'); const { baseUri, urlPathPrefix, cssLoaderName } = require('../Loader/Utils'); @@ -215,10 +222,13 @@ class AssetCompiler { this.afterProcessEntry = this.afterProcessEntry.bind(this); this.beforeResolve = this.beforeResolve.bind(this); this.afterResolve = this.afterResolve.bind(this); + this.beforeModule = this.beforeModule.bind(this); this.afterCreateModule = this.afterCreateModule.bind(this); this.beforeLoader = this.beforeLoader.bind(this); this.afterBuildModule = this.afterBuildModule.bind(this); this.renderManifest = this.renderManifest.bind(this); + this.processAssetsOptimizeSize = this.processAssetsOptimizeSize.bind(this); + this.processAssetsFinalAsync = this.processAssetsFinalAsync.bind(this); this.filterAlternativeRequests = this.filterAlternativeRequests.bind(this); this.afterEmit = this.afterEmit.bind(this); this.done = this.done.bind(this); @@ -338,7 +348,6 @@ class AssetCompiler { compiler.hooks.watchRun.tap(pluginName, (compiler) => { // TODO: avoid double calling by multi-config //console.log('===> hooks.watchRun.tap', { id: compiler.name }); - this.pluginOption.initWatchMode(); PluginService.setWatchMode(compiler, true); PluginService.watchRun(compiler); @@ -348,6 +357,7 @@ class AssetCompiler { this.assetEntry.init({ fs: this.fs, }); + compiler.hooks.entryOption.tap(pluginName, this.afterProcessEntry); // watch changes for entry-points @@ -361,45 +371,23 @@ class AssetCompiler { // TODO: refactor this.compilation = compilation; this.pluginContext.compilation = compilation; - this.assetEntry.setCompilation(compilation); this.assetTrash.init(compilation); this.cssExtractModule.init(compilation); + this.urlDependency.init({ compilation, fs }); this.collection.init({ hooks: AssetCompiler.getHooks(compilation), }); - this.urlDependency.init({ compilation, fs }); - // resolve modules normalModuleFactory.hooks.beforeResolve.tap(pluginName, this.beforeResolve); normalModuleFactory.hooks.afterResolve.tap(pluginName, this.afterResolve); contextModuleFactory.hooks.alternativeRequests.tap(pluginName, this.filterAlternativeRequests); - // TODO: reserved for next major version supported Webpack 5.81+ - // - add support for lazy load CSS in JavaScript, see js-import-css-lazy-url - // - move the code from afterCreateModule to here - // - replace the 'javascript/auto' with constant from webpack - // normalModuleFactory.hooks.createModuleClass.for('javascript/auto').tap(pluginName, (createData, resolveData) => { - // const { - // _bundlerPluginMeta: { isImportedStyle }, - // } = resolveData; - // const query = createData.resourceResolveData?.query || ''; - // const isUrl = query.includes('url'); - // - // if (isImportedStyle && isUrl && !query.includes(cssLoaderName)) { - // const dataUrlOptions = undefined; - // const filename = this.pluginOption.getCss().filename; - // createData.settings.type = 'asset/resource'; - // createData.type = 'asset/resource'; - // createData.binary = true; - // createData.parser = new AssetParser(false); - // createData.generator = new AssetGenerator(dataUrlOptions, filename); - // } - // }); - // build modules + // createModuleClass requires v5.81+ + normalModuleFactory.hooks.createModuleClass.for(JAVASCRIPT_MODULE_TYPE_AUTO).tap(pluginName, this.beforeModule); normalModuleFactory.hooks.module.tap(pluginName, this.afterCreateModule); compilation.hooks.buildModule.tap(pluginName, this.beforeBuildModule); compilation.hooks.succeedModule.tap(pluginName, this.afterBuildModule); @@ -416,46 +404,17 @@ class AssetCompiler { // the license file and the license banner must be removed before PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE compilation.hooks.processAssets.tap( { name: pluginName, stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE + 1 }, - (assets) => { - if (!this.pluginOption.isExtractComments()) { - this.assetTrash.removeComments(); - } - } + this.processAssetsOptimizeSize ); // after render module's sources // Notes: - // - only here is possible to modify an asset content via async function - // - `Infinity` ensures that the process will be run after all optimizations - // compilation.hooks.processAssets.tapPromise({ name: pluginName, stage: Infinity }, (assets) => - // this.collection.render() - // .then(() => { - // // remove all unused assets from compilation - // this.assetTrash.clearCompilation(); - // }) - // .catch((error) => { - // // this hook doesn't provide testable exceptions, therefore, save an exception to throw it in the done hook - // this.exceptions.add(error); - // }) - // ); - - compilation.hooks.processAssets.tapPromise({ name: pluginName, stage: Infinity }, (assets) => { - if (PluginError.size > 0) { - // when the previous compilation hook has an error, then skip this hook - return Promise.resolve(); - } - - return this.collection - .render() - .then(() => { - // remove all unused assets from compilation - this.assetTrash.clearCompilation(); - }) - .catch((error) => { - // this hook doesn't provide testable exceptions, therefore, save an exception to throw it in the done hook - this.exceptions.add(error); - }); - }); + // - only in the processAssets hook is possible to modify an asset content via async function + // - the stage`Infinity` ensures that the process will be run after all optimizations + compilation.hooks.processAssets.tapPromise( + { name: pluginName, stage: Infinity + 1 }, + this.processAssetsFinalAsync + ); }); compiler.hooks.afterEmit.tapPromise(pluginName, this.afterEmit); @@ -772,28 +731,37 @@ class AssetCompiler { } /** - * Called after a module instance is created. + * Called after the `createModule` hook and before the `module` hook. * - * @param {Module} module The Webpack module. * @param {Object} createData * @param {Object} resolveData */ - afterCreateModule(module, createData, resolveData) { + beforeModule(createData, resolveData) { const { _bundlerPluginMeta: meta } = resolveData; - const { rawRequest, resource } = createData; - const query = module.resourceResolveData?.query || ''; + const query = createData.resourceResolveData?.query || ''; const isUrl = query.includes('url'); - // TODO: undocumented experimental support for url load a source style file (with `?url` query) in JavaScript + // lazy load CSS in JS using `?url` query, see js-import-css-lazy-url if (meta.isImportedStyle && isUrl && !query.includes(cssLoaderName)) { const dataUrlOptions = undefined; const filename = this.pluginOption.getCss().filename; - module.type = 'asset/resource'; - module.binary = true; - module.parser = new AssetParser(false); - module.generator = new AssetGenerator(dataUrlOptions, filename); + createData.parser = new AssetParser(false); + createData.generator = new AssetGenerator(dataUrlOptions, filename); + createData.type = ASSET_MODULE_TYPE_RESOURCE; } + } + + /** + * Called after a module instance is created. + * + * @param {Module} module The Webpack module. + * @param {Object} createData + * @param {Object} resolveData + */ + afterCreateModule(module, createData, resolveData) { + const { _bundlerPluginMeta: meta } = resolveData; + const { rawRequest, resource } = createData; this.assetEntry.connectEntryAndModule(module, resolveData); @@ -814,9 +782,9 @@ class AssetCompiler { if (!issuer || this.assetInline.isDataUrl(rawRequest)) return; if ( - type === 'asset/inline' || - type === 'asset' || - (type === 'asset/source' && this.assetInline.isSvgFile(resource)) + type === ASSET_MODULE_TYPE || + type === ASSET_MODULE_TYPE_INLINE || + (type === ASSET_MODULE_TYPE_SOURCE && this.assetInline.isSvgFile(resource)) ) { this.assetInline.add(resource, issuer, this.pluginOption.isEntry(issuer)); } @@ -827,7 +795,7 @@ class AssetCompiler { // - if used url() in SCSS for source assets // - if used import url() in CSS, like `@import url('./styles.css');` // - if used webpack context - if (meta.isDependencyUrl || loaders.length > 0 || type === 'asset/resource') { + if (meta.isDependencyUrl || loaders.length > 0 || type === ASSET_MODULE_TYPE_RESOURCE) { this.resolver.addSourceFile(resource, rawRequest, issuer); } } @@ -839,27 +807,14 @@ class AssetCompiler { * @param {{}} module The extended Webpack module. */ beforeBuildModule(module) { - //const { isScript, isStyle, isLoaderImport, isDependencyUrl } = module.resourceResolveData._bundlerPluginMeta; - // reserved code for future - // skip the module loaded via importModule - // if (isLoaderImport) return; - // if ( - // module.type === 'asset/resource' && - // (isScript || (isStyle && isDependencyUrl)) - // ) { - // // set correct module type for scripts and styles when used the `html` mode of a loader - // module.type = 'javascript/auto'; - // module.binary = false; - // module.parser = new JavascriptParser('auto'); - // module.generator = new JavascriptGenerator(); - // } + // do nothing, reserved for debugging } /** * Called after the build module but right before the execution of a loader. * * @param {Object} loaderContext The Webpack loader context. - * @param {{}} module The extended Webpack module. + * @param {Object} module The extended Webpack module. */ beforeLoader(loaderContext, module) { const { isTemplate, isLoaderImport } = module.resourceResolveData._bundlerPluginMeta; @@ -905,15 +860,14 @@ class AssetCompiler { // process only entries supported by this plugin if (!entry || (!entry.isTemplate && !entry.isStyle)) return; - this.collection.addEntry(entry); - - const assetModules = new Set(); const chunkModules = chunkGraph.getChunkModulesIterable(chunk); + const assetModules = new Set(); + + this.collection.addEntry(entry); for (const module of chunkModules) { const { buildInfo, resource, resourceResolveData } = module; const { isScript, isImportedStyle, isCSSStyleSheet } = resourceResolveData?._bundlerPluginMeta || {}; - let moduleType = module.type; if ( isScript || @@ -935,13 +889,14 @@ class AssetCompiler { issuer = entry.resource; } + let moduleType = module.type; // decide an asset type by webpack option parser.dataUrlCondition.maxSize - if (moduleType === 'asset') { - moduleType = buildInfo.dataUrl === true ? 'asset/inline' : 'asset/resource'; + if (moduleType === ASSET_MODULE_TYPE) { + moduleType = buildInfo.dataUrl === true ? ASSET_MODULE_TYPE_INLINE : ASSET_MODULE_TYPE_RESOURCE; } switch (moduleType) { - case 'javascript/auto': + case JAVASCRIPT_MODULE_TYPE_AUTO: const assetModule = this.createAssetModule(entry, chunk, module); if (assetModule == null) continue; @@ -949,14 +904,14 @@ class AssetCompiler { assetModules.add(assetModule); break; - case 'asset/resource': + case ASSET_MODULE_TYPE_RESOURCE: // resource required in the template or in the CSS via url() this.assetResource.saveData(module); break; - case 'asset/inline': + case ASSET_MODULE_TYPE_INLINE: this.assetInline.saveData(entry, chunk, module, codeGenerationResults); break; - case 'asset/source': + case ASSET_MODULE_TYPE_SOURCE: // support the source type for SVG only if (this.assetInline.isSvgFile(resource)) { this.assetInline.saveData(entry, chunk, module, codeGenerationResults); @@ -987,6 +942,40 @@ class AssetCompiler { } } + /** + * Called in PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE stage. + * + * @param {CompilationAssets} assets + */ + processAssetsOptimizeSize(assets) { + if (!this.pluginOption.isExtractComments()) { + this.assetTrash.removeComments(); + } + } + + /** + * Called after render module's sources, after all optimizations. + * + * @param {CompilationAssets} assets + */ + processAssetsFinalAsync(assets) { + if (PluginError.size > 0) { + // when the previous compilation hook has an error, then skip this hook + return Promise.resolve(); + } + + return this.collection + .render() + .then(() => { + // remove all unused assets from compilation + this.assetTrash.clearCompilation(); + }) + .catch((error) => { + // this hook doesn't provide testable exceptions, therefore, save an exception to throw it in the done hook + this.exceptions.add(error); + }); + } + /** * @param {AssetEntryOptions} entry The entry point of the chunk. * @param {Chunk} chunk The chunk of an asset. diff --git a/test/integration.test.js b/test/integration.test.js index 08773b62..68e5c3f2 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -383,6 +383,7 @@ describe('import styles in JavaScript', () => { }); describe('import lazy styles in JavaScript', () => { + // test createModuleClass hook test('lazy url in js', () => compareFiles('js-import-css-lazy-url')); test('lazy load CSS in js using fetch', () => compareFiles('js-import-css-lazy-url-fetch')); }); diff --git a/test/manual/watch-integrity-import-js/webpack.config.js b/test/manual/watch-integrity-import-js/webpack.config.js index 5213657e..ae793696 100644 --- a/test/manual/watch-integrity-import-js/webpack.config.js +++ b/test/manual/watch-integrity-import-js/webpack.config.js @@ -2,8 +2,8 @@ const path = require('path'); const HtmlBundlerPlugin = require('html-bundler-webpack-plugin'); module.exports = { - //mode: 'production', // 1. test in production mode - mode: 'development', // 2. test in development mode + mode: 'production', // 1. test in production mode + //mode: 'development', // 2. test in development mode stats: 'minimal', output: {