Skip to content

Commit

Permalink
refactor(@angular-devkit/build-angular): support in-memory results fo…
Browse files Browse the repository at this point in the history
…r esbuild builder

To provide support for development server integration, the esbuild-based builder can now
be setup to provide in-memory file results at the completion of a build.  This applies to
both watch and non-watch modes. The result output object structure is not currently considered
part of the public API and is currently only intended to be used by other builders within the
package.
  • Loading branch information
clydin authored and angular-robot[bot] committed Mar 27, 2023
1 parent c9e84d0 commit 0ac5f27
Showing 1 changed file with 89 additions and 70 deletions.
Expand Up @@ -42,16 +42,30 @@ interface RebuildState {
* Represents the result of a single builder execute call.
*/
class ExecutionResult {
readonly outputFiles: OutputFile[] = [];
readonly assetFiles: { source: string; destination: string }[] = [];

constructor(
private success: boolean,
private codeRebuild?: BundlerContext,
private globalStylesRebuild?: BundlerContext,
private codeBundleCache?: SourceFileCache,
) {}

addOutputFile(path: string, content: string): void {
this.outputFiles.push(createOutputFileFromText(path, content));
}

get output() {
return {
success: this.success,
success: this.outputFiles.length > 0,
};
}

get outputWithFiles() {
return {
success: this.outputFiles.length > 0,
outputFiles: this.outputFiles,
assetFiles: this.assetFiles,
};
}

Expand All @@ -67,7 +81,7 @@ class ExecutionResult {
}

async dispose(): Promise<void> {
await Promise.all([this.codeRebuild?.dispose(), this.globalStylesRebuild?.dispose()]);
await Promise.allSettled([this.codeRebuild?.dispose(), this.globalStylesRebuild?.dispose()]);
}
}

Expand All @@ -82,7 +96,6 @@ async function execute(
projectRoot,
workspaceRoot,
optimizationOptions,
outputPath,
assets,
serviceWorkerOptions,
indexHtmlOptions,
Expand Down Expand Up @@ -123,24 +136,15 @@ async function execute(
warnings: [...codeResults.warnings, ...styleResults.warnings],
});

// Return if the bundling failed to generate output files or there are errors
if (codeResults.errors) {
return new ExecutionResult(
false,
codeBundleContext,
globalStylesBundleContext,
codeBundleCache,
);
}
const executionResult = new ExecutionResult(
codeBundleContext,
globalStylesBundleContext,
codeBundleCache,
);

// Return if the global stylesheet bundling has errors
if (styleResults.errors) {
return new ExecutionResult(
false,
codeBundleContext,
globalStylesBundleContext,
codeBundleCache,
);
// Return if the bundling has errors
if (codeResults.errors || styleResults.errors) {
return executionResult;
}

// Filter global stylesheet initial files
Expand All @@ -150,7 +154,7 @@ async function execute(

// Combine the bundling output files
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];
executionResult.outputFiles.push(...codeResults.outputFiles, ...styleResults.outputFiles);

// Combine metafiles used for the stats option as well as bundle budgets and console output
const metafile = {
Expand Down Expand Up @@ -180,7 +184,7 @@ async function execute(
indexHtmlGenerator.readAsset = async function (filePath: string): Promise<string> {
// Remove leading directory separator
const relativefilePath = path.relative(virtualOutputPath, filePath);
const file = outputFiles.find((file) => file.path === relativefilePath);
const file = executionResult.outputFiles.find((file) => file.path === relativefilePath);
if (file) {
return file.text;
}
Expand All @@ -202,29 +206,26 @@ async function execute(
context.logger.warn(warning);
}

outputFiles.push(createOutputFileFromText(indexHtmlOptions.output, content));
executionResult.addOutputFile(indexHtmlOptions.output, content);
}

// Copy assets
let assetFiles;
if (assets) {
// The webpack copy assets helper is used with no base paths defined. This prevents the helper
// from directly writing to disk. This should eventually be replaced with a more optimized helper.
assetFiles = await copyAssets(assets, [], workspaceRoot);
executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot)));
}

// Write metafile if stats option is enabled
if (options.stats) {
outputFiles.push(createOutputFileFromText('stats.json', JSON.stringify(metafile, null, 2)));
executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2));
}

