|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
|
10 |
| -import type { BuildOptions, Metafile, OutputFile } from 'esbuild'; |
11 |
| -import { constants as fsConstants } from 'node:fs'; |
| 10 | +import type { BuildOptions, OutputFile } from 'esbuild'; |
12 | 11 | import fs from 'node:fs/promises';
|
13 | 12 | import path from 'node:path';
|
14 |
| -import { promisify } from 'node:util'; |
15 |
| -import { brotliCompress } from 'node:zlib'; |
| 13 | +import { SourceFileCache, createCompilerPlugin } from '../../tools/esbuild/angular/compiler-plugin'; |
| 14 | +import { BundlerContext } from '../../tools/esbuild/bundler-context'; |
| 15 | +import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; |
| 16 | +import { createExternalPackagesPlugin } from '../../tools/esbuild/external-packages-plugin'; |
| 17 | +import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts'; |
| 18 | +import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles'; |
| 19 | +import { extractLicenses } from '../../tools/esbuild/license-extractor'; |
| 20 | +import { createSourcemapIngorelistPlugin } from '../../tools/esbuild/sourcemap-ignorelist-plugin'; |
| 21 | +import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; |
| 22 | +import { |
| 23 | + calculateEstimatedTransferSizes, |
| 24 | + createOutputFileFromText, |
| 25 | + getFeatureSupport, |
| 26 | + logBuildStats, |
| 27 | + logMessages, |
| 28 | + withNoProgress, |
| 29 | + withSpinner, |
| 30 | + writeResultFiles, |
| 31 | +} from '../../tools/esbuild/utils'; |
| 32 | +import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; |
| 33 | +import type { ChangedFiles } from '../../tools/esbuild/watcher'; |
16 | 34 | import { copyAssets } from '../../utils/copy-assets';
|
17 | 35 | import { assertIsError } from '../../utils/error';
|
18 | 36 | import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
|
19 | 37 | import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
|
20 | 38 | import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
|
21 |
| -import { Spinner } from '../../utils/spinner'; |
22 | 39 | import { getSupportedBrowsers } from '../../utils/supported-browsers';
|
23 |
| -import { BundleStats, generateBuildStatsTable } from '../../webpack/utils/stats'; |
24 |
| -import { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin'; |
25 | 40 | import { logBuilderStatusWarnings } from './builder-status-warnings';
|
26 |
| -import { checkCommonJSModules } from './commonjs-checker'; |
27 |
| -import { BundlerContext, InitialFileRecord, logMessages } from './esbuild'; |
28 |
| -import { createGlobalScriptsBundleOptions } from './global-scripts'; |
29 |
| -import { createGlobalStylesBundleOptions } from './global-styles'; |
30 |
| -import { extractLicenses } from './license-extractor'; |
31 | 41 | import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options';
|
32 | 42 | import { Schema as BrowserBuilderOptions } from './schema';
|
33 |
| -import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin'; |
34 |
| -import { shutdownSassWorkerPool } from './stylesheets/sass-language'; |
35 |
| -import { createVirtualModulePlugin } from './virtual-module-plugin'; |
36 |
| -import type { ChangedFiles } from './watcher'; |
37 |
| - |
38 |
| -const compressAsync = promisify(brotliCompress); |
39 | 43 |
|
40 | 44 | interface RebuildState {
|
41 | 45 | rebuildContexts: BundlerContext[];
|
@@ -279,51 +283,6 @@ async function execute(
|
279 | 283 | return executionResult;
|
280 | 284 | }
|
281 | 285 |
|
282 |
| -async function writeResultFiles( |
283 |
| - outputFiles: OutputFile[], |
284 |
| - assetFiles: { source: string; destination: string }[] | undefined, |
285 |
| - outputPath: string, |
286 |
| -) { |
287 |
| - const directoryExists = new Set<string>(); |
288 |
| - await Promise.all( |
289 |
| - outputFiles.map(async (file) => { |
290 |
| - // Ensure output subdirectories exist |
291 |
| - const basePath = path.dirname(file.path); |
292 |
| - if (basePath && !directoryExists.has(basePath)) { |
293 |
| - await fs.mkdir(path.join(outputPath, basePath), { recursive: true }); |
294 |
| - directoryExists.add(basePath); |
295 |
| - } |
296 |
| - // Write file contents |
297 |
| - await fs.writeFile(path.join(outputPath, file.path), file.contents); |
298 |
| - }), |
299 |
| - ); |
300 |
| - |
301 |
| - if (assetFiles?.length) { |
302 |
| - await Promise.all( |
303 |
| - assetFiles.map(async ({ source, destination }) => { |
304 |
| - // Ensure output subdirectories exist |
305 |
| - const basePath = path.dirname(destination); |
306 |
| - if (basePath && !directoryExists.has(basePath)) { |
307 |
| - await fs.mkdir(path.join(outputPath, basePath), { recursive: true }); |
308 |
| - directoryExists.add(basePath); |
309 |
| - } |
310 |
| - // Copy file contents |
311 |
| - await fs.copyFile(source, path.join(outputPath, destination), fsConstants.COPYFILE_FICLONE); |
312 |
| - }), |
313 |
| - ); |
314 |
| - } |
315 |
| -} |
316 |
| - |
317 |
| -function createOutputFileFromText(path: string, text: string): OutputFile { |
318 |
| - return { |
319 |
| - path, |
320 |
| - text, |
321 |
| - get contents() { |
322 |
| - return Buffer.from(this.text, 'utf-8'); |
323 |
| - }, |
324 |
| - }; |
325 |
| -} |
326 |
| - |
327 | 286 | function createCodeBundleOptions(
|
328 | 287 | options: NormalizedBrowserOptions,
|
329 | 288 | target: string[],
|
@@ -418,43 +377,8 @@ function createCodeBundleOptions(
|
418 | 377 | };
|
419 | 378 |
|
420 | 379 | if (options.externalPackages) {
|
421 |
| - // Add a plugin that marks any resolved path as external if it is within a node modules directory. |
422 |
| - // This is used instead of the esbuild `packages` option to avoid marking bare specifiers that use |
423 |
| - // tsconfig path mapping to resolve to a workspace relative path. This is common for monorepos that |
424 |
| - // contain libraries that are built along with the application. These libraries should not be considered |
425 |
| - // external even though the imports appear to be packages. |
426 |
| - const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION'); |
427 | 380 | buildOptions.plugins ??= [];
|
428 |
| - buildOptions.plugins.push({ |
429 |
| - name: 'angular-external-packages', |
430 |
| - setup(build) { |
431 |
| - build.onResolve({ filter: /./ }, async (args) => { |
432 |
| - if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) { |
433 |
| - return null; |
434 |
| - } |
435 |
| - |
436 |
| - const { importer, kind, resolveDir, namespace, pluginData = {} } = args; |
437 |
| - pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true; |
438 |
| - |
439 |
| - const result = await build.resolve(args.path, { |
440 |
| - importer, |
441 |
| - kind, |
442 |
| - namespace, |
443 |
| - pluginData, |
444 |
| - resolveDir, |
445 |
| - }); |
446 |
| - |
447 |
| - if (result.path && /[\\/]node_modules[\\/]/.test(result.path)) { |
448 |
| - return { |
449 |
| - path: args.path, |
450 |
| - external: true, |
451 |
| - }; |
452 |
| - } |
453 |
| - |
454 |
| - return result; |
455 |
| - }); |
456 |
| - }, |
457 |
| - }); |
| 381 | + buildOptions.plugins.push(createExternalPackagesPlugin()); |
458 | 382 | }
|
459 | 383 |
|
460 | 384 | const polyfills = options.polyfills ? [...options.polyfills] : [];
|
@@ -484,82 +408,6 @@ function createCodeBundleOptions(
|
484 | 408 | return buildOptions;
|
485 | 409 | }
|
486 | 410 |
|
487 |
| -/** |
488 |
| - * Generates a syntax feature object map for Angular applications based on a list of targets. |
489 |
| - * A full set of feature names can be found here: https://esbuild.github.io/api/#supported |
490 |
| - * @param target An array of browser/engine targets in the format accepted by the esbuild `target` option. |
491 |
| - * @returns An object that can be used with the esbuild build `supported` option. |
492 |
| - */ |
493 |
| -function getFeatureSupport(target: string[]): BuildOptions['supported'] { |
494 |
| - const supported: Record<string, boolean> = { |
495 |
| - // Native async/await is not supported with Zone.js. Disabling support here will cause |
496 |
| - // esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild |
497 |
| - // does not currently support downleveling async generators. Instead babel is used within the JS/TS |
498 |
| - // loader to perform the downlevel transformation. |
499 |
| - // NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled. |
500 |
| - 'async-await': false, |
501 |
| - // V8 currently has a performance defect involving object spread operations that can cause signficant |
502 |
| - // degradation in runtime performance. By not supporting the language feature here, a downlevel form |
503 |
| - // will be used instead which provides a workaround for the performance issue. |
504 |
| - // For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536 |
505 |
| - 'object-rest-spread': false, |
506 |
| - // esbuild currently has a defect involving self-referencing a class within a static code block or |
507 |
| - // static field initializer. This is not an issue for projects that use the default browserslist as these |
508 |
| - // elements are an ES2022 feature which is not support by all browsers in the default list. However, if a |
509 |
| - // custom browserslist is used that only has newer browsers than the static code elements may be present. |
510 |
| - // This issue is compounded by the default usage of the tsconfig `"useDefineForClassFields": false` option |
511 |
| - // present in generated CLI projects which causes static code blocks to be used instead of static fields. |
512 |
| - // esbuild currently unconditionally downlevels all static fields in top-level classes so to workaround the |
513 |
| - // Angular issue only static code blocks are disabled here. |
514 |
| - // For more details: https://github.com/evanw/esbuild/issues/2950 |
515 |
| - 'class-static-blocks': false, |
516 |
| - }; |
517 |
| - |
518 |
| - // Detect Safari browser versions that have a class field behavior bug |
519 |
| - // See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033 |
520 |
| - // See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2 |
521 |
| - let safariClassFieldScopeBug = false; |
522 |
| - for (const browser of target) { |
523 |
| - let majorVersion; |
524 |
| - if (browser.startsWith('ios')) { |
525 |
| - majorVersion = Number(browser.slice(3, 5)); |
526 |
| - } else if (browser.startsWith('safari')) { |
527 |
| - majorVersion = Number(browser.slice(6, 8)); |
528 |
| - } else { |
529 |
| - continue; |
530 |
| - } |
531 |
| - // Technically, 14.0 is not broken but rather does not have support. However, the behavior |
532 |
| - // is identical since it would be set to false by esbuild if present as a target. |
533 |
| - if (majorVersion === 14 || majorVersion === 15) { |
534 |
| - safariClassFieldScopeBug = true; |
535 |
| - break; |
536 |
| - } |
537 |
| - } |
538 |
| - // If class field support cannot be used set to false; otherwise leave undefined to allow |
539 |
| - // esbuild to use `target` to determine support. |
540 |
| - if (safariClassFieldScopeBug) { |
541 |
| - supported['class-field'] = false; |
542 |
| - supported['class-static-field'] = false; |
543 |
| - } |
544 |
| - |
545 |
| - return supported; |
546 |
| -} |
547 |
| - |
548 |
| -async function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T> { |
549 |
| - const spinner = new Spinner(text); |
550 |
| - spinner.start(); |
551 |
| - |
552 |
| - try { |
553 |
| - return await action(); |
554 |
| - } finally { |
555 |
| - spinner.stop(); |
556 |
| - } |
557 |
| -} |
558 |
| - |
559 |
| -async function withNoProgress<T>(test: string, action: () => T | Promise<T>): Promise<T> { |
560 |
| - return action(); |
561 |
| -} |
562 |
| - |
563 | 411 | /**
|
564 | 412 | * Main execution function for the esbuild-based application builder.
|
565 | 413 | * The options are compatible with the Webpack-based builder.
|
@@ -675,7 +523,7 @@ export async function* buildEsbuildBrowserInternal(
|
675 | 523 | }
|
676 | 524 |
|
677 | 525 | // Setup a watcher
|
678 |
| - const { createWatcher } = await import('./watcher'); |
| 526 | + const { createWatcher } = await import('../../tools/esbuild/watcher'); |
679 | 527 | const watcher = createWatcher({
|
680 | 528 | polling: typeof userOptions.poll === 'number',
|
681 | 529 | interval: userOptions.poll,
|
@@ -752,66 +600,3 @@ export async function* buildEsbuildBrowserInternal(
|
752 | 600 | }
|
753 | 601 |
|
754 | 602 | export default createBuilder(buildEsbuildBrowser);
|
755 |
| - |
756 |
| -function logBuildStats( |
757 |
| - context: BuilderContext, |
758 |
| - metafile: Metafile, |
759 |
| - initial: Map<string, InitialFileRecord>, |
760 |
| - estimatedTransferSizes?: Map<string, number>, |
761 |
| -) { |
762 |
| - const stats: BundleStats[] = []; |
763 |
| - for (const [file, output] of Object.entries(metafile.outputs)) { |
764 |
| - // Only display JavaScript and CSS files |
765 |
| - if (!file.endsWith('.js') && !file.endsWith('.css')) { |
766 |
| - continue; |
767 |
| - } |
768 |
| - // Skip internal component resources |
769 |
| - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
770 |
| - if ((output as any)['ng-component']) { |
771 |
| - continue; |
772 |
| - } |
773 |
| - |
774 |
| - stats.push({ |
775 |
| - initial: initial.has(file), |
776 |
| - stats: [ |
777 |
| - file, |
778 |
| - initial.get(file)?.name ?? '-', |
779 |
| - output.bytes, |
780 |
| - estimatedTransferSizes?.get(file) ?? '-', |
781 |
| - ], |
782 |
| - }); |
783 |
| - } |
784 |
| - |
785 |
| - const tableText = generateBuildStatsTable(stats, true, true, !!estimatedTransferSizes, undefined); |
786 |
| - |
787 |
| - context.logger.info('\n' + tableText + '\n'); |
788 |
| -} |
789 |
| - |
790 |
| -async function calculateEstimatedTransferSizes(outputFiles: OutputFile[]) { |
791 |
| - const sizes = new Map<string, number>(); |
792 |
| - |
793 |
| - const pendingCompression = []; |
794 |
| - for (const outputFile of outputFiles) { |
795 |
| - // Only calculate JavaScript and CSS files |
796 |
| - if (!outputFile.path.endsWith('.js') && !outputFile.path.endsWith('.css')) { |
797 |
| - continue; |
798 |
| - } |
799 |
| - |
800 |
| - // Skip compressing small files which may end being larger once compressed and will most likely not be |
801 |
| - // compressed in actual transit. |
802 |
| - if (outputFile.contents.byteLength < 1024) { |
803 |
| - sizes.set(outputFile.path, outputFile.contents.byteLength); |
804 |
| - continue; |
805 |
| - } |
806 |
| - |
807 |
| - pendingCompression.push( |
808 |
| - compressAsync(outputFile.contents).then((result) => |
809 |
| - sizes.set(outputFile.path, result.byteLength), |
810 |
| - ), |
811 |
| - ); |
812 |
| - } |
813 |
| - |
814 |
| - await Promise.all(pendingCompression); |
815 |
| - |
816 |
| - return sizes; |
817 |
| -} |
0 commit comments