Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): standardize application builder …
Browse files Browse the repository at this point in the history
…output structure

This commit updates the application builder to output files in a standardized manner. The builder will output a `browser` directory for all the files that can be accessible by the browser, and a `server` directory that contains the SSR application. Both of these directories are created as children in the configured `outputPath`. Stats and license files will be outputted directly in the configured `outputPath`.

Example of output:
```
3rdpartylicenses.txt
├── browser
│   ├── chunk-2XJVAMHT.js
│   ├── favicon.ico
│   ├── index.html
│   ├── main-6JLMM7WW.js
│   ├── polyfills-4UVFGIFL.js
│   └── styles-5INURTSO.css
└── server
    ├── chunk-4ZCEIHD4.mjs
    ├── chunk-PMR7BAU4.mjs
    ├── chunk-TSP6W7K5.mjs
    ├── index.server.html
    ├── main.server.mjs
    └── server.mjs
```
  • Loading branch information
alan-agius4 authored and clydin committed Oct 5, 2023
1 parent f29b744 commit 49f07a8
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BuilderOutput } from '@angular-devkit/architect';
import type { logging } from '@angular-devkit/core';
import fs from 'node:fs/promises';
import path from 'node:path';
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils';
Expand All @@ -25,6 +26,7 @@ export async function* runEsBuildBuildAction(
logger: logging.LoggerApi;
cacheOptions: NormalizedCachedOptions;
writeToFileSystem?: boolean;
writeToFileSystemFilter?: (file: BuildOutputFile) => boolean;
watch?: boolean;
verbose?: boolean;
progress?: boolean;
Expand All @@ -34,6 +36,7 @@ export async function* runEsBuildBuildAction(
},
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
const {
writeToFileSystemFilter,
writeToFileSystem = true,
watch,
poll,
Expand Down Expand Up @@ -177,7 +180,10 @@ export async function* runEsBuildBuildAction(

if (writeToFileSystem) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
const filesToWrite = writeToFileSystemFilter
? result.outputFiles.filter(writeToFileSystemFilter)
: result.outputFiles;
await writeResultFiles(filesToWrite, result.assetFiles, outputPath);

yield result.output;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
createBrowserCodeBundleOptions,
createServerCodeBundleOptions,
} from '../../tools/esbuild/application-code-bundle';
import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { BuildOutputFileType, BundlerContext } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
Expand Down Expand Up @@ -174,10 +174,14 @@ export async function executeBuild(
indexContentOutputNoCssInlining = contentWithoutCriticalCssInlined;
printWarningsAndErrorsToConsole(context, warnings, errors);

executionResult.addOutputFile(indexHtmlOptions.output, content);
executionResult.addOutputFile(indexHtmlOptions.output, content, BuildOutputFileType.Browser);

if (ssrOptions) {
executionResult.addOutputFile('index.server.html', contentWithoutCriticalCssInlined);
executionResult.addOutputFile(
'index.server.html',
contentWithoutCriticalCssInlined,
BuildOutputFileType.Server,
);
}
}

Expand All @@ -203,22 +207,23 @@ export async function executeBuild(
printWarningsAndErrorsToConsole(context, warnings, errors);

for (const [path, content] of Object.entries(output)) {
executionResult.addOutputFile(path, content);
executionResult.addOutputFile(path, content, BuildOutputFileType.Browser);
}
}

// Copy assets
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.
executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot)));
executionResult.addAssets(await copyAssets(assets, [], workspaceRoot));
}

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

