Skip to content

Commit cf608dd

Browse files
committed
feat(@angular-devkit/build-angular): allow customization of output locations
This update introduces the ability for users to define the locations for storing `media`, `browser`, and `server` files. You can achieve this by utilizing the extended `outputPath` option. ```json { "projects": { "my-app": { "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": { "base": "dist/my-app", "browser": "", "server": "node-server", "media": "resources" } } } } } } } ``` While not recommended, choosing to set the `browser` option empty will result in files being output directly under the specified `base` path. It's important to note that this action will generate certain files like `stats.json` and `prerendered-routes.json` that aren't intended for deployment in this directory. **Validation rules:** - `browser` and `server` are relative to the configuration set in the `base` option. - When SSR is enabled, `browser` cannot be set to an empty string, and cannot be the same as `server`. - `media` is relative to the value specified in the `browser` option. - `media` cannot be set to an empty string. - `browser`, `media`, or `server` cannot contain slashes. Closes: angular#26632 and closes: angular#26057
1 parent 68dfe3b commit cf608dd

File tree

10 files changed

+568
-120
lines changed

10 files changed

+568
-120
lines changed

goldens/public-api/angular_devkit/build_angular/index.md

+5-8
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface ApplicationBuilderOptions {
4646
namedChunks?: boolean;
4747
optimization?: OptimizationUnion_2;
4848
outputHashing?: OutputHashing_2;
49-
outputPath: string;
49+
outputPath: OutputPathUnion;
5050
poll?: number;
5151
polyfills?: string[];
5252
prerender?: PrerenderUnion;
@@ -149,13 +149,10 @@ export interface Budget {
149149
}
150150

151151
// @public
152-
export function buildApplication(options: ApplicationBuilderOptions, context: BuilderContext, plugins?: Plugin_2[]): AsyncIterable<BuilderOutput & {
153-
outputFiles?: BuildOutputFile[];
154-
assetFiles?: {
155-
source: string;
156-
destination: string;
157-
}[];
158-
}>;
152+
export function buildApplication(options: ApplicationBuilderOptions, context: BuilderContext, plugins?: Plugin_2[]): AsyncIterable<ApplicationBuilderOutput>;
153+
154+
// @public
155+
export function buildApplication(options: ApplicationBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): AsyncIterable<ApplicationBuilderOutput>;
159156

160157
// @public
161158
export enum CrossOrigin {

packages/angular_devkit/build_angular/src/builders/application/build-action.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbui
1717
import { deleteOutputDir } from '../../utils/delete-output-dir';
1818
import { shouldWatchRoot } from '../../utils/environment-options';
1919
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
20+
import { NormalizedOutputOptions } from './options';
2021

2122
export async function* runEsBuildBuildAction(
2223
action: (rebuildState?: RebuildState) => ExecutionResult | Promise<ExecutionResult>,
2324
options: {
2425
workspaceRoot: string;
2526
projectRoot: string;
26-
outputPath: string;
27+
outputOptions: NormalizedOutputOptions;
2728
logger: logging.LoggerApi;
2829
cacheOptions: NormalizedCachedOptions;
29-
writeToFileSystem?: boolean;
30-
writeToFileSystemFilter?: (file: BuildOutputFile) => boolean;
30+
writeToFileSystem: boolean;
31+
writeToFileSystemFilter: ((file: BuildOutputFile) => boolean) | undefined;
3132
watch?: boolean;
3233
verbose?: boolean;
3334
progress?: boolean;
@@ -39,13 +40,13 @@ export async function* runEsBuildBuildAction(
3940
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
4041
const {
4142
writeToFileSystemFilter,
42-
writeToFileSystem = true,
43+
writeToFileSystem,
4344
watch,
4445
poll,
4546
logger,
4647
deleteOutputPath,
4748
cacheOptions,
48-
outputPath,
49+
outputOptions,
4950
verbose,
5051
projectRoot,
5152
workspaceRoot,
@@ -54,7 +55,10 @@ export async function* runEsBuildBuildAction(
5455
} = options;
5556

5657
if (deleteOutputPath && writeToFileSystem) {
57-
await deleteOutputDir(workspaceRoot, outputPath, ['browser', 'server']);
58+
await deleteOutputDir(workspaceRoot, outputOptions.base, [
59+
outputOptions.browser,
60+
outputOptions.server,
61+
]);
5862
}
5963

6064
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
@@ -79,7 +83,7 @@ export async function* runEsBuildBuildAction(
7983

8084
const ignored: string[] = [
8185
// Ignore the output and cache paths to avoid infinite rebuild cycles
82-
outputPath,
86+
outputOptions.base,
8387
cacheOptions.basePath,
8488
`${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`,
8589
];
@@ -137,7 +141,7 @@ export async function* runEsBuildBuildAction(
137141
// unit tests which execute the builder and modify the file system programmatically.
138142
if (writeToFileSystem) {
139143
// Write output files
140-
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
144+
await writeResultFiles(result.outputFiles, result.assetFiles, outputOptions);
141145

142146
yield result.output;
143147
} else {
@@ -191,7 +195,7 @@ export async function* runEsBuildBuildAction(
191195
const filesToWrite = writeToFileSystemFilter
192196
? result.outputFiles.filter(writeToFileSystemFilter)
193197
: result.outputFiles;
194-
await writeResultFiles(filesToWrite, result.assetFiles, outputPath);
198+
await writeResultFiles(filesToWrite, result.assetFiles, outputOptions);
195199

196200
yield result.output;
197201
} else {

packages/angular_devkit/build_angular/src/builders/application/index.ts

+83-29
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { purgeStaleBuildCache } from '../../utils/purge-cache';
1313
import { assertCompatibleAngularVersion } from '../../utils/version';
1414
import { runEsBuildBuildAction } from './build-action';
1515
import { executeBuild } from './execute-build';
16-
import { ApplicationBuilderInternalOptions, normalizeOptions } from './options';
16+
import {
17+
ApplicationBuilderExtensions,
18+
ApplicationBuilderInternalOptions,
19+
normalizeOptions,
20+
} from './options';
1721
import { Schema as ApplicationBuilderOptions } from './schema';
1822

1923
export { ApplicationBuilderOptions };
@@ -25,28 +29,49 @@ export async function* buildApplicationInternal(
2529
infrastructureSettings?: {
2630
write?: boolean;
2731
},
28-
plugins?: Plugin[],
29-
): AsyncIterable<
30-
BuilderOutput & {
31-
outputFiles?: BuildOutputFile[];
32-
assetFiles?: { source: string; destination: string }[];
33-
}
34-
> {
32+
extensions?: ApplicationBuilderExtensions,
33+
): AsyncIterable<ApplicationBuilderOutput> {
34+
const { workspaceRoot, logger, target } = context;
35+
3536
// Check Angular version.
36-
assertCompatibleAngularVersion(context.workspaceRoot);
37+
assertCompatibleAngularVersion(workspaceRoot);
3738

3839
// Purge old build disk cache.
3940
await purgeStaleBuildCache(context);
4041

4142
// Determine project name from builder context target
42-
const projectName = context.target?.project;
43+
const projectName = target?.project;
4344
if (!projectName) {
44-
context.logger.error(`The 'application' builder requires a target to be specified.`);
45+
yield { success: false, error: `The 'application' builder requires a target to be specified.` };
4546

4647
return;
4748
}
4849

49-
const normalizedOptions = await normalizeOptions(context, projectName, options, plugins);
50+
const normalizedOptions = await normalizeOptions(context, projectName, options, extensions);
51+
const writeToFileSystem = infrastructureSettings?.write ?? true;
52+
const writeServerBundles =
53+
writeToFileSystem && !!(normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint);
54+
55+
if (writeServerBundles) {
56+
const { browser, server } = normalizedOptions.outputOptions;
57+
if (browser === '') {
58+
yield {
59+
success: false,
60+
error: `'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`,
61+
};
62+
63+
return;
64+
}
65+
66+
if (browser === server) {
67+
yield {
68+
success: false,
69+
error: `'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`,
70+
};
71+
72+
return;
73+
}
74+
}
5075

5176
// Setup an abort controller with a builder teardown if no signal is present
5277
let signal = context.signal;
@@ -59,14 +84,11 @@ export async function* buildApplicationInternal(
5984
yield* runEsBuildBuildAction(
6085
async (rebuildState) => {
6186
const startTime = process.hrtime.bigint();
62-
6387
const result = await executeBuild(normalizedOptions, context, rebuildState);
6488

6589
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
6690
const status = result.errors.length > 0 ? 'failed' : 'complete';
67-
context.logger.info(
68-
`Application bundle generation ${status}. [${buildTime.toFixed(3)} seconds]`,
69-
);
91+
logger.info(`Application bundle generation ${status}. [${buildTime.toFixed(3)} seconds]`);
7092

7193
return result;
7294
},
@@ -76,24 +98,28 @@ export async function* buildApplicationInternal(
7698
poll: normalizedOptions.poll,
7799
deleteOutputPath: normalizedOptions.deleteOutputPath,
78100
cacheOptions: normalizedOptions.cacheOptions,
79-
outputPath: normalizedOptions.outputPath,
101+
outputOptions: normalizedOptions.outputOptions,
80102
verbose: normalizedOptions.verbose,
81103
projectRoot: normalizedOptions.projectRoot,
82104
workspaceRoot: normalizedOptions.workspaceRoot,
83105
progress: normalizedOptions.progress,
84-
writeToFileSystem: infrastructureSettings?.write,
106+
writeToFileSystem,
85107
// For app-shell and SSG server files are not required by users.
86108
// Omit these when SSR is not enabled.
87-
writeToFileSystemFilter:
88-
normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint
89-
? undefined
90-
: (file) => file.type !== BuildOutputFileType.Server,
91-
logger: context.logger,
109+
writeToFileSystemFilter: writeServerBundles
110+
? undefined
111+
: (file) => file.type !== BuildOutputFileType.Server,
112+
logger,
92113
signal,
93114
},
94115
);
95116
}
96117

118+
export interface ApplicationBuilderOutput extends BuilderOutput {
119+
outputFiles?: BuildOutputFile[];
120+
assetFiles?: { source: string; destination: string }[];
121+
}
122+
97123
/**
98124
* Builds an application using the `application` builder with the provided
99125
* options.
@@ -112,13 +138,41 @@ export function buildApplication(
112138
options: ApplicationBuilderOptions,
113139
context: BuilderContext,
114140
plugins?: Plugin[],
115-
): AsyncIterable<
116-
BuilderOutput & {
117-
outputFiles?: BuildOutputFile[];
118-
assetFiles?: { source: string; destination: string }[];
141+
): AsyncIterable<ApplicationBuilderOutput>;
142+
143+
/**
144+
* Builds an application using the `application` builder with the provided
145+
* options.
146+
*
147+
* Usage of the `extensions` parameter is NOT supported and may cause unexpected
148+
* build output or build failures.
149+
*
150+
* @experimental Direct usage of this function is considered experimental.
151+
*
152+
* @param options The options defined by the builder's schema to use.
153+
* @param context An Architect builder context instance.
154+
* @param extensions An object contain extension points for the build.
155+
* @returns The build output results of the build.
156+
*/
157+
export function buildApplication(
158+
options: ApplicationBuilderOptions,
159+
context: BuilderContext,
160+
extensions?: ApplicationBuilderExtensions,
161+
): AsyncIterable<ApplicationBuilderOutput>;
162+
163+
export function buildApplication(
164+
options: ApplicationBuilderOptions,
165+
context: BuilderContext,
166+
pluginsOrExtensions?: Plugin[] | ApplicationBuilderExtensions,
167+
): AsyncIterable<ApplicationBuilderOutput> {
168+
let extensions;
169+
if (pluginsOrExtensions && Array.isArray(pluginsOrExtensions)) {
170+
extensions = {
171+
codePlugins: pluginsOrExtensions,
172+
};
119173
}
120-
> {
121-
return buildApplicationInternal(options, context, undefined, plugins);
174+
175+
return buildApplicationInternal(options, context, undefined, extensions);
122176
}
123177

124178
export default createBuilder(buildApplication);

0 commit comments

Comments
 (0)