88
99import { BuilderContext } from '@angular-devkit/architect' ;
1010import {
11+ BuildContext ,
1112 BuildFailure ,
12- BuildInvalidate ,
1313 BuildOptions ,
14- BuildResult ,
14+ Message ,
15+ Metafile ,
1516 OutputFile ,
1617 PartialMessage ,
1718 build ,
19+ context ,
1820 formatMessages ,
1921} from 'esbuild' ;
2022import { basename , extname , relative } from 'node:path' ;
@@ -29,76 +31,116 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
2931 return ! ! value && typeof value === 'object' && 'errors' in value && 'warnings' in value ;
3032}
3133
32- /**
33- * Executes the esbuild build function and normalizes the build result in the event of a
34- * build failure that results in no output being generated.
35- * All builds use the `write` option with a value of `false` to allow for the output files
36- * build result array to be populated.
37- *
38- * @param optionsOrInvalidate The esbuild options object to use when building or the invalidate object
39- * returned from an incremental build to perform an additional incremental build.
40- * @returns If output files are generated, the full esbuild BuildResult; if not, the
41- * warnings and errors for the attempted build.
42- */
43- export async function bundle (
44- workspaceRoot : string ,
45- optionsOrInvalidate : BuildOptions | BuildInvalidate ,
46- ) : Promise <
47- | ( BuildResult & { outputFiles : OutputFile [ ] ; initialFiles : FileInfo [ ] } )
48- | ( BuildFailure & { outputFiles ?: never } )
49- > {
50- let result ;
51- try {
52- if ( typeof optionsOrInvalidate === 'function' ) {
53- result = ( await optionsOrInvalidate ( ) ) as BuildResult & { outputFiles : OutputFile [ ] } ;
54- } else {
55- result = await build ( {
56- ...optionsOrInvalidate ,
57- metafile : true ,
58- write : false ,
59- } ) ;
34+ export class BundlerContext {
35+ #esbuildContext?: BuildContext < { metafile : true ; write : false } > ;
36+ #esbuildOptions: BuildOptions & { metafile : true ; write : false } ;
37+
38+ constructor ( private workspaceRoot : string , private incremental : boolean , options : BuildOptions ) {
39+ this . #esbuildOptions = {
40+ ...options ,
41+ metafile : true ,
42+ write : false ,
43+ } ;
44+ }
45+
46+ /**
47+ * Executes the esbuild build function and normalizes the build result in the event of a
48+ * build failure that results in no output being generated.
49+ * All builds use the `write` option with a value of `false` to allow for the output files
50+ * build result array to be populated.
51+ *
52+ * @returns If output files are generated, the full esbuild BuildResult; if not, the
53+ * warnings and errors for the attempted build.
54+ */
55+ async bundle ( ) : Promise <
56+ | { errors : Message [ ] ; warnings : Message [ ] }
57+ | {
58+ errors : undefined ;
59+ warnings : Message [ ] ;
60+ metafile : Metafile ;
61+ outputFiles : OutputFile [ ] ;
62+ initialFiles : FileInfo [ ] ;
63+ }
64+ > {
65+ let result ;
66+ try {
67+ if ( this . #esbuildContext) {
68+ // Rebuild using the existing incremental build context
69+ result = await this . #esbuildContext. rebuild ( ) ;
70+ } else if ( this . incremental ) {
71+ // Create an incremental build context and perform the first build.
72+ // Context creation does not perform a build.
73+ this . #esbuildContext = await context ( this . #esbuildOptions) ;
74+ result = await this . #esbuildContext. rebuild ( ) ;
75+ } else {
76+ // For non-incremental builds, perform a single build
77+ result = await build ( this . #esbuildOptions) ;
78+ }
79+ } catch ( failure ) {
80+ // Build failures will throw an exception which contains errors/warnings
81+ if ( isEsBuildFailure ( failure ) ) {
82+ return failure ;
83+ } else {
84+ throw failure ;
85+ }
6086 }
61- } catch ( failure ) {
62- // Build failures will throw an exception which contains errors/warnings
63- if ( isEsBuildFailure ( failure ) ) {
64- return failure ;
65- } else {
66- throw failure ;
87+
88+ // Return if the build encountered any errors
89+ if ( result . errors . length ) {
90+ return {
91+ errors : result . errors ,
92+ warnings : result . warnings ,
93+ } ;
6794 }
68- }
6995
70- const initialFiles : FileInfo [ ] = [ ] ;
71- for ( const outputFile of result . outputFiles ) {
72- // Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
73- const relativeFilePath = relative ( workspaceRoot , outputFile . path ) ;
74- const entryPoint = result . metafile ?. outputs [ relativeFilePath ] ?. entryPoint ;
96+ // Find all initial files
97+ const initialFiles : FileInfo [ ] = [ ] ;
98+ for ( const outputFile of result . outputFiles ) {
99+ // Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
100+ const relativeFilePath = relative ( this . workspaceRoot , outputFile . path ) ;
101+ const entryPoint = result . metafile ?. outputs [ relativeFilePath ] ?. entryPoint ;
75102
76- outputFile . path = relativeFilePath ;
103+ outputFile . path = relativeFilePath ;
77104
78- if ( entryPoint ) {
79- // An entryPoint value indicates an initial file
80- initialFiles . push ( {
81- file : outputFile . path ,
82- // The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
83- name : basename ( outputFile . path ) . split ( '.' ) [ 0 ] ,
84- extension : extname ( outputFile . path ) ,
85- } ) ;
105+ if ( entryPoint ) {
106+ // An entryPoint value indicates an initial file
107+ initialFiles . push ( {
108+ file : outputFile . path ,
109+ // The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
110+ name : basename ( outputFile . path ) . split ( '.' ) [ 0 ] ,
111+ extension : extname ( outputFile . path ) ,
112+ } ) ;
113+ }
86114 }
115+
116+ // Return the successful build results
117+ return { ...result , initialFiles, errors : undefined } ;
87118 }
88119
89- return { ...result , initialFiles } ;
120+ /**
121+ * Disposes incremental build resources present in the context.
122+ *
123+ * @returns A promise that resolves when disposal is complete.
124+ */
125+ async dispose ( ) : Promise < void > {
126+ try {
127+ return this . #esbuildContext?. dispose ( ) ;
128+ } finally {
129+ this . #esbuildContext = undefined ;
130+ }
131+ }
90132}
91133
92134export async function logMessages (
93135 context : BuilderContext ,
94- { errors, warnings } : { errors : PartialMessage [ ] ; warnings : PartialMessage [ ] } ,
136+ { errors, warnings } : { errors ? : PartialMessage [ ] ; warnings ? : PartialMessage [ ] } ,
95137) : Promise < void > {
96- if ( warnings . length ) {
138+ if ( warnings ? .length ) {
97139 const warningMessages = await formatMessages ( warnings , { kind : 'warning' , color : true } ) ;
98140 context . logger . warn ( warningMessages . join ( '\n' ) ) ;
99141 }
100142
101- if ( errors . length ) {
143+ if ( errors ? .length ) {
102144 const errorMessages = await formatMessages ( errors , { kind : 'error' , color : true } ) ;
103145 context . logger . error ( errorMessages . join ( '\n' ) ) ;
104146 }
0 commit comments