Expand All @@ -233,8 +238,12 @@ export async function executeBuild(
executionResult.outputFiles,
executionResult.assetFiles,
);
executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest);
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
executionResult.addOutputFile(
'ngsw.json',
serviceWorkerResult.manifest,
BuildOutputFileType.Browser,
);
executionResult.addAssets(serviceWorkerResult.assetFiles);
} catch (error) {
context.logger.error(error instanceof Error ? error.message : `${error}`);

Expand All @@ -261,7 +270,11 @@ export async function executeBuild(

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

return executionResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { BuilderContext } from '@angular-devkit/architect';
import { join } from 'node:path';
import { InitialFileRecord } from '../../tools/esbuild/bundler-context';
import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-context';
import { ExecutionResult } from '../../tools/esbuild/bundler-execution-result';
import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
Expand Down Expand Up @@ -75,7 +75,13 @@ export async function inlineI18n(
locale,
);

localeOutputFiles.push(createOutputFileFromText(options.indexHtmlOptions.output, content));
localeOutputFiles.push(
createOutputFileFromText(
options.indexHtmlOptions.output,
content,
BuildOutputFileType.Browser,
),
);
inlineResult.errors.push(...errors);
inlineResult.warnings.push(...warnings);

Expand All @@ -96,7 +102,9 @@ export async function inlineI18n(
inlineResult.warnings.push(...warnings);

for (const [path, content] of Object.entries(output)) {
localeOutputFiles.push(createOutputFileFromText(path, content));
localeOutputFiles.push(
createOutputFileFromText(path, content, BuildOutputFileType.Browser),
);
}
}
}
Expand All @@ -111,7 +119,11 @@ export async function inlineI18n(
executionResult.assetFiles,
);
localeOutputFiles.push(
createOutputFileFromText('ngsw.json', serviceWorkerResult.manifest),
createOutputFileFromText(
'ngsw.json',
serviceWorkerResult.manifest,
BuildOutputFileType.Browser,
),
);
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { OutputFile } from 'esbuild';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
import { assertCompatibleAngularVersion } from '../../utils/version';
import { runEsBuildBuildAction } from './build-action';
Expand All @@ -24,7 +24,7 @@ export async function* buildApplicationInternal(
},
): AsyncIterable<
BuilderOutput & {
outputFiles?: OutputFile[];
outputFiles?: BuildOutputFile[];
assetFiles?: { source: string; destination: string }[];
}
> {
Expand Down Expand Up @@ -65,6 +65,12 @@ export async function* buildApplicationInternal(
workspaceRoot: normalizedOptions.workspaceRoot,
progress: normalizedOptions.progress,
writeToFileSystem: infrastructureSettings?.write,
// For app-shell and SSG server files are not required by users.
// Omit these when SSR is not enabled.
writeToFileSystemFilter:
normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint
? undefined
: (file) => file.type !== BuildOutputFileType.Server,
logger: context.logger,
signal: context.signal,
},
Expand All @@ -76,7 +82,7 @@ export function buildApplication(
context: BuilderContext,
): AsyncIterable<
BuilderOutput & {
outputFiles?: OutputFile[];
outputFiles?: BuildOutputFile[];
assetFiles?: { source: string; destination: string }[];
}
> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { OutputFile } from 'esbuild';
import { constants as fsConstants } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
import { buildApplicationInternal } from '../application';
import { Schema as ApplicationBuilderOptions } from '../application/schema';
import { logBuilderStatusWarnings } from './builder-status-warnings';
Expand All @@ -20,24 +23,33 @@ import { Schema as BrowserBuilderOptions } from './schema';
* @param context The Architect builder context object
* @returns An async iterable with the builder result output
*/
export function buildEsbuildBrowser(
export async function* buildEsbuildBrowser(
userOptions: BrowserBuilderOptions,
context: BuilderContext,
infrastructureSettings?: {
write?: boolean;
},
): AsyncIterable<
BuilderOutput & {
outputFiles?: OutputFile[];
outputFiles?: BuildOutputFile[];
assetFiles?: { source: string; destination: string }[];
}
> {
// Inform user of status of builder and options
logBuilderStatusWarnings(userOptions, context);

const normalizedOptions = normalizeOptions(userOptions);
const fullOutputPath = path.join(context.workspaceRoot, normalizedOptions.outputPath);

for await (const result of buildApplicationInternal(normalizedOptions, context, {
write: false,
})) {
if (infrastructureSettings?.write !== false && result.outputFiles) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, fullOutputPath);
}

return buildApplicationInternal(normalizedOptions, context, infrastructureSettings);
yield result;
}
}

