diff --git a/lib/helpers.js b/lib/helpers.js index aa12d19..da6c4e7 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,3 +1,5 @@ +const { dirname, join, basename } = require('path'); + const generateManifest = (compilation, files, { generate, seed = {} }) => { let result; if (generate) { @@ -24,8 +26,16 @@ const getFileType = (fileName, { transformExtensions }) => { return transformExtensions.test(extension) ? `${split.pop()}.${extension}` : extension; }; -const reduceAssets = (files, asset, moduleAssets) => { - const name = moduleAssets[asset.name] ? moduleAssets[asset.name] : asset.info.sourceFilename; +const reduceAssets = (files, asset, moduleAssets, assetTypeModuleAssets) => { + let name; + if (moduleAssets[asset.name]) { + name = moduleAssets[asset.name]; + } else if (assetTypeModuleAssets[asset.info.sourceFilename]) { + name = join(dirname(asset.name), assetTypeModuleAssets[asset.info.sourceFilename]); + } else { + name = asset.info.sourceFilename; + } + if (name) { return files.concat({ path: asset.name, @@ -52,29 +62,43 @@ const reduceAssets = (files, asset, moduleAssets) => { }); }; -const reduceChunk = (files, chunk, options) => - Array.of(...Array.from(chunk.files), ...Array.from(chunk.auxiliaryFiles || [])).reduce( - (prev, path) => { - let name = chunk.name ? chunk.name : null; - // chunk name, or for nameless chunks, just map the files directly. - name = name - ? options.useEntryKeys && !path.endsWith('.map') - ? name - : `${name}.${getFileType(path, options)}` - : path; +const reduceChunk = (files, chunk, options, auxiliaryFiles) => { + // auxiliary files contain things like images, fonts AND, most + // importantly, other files like .map sourcemap files + // we modify the auxiliaryFiles so that we can add any of these + // to the manifest that was not added by another method + // (sourcemaps files are not added via any other method) + Array.from(chunk.auxiliaryFiles || []).forEach((auxiliaryFile) => { + auxiliaryFiles[auxiliaryFile] = { + path: auxiliaryFile, + name: basename(auxiliaryFile), + isInitial: false, + isChunk: false, + isAsset: true, + isModuleAsset: true + }; + }); + + return Array.from(chunk.files).reduce((prev, path) => { + let name = chunk.name ? chunk.name : null; + // chunk name, or for nameless chunks, just map the files directly. + name = name + ? options.useEntryKeys && !path.endsWith('.map') + ? name + : `${name}.${getFileType(path, options)}` + : path; - return prev.concat({ - path, - chunk, - name, - isInitial: chunk.isOnlyInitial(), - isChunk: true, - isAsset: false, - isModuleAsset: false - }); - }, - files - ); + return prev.concat({ + path, + chunk, + name, + isInitial: chunk.isOnlyInitial(), + isChunk: true, + isAsset: false, + isModuleAsset: false + }); + }, files); +}; const standardizeFilePaths = (file) => { const result = Object.assign({}, file); diff --git a/lib/hooks.js b/lib/hooks.js index 3e32bf3..a629e50 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -1,5 +1,5 @@ const { mkdirSync, writeFileSync } = require('fs'); -const { basename, dirname, join } = require('path'); +const { basename, dirname, join, relative } = require('path'); const { SyncWaterfallHook } = require('tapable'); const webpack = require('webpack'); @@ -33,7 +33,15 @@ const beforeRunHook = ({ emitCountMap, manifestFileName }, compiler, callback) = }; const emitHook = function emit( - { compiler, emitCountMap, manifestAssetId, manifestFileName, moduleAssets, options }, + { + compiler, + emitCountMap, + manifestAssetId, + manifestFileName, + moduleAssets, + assetTypeModuleAssets, + options + }, compilation ) { const emitCount = emitCountMap.get(manifestFileName) - 1; @@ -51,13 +59,17 @@ const emitHook = function emit( emitCountMap.set(manifestFileName, emitCount); + const auxiliaryFiles = {}; let files = Array.from(compilation.chunks).reduce( - (prev, chunk) => reduceChunk(prev, chunk, options), + (prev, chunk) => reduceChunk(prev, chunk, options, auxiliaryFiles), [] ); // module assets don't show up in assetsByChunkName, we're getting them this way - files = stats.assets.reduce((prev, asset) => reduceAssets(prev, asset, moduleAssets), files); + files = stats.assets.reduce( + (prev, asset) => reduceAssets(prev, asset, moduleAssets, assetTypeModuleAssets), + files + ); // don't add hot updates and don't add manifests from other instances files = files.filter( @@ -66,6 +78,17 @@ const emitHook = function emit( typeof emitCountMap.get(join(compiler.options.output.path, name)) === 'undefined' ); + // auxiliary files are "extra" files that are probably already included + // in other ways. Loop over files and remove any from auxiliaryFiles + files.forEach((file) => { + delete auxiliaryFiles[file.path]; + }); + // if there are any auxiliaryFiles left, add them to the files + // this handles, specifically, sourcemaps + Object.keys(auxiliaryFiles).forEach((auxiliaryFile) => { + files = files.concat(auxiliaryFiles[auxiliaryFile]); + }); + files = files.map((file) => { const changes = { // Append optional basepath onto all references. This allows output path to be reflected in the manifest. @@ -113,9 +136,24 @@ const emitHook = function emit( getCompilerHooks(compiler).afterEmit.call(manifest); }; -const normalModuleLoaderHook = ({ moduleAssets }, loaderContext, module) => { +const normalModuleLoaderHook = ({ moduleAssets, assetTypeModuleAssets }, loaderContext, module) => { const { emitFile } = loaderContext; + // the "emitFile" callback is never called on asset modules + // so, we create a different map that can be used later in the "emit" hook + if (['asset', 'asset/inline', 'asset/resource', 'asset/source'].includes(module.type)) { + // This takes the userRequest (which is an absolute path) and turns it into + // a relative path to the root context. This is done so that the string + // will match asset.info.sourceFilename in the emit hook. + let sourceFilename = relative(loaderContext.rootContext, module.userRequest); + // at this point, Windows paths use \ in their paths + // but in the emit hook, asset.info.sourceFilename fill have UNIX slashes + sourceFilename = sourceFilename.replace(/\\/g, '/'); + Object.assign(assetTypeModuleAssets, { + [sourceFilename]: basename(module.userRequest) + }); + } + // eslint-disable-next-line no-param-reassign loaderContext.emitFile = (file, content, sourceMap) => { if (module.userRequest && !moduleAssets[file]) { diff --git a/lib/index.js b/lib/index.js index 189266b..4c7a0dd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -33,6 +33,7 @@ class WebpackManifestPlugin { apply(compiler) { const moduleAssets = {}; + const assetTypeModuleAssets = {}; const manifestFileName = resolve(compiler.options.output.path, this.options.fileName); const manifestAssetId = relative(compiler.options.output.path, manifestFileName); const beforeRun = beforeRunHook.bind(this, { emitCountMap, manifestFileName }); @@ -42,9 +43,10 @@ class WebpackManifestPlugin { manifestAssetId, manifestFileName, moduleAssets, + assetTypeModuleAssets, options: this.options }); - const normalModuleLoader = normalModuleLoaderHook.bind(this, { moduleAssets }); + const normalModuleLoader = normalModuleLoaderHook.bind(this, { moduleAssets, assetTypeModuleAssets }); const hookOptions = { name: 'WebpackManifestPlugin', stage: Infinity diff --git a/package.json b/package.json index 6e7bebb..d981a14 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lint:package": "prettier --write package.json --plugin=prettier-plugin-package", "posttest": "npm install webpack@^4.44.2", "security": "npm audit --audit-level=moderate", - "test": "npm run test:v4", + "test": "npm run test:v4 && npm run test:v5", "test:v4": "ava", "test:v5": "npm install webpack@^5.0.0 --no-save && ava" }, diff --git a/test/fixtures/import_image.js b/test/fixtures/import_image.js new file mode 100644 index 0000000..d138af3 --- /dev/null +++ b/test/fixtures/import_image.js @@ -0,0 +1 @@ +import '../../assets/manifest.svg'; diff --git a/test/integration/location.js b/test/integration/location.js index c022cf9..e76a42f 100644 --- a/test/integration/location.js +++ b/test/integration/location.js @@ -22,12 +22,12 @@ test('output to the correct location', async (t) => { filename: '[name].js', path: outputPath }, - plugins: [new WebpackManifestPlugin({ fileName: 'webpack.manifest.js' })] + plugins: [new WebpackManifestPlugin({ fileName: 'webpack.manifest.json' })] }; await compile(config, {}, t); - const manifestPath = join(outputPath, 'webpack.manifest.js'); + const manifestPath = join(outputPath, 'webpack.manifest.json'); const result = readJson(manifestPath); t.deepEqual(result, { 'main.js': 'main.js' }); @@ -41,11 +41,11 @@ test('output using absolute path', async (t) => { filename: '[name].js', path: absOutputPath }, - plugins: [new WebpackManifestPlugin({ fileName: join(absOutputPath, 'webpack.manifest.js') })] + plugins: [new WebpackManifestPlugin({ fileName: join(absOutputPath, 'webpack.manifest.json') })] }; await compile(config, {}, t); - const manifestPath = join(absOutputPath, 'webpack.manifest.js'); + const manifestPath = join(absOutputPath, 'webpack.manifest.json'); const result = readJson(manifestPath); t.deepEqual(result, { 'main.js': 'main.js' }); diff --git a/test/unit/index.js b/test/unit/index.js index a1ddd5a..9ee2372 100644 --- a/test/unit/index.js +++ b/test/unit/index.js @@ -2,7 +2,6 @@ const { join } = require('path'); const test = require('ava'); const del = require('del'); -const webpack = require('webpack'); const { getCompilerHooks, WebpackManifestPlugin } = require('../../lib'); const { compile, hashLiteral } = require('../helpers/unit'); @@ -75,15 +74,15 @@ test('works with source maps', async (t) => { one: '../fixtures/file.js' }, output: { - filename: '[name].js', + filename: 'build/[name].js', path: join(outputPath, 'source-maps') } }; const { manifest } = await compile(config, t); t.deepEqual(manifest, { - 'one.js': 'one.js', - 'one.js.map': 'one.js.map' + 'one.js': 'build/one.js', + 'one.js.map': 'build/one.js.map' }); }); @@ -158,11 +157,6 @@ test('outputs a manifest of no-js file', async (t) => { 'file.txt': 'file.txt' }; - // Note: I believe this to be another bug in webpack v5 and cannot find a good workaround atm - if (webpack.version.startsWith('5')) { - expected['main.txt'] = 'file.txt'; - } - t.truthy(manifest); t.deepEqual(manifest, expected); }); @@ -188,3 +182,31 @@ test('make manifest available to other webpack plugins', async (t) => { t.pass(); } }); + +test('works with asset modules', async (t) => { + const config = { + context: __dirname, + entry: '../fixtures/import_image.js', + output: { + path: join(outputPath, 'auxiliary-assets'), + assetModuleFilename: `images/[name].[hash:4][ext]` + }, + module: { + rules: [ + { + test: /\.(svg)/, + type: 'asset/resource' + } + ] + } + }; + + const { manifest } = await compile(config, t); + const expected = { + 'main.js': 'main.js', + 'images/manifest.svg': `images/manifest.14ca.svg` + }; + + t.truthy(manifest); + t.deepEqual(manifest, expected); +}); diff --git a/test/unit/manifest-location.js b/test/unit/manifest-location.js index c7ba0fa..0d889ff 100644 --- a/test/unit/manifest-location.js +++ b/test/unit/manifest-location.js @@ -17,7 +17,7 @@ test('relative path', async (t) => { }; const { manifest } = await compile(config, t, { - fileName: 'webpack.manifest.js' + fileName: 'webpack.manifest.json' }); t.deepEqual(manifest, { 'main.js': 'main.js' }); @@ -31,7 +31,7 @@ test('absolute path', async (t) => { }; const { manifest } = await compile(config, t, { - fileName: join(outputPath, 'absolute/webpack.manifest.js') + fileName: join(outputPath, 'absolute/webpack.manifest.json') }); t.deepEqual(manifest, { 'main.js': 'main.js' }); diff --git a/test/unit/paths.js b/test/unit/paths.js index 0005619..7cf5847 100644 --- a/test/unit/paths.js +++ b/test/unit/paths.js @@ -2,7 +2,6 @@ const { join } = require('path'); const test = require('ava'); const del = require('del'); -const webpack = require('webpack'); const { compile, hashLiteral } = require('../helpers/unit'); @@ -200,11 +199,6 @@ test('ensures the manifest is mapping paths to names', async (t) => { 'file.txt': 'outputfile.txt' }; - // Note: I believe this to be another bug in webpack v5 and cannot find a good workaround atm - if (webpack.version.startsWith('5')) { - expected['main.txt'] = 'outputfile.txt'; - } - t.truthy(manifest); t.deepEqual(manifest, expected); });