-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathutils.ts
221 lines (198 loc) · 8.2 KB
/
utils.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* @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 { BuilderContext } from '@angular-devkit/architect';
import { BuildOptions, Metafile, OutputFile, PartialMessage, formatMessages } from 'esbuild';
import { constants as fsConstants } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { brotliCompress } from 'node:zlib';
import { Spinner } from '../../utils/spinner';
import { BundleStats, generateBuildStatsTable } from '../webpack/utils/stats';
import { InitialFileRecord } from './bundler-context';
const compressAsync = promisify(brotliCompress);
export function logBuildStats(
context: BuilderContext,
metafile: Metafile,
initial: Map<string, InitialFileRecord>,
estimatedTransferSizes?: Map<string, number>,
): void {
const stats: BundleStats[] = [];
for (const [file, output] of Object.entries(metafile.outputs)) {
// Only display JavaScript and CSS files
if (!file.endsWith('.js') && !file.endsWith('.css')) {
continue;
}
// Skip internal component resources
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((output as any)['ng-component']) {
continue;
}
stats.push({
initial: initial.has(file),
stats: [
file,
initial.get(file)?.name ?? '-',
output.bytes,
estimatedTransferSizes?.get(file) ?? '-',
],
});
}
const tableText = generateBuildStatsTable(stats, true, true, !!estimatedTransferSizes, undefined);
context.logger.info('\n' + tableText + '\n');
}
export async function calculateEstimatedTransferSizes(
outputFiles: OutputFile[],
): Promise<Map<string, number>> {
const sizes = new Map<string, number>();
const pendingCompression = [];
for (const outputFile of outputFiles) {
// Only calculate JavaScript and CSS files
if (!outputFile.path.endsWith('.js') && !outputFile.path.endsWith('.css')) {
continue;
}
// Skip compressing small files which may end being larger once compressed and will most likely not be
// compressed in actual transit.
if (outputFile.contents.byteLength < 1024) {
sizes.set(outputFile.path, outputFile.contents.byteLength);
continue;
}
pendingCompression.push(
compressAsync(outputFile.contents).then((result) =>
sizes.set(outputFile.path, result.byteLength),
),
);
}
await Promise.all(pendingCompression);
return sizes;
}
export async function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T> {
const spinner = new Spinner(text);
spinner.start();
try {
return await action();
} finally {
spinner.stop();
}
}
export async function withNoProgress<T>(test: string, action: () => T | Promise<T>): Promise<T> {
return action();
}
export async function logMessages(
context: BuilderContext,
{ errors, warnings }: { errors?: PartialMessage[]; warnings?: PartialMessage[] },
): Promise<void> {
if (warnings?.length) {
const warningMessages = await formatMessages(warnings, { kind: 'warning', color: true });
context.logger.warn(warningMessages.join('\n'));
}
if (errors?.length) {
const errorMessages = await formatMessages(errors, { kind: 'error', color: true });
context.logger.error(errorMessages.join('\n'));
}
}
/**
* Generates a syntax feature object map for Angular applications based on a list of targets.
* A full set of feature names can be found here: https://esbuild.github.io/api/#supported
* @param target An array of browser/engine targets in the format accepted by the esbuild `target` option.
* @returns An object that can be used with the esbuild build `supported` option.
*/
export function getFeatureSupport(target: string[]): BuildOptions['supported'] {
const supported: Record<string, boolean> = {
// Native async/await is not supported with Zone.js. Disabling support here will cause
// esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild
// does not currently support downleveling async generators. Instead babel is used within the JS/TS
// loader to perform the downlevel transformation.
// NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled.
'async-await': false,
// V8 currently has a performance defect involving object spread operations that can cause signficant
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
// will be used instead which provides a workaround for the performance issue.
// For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536
'object-rest-spread': false,
// esbuild currently has a defect involving self-referencing a class within a static code block or
// static field initializer. This is not an issue for projects that use the default browserslist as these
// elements are an ES2022 feature which is not support by all browsers in the default list. However, if a
// custom browserslist is used that only has newer browsers than the static code elements may be present.
// This issue is compounded by the default usage of the tsconfig `"useDefineForClassFields": false` option
// present in generated CLI projects which causes static code blocks to be used instead of static fields.
// esbuild currently unconditionally downlevels all static fields in top-level classes so to workaround the
// Angular issue only static code blocks are disabled here.
// For more details: https://github.com/evanw/esbuild/issues/2950
'class-static-blocks': false,
};
// Detect Safari browser versions that have a class field behavior bug
// See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033
// See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2
let safariClassFieldScopeBug = false;
for (const browser of target) {
let majorVersion;
if (browser.startsWith('ios')) {
majorVersion = Number(browser.slice(3, 5));
} else if (browser.startsWith('safari')) {
majorVersion = Number(browser.slice(6, 8));
} else {
continue;
}
// Technically, 14.0 is not broken but rather does not have support. However, the behavior
// is identical since it would be set to false by esbuild if present as a target.
if (majorVersion === 14 || majorVersion === 15) {
safariClassFieldScopeBug = true;
break;
}
}
// If class field support cannot be used set to false; otherwise leave undefined to allow
// esbuild to use `target` to determine support.
if (safariClassFieldScopeBug) {
supported['class-field'] = false;
supported['class-static-field'] = false;
}
return supported;
}
export async function writeResultFiles(
outputFiles: OutputFile[],
assetFiles: { source: string; destination: string }[] | undefined,
outputPath: string,
) {
const directoryExists = new Set<string>();
await Promise.all(
outputFiles.map(async (file) => {
// Ensure output subdirectories exist
const basePath = path.dirname(file.path);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Write file contents
await fs.writeFile(path.join(outputPath, file.path), file.contents);
}),
);
if (assetFiles?.length) {
await Promise.all(
assetFiles.map(async ({ source, destination }) => {
// Ensure output subdirectories exist
const basePath = path.dirname(destination);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Copy file contents
await fs.copyFile(source, path.join(outputPath, destination), fsConstants.COPYFILE_FICLONE);
}),
);
}
}
export function createOutputFileFromText(path: string, text: string): OutputFile {
return {
path,
text,
get contents() {
return Buffer.from(this.text, 'utf-8');
},
};
}