function normalizeOptions(options: BrowserBuilderOptions): ApplicationBuilderOptions {
Expand All @@ -51,4 +63,41 @@ function normalizeOptions(options: BrowserBuilderOptions): ApplicationBuilderOpt
};
}

// We write the file directly from this builder to maintain webpack output compatibility
// and not output browser files into '/browser'.
async function writeResultFiles(
outputFiles: BuildOutputFile[],
assetFiles: { source: string; destination: string }[] | undefined,
outputPath: string,
) {
const directoryExists = new Set<string>();
await Promise.all(
outputFiles.map(async (file) => {
// Ensure output subdirectories exist
const basePath = path.dirname(file.path);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Write file contents
await fs.writeFile(path.join(outputPath, file.path), file.contents);
}),
);

if (assetFiles?.length) {
await Promise.all(
assetFiles.map(async ({ source, destination }) => {
// Ensure output subdirectories exist
const basePath = path.dirname(destination);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Copy file contents
await fs.copyFile(source, path.join(outputPath), fsConstants.COPYFILE_FICLONE);
}),
);
}
}

export default createBuilder(buildEsbuildBrowser);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type { BuilderContext } from '@angular-devkit/architect';
import type { json, logging } from '@angular-devkit/core';
import type { OutputFile } from 'esbuild';
import { lookup as lookupMimeType } from 'mrmime';
import assert from 'node:assert';
import { BinaryLike, createHash } from 'node:crypto';
Expand All @@ -17,6 +16,7 @@ import { ServerResponse } from 'node:http';
import type { AddressInfo } from 'node:net';
import path, { posix } from 'node:path';
import type { Connect, InlineConfig, ViteDevServer } from 'vite';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
import { RenderOptions, renderPage } from '../../utils/server-rendering/render-page';
Expand All @@ -32,6 +32,7 @@ interface OutputFileRecord {
size: number;
hash?: Buffer;
updated: boolean;
servable: boolean;
}

const SSG_MARKER_REGEXP = /ng-server-context=["']\w*\|?ssg\|?\w*["']/;
Expand Down Expand Up @@ -222,7 +223,7 @@ function handleUpdate(
function analyzeResultFiles(
normalizePath: (id: string) => string,
htmlIndexPath: string,
resultFiles: OutputFile[],
resultFiles: BuildOutputFile[],
generatedFiles: Map<string, OutputFileRecord>,
) {
const seen = new Set<string>(['/index.html']);
Expand All @@ -241,6 +242,8 @@ function analyzeResultFiles(
if (filePath.endsWith('.map')) {
generatedFiles.set(filePath, {
contents: file.contents,
servable:
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media,
size: file.contents.byteLength,
updated: false,
});
Expand Down Expand Up @@ -270,6 +273,8 @@ function analyzeResultFiles(
size: file.contents.byteLength,
hash: fileHash,
updated: true,
servable:
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media,
});
}

Expand Down Expand Up @@ -397,7 +402,7 @@ export async function setupServer(
// dev server sourcemap issues with stylesheets.
if (extension !== '.js' && extension !== '.html') {
const outputFile = outputFiles.get(pathname);
if (outputFile) {
if (outputFile?.servable) {
const mimeType = lookupMimeType(extension);
if (mimeType) {
res.setHeader('Content-Type', mimeType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default createBuilder(
'--experimental-vm-modules',
jest,

`--rootDir="${testOut}"`,
`--rootDir="${path.join(testOut, 'browser')}"`,
'--testEnvironment=jsdom',

// TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it.
Expand Down

0 comments on commit 49f07a8

Please sign in to comment.