-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathcommonjs-checker.ts
178 lines (153 loc) · 5.97 KB
/
commonjs-checker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
* @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.io/license
*/
import type { Metafile, PartialMessage } from 'esbuild';
/**
* 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.
*/
export function checkCommonJSModules(
metafile: Metafile,
allowedCommonJsDependencies?: string[],
): PartialMessage[] {
const messages: PartialMessage[] = [];
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 critters is ESM it has a number of direct and transtive CJS deps.
allowedRequests.add('express');
allowedRequests.add('critters');
// Find all entry points that contain code (JS/TS)
const files: string[] = [];
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<string>(['<runtime>']);
// Analyze the files present by walking the import graph
let currentFile: string | undefined;
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: string): boolean {
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: string): boolean {
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: string, importer: string): PartialMessage {
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.io/guide/build#configuring-commonjs-dependencies',
},
],
};
return error;
}