Skip to content

Commit

Permalink
Detect and handle webpack module cycles
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechszocs committed Apr 1, 2020
1 parent e0e01c5 commit 9308d44
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/gopath
/Godeps/_workspace/src/github.com/openshift/console
/frontend/.cache-loader
/frontend/.webpack-cycles
/frontend/__coverage__
/frontend/__chrome_browser__
/frontend/**/node_modules
Expand Down
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.9.0",
"@patternfly/patternfly": "2.65.3",
"@patternfly/react-catalog-view-extension": "1.4.11",
"@patternfly/react-charts": "5.2.2",
"@patternfly/react-core": "3.140.11",
"@patternfly/react-table": "2.24.41",
"@patternfly/react-tokens": "2.7.10",
"@patternfly/react-topology": "2.11.27",
"@patternfly/react-virtualized-extension": "1.3.40",
"@patternfly/react-catalog-view-extension": "1.4.11",
"abort-controller": "3.0.0",
"classnames": "2.x",
"core-js": "2.x",
Expand Down Expand Up @@ -165,7 +165,7 @@
"cache-loader": "1.x",
"chalk": "2.3.x",
"chromedriver": "77.x",
"circular-dependency-plugin": "5.0.2",
"circular-dependency-plugin": "5.x",
"css-loader": "0.28.x",
"enzyme": "3.10.x",
"enzyme-adapter-react-16": "1.15.2",
Expand All @@ -182,6 +182,7 @@
"jest": "21.x",
"jest-cli": "21.x",
"mini-css-extract-plugin": "0.4.x",
"moment": "2.22.x",
"monaco-editor-core": "0.14.0",
"monaco-editor-webpack-plugin": "^1.7.0",
"node-sass": "4.13.x",
Expand Down
92 changes: 92 additions & 0 deletions frontend/webpack.circular-deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-env node */
import * as webpack from 'webpack';
import * as path from 'path';
import * as fs from 'fs';
import * as moment from 'moment';
import * as CircularDependencyPlugin from 'circular-dependency-plugin';

type PresetOptions = {
exclude: RegExp;
maxCyclesAllowed: number;
reportFile: string;
};

type DetectedCycle = {
// webpack module record that caused the cycle
causedBy: string;
// relative module paths that make up the cycle
modulePaths: string[];
};

export class CircularDependencyPreset {
private readonly HandleCyclesPluginName = 'HandleCyclesPlugin';

constructor(private readonly options: PresetOptions) {}

private getCycleReport(cycles: DetectedCycle[], compilation: webpack.compilation.Compilation) {
const hash = compilation.getStats().hash;
const builtAt = moment(compilation.getStats().endTime).format('MM/DD/YYYY HH:mm:ss');

const countByDir = cycles
.map((c) => c.modulePaths[0].replace(/\/.*$/, ''))
.reduce((acc, dir) => {
acc[dir] = (acc[dir] ?? 0) + 1;
return acc;
}, {} as { [key: string]: number });

const header =
`# webpack compilation ${hash} built at ${builtAt}\n` +
'# this file is auto-generated on every webpack development build\n';

const stats =
`# ${cycles.length} total cycles: ${Object.keys(countByDir)
.map((d) => `${d} (${countByDir[d]})`)
.join(', ')}\n` +
`# ${
cycles.filter((c) => c.modulePaths.length === 3).length
} minimal-length cycles (A -> B -> A)\n`;

const entries = cycles.map((c) => `${c.causedBy}\n${c.modulePaths.join('\n-> ')}\n`).join('\n');

return [header, stats, entries].join('\n');
}

apply(plugins: webpack.Plugin[]) {
const cycles: DetectedCycle[] = [];

plugins.push(
new CircularDependencyPlugin({
exclude: this.options.exclude,
onDetected: ({ module: { resource }, paths: modulePaths }) => {
cycles.push({ causedBy: resource, modulePaths });
},
}),
{
// Ad-hoc plugin to handle detected module cycle information
apply: (compiler) => {
compiler.hooks.emit.tap(this.HandleCyclesPluginName, (compilation) => {
if (cycles.length === 0) {
return;
} else {
console.log(`detected ${cycles.length} cycles`);
}

const maxCycles = this.options.maxCyclesAllowed;
const reportPath = path.resolve(__dirname, this.options.reportFile);

fs.writeFileSync(reportPath, this.getCycleReport(cycles, compilation));
console.log(`module cycle report written to ${reportPath}`);

if (cycles.length > maxCycles) {
compilation.errors.push(
new Error(
`${this.HandleCyclesPluginName}: number of cycles (${cycles.length}) exceeds configured threshold (${maxCycles})`,
),
);
}
});
},
},
);
}
}
14 changes: 12 additions & 2 deletions frontend/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import * as VirtualModulesPlugin from 'webpack-virtual-modules';

import { resolvePluginPackages, getActivePluginsModule } from '@console/plugin-sdk/src/codegen';
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import { CircularDependencyPreset } from './webpack.circular-deps';

interface Configuration extends webpack.Configuration {
devServer?: WebpackDevServerConfiguration;
}

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

const NODE_ENV = process.env.NODE_ENV;
const HOT_RELOAD = process.env.HOT_RELOAD;
const NODE_ENV = process.env.NODE_ENV || 'development';
const HOT_RELOAD = process.env.HOT_RELOAD || 'false';

/* Helpers */
const extractCSS = new MiniCssExtractPlugin({ filename: 'app-bundle.[contenthash].css' });
Expand Down Expand Up @@ -163,6 +164,15 @@ const config: Configuration = {
stats: 'minimal',
};

/* Development settings */
if (NODE_ENV === 'development') {
new CircularDependencyPreset({
exclude: /node_modules|public\/dist/,
maxCyclesAllowed: 168, // Try to keep this as low as possible
reportFile: '.webpack-cycles',
}).apply(config.plugins);
}

/* Production settings */
if (NODE_ENV === 'production') {
config.output.filename = '[name]-bundle-[hash].min.js';
Expand Down
10 changes: 5 additions & 5 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4783,10 +4783,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"

circular-dependency-plugin@5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz#da168c0b37e7b43563fb9f912c1c007c213389ef"
integrity sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==
circular-dependency-plugin@5.x:
version "5.2.0"
resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz#e09dbc2dd3e2928442403e2d45b41cea06bc0a93"
integrity sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==

clap@^1.0.9:
version "1.2.3"
Expand Down Expand Up @@ -11259,7 +11259,7 @@ moment-timezone@^0.4.0, moment-timezone@^0.4.1:
dependencies:
moment ">= 2.6.0"

"moment@>= 2.6.0", moment@^2.10, moment@^2.19.1:
moment@2.22.x, "moment@>= 2.6.0", moment@^2.10, moment@^2.19.1:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"

Expand Down

0 comments on commit 9308d44

Please sign in to comment.