|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; |
10 | | -import type { BuildOptions, OutputFile } from 'esbuild'; |
| 10 | +import type { OutputFile } from 'esbuild'; |
11 | 11 | import fs from 'node:fs/promises'; |
12 | 12 | import path from 'node:path'; |
13 | | -import { SourceFileCache, createCompilerPlugin } from '../../tools/esbuild/angular/compiler-plugin'; |
| 13 | +import { SourceFileCache } from '../../tools/esbuild/angular/compiler-plugin'; |
| 14 | +import { createCodeBundleOptions } from '../../tools/esbuild/application-code-bundle'; |
14 | 15 | import { BundlerContext } from '../../tools/esbuild/bundler-context'; |
| 16 | +import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; |
15 | 17 | import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; |
16 | | -import { createExternalPackagesPlugin } from '../../tools/esbuild/external-packages-plugin'; |
17 | 18 | import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts'; |
18 | 19 | import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles'; |
| 20 | +import { generateIndexHtml } from '../../tools/esbuild/index-html-generator'; |
19 | 21 | import { extractLicenses } from '../../tools/esbuild/license-extractor'; |
20 | | -import { createSourcemapIngorelistPlugin } from '../../tools/esbuild/sourcemap-ignorelist-plugin'; |
21 | 22 | import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; |
22 | 23 | import { |
23 | 24 | calculateEstimatedTransferSizes, |
24 | | - createOutputFileFromText, |
25 | | - getFeatureSupport, |
26 | 25 | logBuildStats, |
27 | 26 | logMessages, |
28 | 27 | withNoProgress, |
29 | 28 | withSpinner, |
30 | 29 | writeResultFiles, |
31 | 30 | } from '../../tools/esbuild/utils'; |
32 | | -import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; |
33 | | -import type { ChangedFiles } from '../../tools/esbuild/watcher'; |
34 | 31 | import { copyAssets } from '../../utils/copy-assets'; |
35 | 32 | import { assertIsError } from '../../utils/error'; |
36 | 33 | import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets'; |
37 | | -import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator'; |
38 | 34 | import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; |
39 | 35 | import { getSupportedBrowsers } from '../../utils/supported-browsers'; |
40 | 36 | import { logBuilderStatusWarnings } from './builder-status-warnings'; |
41 | 37 | import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options'; |
42 | 38 | import { Schema as BrowserBuilderOptions } from './schema'; |
43 | 39 |
|
44 | | -interface RebuildState { |
45 | | - rebuildContexts: BundlerContext[]; |
46 | | - codeBundleCache?: SourceFileCache; |
47 | | - fileChanges: ChangedFiles; |
48 | | -} |
49 | | - |
50 | | -/** |
51 | | - * Represents the result of a single builder execute call. |
52 | | - */ |
53 | | -class ExecutionResult { |
54 | | - readonly outputFiles: OutputFile[] = []; |
55 | | - readonly assetFiles: { source: string; destination: string }[] = []; |
56 | | - |
57 | | - constructor( |
58 | | - private rebuildContexts: BundlerContext[], |
59 | | - private codeBundleCache?: SourceFileCache, |
60 | | - ) {} |
61 | | - |
62 | | - addOutputFile(path: string, content: string): void { |
63 | | - this.outputFiles.push(createOutputFileFromText(path, content)); |
64 | | - } |
65 | | - |
66 | | - get output() { |
67 | | - return { |
68 | | - success: this.outputFiles.length > 0, |
69 | | - }; |
70 | | - } |
71 | | - |
72 | | - get outputWithFiles() { |
73 | | - return { |
74 | | - success: this.outputFiles.length > 0, |
75 | | - outputFiles: this.outputFiles, |
76 | | - assetFiles: this.assetFiles, |
77 | | - }; |
78 | | - } |
79 | | - |
80 | | - get watchFiles() { |
81 | | - return this.codeBundleCache?.referencedFiles ?? []; |
82 | | - } |
83 | | - |
84 | | - createRebuildState(fileChanges: ChangedFiles): RebuildState { |
85 | | - this.codeBundleCache?.invalidate([...fileChanges.modified, ...fileChanges.removed]); |
86 | | - |
87 | | - return { |
88 | | - rebuildContexts: this.rebuildContexts, |
89 | | - codeBundleCache: this.codeBundleCache, |
90 | | - fileChanges, |
91 | | - }; |
92 | | - } |
93 | | - |
94 | | - async dispose(): Promise<void> { |
95 | | - await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose())); |
96 | | - } |
97 | | -} |
98 | | - |
99 | 40 | async function execute( |
100 | 41 | options: NormalizedBrowserOptions, |
101 | 42 | context: BuilderContext, |
@@ -188,39 +129,11 @@ async function execute( |
188 | 129 |
|
189 | 130 | // Generate index HTML file |
190 | 131 | if (indexHtmlOptions) { |
191 | | - // Create an index HTML generator that reads from the in-memory output files |
192 | | - const indexHtmlGenerator = new IndexHtmlGenerator({ |
193 | | - indexPath: indexHtmlOptions.input, |
194 | | - entrypoints: indexHtmlOptions.insertionOrder, |
195 | | - sri: options.subresourceIntegrity, |
196 | | - optimization: optimizationOptions, |
197 | | - crossOrigin: options.crossOrigin, |
198 | | - }); |
199 | | - |
200 | | - /** Virtual output path to support reading in-memory files. */ |
201 | | - const virtualOutputPath = '/'; |
202 | | - indexHtmlGenerator.readAsset = async function (filePath: string): Promise<string> { |
203 | | - // Remove leading directory separator |
204 | | - const relativefilePath = path.relative(virtualOutputPath, filePath); |
205 | | - const file = executionResult.outputFiles.find((file) => file.path === relativefilePath); |
206 | | - if (file) { |
207 | | - return file.text; |
208 | | - } |
209 | | - |
210 | | - throw new Error(`Output file does not exist: ${path}`); |
211 | | - }; |
212 | | - |
213 | | - const { content, warnings, errors } = await indexHtmlGenerator.process({ |
214 | | - baseHref: options.baseHref, |
215 | | - lang: undefined, |
216 | | - outputPath: virtualOutputPath, |
217 | | - files: [...initialFiles].map(([file, record]) => ({ |
218 | | - name: record.name ?? '', |
219 | | - file, |
220 | | - extension: path.extname(file), |
221 | | - })), |
222 | | - }); |
223 | | - |
| 132 | + const { errors, warnings, content } = await generateIndexHtml( |
| 133 | + initialFiles, |
| 134 | + executionResult, |
| 135 | + options, |
| 136 | + ); |
224 | 137 | for (const error of errors) { |
225 | 138 | context.logger.error(error); |
226 | 139 | } |
@@ -283,131 +196,6 @@ async function execute( |
283 | 196 | return executionResult; |
284 | 197 | } |
285 | 198 |
|
286 | | -function createCodeBundleOptions( |
287 | | - options: NormalizedBrowserOptions, |
288 | | - target: string[], |
289 | | - browsers: string[], |
290 | | - sourceFileCache?: SourceFileCache, |
291 | | -): BuildOptions { |
292 | | - const { |
293 | | - workspaceRoot, |
294 | | - entryPoints, |
295 | | - optimizationOptions, |
296 | | - sourcemapOptions, |
297 | | - tsconfig, |
298 | | - outputNames, |
299 | | - outExtension, |
300 | | - fileReplacements, |
301 | | - externalDependencies, |
302 | | - preserveSymlinks, |
303 | | - stylePreprocessorOptions, |
304 | | - advancedOptimizations, |
305 | | - inlineStyleLanguage, |
306 | | - jit, |
307 | | - tailwindConfiguration, |
308 | | - } = options; |
309 | | - |
310 | | - const buildOptions: BuildOptions = { |
311 | | - absWorkingDir: workspaceRoot, |
312 | | - bundle: true, |
313 | | - format: 'esm', |
314 | | - entryPoints, |
315 | | - entryNames: outputNames.bundles, |
316 | | - assetNames: outputNames.media, |
317 | | - target, |
318 | | - supported: getFeatureSupport(target), |
319 | | - mainFields: ['es2020', 'browser', 'module', 'main'], |
320 | | - conditions: ['es2020', 'es2015', 'module'], |
321 | | - resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'], |
322 | | - metafile: true, |
323 | | - legalComments: options.extractLicenses ? 'none' : 'eof', |
324 | | - logLevel: options.verbose ? 'debug' : 'silent', |
325 | | - minify: optimizationOptions.scripts, |
326 | | - pure: ['forwardRef'], |
327 | | - outdir: workspaceRoot, |
328 | | - outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined, |
329 | | - sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true), |
330 | | - splitting: true, |
331 | | - tsconfig, |
332 | | - external: externalDependencies, |
333 | | - write: false, |
334 | | - platform: 'browser', |
335 | | - preserveSymlinks, |
336 | | - plugins: [ |
337 | | - createSourcemapIngorelistPlugin(), |
338 | | - createCompilerPlugin( |
339 | | - // JS/TS options |
340 | | - { |
341 | | - sourcemap: !!sourcemapOptions.scripts, |
342 | | - thirdPartySourcemaps: sourcemapOptions.vendor, |
343 | | - tsconfig, |
344 | | - jit, |
345 | | - advancedOptimizations, |
346 | | - fileReplacements, |
347 | | - sourceFileCache, |
348 | | - loadResultCache: sourceFileCache?.loadResultCache, |
349 | | - }, |
350 | | - // Component stylesheet options |
351 | | - { |
352 | | - workspaceRoot, |
353 | | - optimization: !!optimizationOptions.styles.minify, |
354 | | - sourcemap: |
355 | | - // Hidden component stylesheet sourcemaps are inaccessible which is effectively |
356 | | - // the same as being disabled. Disabling has the advantage of avoiding the overhead |
357 | | - // of sourcemap processing. |
358 | | - !!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'), |
359 | | - outputNames, |
360 | | - includePaths: stylePreprocessorOptions?.includePaths, |
361 | | - externalDependencies, |
362 | | - target, |
363 | | - inlineStyleLanguage, |
364 | | - preserveSymlinks, |
365 | | - browsers, |
366 | | - tailwindConfiguration, |
367 | | - }, |
368 | | - ), |
369 | | - ], |
370 | | - define: { |
371 | | - // Only set to false when script optimizations are enabled. It should not be set to true because |
372 | | - // Angular turns `ngDevMode` into an object for development debugging purposes when not defined |
373 | | - // which a constant true value would break. |
374 | | - ...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined), |
375 | | - 'ngJitMode': jit ? 'true' : 'false', |
376 | | - }, |
377 | | - }; |
378 | | - |
379 | | - if (options.externalPackages) { |
380 | | - buildOptions.plugins ??= []; |
381 | | - buildOptions.plugins.push(createExternalPackagesPlugin()); |
382 | | - } |
383 | | - |
384 | | - const polyfills = options.polyfills ? [...options.polyfills] : []; |
385 | | - if (jit) { |
386 | | - polyfills.push('@angular/compiler'); |
387 | | - } |
388 | | - |
389 | | - if (polyfills?.length) { |
390 | | - const namespace = 'angular:polyfills'; |
391 | | - buildOptions.entryPoints = { |
392 | | - ...buildOptions.entryPoints, |
393 | | - ['polyfills']: namespace, |
394 | | - }; |
395 | | - |
396 | | - buildOptions.plugins?.unshift( |
397 | | - createVirtualModulePlugin({ |
398 | | - namespace, |
399 | | - loadContent: () => ({ |
400 | | - contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'), |
401 | | - loader: 'js', |
402 | | - resolveDir: workspaceRoot, |
403 | | - }), |
404 | | - }), |
405 | | - ); |
406 | | - } |
407 | | - |
408 | | - return buildOptions; |
409 | | -} |
410 | | - |
411 | 199 | /** |
412 | 200 | * Main execution function for the esbuild-based application builder. |
413 | 201 | * The options are compatible with the Webpack-based builder. |
|
0 commit comments