1
+ import { BuilderContext , BuilderOutput } from "@angular-devkit/architect" ;
2
+ import { JsonObject } from "@angular-devkit/core" ;
3
+ import { OperatorFunction , from , Observable , of , ReplaySubject , Subscription } from "rxjs" ;
4
+ import { map , first , pluck , switchMap , finalize , catchError } from "rxjs/operators" ;
5
+ import { cyan , dim } from "chalk" ;
6
+ import { is } from "ramda" ;
7
+ import * as cluster from "cluster" ;
8
+ import * as ora from "ora" ;
9
+
10
+ import { IS_SINGLE_CPU } from "../index" ;
11
+
12
+ /**
13
+ * The global base configuration interface as shared by all builders in the application
14
+ */
15
+ export interface Options extends JsonObject {
16
+ /** Name of the project this build is targeting */
17
+ project : string ;
18
+ /** Run build and output a detailed record of the child tasks console logs. Default is `false`. */
19
+ verbose : boolean ;
20
+ }
21
+
22
+ /**
23
+ * The builder context interface as passed through the chain of builder tasks provided to the `builderHandler`
24
+ */
25
+ export interface Context {
26
+ /** The assigned values of the global options, target options and user provided options. */
27
+ options : Options & JsonObject ;
28
+ /** The builder context */
29
+ context : BuilderContext ;
30
+ /** The target project metadata (as specified in the workspace `angular.json` configuration file) */
31
+ metadata : JsonObject ;
32
+ /** The builder output from the last completed builder task/-s */
33
+ output ?: BuilderOutput ;
34
+ }
35
+
36
+ /**
37
+ * A builder callback function, may return an object, promise or observable. Unhandled exzceptions will be resolved into a `BuilderOutput` object.
38
+ */
39
+ export type BuilderCallback = ( context : Context ) => BuilderOutput | Promise < BuilderOutput > | Observable < BuilderOutput > ;
40
+
41
+ /**
42
+ * Builders provided to `builderHandler`
43
+ */
44
+ export type Builders = OperatorFunction < Context , Context & BuilderOutput > [ ] ;
45
+
46
+ /**
47
+ * Takes a list of builder tasks, executes them in sequence and returns a `BuilderOutput` observable. The builder output will only return `success: true` if all tasks has resolved without error.
48
+ * @param builderMessage Message to print when the builder is initialized
49
+ * @param builders List of build tasks to be executed in this builder context
50
+ */
51
+ export const builderHandler = ( builderMessage : string , ...builders : Builders ) => {
52
+ return < T > ( options : Options & T , context : BuilderContext ) => {
53
+ if ( IS_SINGLE_CPU ) {
54
+ context . logger . info ( `Builder is running on a single-core processing unit. Switching to single-threaded mode.` ) ;
55
+ options . verbose = true ;
56
+ } ;
57
+ const project = context . target && context . target . project ;
58
+ if ( ! project ) {
59
+ context . logger . fatal ( `The builder '${ context . builder . builderName } ' could not execute. No project was found.` ) ;
60
+ }
61
+ const projectMetadata = context . getProjectMetadata ( project ) ;
62
+ const assignContext = map ( ( metadata : JsonObject ) => ( {
63
+ project,
64
+ options,
65
+ context,
66
+ metadata
67
+ } as Context ) ) ;
68
+ // Clear console from previous build
69
+ console . clear ( ) ;
70
+ // Logs initializaton message
71
+ context . logger . info ( `\n${ builderMessage } ${ cyan ( project ) } \n` ) ;
72
+
73
+ const initializer = from ( projectMetadata )
74
+ // Initialize the builder
75
+ . pipe (
76
+ first ( ) ,
77
+ assignContext
78
+ )
79
+
80
+ const proccesser = initializer
81
+ // Apply builder tasks
82
+ . pipe . apply ( initializer , builders )
83
+ . pipe (
84
+ pluck < Context , BuilderOutput > ( "output" ) ,
85
+ map < BuilderOutput , BuilderOutput > ( ( { success, error } ) => {
86
+ if ( success )
87
+ context . logger . info ( dim ( `\nCompleted successfully.\n` ) )
88
+ else {
89
+ context . logger . info ( dim ( `\nCompleted with error.\n` ) ) ;
90
+ }
91
+ return { success, error } ;
92
+ } )
93
+ ) as Observable < BuilderOutput > ;
94
+
95
+ return proccesser ;
96
+
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Shedules a build run for a specific builder target, logs the process of that build and returns an observable function which wraps a `Context` object
102
+ * @param builder The name of a builder, i.e. its `package:builderName` tuple, or a builder callback function
103
+ * @param builderOptions Additional options passed to the builder
104
+ * @param builderMessage Message to print when the builder is either initalized or completed
105
+ */
106
+ export const scheduleBuilder = ( builder : string | BuilderCallback , builderOptions ?: JsonObject , builderMessage : string = "" ) => {
107
+
108
+ return switchMap ( ( context : Context ) => {
109
+
110
+ const loader = ora ( { indent : 2 } ) ;
111
+
112
+ /**
113
+ * Transforms `BuilderOutput` to `Context` object
114
+ * @param builderOutput `BuilderOutput` object
115
+ */
116
+ const toContext = map ( ( { success, error } : BuilderOutput ) => ( {
117
+ ...context , output : {
118
+ // Only failed outcomes should persist
119
+ success : success === false ? false : context . output . success ,
120
+ error
121
+ } as BuilderOutput
122
+ } as Context ) ) ;
123
+
124
+ /**
125
+ * Initialize a new loading state for the worker
126
+ */
127
+ const onOnline = ( ) => {
128
+ if ( cluster . isMaster ) {
129
+ // Close all running processes on hot reloads
130
+ for ( const index in cluster . workers ) {
131
+ if ( cluster . workers [ index ] . isConnected ( ) ) {
132
+ loader . info ( `Builder ${ builderMessage } terminated` ) ;
133
+ cluster . workers [ index ] . disconnect ( ) ;
134
+ }
135
+ }
136
+ loader . start ( `Building ${ builderMessage } ` ) ;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Sets the `BuilderOutput` state. Runs every time the cluster master receives a message from its worker.
142
+ * @param builderOutput `BuilderOutput` object
143
+ */
144
+ const onWorkerMessage = ( { success, error } : BuilderOutput ) => {
145
+ if ( success ) {
146
+ loader . succeed ( )
147
+ }
148
+ builderOutput$ . next ( { success, error } ) ;
149
+ }
150
+
151
+ /**
152
+ * Handles errors gracefully when a worker process has failed
153
+ * @param error `Error` object
154
+ */
155
+ const onWorkerError = ( { message } : Error ) => {
156
+ builderOutput$ . next ( {
157
+ success : false ,
158
+ error :
159
+ // prettier-ignore
160
+ `Worker process for ${ context . context . builder . builderName } failed with an exception.\n\n` +
161
+ `The reason for this is due to one of the following reasons:\n\n` +
162
+ ` 1. The worker could not be spawned, or\n` +
163
+ ` 2. The worker could not be killed, or\n` +
164
+ ` 3. The worker were unable to send a message to the master.\n\n` +
165
+ `Error message: ${ message } `
166
+ } ) ;
167
+ builderOutput$ . complete ( ) ;
168
+ }
169
+
170
+ /**
171
+ * Exists process when a worker was killed or exited
172
+ * @param worker `Worker` object
173
+ * @param code Exit code
174
+ * @param signal Exit signal
175
+ */
176
+ const onWorkerExit = ( _worker : cluster . Worker , code : number , signal : string ) => {
177
+ if ( code ) {
178
+ builderOutput$ . next ( {
179
+ success : false ,
180
+ error :
181
+ // prettier-ignore
182
+ `Worker process for ${ context . context . builder . builderName } failed with an exception.\n\n` +
183
+ `Process failed with exit code '${ code } ' and signal '${ signal } '`
184
+ } ) ;
185
+ }
186
+ builderOutput$ . complete ( ) ;
187
+ }
188
+
189
+ /**
190
+ * Executes when the builder is an observable and that observable has emitted a new value. Will only be called when the builder is in watch mode.
191
+ * @param builderOutput Builder output object
192
+ */
193
+ const onBuilderCallbackNext = ( { success, error } : BuilderOutput ) => {
194
+ if ( cluster . isWorker ) {
195
+ if ( process . send ) {
196
+ process . send ( { success, error } ) ;
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Executes when the builder's callback either has:
203
+ * 1. Returned a observable which has completed with an error, or
204
+ * 2. Returned a promise which has rejected, or
205
+ * 3. Thrown an error
206
+ * @param error Error message or object
207
+ */
208
+ const onBuilderCallbackError = ( error : any ) => {
209
+ if ( cluster . isWorker ) {
210
+ if ( process . send ) {
211
+ process . send ( { success : false , error } ) ;
212
+ }
213
+ }
214
+ return of ( { success : false , error } ) as Observable < BuilderOutput > ;
215
+ }
216
+
217
+ /**
218
+ * Executes when the builder's callback either has:
219
+ * 1. Returned a observable which has completed without error, or
220
+ * 2. Returned a promise which has resolved, or
221
+ * 3. Returned a value
222
+ */
223
+ const onBuilderCallbackComplete = ( ) => {
224
+ if ( cluster . isWorker ) {
225
+ if ( process . send ) {
226
+ process . send ( { success : true } ) ;
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Returns a builder callback as observable
233
+ */
234
+ const getBuilderCallback = ( ) => {
235
+ if ( is ( String , builder ) ) {
236
+ return from ( context . context . scheduleBuilder ( builder as string , { ...context . options , ...{ builderOptions } } ) )
237
+ . pipe ( switchMap ( ( builderRun ) => builderRun . output ) ) ;
238
+ }
239
+ if ( is ( Function , builder ) ) {
240
+ let builderCallback = ( builder as BuilderCallback ) ( context ) ;
241
+ if ( is ( Promise , builderCallback ) )
242
+ builderCallback = from ( builderCallback as Promise < BuilderOutput > ) ;
243
+ if ( is ( Object , builderCallback ) )
244
+ builderCallback = of ( builderCallback as BuilderOutput ) ;
245
+ return ( builderCallback as Observable < BuilderOutput > )
246
+ . pipe ( catchError ( onBuilderCallbackError ) ) ;
247
+ }
248
+ }
249
+
250
+ let builderOutput$ : ReplaySubject < BuilderOutput > ;
251
+
252
+ if ( context . options . verbose ) {
253
+ // Verbose output will execute on a single thread
254
+ return getBuilderCallback ( ) . pipe ( toContext ) ;
255
+ } else {
256
+ if ( cluster . isMaster ) {
257
+ // Do not pipe the worker's stdout or stderr
258
+ cluster . setupMaster ( { silent : true } ) ;
259
+ cluster . fork ( )
260
+ // When the worker has been connected to master
261
+ . on ( "online" , onOnline )
262
+ // When the worker emits a message
263
+ . on ( "message" , onWorkerMessage )
264
+ // When the worker has thrown a critical error
265
+ . on ( "error" , onWorkerError )
266
+ // When the worker has been either exited or killed
267
+ . on ( "exit" , onWorkerExit ) ;
268
+ } else {
269
+
270
+ const subscription : Subscription = getBuilderCallback ( )
271
+ . pipe ( finalize ( ( ) => subscription . unsubscribe ( ) ) )
272
+ . subscribe ( onBuilderCallbackNext , onBuilderCallbackError , onBuilderCallbackComplete ) ;
273
+
274
+ }
275
+ }
276
+
277
+ } )
278
+ }
0 commit comments