<commit_before>/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as assert from 'assert';
import type { Message, OutputFile } from 'esbuild';
import { promises as fs } from 'fs';
import * as path from 'path';
import { NormalizedOptimizationOptions, deleteOutputDir } from '../../utils';
import { copyAssets } from '../../utils/copy-assets';
import { assertIsError } from '../../utils/error';
import { FileInfo } from '../../utils/index-file/augment-index-html';
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
import { generateEntryPoints } from '../../utils/package-chunk-sort';
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
import { resolveGlobalStyles } from '../../webpack/configs';
import { createCompilerPlugin } from './compiler-plugin';
import { bundle, logMessages } from './esbuild';
import { logExperimentalWarnings } from './experimental-warnings';
import { normalizeOptions } from './options';
import { Schema as BrowserBuilderOptions, SourceMapClass } from './schema';
import { bundleStylesheetText } from './stylesheets';

/**
 * Main execution function for the esbuild-based application builder.
 * The options are compatible with the Webpack-based builder.
 * @param options The browser builder options to use when setting up the application build
 * @param context The Architect builder context object
 * @returns A promise with the builder result output
 */
// eslint-disable-next-line max-lines-per-function
export async function buildEsbuildBrowser(
  options: BrowserBuilderOptions,
  context: BuilderContext,
): Promise<BuilderOutput> {
  const startTime = Date.now();

  // Only AOT is currently supported
  if (options.aot !== true) {
    context.logger.error(
      'JIT mode is currently not supported by this experimental builder. AOT mode must be used.',
    );

    return { success: false };
  }

  // Inform user of experimental status of builder and options
  logExperimentalWarnings(options, context);

  // Determine project name from builder context target
  const projectName = context.target?.project;
  if (!projectName) {
    context.logger.error(`The 'browser-esbuild' builder requires a target to be specified.`);

    return { success: false };
  }

  const {
    projectRoot,
    workspaceRoot,
    mainEntryPoint,
    polyfillsEntryPoint,
    optimizationOptions,
    outputPath,
    sourcemapOptions,
    tsconfig,
    assets,
    outputNames,
  } = await normalizeOptions(context, projectName, options);

  // Clean output path if enabled
  if (options.deleteOutputPath) {
    deleteOutputDir(workspaceRoot, options.outputPath);
  }

  // Setup bundler entry points
  const entryPoints: Record<string, string> = {
    main: mainEntryPoint,
  };
  if (polyfillsEntryPoint) {
    entryPoints['polyfills'] = polyfillsEntryPoint;
  }
  // Create reverse lookup used during index HTML generation
  const entryPointNameLookup: ReadonlyMap<string, string> = new Map(
    Object.entries(entryPoints).map(
      ([name, filePath]) => [path.relative(workspaceRoot, filePath), name] as const,
    ),
  );

  // Execute esbuild
  const result = await bundleCode(
    workspaceRoot,
    entryPoints,
    outputNames,
    options,
    optimizationOptions,
    sourcemapOptions,
    tsconfig,
  );

  // Log all warnings and errors generated during bundling
  await logMessages(context, result);

  // Return if the bundling failed to generate output files or there are errors
  if (!result.outputFiles || result.errors.length) {
    return { success: false };
  }

  // Structure the bundling output files
  const initialFiles: FileInfo[] = [];
  const outputFiles: OutputFile[] = [];
  for (const outputFile of result.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 = result.metafile?.outputs[relativeFilePath]?.entryPoint;

    outputFile.path = relativeFilePath;

    if (entryPoint) {
      // An entryPoint value indicates an initial file
      initialFiles.push({
        file: outputFile.path,
        name: entryPointNameLookup.get(entryPoint) ?? '',
        extension: path.extname(outputFile.path),
      });
    }
    outputFiles.push(outputFile);
  }

  // Create output directory if needed
  try {
    await fs.mkdir(outputPath, { recursive: true });
  } catch (e) {
    assertIsError(e);
    context.logger.error('Unable to create output directory: ' + e.message);

    return { success: false };
  }

  // Process global stylesheets
  const styleResults = await bundleGlobalStylesheets(
    workspaceRoot,
    outputNames,
    options,
    optimizationOptions,
    sourcemapOptions,
  );
  outputFiles.push(...styleResults.outputFiles);
  initialFiles.push(...styleResults.initialFiles);

  // Log all warnings and errors generated during bundling
  await logMessages(context, styleResults);

  // Return if the bundling has errors
  if (styleResults.errors.length) {
    return { success: false };
  }

  // Generate index HTML file
  if (options.index) {
    const entrypoints = generateEntryPoints({
      scripts: options.scripts ?? [],
      styles: options.styles ?? [],
    });

    // Create an index HTML generator that reads from the in-memory output files
    const indexHtmlGenerator = new IndexHtmlGenerator({
      indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)),
      entrypoints,
      sri: options.subresourceIntegrity,
      optimization: optimizationOptions,<commit_msg>refactor(@angular-devkit/build-angular): process esbuild builder global stylesheets and code at the same time<commit_after>/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as assert from 'assert';
import type { Message, OutputFile } from 'esbuild';
import { promises as fs } from 'fs';
import * as path from 'path';
import { NormalizedOptimizationOptions, deleteOutputDir } from '../../utils';
import { copyAssets } from '../../utils/copy-assets';
import { assertIsError } from '../../utils/error';
import { FileInfo } from '../../utils/index-file/augment-index-html';
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
import { generateEntryPoints } from '../../utils/package-chunk-sort';
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
import { resolveGlobalStyles } from '../../webpack/configs';
import { createCompilerPlugin } from './compiler-plugin';
import { bundle, logMessages } from './esbuild';
import { logExperimentalWarnings } from './experimental-warnings';
import { normalizeOptions } from './options';
import { Schema as BrowserBuilderOptions, SourceMapClass } from './schema';
import { bundleStylesheetText } from './stylesheets';

/**
 * Main execution function for the esbuild-based application builder.
 * The options are compatible with the Webpack-based builder.
 * @param options The browser builder options to use when setting up the application build
 * @param context The Architect builder context object
 * @returns A promise with the builder result output
 */
// eslint-disable-next-line max-lines-per-function
export async function buildEsbuildBrowser(
  options: BrowserBuilderOptions,
  context: BuilderContext,
): Promise<BuilderOutput> {
  const startTime = Date.now();

  // Only AOT is currently supported
  if (options.aot !== true) {
    context.logger.error(
      'JIT mode is currently not supported by this experimental builder. AOT mode must be used.',
    );

    return { success: false };
  }

  // Inform user of experimental status of builder and options
  logExperimentalWarnings(options, context);

  // Determine project name from builder context target
  const projectName = context.target?.project;
  if (!projectName) {
    context.logger.error(`The 'browser-esbuild' builder requires a target to be specified.`);

    return { success: false };
  }

  const {
    projectRoot,
    workspaceRoot,
    mainEntryPoint,
    polyfillsEntryPoint,
    optimizationOptions,
    outputPath,
    sourcemapOptions,
    tsconfig,
    assets,
    outputNames,
  } = await normalizeOptions(context, projectName, options);

  // Clean output path if enabled
  if (options.deleteOutputPath) {
    deleteOutputDir(workspaceRoot, options.outputPath);
  }

  // Setup bundler entry points
  const entryPoints: Record<string, string> = {
    main: mainEntryPoint,
  };
  if (polyfillsEntryPoint) {
    entryPoints['polyfills'] = polyfillsEntryPoint;
  }
  // Create reverse lookup used during index HTML generation
  const entryPointNameLookup: ReadonlyMap<string, string> = new Map(
    Object.entries(entryPoints).map(
      ([name, filePath]) => [path.relative(workspaceRoot, filePath), name] as const,
    ),
  );

  const [codeResults, styleResults] = await Promise.all([
    // Execute esbuild to bundle the application code
    bundleCode(
      workspaceRoot,
      entryPoints,
      outputNames,
      options,
      optimizationOptions,
      sourcemapOptions,
      tsconfig,
    ),
    // Execute esbuild to bundle the global stylesheets
    bundleGlobalStylesheets(
      workspaceRoot,
      outputNames,
      options,
      optimizationOptions,
      sourcemapOptions,
    ),
  ]);

  // Log all warnings and errors generated during bundling
  await logMessages(context, {
    errors: [...codeResults.errors, ...styleResults.errors],
    warnings: [...codeResults.warnings, ...styleResults.warnings],
  });

  // Return if the bundling failed to generate output files or there are errors
  if (!codeResults.outputFiles || codeResults.errors.length) {
    return { success: false };
  }

  // 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,
        name: entryPointNameLookup.get(entryPoint) ?? '',
        extension: path.extname(outputFile.path),
      });
    }
    outputFiles.push(outputFile);
  }

  // Add global stylesheets output files
  outputFiles.push(...styleResults.outputFiles);
  initialFiles.push(...styleResults.initialFiles);

  // Return if the global stylesheet bundling has errors
  if (styleResults.errors.length) {
    return { success: false };
  }

  // Create output directory if needed
  try {
    await fs.mkdir(outputPath, { recursive: true });
  } catch (e) {
    assertIsError(e);
    context.logger.error('Unable to create output directory: ' + e.message);

    return { success: false };
  }

  // Generate index HTML file
  if (options.index) {
    const entrypoints = generateEntryPoints({
      scripts: options.scripts ?? [],
      styles: options.styles ?? [],
    });

    // Create an index HTML generator that reads from the in-memory output files
    const indexHtmlGenerator = new IndexHtmlGenerator({
      indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)),
      entrypoints,
      sri: options.subresourceIntegrity,
      optimization: optimizationOptions,
{'fraction_non_alphanumeric': 0.10795081967213115, 'fraction_numerical': 0.0, 'mean_word_length': 3.605889014722537, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 11, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3875024', 'n_tokens_mistral': 3247, 'n_tokens_neox': 3213, 'n_words': 1020}