├── .nvmrc ├── __tests__ ├── deps │ ├── nocycle.js │ ├── a.js │ ├── b.js │ ├── c.js │ ├── d.js │ ├── e.js │ ├── f.js │ ├── g.js │ ├── h.js │ ├── context │ │ └── a.js │ ├── self-referencing │ │ ├── uses-this.js │ │ ├── uses-exports.js │ │ └── imports-self.js │ ├── ts │ │ ├── b.tsx │ │ ├── a.tsx │ │ └── tsconfig.json │ ├── i.js │ └── module-concat-plugin-compat │ │ ├── index.js │ │ ├── a.js │ │ └── b.js └── index.test.js ├── .gitignore ├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.3.0 2 | -------------------------------------------------------------------------------- /__tests__/deps/nocycle.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /__tests__/deps/a.js: -------------------------------------------------------------------------------- 1 | require('./b'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/b.js: -------------------------------------------------------------------------------- 1 | require('./c'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/c.js: -------------------------------------------------------------------------------- 1 | require('./b'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/d.js: -------------------------------------------------------------------------------- 1 | require('./e'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/e.js: -------------------------------------------------------------------------------- 1 | require('./f'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/f.js: -------------------------------------------------------------------------------- 1 | require('./g'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/g.js: -------------------------------------------------------------------------------- 1 | require('./e'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/h.js: -------------------------------------------------------------------------------- 1 | require('./i'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/context/a.js: -------------------------------------------------------------------------------- 1 | require('../i'); 2 | -------------------------------------------------------------------------------- /__tests__/deps/self-referencing/uses-this.js: -------------------------------------------------------------------------------- 1 | this; 2 | -------------------------------------------------------------------------------- /__tests__/deps/ts/b.tsx: -------------------------------------------------------------------------------- 1 | let b = 1; 2 | export default b; 3 | -------------------------------------------------------------------------------- /__tests__/deps/i.js: -------------------------------------------------------------------------------- 1 | require.context('./context/', false, /.*/); 2 | -------------------------------------------------------------------------------- /__tests__/deps/self-referencing/uses-exports.js: -------------------------------------------------------------------------------- 1 | module.exports = 1; 2 | -------------------------------------------------------------------------------- /__tests__/deps/ts/a.tsx: -------------------------------------------------------------------------------- 1 | import b from './b'; 2 | 3 | let a = 1; 4 | module.exports = a; 5 | -------------------------------------------------------------------------------- /__tests__/deps/module-concat-plugin-compat/index.js: -------------------------------------------------------------------------------- 1 | import a from './a' 2 | import b from './b' 3 | -------------------------------------------------------------------------------- /__tests__/deps/module-concat-plugin-compat/a.js: -------------------------------------------------------------------------------- 1 | import b from './b' 2 | 3 | var a = { name: 'a', dep: b && b.name } 4 | 5 | export default a 6 | -------------------------------------------------------------------------------- /__tests__/deps/module-concat-plugin-compat/b.js: -------------------------------------------------------------------------------- 1 | import a from './a' 2 | 3 | var b = { name: 'b', dep: a && a.name } 4 | 5 | export default b 6 | -------------------------------------------------------------------------------- /__tests__/deps/self-referencing/imports-self.js: -------------------------------------------------------------------------------- 1 | let own = require('./imports-self') 2 | let a = 1; 3 | console.log(a, own); 4 | module.exports = a; 5 | -------------------------------------------------------------------------------- /__tests__/deps/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Use Node.js 10.x 11 | uses: actions/setup-node@v1 12 | with: 13 | version: 10.x 14 | - name: npm install and test 15 | run: | 16 | npm install 17 | npm test 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Aaron Ackerman 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circular-dependency-plugin", 3 | "main": "index.js", 4 | "devDependencies": { 5 | "jest": "^22.4.2", 6 | "memory-fs": "^0.4.1", 7 | "ts-loader": "^8.0.7", 8 | "typescript": "^4.0.5", 9 | "webpack": "^4.0.1", 10 | "webpack5": "npm:webpack@^5.0.0" 11 | }, 12 | "peerDependencies": { 13 | "webpack": ">=4.0.1" 14 | }, 15 | "description": "Detect modules with circular dependencies when bundling with webpack.", 16 | "version": "5.2.2", 17 | "engines": { 18 | "node": ">=6.0.0" 19 | }, 20 | "scripts": { 21 | "test": "jest" 22 | }, 23 | "files": [ 24 | "index.js" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/aackerman/circular-dependency-plugin.git" 29 | }, 30 | "author": "", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/aackerman/circular-dependency-plugin/issues" 34 | }, 35 | "homepage": "https://github.com/aackerman/circular-dependency-plugin#readme", 36 | "jest": { 37 | "testMatch": [ 38 | "**/?(*.)(spec|test).js?(x)" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.2.2 4 | 5 | * Fixed an issue where typescript modules were identified as having a circular dependency on themselves in Webpack 5 6 | 7 | ## 5.2.1 8 | 9 | * Fixed an issue where modules that do not include themselves were marked as having circular dependencies 10 | 11 | ## 5.2.0 12 | 13 | * Webpack 5 compatibility 14 | 15 | ## 5.1.0 16 | 17 | * Added `include` option to allow checking only certain directories 18 | 19 | ## 5.0.1 20 | 21 | * Set webpack peer dependency to greater than `4.0.1` 22 | 23 | ## 5.0.0 24 | 25 | * Support for webpack 4 26 | * Exclude logic will now be checked regardless of presence of `onDetected` method 27 | 28 | ## 4.4.0 29 | 30 | * Added `onStart` and `onEnd` callbacks 31 | 32 | ## 4.3.0 33 | 34 | * Added `cwd` parameter to allow setting the current working directory for displaying module paths 35 | 36 | ## 4.2.0 37 | 38 | * The webpack module record is now passed into the `onDetected` callback 39 | 40 | ## 4.1.0 41 | 42 | * Added support for the `ModuleConcatenationPlugin` from webpack 43 | 44 | ## 4.0.0 45 | 46 | * Dropped support for Node 4.x 47 | 48 | ## 3.0.0 49 | 50 | * Started using Error objects instead of plain strings for webpack compilation warnings/errors 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Circular Dependency Plugin 2 | 3 | Detect modules with circular dependencies when bundling with webpack. 4 | 5 | Circular dependencies are often a necessity in complex software, the presence of a circular dependency doesn't always imply a bug, but in the case where you believe a bug exists, this module may help find it. 6 | 7 | ### Webpack Versions 8 | 9 | The latest major version of this plugin `5`, supports webpack `4.0.1` and greater as a peer dependency. Major version `4` of this plugin and below are intended to support webpack `3.x.x` and below as a peer dependency. 10 | 11 | ### Basic Usage 12 | 13 | ```js 14 | // webpack.config.js 15 | const CircularDependencyPlugin = require('circular-dependency-plugin') 16 | 17 | module.exports = { 18 | entry: "./src/index", 19 | plugins: [ 20 | new CircularDependencyPlugin({ 21 | // exclude detection of files based on a RegExp 22 | exclude: /a\.js|node_modules/, 23 | // include specific files based on a RegExp 24 | include: /dir/, 25 | // add errors to webpack instead of warnings 26 | failOnError: true, 27 | // allow import cycles that include an asyncronous import, 28 | // e.g. via import(/* webpackMode: "weak" */ './file.js') 29 | allowAsyncCycles: false, 30 | // set the current working directory for displaying module paths 31 | cwd: process.cwd(), 32 | }) 33 | ] 34 | } 35 | ``` 36 | 37 | ### Advanced Usage 38 | 39 | ```js 40 | // webpack.config.js 41 | const CircularDependencyPlugin = require('circular-dependency-plugin') 42 | 43 | module.exports = { 44 | entry: "./src/index", 45 | plugins: [ 46 | new CircularDependencyPlugin({ 47 | // `onStart` is called before the cycle detection starts 48 | onStart({ compilation }) { 49 | console.log('start detecting webpack modules cycles'); 50 | }, 51 | // `onDetected` is called for each module that is cyclical 52 | onDetected({ module: webpackModuleRecord, paths, compilation }) { 53 | // `paths` will be an Array of the relative module paths that make up the cycle 54 | // `module` will be the module record generated by webpack that caused the cycle 55 | compilation.errors.push(new Error(paths.join(' -> '))) 56 | }, 57 | // `onEnd` is called before the cycle detection ends 58 | onEnd({ compilation }) { 59 | console.log('end detecting webpack modules cycles'); 60 | }, 61 | }) 62 | ] 63 | } 64 | ``` 65 | 66 | If you have some number of cycles and want to fail if any new ones are 67 | introduced, you can use the life cycle methods to count and fail when the 68 | count is exceeded. (Note if you care about detecting a cycle being replaced by 69 | another, this won't catch that.) 70 | 71 | ```js 72 | // webpack.config.js 73 | const CircularDependencyPlugin = require('circular-dependency-plugin') 74 | 75 | const MAX_CYCLES = 5; 76 | let numCyclesDetected = 0; 77 | 78 | module.exports = { 79 | entry: "./src/index", 80 | plugins: [ 81 | new CircularDependencyPlugin({ 82 | onStart({ compilation }) { 83 | numCyclesDetected = 0; 84 | }, 85 | onDetected({ module: webpackModuleRecord, paths, compilation }) { 86 | numCyclesDetected++; 87 | compilation.warnings.push(new Error(paths.join(' -> '))) 88 | }, 89 | onEnd({ compilation }) { 90 | if (numCyclesDetected > MAX_CYCLES) { 91 | compilation.errors.push(new Error( 92 | `Detected ${numCyclesDetected} cycles which exceeds configured limit of ${MAX_CYCLES}` 93 | )); 94 | } 95 | }, 96 | }) 97 | ] 98 | } 99 | ``` 100 | 101 | ### Maintenance 102 | 103 | This module is maintained despite few changes being necessary, please open issues if you find any bugs relating to integration with webpack core. 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 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 | 6 | class CircularDependencyPlugin { 7 | constructor(options) { 8 | this.options = extend({ 9 | exclude: new RegExp('$^'), 10 | include: new RegExp('.*'), 11 | failOnError: false, 12 | allowAsyncCycles: false, 13 | onDetected: false, 14 | cwd: process.cwd() 15 | }, options) 16 | } 17 | 18 | apply(compiler) { 19 | let plugin = this 20 | let cwd = this.options.cwd 21 | 22 | compiler.hooks.compilation.tap(PluginTitle, (compilation) => { 23 | compilation.hooks.optimizeModules.tap(PluginTitle, (modules) => { 24 | if (plugin.options.onStart) { 25 | plugin.options.onStart({ compilation }); 26 | } 27 | for (let module of modules) { 28 | const shouldSkip = ( 29 | module.resource == null || 30 | plugin.options.exclude.test(module.resource) || 31 | !plugin.options.include.test(module.resource) 32 | ) 33 | // skip the module if it matches the exclude pattern 34 | if (shouldSkip) { 35 | continue 36 | } 37 | 38 | let maybeCyclicalPathsList = this.isCyclic(module, module, {}, compilation) 39 | if (maybeCyclicalPathsList) { 40 | // allow consumers to override all behavior with onDetected 41 | if (plugin.options.onDetected) { 42 | try { 43 | plugin.options.onDetected({ 44 | module: module, 45 | paths: maybeCyclicalPathsList, 46 | compilation: compilation 47 | }) 48 | } catch(err) { 49 | compilation.errors.push(err) 50 | } 51 | continue 52 | } 53 | 54 | // mark warnings or errors on webpack compilation 55 | let error = new Error(BASE_ERROR.concat(maybeCyclicalPathsList.join(' -> '))) 56 | if (plugin.options.failOnError) { 57 | compilation.errors.push(error) 58 | } else { 59 | compilation.warnings.push(error) 60 | } 61 | } 62 | } 63 | if (plugin.options.onEnd) { 64 | plugin.options.onEnd({ compilation }); 65 | } 66 | }) 67 | }) 68 | } 69 | 70 | isCyclic(initialModule, currentModule, seenModules, compilation) { 71 | let cwd = this.options.cwd 72 | 73 | // Add the current module to the seen modules cache 74 | seenModules[currentModule.debugId] = true 75 | 76 | // If the modules aren't associated to resources 77 | // it's not possible to display how they are cyclical 78 | if (!currentModule.resource || !initialModule.resource) { 79 | return false 80 | } 81 | 82 | // Iterate over the current modules dependencies 83 | for (let dependency of currentModule.dependencies) { 84 | if ( 85 | dependency.constructor && 86 | dependency.constructor.name === 'CommonJsSelfReferenceDependency' 87 | ) { 88 | continue 89 | } 90 | 91 | let depModule = null 92 | if (compilation.moduleGraph) { 93 | // handle getting a module for webpack 5 94 | depModule = compilation.moduleGraph.getModule(dependency) 95 | } else { 96 | // handle getting a module for webpack 4 97 | depModule = dependency.module 98 | } 99 | 100 | if (!depModule) { continue } 101 | // ignore dependencies that don't have an associated resource 102 | if (!depModule.resource) { continue } 103 | // ignore dependencies that are resolved asynchronously 104 | if (this.options.allowAsyncCycles && dependency.weak) { continue } 105 | // the dependency was resolved to the current module due to how webpack internals 106 | // setup dependencies like CommonJsSelfReferenceDependency and ModuleDecoratorDependency 107 | if (currentModule === depModule) { 108 | continue 109 | } 110 | 111 | if (depModule.debugId in seenModules) { 112 | if (depModule.debugId === initialModule.debugId) { 113 | // Initial module has a circular dependency 114 | return [ 115 | path.relative(cwd, currentModule.resource), 116 | path.relative(cwd, depModule.resource) 117 | ] 118 | } 119 | // Found a cycle, but not for this module 120 | continue 121 | } 122 | 123 | let maybeCyclicalPathsList = this.isCyclic(initialModule, depModule, seenModules, compilation) 124 | if (maybeCyclicalPathsList) { 125 | maybeCyclicalPathsList.unshift(path.relative(cwd, currentModule.resource)) 126 | return maybeCyclicalPathsList 127 | } 128 | } 129 | 130 | return false 131 | } 132 | } 133 | 134 | module.exports = CircularDependencyPlugin 135 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let MemoryFS = require("memory-fs") 3 | let CircularDependencyPlugin = require('../index') 4 | 5 | let wrapRun = (run) => { 6 | return () => new Promise((resolve, reject) => { 7 | run((err, result) => { 8 | if (err) { return reject(err) } 9 | return resolve(result.toJson()) 10 | }) 11 | }) 12 | } 13 | 14 | let versions = [{ 15 | name: 'webpack', 16 | module: require('webpack'), 17 | }, { 18 | name: 'webpack5', 19 | module: require('webpack5'), 20 | }] 21 | 22 | let getWarningMessage = (stats, index) => { 23 | return getStatsMessage(stats, index, 'warnings') 24 | } 25 | 26 | let getErrorsMessage = (stats, index) => { 27 | return getStatsMessage(stats, index, 'errors') 28 | } 29 | 30 | let getStatsMessage = (stats, index, type) => { 31 | if (stats[type][index] == null) { 32 | return null 33 | } else if (stats[type][index].message) { 34 | // handle webpack 5 35 | return stats[type][index].message 36 | } else { 37 | // handle webpack 4 38 | return stats[type][index] 39 | } 40 | } 41 | 42 | for (let version of versions) { 43 | let webpack = version.module 44 | 45 | describe(`circular dependency ${version.name}`, () => { 46 | it('detects circular dependencies from a -> b -> c -> b', async () => { 47 | let fs = new MemoryFS() 48 | let compiler = webpack({ 49 | mode: 'development', 50 | entry: path.join(__dirname, 'deps/a.js'), 51 | output: { path: __dirname }, 52 | plugins: [ new CircularDependencyPlugin() ] 53 | }) 54 | compiler.outputFileSystem = fs 55 | 56 | let runAsync = wrapRun(compiler.run.bind(compiler)) 57 | let stats = await runAsync() 58 | 59 | let msg0 = getWarningMessage(stats, 0) 60 | let msg1 = getWarningMessage(stats, 1) 61 | expect(msg0).toContain('__tests__/deps/b.js -> __tests__/deps/c.js -> __tests__/deps/b.js') 62 | expect(msg0).toMatch(/Circular/) 63 | expect(msg1).toMatch(/b\.js/) 64 | expect(msg1).toMatch(/c\.js/) 65 | }) 66 | 67 | it('detects circular dependencies from d -> e -> f -> g -> e', async () => { 68 | let fs = new MemoryFS() 69 | let compiler = webpack({ 70 | mode: 'development', 71 | entry: path.join(__dirname, 'deps/d.js'), 72 | output: { path: __dirname }, 73 | plugins: [ new CircularDependencyPlugin() ] 74 | }) 75 | compiler.outputFileSystem = fs 76 | 77 | let runAsync = wrapRun(compiler.run.bind(compiler)) 78 | let stats = await runAsync() 79 | 80 | let msg0 = getWarningMessage(stats, 0) 81 | let msg1 = getWarningMessage(stats, 1) 82 | expect(msg0).toContain('__tests__/deps/e.js -> __tests__/deps/f.js -> __tests__/deps/g.js -> __tests__/deps/e.js') 83 | expect(msg0).toMatch(/Circular/) 84 | expect(msg1).toMatch(/e\.js/) 85 | expect(msg1).toMatch(/f\.js/) 86 | expect(msg1).toMatch(/g\.js/) 87 | }) 88 | 89 | it('uses errors instead of warnings with failOnError', async () => { 90 | let fs = new MemoryFS() 91 | let compiler = webpack({ 92 | mode: 'development', 93 | entry: path.join(__dirname, 'deps/d.js'), 94 | output: { path: __dirname }, 95 | plugins: [ new CircularDependencyPlugin({ 96 | failOnError: true 97 | }) ] 98 | }) 99 | compiler.outputFileSystem = fs 100 | 101 | let runAsync = wrapRun(compiler.run.bind(compiler)) 102 | let stats = await runAsync() 103 | 104 | let err0 = getErrorsMessage(stats, 0) 105 | let err1 = getErrorsMessage(stats, 1) 106 | expect(err0).toContain('__tests__/deps/e.js -> __tests__/deps/f.js -> __tests__/deps/g.js -> __tests__/deps/e.js') 107 | expect(err0).toMatch(/Circular/) 108 | expect(err1).toMatch(/e\.js/) 109 | expect(err1).toMatch(/f\.js/) 110 | expect(err1).toMatch(/g\.js/) 111 | }) 112 | 113 | it('can exclude cyclical deps from being output', async () => { 114 | let fs = new MemoryFS() 115 | let compiler = webpack({ 116 | mode: 'development', 117 | entry: path.join(__dirname, 'deps/d.js'), 118 | output: { path: __dirname }, 119 | plugins: [ 120 | new CircularDependencyPlugin({ 121 | exclude: /f\.js/ 122 | }) 123 | ] 124 | }) 125 | compiler.outputFileSystem = fs 126 | 127 | let runAsync = wrapRun(compiler.run.bind(compiler)) 128 | let stats = await runAsync() 129 | 130 | let msg0 = getWarningMessage(stats, 0) 131 | let msg1 = getWarningMessage(stats, 1) 132 | expect(msg0).toMatch(/Circular/) 133 | expect(msg1).toMatch(/e\.js/) 134 | expect(msg1).toMatch(/g\.js/) 135 | }) 136 | 137 | it('can include only specific cyclical deps in the output', async () => { 138 | let fs = new MemoryFS() 139 | let compiler = webpack({ 140 | mode: 'development', 141 | entry: path.join(__dirname, 'deps/d.js'), 142 | output: { path: __dirname }, 143 | plugins: [ 144 | new CircularDependencyPlugin({ 145 | include: /f\.js/ 146 | }) 147 | ] 148 | }) 149 | compiler.outputFileSystem = fs 150 | 151 | let runAsync = wrapRun(compiler.run.bind(compiler)) 152 | let stats = await runAsync() 153 | stats.warnings.forEach(warning => { 154 | let msg = typeof warning == 'string' ? warning : warning.message 155 | const firstFile = msg.match(/\w+\.js/)[0] 156 | expect(firstFile).toMatch(/f\.js/) 157 | }) 158 | }) 159 | 160 | it(`can handle context modules that have an undefined resource h -> i -> a -> i`, async () => { 161 | let fs = new MemoryFS() 162 | let compiler = webpack({ 163 | mode: 'development', 164 | entry: path.join(__dirname, 'deps/h.js'), 165 | output: { path: __dirname }, 166 | plugins: [ 167 | new CircularDependencyPlugin() 168 | ] 169 | }) 170 | compiler.outputFileSystem = fs 171 | 172 | let runAsync = wrapRun(compiler.run.bind(compiler)) 173 | let stats = await runAsync() 174 | expect(stats.warnings.length).toEqual(0) 175 | expect(stats.errors.length).toEqual(0) 176 | }) 177 | 178 | it('allows hooking into detection cycle', async () => { 179 | let fs = new MemoryFS() 180 | let compiler = webpack({ 181 | mode: 'development', 182 | entry: path.join(__dirname, 'deps/nocycle.js'), 183 | output: { path: __dirname }, 184 | plugins: [ 185 | new CircularDependencyPlugin({ 186 | onStart({ compilation }) { 187 | compilation.warnings.push('started'); 188 | }, 189 | onEnd({ compilation }) { 190 | compilation.errors.push('ended'); 191 | } 192 | }) 193 | ] 194 | }) 195 | compiler.outputFileSystem = fs 196 | 197 | let runAsync = wrapRun(compiler.run.bind(compiler)) 198 | let stats = await runAsync() 199 | 200 | if (/^5/.test(webpack.version)) { 201 | expect(stats.warnings).toEqual([{ message: 'started' }]) 202 | expect(stats.errors).toEqual([{ message: 'ended' }]) 203 | } else { 204 | expect(stats.warnings).toEqual(['started']) 205 | expect(stats.errors).toEqual(['ended']) 206 | } 207 | }) 208 | 209 | 210 | it('allows overriding all behavior with onDetected', async () => { 211 | let cyclesPaths 212 | let fs = new MemoryFS() 213 | let compiler = webpack({ 214 | mode: 'development', 215 | entry: path.join(__dirname, 'deps/d.js'), 216 | output: { path: __dirname }, 217 | plugins: [ 218 | new CircularDependencyPlugin({ 219 | onDetected({ paths }) { 220 | cyclesPaths = paths 221 | } 222 | }) 223 | ] 224 | }) 225 | compiler.outputFileSystem = fs 226 | 227 | let runAsync = wrapRun(compiler.run.bind(compiler)) 228 | await runAsync() 229 | expect(cyclesPaths).toEqual([ 230 | '__tests__/deps/g.js', 231 | '__tests__/deps/e.js', 232 | '__tests__/deps/f.js', 233 | '__tests__/deps/g.js' 234 | ]) 235 | }) 236 | 237 | it('detects circular dependencies from d -> e -> f -> g -> e', async () => { 238 | let fs = new MemoryFS() 239 | let compiler = webpack({ 240 | mode: 'development', 241 | entry: path.join(__dirname, 'deps/d.js'), 242 | output: { path: __dirname }, 243 | plugins: [ 244 | new CircularDependencyPlugin({ 245 | onDetected({ paths, compilation }) { 246 | compilation.warnings.push(paths.join(' -> ')) 247 | } 248 | }) 249 | ] 250 | }) 251 | compiler.outputFileSystem = fs 252 | 253 | let runAsync = wrapRun(compiler.run.bind(compiler)) 254 | let stats = await runAsync() 255 | 256 | let msg0 = getWarningMessage(stats, 0) 257 | let msg1 = getWarningMessage(stats, 1) 258 | expect(msg0).toContain('__tests__/deps/e.js -> __tests__/deps/f.js -> __tests__/deps/g.js -> __tests__/deps/e.js') 259 | expect(msg1).toMatch(/e\.js/) 260 | expect(msg1).toMatch(/f\.js/) 261 | expect(msg1).toMatch(/g\.js/) 262 | }) 263 | 264 | it('can detect circular dependencies when the ModuleConcatenationPlugin is used', async () => { 265 | let fs = new MemoryFS() 266 | let compiler = webpack({ 267 | mode: 'development', 268 | entry: path.join(__dirname, 'deps/module-concat-plugin-compat/index.js'), 269 | output: { path: __dirname }, 270 | plugins: [ 271 | new webpack.optimize.ModuleConcatenationPlugin(), 272 | new CircularDependencyPlugin() 273 | ] 274 | }) 275 | compiler.outputFileSystem = fs 276 | 277 | let runAsync = wrapRun(compiler.run.bind(compiler)) 278 | let stats = await runAsync() 279 | 280 | let msg0 = getWarningMessage(stats, 0) 281 | let msg1 = getWarningMessage(stats, 1) 282 | expect(msg0).toContain('__tests__/deps/module-concat-plugin-compat/a.js -> __tests__/deps/module-concat-plugin-compat/b.js -> __tests__/deps/module-concat-plugin-compat/a.js') 283 | expect(msg1).toContain('__tests__/deps/module-concat-plugin-compat/b.js -> __tests__/deps/module-concat-plugin-compat/a.js -> __tests__/deps/module-concat-plugin-compat/b.js') 284 | }) 285 | 286 | describe('ignores self referencing webpack internal dependencies', () => { 287 | it('ignores this references', async () => { 288 | let fs = new MemoryFS() 289 | let compiler = webpack({ 290 | mode: 'development', 291 | entry: path.join(__dirname, 'deps', 'self-referencing', 'uses-this.js'), 292 | output: { path: __dirname }, 293 | plugins: [ new CircularDependencyPlugin() ] 294 | }) 295 | compiler.outputFileSystem = fs 296 | 297 | let runAsync = wrapRun(compiler.run.bind(compiler)) 298 | let stats = await runAsync() 299 | 300 | expect(stats.errors.length).toEqual(0) 301 | expect(stats.warnings.length).toEqual(0) 302 | }) 303 | 304 | it('ignores module.exports references', async () => { 305 | let fs = new MemoryFS() 306 | let compiler = webpack({ 307 | mode: 'development', 308 | entry: path.join(__dirname, 'deps', 'self-referencing', 'uses-exports.js'), 309 | output: { path: __dirname }, 310 | plugins: [ new CircularDependencyPlugin() ] 311 | }) 312 | compiler.outputFileSystem = fs 313 | 314 | let runAsync = wrapRun(compiler.run.bind(compiler)) 315 | let stats = await runAsync() 316 | 317 | expect(stats.errors.length).toEqual(0) 318 | expect(stats.warnings.length).toEqual(0) 319 | }) 320 | 321 | it('ignores self references', async () => { 322 | let fs = new MemoryFS() 323 | let compiler = webpack({ 324 | mode: 'development', 325 | entry: path.join(__dirname, 'deps', 'self-referencing', 'imports-self.js'), 326 | output: { path: __dirname }, 327 | plugins: [ new CircularDependencyPlugin() ] 328 | }) 329 | compiler.outputFileSystem = fs 330 | 331 | let runAsync = wrapRun(compiler.run.bind(compiler)) 332 | let stats = await runAsync() 333 | 334 | expect(stats.warnings.length).toEqual(0) 335 | expect(stats.errors.length).toEqual(0) 336 | }) 337 | 338 | it('works with typescript', async () => { 339 | let fs = new MemoryFS() 340 | let compiler = webpack({ 341 | mode: 'development', 342 | entry: path.join(__dirname, 'deps', 'ts', 'a.tsx'), 343 | output: { path: __dirname }, 344 | module: { 345 | rules: [ 346 | { 347 | test: /\.tsx?$/, 348 | use: [{ 349 | loader: 'ts-loader', 350 | options: { 351 | configFile: path.resolve(path.join(__dirname, 'deps', 'ts', 'tsconfig.json')), 352 | }, 353 | }], 354 | exclude: /node_modules/, 355 | }, 356 | ], 357 | }, 358 | plugins: [ new CircularDependencyPlugin() ] 359 | }) 360 | compiler.outputFileSystem = fs 361 | 362 | let runAsync = wrapRun(compiler.run.bind(compiler)) 363 | let stats = await runAsync() 364 | 365 | expect(stats.errors.length).toEqual(0) 366 | expect(stats.warnings.length).toEqual(0) 367 | }) 368 | }) 369 | }) 370 | } 371 | 372 | --------------------------------------------------------------------------------