webpack-remove-empty-scripts
Version:
Webpack plugin removes empty JavaScript files generated when using styles.
172 lines (137 loc) • 6.01 kB
JavaScript
/**
* 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;