/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { isAbsolute } from 'path'; import { Compilation, Compiler, Dependency, Module, NormalModule } from 'webpack'; import { addWarning } from '../../../utils/webpack-diagnostics'; // Webpack doesn't export these so the deep imports can potentially break. const AMDDefineDependency = require('webpack/lib/dependencies/AMDDefineDependency'); const CommonJsExportsDependency = require('webpack/lib/dependencies/CommonJsExportsDependency'); const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency'); const CommonJsSelfReferenceDependency = require('webpack/lib/dependencies/CommonJsSelfReferenceDependency'); export interface CommonJsUsageWarnPluginOptions { /** A list of CommonJS or AMD packages that are allowed to be used without a warning. Use `'*'` to allow all. */ allowedDependencies?: string[]; } export class CommonJsUsageWarnPlugin { private shownWarnings = new Set<string>(); private allowedDependencies: Set<string>; constructor(private options: CommonJsUsageWarnPluginOptions = {}) { this.allowedDependencies = new Set(this.options.allowedDependencies); } apply(compiler: Compiler) { if (this.allowedDependencies.has('*')) { return; } compiler.hooks.compilation.tap('CommonJsUsageWarnPlugin', (compilation) => { compilation.hooks.finishModules.tap('CommonJsUsageWarnPlugin', (modules) => { const mainEntry = compilation.entries.get('main'); if (!mainEntry) { return; } const mainModules = new Set( mainEntry.dependencies.map((dep) => compilation.moduleGraph.getModule(dep)), ); for (const module of modules) { const { dependencies, rawRequest } = module as NormalModule; if ( !rawRequest || rawRequest.startsWith('.') || isAbsolute(rawRequest) || this.allowedDependencies.has(rawRequest) || this.allowedDependencies.has(this.rawRequestToPackageName(rawRequest)) || rawRequest.startsWith('@angular/common/locales/') ) { /** * Skip when: * - module is absolute or relative. * - module is allowed even if it's a CommonJS. * - module is a locale imported from '@angular/common'. */ continue; } if (this.hasCommonJsDependencies(compilation, dependencies)) { // Dependency is CommonsJS or AMD. const issuer = getIssuer(compilation, module); // Check if it's parent issuer is also a CommonJS dependency. // In case it is skip as an warning will be show for the parent CommonJS dependency. const parentDependencies = getIssuer(compilation, issuer)?.dependencies; if ( parentDependencies && this.hasCommonJsDependencies(compilation, parentDependencies, true) ) { continue; } // Find the main issuer (entry-point). let mainIssuer = issuer; let nextIssuer = getIssuer(compilation, mainIssuer); while (nextIssuer) { mainIssuer = nextIssuer; nextIssuer = getIssuer(compilation, mainIssuer); } // Only show warnings for modules from main entrypoint. // And if the issuer request is not from 'webpack-dev-server', as 'webpack-dev-server' // will require CommonJS libraries for live reloading such as 'sockjs-node'. if (mainIssuer && mainModules.has(mainIssuer)) { const warning = `${(issuer as NormalModule | null)?.userRequest} depends on '${rawRequest}'. ` + 'CommonJS or AMD dependencies can cause optimization bailouts.\n' + 'For more info see: https://angular.dev/tools/cli/build#configuring-commonjs-dependencies'; // Avoid showing the same warning multiple times when in 'watch' mode. if (!this.shownWarnings.has(warning)) { addWarning(compilation, warning); this.shownWarnings.add(warning); } } } } }); }); } private hasCommonJsDependencies( compilation: Compilation, dependencies: Dependency[], checkParentModules = false, ): boolean { for (const dep of dependencies) { if ( dep instanceof CommonJsRequireDependency || dep instanceof CommonJsExportsDependency || dep instanceof CommonJsSelfReferenceDependency || dep instanceof AMDDefineDependency ) { return true; } if (checkParentModules) { const module = getWebpackModule(compilation, dep); if (module && this.hasCommonJsDependencies(compilation, module.dependencies)) { return true; } } } return false; } private rawRequestToPackageName(rawRequest: string): string { return rawRequest.startsWith('@') ? // Scoped request ex: @angular/common/locale/en -> @angular/common rawRequest.split('/', 2).join('/') : // Non-scoped request ex: lodash/isEmpty -> lodash rawRequest.split('/', 1)[0]; } } function getIssuer( compilation: Compilation, module: Module | null | undefined, ): Module | null | undefined { if (!module) { return null; } return compilation.moduleGraph.getIssuer(module); } function getWebpackModule( compilation: Compilation, dependency: Dependency | null, ): Module | null | undefined { if (!dependency) { return null; } return compilation.moduleGraph.getModule(dependency); }