|
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[]; |
@@ -299,51 +303,6 @@ async function execute( |
299 | 303 | return executionResult; |
300 | 304 | } |
301 | 305 |
|
302 | | -async function writeResultFiles( |
303 | | - outputFiles: OutputFile[], |
304 | | - assetFiles: { source: string; destination: string }[] | undefined, |
305 | | - outputPath: string, |
306 | | -) { |
307 | | - const directoryExists = new Set<string>(); |
308 | | - await Promise.all( |
309 | | - outputFiles.map(async (file) => { |
310 | | - // Ensure output subdirectories exist |
311 | | - const basePath = path.dirname(file.path); |
312 | | - if (basePath && !directoryExists.has(basePath)) { |
313 | | - await fs.mkdir(path.join(outputPath, basePath), { recursive: true }); |
314 | | - directoryExists.add(basePath); |
315 | | - } |
316 | | - // Write file contents |
317 | | - await fs.writeFile(path.join(outputPath, file.path), file.contents); |
318 | | - }), |
319 | | - ); |
320 | | - |
321 | | - if (assetFiles?.length) { |
322 | | - await Promise.all( |
323 | | - assetFiles.map(async ({ source, destination }) => { |
324 | | - // Ensure output subdirectories exist |
325 | | - const basePath = path.dirname(destination); |
326 | | - if (basePath && !directoryExists.has(basePath)) { |
327 | | - await fs.mkdir(path.join(outputPath, basePath), { recursive: true }); |
328 | | - directoryExists.add(basePath); |
329 | | - } |
330 | | - // Copy file contents |
331 | | - await fs.copyFile(source, path.join(outputPath, destination), fsConstants.COPYFILE_FICLONE); |
332 | | - }), |
333 | | - ); |
334 | | - } |
335 | | -} |
336 | | - |
337 | | -function createOutputFileFromText(path: string, text: string): OutputFile { |
338 | | - return { |
339 | | - path, |
340 | | - text, |
341 | | - get contents() { |
342 | | - return Buffer.from(this.text, 'utf-8'); |
343 | | - }, |
344 | | - }; |
345 | | -} |
346 | | - |
347 | 306 | function createCodeBundleOptions( |
348 | 307 | options: NormalizedBrowserOptions, |
349 | 308 | target: string[], |
@@ -438,43 +397,8 @@ function createCodeBundleOptions( |
438 | 397 | }; |
439 | 398 |
|
440 | 399 | if (options.externalPackages) { |
441 | | - // Add a plugin that marks any resolved path as external if it is within a node modules directory. |
442 | | - // This is used instead of the esbuild `packages` option to avoid marking bare specifiers that use |
443 | | - // tsconfig path mapping to resolve to a workspace relative path. This is common for monorepos that |
444 | | - // contain libraries that are built along with the application. These libraries should not be considered |
445 | | - // external even though the imports appear to be packages. |
446 | | - const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION'); |
447 | 400 | buildOptions.plugins ??= []; |
448 | | - buildOptions.plugins.push({ |
449 | | - name: 'angular-external-packages', |
450 | | - setup(build) { |
451 | | - build.onResolve({ filter: /./ }, async (args) => { |
452 | | - if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) { |
453 | | - return null; |
454 | | - } |
455 | | - |
456 | | - const { importer, kind, resolveDir, namespace, pluginData = {} } = args; |
457 | | - pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true; |
458 | | - |
459 | | - const result = await build.resolve(args.path, { |
460 | | - importer, |
461 | | - kind, |
462 | | - namespace, |
463 | | - pluginData, |
464 | | - resolveDir, |
465 | | - }); |
466 | | - |
467 | | - if (result.path && /[\\/]node_modules[\\/]/.test(result.path)) { |
468 | | - return { |
469 | | - path: args.path, |
470 | | - external: true, |
471 | | - }; |
472 | | - } |
473 | | - |
474 | | - return result; |
475 | | - }); |
476 | | - }, |
477 | | - }); |
| 401 | + buildOptions.plugins.push(createExternalPackagesPlugin()); |
478 | 402 | } |
479 | 403 |
|
480 | 404 | const polyfills = options.polyfills ? [...options.polyfills] : []; |
@@ -504,82 +428,6 @@ function createCodeBundleOptions( |
504 | 428 | return buildOptions; |
505 | 429 | } |
506 | 430 |
|
507 | | -/** |
508 | | - * Generates a syntax feature object map for Angular applications based on a list of targets. |
509 | | - * A full set of feature names can be found here: https://esbuild.github.io/api/#supported |
510 | | - * @param target An array of browser/engine targets in the format accepted by the esbuild `target` option. |
511 | | - * @returns An object that can be used with the esbuild build `supported` option. |
512 | | - */ |
513 | | -function getFeatureSupport(target: string[]): BuildOptions['supported'] { |
514 | | - const supported: Record<string, boolean> = { |
515 | | - // Native async/await is not supported with Zone.js. Disabling support here will cause |
516 | | - // esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild |
517 | | - // does not currently support downleveling async generators. Instead babel is used within the JS/TS |
518 | | - // loader to perform the downlevel transformation. |
519 | | - // NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled. |
520 | | - 'async-await': false, |
521 | | - // V8 currently has a performance defect involving object spread operations that can cause signficant |
522 | | - // degradation in runtime performance. By not supporting the language feature here, a downlevel form |
523 | | - // will be used instead which provides a workaround for the performance issue. |
524 | | - // For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536 |
525 | | - 'object-rest-spread': false, |
526 | | - // esbuild currently has a defect involving self-referencing a class within a static code block or |
527 | | - // static field initializer. This is not an issue for projects that use the default browserslist as these |
528 | | - // elements are an ES2022 feature which is not support by all browsers in the default list. However, if a |
529 | | - // custom browserslist is used that only has newer browsers than the static code elements may be present. |
530 | | - // This issue is compounded by the default usage of the tsconfig `"useDefineForClassFields": false` option |
531 | | - // present in generated CLI projects which causes static code blocks to be used instead of static fields. |
532 | | - // esbuild currently unconditionally downlevels all static fields in top-level classes so to workaround the |
533 | | - // Angular issue only static code blocks are disabled here. |
534 | | - // For more details: https://github.com/evanw/esbuild/issues/2950 |
535 | | - 'class-static-blocks': false, |
536 | | - }; |
537 | | - |
538 | | - // Detect Safari browser versions that have a class field behavior bug |
539 | | - // See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033 |
540 | | - // See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2 |
541 | | - let safariClassFieldScopeBug = false; |
542 | | - for (const browser of target) { |
543 | | - let majorVersion; |
544 | | - if (browser.startsWith('ios')) { |
545 | | - majorVersion = Number(browser.slice(3, 5)); |
546 | | - } else if (browser.startsWith('safari')) { |
547 | | - majorVersion = Number(browser.slice(6, 8)); |
548 | | - } else { |
549 | | - continue; |
550 | | - } |
551 | | - // Technically, 14.0 is not broken but rather does not have support. However, the behavior |
552 | | - // is identical since it would be set to false by esbuild if present as a target. |
553 | | - if (majorVersion === 14 || majorVersion === 15) { |
554 | | - safariClassFieldScopeBug = true; |
555 | | - break; |
556 | | - } |
557 | | - } |
558 | | - // If class field support cannot be used set to false; otherwise leave undefined to allow |
559 | | - // esbuild to use `target` to determine support. |
560 | | - if (safariClassFieldScopeBug) { |
561 | | - supported['class-field'] = false; |
562 | | - supported['class-static-field'] = false; |
563 | | - } |
564 | | - |
565 | | - return supported; |
566 | | -} |
567 | | - |
568 | | -async function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T> { |
569 | | - const spinner = new Spinner(text); |
570 | | - spinner.start(); |
571 | | - |
572 | | - try { |
573 | | - return await action(); |
574 | | - } finally { |
575 | | - spinner.stop(); |
576 | | - } |
577 | | -} |
578 | | - |
579 | | -async function withNoProgress<T>(test: string, action: () => T | Promise<T>): Promise<T> { |
580 | | - return action(); |
581 | | -} |
582 | | - |
583 | 431 | /** |
584 | 432 | * Main execution function for the esbuild-based application builder. |
585 | 433 | * The options are compatible with the Webpack-based builder. |
@@ -695,7 +543,7 @@ export async function* buildEsbuildBrowserInternal( |
695 | 543 | } |
696 | 544 |
|
697 | 545 | // Setup a watcher |
698 | | - const { createWatcher } = await import('./watcher'); |
| 546 | + const { createWatcher } = await import('../../tools/esbuild/watcher'); |
699 | 547 | const watcher = createWatcher({ |
700 | 548 | polling: typeof userOptions.poll === 'number', |
701 | 549 | interval: userOptions.poll, |
@@ -772,66 +620,3 @@ export async function* buildEsbuildBrowserInternal( |
772 | 620 | } |
773 | 621 |
|
774 | 622 | export default createBuilder(buildEsbuildBrowser); |
775 | | - |
776 | | -function logBuildStats( |
777 | | - context: BuilderContext, |
778 | | - metafile: Metafile, |
779 | | - initial: Map<string, InitialFileRecord>, |
780 | | - estimatedTransferSizes?: Map<string, number>, |
781 | | -) { |
782 | | - const stats: BundleStats[] = []; |
783 | | - for (const [file, output] of Object.entries(metafile.outputs)) { |
784 | | - // Only display JavaScript and CSS files |
785 | | - if (!file.endsWith('.js') && !file.endsWith('.css')) { |
786 | | - continue; |
787 | | - } |
788 | | - // Skip internal component resources |
789 | | - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
790 | | - if ((output as any)['ng-component']) { |
791 | | - continue; |
792 | | - } |
793 | | - |
794 | | - stats.push({ |
795 | | - initial: initial.has(file), |
796 | | - stats: [ |
797 | | - file, |
798 | | - initial.get(file)?.name ?? '-', |
799 | | - output.bytes, |
800 | | - estimatedTransferSizes?.get(file) ?? '-', |
801 | | - ], |
802 | | - }); |
803 | | - } |
804 | | - |
805 | | - const tableText = generateBuildStatsTable(stats, true, true, !!estimatedTransferSizes, undefined); |
806 | | - |
807 | | - context.logger.info('\n' + tableText + '\n'); |
808 | | -} |
809 | | - |
810 | | -async function calculateEstimatedTransferSizes(outputFiles: OutputFile[]) { |
811 | | - const sizes = new Map<string, number>(); |
812 | | - |
813 | | - const pendingCompression = []; |
814 | | - for (const outputFile of outputFiles) { |
815 | | - // Only calculate JavaScript and CSS files |
816 | | - if (!outputFile.path.endsWith('.js') && !outputFile.path.endsWith('.css')) { |
817 | | - continue; |
818 | | - } |
819 | | - |
820 | | - // Skip compressing small files which may end being larger once compressed and will most likely not be |
821 | | - // compressed in actual transit. |
822 | | - if (outputFile.contents.byteLength < 1024) { |
823 | | - sizes.set(outputFile.path, outputFile.contents.byteLength); |
824 | | - continue; |
825 | | - } |
826 | | - |
827 | | - pendingCompression.push( |
828 | | - compressAsync(outputFile.contents).then((result) => |
829 | | - sizes.set(outputFile.path, result.byteLength), |
830 | | - ), |
831 | | - ); |
832 | | - } |
833 | | - |
834 | | - await Promise.all(pendingCompression); |
835 | | - |
836 | | - return sizes; |
837 | | -} |
0 commit comments