Skip to content

Commit

Permalink
refactor(@angular-devkit/build-angular): update code base structure t…
Browse files Browse the repository at this point in the history
…o facilitate future builders

This commit updates the code base structure in preparation for future works.
  • Loading branch information
alan-agius4 committed Jun 9, 2023
1 parent c0fa3cb commit 466d86d
Show file tree
Hide file tree
Showing 96 changed files with 409 additions and 364 deletions.
4 changes: 2 additions & 2 deletions goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
[
"packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts",
"packages/angular_devkit/build_angular/src/webpack/utils/stats.ts"
"packages/angular_devkit/build_angular/src/tools/webpack/utils/stats.ts",
"packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts"
],
[
"packages/angular/cli/src/analytics/analytics-collector.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular_devkit/build_angular/plugins/karma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* found in the LICENSE file at https://angular.io/license
*/

module.exports = require('../src/webpack/plugins/karma/karma');
module.exports = require('../src/tools/webpack/plugins/karma/karma');
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,39 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { BuildOptions, Metafile, OutputFile } from 'esbuild';
import { constants as fsConstants } from 'node:fs';
import type { BuildOptions, OutputFile } from 'esbuild';
import fs from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { brotliCompress } from 'node:zlib';
import { SourceFileCache, createCompilerPlugin } from '../../tools/esbuild/angular/compiler-plugin';
import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
import { createExternalPackagesPlugin } from '../../tools/esbuild/external-packages-plugin';
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles';
import { extractLicenses } from '../../tools/esbuild/license-extractor';
import { createSourcemapIngorelistPlugin } from '../../tools/esbuild/sourcemap-ignorelist-plugin';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import {
calculateEstimatedTransferSizes,
createOutputFileFromText,
getFeatureSupport,
logBuildStats,
logMessages,
withNoProgress,
withSpinner,
writeResultFiles,
} from '../../tools/esbuild/utils';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
import type { ChangedFiles } from '../../tools/esbuild/watcher';
import { copyAssets } from '../../utils/copy-assets';
import { assertIsError } from '../../utils/error';
import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
import { Spinner } from '../../utils/spinner';
import { getSupportedBrowsers } from '../../utils/supported-browsers';
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, InitialFileRecord, logMessages } from './esbuild';
import { createGlobalScriptsBundleOptions } from './global-scripts';
import { createGlobalStylesBundleOptions } from './global-styles';
import { extractLicenses } from './license-extractor';
import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options';
import { Schema as BrowserBuilderOptions } from './schema';
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
import { shutdownSassWorkerPool } from './stylesheets/sass-language';
import { createVirtualModulePlugin } from './virtual-module-plugin';
import type { ChangedFiles } from './watcher';

const compressAsync = promisify(brotliCompress);

interface RebuildState {
rebuildContexts: BundlerContext[];
Expand Down Expand Up @@ -299,51 +303,6 @@ async function execute(
return executionResult;
}

async function writeResultFiles(
outputFiles: OutputFile[],
assetFiles: { source: string; destination: string }[] | undefined,
outputPath: string,
) {
const directoryExists = new Set<string>();
await Promise.all(
outputFiles.map(async (file) => {
// Ensure output subdirectories exist
const basePath = path.dirname(file.path);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Write file contents
await fs.writeFile(path.join(outputPath, file.path), file.contents);
}),
);

if (assetFiles?.length) {
await Promise.all(
assetFiles.map(async ({ source, destination }) => {
// Ensure output subdirectories exist
const basePath = path.dirname(destination);
if (basePath && !directoryExists.has(basePath)) {
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
directoryExists.add(basePath);
}
// Copy file contents
await fs.copyFile(source, path.join(outputPath, destination), fsConstants.COPYFILE_FICLONE);
}),
);
}
}

function createOutputFileFromText(path: string, text: string): OutputFile {
return {
path,
text,
get contents() {
return Buffer.from(this.text, 'utf-8');
},
};
}

function createCodeBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
Expand Down Expand Up @@ -438,43 +397,8 @@ function createCodeBundleOptions(
};

if (options.externalPackages) {
// Add a plugin that marks any resolved path as external if it is within a node modules directory.
// This is used instead of the esbuild `packages` option to avoid marking bare specifiers that use
// tsconfig path mapping to resolve to a workspace relative path. This is common for monorepos that
// contain libraries that are built along with the application. These libraries should not be considered
// external even though the imports appear to be packages.
const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION');
buildOptions.plugins ??= [];
buildOptions.plugins.push({
name: 'angular-external-packages',
setup(build) {
build.onResolve({ filter: /./ }, async (args) => {
if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) {
return null;
}

const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true;

const result = await build.resolve(args.path, {
importer,
kind,
namespace,
pluginData,
resolveDir,
});

if (result.path && /[\\/]node_modules[\\/]/.test(result.path)) {
return {
path: args.path,
external: true,
};
}

return result;
});
},
});
buildOptions.plugins.push(createExternalPackagesPlugin());
}

