Skip to content

Commit

Permalink
refactor: move esbuild index generator, code bundle option and execut…
Browse files Browse the repository at this point in the history
…ion results

This commit extracts code in separate files.

(cherry picked from commit 6bebc45)
  • Loading branch information
alan-agius4 committed Jun 9, 2023
1 parent 15e0a88 commit 72629bd
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 222 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,95 +7,36 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { BuildOptions, OutputFile } from 'esbuild';
import type { OutputFile } from 'esbuild';
import fs from 'node:fs/promises';
import path from 'node:path';
import { SourceFileCache, createCompilerPlugin } from '../../tools/esbuild/angular/compiler-plugin';
import { SourceFileCache } from '../../tools/esbuild/angular/compiler-plugin';
import { createCodeBundleOptions } from '../../tools/esbuild/application-code-bundle';
import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
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 { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
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 { getSupportedBrowsers } from '../../utils/supported-browsers';
import { logBuilderStatusWarnings } from './builder-status-warnings';
import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options';
import { Schema as BrowserBuilderOptions } from './schema';

interface RebuildState {
rebuildContexts: BundlerContext[];
codeBundleCache?: SourceFileCache;
fileChanges: ChangedFiles;
}

/**
* Represents the result of a single builder execute call.
*/
class ExecutionResult {
readonly outputFiles: OutputFile[] = [];
readonly assetFiles: { source: string; destination: string }[] = [];

constructor(
private rebuildContexts: BundlerContext[],
private codeBundleCache?: SourceFileCache,
) {}

addOutputFile(path: string, content: string): void {
this.outputFiles.push(createOutputFileFromText(path, content));
}

get output() {
return {
success: this.outputFiles.length > 0,
};
}

get outputWithFiles() {
return {
success: this.outputFiles.length > 0,
outputFiles: this.outputFiles,
assetFiles: this.assetFiles,
};
}

get watchFiles() {
return this.codeBundleCache?.referencedFiles ?? [];
}

createRebuildState(fileChanges: ChangedFiles): RebuildState {
this.codeBundleCache?.invalidate([...fileChanges.modified, ...fileChanges.removed]);

return {
rebuildContexts: this.rebuildContexts,
codeBundleCache: this.codeBundleCache,
fileChanges,
};
}

async dispose(): Promise<void> {
await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose()));
}
}

async function execute(
options: NormalizedBrowserOptions,
context: BuilderContext,
Expand Down Expand Up @@ -188,39 +129,11 @@ async function execute(

// Generate index HTML file
if (indexHtmlOptions) {
// Create an index HTML generator that reads from the in-memory output files
const indexHtmlGenerator = new IndexHtmlGenerator({
indexPath: indexHtmlOptions.input,
entrypoints: indexHtmlOptions.insertionOrder,
sri: options.subresourceIntegrity,
optimization: optimizationOptions,
crossOrigin: options.crossOrigin,
});

/** Virtual output path to support reading in-memory files. */
const virtualOutputPath = '/';
indexHtmlGenerator.readAsset = async function (filePath: string): Promise<string> {
// Remove leading directory separator
const relativefilePath = path.relative(virtualOutputPath, filePath);
const file = executionResult.outputFiles.find((file) => file.path === relativefilePath);
if (file) {
return file.text;
}

throw new Error(`Output file does not exist: ${path}`);
};

const { content, warnings, errors } = await indexHtmlGenerator.process({
baseHref: options.baseHref,
lang: undefined,
outputPath: virtualOutputPath,
files: [...initialFiles].map(([file, record]) => ({
name: record.name ?? '',
file,
extension: path.extname(file),
})),
});

const { errors, warnings, content } = await generateIndexHtml(
initialFiles,
executionResult,
options,
);
for (const error of errors) {
context.logger.error(error);
}
Expand Down Expand Up @@ -283,131 +196,6 @@ async function execute(
return executionResult;
}

function createCodeBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
sourceFileCache?: SourceFileCache,
): BuildOptions {
const {
workspaceRoot,
entryPoints,
optimizationOptions,
sourcemapOptions,
tsconfig,
outputNames,
outExtension,
fileReplacements,
externalDependencies,
preserveSymlinks,
stylePreprocessorOptions,
advancedOptimizations,
inlineStyleLanguage,
jit,
tailwindConfiguration,
} = options;

const buildOptions: BuildOptions = {
absWorkingDir: workspaceRoot,
bundle: true,
format: 'esm',
entryPoints,
entryNames: outputNames.bundles,
assetNames: outputNames.media,
target,
supported: getFeatureSupport(target),
mainFields: ['es2020', 'browser', 'module', 'main'],
conditions: ['es2020', 'es2015', 'module'],
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'],
metafile: true,
legalComments: options.extractLicenses ? 'none' : 'eof',
logLevel: options.verbose ? 'debug' : 'silent',
minify: optimizationOptions.scripts,
pure: ['forwardRef'],
outdir: workspaceRoot,
outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined,
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
splitting: true,
tsconfig,
external: externalDependencies,
write: false,
platform: 'browser',
preserveSymlinks,
plugins: [
createSourcemapIngorelistPlugin(),
createCompilerPlugin(
// JS/TS options
{
sourcemap: !!sourcemapOptions.scripts,
thirdPartySourcemaps: sourcemapOptions.vendor,
tsconfig,
jit,
advancedOptimizations,
fileReplacements,
sourceFileCache,
loadResultCache: sourceFileCache?.loadResultCache,
},
// Component stylesheet options
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap:
// Hidden component stylesheet sourcemaps are inaccessible which is effectively
// the same as being disabled. Disabling has the advantage of avoiding the overhead
// of sourcemap processing.
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
externalDependencies,
target,
inlineStyleLanguage,
preserveSymlinks,
browsers,
tailwindConfiguration,
},
),
],
define: {
// Only set to false when script optimizations are enabled. It should not be set to true because
// Angular turns `ngDevMode` into an object for development debugging purposes when not defined
// which a constant true value would break.
...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined),
'ngJitMode': jit ? 'true' : 'false',
},
};

if (options.externalPackages) {
buildOptions.plugins ??= [];
buildOptions.plugins.push(createExternalPackagesPlugin());
}

const polyfills = options.polyfills ? [...options.polyfills] : [];
if (jit) {
polyfills.push('@angular/compiler');
}

if (polyfills?.length) {
const namespace = 'angular:polyfills';
buildOptions.entryPoints = {
...buildOptions.entryPoints,
['polyfills']: namespace,
};

buildOptions.plugins?.unshift(
createVirtualModulePlugin({
namespace,
loadContent: () => ({
contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'),
loader: 'js',
resolveDir: workspaceRoot,
}),
}),
);
}

return buildOptions;
}

/**
* Main execution function for the esbuild-based application builder.
* The options are compatible with the Webpack-based builder.
Expand Down
Loading

0 comments on commit 72629bd

Please sign in to comment.