From c05c83be7c6c8bcdad4be8686a6e0701a55304cc Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Jun 2023 18:23:52 +0000 Subject: [PATCH] feat(@angular-devkit/build-angular): add initial application builder implementation This commits add the initial application builder schema and build configuration and refactors several files. --- packages/angular/cli/BUILD.bazel | 1 + .../cli/lib/config/workspace-schema.json | 23 + .../angular_devkit/build_angular/BUILD.bazel | 16 +- .../build_angular/builders.json | 5 + .../src/builders/application/build-action.ts | 180 ++++++ .../src/builders/application/execute-build.ts | 187 +++++++ .../src/builders/application/index.ts | 79 +++ .../options.ts | 39 +- .../src/builders/application/schema.json | 524 ++++++++++++++++++ .../tests/behavior/browser-support_spec.ts | 6 +- .../behavior/component-stylesheets_spec.ts | 6 +- .../behavior/index-preload-hints_spec.ts | 6 +- .../stylesheet-url-resolution_spec.ts | 6 +- .../behavior/stylesheet_autoprefixer_spec.ts | 6 +- .../behavior/typescript-path-mapping_spec.ts | 6 +- .../behavior/typescript-resolve-json_spec.ts | 6 +- .../allowed-common-js-dependencies_spec.ts | 6 +- .../tests/options/assets_spec.ts | 6 +- .../tests/options/base-href_spec.ts | 6 +- .../application/tests/options/browser_spec.ts | 90 +++ .../tests/options/cross-origin_spec.ts | 6 +- .../options/external-dependencies_spec.ts | 6 +- .../tests/options/extract-licenses_spec.ts | 6 +- .../tests/options/inline-critical_spec.ts | 6 +- .../options/inline-style-language_spec.ts | 7 +- .../tests/options/output-hashing_spec.ts | 6 +- .../tests/options/polyfills_spec.ts | 6 +- .../tests/options/scripts_spec.ts | 6 +- .../tests/options/sourcemap_spec.ts | 6 +- .../tests/options/styles_spec.ts | 6 +- .../options/subresource-integrity_spec.ts | 6 +- .../src/builders/application/tests/setup.ts | 31 ++ .../builder-status-warnings.ts | 3 +- .../src/builders/browser-esbuild/index.ts | 361 +----------- .../tests/options/entry-points_spec.ts | 120 ---- .../tests/options/out-extension_spec.ts | 52 -- .../src/builders/dev-server/vite-server.ts | 7 +- .../build_angular/src/builders/jest/index.ts | 10 +- .../tools/esbuild/application-code-bundle.ts | 4 +- .../src/tools/esbuild/global-scripts.ts | 4 +- .../src/tools/esbuild/global-styles.ts | 4 +- .../src/tools/esbuild/index-html-generator.ts | 4 +- .../build_angular/src/tools/esbuild/utils.ts | 50 ++ .../webpack/plugins/css-optimizer-plugin.ts | 2 +- .../plugins/javascript-optimizer-plugin.ts | 2 +- .../src/utils/esbuild-targets.ts | 57 -- 46 files changed, 1294 insertions(+), 688 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/build-action.ts create mode 100644 packages/angular_devkit/build_angular/src/builders/application/execute-build.ts create mode 100644 packages/angular_devkit/build_angular/src/builders/application/index.ts rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/options.ts (93%) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/schema.json rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/browser-support_spec.ts (94%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/component-stylesheets_spec.ts (77%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/index-preload-hints_spec.ts (82%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/stylesheet-url-resolution_spec.ts (80%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/stylesheet_autoprefixer_spec.ts (96%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/typescript-path-mapping_spec.ts (94%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/behavior/typescript-resolve-json_spec.ts (93%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/allowed-common-js-dependencies_spec.ts (96%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/assets_spec.ts (98%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/base-href_spec.ts (93%) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/tests/options/browser_spec.ts rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/cross-origin_spec.ts (94%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/external-dependencies_spec.ts (85%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/extract-licenses_spec.ts (86%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/inline-critical_spec.ts (95%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/inline-style-language_spec.ts (95%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/output-hashing_spec.ts (96%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/polyfills_spec.ts (90%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/scripts_spec.ts (98%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/sourcemap_spec.ts (95%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/styles_spec.ts (98%) rename packages/angular_devkit/build_angular/src/builders/{browser-esbuild => application}/tests/options/subresource-integrity_spec.ts (90%) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/tests/setup.ts delete mode 100644 packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/entry-points_spec.ts delete mode 100644 packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/out-extension_spec.ts delete mode 100644 packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 44fa11ec88b9..07c5a7039608 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -79,6 +79,7 @@ ts_library( # @external_begin CLI_SCHEMA_DATA = [ + "//packages/angular_devkit/build_angular:src/builders/application/schema.json", "//packages/angular_devkit/build_angular:src/builders/app-shell/schema.json", "//packages/angular_devkit/build_angular:src/builders/browser/schema.json", "//packages/angular_devkit/build_angular:src/builders/browser-esbuild/schema.json", diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index d3fca5b16c48..a10c0196c424 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -354,6 +354,7 @@ "description": "The builder used for this package.", "not": { "enum": [ + "@angular-devkit/build-angular:application", "@angular-devkit/build-angular:app-shell", "@angular-devkit/build-angular:browser", "@angular-devkit/build-angular:browser-esbuild", @@ -385,6 +386,28 @@ "additionalProperties": false, "required": ["builder"] }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:application" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/application/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/application/schema.json" + } + } + } + }, { "type": "object", "additionalProperties": false, diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index 50f3decc08b2..b7134b720ab8 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -13,6 +13,11 @@ licenses(["notice"]) package(default_visibility = ["//visibility:public"]) +ts_json_schema( + name = "application_schema", + src = "src/builders/application/schema.json", +) + ts_json_schema( name = "app_shell_schema", src = "src/builders/app-shell/schema.json", @@ -80,6 +85,7 @@ ts_library( ], ) + [ "//packages/angular_devkit/build_angular:src/builders/app-shell/schema.ts", + "//packages/angular_devkit/build_angular:src/builders/application/schema.ts", "//packages/angular_devkit/build_angular:src/builders/browser-esbuild/schema.ts", "//packages/angular_devkit/build_angular:src/builders/browser/schema.ts", "//packages/angular_devkit/build_angular:src/builders/dev-server/schema.ts", @@ -290,6 +296,12 @@ ts_library( ) LARGE_SPECS = { + "application": { + "shards": 10, + "extra_deps": [ + "@npm//buffer", + ], + }, "app-shell": { }, "dev-server": { @@ -347,10 +359,6 @@ LARGE_SPECS = { ], }, "browser-esbuild": { - "shards": 10, - "extra_deps": [ - "@npm//buffer", - ], }, "jest": { "extra_deps": [ diff --git a/packages/angular_devkit/build_angular/builders.json b/packages/angular_devkit/build_angular/builders.json index 8b8afe428cd1..7165509d2644 100644 --- a/packages/angular_devkit/build_angular/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -1,6 +1,11 @@ { "$schema": "../architect/src/builders-schema.json", "builders": { + "application": { + "implementation": "./src/builders/application", + "schema": "./src/builders/application/schema.json", + "description": "Build an application." + }, "app-shell": { "implementation": "./src/builders/app-shell", "schema": "./src/builders/app-shell/schema.json", diff --git a/packages/angular_devkit/build_angular/src/builders/application/build-action.ts b/packages/angular_devkit/build_angular/src/builders/application/build-action.ts new file mode 100644 index 000000000000..602e3a8f4ad9 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/build-action.ts @@ -0,0 +1,180 @@ +/** + * @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 { BuilderOutput } from '@angular-devkit/architect'; +import type { logging } from '@angular-devkit/core'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; +import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; +import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils'; +import { assertIsError } from '../../utils/error'; +import { NormalizedCachedOptions } from '../../utils/normalize-cache'; + +export async function* runEsBuildBuildAction( + action: (rebuildState?: RebuildState) => ExecutionResult | Promise, + options: { + workspaceRoot: string; + projectRoot: string; + outputPath: string; + logger: logging.LoggerApi; + cacheOptions: NormalizedCachedOptions; + writeToFileSystem?: boolean; + watch?: boolean; + verbose?: boolean; + progress?: boolean; + deleteOutputPath?: boolean; + poll?: number; + }, +): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> { + const { + writeToFileSystem = true, + watch, + poll, + logger, + deleteOutputPath, + cacheOptions, + outputPath, + verbose, + projectRoot, + workspaceRoot, + progress, + } = options; + + if (writeToFileSystem) { + // Clean output path if enabled + if (deleteOutputPath) { + if (outputPath === workspaceRoot) { + logger.error('Output path MUST not be workspace root directory!'); + + return; + } + + await fs.rm(outputPath, { force: true, recursive: true, maxRetries: 3 }); + } + + // Create output directory if needed + try { + await fs.mkdir(outputPath, { recursive: true }); + } catch (e) { + assertIsError(e); + logger.error('Unable to create output directory: ' + e.message); + + return; + } + } + + const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress; + + // Initial build + let result: ExecutionResult; + try { + result = await withProgress('Building...', () => action()); + + if (writeToFileSystem) { + // Write output files + await writeResultFiles(result.outputFiles, result.assetFiles, outputPath); + + yield result.output; + } else { + // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yield result.outputWithFiles as any; + } + + // Finish if watch mode is not enabled + if (!watch) { + return; + } + } finally { + // Ensure Sass workers are shutdown if not watching + if (!watch) { + shutdownSassWorkerPool(); + } + } + + if (progress) { + logger.info('Watch mode enabled. Watching for file changes...'); + } + + // Setup a watcher + const { createWatcher } = await import('../../tools/esbuild/watcher'); + const watcher = createWatcher({ + polling: typeof poll === 'number', + interval: poll, + ignored: [ + // Ignore the output and cache paths to avoid infinite rebuild cycles + outputPath, + cacheOptions.basePath, + // Ignore all node modules directories to avoid excessive file watchers. + // Package changes are handled below by watching manifest and lock files. + '**/node_modules/**', + '**/.*/**', + ], + }); + + // Temporarily watch the entire project + watcher.add(projectRoot); + + // Watch workspace for package manager changes + const packageWatchFiles = [ + // manifest can affect module resolution + 'package.json', + // npm lock file + 'package-lock.json', + // pnpm lock file + 'pnpm-lock.yaml', + // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/) + 'yarn.lock', + '.pnp.cjs', + '.pnp.data.json', + ]; + + watcher.add(packageWatchFiles.map((file) => path.join(workspaceRoot, file))); + + // Watch locations provided by the initial build result + let previousWatchFiles = new Set(result.watchFiles); + watcher.add(result.watchFiles); + + // Wait for changes and rebuild as needed + try { + for await (const changes of watcher) { + if (verbose) { + logger.info(changes.toDebugString()); + } + + result = await withProgress('Changes detected. Rebuilding...', () => + action(result.createRebuildState(changes)), + ); + + // Update watched locations provided by the new build result. + // Add any new locations + watcher.add(result.watchFiles.filter((watchFile) => !previousWatchFiles.has(watchFile))); + const newWatchFiles = new Set(result.watchFiles); + // Remove any old locations + watcher.remove([...previousWatchFiles].filter((watchFile) => !newWatchFiles.has(watchFile))); + previousWatchFiles = newWatchFiles; + + if (writeToFileSystem) { + // Write output files + await writeResultFiles(result.outputFiles, result.assetFiles, outputPath); + + yield result.output; + } else { + // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yield result.outputWithFiles as any; + } + } + } finally { + // Stop the watcher and cleanup incremental rebuild state + await Promise.allSettled([watcher.close(), result.dispose()]); + + shutdownSassWorkerPool(); + } +} diff --git a/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts new file mode 100644 index 000000000000..d90ea0868829 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts @@ -0,0 +1,187 @@ +/** + * @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 } from '@angular-devkit/architect'; +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 { 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 { + calculateEstimatedTransferSizes, + logBuildStats, + logMessages, + transformSupportedBrowsersToTargets, +} from '../../tools/esbuild/utils'; +import { copyAssets } from '../../utils/copy-assets'; +import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; +import { getSupportedBrowsers } from '../../utils/supported-browsers'; +import { NormalizedApplicationBuildOptions } from './options'; + +export async function executeBuild( + options: NormalizedApplicationBuildOptions, + context: BuilderContext, + rebuildState?: RebuildState, +): Promise { + const startTime = process.hrtime.bigint(); + + const { + projectRoot, + workspaceRoot, + serviceWorker, + optimizationOptions, + assets, + indexHtmlOptions, + cacheOptions, + } = options; + + const browsers = getSupportedBrowsers(projectRoot, context.logger); + const target = transformSupportedBrowsersToTargets(browsers); + + // Reuse rebuild state or create new bundle contexts for code and global stylesheets + let bundlerContexts = rebuildState?.rebuildContexts; + const codeBundleCache = + rebuildState?.codeBundleCache ?? + new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); + if (bundlerContexts === undefined) { + bundlerContexts = []; + + // Application code + bundlerContexts.push( + new BundlerContext( + workspaceRoot, + !!options.watch, + createCodeBundleOptions(options, target, browsers, codeBundleCache), + ), + ); + + // Global Stylesheets + if (options.globalStyles.length > 0) { + for (const initial of [true, false]) { + const bundleOptions = createGlobalStylesBundleOptions( + options, + target, + browsers, + initial, + codeBundleCache?.loadResultCache, + ); + if (bundleOptions) { + bundlerContexts.push( + new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial), + ); + } + } + } + + // Global Scripts + if (options.globalScripts.length > 0) { + for (const initial of [true, false]) { + const bundleOptions = createGlobalScriptsBundleOptions(options, initial); + if (bundleOptions) { + bundlerContexts.push( + new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial), + ); + } + } + } + } + + const bundlingResult = await BundlerContext.bundleAll(bundlerContexts); + + // Log all warnings and errors generated during bundling + await logMessages(context, bundlingResult); + + const executionResult = new ExecutionResult(bundlerContexts, codeBundleCache); + + // Return if the bundling has errors + if (bundlingResult.errors) { + return executionResult; + } + + const { metafile, initialFiles, outputFiles } = bundlingResult; + + executionResult.outputFiles.push(...outputFiles); + + // Check metafile for CommonJS module usage if optimizing scripts + if (optimizationOptions.scripts) { + const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies); + await logMessages(context, { warnings: messages }); + } + + // Generate index HTML file + if (indexHtmlOptions) { + const { errors, warnings, content } = await generateIndexHtml( + initialFiles, + executionResult, + options, + ); + for (const error of errors) { + context.logger.error(error); + } + for (const warning of warnings) { + context.logger.warn(warning); + } + + executionResult.addOutputFile(indexHtmlOptions.output, content); + } + + // Copy assets + if (assets) { + // The webpack copy assets helper is used with no base paths defined. This prevents the helper + // from directly writing to disk. This should eventually be replaced with a more optimized helper. + executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot))); + } + + // Write metafile if stats option is enabled + if (options.stats) { + executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2)); + } + + // Extract and write licenses for used packages + if (options.extractLicenses) { + executionResult.addOutputFile( + '3rdpartylicenses.txt', + await extractLicenses(metafile, workspaceRoot), + ); + } + + // Augment the application with service worker support + if (serviceWorker) { + try { + const serviceWorkerResult = await augmentAppWithServiceWorkerEsbuild( + workspaceRoot, + serviceWorker, + options.baseHref || '/', + executionResult.outputFiles, + executionResult.assetFiles, + ); + executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest); + executionResult.assetFiles.push(...serviceWorkerResult.assetFiles); + } catch (error) { + context.logger.error(error instanceof Error ? error.message : `${error}`); + + return executionResult; + } + } + + // Calculate estimated transfer size if scripts are optimized + let estimatedTransferSizes; + if (optimizationOptions.scripts || optimizationOptions.styles.minify) { + estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); + } + logBuildStats(context, metafile, initialFiles, estimatedTransferSizes); + + const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9; + context.logger.info(`Application bundle generation complete. [${buildTime.toFixed(3)} seconds]`); + + return executionResult; +} diff --git a/packages/angular_devkit/build_angular/src/builders/application/index.ts b/packages/angular_devkit/build_angular/src/builders/application/index.ts new file mode 100644 index 000000000000..5d7b25279b24 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/index.ts @@ -0,0 +1,79 @@ +/** + * @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 type { OutputFile } from 'esbuild'; +import { purgeStaleBuildCache } from '../../utils/purge-cache'; +import { assertCompatibleAngularVersion } from '../../utils/version'; +import { runEsBuildBuildAction } from './build-action'; +import { executeBuild } from './execute-build'; +import { ApplicationBuilderInternalOptions, normalizeOptions } from './options'; +import { Schema as ApplicationBuilderOptions } from './schema'; + +export async function* buildApplicationInternal( + options: ApplicationBuilderInternalOptions, + context: BuilderContext, + infrastructureSettings?: { + write?: boolean; + }, +): AsyncIterable< + BuilderOutput & { + outputFiles?: OutputFile[]; + assetFiles?: { source: string; destination: string }[]; + } +> { + // Check Angular version. + assertCompatibleAngularVersion(context.workspaceRoot); + + // Purge old build disk cache. + await purgeStaleBuildCache(context); + + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + context.logger.error(`The 'application' builder requires a target to be specified.`); + + return; + } + + const normalizedOptions = await normalizeOptions(context, projectName, options); + yield* runEsBuildBuildAction( + (rebuildState) => executeBuild(normalizedOptions, context, rebuildState), + { + watch: normalizedOptions.watch, + poll: normalizedOptions.poll, + deleteOutputPath: normalizedOptions.deleteOutputPath, + cacheOptions: normalizedOptions.cacheOptions, + outputPath: normalizedOptions.outputPath, + verbose: normalizedOptions.verbose, + projectRoot: normalizedOptions.projectRoot, + workspaceRoot: normalizedOptions.workspaceRoot, + progress: normalizedOptions.progress, + writeToFileSystem: infrastructureSettings?.write, + logger: context.logger, + }, + ); +} + +export function buildApplication( + options: ApplicationBuilderOptions, + context: BuilderContext, +): AsyncIterable< + BuilderOutput & { + outputFiles?: OutputFile[]; + assetFiles?: { source: string; destination: string }[]; + } +> { + context.logger.warn( + 'The application builder is currently in developer preview and is not yet recommended for production use.', + ); + + return buildApplicationInternal(options, context); +} + +export default createBuilder(buildApplication); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts b/packages/angular_devkit/build_angular/src/builders/application/options.ts similarity index 93% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts rename to packages/angular_devkit/build_angular/src/builders/application/options.ts index 5c87bf7396c5..2ec3a8a5d115 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/options.ts @@ -18,9 +18,9 @@ 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 { Schema as BrowserBuilderOptions, OutputHashing } from './schema'; +import { Schema as ApplicationBuilderOptions, OutputHashing } from './schema'; -export type NormalizedBrowserOptions = Awaited>; +export type NormalizedApplicationBuildOptions = Awaited>; /** Internal options hidden from builder schema but available when invoked programmatically. */ interface InternalOptions { @@ -43,9 +43,12 @@ interface InternalOptions { } /** Full set of options for `browser-esbuild` builder. */ -export type BrowserEsbuildOptions = Omit & { - // `main` can be `undefined` if `entryPoints` is used. - main?: string; +export type ApplicationBuilderInternalOptions = Omit< + ApplicationBuilderOptions & InternalOptions, + 'browser' +> & { + // `browser` can be `undefined` if `entryPoints` is used. + browser?: string; }; /** @@ -61,7 +64,7 @@ export type BrowserEsbuildOptions = Omit { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Browser support"', () => { it('creates correct sourcemaps when downleveling async functions', async () => { // Add a JavaScript file with async code diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/component-stylesheets_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/component-stylesheets_spec.ts similarity index 77% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/component-stylesheets_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/component-stylesheets_spec.ts index 4729deaf3332..037ff4c9d14c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/component-stylesheets_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/component-stylesheets_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Component Stylesheets"', () => { it('should successfuly compile with an empty inline style', async () => { await harness.modifyFile('src/app/app.component.ts', (content) => { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/index-preload-hints_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/index-preload-hints_spec.ts similarity index 82% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/index-preload-hints_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/index-preload-hints_spec.ts index 0f1b56231966..342f7a94e4b6 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/index-preload-hints_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/index-preload-hints_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Preload hints"', () => { it('should add preload hints for transitive global style imports', async () => { await harness.writeFile( diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet-url-resolution_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts similarity index 80% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet-url-resolution_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts index f50a23cbe9fa..f1f07c2fbebf 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet-url-resolution_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Stylesheet url() Resolution"', () => { it('should show a note when using tilde prefix', async () => { await harness.writeFile( diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet_autoprefixer_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts similarity index 96% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet_autoprefixer_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts index 84aa4039d406..df5c96c31cbc 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/stylesheet_autoprefixer_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; const styleBaseContent: Record = Object.freeze({ 'css': ` @@ -20,7 +20,7 @@ const styleImportedContent: Record = Object.freeze({ 'css': 'section { hyphens: none; }', }); -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Stylesheet autoprefixer"', () => { for (const ext of ['css'] /* ['css', 'sass', 'scss', 'less'] */) { it(`should add prefixes for listed browsers in global styles [${ext}]`, async () => { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-path-mapping_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts similarity index 94% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-path-mapping_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts index 42f041da77b8..e1d8dafbf955 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-path-mapping_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "TypeScript Path Mapping"', () => { it('should resolve TS files when imported with a path mapping', async () => { // Change main module import to use path mapping diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-resolve-json_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts similarity index 93% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-resolve-json_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts index 627b1a5560d9..198e9db71b84 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/behavior/typescript-resolve-json_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "TypeScript JSON module resolution"', () => { it('should resolve JSON files when imported with resolveJsonModule enabled', async () => { await harness.writeFiles({ diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/allowed-common-js-dependencies_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts similarity index 96% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/allowed-common-js-dependencies_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts index a53a1c16b48b..1c7d1d82faf9 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/allowed-common-js-dependencies_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts @@ -7,10 +7,10 @@ */ import { logging } from '@angular-devkit/core'; -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "allowedCommonJsDependencies"', () => { describe('given option is not set', () => { for (const aot of [true, false]) { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/assets_spec.ts similarity index 98% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/assets_spec.ts index 26482b8f3998..4a79438d5857 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/assets_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "assets"', () => { beforeEach(async () => { // Application code is not needed for asset tests diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/base-href_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/base-href_spec.ts similarity index 93% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/base-href_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/base-href_spec.ts index 0a4df42311cd..0147a5f0c6a2 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/base-href_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/base-href_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "baseHref"', () => { beforeEach(async () => { // Application code is not needed for asset tests diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/options/browser_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/browser_spec.ts new file mode 100644 index 000000000000..007e2fabf5b7 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/browser_spec.ts @@ -0,0 +1,90 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "browser"', () => { + it('uses a provided TypeScript file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + browser: 'src/main.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/main.js').toExist(); + harness.expectFile('dist/index.html').toExist(); + }); + + it('uses a provided JavaScript file', async () => { + await harness.writeFile('src/main.js', `console.log('main');`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + browser: 'src/main.js', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/main.js').toExist(); + harness.expectFile('dist/index.html').toExist(); + + harness.expectFile('dist/main.js').content.toContain('console.log("main")'); + }); + + it('fails and shows an error when file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + browser: 'src/missing.ts', + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Could not resolve "') }), + ); + + harness.expectFile('dist/main.js').toNotExist(); + harness.expectFile('dist/index.html').toNotExist(); + }); + + it('throws an error when given an empty string', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + browser: '', + }); + + const { result, error } = await harness.executeOnce(); + expect(result).toBeUndefined(); + + expect(error?.message).toContain('cannot be an empty string'); + }); + + it('resolves an absolute path as relative inside the workspace root', async () => { + await harness.writeFile('file.mjs', `console.log('Hello!');`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + browser: '/file.mjs', + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Always uses the name `main.js` for the `browser` option. + harness.expectFile('dist/main.js').toExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/cross-origin_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/cross-origin_spec.ts similarity index 94% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/cross-origin_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/cross-origin_spec.ts index 5f91d148ced2..c201b9296910 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/cross-origin_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/cross-origin_spec.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; +import { buildApplication } from '../../index'; import { CrossOrigin } from '../../schema'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "crossOrigin"', () => { beforeEach(async () => { // Application code is not needed for asset tests diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/external-dependencies_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/external-dependencies_spec.ts similarity index 85% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/external-dependencies_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/external-dependencies_spec.ts index fd3d4f5c87d5..96d09f4f9bd0 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/external-dependencies_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/external-dependencies_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "externalDependencies"', () => { it('should not externalize any dependency when option is not set', async () => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/extract-licenses_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/extract-licenses_spec.ts similarity index 86% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/extract-licenses_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/extract-licenses_spec.ts index d88f23346b63..86b912361cdd 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/extract-licenses_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/extract-licenses_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "extractLicenses"', () => { it(`should generate '3rdpartylicenses.txt' when 'extractLicenses' is true`, async () => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-critical_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-critical_spec.ts similarity index 95% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-critical_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-critical_spec.ts index 930366ed9526..7dc530346004 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-critical_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-critical_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "inlineCritical"', () => { beforeEach(async () => { await harness.writeFile('src/styles.css', 'body { color: #000 }'); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-style-language_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-style-language_spec.ts similarity index 95% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-style-language_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-style-language_spec.ts index 609b0e6f0013..564901da7f8e 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/inline-style-language_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-style-language_spec.ts @@ -7,11 +7,11 @@ */ import { concatMap, count, take, timeout } from 'rxjs'; -import { buildEsbuildBrowser } from '../../index'; +import { buildApplication } from '../../index'; import { InlineStyleLanguage } from '../../schema'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "inlineStyleLanguage"', () => { beforeEach(async () => { // Setup application component with inline style property @@ -78,7 +78,6 @@ describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { xit('updates produced stylesheet in watch mode', async () => { harness.useTarget('build', { ...BASE_OPTIONS, - main: 'src/main.ts', inlineStyleLanguage: InlineStyleLanguage.Scss, aot, watch: true, diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/output-hashing_spec.ts similarity index 96% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/output-hashing_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/output-hashing_spec.ts index 1bc0c5c9179e..9d59b3f8a8de 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/output-hashing_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/output-hashing_spec.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; +import { buildApplication } from '../../index'; import { OutputHashing } from '../../schema'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "outputHashing"', () => { beforeEach(async () => { // Application code is not needed for asset tests diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/polyfills_spec.ts similarity index 90% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/polyfills_spec.ts index 27adcf879636..69f4dd001828 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/polyfills_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "polyfills"', () => { it('uses a provided TypeScript file', async () => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/scripts_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/scripts_spec.ts similarity index 98% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/scripts_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/scripts_spec.ts index 66195582c1cf..d332f6fe11cb 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/scripts_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/scripts_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "scripts"', () => { beforeEach(async () => { // Application code is not needed for scripts tests diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/sourcemap_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/sourcemap_spec.ts similarity index 95% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/sourcemap_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/sourcemap_spec.ts index 570c5091374d..23eb463dc8bc 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/sourcemap_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/sourcemap_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "sourceMap"', () => { it('should not generate script sourcemap files by default', async () => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/styles_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/styles_spec.ts similarity index 98% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/styles_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/styles_spec.ts index 02b26e83f164..9267749cb1bf 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/styles_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "styles"', () => { beforeEach(async () => { // Application code is not needed for styles tests diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/subresource-integrity_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/subresource-integrity_spec.ts similarity index 90% rename from packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/subresource-integrity_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/subresource-integrity_spec.ts index 01ebc05cb65e..036c47497498 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/subresource-integrity_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/subresource-integrity_spec.ts @@ -7,10 +7,10 @@ */ import { logging } from '@angular-devkit/core'; -import { buildEsbuildBrowser } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "subresourceIntegrity"', () => { it(`does not add integrity attribute when not present`, async () => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/setup.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/setup.ts new file mode 100644 index 000000000000..4d07a8b4171a --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/setup.ts @@ -0,0 +1,31 @@ +/** + * @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 { Schema } from '../schema'; + +export { describeBuilder } from '../../../testing'; + +export const APPLICATION_BUILDER_INFO = Object.freeze({ + name: '@angular-devkit/build-angular:application', + schemaPath: __dirname + '/../schema.json', +}); + +/** + * Contains all required browser builder fields. + * Also disables progress reporting to minimize logging output. + */ +export const BASE_OPTIONS = Object.freeze({ + index: 'src/index.html', + browser: 'src/main.ts', + outputPath: 'dist', + tsConfig: 'src/tsconfig.app.json', + progress: false, + + // Disable optimizations + optimization: false, +}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/builder-status-warnings.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/builder-status-warnings.ts index f84e702fc61e..629ea4cbb3a0 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/builder-status-warnings.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/builder-status-warnings.ts @@ -8,7 +8,6 @@ import { BuilderContext } from '@angular-devkit/architect'; import { Schema as BrowserBuilderOptions } from '../browser/schema'; -import { BrowserEsbuildOptions } from './options'; const UNSUPPORTED_OPTIONS: Array = [ 'budgets', @@ -33,7 +32,7 @@ const UNSUPPORTED_OPTIONS: Array = [ 'webWorkerTsConfig', ]; -export function logBuilderStatusWarnings(options: BrowserEsbuildOptions, context: BuilderContext) { +export function logBuilderStatusWarnings(options: BrowserBuilderOptions, context: BuilderContext) { context.logger.warn( `The esbuild-based browser application builder ('browser-esbuild') is currently in developer preview` + ' and is not yet recommended for production use.' + diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index 30749522ea73..21225398b84e 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -8,194 +8,11 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import type { OutputFile } from 'esbuild'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -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 { 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 { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; -import { - calculateEstimatedTransferSizes, - logBuildStats, - logMessages, - withNoProgress, - withSpinner, - writeResultFiles, -} from '../../tools/esbuild/utils'; -import { copyAssets } from '../../utils/copy-assets'; -import { assertIsError } from '../../utils/error'; -import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets'; -import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; -import { getSupportedBrowsers } from '../../utils/supported-browsers'; +import { buildApplicationInternal } from '../application'; +import { Schema as ApplicationBuilderOptions } from '../application/schema'; import { logBuilderStatusWarnings } from './builder-status-warnings'; -import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options'; import { Schema as BrowserBuilderOptions } from './schema'; -async function execute( - options: NormalizedBrowserOptions, - context: BuilderContext, - rebuildState?: RebuildState, -): Promise { - const startTime = process.hrtime.bigint(); - - const { - projectRoot, - workspaceRoot, - optimizationOptions, - assets, - serviceWorkerOptions, - indexHtmlOptions, - cacheOptions, - } = options; - - const browsers = getSupportedBrowsers(projectRoot, context.logger); - const target = transformSupportedBrowsersToTargets(browsers); - - // Reuse rebuild state or create new bundle contexts for code and global stylesheets - let bundlerContexts = rebuildState?.rebuildContexts; - const codeBundleCache = - rebuildState?.codeBundleCache ?? - new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); - if (bundlerContexts === undefined) { - bundlerContexts = []; - - // Application code - bundlerContexts.push( - new BundlerContext( - workspaceRoot, - !!options.watch, - createCodeBundleOptions(options, target, browsers, codeBundleCache), - ), - ); - - // Global Stylesheets - if (options.globalStyles.length > 0) { - for (const initial of [true, false]) { - const bundleOptions = createGlobalStylesBundleOptions( - options, - target, - browsers, - initial, - codeBundleCache?.loadResultCache, - ); - if (bundleOptions) { - bundlerContexts.push( - new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial), - ); - } - } - } - - // Global Scripts - if (options.globalScripts.length > 0) { - for (const initial of [true, false]) { - const bundleOptions = createGlobalScriptsBundleOptions(options, initial); - if (bundleOptions) { - bundlerContexts.push( - new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial), - ); - } - } - } - } - - const bundlingResult = await BundlerContext.bundleAll(bundlerContexts); - - // Log all warnings and errors generated during bundling - await logMessages(context, bundlingResult); - - const executionResult = new ExecutionResult(bundlerContexts, codeBundleCache); - - // Return if the bundling has errors - if (bundlingResult.errors) { - return executionResult; - } - - const { metafile, initialFiles, outputFiles } = bundlingResult; - - executionResult.outputFiles.push(...outputFiles); - - // Check metafile for CommonJS module usage if optimizing scripts - if (optimizationOptions.scripts) { - const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies); - await logMessages(context, { warnings: messages }); - } - - // Generate index HTML file - if (indexHtmlOptions) { - const { errors, warnings, content } = await generateIndexHtml( - initialFiles, - executionResult, - options, - ); - for (const error of errors) { - context.logger.error(error); - } - for (const warning of warnings) { - context.logger.warn(warning); - } - - executionResult.addOutputFile(indexHtmlOptions.output, content); - } - - // Copy assets - if (assets) { - // The webpack copy assets helper is used with no base paths defined. This prevents the helper - // from directly writing to disk. This should eventually be replaced with a more optimized helper. - executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot))); - } - - // Write metafile if stats option is enabled - if (options.stats) { - executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2)); - } - - // Extract and write licenses for used packages - if (options.extractLicenses) { - executionResult.addOutputFile( - '3rdpartylicenses.txt', - await extractLicenses(metafile, workspaceRoot), - ); - } - - // Augment the application with service worker support - if (serviceWorkerOptions) { - try { - const serviceWorkerResult = await augmentAppWithServiceWorkerEsbuild( - workspaceRoot, - serviceWorkerOptions, - options.baseHref || '/', - executionResult.outputFiles, - executionResult.assetFiles, - ); - executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest); - executionResult.assetFiles.push(...serviceWorkerResult.assetFiles); - } catch (error) { - context.logger.error(error instanceof Error ? error.message : `${error}`); - - return executionResult; - } - } - - // Calculate estimated transfer size if scripts are optimized - let estimatedTransferSizes; - if (optimizationOptions.scripts || optimizationOptions.styles.minify) { - estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); - } - logBuildStats(context, metafile, initialFiles, estimatedTransferSizes); - - const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9; - context.logger.info(`Application bundle generation complete. [${buildTime.toFixed(3)} seconds]`); - - return executionResult; -} - /** * Main execution function for the esbuild-based application builder. * The options are compatible with the Webpack-based builder. @@ -214,177 +31,23 @@ export function buildEsbuildBrowser( outputFiles?: OutputFile[]; assetFiles?: { source: string; destination: string }[]; } -> { - return buildEsbuildBrowserInternal(userOptions, context, infrastructureSettings); -} - -/** - * Internal version of the main execution function for the esbuild-based application builder. - * Exposes some additional "private" options in addition to those exposed by the schema. - * @param userOptions The browser-esbuild builder options to use when setting up the application build - * @param context The Architect builder context object - * @returns An async iterable with the builder result output - */ -export async function* buildEsbuildBrowserInternal( - userOptions: BrowserEsbuildOptions, - context: BuilderContext, - infrastructureSettings?: { - write?: boolean; - }, -): AsyncIterable< - BuilderOutput & { - outputFiles?: OutputFile[]; - assetFiles?: { source: string; destination: string }[]; - } > { // Inform user of status of builder and options logBuilderStatusWarnings(userOptions, 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; - } - - const normalizedOptions = await normalizeOptions(context, projectName, userOptions); - // Writing the result to the filesystem is the default behavior - const shouldWriteResult = infrastructureSettings?.write !== false; - - if (shouldWriteResult) { - // Clean output path if enabled - if (userOptions.deleteOutputPath) { - if (normalizedOptions.outputPath === normalizedOptions.workspaceRoot) { - context.logger.error('Output path MUST not be workspace root directory!'); - - return; - } - - await fs.rm(normalizedOptions.outputPath, { force: true, recursive: true, maxRetries: 3 }); - } + const normalizedOptions = normalizeOptions(userOptions); - // Create output directory if needed - try { - await fs.mkdir(normalizedOptions.outputPath, { recursive: true }); - } catch (e) { - assertIsError(e); - context.logger.error('Unable to create output directory: ' + e.message); - - return; - } - } - - const withProgress: typeof withSpinner = normalizedOptions.progress - ? withSpinner - : withNoProgress; - - // Initial build - let result: ExecutionResult; - try { - result = await withProgress('Building...', () => execute(normalizedOptions, context)); - - if (shouldWriteResult) { - // Write output files - await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath); - - yield result.output; - } else { - // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - yield result.outputWithFiles as any; - } - - // Finish if watch mode is not enabled - if (!userOptions.watch) { - return; - } - } finally { - // Ensure Sass workers are shutdown if not watching - if (!userOptions.watch) { - shutdownSassWorkerPool(); - } - } - - if (normalizedOptions.progress) { - context.logger.info('Watch mode enabled. Watching for file changes...'); - } - - // Setup a watcher - const { createWatcher } = await import('../../tools/esbuild/watcher'); - const watcher = createWatcher({ - polling: typeof userOptions.poll === 'number', - interval: userOptions.poll, - ignored: [ - // Ignore the output and cache paths to avoid infinite rebuild cycles - normalizedOptions.outputPath, - normalizedOptions.cacheOptions.basePath, - // Ignore all node modules directories to avoid excessive file watchers. - // Package changes are handled below by watching manifest and lock files. - '**/node_modules/**', - '**/.*/**', - ], - }); - - // Temporarily watch the entire project - watcher.add(normalizedOptions.projectRoot); - - // Watch workspace for package manager changes - const packageWatchFiles = [ - // manifest can affect module resolution - 'package.json', - // npm lock file - 'package-lock.json', - // pnpm lock file - 'pnpm-lock.yaml', - // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/) - 'yarn.lock', - '.pnp.cjs', - '.pnp.data.json', - ]; - watcher.add(packageWatchFiles.map((file) => path.join(normalizedOptions.workspaceRoot, file))); - - // Watch locations provided by the initial build result - let previousWatchFiles = new Set(result.watchFiles); - watcher.add(result.watchFiles); - - // Wait for changes and rebuild as needed - try { - for await (const changes of watcher) { - if (userOptions.verbose) { - context.logger.info(changes.toDebugString()); - } - - result = await withProgress('Changes detected. Rebuilding...', () => - execute(normalizedOptions, context, result.createRebuildState(changes)), - ); - - // Update watched locations provided by the new build result. - // Add any new locations - watcher.add(result.watchFiles.filter((watchFile) => !previousWatchFiles.has(watchFile))); - const newWatchFiles = new Set(result.watchFiles); - // Remove any old locations - watcher.remove([...previousWatchFiles].filter((watchFile) => !newWatchFiles.has(watchFile))); - previousWatchFiles = newWatchFiles; + return buildApplicationInternal(normalizedOptions, context, infrastructureSettings); +} - if (shouldWriteResult) { - // Write output files - await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath); +function normalizeOptions(options: BrowserBuilderOptions): ApplicationBuilderOptions { + const { main: browser, ngswConfigPath, serviceWorker, ...otherOptions } = options; - yield result.output; - } else { - // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - yield result.outputWithFiles as any; - } - } - } finally { - // Stop the watcher - await watcher.close(); - // Cleanup incremental rebuild state - await result.dispose(); - shutdownSassWorkerPool(); - } + return { + browser, + serviceWorker: serviceWorker ? ngswConfigPath : false, + ...otherOptions, + }; } export default createBuilder(buildEsbuildBrowser); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/entry-points_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/entry-points_spec.ts deleted file mode 100644 index 9aa83c6d678d..000000000000 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/entry-points_spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @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 { promises as fs } from 'fs'; -import { tmpdir } from 'os'; -import * as path from 'path'; -import { buildEsbuildBrowserInternal } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; - -describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => { - let tempDir!: string; - - beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(tmpdir(), 'angular-cli-e2e-browser-esbuild-main-spec-')); - }); - - afterEach(async () => { - await fs.rm(tempDir, { recursive: true }); - }); - - describe('Option: "entryPoints"', () => { - it('provides multiple entry points', async () => { - await harness.writeFiles({ - 'src/entry1.ts': `console.log('entry1');`, - 'src/entry2.ts': `console.log('entry2');`, - 'tsconfig.app.json': ` - { - "extends": "./tsconfig.json", - "files": ["src/entry1.ts", "src/entry2.ts"] - } - `, - }); - - harness.useTarget('build', { - ...BASE_OPTIONS, - main: undefined, - tsConfig: 'tsconfig.app.json', - entryPoints: new Set(['src/entry1.ts', 'src/entry2.ts']), - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/entry1.js').toExist(); - harness.expectFile('dist/entry2.js').toExist(); - }); - - it('throws when `main` is omitted and an empty `entryPoints` Set is provided', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - main: undefined, - entryPoints: new Set(), - }); - - const { result, error } = await harness.executeOnce(); - expect(result).toBeUndefined(); - - expect(error?.message).toContain('Either `main` or at least one `entryPoints`'); - }); - - it('throws when provided with a `main` option', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - main: 'src/main.ts', - entryPoints: new Set(['src/entry.ts']), - }); - - const { result, error } = await harness.executeOnce(); - expect(result).toBeUndefined(); - - expect(error?.message).toContain('Only one of `main` or `entryPoints` may be provided.'); - }); - - it('resolves entry points outside the workspace root', async () => { - const entry = path.join(tempDir, 'entry.mjs'); - await fs.writeFile(entry, `console.log('entry');`); - - harness.useTarget('build', { - ...BASE_OPTIONS, - main: undefined, - entryPoints: new Set([entry]), - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/entry.js').toExist(); - }); - - it('throws an error when multiple entry points output to the same location', async () => { - // Would generate `/entry.mjs` in the output directory. - const entry1 = path.join(tempDir, 'entry.mjs'); - await fs.writeFile(entry1, `console.log('entry1');`); - - // Would also generate `/entry.mjs` in the output directory. - const subDir = path.join(tempDir, 'subdir'); - await fs.mkdir(subDir); - const entry2 = path.join(subDir, 'entry.mjs'); - await fs.writeFile(entry2, `console.log('entry2');`); - - harness.useTarget('build', { - ...BASE_OPTIONS, - main: undefined, - entryPoints: new Set([entry1, entry2]), - }); - - const { result, error } = await harness.executeOnce(); - expect(result).toBeUndefined(); - - expect(error?.message).toContain(entry1); - expect(error?.message).toContain(entry2); - expect(error?.message).toContain('both output to the same location'); - }); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/out-extension_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/out-extension_spec.ts deleted file mode 100644 index 331bf72f9071..000000000000 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/out-extension_spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @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 { buildEsbuildBrowserInternal } from '../../index'; -import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; - -describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => { - describe('Option: "outExtension"', () => { - it('outputs `.js` files when explicitly set to "js"', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - main: 'src/main.ts', - outExtension: 'js', - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - // Should generate the correct file extension. - harness.expectFile('dist/main.js').toExist(); - expect(harness.hasFile('dist/main.mjs')).toBeFalse(); - - // Index page should link to the correct file extension. - const indexContents = harness.readFile('dist/index.html'); - expect(indexContents).toContain('src="main.js"'); - }); - - it('outputs `.mjs` files when set to "mjs"', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - main: 'src/main.ts', - outExtension: 'mjs', - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - // Should generate the correct file extension. - harness.expectFile('dist/main.mjs').toExist(); - expect(harness.hasFile('dist/main.js')).toBeFalse(); - - // Index page should link to the correct file extension. - const indexContents = harness.readFile('dist/index.html'); - expect(indexContents).toContain('src="main.mjs"'); - }); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index 6fbdb96e5645..32eedaa725ba 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -17,8 +17,7 @@ import type { AddressInfo } from 'node:net'; import path from 'node:path'; import { InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite'; import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer'; -import { buildEsbuildBrowserInternal } from '../browser-esbuild'; -import { BrowserEsbuildOptions } from '../browser-esbuild/options'; +import { buildEsbuildBrowser } from '../browser-esbuild'; import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema'; import { loadProxyConfiguration } from './load-proxy-config'; import type { NormalizedDevServerOptions } from './options'; @@ -54,7 +53,7 @@ export async function* serveWithVite( verbose: serverOptions.verbose, } as json.JsonObject & BrowserBuilderOptions, builderName, - )) as json.JsonObject & BrowserEsbuildOptions; + )) as json.JsonObject & BrowserBuilderOptions; // Set all packages as external to support Vite's prebundle caching browserOptions.externalPackages = serverOptions.cacheOptions.enabled; @@ -67,7 +66,7 @@ export async function* serveWithVite( const generatedFiles = new Map(); const assetFiles = new Map(); // TODO: Switch this to an architect schedule call when infrastructure settings are supported - for await (const result of buildEsbuildBrowserInternal(browserOptions, context, { + for await (const result of buildEsbuildBrowser(browserOptions, context, { write: false, })) { assert(result.outputFiles, 'Builder did not provide result files.'); diff --git a/packages/angular_devkit/build_angular/src/builders/jest/index.ts b/packages/angular_devkit/build_angular/src/builders/jest/index.ts index 262c5c0668d2..7869029fd4fb 100644 --- a/packages/angular_devkit/build_angular/src/builders/jest/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/jest/index.ts @@ -11,8 +11,8 @@ import { execFile as execFileCb } from 'child_process'; import * as path from 'path'; import { promisify } from 'util'; import { colors } from '../../utils/color'; -import { buildEsbuildBrowserInternal } from '../browser-esbuild'; -import { BrowserEsbuildOptions } from '../browser-esbuild/options'; +import { buildApplicationInternal } from '../application'; +import { ApplicationBuilderInternalOptions } from '../application/options'; import { OutputHashing } from '../browser-esbuild/schema'; import { normalizeOptions } from './options'; import { Schema as JestBuilderSchema } from './schema'; @@ -68,9 +68,7 @@ export default createBuilder( index: null, outputHashing: OutputHashing.None, outExtension: 'mjs', // Force native ESM. - commonChunk: false, optimization: false, - buildOptimizer: false, sourceMap: { scripts: true, styles: false, @@ -140,10 +138,10 @@ export default createBuilder( async function build( context: BuilderContext, - options: BrowserEsbuildOptions, + options: ApplicationBuilderInternalOptions, ): Promise { try { - for await (const _ of buildEsbuildBrowserInternal(options, context)) { + for await (const _ of buildApplicationInternal(options, context)) { // Nothing to do for each event, just wait for the whole build. } diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts index 9f2619934f10..7ae34549229b 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts @@ -7,7 +7,7 @@ */ import type { BuildOptions } from 'esbuild'; -import { NormalizedBrowserOptions } from '../../builders/browser-esbuild/options'; +import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { SourceFileCache, createCompilerPlugin } from '../../tools/esbuild/angular/compiler-plugin'; import { createExternalPackagesPlugin } from '../../tools/esbuild/external-packages-plugin'; import { createSourcemapIngorelistPlugin } from '../../tools/esbuild/sourcemap-ignorelist-plugin'; @@ -16,7 +16,7 @@ import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-pl import { allowMangle } from '../../utils/environment-options'; export function createCodeBundleOptions( - options: NormalizedBrowserOptions, + options: NormalizedApplicationBuildOptions, target: string[], browsers: string[], sourceFileCache?: SourceFileCache, diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts index d3ca2c0c3d85..37a905a163cd 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts @@ -11,7 +11,7 @@ import MagicString, { Bundle } from 'magic-string'; import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import type { NormalizedBrowserOptions } from '../../builders/browser-esbuild/options'; +import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { assertIsError } from '../../utils/error'; import { LoadResultCache, createCachedLoad } from './load-result-cache'; import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin'; @@ -24,7 +24,7 @@ import { createVirtualModulePlugin } from './virtual-module-plugin'; * @returns An esbuild BuildOptions object. */ export function createGlobalScriptsBundleOptions( - options: NormalizedBrowserOptions, + options: NormalizedApplicationBuildOptions, initial: boolean, loadCache?: LoadResultCache, ): BuildOptions | undefined { diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts index 0722cb361250..7f8b9d592d05 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts @@ -8,13 +8,13 @@ import type { BuildOptions } from 'esbuild'; import assert from 'node:assert'; -import { NormalizedBrowserOptions } from '../../builders/browser-esbuild/options'; +import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { LoadResultCache } from './load-result-cache'; import { createStylesheetBundleOptions } from './stylesheets/bundle-options'; import { createVirtualModulePlugin } from './virtual-module-plugin'; export function createGlobalStylesBundleOptions( - options: NormalizedBrowserOptions, + options: NormalizedApplicationBuildOptions, target: string[], browsers: string[], initial: boolean, diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/index-html-generator.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/index-html-generator.ts index 030723ca359f..3a113c6d9693 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/index-html-generator.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/index-html-generator.ts @@ -8,7 +8,7 @@ import assert from 'node:assert'; import path from 'node:path'; -import { NormalizedBrowserOptions } from '../../builders/browser-esbuild/options'; +import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { IndexHtmlGenerator, IndexHtmlTransformResult, @@ -19,7 +19,7 @@ import type { ExecutionResult } from './bundler-execution-result'; export function generateIndexHtml( initialFiles: Map, executionResult: ExecutionResult, - buildOptions: NormalizedBrowserOptions, + buildOptions: NormalizedApplicationBuildOptions, ): Promise { // Analyze metafile for initial link-based hints. // Skip if the internal externalPackages option is enabled since this option requires diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts index b638bb69a8e3..421a0d96fdd3 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts @@ -209,3 +209,53 @@ export function createOutputFileFromText(path: string, text: string): OutputFile }, }; } + +/** + * Transform browserlists result to esbuild target. + * @see https://esbuild.github.io/api/#target + */ +export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] { + const transformed: string[] = []; + + // https://esbuild.github.io/api/#target + const esBuildSupportedBrowsers = new Set([ + 'chrome', + 'edge', + 'firefox', + 'ie', + 'ios', + 'node', + 'opera', + 'safari', + ]); + + for (const browser of supportedBrowsers) { + let [browserName, version] = browser.toLowerCase().split(' '); + + // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` + if (browserName === 'ios_saf') { + browserName = 'ios'; + } + + // browserslist uses ranges `15.2-15.3` versions but only the lowest is required + // to perform minimum supported feature checks. esbuild also expects a single version. + [version] = version.split('-'); + + if (esBuildSupportedBrowsers.has(browserName)) { + if (browserName === 'safari' && version === 'tp') { + // esbuild only supports numeric versions so `TP` is converted to a high number (999) since + // a Technology Preview (TP) of Safari is assumed to support all currently known features. + version = '999'; + } else if (!version.includes('.')) { + // A lone major version is considered by esbuild to include all minor versions. However, + // browserslist does not and is also inconsistent in its `.0` version naming. For example, + // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`. + version += '.0'; + } + + transformed.push(browserName + version); + } + } + + return transformed; +} diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/plugins/css-optimizer-plugin.ts b/packages/angular_devkit/build_angular/src/tools/webpack/plugins/css-optimizer-plugin.ts index 3627c5f9a9bb..95ba7340853f 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/plugins/css-optimizer-plugin.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/plugins/css-optimizer-plugin.ts @@ -8,8 +8,8 @@ import type { Message, TransformResult } from 'esbuild'; import type { Compilation, Compiler, sources } from 'webpack'; -import { transformSupportedBrowsersToTargets } from '../../../utils/esbuild-targets'; import { addWarning } from '../../../utils/webpack-diagnostics'; +import { transformSupportedBrowsersToTargets } from '../../esbuild/utils'; import { EsbuildExecutor } from './esbuild-executor'; /** diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/plugins/javascript-optimizer-plugin.ts b/packages/angular_devkit/build_angular/src/tools/webpack/plugins/javascript-optimizer-plugin.ts index 3c1436522328..0ba4c4bd4504 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/plugins/javascript-optimizer-plugin.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/plugins/javascript-optimizer-plugin.ts @@ -9,8 +9,8 @@ import Piscina from 'piscina'; import type { Compiler, sources } from 'webpack'; import { maxWorkers } from '../../../utils/environment-options'; -import { transformSupportedBrowsersToTargets } from '../../../utils/esbuild-targets'; import { addError } from '../../../utils/webpack-diagnostics'; +import { transformSupportedBrowsersToTargets } from '../../esbuild/utils'; import { EsbuildExecutor } from './esbuild-executor'; import type { OptimizeRequestOptions } from './javascript-optimizer-worker'; diff --git a/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts b/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts deleted file mode 100644 index e9310e171d5a..000000000000 --- a/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @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 - */ - -/** - * Transform browserlists result to esbuild target. - * @see https://esbuild.github.io/api/#target - */ -export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] { - const transformed: string[] = []; - - // https://esbuild.github.io/api/#target - const esBuildSupportedBrowsers = new Set([ - 'chrome', - 'edge', - 'firefox', - 'ie', - 'ios', - 'node', - 'opera', - 'safari', - ]); - - for (const browser of supportedBrowsers) { - let [browserName, version] = browser.toLowerCase().split(' '); - - // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` - if (browserName === 'ios_saf') { - browserName = 'ios'; - } - - // browserslist uses ranges `15.2-15.3` versions but only the lowest is required - // to perform minimum supported feature checks. esbuild also expects a single version. - [version] = version.split('-'); - - if (esBuildSupportedBrowsers.has(browserName)) { - if (browserName === 'safari' && version === 'tp') { - // esbuild only supports numeric versions so `TP` is converted to a high number (999) since - // a Technology Preview (TP) of Safari is assumed to support all currently known features. - version = '999'; - } else if (!version.includes('.')) { - // A lone major version is considered by esbuild to include all minor versions. However, - // browserslist does not and is also inconsistent in its `.0` version naming. For example, - // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`. - version += '.0'; - } - - transformed.push(browserName + version); - } - } - - return transformed; -}