const polyfills = options.polyfills ? [...options.polyfills] : [];
Expand Down Expand Up @@ -504,82 +428,6 @@ function createCodeBundleOptions(
return buildOptions;
}

/**
* Generates a syntax feature object map for Angular applications based on a list of targets.
* A full set of feature names can be found here: https://esbuild.github.io/api/#supported
* @param target An array of browser/engine targets in the format accepted by the esbuild `target` option.
* @returns An object that can be used with the esbuild build `supported` option.
*/
function getFeatureSupport(target: string[]): BuildOptions['supported'] {
const supported: Record<string, boolean> = {
// Native async/await is not supported with Zone.js. Disabling support here will cause
// esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild
// does not currently support downleveling async generators. Instead babel is used within the JS/TS
// loader to perform the downlevel transformation.
// NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled.
'async-await': false,
// V8 currently has a performance defect involving object spread operations that can cause signficant
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
// will be used instead which provides a workaround for the performance issue.
// For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536
'object-rest-spread': false,
// esbuild currently has a defect involving self-referencing a class within a static code block or
// static field initializer. This is not an issue for projects that use the default browserslist as these
// elements are an ES2022 feature which is not support by all browsers in the default list. However, if a
// custom browserslist is used that only has newer browsers than the static code elements may be present.
// This issue is compounded by the default usage of the tsconfig `"useDefineForClassFields": false` option
// present in generated CLI projects which causes static code blocks to be used instead of static fields.
// esbuild currently unconditionally downlevels all static fields in top-level classes so to workaround the
// Angular issue only static code blocks are disabled here.
// For more details: https://github.com/evanw/esbuild/issues/2950
'class-static-blocks': false,
};

// Detect Safari browser versions that have a class field behavior bug
// See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033
// See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2
let safariClassFieldScopeBug = false;
for (const browser of target) {
let majorVersion;
if (browser.startsWith('ios')) {
majorVersion = Number(browser.slice(3, 5));
} else if (browser.startsWith('safari')) {
majorVersion = Number(browser.slice(6, 8));
} else {
continue;
}
// Technically, 14.0 is not broken but rather does not have support. However, the behavior
// is identical since it would be set to false by esbuild if present as a target.
if (majorVersion === 14 || majorVersion === 15) {
safariClassFieldScopeBug = true;
break;
}
}
// If class field support cannot be used set to false; otherwise leave undefined to allow
// esbuild to use `target` to determine support.
if (safariClassFieldScopeBug) {
supported['class-field'] = false;
supported['class-static-field'] = false;
}

return supported;
}

async function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T> {
const spinner = new Spinner(text);
spinner.start();

try {
return await action();
} finally {
spinner.stop();
}
}

async function withNoProgress<T>(test: string, action: () => T | Promise<T>): Promise<T> {
return action();
}

