@angular/build
Version:
Official build system for Angular
151 lines (150 loc) • 6.45 kB
JavaScript
;
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkCommonJSModules = checkCommonJSModules;
/**
* Checks the input files of a build to determine if any of the files included
* in the build are not ESM. ESM files can be tree-shaken and otherwise optimized
* in ways that CommonJS and other module formats cannot. The esbuild metafile
* information is used as the basis for the analysis as it contains information
* for each input file including its respective format.
*
* If any allowed dependencies are provided via the `allowedCommonJsDependencies`
* parameter, both the direct import and any deep imports will be ignored and no
* diagnostic will be generated. Use `'*'` as entry to skip the check.
*
* If a module has been issued a diagnostic message, then all descendant modules
* will not be checked. This prevents a potential massive amount of inactionable
* messages since the initial module import is the cause of the problem.
*
* @param metafile An esbuild metafile object to check.
* @param allowedCommonJsDependencies An optional list of allowed dependencies.
* @returns Zero or more diagnostic messages for any non-ESM modules.
*/
function checkCommonJSModules(metafile, allowedCommonJsDependencies) {
const messages = [];
const allowedRequests = new Set(allowedCommonJsDependencies);
if (allowedRequests.has('*')) {
return messages;
}
// Ignore Angular locale definitions which are currently UMD
allowedRequests.add('@angular/common/locales');
// Ignore zone.js due to it currently being built with a UMD like structure.
// Once the build output is updated to be fully ESM, this can be removed.
allowedRequests.add('zone.js');
// Used by '@angular/platform-server' and is in a seperate chunk that is unused when
// using `provideHttpClient(withFetch())`.
allowedRequests.add('xhr2');
// Packages used by @angular/ssr.
// While beasties is ESM it has a number of direct and transtive CJS deps.
allowedRequests.add('express');
allowedRequests.add('beasties');
// Find all entry points that contain code (JS/TS)
const files = [];
for (const { entryPoint } of Object.values(metafile.outputs)) {
if (!entryPoint) {
continue;
}
if (!isPathCode(entryPoint)) {
continue;
}
files.push(entryPoint);
}
// Track seen files so they are only analyzed once.
// Bundler runtime code is also ignored since it cannot be actionable.
const seenFiles = new Set(['<runtime>']);
// Analyze the files present by walking the import graph
let currentFile;
while ((currentFile = files.shift())) {
const input = metafile.inputs[currentFile];
for (const imported of input.imports) {
// Ignore imports that were already seen or not originally in the code (bundler injected)
if (!imported.original || seenFiles.has(imported.path)) {
continue;
}
seenFiles.add(imported.path);
// If the dependency is allowed ignore all other checks
if (allowedRequests.has(imported.original)) {
continue;
}
// Only check actual code files
if (!isPathCode(imported.path)) {
continue;
}
// Check if non-relative import is ESM format and issue a diagnostic if the file is not allowed
if (!isPotentialRelative(imported.original) &&
metafile.inputs[imported.path].format !== 'esm') {
const request = imported.original;
let notAllowed = true;
if (allowedRequests.has(request)) {
notAllowed = false;
}
else {
// Check for deep imports of allowed requests
for (const allowed of allowedRequests) {
if (request.startsWith(allowed + '/')) {
notAllowed = false;
break;
}
}
}
if (notAllowed) {
// Issue a diagnostic message for CommonJS module
messages.push(createCommonJSModuleError(request, currentFile));
}
// Skip all descendants since they are also most likely not ESM but solved by addressing this import
continue;
}
// Add the path so that its imports can be checked
files.push(imported.path);
}
}
return messages;
}
/**
* Determines if a file path has an extension that is a JavaScript or TypeScript
* code file.
*
* @param name A path to check for code file extensions.
* @returns True, if a code file path; false, otherwise.
*/
function isPathCode(name) {
return /\.[cm]?[jt]sx?$/.test(name);
}
/**
* Test an import module specifier to determine if the string potentially references a relative file.
* npm packages should not start with a period so if the first character is a period than it is not a
* package. While this is sufficient for the use case in the CommmonJS checker, only checking the
* first character does not definitely indicate the specifier is a relative path.
*
* @param specifier An import module specifier.
* @returns True, if specifier is potentially relative; false, otherwise.
*/
function isPotentialRelative(specifier) {
return specifier[0] === '.';
}
/**
* Creates an esbuild diagnostic message for a given non-ESM module request.
*
* @param request The requested non-ESM module name.
* @param importer The path of the file containing the import.
* @returns A message representing the diagnostic.
*/
function createCommonJSModuleError(request, importer) {
const error = {
text: `Module '${request}' used by '${importer}' is not ESM`,
notes: [
{
text: 'CommonJS or AMD dependencies can cause optimization bailouts.\n' +
'For more information see: https://angular.dev/tools/cli/build#configuring-commonjs-dependencies',
},
],
};
return error;
}