8
8
9
9
import { BuilderContext } from '@angular-devkit/architect' ;
10
10
import {
11
+ BuildContext ,
11
12
BuildFailure ,
12
- BuildInvalidate ,
13
13
BuildOptions ,
14
- BuildResult ,
14
+ Message ,
15
+ Metafile ,
15
16
OutputFile ,
16
17
PartialMessage ,
17
18
build ,
19
+ context ,
18
20
formatMessages ,
19
21
} from 'esbuild' ;
20
22
import { basename , extname , relative } from 'node:path' ;
@@ -29,76 +31,116 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
29
31
return ! ! value && typeof value === 'object' && 'errors' in value && 'warnings' in value ;
30
32
}
31
33
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
+ }
60
86
}
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
+ } ;
67
94
}
68
- }
69
95
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 ;
75
102
76
- outputFile . path = relativeFilePath ;
103
+ outputFile . path = relativeFilePath ;
77
104
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
+ }
86
114
}
115
+
116
+ // Return the successful build results
117
+ return { ...result , initialFiles, errors : undefined } ;
87
118
}
88
119
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
+ }
90
132
}
91
133
92
134
export async function logMessages (
93
135
context : BuilderContext ,
94
- { errors, warnings } : { errors : PartialMessage [ ] ; warnings : PartialMessage [ ] } ,
136
+ { errors, warnings } : { errors ? : PartialMessage [ ] ; warnings ? : PartialMessage [ ] } ,
95
137
) : Promise < void > {
96
- if ( warnings . length ) {
138
+ if ( warnings ? .length ) {
97
139
const warningMessages = await formatMessages ( warnings , { kind : 'warning' , color : true } ) ;
98
140
context . logger . warn ( warningMessages . join ( '\n' ) ) ;
99
141
}
100
142
101
- if ( errors . length ) {
143
+ if ( errors ? .length ) {
102
144
const errorMessages = await formatMessages ( errors , { kind : 'error' , color : true } ) ;
103
145
context . logger . error ( errorMessages . join ( '\n' ) ) ;
104
146
}
0 commit comments