Skip to content

Commit

Permalink
refactor(@angular-devkit/build-angular): improve initial file analysi…
Browse files Browse the repository at this point in the history
…s for esbuild builder

When using the esbuild-based browser application builder, the set of initially loaded files
for the application is now calculated by analyzing potential transitively loading JavaScript
and/or CSS files. This ensures that the full set of bundled files is available for bundle
size calculations as well as further analysis in areas such as link-based hint generation in
the application's index HTML.
This also fixes a bug where non-injected `scripts` where incorrectly shown as initial files.
  • Loading branch information
clydin authored and alan-agius4 committed Jun 7, 2023
1 parent 7155cbe commit bc5b7d5
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
formatMessages,
} from 'esbuild';
import { basename, extname, relative } from 'node:path';
import { FileInfo } from '../../utils/index-file/augment-index-html';

export type BundleContextResult =
| { errors: Message[]; warnings: Message[] }
Expand All @@ -29,7 +28,7 @@ export type BundleContextResult =
warnings: Message[];
metafile: Metafile;
outputFiles: OutputFile[];
initialFiles: FileInfo[];
initialFiles: Map<string, InitialFileRecord>;
};

/**
Expand All @@ -41,11 +40,23 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
return !!value && typeof value === 'object' && 'errors' in value && 'warnings' in value;
}

export interface InitialFileRecord {
entrypoint: boolean;
name?: string;
type: 'script' | 'style';
external?: boolean;
}

export class BundlerContext {
#esbuildContext?: BuildContext<{ metafile: true; write: false }>;
#esbuildOptions: BuildOptions & { metafile: true; write: false };

constructor(private workspaceRoot: string, private incremental: boolean, options: BuildOptions) {
constructor(
private workspaceRoot: string,
private incremental: boolean,
options: BuildOptions,
private initialFilter?: (initial: Readonly<InitialFileRecord>) => boolean,
) {
this.#esbuildOptions = {
...options,
metafile: true,
Expand All @@ -64,7 +75,7 @@ export class BundlerContext {
let errors: Message[] | undefined;
const warnings: Message[] = [];
const metafile: Metafile = { inputs: {}, outputs: {} };
const initialFiles = [];
const initialFiles = new Map<string, InitialFileRecord>();
const outputFiles = [];
for (const result of individualResults) {
warnings.push(...result.warnings);
Expand All @@ -80,7 +91,7 @@ export class BundlerContext {
metafile.outputs = { ...metafile.outputs, ...result.metafile.outputs };
}

initialFiles.push(...result.initialFiles);
result.initialFiles.forEach((value, key) => initialFiles.set(key, value));
outputFiles.push(...result.outputFiles);
}

Expand Down Expand Up @@ -139,27 +150,59 @@ export class BundlerContext {
}

// Find all initial files
const initialFiles: FileInfo[] = [];
const initialFiles = new Map<string, InitialFileRecord>();
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(this.workspaceRoot, outputFile.path);
const entryPoint = result.metafile?.outputs[relativeFilePath]?.entryPoint;
const entryPoint = result.metafile.outputs[relativeFilePath]?.entryPoint;

outputFile.path = relativeFilePath;

if (entryPoint) {
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
const name = basename(outputFile.path).split('.', 1)[0];
const name = basename(relativeFilePath).split('.', 1)[0];
// Entry points are only styles or scripts
const type = extname(relativeFilePath) === '.css' ? 'style' : 'script';

// Only entrypoints with an entry in the options are initial files.
// Dynamic imports also have an entryPoint value in the meta file.
if ((this.#esbuildOptions.entryPoints as Record<string, string>)?.[name]) {
// An entryPoint value indicates an initial file
initialFiles.push({
file: outputFile.path,
const record: InitialFileRecord = {
name,
extension: extname(outputFile.path),
});
type,
entrypoint: true,
};

if (!this.initialFilter || this.initialFilter(record)) {
initialFiles.set(relativeFilePath, record);
}
}
}
}

// Analyze for transitive initial files
const files = [...initialFiles.keys()];
for (const file of files) {
for (const initialImport of result.metafile.outputs[file].imports) {
if (initialFiles.has(initialImport.path)) {
continue;
}

if (initialImport.kind === 'import-statement' || initialImport.kind === 'import-rule') {
const record: InitialFileRecord = {
type: initialImport.kind === 'import-rule' ? 'style' : 'script',
entrypoint: false,
external: initialImport.external,
};

if (!this.initialFilter || this.initialFilter(record)) {
initialFiles.set(initialImport.path, record);
}

if (!initialImport.external) {
files.push(initialImport.path);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { brotliCompress } from 'node:zlib';
import { copyAssets } from '../../utils/copy-assets';
import { assertIsError } from '../../utils/error';
import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
import { FileInfo } from '../../utils/index-file/augment-index-html';
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
import { Spinner } from '../../utils/spinner';
Expand All @@ -25,7 +24,7 @@ import { BundleStats, generateBuildStatsTable } from '../../webpack/utils/stats'
import { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin';
import { logBuilderStatusWarnings } from './builder-status-warnings';
import { checkCommonJSModules } from './commonjs-checker';
import { BundlerContext, logMessages } from './esbuild';
import { BundlerContext, InitialFileRecord, logMessages } from './esbuild';
import { createGlobalScriptsBundleOptions } from './global-scripts';
import { createGlobalStylesBundleOptions } from './global-styles';
import { extractLicenses } from './license-extractor';
Expand Down Expand Up @@ -141,7 +140,9 @@ async function execute(
codeBundleCache?.loadResultCache,
);
if (bundleOptions) {
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
);
}
}
}
Expand All @@ -151,7 +152,9 @@ async function execute(
for (const initial of [true, false]) {
const bundleOptions = createGlobalScriptsBundleOptions(options, initial);
if (bundleOptions) {
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
);
}
}
}
Expand All @@ -169,15 +172,6 @@ async function execute(
return executionResult;
}

// Filter global stylesheet initial files. Currently all initial CSS files are from the global styles option.
if (options.globalStyles.length > 0) {
bundlingResult.initialFiles = bundlingResult.initialFiles.filter(
({ file, name }) =>
!file.endsWith('.css') ||
options.globalStyles.find((style) => style.name === name)?.initial,
);
}

const { metafile, initialFiles, outputFiles } = bundlingResult;

executionResult.outputFiles.push(...outputFiles);
Expand Down Expand Up @@ -216,7 +210,11 @@ async function execute(
baseHref: options.baseHref,
lang: undefined,
outputPath: virtualOutputPath,
files: initialFiles,
files: [...initialFiles].map(([file, record]) => ({
name: record.name ?? '',
file,
extension: path.extname(file),
})),
});

for (const error of errors) {
Expand Down Expand Up @@ -758,10 +756,9 @@ export default createBuilder(buildEsbuildBrowser);
function logBuildStats(
context: BuilderContext,
metafile: Metafile,
initialFiles: FileInfo[],
initial: Map<string, InitialFileRecord>,
estimatedTransferSizes?: Map<string, number>,
) {
const initial = new Map(initialFiles.map((info) => [info.file, info.name]));
const stats: BundleStats[] = [];
for (const [file, output] of Object.entries(metafile.outputs)) {
// Only display JavaScript and CSS files
Expand All @@ -778,7 +775,7 @@ function logBuildStats(
initial: initial.has(file),
stats: [
file,
initial.get(file) ?? '-',
initial.get(file)?.name ?? '-',
output.bytes,
estimatedTransferSizes?.get(file) ?? '-',
],
Expand Down

0 comments on commit bc5b7d5

Please sign in to comment.