88
99import { BuilderContext , BuilderOutput , createBuilder } from '@angular-devkit/architect' ;
1010import type { Plugin } from 'esbuild' ;
11+ import assert from 'node:assert' ;
12+ import fs from 'node:fs/promises' ;
13+ import path from 'node:path' ;
1114import { BuildOutputFile , BuildOutputFileType } from '../../tools/esbuild/bundler-context' ;
12- import { createJsonBuildManifest } from '../../tools/esbuild/utils' ;
15+ import { createJsonBuildManifest , emitFilesToDisk } from '../../tools/esbuild/utils' ;
1316import { colors as ansiColors } from '../../utils/color' ;
17+ import { deleteOutputDir } from '../../utils/delete-output-dir' ;
18+ import { useJSONBuildLogs } from '../../utils/environment-options' ;
1419import { purgeStaleBuildCache } from '../../utils/purge-cache' ;
1520import { assertCompatibleAngularVersion } from '../../utils/version' ;
1621import { runEsBuildBuildAction } from './build-action' ;
1722import { executeBuild } from './execute-build' ;
1823import {
1924 ApplicationBuilderExtensions ,
2025 ApplicationBuilderInternalOptions ,
26+ NormalizedOutputOptions ,
2127 normalizeOptions ,
2228} from './options' ;
2329import { Result , ResultKind } from './results' ;
@@ -29,9 +35,6 @@ export async function* buildApplicationInternal(
2935 options : ApplicationBuilderInternalOptions ,
3036 // TODO: Integrate abort signal support into builder system
3137 context : BuilderContext & { signal ?: AbortSignal } ,
32- infrastructureSettings ?: {
33- write ?: boolean ;
34- } ,
3538 extensions ?: ApplicationBuilderExtensions ,
3639) : AsyncIterable < Result > {
3740 const { workspaceRoot, logger, target } = context ;
@@ -53,11 +56,8 @@ export async function* buildApplicationInternal(
5356 }
5457
5558 const normalizedOptions = await normalizeOptions ( context , projectName , options , extensions ) ;
56- const writeToFileSystem = infrastructureSettings ?. write ?? true ;
57- const writeServerBundles =
58- writeToFileSystem && ! ! ( normalizedOptions . ssrOptions && normalizedOptions . serverEntryPoint ) ;
5959
60- if ( writeServerBundles ) {
60+ if ( ! normalizedOptions . outputOptions . ignoreServer ) {
6161 const { browser, server } = normalizedOptions . outputOptions ;
6262 if ( browser === '' ) {
6363 context . logger . error (
@@ -88,7 +88,7 @@ export async function* buildApplicationInternal(
8888
8989 yield * runEsBuildBuildAction (
9090 async ( rebuildState ) => {
91- const { prerenderOptions, outputOptions , jsonLogs } = normalizedOptions ;
91+ const { prerenderOptions, jsonLogs } = normalizedOptions ;
9292
9393 const startTime = process . hrtime . bigint ( ) ;
9494 const result = await executeBuild ( normalizedOptions , context , rebuildState ) ;
@@ -106,9 +106,6 @@ export async function* buildApplicationInternal(
106106
107107 const buildTime = Number ( process . hrtime . bigint ( ) - startTime ) / 10 ** 9 ;
108108 const hasError = result . errors . length > 0 ;
109- if ( writeToFileSystem && ! hasError ) {
110- result . addLog ( `Output location: ${ outputOptions . base } \n` ) ;
111- }
112109
113110 result . addLog (
114111 `Application bundle generation ${ hasError ? 'failed' : 'complete' } . [${ buildTime . toFixed ( 3 ) } seconds]\n` ,
@@ -121,7 +118,6 @@ export async function* buildApplicationInternal(
121118 watch : normalizedOptions . watch ,
122119 preserveSymlinks : normalizedOptions . preserveSymlinks ,
123120 poll : normalizedOptions . poll ,
124- deleteOutputPath : normalizedOptions . deleteOutputPath ,
125121 cacheOptions : normalizedOptions . cacheOptions ,
126122 outputOptions : normalizedOptions . outputOptions ,
127123 verbose : normalizedOptions . verbose ,
@@ -131,12 +127,6 @@ export async function* buildApplicationInternal(
131127 clearScreen : normalizedOptions . clearScreen ,
132128 colors : normalizedOptions . colors ,
133129 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 ,
140130 logger,
141131 signal,
142132 } ,
@@ -202,8 +192,80 @@ export async function* buildApplication(
202192 extensions = pluginsOrExtensions ;
203193 }
204194
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 } ;
207269 }
208270}
209271
0 commit comments