var entries = require('object.entries'); var path = require('path'); var fse = require('fs-extra'); var _ = require('lodash'); const emitCountMap = new Map(); const compilerHookMap = new WeakMap(); const standardizeFilePaths = (file) => { file.name = file.name.replace(/\\/g, '/'); file.path = file.path.replace(/\\/g, '/'); return file; }; function ManifestPlugin(opts) { this.opts = _.assign({ publicPath: null, basePath: '', fileName: 'manifest.json', transformExtensions: /^(gz|map)$/i, writeToFileEmit: false, seed: null, filter: null, map: null, generate: null, sort: null, serialize: function(manifest) { return JSON.stringify(manifest, null, 2); }, }, opts || {}); } ManifestPlugin.getCompilerHooks = (compiler) => { var hooks = compilerHookMap.get(compiler); if (hooks === undefined) { const SyncWaterfallHook = require('tapable').SyncWaterfallHook; hooks = { afterEmit: new SyncWaterfallHook(['manifest']) }; compilerHookMap.set(compiler, hooks); } return hooks; } ManifestPlugin.prototype.getFileType = function(str) { str = str.replace(/\?.*/, ''); var split = str.split('.'); var ext = split.pop(); if (this.opts.transformExtensions.test(ext)) { ext = split.pop() + '.' + ext; } return ext; }; ManifestPlugin.prototype.apply = function(compiler) { var moduleAssets = {}; var outputFolder = compiler.options.output.path; var outputFile = path.resolve(outputFolder, this.opts.fileName); var outputName = path.relative(outputFolder, outputFile); var moduleAsset = function (module, file) { if (module.userRequest) { moduleAssets[file] = path.join( path.dirname(file), path.basename(module.userRequest) ); } }; var emit = function(compilation, compileCallback) { const emitCount = emitCountMap.get(outputFile) - 1 emitCountMap.set(outputFile, emitCount); var seed = this.opts.seed || {}; var publicPath = this.opts.publicPath != null ? this.opts.publicPath : compilation.options.output.publicPath; var stats = compilation.getStats().toJson({ // Disable data generation of everything we don't use all: false, // Add asset Information assets: true, // Show cached assets (setting this to `false` only shows emitted files) cachedAssets: true, }); var files = compilation.chunks.reduce(function(files, chunk) { return chunk.files.reduce(function (files, path) { var name = chunk.name ? chunk.name : null; if (name) { name = name + '.' + this.getFileType(path); } else { // For nameless chunks, just map the files directly. name = path; } // Webpack 4: .isOnlyInitial() // Webpack 3: .isInitial() // Webpack 1/2: .initial return files.concat({ path: path, chunk: chunk, name: name, isInitial: chunk.isOnlyInitial ? chunk.isOnlyInitial() : (chunk.isInitial ? chunk.isInitial() : chunk.initial), isChunk: true, isAsset: false, isModuleAsset: false }); }.bind(this), files); }.bind(this), []); // module assets don't show up in assetsByChunkName. // we're getting them this way; files = stats.assets.reduce(function (files, asset) { var name = moduleAssets[asset.name]; if (name) { return files.concat({ path: asset.name, name: name, isInitial: false, isChunk: false, isAsset: true, isModuleAsset: true }); } var isEntryAsset = 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 }); }, files); files = files.filter(function (file) { // Don't add hot updates to manifest var isUpdateChunk = file.path.indexOf('hot-update') >= 0; // Don't add manifest from another instance var isManifest = emitCountMap.get(path.join(outputFolder, file.name)) !== undefined; return !isUpdateChunk && !isManifest; }); // Append optional basepath onto all references. // This allows output path to be reflected in the manifest. if (this.opts.basePath) { files = files.map(function(file) { file.name = this.opts.basePath + file.name; return file; }.bind(this)); } if (publicPath) { // Similar to basePath but only affects the value (similar to how // output.publicPath turns require('foo/bar') into '/public/foo/bar', see // https://github.com/webpack/docs/wiki/configuration#outputpublicpath files = files.map(function(file) { file.path = publicPath + file.path; return file; }.bind(this)); } files = files.map(standardizeFilePaths); if (this.opts.filter) { files = files.filter(this.opts.filter); } if (this.opts.map) { files = files.map(this.opts.map).map(standardizeFilePaths); } if (this.opts.sort) { files = files.sort(this.opts.sort); } var manifest; if (this.opts.generate) { const entrypointsArray = Array.from( compilation.entrypoints instanceof Map ? // Webpack 4+ compilation.entrypoints.entries() : // Webpack 3 entries(compilation.entrypoints) ); const entrypoints = entrypointsArray.reduce( (e, [name, entrypoint]) => Object.assign(e, { [name]: entrypoint.getFiles() }), {} ); manifest = this.opts.generate(seed, files, entrypoints); } else { manifest = files.reduce(function (manifest, file) { manifest[file.name] = file.path; return manifest; }, seed); } const isLastEmit = emitCount === 0 if (isLastEmit) { var output = this.opts.serialize(manifest); compilation.assets[outputName] = { source: function() { return output; }, size: function() { return output.length; } }; if (this.opts.writeToFileEmit) { fse.outputFileSync(outputFile, output); } } if (compiler.hooks) { ManifestPlugin.getCompilerHooks(compiler).afterEmit.call(manifest); } else { compilation.applyPluginsAsync('webpack-manifest-plugin-after-emit', manifest, compileCallback); } }.bind(this); function beforeRun (compiler, callback) { let emitCount = emitCountMap.get(outputFile) || 0; emitCountMap.set(outputFile, emitCount + 1); if (callback) { callback(); } } if (compiler.hooks) { const pluginOptions = { name: 'ManifestPlugin', stage: Infinity }; // Preserve exposure of custom hook in Webpack 4 for back compatability. // Going forward, plugins should call `ManifestPlugin.getCompilerHooks(compiler)` directy. if (!Object.isFrozen(compiler.hooks)) { compiler.hooks.webpackManifestPluginAfterEmit = ManifestPlugin.getCompilerHooks(compiler).afterEmit; } compiler.hooks.compilation.tap(pluginOptions, function (compilation) { compilation.hooks.moduleAsset.tap(pluginOptions, moduleAsset); }); compiler.hooks.emit.tap(pluginOptions, emit); compiler.hooks.run.tap(pluginOptions, beforeRun); compiler.hooks.watchRun.tap(pluginOptions, beforeRun); } else { compiler.plugin('compilation', function (compilation) { compilation.plugin('module-asset', moduleAsset); }); compiler.plugin('emit', emit); compiler.plugin('before-run', beforeRun); compiler.plugin('watch-run', beforeRun); } }; module.exports = ManifestPlugin;