plugins_analyze_circular.js
/**
* @file plugins/analyze/circular.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const { relative } = require("path");
const WpwAnalyzePlugin = require("./base");
const { isNulled } = require("@spmhome/type-utils");
class WpwCircularAnalyzePlugin extends WpwAnalyzePlugin
{
allowAsyncCycles
/**
* @type {RegExp}
*/
include;
/**
* @type {RegExp}
*/
exclude;
/**
* @param {WpwPluginOptions} options Plugin options to be applied
*/
constructor(options)
{
super(options);
this.exclude = /node_modules/;
this.include = new RegExp(this.build.getSrcPath());
// this.include = new RegExp(escapeRegExp(this.build.getSrcPath()));
this.buildOptions = /** @type {WpwCircularAnalyzePluginOptions} */(this.buildOptions);
}
/**
* @override
*/
static create = WpwCircularAnalyzePlugin.wrap.bind(this);
/**
* @override
* @returns {WpwPluginTapOptions<any, any, boolean> | undefined}
*/
onApply()
{
return {
detectCircularDependencies: {
async: false,
forceRun: true,
hook: "compilation",
// stage: "REPORT",
hookCompilation: "optimizeModules",
callback: this.detectCircularDependencies.bind(this)
}
};
}
// /**
// * @param {Iterable<WebpackModule>} modules
// */
// detectCircularDependencies(modules)
// {
// const l = this.hookstart();
// try
// { const fModules = modules.filter(/** @param {WebpackModule & {resource?: never} | WebpackNormalModule} m */
// (m) => m.resource && !this.exclude.test(m.resource) && (!this.include || this.include.test(m.resource))
// );
// fModules.map(/** @param { WebpackNormalModule } m */
// (m) => { const paths = this.isCyclic(m, m, {}); if (paths) { return { mod: m, paths }; }}
// )
// .forEach((/** @type {{ mod: WebpackNormalModule, paths: string[] }}*/d) =>
// { l.write(` detected circular dependency in path sequence ${d.paths.join(" -> ")}`, 1);
// this.addMessage({
// message: d.paths.join(" -> "),
// code: !!this.buildOptions.fail ? SpmhError.Code.ERROR_CIRCULAR_DEP : SpmhError.Code.WARNING_CIRCULAR_DEP
// });
// });
// }
// catch (e)
// { this.addMessage(
// { exception: e,
// message: "an exception was thrown while trying to detect circular module dependencies",
// code: !!this.buildOptions.fail ? SpmhError.Code.ERROR_CIRCULAR_DEP : SpmhError.Code.WARNING_CIRCULAR_DEP
// });
// }
// finally { this.hookdone(); }
// }
// /**
// * @param {WebpackNormalModule} ogMod
// * @param {WebpackNormalModule} curMod
// * @param {Record<number, boolean>} seenModules
// * @returns {string[] | void}
// */
// isCyclic(ogMod, curMod, seenModules)
// {
// const baseDir = this.build.getBasePath(),
// cjsSelfRefDsc = "CommonJsSelfReferenceDependency";
// seenModules[curMod.debugId] = true;
// if (curMod.resource && ogMod.resource)
// {
// for (const d of curMod.dependencies.filter((d) => d.constructor && d.constructor.name !== cjsSelfRefDsc))
// {
// const depModule = this.compilation.moduleGraph ? this.compilation.moduleGraph.getModule(d) : d.module;
// if (!depModule) { continue; }
// if (curMod === depModule) { continue; }
// if (depModule.debugId in seenModules)
// { if (depModule.debugId === ogMod.debugId) {
// return [ relative(baseDir, curMod.resource), relative(baseDir, depModule.resource) ];
// }
// continue;
// }
// const cyc = this.isCyclic(ogMod, depModule, seenModules);
// if (cyc) { cyc.unshift(relative(baseDir, curMod.resource)); return cyc; }
// }
// }
// }
detectCircularDependencies(modules)
{
for (const module of modules)
{ if (!isNulled(module.resource) && !this.exclude.test(module.resource) && this.include.test(module.resource))
{ const maybeCyclicalPathsList = this.isCyclic2(module, module, {}, this.compilation);
if (maybeCyclicalPathsList)
{ this.build.addMessage({
plugin: "analyze|circular",
message: maybeCyclicalPathsList.join(" -> "),
code: this.buildOptions.fail ? this.MsgCode.ERROR_CIRCULAR_DEP : this.MsgCode.WARNING_CIRCULAR_DEP
});
}
}
}
}
isCyclic2(initialModule, currentModule, seenModules, compilation)
{
const cwd = this.build.getBasePath();
seenModules[currentModule.debugId] = true;
if (!currentModule.resource || !initialModule.resource) { return false; }
for (const dependency of currentModule.dependencies)
{ if (dependency.constructor && dependency.constructor.name === this) { continue; }
const depModule = compilation.moduleGraph ?
/* webpack5 */ compilation.moduleGraph.getModule(dependency) :
/* webpack4 */ dependency.module;
if (!depModule) { continue; }
if (!depModule.resource) { continue; } // ignore dependencies that don't have an associated resource
if (this.allowAsyncCycles && dependency.weak) { continue; } // ignore deps that resolve asynchronously
if (currentModule === depModule) { continue; } // the dependency was resolved to the current module
if (depModule.debugId in seenModules) // due to how webpack internals setup deps e.g.
{ if (depModule.debugId === initialModule.debugId) // CommonJsSelfReferenceDependency/ModuleDecoratorDependency
{ return [
relative(cwd, currentModule.resource),
relative(cwd, depModule.resource)
];
} continue; // Found a cycle, but not for this module
}
const maybeCyclicalPathsList = this.isCyclic2(initialModule, depModule, seenModules, this.compilation);
if (maybeCyclicalPathsList) {
maybeCyclicalPathsList.unshift(relative(cwd, currentModule.resource));
return maybeCyclicalPathsList;
}
}
return false;
}
}
module.exports = WpwCircularAnalyzePlugin.create;