Skip to content

Commit da9e152

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. - `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 66edac4 commit da9e152

File tree

11 files changed

+485
-97
lines changed

11 files changed

+485
-97
lines changed

goldens/circular-deps/packages.json

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
[
2+
[
3+
"packages/angular_devkit/build_angular/src/builders/application/options.ts",
4+
"packages/angular_devkit/build_angular/src/utils/index.ts",
5+
"packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts"
6+
],
7+
[
8+
"packages/angular_devkit/build_angular/src/builders/application/options.ts",
9+
"packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts",
10+
"packages/angular_devkit/build_angular/src/utils/index.ts",
11+
"packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts"
12+
],
213
[
314
"packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts",
415
"packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts"

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

+1-1
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;

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

+10-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,7 @@ export async function* runEsBuildBuildAction(
5455
} = options;
5556

5657
if (deleteOutputPath && writeToFileSystem) {
57-
await deleteOutputDir(workspaceRoot, outputPath, ['browser', 'server']);
58+
await deleteOutputDir(workspaceRoot, outputOptions);
5859
}
5960

6061
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
@@ -79,7 +80,7 @@ export async function* runEsBuildBuildAction(
7980

8081
const ignored: string[] = [
8182
// Ignore the output and cache paths to avoid infinite rebuild cycles
82-
outputPath,
83+
outputOptions.base,
8384
cacheOptions.basePath,
8485
`${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`,
8586
];
@@ -137,7 +138,7 @@ export async function* runEsBuildBuildAction(
137138
// unit tests which execute the builder and modify the file system programmatically.
138139
if (writeToFileSystem) {
139140
// Write output files
140-
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
141+
await writeResultFiles(result.outputFiles, result.assetFiles, outputOptions);
141142

142143
yield result.output;
143144
} else {
@@ -191,7 +192,7 @@ export async function* runEsBuildBuildAction(
191192
const filesToWrite = writeToFileSystemFilter
192193
? result.outputFiles.filter(writeToFileSystemFilter)
193194
: result.outputFiles;
194-
await writeResultFiles(filesToWrite, result.assetFiles, outputPath);
195+
await writeResultFiles(filesToWrite, result.assetFiles, outputOptions);
195196

196197
yield result.output;
197198
} else {

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

+23-14
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,34 @@ export async function* buildApplicationInternal(
3131
},
3232
extensions?: ApplicationBuilderExtensions,
3333
): AsyncIterable<ApplicationBuilderOutput> {
34+
const { workspaceRoot, logger, target } = context;
35+
3436
// Check Angular version.
35-
assertCompatibleAngularVersion(context.workspaceRoot);
37+
assertCompatibleAngularVersion(workspaceRoot);
3638

3739
// Purge old build disk cache.
3840
await purgeStaleBuildCache(context);
3941

4042
// Determine project name from builder context target
41-
const projectName = context.target?.project;
43+
const projectName = target?.project;
4244
if (!projectName) {
43-
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.` };
4446

4547
return;
4648
}
4749

4850
const normalizedOptions = await normalizeOptions(context, projectName, options, extensions);
51+
const writeToFileSystem = infrastructureSettings?.write ?? true;
52+
const writeServerBundles = !!(normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint);
53+
54+
if (writeToFileSystem && writeServerBundles && normalizedOptions.outputOptions.browser === '') {
55+
yield {
56+
success: false,
57+
error: `'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`,
58+
};
59+
60+
return;
61+
}
4962

