8
8
9
9
import { BuilderContext , BuilderOutput , createBuilder } from '@angular-devkit/architect' ;
10
10
import type { Plugin } from 'esbuild' ;
11
+ import assert from 'node:assert' ;
12
+ import fs from 'node:fs/promises' ;
13
+ import path from 'node:path' ;
11
14
import { BuildOutputFile , BuildOutputFileType } from '../../tools/esbuild/bundler-context' ;
12
- import { createJsonBuildManifest } from '../../tools/esbuild/utils' ;
15
+ import { createJsonBuildManifest , emitFilesToDisk } from '../../tools/esbuild/utils' ;
13
16
import { colors as ansiColors } from '../../utils/color' ;
17
+ import { deleteOutputDir } from '../../utils/delete-output-dir' ;
18
+ import { useJSONBuildLogs } from '../../utils/environment-options' ;
14
19
import { purgeStaleBuildCache } from '../../utils/purge-cache' ;
15
20
import { assertCompatibleAngularVersion } from '../../utils/version' ;
16
21
import { runEsBuildBuildAction } from './build-action' ;
17
22
import { executeBuild } from './execute-build' ;
18
23
import {
19
24
ApplicationBuilderExtensions ,
20
25
ApplicationBuilderInternalOptions ,
26
+ NormalizedOutputOptions ,
21
27
normalizeOptions ,
22
28
} from './options' ;
23
29
import { Result , ResultKind } from './results' ;
@@ -29,9 +35,6 @@ export async function* buildApplicationInternal(
29
35
options : ApplicationBuilderInternalOptions ,
30
36
// TODO: Integrate abort signal support into builder system
31
37
context : BuilderContext & { signal ?: AbortSignal } ,
32
- infrastructureSettings ?: {
33
- write ?: boolean ;
34
- } ,
35
38
extensions ?: ApplicationBuilderExtensions ,
36
39
) : AsyncIterable < Result > {
37
40
const { workspaceRoot, logger, target } = context ;
@@ -53,11 +56,8 @@ export async function* buildApplicationInternal(
53
56
}
54
57
55
58
const normalizedOptions = await normalizeOptions ( context , projectName , options , extensions ) ;
56
- const writeToFileSystem = infrastructureSettings ?. write ?? true ;
57
- const writeServerBundles =
58
- writeToFileSystem && ! ! ( normalizedOptions . ssrOptions && normalizedOptions . serverEntryPoint ) ;
59
59
60
- if ( writeServerBundles ) {
60
+ if ( ! normalizedOptions . outputOptions . ignoreServer ) {
61
61
const { browser, server } = normalizedOptions . outputOptions ;
62
62
if ( browser === '' ) {
63
63
context . logger . error (
@@ -88,7 +88,7 @@ export async function* buildApplicationInternal(
88
88
89
89
yield * runEsBuildBuildAction (
90
90
async ( rebuildState ) => {
91
- const { prerenderOptions, outputOptions , jsonLogs } = normalizedOptions ;
91
+ const { prerenderOptions, jsonLogs } = normalizedOptions ;
92
92
93
93
const startTime = process . hrtime . bigint ( ) ;
94
94
const result = await executeBuild ( normalizedOptions , context , rebuildState ) ;
@@ -106,9 +106,6 @@ export async function* buildApplicationInternal(
106
106
107
107
const buildTime = Number ( process . hrtime . bigint ( ) - startTime ) / 10 ** 9 ;
108
108
const hasError = result . errors . length > 0 ;
109
- if ( writeToFileSystem && ! hasError ) {
110
- result . addLog ( `Output location: ${ outputOptions . base } \n` ) ;
111
- }
112
109
113
110
result . addLog (
114
111
`Application bundle generation ${ hasError ? 'failed' : 'complete' } . [${ buildTime . toFixed ( 3 ) } seconds]\n` ,
@@ -121,7 +118,6 @@ export async function* buildApplicationInternal(
121
118
watch : normalizedOptions . watch ,
122
119
preserveSymlinks : normalizedOptions . preserveSymlinks ,
123
120
poll : normalizedOptions . poll ,
124
- deleteOutputPath : normalizedOptions . deleteOutputPath ,
125
121
cacheOptions : normalizedOptions . cacheOptions ,
126
122
outputOptions : normalizedOptions . outputOptions ,
127
123
verbose : normalizedOptions . verbose ,
@@ -131,12 +127,6 @@ export async function* buildApplicationInternal(
131
127
clearScreen : normalizedOptions . clearScreen ,
132
128
colors : normalizedOptions . colors ,
133
129
jsonLogs : normalizedOptions . jsonLogs ,
134
- writeToFileSystem,
135
- // For app-shell and SSG server files are not required by users.
136
- // Omit these when SSR is not enabled.
137
- writeToFileSystemFilter : writeServerBundles
138
- ? undefined
139
- : ( file ) => file . type !== BuildOutputFileType . Server ,
140
130
logger,
141
131
signal,
142
132
} ,
@@ -202,8 +192,80 @@ export async function* buildApplication(
202
192
extensions = pluginsOrExtensions ;
203
193
}
204
194
205
- for await ( const result of buildApplicationInternal ( options , context , undefined , extensions ) ) {
206
- yield { success : result . kind !== ResultKind . Failure } ;
195
+ let initial = true ;
196
+ for await ( const result of buildApplicationInternal ( options , context , extensions ) ) {
197
+ const outputOptions = result . detail ?. [ 'outputOptions' ] as NormalizedOutputOptions | undefined ;
198
+
199
+ if ( initial ) {
200
+ initial = false ;
201
+
202
+ // Clean the output location if requested.
203
+ // Output options may not be present if the build failed.
204
+ if ( outputOptions ?. clean ) {
205
+ await deleteOutputDir ( context . workspaceRoot , outputOptions . base , [
206
+ outputOptions . browser ,
207
+ outputOptions . server ,
208
+ ] ) ;
209
+ }
210
+ }
211
+
212
+ if ( result . kind === ResultKind . Failure ) {
213
+ yield { success : false } ;
214
+ continue ;
215
+ }
216
+
217
+ assert ( outputOptions , 'Application output options are required for builder usage.' ) ;
218
+ assert ( result . kind === ResultKind . Full , 'Application build did not provide a full output.' ) ;
219
+
220
+ // TODO: Restructure output logging to better handle stdout JSON piping
221
+ if ( ! useJSONBuildLogs ) {
222
+ context . logger . info ( `Output location: ${ outputOptions . base } \n` ) ;
223
+ }
224
+
225
+ // Writes the output files to disk and ensures the containing directories are present
226
+ const directoryExists = new Set < string > ( ) ;
227
+ await emitFilesToDisk ( Object . entries ( result . files ) , async ( [ filePath , file ] ) => {
228
+ if ( outputOptions . ignoreServer && file . type === BuildOutputFileType . Server ) {
229
+ return ;
230
+ }
231
+
232
+ let typeDirectory : string ;
233
+ switch ( file . type ) {
234
+ case BuildOutputFileType . Browser :
235
+ case BuildOutputFileType . Media :
236
+ typeDirectory = outputOptions . browser ;
237
+ break ;
238
+ case BuildOutputFileType . Server :
239
+ typeDirectory = outputOptions . server ;
240
+ break ;
241
+ case BuildOutputFileType . Root :
242
+ typeDirectory = '' ;
243
+ break ;
244
+ default :
245
+ throw new Error (
246
+ `Unhandled write for file "${ filePath } " with type "${ BuildOutputFileType [ file . type ] } ".` ,
247
+ ) ;
248
+ }
249
+ // NOTE: 'base' is a fully resolved path at this point
250
+ const fullFilePath = path . join ( outputOptions . base , typeDirectory , filePath ) ;
251
+
252
+ // Ensure output subdirectories exist
253
+ const fileBasePath = path . dirname ( fullFilePath ) ;
254
+ if ( fileBasePath && ! directoryExists . has ( fileBasePath ) ) {
255
+ await fs . mkdir ( fileBasePath , { recursive : true } ) ;
256
+ directoryExists . add ( fileBasePath ) ;
257
+ }
258
+
259
+ if ( file . origin === 'memory' ) {
260
+ // Write file contents
261
+ await fs . writeFile ( fullFilePath , file . contents ) ;
262
+ } else {
263
+ // Copy file contents
264
+ await fs . copyFile ( file . inputPath , fullFilePath , fs . constants . COPYFILE_FICLONE ) ;
265
+ }
266
+ } ) ;
267
+
268
+ yield { success : true } ;
207
269
}
208
270
}
209
271
0 commit comments