UNPKG

webpack-remove-empty-scripts

Version:

Webpack plugin removes empty JavaScript files generated when using styles.

172 lines (137 loc) 6.01 kB
/** * Webpack 5 plugin to remove empty scripts generated by usage only styles in webpack entry. */ const path = require('path'); const { cyan, black, gray} = require('ansis/colors'); const { outToConsole } = require('./utils'); const pluginName = 'remove-empty-scripts'; const infoPluginName = `\n${black.bgYellow`[${pluginName}]`}`; const errorPluginName = `\n${black.bgRed`[${pluginName}]`}`; // Save unique id in dependency object as marker of 'analysed module' // to avoid the infinite recursion by collect of resources. let dependencyId = 1; class WebpackRemoveEmptyScriptsPlugin { outputPath = ''; trash = []; static STAGE_BEFORE_PROCESS_PLUGINS = 100; static STAGE_AFTER_PROCESS_PLUGINS = 200; constructor (options) { this.apply = this.apply.bind(this); this.options = Object.assign({}, defaultOptions, options); this.enabled = this.options.enabled !== false; this.verbose = this.options.verbose; // if by assigned option the `ignore` was not array, then set as array if (!Array.isArray(this.options.ignore)) { this.options.ignore = [this.options.ignore]; } if (Array.isArray(this.options.extensions)) { const pattern = this.options.extensions.map(etx => etx[0] === '.' ? etx.substring(1) : etx).join('|'); // note: the pattern must match a resource with a query, e.g.: style.css?key=val this.options.extensions = new RegExp(`\.(${pattern})([?].*)?$`); } } apply (compiler) { if (!this.enabled) return; // clear cache for webpack dev server this.trash = []; this.outputPath = compiler.options.output.path; compiler.hooks.thisCompilation.tap(pluginName, compilation => { const resourcesCache = []; compilation.hooks.chunkAsset.tap(pluginName, (chunk, filename) => { const { remove: removeAssets, ignore: ignoreEntryResource, extensions: styleExtensionRegexp } = this.options; if (!removeAssets.test(filename)) return; const chunkGraph = compilation.chunkGraph; let entryResources = []; for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) { if (!compilation.modules.has(module)) { throw new Error( `${errorPluginName} Entry module in chunk but not in compilation ${chunk.debugId} ${module.debugId}`, ); } const moduleResources = getEntryResources(compilation, module, resourcesCache); entryResources = entryResources.concat(moduleResources); } const resources = ignoreEntryResource.length > 0 ? entryResources.filter(res => ignoreEntryResource.every(item => !res.match(item))) : entryResources; const isEmptyScript = resources.length > 0 && resources.every(resource => styleExtensionRegexp.test(resource)); if (isEmptyScript) { const stage = this.options.stage; if (this.verbose) { const outputFile = path.join(this.outputPath, filename); outToConsole(`${infoPluginName} remove ${cyan(outputFile)}`); } switch (stage) { case WebpackRemoveEmptyScriptsPlugin.STAGE_BEFORE_PROCESS_PLUGINS: // remove empty script immediately, before other plugins will be called compilation.deleteAsset(filename); break; case WebpackRemoveEmptyScriptsPlugin.STAGE_AFTER_PROCESS_PLUGINS: // add file to trash, which at 'afterProcessAssets' wird cleaned // remove empty script later, after all plugins are called this.trash.push(filename); break; default: throw new Error( `${errorPluginName} Invalid value of config option 'stage': ${gray(stage ? stage.toString() : '')}. `+ `See the option description in README.` ); } } }); // Delete empty scripts only after processing all plugins, // otherwise, by usage some plugins, the necessary files may be not created. compilation.hooks.afterProcessAssets.tap(pluginName, () => { this.trash.forEach(file => compilation.deleteAsset(file)); }); }); } } const defaultOptions = { enabled: true, verbose: false, /** @type {Array<string>|RegExp} */ extensions: ['css', 'scss', 'sass', 'less', 'styl'], ignore: [], remove: /\.(js|mjs)$/, stage: WebpackRemoveEmptyScriptsPlugin.STAGE_BEFORE_PROCESS_PLUGINS, }; function getEntryResources (compilation, module, cache) { const moduleGraph = compilation.moduleGraph, index = moduleGraph.getPreOrderIndex(module), propNameDependencyId = '__webpackRemoveEmptyScriptsUniqueId', resources = []; // the index can be null if (index == null) return resources; // index of module is unique per compilation // module.id can be null, not used here if (cache[index] !== undefined) return cache[index]; if (typeof module.resource === 'string') { const resources = [module.resource]; cache[index] = resources; return resources; } if (module.dependencies) { module.dependencies.forEach(dependency => { let module = moduleGraph.getModule(dependency), originModule = moduleGraph.getParentModule(dependency), nextModule = module || originModule, useNextModule = false; if (!dependency.hasOwnProperty(propNameDependencyId)) { dependency[propNameDependencyId] = dependencyId++; useNextModule = true; } if (nextModule && useNextModule) { const dependencyResources = getEntryResources(compilation, nextModule, cache); for (let i = 0, length = dependencyResources.length; i !== length; i++) { const file = dependencyResources[i]; if (resources.indexOf(file) < 0) resources.push(file); } } }); } if (resources.length > 0) cache[index] = resources; return resources; } module.exports = WebpackRemoveEmptyScriptsPlugin;