|
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