/**
* Main execution function for the esbuild-based application builder.
* The options are compatible with the Webpack-based builder.
Expand Down Expand Up @@ -695,7 +543,7 @@ export async function* buildEsbuildBrowserInternal(
}

// Setup a watcher
const { createWatcher } = await import('./watcher');
const { createWatcher } = await import('../../tools/esbuild/watcher');
const watcher = createWatcher({
polling: typeof userOptions.poll === 'number',
interval: userOptions.poll,
Expand Down Expand Up @@ -772,66 +620,3 @@ export async function* buildEsbuildBrowserInternal(
}

export default createBuilder(buildEsbuildBrowser);

function logBuildStats(
context: BuilderContext,
metafile: Metafile,
initial: Map<string, InitialFileRecord>,
estimatedTransferSizes?: Map<string, number>,
) {
const stats: BundleStats[] = [];
for (const [file, output] of Object.entries(metafile.outputs)) {
// Only display JavaScript and CSS files
if (!file.endsWith('.js') && !file.endsWith('.css')) {
continue;
}
// Skip internal component resources
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((output as any)['ng-component']) {
continue;
}

stats.push({
initial: initial.has(file),
stats: [
file,
initial.get(file)?.name ?? '-',
output.bytes,
estimatedTransferSizes?.get(file) ?? '-',
],
});
}

const tableText = generateBuildStatsTable(stats, true, true, !!estimatedTransferSizes, undefined);

context.logger.info('\n' + tableText + '\n');
}

async function calculateEstimatedTransferSizes(outputFiles: OutputFile[]) {
const sizes = new Map<string, number>();

const pendingCompression = [];
for (const outputFile of outputFiles) {
// Only calculate JavaScript and CSS files
if (!outputFile.path.endsWith('.js') && !outputFile.path.endsWith('.css')) {
continue;
}

// Skip compressing small files which may end being larger once compressed and will most likely not be
// compressed in actual transit.
if (outputFile.contents.byteLength < 1024) {
sizes.set(outputFile.path, outputFile.contents.byteLength);
continue;
}

pendingCompression.push(
compressAsync(outputFile.contents).then((result) =>
sizes.set(outputFile.path, result.byteLength),
),
);
}

await Promise.all(pendingCompression);

return sizes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import { BuilderContext } from '@angular-devkit/architect';
import { createRequire } from 'node:module';
import path from 'node:path';
import {
globalScriptsByBundleName,
normalizeGlobalStyles,
} from '../../tools/webpack/utils/helpers';
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
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 { globalScriptsByBundleName, normalizeGlobalStyles } from '../../webpack/utils/helpers';
import { Schema as BrowserBuilderOptions, OutputHashing } from './schema';

export type NormalizedBrowserOptions = Awaited<ReturnType<typeof normalizeOptions>>;
Expand Down
24 changes: 12 additions & 12 deletions packages/angular_devkit/build_angular/src/builders/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import * as fs from 'fs';
import * as path from 'path';
import { Observable, concatMap, from, map, switchMap } from 'rxjs';
import webpack, { StatsCompilation } from 'webpack';
import { getCommonConfig, getStylesConfig } from '../../tools/webpack/configs';
import { markAsyncChunksNonInitial } from '../../tools/webpack/utils/async-chunks';
import { normalizeExtraEntryPoints } from '../../tools/webpack/utils/helpers';
import {
BuildEventStats,
generateBuildEventStats,
statsErrorsToString,
statsHasErrors,
statsHasWarnings,
statsWarningsToString,
webpackStatsLogger,
} from '../../tools/webpack/utils/stats';
import { ExecutionTransformer } from '../../transforms';
import {
deleteOutputDir,
Expand Down Expand Up @@ -46,18 +58,6 @@ import {
getIndexInputFile,
getIndexOutputFile,
} from '../../utils/webpack-browser-config';
import { getCommonConfig, getStylesConfig } from '../../webpack/configs';
import { markAsyncChunksNonInitial } from '../../webpack/utils/async-chunks';
import { normalizeExtraEntryPoints } from '../../webpack/utils/helpers';
import {
BuildEventStats,
generateBuildEventStats,
statsErrorsToString,
statsHasErrors,
statsHasWarnings,
statsWarningsToString,
webpackStatsLogger,
} from '../../webpack/utils/stats';
import { Schema as BrowserBuilderSchema } from './schema';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';

const MAIN_OUTPUT = 'dist/main.js';
const NAMED_LAZY_OUTPUT = 'dist/src_lazy-module_ts.js';
const UNNAMED_LAZY_OUTPUT = 'dist/459.js';
const UNNAMED_LAZY_OUTPUT = 'dist/631.js';

describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
describe('Option: "namedChunks"', () => {
Expand Down
Loading

0 comments on commit 466d86d

Please sign in to comment.