Filipe Silva 1f3738b19f feat(@angular/cli): add circular dependency detection
Circular dependencies, like `app.module.ts` importing `app.component.ts` which in turn imports `app.module.ts`, now display a warning during builds:

```
kamik@T460p MINGW64 /d/sandbox/master-project (master)
$ ng build
Hash: 3516b252f4e32d6c5bb8
Time: 8693ms
chunk    {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 160 kB {4} [initial] [rendered]
chunk    {1} main.bundle.js, main.bundle.js.map (main) 5.95 kB {3} [initial] [rendered]
chunk    {2} styles.bundle.js, styles.bundle.js.map (styles) 10.5 kB {4} [initial] [rendered]
chunk    {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 1.88 MB [initial] [rendered]
chunk    {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]

WARNING in Circular dependency detected:
src\app\app.module.ts -> src\app\app.component.ts -> src\app\app.module.ts

WARNING in Circular dependency detected:
src\app\app.component.ts -> src\app\app.module.ts -> src\app\app.component.ts
```

It is important to detect and eliminate circular dependencies because leaving them in might lead to `Maximum call stack size exceeded` errors, or imports being `undefined` at runtime.

To remove these warnings from your project you can factor out the circular dependency into a separate module.

For instance, if module A imports `foo` from module B, and module B imports `bar` from module A, it is enough to extract `foo` into module C.

You can turn off these warnings by running ng set apps.0.hideCircularDependencyWarnings=true. This will add the "hideCircularDependencyWarnings": true value to your .angular-cli.json and disable the warnings.

Fix #6309
Fix #6739
2017-06-27 16:30:30 +01:00

127 lines
3.8 KiB
TypeScript

import * as webpack from 'webpack';
import * as path from 'path';
import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin';
import { extraEntryParser, getOutputHashFormat } from './utils';
import { WebpackConfigOptions } from '../webpack-config';
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
/**
* Enumerate loaders and their dependencies from this file to let the dependency validator
* know they are used.
*
* require('source-map-loader')
* require('raw-loader')
* require('script-loader')
* require('json-loader')
* require('url-loader')
* require('file-loader')
*/
export function getCommonConfig(wco: WebpackConfigOptions) {
const { projectRoot, buildOptions, appConfig } = wco;
const appRoot = path.resolve(projectRoot, appConfig.root);
const nodeModules = path.resolve(projectRoot, 'node_modules');
let extraPlugins: any[] = [];
let extraRules: any[] = [];
let entryPoints: { [key: string]: string[] } = {};
if (appConfig.main) {
entryPoints['main'] = [path.resolve(appRoot, appConfig.main)];
}
if (appConfig.polyfills) {
entryPoints['polyfills'] = [path.resolve(appRoot, appConfig.polyfills)];
}
// determine hashing format
const hashFormat = getOutputHashFormat(buildOptions.outputHashing);
// process global scripts
if (appConfig.scripts.length > 0) {
const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts');
// add entry points and lazy chunks
globalScripts.forEach(script => {
let scriptPath = `script-loader!${script.path}`;
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(scriptPath);
});
}
// process asset entries
if (appConfig.assets) {
extraPlugins.push(new GlobCopyWebpackPlugin({
patterns: appConfig.assets,
globOptions: { cwd: appRoot, dot: true, ignore: '**/.gitkeep' }
}));
}
if (buildOptions.progress) {
extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose, colors: true }));
}
if (buildOptions.sourcemaps) {
extraPlugins.push(new webpack.SourceMapDevToolPlugin({
filename: '[file].map[query]',
moduleFilenameTemplate: '[resource-path]',
fallbackModuleFilenameTemplate: '[resource-path]?[hash]',
sourceRoot: 'webpack:///'
}));
}
if (!appConfig.hideCircularDependencyWarnings) {
extraPlugins.push(new CircularDependencyPlugin({
exclude: /(\\|\/)node_modules(\\|\/)/
}));
}
return {
resolve: {
extensions: ['.ts', '.js'],
modules: ['node_modules', nodeModules],
symlinks: !buildOptions.preserveSymlinks
},
resolveLoader: {
modules: [nodeModules, 'node_modules']
},
context: __dirname,
entry: entryPoints,
output: {
path: path.resolve(projectRoot, buildOptions.outputPath),
publicPath: buildOptions.deployUrl,
filename: `[name]${hashFormat.chunk}.bundle.js`,
chunkFilename: `[id]${hashFormat.chunk}.chunk.js`
},
module: {
rules: [
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` },
{
test: /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|cur|ani)$/,
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
}
].concat(extraRules)
},
plugins: [
new webpack.NoEmitOnErrorsPlugin()
].concat(extraPlugins),
node: {
fs: 'empty',
global: true,
crypto: 'empty',
tls: 'empty',
net: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
}
};
}