5063
// Setup an abort controller with a builder teardown if no signal is present
5164
let signal = context.signal;
@@ -58,14 +71,11 @@ export async function* buildApplicationInternal(
5871
yield* runEsBuildBuildAction(
5972
async (rebuildState) => {
6073
const startTime = process.hrtime.bigint();
61-
6274
const result = await executeBuild(normalizedOptions, context, rebuildState);
6375

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

7080
return result;
7181
},
@@ -75,19 +85,18 @@ export async function* buildApplicationInternal(
7585
poll: normalizedOptions.poll,
7686
deleteOutputPath: normalizedOptions.deleteOutputPath,
7787
cacheOptions: normalizedOptions.cacheOptions,
78-
outputPath: normalizedOptions.outputPath,
88+
outputOptions: normalizedOptions.outputOptions,
7989
verbose: normalizedOptions.verbose,
8090
projectRoot: normalizedOptions.projectRoot,
8191
workspaceRoot: normalizedOptions.workspaceRoot,
8292
progress: normalizedOptions.progress,
83-
writeToFileSystem: infrastructureSettings?.write,
93+
writeToFileSystem,
8494
// For app-shell and SSG server files are not required by users.
8595
// Omit these when SSR is not enabled.
86-
writeToFileSystemFilter:
87-
normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint
88-
? undefined
89-
: (file) => file.type !== BuildOutputFileType.Server,
90-
logger: context.logger,
96+
writeToFileSystemFilter: writeServerBundles
97+
? undefined
98+
: (file) => file.type !== BuildOutputFileType.Server,
99+
logger,
91100
signal,
92101
},
93102
);

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

+53-27
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ import { normalizeCacheOptions } from '../../utils/normalize-cache';
2323
import { generateEntryPoints } from '../../utils/package-chunk-sort';
2424
import { findTailwindConfigurationFile } from '../../utils/tailwind';
2525
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
26-
import { Schema as ApplicationBuilderOptions, I18NTranslation, OutputHashing } from './schema';
26+
import {
27+
Schema as ApplicationBuilderOptions,
28+
I18NTranslation,
29+
OutputHashing,
30+
OutputPathClass,
31+
} from './schema';
2732

33+
export type NormalizedOutputOptions = Required<OutputPathClass>;
2834
export type NormalizedApplicationBuildOptions = Awaited<ReturnType<typeof normalizeOptions>>;
2935

3036
export interface ApplicationBuilderExtensions {
@@ -125,23 +131,33 @@ export async function normalizeOptions(
125131

126132
const entryPoints = normalizeEntryPoints(workspaceRoot, options.browser, options.entryPoints);
127133
const tsconfig = path.join(workspaceRoot, options.tsConfig);
128-
const outputPath = normalizeDirectoryPath(path.join(workspaceRoot, options.outputPath));
129134
const optimizationOptions = normalizeOptimization(options.optimization);
130135
const sourcemapOptions = normalizeSourceMaps(options.sourceMap ?? false);
131136
const assets = options.assets?.length
132137
? normalizeAssetPatterns(options.assets, workspaceRoot, projectRoot, projectSourceRoot)
133138
: undefined;
134139

140+
const outputPath = options.outputPath;
141+
const outputOptions: NormalizedOutputOptions = {
142+
browser: 'browser',
143+
server: 'server',
144+
media: 'media',
145+
...(typeof outputPath === 'string' ? undefined : outputPath),
146+
base: normalizeDirectoryPath(
147+
path.join(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base),
148+
),
149+
};
150+
135151
const outputNames = {
136152
bundles:
137153
options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Bundles
138154
? '[name]-[hash]'
139155
: '[name]',
140156
media:
141-
'media/' +
157+
outputOptions.media +
142158
(options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Media
143-
? '[name]-[hash]'
144-
: '[name]'),
159+
? '/[name]-[hash]'
160+
: '/[name]'),
145161
};
146162

147163
let fileReplacements: Record<string, string> | undefined;
@@ -191,26 +207,6 @@ export async function normalizeOptions(
191207
}
192208
}
193209

194-
let tailwindConfiguration: { file: string; package: string } | undefined;
195-
const tailwindConfigurationPath = await findTailwindConfigurationFile(workspaceRoot, projectRoot);
196-
if (tailwindConfigurationPath) {
197-
// Create a node resolver at the project root as a directory
198-
const resolver = createRequire(projectRoot + '/');
199-
try {
200-
tailwindConfiguration = {
201-
file: tailwindConfigurationPath,
202-
package: resolver.resolve('tailwindcss'),
203-
};
204-
} catch {
205-
const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath);
206-
context.logger.warn(
207-
`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
208-
` but the 'tailwindcss' package is not installed.` +
209-
` To enable Tailwind CSS, please install the 'tailwindcss' package.`,
210-
);
211-
}
212-
}
213-
214210
let indexHtmlOptions;
215211
// index can never have a value of `true` but in the schema it's of type `boolean`.
216212
if (typeof options.index !== 'boolean') {
@@ -318,7 +314,7 @@ export async function normalizeOptions(
318314
workspaceRoot,
319315
entryPoints,
320316
optimizationOptions,
321-
outputPath,
317+
outputOptions,
322318
outExtension,
323319
sourcemapOptions,
324320
tsconfig,
@@ -331,7 +327,7 @@ export async function normalizeOptions(
331327
serviceWorker:
332328
typeof serviceWorker === 'string' ? path.join(workspaceRoot, serviceWorker) : undefined,
333329
indexHtmlOptions,
334-
tailwindConfiguration,
330+
tailwindConfiguration: await getTailwindConfig(workspaceRoot, projectRoot, context),
335331
i18nOptions,
336332
namedChunks,
337333
budgets: budgets?.length ? budgets : undefined,
@@ -341,6 +337,36 @@ export async function normalizeOptions(
341337
};
342338
}
343339

340+
async function getTailwindConfig(
341+
workspaceRoot: string,
342+
projectRoot: string,
343+
context: BuilderContext,
344+
): Promise<{ file: string; package: string } | undefined> {
345+
const tailwindConfigurationPath = await findTailwindConfigurationFile(workspaceRoot, projectRoot);
346+
347+
if (!tailwindConfigurationPath) {
348+
return undefined;
349+
}
350+
351+
// Create a node resolver at the project root as a directory
352+
const resolver = createRequire(projectRoot + '/');
353+
try {
354+
return {
355+
file: tailwindConfigurationPath,
356+
package: resolver.resolve('tailwindcss'),
357+
};
358+
} catch {
359+
const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath);
360+
context.logger.warn(
361+
`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
362+
` but the 'tailwindcss' package is not installed.` +
363+
` To enable Tailwind CSS, please install the 'tailwindcss' package.`,
364+
);
365+
}
366+
367+
return undefined;
368+
}
369+
344370
/**
345371
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser`
346372
* option which defines a single entry point. However, we also want to support multiple entry points as an internal option.

packages/angular_devkit/build_angular/src/builders/application/schema.json

+35-2
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,41 @@
220220
"default": []
221221
},
222222
"outputPath": {
223-
"type": "string",
224-
"description": "The full path for the new output directory, relative to the current workspace."
223+
"description": "Specify the output path relative to workspace root.",
224+
"oneOf": [
225+
{
226+
"type": "object",
227+
"properties": {
228+
"base": {
229+
"type": "string",
230+
"description": "Specify the output path relative to workspace root."
231+
},
232+
"browser": {
233+
"type": "string",
234+
"pattern": "^[-\\w\\.]*$",
235+
"default": "browser",
236+
"description": "The output directory name of your browser build, relative to the output root. Defaults to 'browser'."
237+
},
238+
"server": {
239+
"type": "string",
240+
"pattern": "^[-\\w\\.]*$",
241+
"default": "server",
242+
"description": "The output directory name of your server build, relative to the output root. Defaults to 'server'."
243+
},
244+
"media": {
245+
"type": "string",
246+
"pattern": "^[-\\w\\.]+$",
247+
"default": "media",
248+
"description": "The output directory name of your media files. This is relative to the browser directory. Defaults to 'media'."
249+
}
250+
},
251+
"required": ["base"],
252+
"additionalProperties": false
253+
},
254+
{
255+
"type": "string"
256+
}
257+
]
225258
},
226259
"aot": {
227260
"type": "boolean",

0 commit comments

Comments
 (0)