diff --git a/CHANGELOG.md b/CHANGELOG.md index 278f4fe1..82607e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change log +## 3.4.12 (2024-01-29) + +- fix: serialization issue when used the `cache.type = 'filesystem'` +- fix: missing output js files after second run build when used the `cache.type = 'filesystem'` + ## 3.4.11 (2024-01-22) - fix: error by resolving url() in the CSS file defined in the entry option diff --git a/package.json b/package.json index f705680c..df779b81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-bundler-webpack-plugin", - "version": "3.4.11", + "version": "3.4.12", "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", diff --git a/src/Plugin/AssetCompiler.js b/src/Plugin/AssetCompiler.js index 154633da..248df6eb 100644 --- a/src/Plugin/AssetCompiler.js +++ b/src/Plugin/AssetCompiler.js @@ -275,7 +275,7 @@ class AssetCompiler { AssetEntry.init({ compilation, entryLibrary: this.entryLibrary, collection: Collection }); AssetTrash.init(compilation); CssExtractModule.init(compilation); - Collection.init(compilation, AssetCompiler.getHooks(compilation)); + Collection.init({ compilation, assetEntry: AssetEntry, hooks: AssetCompiler.getHooks(compilation) }); Resolver.init({ fs, diff --git a/src/Plugin/AssetEntry.js b/src/Plugin/AssetEntry.js index 93408164..470e4d85 100644 --- a/src/Plugin/AssetEntry.js +++ b/src/Plugin/AssetEntry.js @@ -31,10 +31,6 @@ const loader = require.resolve('../Loader'); * [name], [base], [path], [ext], [id], [contenthash], [contenthash:nn] * See https://webpack.js.org/configuration/output/#outputfilename * @property {Function} filenameFn The function to generate the output filename dynamically. - * TODO: remove filename or assetFile, currently the assetFile is undeined (unused?) - * @property {string=} assetFile The output asset file with the relative path by webpack output path. - * Note: the method compilation.emitAsset() use this file as the key of an asset object - * and save the file relative by output path, defined in webpack.options.output.path. * @property {string} resource The absolute import file with a query. * @property {string} importFile The original import entry file. * @property {string} sourceFile The absolute import file only w/o a query. @@ -114,7 +110,7 @@ class AssetEntry { for (let name in pluginEntry) { const entry = pluginEntry[name]; - const entryName = `${this.entryNamePrefix}${name}`; + const entryName = this.createEntryName(name); if (entry.import == null) { if (Option.isEntry(entry)) { @@ -162,13 +158,21 @@ class AssetEntry { files.forEach((file) => { const outputFile = path.relative(dir, file); const name = outputFile.slice(0, outputFile.lastIndexOf('.')); - const entryName = `${this.entryNamePrefix}${name}`; + const entryName = this.createEntryName(name); entry[entryName] = { import: [file] }; }); return entry; } + /** + * @param {string} name + * @return {string} + */ + static createEntryName(name) { + return `${this.entryNamePrefix}${name}`; + } + /** * @param {string} name * @return {string} @@ -457,7 +461,6 @@ class AssetEntry { originalName, filenameTemplate, filename: undefined, - assetFile: undefined, resource, importFile, sourceFile, diff --git a/src/Plugin/Collection.js b/src/Plugin/Collection.js index c0203d94..73035ff7 100644 --- a/src/Plugin/Collection.js +++ b/src/Plugin/Collection.js @@ -35,6 +35,9 @@ class Collection { /** @type {Compilation} */ static compilation; + /** @type {AssetEntry} */ + static assetEntry; + static assets = new Map(); /** @type {Map} >} Entries data */ @@ -330,10 +333,12 @@ class Collection { /** * @param {Compilation} compilation + * @param {AssetEntry} assetEntry * @param {HtmlBundlerPlugin.Hooks} hooks */ - static init(compilation, hooks) { + static init({ compilation, assetEntry, hooks }) { this.compilation = compilation; + this.assetEntry = assetEntry; this.hooks = hooks; } @@ -1005,41 +1010,6 @@ class Collection { }); } - // TODO: add hook for addHeadTags: beforeStyles, beforeScripts, endHead - // let res = this.hooks.insertHeadTags.call(content); - // if (res && Array.isArray(res.tags)) { - // let headTags = res.tags; - // let pos = res.pos; - // let sep = Option.isMinify() ? '' : '\n'; - // let headBlock = headTags.join(sep); - // - // let startPos; - // let headEndPos = content.indexOf(''); - // let headEndPosRegExp = /<\/head>/; - // let posRegexp; - // - // if (!isNumber(pos)) { - // switch (pos) { - // case 'beforePreload': - // posRegexp = / hooks.beforeEmit.promise(content, compileEntry) || content); @@ -1118,17 +1088,44 @@ class Collection { this.importStyleSources.clear(); } + /* istanbul ignore next: test it manual using `cache.type` as `filesystem` after 2nd run the same project */ + /** + * Called by first start or after changes. + * + * @param {Function} write The serialize function. + */ static serialize({ write }) { + for (let [, { entry }] of this.data) { + // note: set the function properties as null to able the serialization of the entry object, + // the original functions will be recovered by deserialization from the cached object `AssetEntry` + entry.filenameFn = null; + entry.filenameTemplate = null; + } + write(this.assets); write(this.data); } + /* istanbul ignore next: test it manual using `cache.type` as `filesystem` after 2nd run the same project */ + /** + * @param {Function} read The deserialize function. + */ static deserialize({ read }) { this.assets = read(); this.data = read(); + + for (let [, { entry }] of this.data) { + const cachedEntry = this.assetEntry.entriesById.get(entry.id); + + // recovery original not serializable functions from the object cached in the memory + entry.filenameFn = cachedEntry.filenameFn; + entry.filenameTemplate = cachedEntry.filenameTemplate; + } + this.isDeserialized = true; } + /* istanbul ignore next: test it manual using `cache.type` as `filesystem` after 2nd run the same project */ /** * Add the script files loaded in the template to the compilation after deserialization. * @@ -1140,45 +1137,13 @@ class Collection { */ static addToCompilationDeserializedFiles(issuer) { for (const [resource, item] of this.assets) { - const { isCompiled, type, name, entries } = item; - if (!isCompiled && type === this.type.script && entries.has(issuer)) { - item.isCompiled = true; + const { type, name, entries } = item; + + if (type === this.type.script && entries.has(issuer)) { this.#addToCompilation({ name, resource, issuer }); } } } - - /** - * TODO: Reserved for debug. - */ - // static getResourceIssuers(resource) { - // const item = this.assets.get(resource); - // - // if (!item) return []; - // - // const issuers = Array.from(item.entries.keys()); - // - // // extract from all issuer's request only filename, w/o a query - // issuers.forEach((request, index) => { - // let [file] = request.split('?', 1); - // issuers[index] = file; - // }); - // - // return issuers; - // } - - /** - * TODO: Reserved for debug. - */ - // static getCompilationModule(resource) { - // const { modules } = this.compilation; - // for (const module of modules) { - // if (module.resource === resource) { - // return module; - // } - // } - // return null; - // } } module.exports = Collection; diff --git a/test/cases/cache-filesystem-js/expected/index.html b/test/cases/cache-filesystem-js/expected/index.html new file mode 100644 index 00000000..82766113 --- /dev/null +++ b/test/cases/cache-filesystem-js/expected/index.html @@ -0,0 +1,10 @@ + + + + Test + + +

