diff --git a/.eslintrc.js b/.eslintrc.js index 78155ff7..a018f2a4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { "es6": true, }, "parserOptions": { "ecmaVersion": 2017 }, + "ignorePatterns": ["lib/webpack-manifest-plugin"], "rules": { "quotes": ["error", "single"], "no-undef": "error", diff --git a/.github/workflows/node-windows.yml b/.github/workflows/node-windows.yml index ce0afa5b..3c35389d 100644 --- a/.github/workflows/node-windows.yml +++ b/.github/workflows/node-windows.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node: [ '14', '12', '10' ] + node: [ '14', '10' ] name: ${{ matrix.node }} (Windows) steps: diff --git a/fixtures/js/import_node_modules_image.js b/fixtures/js/import_node_modules_image.js new file mode 100644 index 00000000..828667a6 --- /dev/null +++ b/fixtures/js/import_node_modules_image.js @@ -0,0 +1,10 @@ +// this helps us trigger a manifest.json bug +// https://github.com/shellscape/webpack-manifest-plugin/pull/249 +import 'mocha/assets/growl/ok.png'; +import '../images/symfony_logo.png'; + + +// module.userRequest +// growl: /Users/weaverryan/Sites/os/webpack-encore/node_modules/mocha/assets/growl/ok.png +// logo: /Users/weaverryan/Sites/os/webpack-encore/test_tmp/51witp/images/symfony_logo.png + diff --git a/lib/plugins/manifest.js b/lib/plugins/manifest.js index 4073b7fb..9173f497 100644 --- a/lib/plugins/manifest.js +++ b/lib/plugins/manifest.js @@ -10,7 +10,7 @@ 'use strict'; const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const { WebpackManifestPlugin } = require('../webpack-manifest-plugin'); const PluginPriorities = require('./plugin-priorities'); const applyOptionsCallback = require('../utils/apply-options-callback'); const copyEntryTmpName = require('../utils/copyEntryTmpName'); diff --git a/lib/webpack-manifest-plugin/LICENSE b/lib/webpack-manifest-plugin/LICENSE new file mode 100644 index 00000000..e540a67b --- /dev/null +++ b/lib/webpack-manifest-plugin/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Dane Thurber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/webpack-manifest-plugin/README.md b/lib/webpack-manifest-plugin/README.md new file mode 100644 index 00000000..ea088324 --- /dev/null +++ b/lib/webpack-manifest-plugin/README.md @@ -0,0 +1,6 @@ +# webpack-manifest-plugin + +This is a copy of https://github.com/shellscape/webpack-manifest-plugin +at sha: 9f408f609d9b1af255491036b6fc127777ee6f9a. + +It has been modified to fix this bug: https://github.com/shellscape/webpack-manifest-plugin/pull/249 diff --git a/lib/webpack-manifest-plugin/helpers.js b/lib/webpack-manifest-plugin/helpers.js new file mode 100644 index 00000000..0d53a854 --- /dev/null +++ b/lib/webpack-manifest-plugin/helpers.js @@ -0,0 +1,115 @@ +const { dirname, join, basename } = require('path'); + +const generateManifest = (compilation, files, { generate, seed = {} }) => { + let result; + if (generate) { + const entrypointsArray = Array.from(compilation.entrypoints.entries()); + const entrypoints = entrypointsArray.reduce( + (e, [name, entrypoint]) => Object.assign(e, { [name]: entrypoint.getFiles() }), + {} + ); + result = generate(seed, files, entrypoints); + } else { + result = files.reduce( + (manifest, file) => Object.assign(manifest, { [file.name]: file.path }), + seed + ); + } + + return result; +}; + +const getFileType = (fileName, { transformExtensions }) => { + const replaced = fileName.replace(/\?.*/, ''); + const split = replaced.split('.'); + const extension = split.pop(); + return transformExtensions.test(extension) ? `${split.pop()}.${extension}` : extension; +}; + +const reduceAssets = (files, asset, moduleAssets) => { + let name; + if (moduleAssets[asset.name]) { + name = moduleAssets[asset.name]; + } else if (asset.info.sourceFilename) { + name = join(dirname(asset.name), basename(asset.info.sourceFilename)); + } + + if (name) { + return files.concat({ + path: asset.name, + name, + isInitial: false, + isChunk: false, + isAsset: true, + isModuleAsset: true + }); + } + + const isEntryAsset = asset.chunks && asset.chunks.length > 0; + if (isEntryAsset) { + return files; + } + + return files.concat({ + path: asset.name, + name: asset.name, + isInitial: false, + isChunk: false, + isAsset: true, + isModuleAsset: false + }); +}; + +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); +}; + +const standardizeFilePaths = (file) => { + const result = Object.assign({}, file); + result.name = file.name.replace(/\\/g, '/'); + result.path = file.path.replace(/\\/g, '/'); + return result; +}; + +const transformFiles = (files, options) => + ['filter', 'map', 'sort'] + .filter((fname) => !!options[fname]) + // TODO: deprecate these + .reduce((prev, fname) => prev[fname](options[fname]), files) + .map(standardizeFilePaths); + +module.exports = { generateManifest, reduceAssets, reduceChunk, transformFiles }; diff --git a/lib/webpack-manifest-plugin/hooks.js b/lib/webpack-manifest-plugin/hooks.js new file mode 100644 index 00000000..bdbc84ce --- /dev/null +++ b/lib/webpack-manifest-plugin/hooks.js @@ -0,0 +1,141 @@ +const { mkdirSync, writeFileSync } = require('fs'); +const { basename, dirname, join } = require('path'); + +const { SyncWaterfallHook } = require('tapable'); +const webpack = require('webpack'); +// eslint-disable-next-line global-require +const { RawSource } = webpack.sources || require('webpack-sources'); + +const { generateManifest, reduceAssets, reduceChunk, transformFiles } = require('./helpers'); + +const compilerHookMap = new WeakMap(); + +const getCompilerHooks = (compiler) => { + let hooks = compilerHookMap.get(compiler); + if (typeof hooks === 'undefined') { + hooks = { + afterEmit: new SyncWaterfallHook(['manifest']), + beforeEmit: new SyncWaterfallHook(['manifest']) + }; + compilerHookMap.set(compiler, hooks); + } + return hooks; +}; + +const beforeRunHook = ({ emitCountMap, manifestFileName }, compiler, callback) => { + const emitCount = emitCountMap.get(manifestFileName) || 0; + emitCountMap.set(manifestFileName, emitCount + 1); + + /* istanbul ignore next */ + if (callback) { + callback(); + } +}; + +const emitHook = function emit( + { compiler, emitCountMap, manifestAssetId, manifestFileName, moduleAssets, options }, + compilation +) { + const emitCount = emitCountMap.get(manifestFileName) - 1; + // Disable everything we don't use, add asset info, show cached assets + const stats = compilation.getStats().toJson({ + all: false, + assets: true, + cachedAssets: true, + ids: true, + publicPath: true + }); + + const publicPath = options.publicPath !== null ? options.publicPath : stats.publicPath; + const { basePath, removeKeyHash } = options; + + emitCountMap.set(manifestFileName, emitCount); + + const auxiliaryFiles = {}; + let files = Array.from(compilation.chunks).reduce( + (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); + + // don't add hot updates and don't add manifests from other instances + files = files.filter( + ({ name, path }) => + !path.includes('hot-update') && + 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. + name: basePath ? basePath + file.name : file.name, + // Similar to basePath but only affects the value (e.g. how output.publicPath turns + // require('foo/bar') into '/public/foo/bar', see https://github.com/webpack/docs/wiki/configuration#outputpublicpath + path: publicPath ? publicPath + file.path : file.path + }; + + // Fixes #210 + changes.name = removeKeyHash ? changes.name.replace(removeKeyHash, '') : changes.name; + + return Object.assign(file, changes); + }); + + files = transformFiles(files, options); + + let manifest = generateManifest(compilation, files, options); + const isLastEmit = emitCount === 0; + + manifest = getCompilerHooks(compiler).beforeEmit.call(manifest); + + if (isLastEmit) { + const output = options.serialize(manifest); + // + // Object.assign(compilation.assets, { + // [manifestAssetId]: { + // source() { + // return output; + // }, + // size() { + // return output.length; + // } + // } + // }); + // + compilation.emitAsset(manifestAssetId, new RawSource(output)); + + if (options.writeToFileEmit) { + mkdirSync(dirname(manifestFileName), { recursive: true }); + writeFileSync(manifestFileName, output); + } + } + + getCompilerHooks(compiler).afterEmit.call(manifest); +}; + +const normalModuleLoaderHook = ({ moduleAssets }, loaderContext, module) => { + const { emitFile } = loaderContext; + + // eslint-disable-next-line no-param-reassign + loaderContext.emitFile = (file, content, sourceMap) => { + if (module.userRequest && !moduleAssets[file]) { + Object.assign(moduleAssets, { [file]: join(dirname(file), basename(module.userRequest)) }); + } + + return emitFile.call(module, file, content, sourceMap); + }; +}; + +module.exports = { beforeRunHook, emitHook, getCompilerHooks, normalModuleLoaderHook }; diff --git a/lib/webpack-manifest-plugin/index.js b/lib/webpack-manifest-plugin/index.js new file mode 100644 index 00000000..189266b1 --- /dev/null +++ b/lib/webpack-manifest-plugin/index.js @@ -0,0 +1,73 @@ +const { relative, resolve } = require('path'); + +const webpack = require('webpack'); +const NormalModule = require('webpack/lib/NormalModule'); + +const { beforeRunHook, emitHook, getCompilerHooks, normalModuleLoaderHook } = require('./hooks'); + +const emitCountMap = new Map(); + +const defaults = { + basePath: '', + fileName: 'manifest.json', + filter: null, + generate: void 0, + map: null, + publicPath: null, + removeKeyHash: /([a-f0-9]{32}\.?)/gi, + // seed must be reset for each compilation. let the code initialize it to {} + seed: void 0, + serialize(manifest) { + return JSON.stringify(manifest, null, 2); + }, + sort: null, + transformExtensions: /^(gz|map)$/i, + useEntryKeys: false, + writeToFileEmit: false +}; + +class WebpackManifestPlugin { + constructor(opts) { + this.options = Object.assign({}, defaults, opts); + } + + apply(compiler) { + const moduleAssets = {}; + 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 }); + const emit = emitHook.bind(this, { + compiler, + emitCountMap, + manifestAssetId, + manifestFileName, + moduleAssets, + options: this.options + }); + const normalModuleLoader = normalModuleLoaderHook.bind(this, { moduleAssets }); + const hookOptions = { + name: 'WebpackManifestPlugin', + stage: Infinity + }; + + compiler.hooks.compilation.tap(hookOptions, (compilation) => { + const hook = !NormalModule.getCompilationHooks + ? compilation.hooks.normalModuleLoader + : NormalModule.getCompilationHooks(compilation).loader; + hook.tap(hookOptions, normalModuleLoader); + }); + + if (webpack.version.startsWith('4')) { + compiler.hooks.emit.tap(hookOptions, emit); + } else { + compiler.hooks.thisCompilation.tap(hookOptions, (compilation) => { + compilation.hooks.processAssets.tap(hookOptions, () => emit(compilation)); + }); + } + + compiler.hooks.run.tap(hookOptions, beforeRun); + compiler.hooks.watchRun.tap(hookOptions, beforeRun); + } +} + +module.exports = { getCompilerHooks, WebpackManifestPlugin }; diff --git a/package.json b/package.json index cb34bf3b..55144fe6 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "webpack": "^5.12", "webpack-cli": "^4", "webpack-dev-server": "^4.0.0-beta.0", - "webpack-manifest-plugin": "^3", "yargs-parser": "^20.2.4" }, "devDependencies": { @@ -67,7 +66,7 @@ "chai-fs": "^2.0.0", "chai-subset": "^1.6.0", "core-js": "^3.0.0", - "eslint": "^6.0.0 || ^7.0.0", + "eslint": "^6.7.0 || ^7.0.0", "eslint-loader": "^4.0.0", "eslint-plugin-header": "^3.0.0", "eslint-plugin-import": "^2.8.0", diff --git a/test/config-generator.js b/test/config-generator.js index 6aefe2ef..dbee0a15 100644 --- a/test/config-generator.js +++ b/test/config-generator.js @@ -14,7 +14,7 @@ const WebpackConfig = require('../lib/WebpackConfig'); const RuntimeConfig = require('../lib/config/RuntimeConfig'); const configGenerator = require('../lib/config-generator'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const { WebpackManifestPlugin } = require('../lib/webpack-manifest-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const webpack = require('webpack'); const path = require('path'); diff --git a/test/functional.js b/test/functional.js index aafd9133..570b8585 100644 --- a/test/functional.js +++ b/test/functional.js @@ -152,6 +152,26 @@ describe('Functional tests using webpack', function() { }); }); + it('Check manifest.json with node_module includes', (done) => { + const config = createWebpackConfig('web/build', 'dev'); + config.addEntry('main', './js/import_node_modules_image'); + config.setPublicPath('/build'); + + testSetup.runWebpack(config, (webpackAssert) => { + // should have a main.js file + // should have a manifest.json with public/main.js + + webpackAssert.assertOutputJsonFileMatches('manifest.json', { + 'build/main.js': '/build/main.js', + 'build/runtime.js': '/build/runtime.js', + 'build/images/symfony_logo.png': '/build/images/symfony_logo.91beba37.png', + 'build/images/ok.png': '/build/images/ok.c3f4e113.png', + }); + + done(); + }); + }); + it('Use "all" splitChunks & look at entrypoints.json', (done) => { const config = createWebpackConfig('web/build', 'dev'); config.addEntry('main', ['./css/roboto_font.css', './js/no_require', 'vue']); @@ -2377,7 +2397,7 @@ module.exports = { // and be versioned config.copyFiles({ from: './copy', - to: './[path][name]-[hash].[ext]', + to: './[path][name]-[hash:8].[ext]', pattern: /\.(css|js)$/, }); @@ -2407,21 +2427,20 @@ module.exports = { }); testSetup.runWebpack(config, (webpackAssert) => { - expect(config.outputPath).to.be.a.directory() - .with.files([ - 'entrypoints.json', - 'runtime.js', - 'main.js', - 'manifest.json', + webpackAssert.assertDirectoryContents([ + 'entrypoints.json', + 'runtime.js', + 'main.js', + 'manifest.json', - // 1st rule - 'foo-5d76c098640df1edecc7ca66ee62b1ea.css', - 'foo-5d76c098640df1edecc7ca66ee62b1ea.js', + // 1st rule + 'foo-[hash:8].css', + 'foo-[hash:8].js', - // 2nd rule - 'foo.json', - 'foo.png', - ]); + // 2nd rule + 'foo.json', + 'foo.png', + ]); done(); }); diff --git a/test/plugins/manifest.js b/test/plugins/manifest.js index b172966a..89a3405e 100644 --- a/test/plugins/manifest.js +++ b/test/plugins/manifest.js @@ -10,7 +10,7 @@ 'use strict'; const expect = require('chai').expect; -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const { WebpackManifestPlugin } = require('../../lib/webpack-manifest-plugin'); const WebpackConfig = require('../../lib/WebpackConfig'); const RuntimeConfig = require('../../lib/config/RuntimeConfig'); const manifestPluginUtil = require('../../lib/plugins/manifest'); diff --git a/yarn.lock b/yarn.lock index 5ab662ad..58f6bc5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3166,10 +3166,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -"eslint@^6.0.0 || ^7.0.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67" - integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ== +"eslint@^6.7.0 || ^7.0.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" + integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== dependencies: "@babel/code-frame" "^7.0.0" "@eslint/eslintrc" "^0.3.0" @@ -7051,7 +7051,7 @@ tapable@^1.0.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.1.1, tapable@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== @@ -7539,14 +7539,6 @@ webpack-dev-server@^4.0.0-beta.0: webpack-dev-middleware "^4.0.2" ws "^7.4.0" -webpack-manifest-plugin@^3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-3.0.0.tgz#426644300e5dc41a75a9c996c4d4f876eb3c2b5b" - integrity sha512-nbORTdky2HxD8XSaaT+zrsHb30AAgyWAWgCLWaAeQO21VGCScGb52ipqlHA/njix1Z8OW8IOlo4+XK0OKr1fkw== - dependencies: - tapable "^2.0.0" - webpack-sources "^2.2.0" - webpack-merge@^5.7.3: version "5.7.3" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" @@ -7571,7 +7563,7 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.3: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^2.1.1, webpack-sources@^2.2.0: +webpack-sources@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac" integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==