diff --git a/CHANGELOG.md b/CHANGELOG.md index 943f4c0e..26943700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +## 3.6.3 (2024-03-14) + +- fix: resolving asset files on windows + ## 3.6.2 (2024-03-11) - fix: avoid recompiling all entry templates after changes of a non-entry partial file, [pug-plugin issue](https://github.com/webdiscus/pug-plugin/issues/66) diff --git a/package.json b/package.json index 8df208c6..71ce04fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-bundler-webpack-plugin", - "version": "3.6.2", + "version": "3.6.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", diff --git a/src/Loader/Modes/Compile.js b/src/Loader/Modes/Compile.js index 799ebc25..6c535b46 100644 --- a/src/Loader/Modes/Compile.js +++ b/src/Loader/Modes/Compile.js @@ -1,6 +1,7 @@ const PreprocessorMode = require('./PreprocessorMode'); const { errorToHtml } = require('../Messages/Exeptions'); const { decodeReservedChars, escapeSequences } = require('../Utils'); +const { isWin, pathToPosix } = require('../../Common/Helpers'); /** * Compile into JS function and export as a JS module. @@ -28,6 +29,8 @@ class Compile extends PreprocessorMode { requireExpression(file) { const quote = this.enclosingQuotes; + if (isWin) file = pathToPosix(file); + return `${quote} + require('${file}') + ${quote}`; } diff --git a/src/Loader/Preprocessors/Eta/index.js b/src/Loader/Preprocessors/Eta/index.js index 6caff513..ba37ef81 100644 --- a/src/Loader/Preprocessors/Eta/index.js +++ b/src/Loader/Preprocessors/Eta/index.js @@ -82,8 +82,9 @@ const preprocessor = (loaderContext, options) => { * @return {string} The exported template function. */ export(templateFunction, { data }) { - // note: resolved the file is for node, therefore, we need to get the module path plus file for browser - const runtimeFile = path.join(path.dirname(require.resolve('eta')), 'browser.module.mjs'); + // resolved the file is for node, therefore, we need to get the module path plus file for browser, + // fix windows-like path into the posix standard :-/ + const runtimeFile = path.join(path.dirname(require.resolve('eta')), 'browser.module.mjs').replace(/\\/g, '/'); const exportFunctionName = 'templateFn'; const exportCode = 'module.exports='; diff --git a/src/Loader/Preprocessors/Handlebars/index.js b/src/Loader/Preprocessors/Handlebars/index.js index 5d602990..4ff5ec06 100644 --- a/src/Loader/Preprocessors/Handlebars/index.js +++ b/src/Loader/Preprocessors/Handlebars/index.js @@ -212,7 +212,8 @@ const preprocessor = (loaderContext, options) => { * @return {string} The exported template function. */ export(precompiledTemplate, { data }) { - const runtimeFile = require.resolve('handlebars/dist/handlebars.runtime.min'); + // fix windows-like path + const runtimeFile = require.resolve('handlebars/dist/handlebars.runtime.min').replace(/\\/g, '/'); const exportFunctionName = 'templateFn'; const exportCode = 'module.exports='; diff --git a/src/Loader/Preprocessors/Nunjucks/index.js b/src/Loader/Preprocessors/Nunjucks/index.js index 737cdd48..85622002 100644 --- a/src/Loader/Preprocessors/Nunjucks/index.js +++ b/src/Loader/Preprocessors/Nunjucks/index.js @@ -75,11 +75,14 @@ const preprocessor = (loaderContext, options = {}, { esModule, watch }) => { if (requiredTemplates.has(templateFile)) continue; // try to resolve the template file in multiple paths - const file = require.resolve(templateFile, { paths: viewPaths }); + let file = require.resolve(templateFile, { paths: viewPaths }); if (file) { - // unique template name as the template path - const templatePath = path.relative(rootContext, file); + // unique template name as the template path, fix windows-like path + const templatePath = path.relative(rootContext, file).replace(/\\/g, '/'); + + // fix windows-like path + file = file.replace(/\\/g, '/'); dependencies += `dependencies["${templatePath}"] = require("${file}");`; // if used partial paths (defined in `views` option) to include a partial, @@ -113,7 +116,8 @@ const preprocessor = (loaderContext, options = {}, { esModule, watch }) => { * @return {string} The exported template function. */ export(precompiledTemplate, { data }) { - const runtimeFile = require.resolve('nunjucks/browser/nunjucks-slim.min'); + // fix windows-like path + const runtimeFile = require.resolve('nunjucks/browser/nunjucks-slim.min').replace(/\\/g, '/'); return ` var nunjucks = require('${runtimeFile}'); diff --git a/src/Loader/Preprocessors/Pug/ResolvePlugin.js b/src/Loader/Preprocessors/Pug/ResolvePlugin.js index 13ff10db..58c18fe8 100644 --- a/src/Loader/Preprocessors/Pug/ResolvePlugin.js +++ b/src/Loader/Preprocessors/Pug/ResolvePlugin.js @@ -1,6 +1,7 @@ const path = require('path'); const Resolver = require('../../Resolver'); -const { encodeReservedChars, isWin, pathToPosix } = require('../../Utils'); +const { encodeReservedChars } = require('../../Utils'); +const { isWin, pathToPosix } = require('../../../Common/Helpers'); const scriptExtensionRegexp = /\.js[a-z\d]*$/i; const isRequireableScript = (file) => !path.extname(file) || scriptExtensionRegexp.test(file); @@ -146,7 +147,8 @@ const LoaderResolvers = { const requireExpression = (value, issuer, type = 'default') => { const [, requiredFile] = /require\((.+?)(?=\))/.exec(value) || []; const file = requiredFile || value; - //if (isWin) issuer = pathToPosix(issuer); + + if (isWin) issuer = pathToPosix(issuer); if (ResolvePlugin.mode === 'render') { const requireType = requireTypes[type]; @@ -155,7 +157,9 @@ const requireExpression = (value, issuer, type = 'default') => { } if (ResolvePlugin.mode === 'compile') { - const interpolatedValue = Resolver.interpolate(file, issuer, type); + let interpolatedValue = Resolver.interpolate(file, issuer, type); + + if (isWin) interpolatedValue = pathToPosix(interpolatedValue); return requiredFile ? `require('${interpolatedValue}')` : `require(${interpolatedValue})`; } diff --git a/src/Loader/Preprocessors/Twig/index.js b/src/Loader/Preprocessors/Twig/index.js index 495a3809..768e3717 100644 --- a/src/Loader/Preprocessors/Twig/index.js +++ b/src/Loader/Preprocessors/Twig/index.js @@ -19,9 +19,12 @@ const preprocessor = (loaderContext, options) => { const resolveDependency = async (token) => { // if the `namespaces` twig option contains not absolute path, then a parsed path is the path relative to root context const filePath = TwigEngine.path.parsePath(template, token.value); - const file = path.isAbsolute(filePath) ? filePath : path.resolve(rootContext, filePath); + let file = path.isAbsolute(filePath) ? filePath : path.resolve(rootContext, filePath); token.value = makeTemplateId(rootContext, file); + + // fix windows-like path + file = file.replace(/\\/g, '/'); dependencies.add(file); loaderContext.addDependency(file); }; @@ -144,7 +147,8 @@ const preprocessor = (loaderContext, options) => { * @return {string} The exported template function. */ export(precompiledTemplate, { data, hot }) { - const runtimeFile = require.resolve('twig/twig.min.js'); + // fix windows-like path + const runtimeFile = require.resolve('twig/twig.min.js').replace(/\\/g, '/'); const exportFunctionName = 'templateFn'; const exportCode = 'module.exports='; let loadDependencies = ''; diff --git a/src/Loader/Resolver.js b/src/Loader/Resolver.js index ac7da8f1..83972069 100644 --- a/src/Loader/Resolver.js +++ b/src/Loader/Resolver.js @@ -285,9 +285,6 @@ class Resolver { return value; } - // TODO: check on win - //if (isWin) interpolatedValue = pathToPosix(interpolatedValue); - // remove quotes: '/path/to/file.js' -> /path/to/file.js let resolvedValue = interpolatedValue.slice(1, -1); let resolvedFile; @@ -304,8 +301,6 @@ class Resolver { } if (isScript) resolvedFile = this.resolveScriptExtension(resolvedFile); - // TODO: check on win - //return isWin ? pathToPosix(resolvedFile) : resolvedFile; return resolvedFile; } diff --git a/src/Loader/Template.js b/src/Loader/Template.js index 355083f0..1654ffa6 100644 --- a/src/Loader/Template.js +++ b/src/Loader/Template.js @@ -92,6 +92,10 @@ class Template { * - mailto:admin@test.com * - `\\u0027 + require(\\u0027/resolved/path/to/file.ext\\u0027) + \\u0027` // an expression of resolved file via a template engine * + * Allow special cases when value contains `:` + * - C:\path\to\file.ext + * - image.png?{size:600} + * * @param {boolean} isBasedir Whether is used the `root` option. * @param {string} type The type of source: 'style', 'script', 'asset'. * @param {string} value The attribute value to resolve as an absolute file path. @@ -108,7 +112,7 @@ class Template { value.startsWith('//') || value.startsWith('#') || value.startsWith('\\u0027') || - (value.indexOf(':') > 0 && value.indexOf('?{') < 0) + (value.indexOf(':') > 0 && value.indexOf(':\\') < 0 && value.indexOf('?{') < 0) ) { return false; } diff --git a/src/Loader/Utils.js b/src/Loader/Utils.js index 3c500d53..c23947b3 100644 --- a/src/Loader/Utils.js +++ b/src/Loader/Utils.js @@ -36,7 +36,14 @@ const resolveModule = (moduleName, context = process.cwd()) => { */ const eachAsync = async (data, fn) => (Array.isArray(data) ? Promise.all(data.map(fn)) : Promise.resolve()); -const makeTemplateId = (context, filePath) => path.relative(context, filePath); +/** + * Make template ID as relative posix path. + * + * @param {string} context + * @param {string} filePath + * @return {string} + */ +const makeTemplateId = (context, filePath) => path.relative(context, filePath).replace(/\\/g, '/'); /** * Inject a string before closing tag. diff --git a/test/cases/hook-afterEmit/webpack.config.js b/test/cases/hook-afterEmit/webpack.config.js index 7b443314..9798b0a4 100644 --- a/test/cases/hook-afterEmit/webpack.config.js +++ b/test/cases/hook-afterEmit/webpack.config.js @@ -7,7 +7,8 @@ const HtmlBundlerPlugin = require('@test/html-bundler-webpack-plugin'); * @return {Array<{resource: string, assetFile: string | Array}>} */ const manifest = (entries) => { - const relPath = (file) => path.relative(__dirname, file); + // fix windows-like path + const relPath = (file) => path.relative(__dirname, file).replace(/\\/g, '/'); const assets = []; //console.dir({ entries }, { depth: 7 }); diff --git a/test/cases/integrity-publicPath-auto/webpack.config.js b/test/cases/integrity-publicPath-auto/webpack.config.js index 86c8718e..0c1b9e19 100644 --- a/test/cases/integrity-publicPath-auto/webpack.config.js +++ b/test/cases/integrity-publicPath-auto/webpack.config.js @@ -7,7 +7,7 @@ module.exports = { target: 'web', output: { - publicPath: 'auto', // test empty + //publicPath: 'auto', // test auto path: path.join(__dirname, 'dist/'), crossOriginLoading: 'use-credentials', // required for test Subresource Integrity }, diff --git a/test/cases/option-afterEmit/webpack.config.js b/test/cases/option-afterEmit/webpack.config.js index 79d2ff7a..5752b383 100644 --- a/test/cases/option-afterEmit/webpack.config.js +++ b/test/cases/option-afterEmit/webpack.config.js @@ -13,7 +13,8 @@ const manifest = (entries) => { for (let entry of entries) { assets.push({ - resource: path.relative(__dirname, entry.resource), + // fix windows-like path + resource: path.relative(__dirname, entry.resource).replace(/\\/g, '/'), assetFile: entry.assetFile, }); @@ -21,7 +22,8 @@ const manifest = (entries) => { switch (asset.type) { case 'script': let assetItem = { - resource: path.relative(__dirname, asset.resource), + // fix windows-like path + resource: path.relative(__dirname, asset.resource).replace(/\\/g, '/'), assetFile: [], }; assets.push(assetItem); @@ -37,7 +39,8 @@ const manifest = (entries) => { break; case 'style': assets.push({ - resource: path.relative(__dirname, asset.resource), + // fix windows-like path + resource: path.relative(__dirname, asset.resource).replace(/\\/g, '/'), assetFile: asset.assetFile, }); break; diff --git a/test/cases/option-beforePreprocessor-return-undefined/webpack.config.js b/test/cases/option-beforePreprocessor-return-undefined/webpack.config.js index 15a82e95..be063f17 100644 --- a/test/cases/option-beforePreprocessor-return-undefined/webpack.config.js +++ b/test/cases/option-beforePreprocessor-return-undefined/webpack.config.js @@ -21,7 +21,7 @@ module.exports = { }, beforePreprocessor: (template, { resourcePath, data }) => { let sitename = 'Homepage'; - if (resourcePath.includes('/about.html')) sitename = 'About'; + if (resourcePath.includes('about.html')) sitename = 'About'; data.title = data.title.replace('[sitename]', sitename); // modify template data // test return undefined diff --git a/test/cases/option-beforePreprocessor/webpack.config.js b/test/cases/option-beforePreprocessor/webpack.config.js index 6b5ed081..89f9acaa 100644 --- a/test/cases/option-beforePreprocessor/webpack.config.js +++ b/test/cases/option-beforePreprocessor/webpack.config.js @@ -26,7 +26,7 @@ module.exports = { const loaderObject = loaderContext.loaders[loaderIndex]; let sitename = 'Homepage'; - if (resourcePath.includes('/about.html')) sitename = 'About'; + if (resourcePath.includes('about.html')) sitename = 'About'; let dataAsString = JSON.stringify(data).replace('[sitename]', sitename); const newData = JSON.parse(dataAsString); diff --git a/test/integration-pug.test.js b/test/integration-pug.test.js index a1de1407..453b653a 100644 --- a/test/integration-pug.test.js +++ b/test/integration-pug.test.js @@ -46,7 +46,7 @@ describe('require code', () => { test('require js module relative', () => compareFiles('_pug/require-js-relative')); }); -describe('resolve assets', () => { +describe('resolve assets with require', () => { test('img attributes', () => compareFiles('_pug/resolve-img-attributes')); test('img attributes, require', () => compareFiles('_pug/resolve-img-attributes-require')); diff --git a/test/integration.test.js b/test/integration.test.js index 84c2949f..2813ab8c 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -161,7 +161,7 @@ describe('plugin hooks', () => { }); describe('plugin callbacks', () => { - test('beforePreprocessor', () => compareFiles('option-beforePreprocessor')); + test('beforePreprocessor, return template', () => compareFiles('option-beforePreprocessor')); test('beforePreprocessor, return undefined', () => compareFiles('option-beforePreprocessor-return-undefined')); test('preprocessor', () => compareFiles('option-preprocessor')); @@ -419,12 +419,13 @@ describe('special cases', () => { // test('resolve hmr file', () => watchCompareFiles('resolve-hmr-file')); }); -describe('integrity', () => { +describe('integrity, common use cases', () => { // TODO: implement and add tests for preload + // TODO: fix issue on windows + test('script, link, publicPath="auto"', () => compareFiles('integrity-publicPath-auto')); test('script, link, publicPath=""', () => compareFiles('integrity-publicPath-empty')); test('script, link, publicPath="/"', () => compareFiles('integrity-publicPath-root')); - test('script, link, publicPath="auto"', () => compareFiles('integrity-publicPath-auto')); test('split chunks', () => compareFiles('integrity-split-chunks')); test('import css', () => compareFiles('integrity-import-css-in-js'));