Hello World!

+ + + \ No newline at end of file diff --git a/test/cases/cache-filesystem-js/expected/main.6ad9992e.js b/test/cases/cache-filesystem-js/expected/main.6ad9992e.js new file mode 100644 index 00000000..1244f5c8 --- /dev/null +++ b/test/cases/cache-filesystem-js/expected/main.6ad9992e.js @@ -0,0 +1 @@ +console.log("Hello World! Test: 123"); \ No newline at end of file diff --git a/test/cases/cache-filesystem-js/src/index.html b/test/cases/cache-filesystem-js/src/index.html new file mode 100644 index 00000000..80ff122e --- /dev/null +++ b/test/cases/cache-filesystem-js/src/index.html @@ -0,0 +1,10 @@ + + + + Test + + +

Hello World!

+ + + \ No newline at end of file diff --git a/test/cases/cache-filesystem-js/src/main.js b/test/cases/cache-filesystem-js/src/main.js new file mode 100644 index 00000000..818d907c --- /dev/null +++ b/test/cases/cache-filesystem-js/src/main.js @@ -0,0 +1 @@ +console.log('Hello World! Test: 123'); diff --git a/test/cases/cache-filesystem-js/webpack.config.js b/test/cases/cache-filesystem-js/webpack.config.js new file mode 100644 index 00000000..1b4d9ba4 --- /dev/null +++ b/test/cases/cache-filesystem-js/webpack.config.js @@ -0,0 +1,45 @@ +const path = require('path'); +const HtmlBundlerPlugin = require('html-bundler-webpack-plugin'); + +module.exports = { + mode: 'production', + //mode: 'development', + stats: 'errors-warnings', + + output: { + path: path.join(__dirname, 'dist/'), + clean: true, + }, + + plugins: [ + new HtmlBundlerPlugin({ + entry: { + index: { + import: './src/index.html', + filename: (pathData) => { + //console.log('\n\n*** entry.index.filename: : ', { pathData }); + return '[name].html'; + }, + }, + }, + + // filename: (pathData) => { + // console.log('\n\n*** plugin.filename: : ', { pathData }); + // return '[name].html'; + // }, + + js: { + filename: (pathData) => { + //console.log('\n\n*** js.filename: : ', { pathData }); + return '[name].[contenthash:8].js'; + }, + }, + }), + ], + + // test filesystem cache + cache: { + type: 'filesystem', + cacheDirectory: path.join(__dirname, '.cache'), + }, +}; diff --git a/test/integration.test.js b/test/integration.test.js index e5a975b9..717c8aec 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,4 +1,4 @@ -import { compareFiles, watchCompareFiles } from './utils/helpers'; +import { compareFiles, compareFilesRuns, watchCompareFiles } from './utils/helpers'; //import { removeDirsSync } from './utils/file'; // Remove all 'dist/' directories from tests, use it only for some local tests. @@ -22,6 +22,16 @@ describe('features tests', () => { test('resolve-js-in-many-pages', () => compareFiles('resolve-js-in-many-pages')); }); +// TODO: test after N runs +// describe('cache tests', () => { +// afterEach(async () => { +// // sleep between tests to give time for GC +// await new Promise((r) => setTimeout(r, 500)); +// }); +// +// test('cache-filesystem-js3', () => compareFilesRuns('cache-filesystem-js', false, 3)); +// }); + describe('resolve files', () => { test('script style asset', () => compareFiles('resolve-script-style-asset')); test('many pages from same tmpl', () => compareFiles('resolve-in-many-pages-from-same-tmpl')); diff --git a/test/manual/cache-filesystem-js/README.md b/test/manual/cache-filesystem-js/README.md new file mode 100644 index 00000000..36d14a77 --- /dev/null +++ b/test/manual/cache-filesystem-js/README.md @@ -0,0 +1,4 @@ +# Test + +Test the `cache.type` as `filesystem` for simple JS app. +See the [issue 73](https://github.com/webdiscus/html-bundler-webpack-plugin/issues/73). diff --git a/test/manual/cache-filesystem-js/package.json b/test/manual/cache-filesystem-js/package.json new file mode 100644 index 00000000..2666f17b --- /dev/null +++ b/test/manual/cache-filesystem-js/package.json @@ -0,0 +1,13 @@ +{ + "description": "IMPORTANT: don't install webpack here because the Webpack instance MUST be one, otherwise appear the error: The 'compilation' argument must be an instance of Compilation.", + "scripts": { + "start": "webpack serve --mode development", + "start:prod": "webpack serve --mode production", + "watch": "webpack watch --mode development", + "build": "webpack --mode=production --progress" + }, + "license": "ISC", + "devDependencies": { + "html-bundler-webpack-plugin": "file:../../.." + } +} diff --git a/test/manual/cache-filesystem-js/src/index.html b/test/manual/cache-filesystem-js/src/index.html new file mode 100644 index 00000000..80ff122e --- /dev/null +++ b/test/manual/cache-filesystem-js/src/index.html @@ -0,0 +1,10 @@ + + + + Test + + +

Hello World!

+ + + \ No newline at end of file diff --git a/test/manual/cache-filesystem-js/src/main.js b/test/manual/cache-filesystem-js/src/main.js new file mode 100644 index 00000000..818d907c --- /dev/null +++ b/test/manual/cache-filesystem-js/src/main.js @@ -0,0 +1 @@ +console.log('Hello World! Test: 123'); diff --git a/test/manual/cache-filesystem-js/webpack.config.js b/test/manual/cache-filesystem-js/webpack.config.js new file mode 100644 index 00000000..6134b58f --- /dev/null +++ b/test/manual/cache-filesystem-js/webpack.config.js @@ -0,0 +1,69 @@ +const path = require('path'); +const HtmlBundlerPlugin = require('html-bundler-webpack-plugin'); + +module.exports = { + //mode: 'production', + mode: 'development', + //stats: 'normal', + stats: 'errors-warnings', + //devtool: false, + + output: { + path: path.join(__dirname, 'dist/'), + clean: true, + }, + + plugins: [ + new HtmlBundlerPlugin({ + entry: { + //index: './src/index.html', + index: { + import: './src/index.html', + // test: filename as function + filename: (pathData) => { + //console.log('\n\n*** entry.index.filename: : ', { pathData }); + return '[name].html'; + }, + }, + }, + + // filename: (pathData) => { + // console.log('\n\n*** plugin.filename: : ', { pathData }); + // return '[name].html'; + // }, + + js: { + //filename: 'js/[name].[contenthash:8].js', + // test: filename as function + filename: (pathData) => { + //console.log('\n\n*** js.filename: : ', { pathData }); + return 'js/[name].[contenthash:8].js'; + }, + //inline: true, + }, + + verbose: true, + }), + ], + + performance: { + hints: false, // disable the size limit warning + }, + + // test filesystem cache + cache: { + type: 'filesystem', + cacheDirectory: path.join(__dirname, '.cache'), + }, + + // enable live reload + devServer: { + static: path.join(__dirname, 'dist'), + watchFiles: { + paths: ['src/**/*.*'], + options: { + usePolling: true, + }, + }, + }, +}; diff --git a/test/manual/cache-filesystem/package.json b/test/manual/cache-filesystem-react-css/package.json similarity index 100% rename from test/manual/cache-filesystem/package.json rename to test/manual/cache-filesystem-react-css/package.json diff --git a/test/manual/cache-filesystem/src/app.js b/test/manual/cache-filesystem-react-css/src/app.js similarity index 100% rename from test/manual/cache-filesystem/src/app.js rename to test/manual/cache-filesystem-react-css/src/app.js diff --git a/test/manual/cache-filesystem/src/apple.png b/test/manual/cache-filesystem-react-css/src/apple.png similarity index 100% rename from test/manual/cache-filesystem/src/apple.png rename to test/manual/cache-filesystem-react-css/src/apple.png diff --git a/test/manual/cache-filesystem/src/home.html b/test/manual/cache-filesystem-react-css/src/home.html similarity index 100% rename from test/manual/cache-filesystem/src/home.html rename to test/manual/cache-filesystem-react-css/src/home.html diff --git a/test/manual/cache-filesystem/src/iphone.svg b/test/manual/cache-filesystem-react-css/src/iphone.svg similarity index 100% rename from test/manual/cache-filesystem/src/iphone.svg rename to test/manual/cache-filesystem-react-css/src/iphone.svg diff --git a/test/manual/cache-filesystem/src/main.js b/test/manual/cache-filesystem-react-css/src/main.js similarity index 100% rename from test/manual/cache-filesystem/src/main.js rename to test/manual/cache-filesystem-react-css/src/main.js diff --git a/test/manual/cache-filesystem/src/styles.css b/test/manual/cache-filesystem-react-css/src/styles.css similarity index 100% rename from test/manual/cache-filesystem/src/styles.css rename to test/manual/cache-filesystem-react-css/src/styles.css diff --git a/test/manual/cache-filesystem/webpack.config.js b/test/manual/cache-filesystem-react-css/webpack.config.js similarity index 100% rename from test/manual/cache-filesystem/webpack.config.js rename to test/manual/cache-filesystem-react-css/webpack.config.js diff --git a/test/utils/helpers.js b/test/utils/helpers.js index 11c0a970..bf17daa2 100644 --- a/test/utils/helpers.js +++ b/test/utils/helpers.js @@ -69,6 +69,50 @@ export const compareFiles = (relTestCasePath, compareContent = true) => { ).resolves.toBe(true); }; +/** + * Compare the file list and content of files after calling N times. + * Used for testing the cache filesystem. + * + * @param {string} relTestCasePath The relative path to the test directory. + * @param {boolean} compareContent Whether the content of files should be compared too. + * @param {number} num Number of calls + * @return {Promise} + */ +export const compareFilesRuns = (relTestCasePath, compareContent = true, num = 1) => { + const absTestPath = path.join(PATHS.testSource, relTestCasePath), + webRootPath = path.join(absTestPath, PATHS.webRoot), + expectedPath = path.join(absTestPath, PATHS.expected); + + const results = []; + const expected = Array(num).fill(true); + + for (let i = 0; i < num; i++) { + const res = compile(PATHS, relTestCasePath, {}) + .then(() => { + const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); + expect(receivedFiles).toEqual(expectedFiles); + + if (compareContent) { + expectedFiles.forEach((file) => { + const { received, expected } = getCompareFileContents( + path.join(webRootPath, file), + path.join(expectedPath, file) + ); + expect(received).toEqual(expected); + }); + } + + return Promise.resolve(true); + }) + .catch((error) => { + return Promise.reject(error); + }); + results.push(res); + } + + return expect(Promise.all(results)).resolves.toEqual(expected); +}; + /** * Compare the file list and content of files it the serve/watch mode. * diff --git a/test/utils/webpack.js b/test/utils/webpack.js index 60346578..a5bd58cd 100644 --- a/test/utils/webpack.js +++ b/test/utils/webpack.js @@ -47,6 +47,12 @@ const prepareWebpackConfig = (PATHS, relTestCasePath, webpackOpts = {}) => { return merge(baseConfig, commonConfig, webpackOpts, testConfig); }; +/** + * @param {{}} PATHS + * @param {string} testCasePath + * @param {{}} webpackOpts + * @return {Promise} + */ export const compile = (PATHS, testCasePath, webpackOpts) => new Promise((resolve, reject) => { let config;