index.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. let path = require('path')
  2. let extend = require('util')._extend
  3. let BASE_ERROR = 'Circular dependency detected:\r\n'
  4. let PluginTitle = 'CircularDependencyPlugin'
  5. class CircularDependencyPlugin {
  6. constructor(options) {
  7. this.options = extend({
  8. exclude: new RegExp('$^'),
  9. include: new RegExp('.*'),
  10. failOnError: false,
  11. allowAsyncCycles: false,
  12. onDetected: false,
  13. cwd: process.cwd()
  14. }, options)
  15. }
  16. apply(compiler) {
  17. let plugin = this
  18. let cwd = this.options.cwd
  19. compiler.hooks.compilation.tap(PluginTitle, (compilation) => {
  20. compilation.hooks.optimizeModules.tap(PluginTitle, (modules) => {
  21. if (plugin.options.onStart) {
  22. plugin.options.onStart({ compilation });
  23. }
  24. for (let module of modules) {
  25. const shouldSkip = (
  26. module.resource == null ||
  27. plugin.options.exclude.test(module.resource) ||
  28. !plugin.options.include.test(module.resource)
  29. )
  30. // skip the module if it matches the exclude pattern
  31. if (shouldSkip) {
  32. continue
  33. }
  34. let maybeCyclicalPathsList = this.isCyclic(module, module, {}, compilation)
  35. if (maybeCyclicalPathsList) {
  36. // allow consumers to override all behavior with onDetected
  37. if (plugin.options.onDetected) {
  38. try {
  39. plugin.options.onDetected({
  40. module: module,
  41. paths: maybeCyclicalPathsList,
  42. compilation: compilation
  43. })
  44. } catch(err) {
  45. compilation.errors.push(err)
  46. }
  47. continue
  48. }
  49. // mark warnings or errors on webpack compilation
  50. let error = new Error(BASE_ERROR.concat(maybeCyclicalPathsList.join(' -> ')))
  51. if (plugin.options.failOnError) {
  52. compilation.errors.push(error)
  53. } else {
  54. compilation.warnings.push(error)
  55. }
  56. }
  57. }
  58. if (plugin.options.onEnd) {
  59. plugin.options.onEnd({ compilation });
  60. }
  61. })
  62. })
  63. }
  64. isCyclic(initialModule, currentModule, seenModules, compilation) {
  65. let cwd = this.options.cwd
  66. // Add the current module to the seen modules cache
  67. seenModules[currentModule.debugId] = true
  68. // If the modules aren't associated to resources
  69. // it's not possible to display how they are cyclical
  70. if (!currentModule.resource || !initialModule.resource) {
  71. return false
  72. }
  73. // Iterate over the current modules dependencies
  74. for (let dependency of currentModule.dependencies) {
  75. let depModule = null
  76. if (compilation.moduleGraph) {
  77. // handle getting a module for webpack 5
  78. depModule = compilation.moduleGraph.getModule(dependency)
  79. } else {
  80. // handle getting a module for webpack 4
  81. depModule = dependency.module
  82. }
  83. if (!depModule) { continue }
  84. // ignore dependencies that don't have an associated resource
  85. if (!depModule.resource) { continue }
  86. // ignore dependencies that are resolved asynchronously
  87. if (this.options.allowAsyncCycles && dependency.weak) { continue }
  88. if (depModule.debugId in seenModules) {
  89. if (depModule.debugId === initialModule.debugId) {
  90. // Initial module has a circular dependency
  91. return [
  92. path.relative(cwd, currentModule.resource),
  93. path.relative(cwd, depModule.resource)
  94. ]
  95. }
  96. // Found a cycle, but not for this module
  97. continue
  98. }
  99. let maybeCyclicalPathsList = this.isCyclic(initialModule, depModule, seenModules, compilation)
  100. if (maybeCyclicalPathsList) {
  101. maybeCyclicalPathsList.unshift(path.relative(cwd, currentModule.resource))
  102. return maybeCyclicalPathsList
  103. }
  104. }
  105. return false
  106. }
  107. }
  108. module.exports = CircularDependencyPlugin