Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
build,
formatMessages,
} from 'esbuild';
import { basename, extname, relative } from 'node:path';
import { FileInfo } from '../../utils/index-file/augment-index-html';

/**
* Determines if an unknown value is an esbuild BuildFailure error object thrown by esbuild.
Expand All @@ -39,16 +41,20 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
* warnings and errors for the attempted build.
*/
export async function bundle(
workspaceRoot: string,
optionsOrInvalidate: BuildOptions | BuildInvalidate,
): Promise<
(BuildResult & { outputFiles: OutputFile[] }) | (BuildFailure & { outputFiles?: never })
| (BuildResult & { outputFiles: OutputFile[]; initialFiles: FileInfo[] })
| (BuildFailure & { outputFiles?: never })
> {
let result;
try {
if (typeof optionsOrInvalidate === 'function') {
return (await optionsOrInvalidate()) as BuildResult & { outputFiles: OutputFile[] };
result = (await optionsOrInvalidate()) as BuildResult & { outputFiles: OutputFile[] };
} else {
return await build({
result = await build({
...optionsOrInvalidate,
metafile: true,
write: false,
});
}
Expand All @@ -60,6 +66,27 @@ export async function bundle(
throw failure;
}
}

const initialFiles: FileInfo[] = [];
for (const outputFile of result.outputFiles) {
// Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
const relativeFilePath = relative(workspaceRoot, outputFile.path);
const entryPoint = result.metafile?.outputs[relativeFilePath]?.entryPoint;

outputFile.path = relativeFilePath;

if (entryPoint) {
// An entryPoint value indicates an initial file
initialFiles.push({
file: outputFile.path,
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
name: basename(outputFile.path).split('.')[0],
extension: extname(outputFile.path),
});
}
}

return { ...result, initialFiles };
}

export async function logMessages(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as assert from 'assert';
import type { BuildInvalidate, BuildOptions, Message, OutputFile } from 'esbuild';
import * as fs from 'fs/promises';
import * as path from 'path';
import type { BuildInvalidate, BuildOptions, OutputFile } from 'esbuild';
import assert from 'node:assert';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { deleteOutputDir } from '../../utils';
import { copyAssets } from '../../utils/copy-assets';
import { assertIsError } from '../../utils/error';
Expand All @@ -25,11 +25,12 @@ import { logExperimentalWarnings } from './experimental-warnings';
import { NormalizedBrowserOptions, normalizeOptions } from './options';
import { shutdownSassWorkerPool } from './sass-plugin';
import { Schema as BrowserBuilderOptions } from './schema';
import { bundleStylesheetText } from './stylesheets';
import { createStylesheetBundleOptions } from './stylesheets';
import { ChangedFiles, createWatcher } from './watcher';

interface RebuildState {
codeRebuild?: BuildInvalidate;
globalStylesRebuild?: BuildInvalidate;
codeBundleCache?: SourceFileCache;
fileChanges: ChangedFiles;
}
Expand All @@ -41,6 +42,7 @@ class ExecutionResult {
constructor(
private success: boolean,
private codeRebuild?: BuildInvalidate,
private globalStylesRebuild?: BuildInvalidate,
private codeBundleCache?: SourceFileCache,
) {}

Expand All @@ -55,6 +57,7 @@ class ExecutionResult {

return {
codeRebuild: this.codeRebuild,
globalStylesRebuild: this.globalStylesRebuild,
codeBundleCache: this.codeBundleCache,
fileChanges,
};
Expand Down Expand Up @@ -92,9 +95,15 @@ async function execute(

const [codeResults, styleResults] = await Promise.all([
// Execute esbuild to bundle the application code
bundle(rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache)),
bundle(
workspaceRoot,
rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache),
),
// Execute esbuild to bundle the global stylesheets
bundleGlobalStylesheets(options, target),
bundle(
workspaceRoot,
rebuildState?.globalStylesRebuild ?? createGlobalStylesBundleOptions(options, target),
),
]);

// Log all warnings and errors generated during bundling
Expand All @@ -105,39 +114,32 @@ async function execute(

// Return if the bundling failed to generate output files or there are errors
if (!codeResults.outputFiles || codeResults.errors.length) {
return new ExecutionResult(false, rebuildState?.codeRebuild, codeBundleCache);
return new ExecutionResult(
false,
rebuildState?.codeRebuild,
(styleResults.outputFiles && styleResults.rebuild) ?? rebuildState?.globalStylesRebuild,
codeBundleCache,
);
}

// Structure the code bundling output files
const initialFiles: FileInfo[] = [];
const outputFiles: OutputFile[] = [];
for (const outputFile of codeResults.outputFiles) {
// Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
const relativeFilePath = path.relative(workspaceRoot, outputFile.path);
const entryPoint = codeResults.metafile?.outputs[relativeFilePath]?.entryPoint;

outputFile.path = relativeFilePath;

if (entryPoint) {
// An entryPoint value indicates an initial file
initialFiles.push({
file: outputFile.path,
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
name: path.basename(outputFile.path).split('.')[0],
extension: path.extname(outputFile.path),
});
}
outputFiles.push(outputFile);
// Return if the global stylesheet bundling has errors
if (!styleResults.outputFiles || styleResults.errors.length) {
return new ExecutionResult(
false,
codeResults.rebuild,
rebuildState?.globalStylesRebuild,
codeBundleCache,
);
}

// Add global stylesheets output files
outputFiles.push(...styleResults.outputFiles);
initialFiles.push(...styleResults.initialFiles);
// Filter global stylesheet initial files
styleResults.initialFiles = styleResults.initialFiles.filter(
({ name }) => options.globalStyles.find((style) => style.name === name)?.initial,
);

// Return if the global stylesheet bundling has errors
if (styleResults.errors.length) {
return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
}
// Combine the bundling output files
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];

// Generate index HTML file
if (indexHtmlOptions) {
Expand Down Expand Up @@ -203,14 +205,14 @@ async function execute(
} catch (error) {
context.logger.error(error instanceof Error ? error.message : `${error}`);

return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
return new ExecutionResult(false, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
}
}

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

return new ExecutionResult(true, codeResults.rebuild, codeBundleCache);
return new ExecutionResult(true, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
}

function createOutputFileFromText(path: string, text: string): OutputFile {
Expand Down Expand Up @@ -268,7 +270,6 @@ function createCodeBundleOptions(
conditions: ['es2020', 'es2015', 'module'],
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'],
logLevel: options.verbose ? 'debug' : 'silent',
metafile: true,
minify: optimizationOptions.scripts,
pure: ['forwardRef'],
outdir: workspaceRoot,
Expand Down Expand Up @@ -313,7 +314,10 @@ function createCodeBundleOptions(
};
}

async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target: string[]) {
function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
): BuildOptions {
const {
workspaceRoot,
optimizationOptions,
Expand All @@ -323,70 +327,54 @@ async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target
preserveSymlinks,
externalDependencies,
stylePreprocessorOptions,
watch,
} = options;

const outputFiles: OutputFile[] = [];
const initialFiles: FileInfo[] = [];
const errors: Message[] = [];
const warnings: Message[] = [];

for (const { name, files, initial } of globalStyles) {
const virtualEntryData = files
.map((file) => `@import '${file.replace(/\\/g, '/')}';`)
.join('\n');
const sheetResult = await bundleStylesheetText(
virtualEntryData,
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
outputNames: initial ? outputNames : { media: outputNames.media },
includePaths: stylePreprocessorOptions?.includePaths,
preserveSymlinks,
externalDependencies,
target,
},
);

errors.push(...sheetResult.errors);
warnings.push(...sheetResult.warnings);

if (!sheetResult.path) {
// Failed to process the stylesheet
assert.ok(
sheetResult.errors.length,
`Global stylesheet processing for '${name}' failed with no errors.`,
);

continue;
}
const buildOptions = createStylesheetBundleOptions({
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
});
buildOptions.incremental = watch;

// The virtual stylesheets will be named `stdin` by esbuild. This must be replaced
// with the actual name of the global style and the leading directory separator must
// also be removed to make the path relative.
const sheetPath = sheetResult.path.replace('stdin', name);
let sheetContents = sheetResult.contents;
if (sheetResult.map) {
outputFiles.push(createOutputFileFromText(sheetPath + '.map', sheetResult.map));
sheetContents = sheetContents.replace(
'sourceMappingURL=stdin.css.map',
`sourceMappingURL=${name}.css.map`,
);
}
outputFiles.push(createOutputFileFromText(sheetPath, sheetContents));
const namespace = 'angular:styles/global';
buildOptions.entryPoints = {};
for (const { name } of globalStyles) {
buildOptions.entryPoints[name] = `${namespace};${name}`;
}

if (initial) {
initialFiles.push({
file: sheetPath,
name,
extension: '.css',
buildOptions.plugins.unshift({
name: 'angular-global-styles',
setup(build) {
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
if (args.kind !== 'entry-point') {
return null;
}

return {
path: args.path.split(';', 2)[1],
namespace,
};
});
}
outputFiles.push(...sheetResult.resourceFiles);
}
build.onLoad({ filter: /./, namespace }, (args) => {
const files = globalStyles.find(({ name }) => name === args.path)?.files;
assert(files, `global style name should always be found [${args.path}]`);

return {
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
loader: 'css',
resolveDir: workspaceRoot,
};
});
},
});

return { outputFiles, initialFiles, errors, warnings };
return buildOptions;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,10 @@ export interface BundleStylesheetOptions {
target: string[];
}

async function bundleStylesheet(
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
export function createStylesheetBundleOptions(
options: BundleStylesheetOptions,
) {
// Execute esbuild
const result = await bundle({
...entry,
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
return {
absWorkingDir: options.workspaceRoot,
bundle: true,
entryNames: options.outputNames?.bundles,
Expand All @@ -49,6 +46,17 @@ async function bundleStylesheet(
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths: options.includePaths }),
createCssResourcePlugin(),
],
};
}

async function bundleStylesheet(
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
options: BundleStylesheetOptions,
) {
// Execute esbuild
const result = await bundle(options.workspaceRoot, {
...createStylesheetBundleOptions(options),
...entry,
});

// Extract the result of the bundling from the output files
Expand All @@ -58,7 +66,6 @@ async function bundleStylesheet(
const resourceFiles: OutputFile[] = [];
if (result.outputFiles) {
for (const outputFile of result.outputFiles) {
outputFile.path = path.relative(options.workspaceRoot, outputFile.path);
const filename = path.basename(outputFile.path);
if (filename.endsWith('.css')) {
outputPath = outputFile.path;
Expand Down