// Extract and write licenses for used packages
if (options.extractLicenses) {
outputFiles.push(
createOutputFileFromText(
'3rdpartylicenses.txt',
await extractLicenses(metafile, workspaceRoot),
),
executionResult.addOutputFile(
'3rdpartylicenses.txt',
await extractLicenses(metafile, workspaceRoot),
);
}

Expand All @@ -235,31 +236,22 @@ async function execute(
workspaceRoot,
serviceWorkerOptions,
options.baseHref || '/',
outputFiles,
assetFiles || [],
executionResult.outputFiles,
executionResult.assetFiles,
);
outputFiles.push(createOutputFileFromText('ngsw.json', serviceWorkerResult.manifest));
assetFiles ??= [];
assetFiles.push(...serviceWorkerResult.assetFiles);
executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest);
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
} catch (error) {
context.logger.error(error instanceof Error ? error.message : `${error}`);

return new ExecutionResult(
false,
codeBundleContext,
globalStylesBundleContext,
codeBundleCache,
);
return executionResult;
}
}

// Write output files
await writeResultFiles(outputFiles, assetFiles, outputPath);

const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
context.logger.info(`Complete. [${buildTime.toFixed(3)} seconds]`);

return new ExecutionResult(true, codeBundleContext, globalStylesBundleContext, codeBundleCache);
return executionResult;
}

async function writeResultFiles(
Expand Down Expand Up @@ -521,16 +513,19 @@ function createGlobalStylesBundleOptions(
/**
* Main execution function for the esbuild-based application builder.
* The options are compatible with the Webpack-based builder.
* @param initialOptions The browser builder options to use when setting up the application build
* @param userOptions The browser builder options to use when setting up the application build
* @param context The Architect builder context object
* @returns An async iterable with the builder result output
*/
export async function* buildEsbuildBrowser(
initialOptions: BrowserBuilderOptions,
userOptions: BrowserBuilderOptions,
context: BuilderContext,
): AsyncIterable<BuilderOutput> {
infrastructureSettings?: {
write?: boolean;
},
): AsyncIterable<BuilderOutput & { outputFiles?: OutputFile[] }> {
// Inform user of experimental status of builder and options
logExperimentalWarnings(initialOptions, context);
logExperimentalWarnings(userOptions, context);

// Determine project name from builder context target
const projectName = context.target?.project;
Expand All @@ -540,36 +535,50 @@ export async function* buildEsbuildBrowser(
return;
}

const normalizedOptions = await normalizeOptions(context, projectName, initialOptions);
const normalizedOptions = await normalizeOptions(context, projectName, userOptions);
// Writing the result to the filesystem is the default behavior
const shouldWriteResult = infrastructureSettings?.write !== false;

// Clean output path if enabled
if (initialOptions.deleteOutputPath) {
deleteOutputDir(normalizedOptions.workspaceRoot, initialOptions.outputPath);
}
if (shouldWriteResult) {
// Clean output path if enabled
if (userOptions.deleteOutputPath) {
deleteOutputDir(normalizedOptions.workspaceRoot, userOptions.outputPath);
}

// Create output directory if needed
try {
await fs.mkdir(normalizedOptions.outputPath, { recursive: true });
} catch (e) {
assertIsError(e);
context.logger.error('Unable to create output directory: ' + e.message);
// Create output directory if needed
try {
await fs.mkdir(normalizedOptions.outputPath, { recursive: true });
} catch (e) {
assertIsError(e);
context.logger.error('Unable to create output directory: ' + e.message);

return;
return;
}
}

// Initial build
let result: ExecutionResult;
try {
result = await execute(normalizedOptions, context);
yield result.output;

if (shouldWriteResult) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath);

yield result.output;
} else {
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield result.outputWithFiles as any;
}

// Finish if watch mode is not enabled
if (!initialOptions.watch) {
if (!userOptions.watch) {
return;
}
} finally {
// Ensure Sass workers are shutdown if not watching
if (!initialOptions.watch) {
if (!userOptions.watch) {
shutdownSassWorkerPool();
}
}
Expand All @@ -578,8 +587,8 @@ export async function* buildEsbuildBrowser(

// Setup a watcher
const watcher = createWatcher({
polling: typeof initialOptions.poll === 'number',
interval: initialOptions.poll,
polling: typeof userOptions.poll === 'number',
interval: userOptions.poll,
// Ignore the output and cache paths to avoid infinite rebuild cycles
ignored: [normalizedOptions.outputPath, normalizedOptions.cacheOptions.basePath],
});
Expand All @@ -598,12 +607,22 @@ export async function* buildEsbuildBrowser(
for await (const changes of watcher) {
context.logger.info('Changes detected. Rebuilding...');

if (initialOptions.verbose) {
if (userOptions.verbose) {
context.logger.info(changes.toDebugString());
}

result = await execute(normalizedOptions, context, result.createRebuildState(changes));
yield result.output;

if (shouldWriteResult) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath);

yield result.output;
} else {
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield result.outputWithFiles as any;
}
}
} finally {
// Stop the watcher
Expand Down

0 comments on commit 0ac5f27

Please sign in to comment.