Skip to content

feat(@angular-devkit/build-angular): allow customization of output locations #26675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/public-api/angular_devkit/build_angular/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface ApplicationBuilderOptions {
namedChunks?: boolean;
optimization?: OptimizationUnion_2;
outputHashing?: OutputHashing_2;
outputPath: string;
outputPath: OutputPathUnion;
poll?: number;
polyfills?: string[];
prerender?: PrerenderUnion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbui
import { deleteOutputDir } from '../../utils/delete-output-dir';
import { shouldWatchRoot } from '../../utils/environment-options';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
import { NormalizedOutputOptions } from './options';

export async function* runEsBuildBuildAction(
action: (rebuildState?: RebuildState) => ExecutionResult | Promise<ExecutionResult>,
options: {
workspaceRoot: string;
projectRoot: string;
outputPath: string;
outputOptions: NormalizedOutputOptions;
logger: logging.LoggerApi;
cacheOptions: NormalizedCachedOptions;
writeToFileSystem?: boolean;
writeToFileSystemFilter?: (file: BuildOutputFile) => boolean;
writeToFileSystem: boolean;
writeToFileSystemFilter: ((file: BuildOutputFile) => boolean) | undefined;
watch?: boolean;
verbose?: boolean;
progress?: boolean;
Expand All @@ -39,13 +40,13 @@ export async function* runEsBuildBuildAction(
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
const {
writeToFileSystemFilter,
writeToFileSystem = true,
writeToFileSystem,
watch,
poll,
logger,
deleteOutputPath,
cacheOptions,
outputPath,
outputOptions,
verbose,
projectRoot,
workspaceRoot,
Expand All @@ -54,7 +55,10 @@ export async function* runEsBuildBuildAction(
} = options;

if (deleteOutputPath && writeToFileSystem) {
await deleteOutputDir(workspaceRoot, outputPath, ['browser', 'server']);
await deleteOutputDir(workspaceRoot, outputOptions.base, [
outputOptions.browser,
outputOptions.server,
]);
}

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

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

yield result.output;
} else {
Expand Down Expand Up @@ -191,7 +195,7 @@ export async function* runEsBuildBuildAction(
const filesToWrite = writeToFileSystemFilter
? result.outputFiles.filter(writeToFileSystemFilter)
: result.outputFiles;
await writeResultFiles(filesToWrite, result.assetFiles, outputPath);
await writeResultFiles(filesToWrite, result.assetFiles, outputOptions);

yield result.output;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,47 @@ export async function* buildApplicationInternal(
},
extensions?: ApplicationBuilderExtensions,
): AsyncIterable<ApplicationBuilderOutput> {
const { workspaceRoot, logger, target } = context;

// Check Angular version.
assertCompatibleAngularVersion(context.workspaceRoot);
assertCompatibleAngularVersion(workspaceRoot);

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

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

return;
}

const normalizedOptions = await normalizeOptions(context, projectName, options, extensions);
const writeToFileSystem = infrastructureSettings?.write ?? true;
const writeServerBundles =
writeToFileSystem && !!(normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint);

if (writeServerBundles) {
const { browser, server } = normalizedOptions.outputOptions;
if (browser === '') {
yield {
success: false,
error: `'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`,
};

return;
}

if (browser === server) {
yield {
success: false,
error: `'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`,
};

return;
}
}

// Setup an abort controller with a builder teardown if no signal is present
let signal = context.signal;
Expand All @@ -58,14 +84,11 @@ export async function* buildApplicationInternal(
yield* runEsBuildBuildAction(
async (rebuildState) => {
const startTime = process.hrtime.bigint();

const result = await executeBuild(normalizedOptions, context, rebuildState);

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

return result;
},
Expand All @@ -75,19 +98,18 @@ export async function* buildApplicationInternal(
poll: normalizedOptions.poll,
deleteOutputPath: normalizedOptions.deleteOutputPath,
cacheOptions: normalizedOptions.cacheOptions,
outputPath: normalizedOptions.outputPath,
outputOptions: normalizedOptions.outputOptions,
verbose: normalizedOptions.verbose,
projectRoot: normalizedOptions.projectRoot,
workspaceRoot: normalizedOptions.workspaceRoot,
progress: normalizedOptions.progress,
writeToFileSystem: infrastructureSettings?.write,
writeToFileSystem,
// 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,
writeToFileSystemFilter: writeServerBundles
? undefined
: (file) => file.type !== BuildOutputFileType.Server,
logger,
signal,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ import { normalizeCacheOptions } from '../../utils/normalize-cache';
import { generateEntryPoints } from '../../utils/package-chunk-sort';
import { findTailwindConfigurationFile } from '../../utils/tailwind';
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
import { Schema as ApplicationBuilderOptions, I18NTranslation, OutputHashing } from './schema';
import {
Schema as ApplicationBuilderOptions,
I18NTranslation,
OutputHashing,
OutputPathClass,
} from './schema';

export type NormalizedOutputOptions = Required<OutputPathClass>;
export type NormalizedApplicationBuildOptions = Awaited<ReturnType<typeof normalizeOptions>>;

export interface ApplicationBuilderExtensions {
Expand Down Expand Up @@ -125,23 +131,33 @@ export async function normalizeOptions(

const entryPoints = normalizeEntryPoints(workspaceRoot, options.browser, options.entryPoints);
const tsconfig = path.join(workspaceRoot, options.tsConfig);
const outputPath = normalizeDirectoryPath(path.join(workspaceRoot, options.outputPath));
const optimizationOptions = normalizeOptimization(options.optimization);
const sourcemapOptions = normalizeSourceMaps(options.sourceMap ?? false);
const assets = options.assets?.length
? normalizeAssetPatterns(options.assets, workspaceRoot, projectRoot, projectSourceRoot)
: undefined;

const outputPath = options.outputPath;
const outputOptions: NormalizedOutputOptions = {
browser: 'browser',
server: 'server',
media: 'media',
...(typeof outputPath === 'string' ? undefined : outputPath),
base: normalizeDirectoryPath(
path.join(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base),
),
};

const outputNames = {
bundles:
options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Bundles
? '[name]-[hash]'
: '[name]',
media:
'media/' +
outputOptions.media +
(options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Media
? '[name]-[hash]'
: '[name]'),
? '/[name]-[hash]'
: '/[name]'),
};

let fileReplacements: Record<string, string> | undefined;
Expand Down Expand Up @@ -191,26 +207,6 @@ export async function normalizeOptions(
}
}

let tailwindConfiguration: { file: string; package: string } | undefined;
const tailwindConfigurationPath = await findTailwindConfigurationFile(workspaceRoot, projectRoot);
if (tailwindConfigurationPath) {
// Create a node resolver at the project root as a directory
const resolver = createRequire(projectRoot + '/');
try {
tailwindConfiguration = {
file: tailwindConfigurationPath,
package: resolver.resolve('tailwindcss'),
};
} catch {
const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath);
context.logger.warn(
`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
` but the 'tailwindcss' package is not installed.` +
` To enable Tailwind CSS, please install the 'tailwindcss' package.`,
);
}
}

let indexHtmlOptions;
// index can never have a value of `true` but in the schema it's of type `boolean`.
if (typeof options.index !== 'boolean') {
Expand Down Expand Up @@ -318,7 +314,7 @@ export async function normalizeOptions(
workspaceRoot,
entryPoints,
optimizationOptions,
outputPath,
outputOptions,
outExtension,
sourcemapOptions,
tsconfig,
Expand All @@ -331,7 +327,7 @@ export async function normalizeOptions(
serviceWorker:
typeof serviceWorker === 'string' ? path.join(workspaceRoot, serviceWorker) : undefined,
indexHtmlOptions,
tailwindConfiguration,
tailwindConfiguration: await getTailwindConfig(workspaceRoot, projectRoot, context),
i18nOptions,
namedChunks,
budgets: budgets?.length ? budgets : undefined,
Expand All @@ -341,6 +337,36 @@ export async function normalizeOptions(
};
}

async function getTailwindConfig(
workspaceRoot: string,
projectRoot: string,
context: BuilderContext,
): Promise<{ file: string; package: string } | undefined> {
const tailwindConfigurationPath = await findTailwindConfigurationFile(workspaceRoot, projectRoot);

if (!tailwindConfigurationPath) {
return undefined;
}

// Create a node resolver at the project root as a directory
const resolver = createRequire(projectRoot + '/');
try {
return {
file: tailwindConfigurationPath,
package: resolver.resolve('tailwindcss'),
};
} catch {
const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath);
context.logger.warn(
`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
` but the 'tailwindcss' package is not installed.` +
` To enable Tailwind CSS, please install the 'tailwindcss' package.`,
);
}

return undefined;
}

/**
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser`
* option which defines a single entry point. However, we also want to support multiple entry points as an internal option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,41 @@
"default": []
},
"outputPath": {
"type": "string",
"description": "The full path for the new output directory, relative to the current workspace."
"description": "Specify the output path relative to workspace root.",
"oneOf": [
{
"type": "object",
"properties": {
"base": {
"type": "string",
"description": "Specify the output path relative to workspace root."
},
"browser": {
"type": "string",
"pattern": "^[-\\w\\.]*$",
"default": "browser",
"description": "The output directory name of your browser build within the output path base. Defaults to 'browser'."
},
"server": {
"type": "string",
"pattern": "^[-\\w\\.]*$",
"default": "server",
"description": "The output directory name of your server build within the output path base. Defaults to 'server'."
},
"media": {
"type": "string",
"pattern": "^[-\\w\\.]+$",
"default": "media",
"description": "The output directory name of your media files within the output browser directory. Defaults to 'media'."
}
},
"required": ["base"],
"additionalProperties": false
},
{
"type": "string"
}
]
},
"aot": {
"type": "boolean",
Expand Down
Loading