diff --git a/.browserslistrc b/.browserslistrc index 16b54cbe..41deb91a 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,7 +1,7 @@ -Chrome >= 110 -ChromeAndroid >= 110 -Edge >= 110 -Firefox >= 110 -FirefoxAndroid >= 110 +Chrome >= 107 +ChromeAndroid >= 107 +Edge >= 107 +Firefox >= 104 +FirefoxAndroid >= 104 Safari >= 16 iOS >= 16 diff --git a/package.json b/package.json index b5a33c1b..f333fce2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/build", - "version": "21.0.0-next.2+sha-9749ec6", + "version": "20.3.13+sha-948869d", "description": "Official build system for Angular", "keywords": [ "Angular CLI", @@ -23,11 +23,11 @@ "builders": "builders.json", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#9749ec6", - "@babel/core": "7.28.4", + "@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#948869d", + "@babel/core": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.16", + "@inquirer/confirm": "5.1.14", "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", "browserslist": "^4.23.0", @@ -35,39 +35,39 @@ "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", - "listr2": "9.0.3", - "magic-string": "0.30.19", + "listr2": "9.0.1", + "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", "piscina": "5.1.3", - "rolldown": "1.0.0-beta.36", - "sass": "1.92.1", + "rollup": "4.52.3", + "sass": "1.90.0", "semver": "7.7.2", "source-map-support": "0.5.21", - "tinyglobby": "0.2.15", - "vite": "7.1.5", + "tinyglobby": "0.2.14", + "vite": "7.1.11", "watchpack": "2.4.4" }, "optionalDependencies": { "lmdb": "3.4.2" }, "peerDependencies": { - "@angular/core": "^21.0.0-next.0", - "@angular/compiler": "^21.0.0-next.0", - "@angular/compiler-cli": "^21.0.0-next.0", - "@angular/localize": "^21.0.0-next.0", - "@angular/platform-browser": "^21.0.0-next.0", - "@angular/platform-server": "^21.0.0-next.0", - "@angular/service-worker": "^21.0.0-next.0", - "@angular/ssr": "github:angular/angular-ssr-builds#9749ec6", + "@angular/core": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@angular/localize": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/service-worker": "^20.0.0", + "@angular/ssr": "github:angular/angular-ssr-builds#948869d", "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^21.0.0-next.0", + "ng-packagr": "^20.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.9 <6.0", + "typescript": ">=5.8 <6.0", "vitest": "^3.1.1" }, "peerDependenciesMeta": { @@ -112,7 +112,7 @@ "type": "git", "url": "https://github.com/angular/angular-cli.git" }, - "packageManager": "pnpm@10.15.1", + "packageManager": "pnpm@10.19.0", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", diff --git a/src/builders/application/chunk-optimizer.js b/src/builders/application/chunk-optimizer.js index 06353085..959f08d5 100644 --- a/src/builders/application/chunk-optimizer.js +++ b/src/builders/application/chunk-optimizer.js @@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.optimizeChunks = optimizeChunks; const node_assert_1 = __importDefault(require("node:assert")); -const rolldown_1 = require("rolldown"); +const rollup_1 = require("rollup"); const bundler_context_1 = require("../../tools/esbuild/bundler-context"); const utils_1 = require("../../tools/esbuild/utils"); const error_1 = require("../../utils/error"); @@ -171,7 +171,7 @@ async function optimizeChunks(original, sourcemap) { let bundle; let optimizedOutput; try { - bundle = await (0, rolldown_1.rolldown)({ + bundle = await (0, rollup_1.rollup)({ input: mainFile, plugins: [ { @@ -198,8 +198,7 @@ async function optimizeChunks(original, sourcemap) { ], }); const result = await bundle.generate({ - minify: { mangle: false, compress: false }, - advancedChunks: { minSize: 8192 }, + compact: true, sourcemap, chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, }); diff --git a/src/builders/application/index.js b/src/builders/application/index.js index 3ad144c3..69e9fe9f 100644 --- a/src/builders/application/index.js +++ b/src/builders/application/index.js @@ -6,6 +6,39 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -44,6 +77,12 @@ context, extensions) { yield { kind: results_1.ResultKind.Failure, errors: [] }; return; } + if (environment_options_1.bazelEsbuildPluginPath) { + extensions ??= {}; + extensions.codePlugins ??= []; + const { default: bazelEsbuildPlugin } = await Promise.resolve(`${environment_options_1.bazelEsbuildPluginPath}`).then(s => __importStar(require(s))); + extensions.codePlugins.push(bazelEsbuildPlugin); + } const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options, extensions); if (!normalizedOptions.outputOptions.ignoreServer) { const { browser, server } = normalizedOptions.outputOptions; @@ -81,7 +120,8 @@ context, extensions) { } const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9; const hasError = result.errors.length > 0; - result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`); + result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}.` + + ` [${buildTime.toFixed(3)} seconds] - ${new Date().toISOString()}\n`); } return result; }, { diff --git a/src/builders/application/options.d.ts b/src/builders/application/options.d.ts index f7d34873..5c3a94b2 100644 --- a/src/builders/application/options.d.ts +++ b/src/builders/application/options.d.ts @@ -177,10 +177,7 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s file: string; package: string; } | undefined; - postcssConfiguration: { - configPath: string; - config: import("../../utils/postcss-configuration").PostcssConfiguration; - } | undefined; + postcssConfiguration: import("../../utils/postcss-configuration").PostcssConfiguration | undefined; i18nOptions: I18nOptions & { duplicateTranslationBehavior?: I18NTranslation; missingTranslationBehavior?: I18NTranslation; diff --git a/src/builders/application/options.js b/src/builders/application/options.js index 28bb02db..f9d8637d 100644 --- a/src/builders/application/options.js +++ b/src/builders/application/options.js @@ -449,7 +449,9 @@ function getLocaleBaseHref(baseHref = '', i18n, locale) { return undefined; } const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/'; - return baseHrefSuffix !== '' ? (0, url_1.urlJoin)(baseHref, baseHrefSuffix) : undefined; + return baseHrefSuffix !== '' + ? (0, url_1.addTrailingSlash)((0, url_1.joinUrlParts)(baseHref, baseHrefSuffix)) + : undefined; } /** * Normalizes an array of external dependency paths by ensuring that diff --git a/src/builders/application/schema.d.ts b/src/builders/application/schema.d.ts index b87232e4..6f48a1ce 100644 --- a/src/builders/application/schema.d.ts +++ b/src/builders/application/schema.d.ts @@ -16,7 +16,8 @@ export type Schema = { */ appShell?: boolean; /** - * List of static application assets. + * Define the assets to be copied to the output directory. These assets are copied as-is + * without any further processing or hashing. */ assets?: AssetPattern[]; /** @@ -126,12 +127,18 @@ export type Schema = { optimization?: OptimizationUnion; /** * Define the output filename cache-busting hashing mode. + * + * - `none`: No hashing. + * - `all`: Hash for all output bundles. + * - `media`: Hash for all output media (e.g., images, fonts, etc. that are referenced in + * CSS files). + * - `bundles`: Hash for output of lazy and main bundles. */ outputHashing?: OutputHashing; /** - * Defines the build output target. 'static': Generates a static site for deployment on any - * static hosting service. 'server': Produces an application designed for deployment on a - * server that supports server-side rendering (SSR). + * Defines the type of build output artifact. 'static': Generates a static site build + * artifact for deployment on any static hosting service. 'server': Generates a server + * application build artifact, required for applications using hybrid rendering or APIs. */ outputMode?: OutputMode; /** @@ -407,6 +414,12 @@ export type StylesClass = { }; /** * Define the output filename cache-busting hashing mode. + * + * - `none`: No hashing. + * - `all`: Hash for all output bundles. + * - `media`: Hash for all output media (e.g., images, fonts, etc. that are referenced in + * CSS files). + * - `bundles`: Hash for output of lazy and main bundles. */ export declare enum OutputHashing { All = "all", @@ -415,9 +428,9 @@ export declare enum OutputHashing { None = "none" } /** - * Defines the build output target. 'static': Generates a static site for deployment on any - * static hosting service. 'server': Produces an application designed for deployment on a - * server that supports server-side rendering (SSR). + * Defines the type of build output artifact. 'static': Generates a static site build + * artifact for deployment on any static hosting service. 'server': Generates a server + * application build artifact, required for applications using hybrid rendering or APIs. */ export declare enum OutputMode { Server = "server", diff --git a/src/builders/application/schema.js b/src/builders/application/schema.js index 1df5f7bf..ee708c2d 100644 --- a/src/builders/application/schema.js +++ b/src/builders/application/schema.js @@ -48,6 +48,12 @@ var InlineStyleLanguage; })(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); /** * Define the output filename cache-busting hashing mode. + * + * - `none`: No hashing. + * - `all`: Hash for all output bundles. + * - `media`: Hash for all output media (e.g., images, fonts, etc. that are referenced in + * CSS files). + * - `bundles`: Hash for output of lazy and main bundles. */ var OutputHashing; (function (OutputHashing) { @@ -57,9 +63,9 @@ var OutputHashing; OutputHashing["None"] = "none"; })(OutputHashing || (exports.OutputHashing = OutputHashing = {})); /** - * Defines the build output target. 'static': Generates a static site for deployment on any - * static hosting service. 'server': Produces an application designed for deployment on a - * server that supports server-side rendering (SSR). + * Defines the type of build output artifact. 'static': Generates a static site build + * artifact for deployment on any static hosting service. 'server': Generates a server + * application build artifact, required for applications using hybrid rendering or APIs. */ var OutputMode; (function (OutputMode) { diff --git a/src/builders/application/schema.json b/src/builders/application/schema.json index 3ee8699e..8db4e614 100644 --- a/src/builders/application/schema.json +++ b/src/builders/application/schema.json @@ -6,7 +6,7 @@ "properties": { "assets": { "type": "array", - "description": "List of static application assets.", + "description": "Define the assets to be copied to the output directory. These assets are copied as-is without any further processing or hashing.", "default": [], "items": { "$ref": "#/definitions/assetPattern" @@ -441,7 +441,7 @@ }, "outputHashing": { "type": "string", - "description": "Define the output filename cache-busting hashing mode.", + "description": "Define the output filename cache-busting hashing mode.\n\n- `none`: No hashing.\n- `all`: Hash for all output bundles. \n- `media`: Hash for all output media (e.g., images, fonts, etc. that are referenced in CSS files).\n- `bundles`: Hash for output of lazy and main bundles.", "default": "none", "enum": ["none", "all", "media", "bundles"] }, @@ -611,7 +611,7 @@ }, "outputMode": { "type": "string", - "description": "Defines the build output target. 'static': Generates a static site for deployment on any static hosting service. 'server': Produces an application designed for deployment on a server that supports server-side rendering (SSR).", + "description": "Defines the type of build output artifact. 'static': Generates a static site build artifact for deployment on any static hosting service. 'server': Generates a server application build artifact, required for applications using hybrid rendering or APIs.", "enum": ["static", "server"] } }, diff --git a/src/builders/dev-server/builder.js b/src/builders/dev-server/builder.js index ff30b1ee..3965771a 100644 --- a/src/builders/dev-server/builder.js +++ b/src/builders/dev-server/builder.js @@ -11,7 +11,7 @@ exports.execute = execute; const check_port_1 = require("../../utils/check-port"); const internal_1 = require("./internal"); const options_1 = require("./options"); -const vite_1 = require("./vite"); +const vite_server_1 = require("./vite-server"); /** * A Builder that executes a development server based on the provided browser target option. * @@ -33,7 +33,7 @@ async function* execute(options, context, extensions) { return; } const { builderName, normalizedOptions } = await initialize(options, projectName, context); - yield* (0, vite_1.serveWithVite)(normalizedOptions, builderName, (options, context, plugins) => (0, internal_1.buildApplicationInternal)(options, context, { codePlugins: plugins }), context, { indexHtml: extensions?.indexHtmlTransformer }, extensions); + yield* (0, vite_server_1.serveWithVite)(normalizedOptions, builderName, (options, context, plugins) => (0, internal_1.buildApplicationInternal)(options, context, { codePlugins: plugins }), context, { indexHtml: extensions?.indexHtmlTransformer }, extensions); } async function initialize(initialOptions, projectName, context) { // Purge old build disk cache. diff --git a/src/builders/dev-server/vite-server.d.ts b/src/builders/dev-server/vite-server.d.ts new file mode 100644 index 00000000..2452ec43 --- /dev/null +++ b/src/builders/dev-server/vite-server.d.ts @@ -0,0 +1,42 @@ +/** + * @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.dev/license + */ +import type { BuilderContext } from '@angular-devkit/architect'; +import type { Plugin } from 'esbuild'; +import type { Connect, InlineConfig } from 'vite'; +import type { ComponentStyleRecord } from '../../tools/vite/middlewares'; +import { ServerSsrMode } from '../../tools/vite/plugins'; +import { EsbuildLoaderOption } from '../../tools/vite/utils'; +import { Result } from '../application/results'; +import { type ApplicationBuilderInternalOptions, BuildOutputFileType, type ExternalResultMetadata, JavaScriptTransformer } from './internal'; +import type { NormalizedDevServerOptions } from './options'; +import type { DevServerBuilderOutput } from './output'; +interface OutputFileRecord { + contents: Uint8Array; + size: number; + hash: string; + updated: boolean; + servable: boolean; + type: BuildOutputFileType; +} +interface OutputAssetRecord { + source: string; + updated: boolean; +} +interface DevServerExternalResultMetadata extends Omit { + explicitBrowser: string[]; + explicitServer: string[]; +} +export type BuilderAction = (options: ApplicationBuilderInternalOptions, context: BuilderContext, plugins?: Plugin[]) => AsyncIterable; +export declare function serveWithVite(serverOptions: NormalizedDevServerOptions, builderName: string, builderAction: BuilderAction, context: BuilderContext, transformers?: { + indexHtml?: (content: string) => Promise; +}, extensions?: { + middleware?: Connect.NextHandleFunction[]; + buildPlugins?: Plugin[]; +}): AsyncIterableIterator; +export declare function setupServer(serverOptions: NormalizedDevServerOptions, outputFiles: Map, assets: Map, preserveSymlinks: boolean | undefined, externalMetadata: DevServerExternalResultMetadata, ssrMode: ServerSsrMode, prebundleTransformer: JavaScriptTransformer, target: string[], zoneless: boolean, componentStyles: Map, templateUpdates: Map, prebundleLoaderExtensions: EsbuildLoaderOption | undefined, define: ApplicationBuilderInternalOptions['define'], extensionMiddleware?: Connect.NextHandleFunction[], indexHtmlTransformer?: (content: string) => Promise, thirdPartySourcemaps?: boolean): Promise; +export {}; diff --git a/src/builders/dev-server/vite/index.js b/src/builders/dev-server/vite-server.js similarity index 53% rename from src/builders/dev-server/vite/index.js rename to src/builders/dev-server/vite-server.js index b6696d50..271dc0c2 100644 --- a/src/builders/dev-server/vite/index.js +++ b/src/builders/dev-server/vite-server.js @@ -44,19 +44,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.serveWithVite = serveWithVite; +exports.setupServer = setupServer; const node_assert_1 = __importDefault(require("node:assert")); +const promises_1 = require("node:fs/promises"); const node_module_1 = require("node:module"); const node_path_1 = require("node:path"); -const plugins_1 = require("../../../tools/vite/plugins"); -const utils_1 = require("../../../utils"); -const environment_options_1 = require("../../../utils/environment-options"); -const load_esm_1 = require("../../../utils/load-esm"); -const results_1 = require("../../application/results"); -const schema_1 = require("../../application/schema"); -const internal_1 = require("../internal"); -const hmr_1 = require("./hmr"); -const server_1 = require("./server"); -const utils_2 = require("./utils"); +const plugins_1 = require("../../tools/vite/plugins"); +const utils_1 = require("../../tools/vite/utils"); +const utils_2 = require("../../utils"); +const environment_options_1 = require("../../utils/environment-options"); +const load_esm_1 = require("../../utils/load-esm"); +const results_1 = require("../application/results"); +const schema_1 = require("../application/schema"); +const internal_1 = require("./internal"); /** * Build options that are also present on the dev server but are only passed * to the build. @@ -110,7 +110,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context // When localization is enabled with a single locale, force a flat path to maintain behavior with the existing Webpack-based dev server. browserOptions.forceI18nFlatOutput = true; } - const { vendor: thirdPartySourcemaps, scripts: scriptsSourcemaps } = (0, utils_1.normalizeSourceMaps)(browserOptions.sourceMap ?? false); + const { vendor: thirdPartySourcemaps, scripts: scriptsSourcemaps } = (0, utils_2.normalizeSourceMaps)(browserOptions.sourceMap ?? false); if (scriptsSourcemaps && browserOptions.server) { // https://nodejs.org/api/process.html#processsetsourcemapsenabledval process.setSourceMapsEnabled(true); @@ -203,7 +203,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context componentStyles.clear(); generatedFiles.clear(); for (const [outputPath, file] of Object.entries(result.files)) { - (0, utils_2.updateResultRecord)(outputPath, file, normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles, + updateResultRecord(outputPath, file, normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles, // The initial build will not yet have a server setup !server); } @@ -220,10 +220,10 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context assetFiles.delete(filePath); } for (const modified of result.modified) { - (0, utils_2.updateResultRecord)(modified, result.files[modified], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles); + updateResultRecord(modified, result.files[modified], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles); } for (const added of result.added) { - (0, utils_2.updateResultRecord)(added, result.files[added], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles); + updateResultRecord(added, result.files[added], normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles); } break; case results_1.ResultKind.ComponentUpdate: @@ -247,8 +247,8 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context // To avoid disconnecting the array objects from the option, these arrays need to be mutated instead of replaced. if (result.detail?.['externalMetadata']) { const { implicitBrowser, implicitServer, explicit } = result.detail['externalMetadata']; - const implicitServerFiltered = implicitServer.filter((m) => !(0, node_module_1.isBuiltin)(m) && !(0, utils_2.isAbsoluteUrl)(m)); - const implicitBrowserFiltered = implicitBrowser.filter((m) => !(0, utils_2.isAbsoluteUrl)(m)); + const implicitServerFiltered = implicitServer.filter((m) => !(0, node_module_1.isBuiltin)(m) && !isAbsoluteUrl(m)); + const implicitBrowserFiltered = implicitBrowser.filter((m) => !isAbsoluteUrl(m)); // Empty Arrays to avoid growing unlimited with every re-build. externalMetadata.explicitBrowser.length = 0; externalMetadata.explicitServer.length = 0; @@ -274,9 +274,9 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context ...[...assetFiles.values()].map(({ source }) => source), ]), ]; - const updatedFiles = await (0, hmr_1.invalidateUpdatedFiles)(normalizePath, generatedFiles, assetFiles, server); + const updatedFiles = await invalidateUpdatedFiles(normalizePath, generatedFiles, assetFiles, server); if (needClientUpdate) { - (0, hmr_1.handleUpdate)(server, serverOptions, context.logger, componentStyles, updatedFiles); + handleUpdate(server, serverOptions, context.logger, componentStyles, updatedFiles); } } else { @@ -315,7 +315,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context }); } // Setup server and start listening - const serverConfiguration = await (0, server_1.setupServer)(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), componentStyles, templateUpdates, browserOptions.loader, { + const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), componentStyles, templateUpdates, browserOptions.loader, { ...browserOptions.define, 'ngJitMode': browserOptions.aot ? 'false' : 'true', 'ngHmrMode': browserOptions.templateUpdates ? 'true' : 'false', @@ -394,3 +394,343 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context } await new Promise((resolve) => (deferred = resolve)); } +/** + * Invalidates any updated asset or generated files and resets their `updated` state. + * This function also clears the server application cache when necessary. + * + * @returns A list of files that were updated and invalidated. + */ +async function invalidateUpdatedFiles(normalizePath, generatedFiles, assetFiles, server) { + const updatedFiles = []; + // Invalidate any updated asset + for (const [file, record] of assetFiles) { + if (!record.updated) { + continue; + } + record.updated = false; + updatedFiles.push(file); + } + // Invalidate any updated files + let serverApplicationChanged = false; + for (const [file, record] of generatedFiles) { + if (!record.updated) { + continue; + } + record.updated = false; + updatedFiles.push(file); + serverApplicationChanged ||= record.type === internal_1.BuildOutputFileType.ServerApplication; + const updatedModules = server.moduleGraph.getModulesByFile(normalizePath((0, node_path_1.join)(server.config.root, file))); + updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m)); + } + if (serverApplicationChanged) { + // Clear the server app cache and trigger module evaluation before reload to initiate dependency optimization. + // The querystring is needed as a workaround for: + // `ɵgetOrCreateAngularServerApp` can be undefined right after an error. + const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule(`/main.server.mjs?timestamp=${Date.now()}`)); + ɵdestroyAngularServerApp(); + } + return updatedFiles; +} +/** + * Handles updates for the client by sending HMR or full page reload commands + * based on the updated files. It also ensures proper tracking of component styles and determines if + * a full reload is needed. + */ +function handleUpdate(server, serverOptions, logger, componentStyles, updatedFiles) { + if (!updatedFiles.length) { + return; + } + if (serverOptions.hmr) { + if (updatedFiles.every((f) => f.endsWith('.css'))) { + let requiresReload = false; + const timestamp = Date.now(); + const updates = updatedFiles.flatMap((filePath) => { + // For component styles, an HMR update must be sent for each one with the corresponding + // component identifier search parameter (`ngcomp`). The Vite client code will not keep + // the existing search parameters when it performs an update and each one must be + // specified explicitly. Typically, there is only one each though as specific style files + // are not typically reused across components. + const record = componentStyles.get(filePath); + if (record) { + if (record.reload) { + // Shadow DOM components currently require a full reload. + // Vite's CSS hot replacement does not support shadow root searching. + requiresReload = true; + return []; + } + return Array.from(record.used ?? []).map((id) => { + return { + type: 'css-update', + timestamp, + path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''), + acceptedPath: filePath, + }; + }); + } + return { + type: 'css-update', + timestamp, + path: filePath, + acceptedPath: filePath, + }; + }); + if (!requiresReload) { + server.ws.send({ + type: 'update', + updates, + }); + logger.info('Stylesheet update sent to client(s).'); + return; + } + } + } + // Send reload command to clients + if (serverOptions.liveReload) { + // Clear used component tracking on full reload + componentStyles.forEach((record) => record.used?.clear()); + server.ws.send({ + type: 'full-reload', + path: '*', + }); + logger.info('Page reload sent to client(s).'); + } +} +function updateResultRecord(outputPath, file, normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles, initial = false) { + if (file.origin === 'disk') { + assetFiles.set('/' + normalizePath(outputPath), { + source: normalizePath(file.inputPath), + updated: !initial, + }); + return; + } + let filePath; + if (outputPath === htmlIndexPath) { + // Convert custom index output path to standard index path for dev-server usage. + // This mimics the Webpack dev-server behavior. + filePath = '/index.html'; + } + else { + filePath = '/' + normalizePath(outputPath); + } + const servable = file.type === internal_1.BuildOutputFileType.Browser || file.type === internal_1.BuildOutputFileType.Media; + // Skip analysis of sourcemaps + if (filePath.endsWith('.map')) { + generatedFiles.set(filePath, { + contents: file.contents, + servable, + size: file.contents.byteLength, + hash: file.hash, + type: file.type, + updated: false, + }); + return; + } + // New or updated file + generatedFiles.set(filePath, { + contents: file.contents, + size: file.contents.byteLength, + hash: file.hash, + // Consider the files updated except on the initial build result + updated: !initial, + type: file.type, + servable, + }); + // Record any external component styles + if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) { + const componentStyle = componentStyles.get(filePath); + if (componentStyle) { + componentStyle.rawContent = file.contents; + } + else { + componentStyles.set(filePath, { + rawContent: file.contents, + }); + } + } +} +// eslint-disable-next-line max-lines-per-function +async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, componentStyles, templateUpdates, prebundleLoaderExtensions, define, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) { + const proxy = await (0, utils_2.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig); + // dynamically import Vite for ESM compatibility + const { normalizePath } = await (0, load_esm_1.loadEsmModule)('vite'); + // Path will not exist on disk and only used to provide separate path for Vite requests + const virtualProjectRoot = normalizePath((0, node_path_1.join)(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project)); + // Files used for SSR warmup. + let ssrFiles; + switch (ssrMode) { + case plugins_1.ServerSsrMode.InternalSsrMiddleware: + ssrFiles = ['./main.server.mjs']; + break; + case plugins_1.ServerSsrMode.ExternalSsrMiddleware: + ssrFiles = ['./main.server.mjs', './server.mjs']; + break; + } + /** + * Required when using `externalDependencies` to prevent Vite load errors. + * + * @note Can be removed if Vite introduces native support for externals. + * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments + * (e.g., 'foo/bar'), as they are not correctly re-based from the base href. + */ + const preTransformRequests = externalMetadata.explicitBrowser.length === 0 && ssrMode === plugins_1.ServerSsrMode.NoSsr; + const cacheDir = (0, node_path_1.join)(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite'); + const configuration = { + configFile: false, + envFile: false, + cacheDir, + root: virtualProjectRoot, + publicDir: false, + esbuild: false, + mode: 'development', + // We use custom as we do not rely on Vite's htmlFallbackMiddleware and indexHtmlMiddleware. + appType: 'custom', + css: { + devSourcemap: true, + }, + // Ensure custom 'file' loader build option entries are handled by Vite in application code that + // reference third-party libraries. Relative usage is handled directly by the build and not Vite. + // Only 'file' loader entries are currently supported directly by Vite. + assetsInclude: prebundleLoaderExtensions && + Object.entries(prebundleLoaderExtensions) + .filter(([, value]) => value === 'file') + // Create a file extension glob for each key + .map(([key]) => '*' + key), + // Vite will normalize the `base` option by adding a leading slash. + base: serverOptions.servePath, + resolve: { + mainFields: ['es2020', 'browser', 'module', 'main'], + preserveSymlinks, + }, + dev: { + preTransformRequests, + }, + server: { + preTransformRequests, + warmup: { + ssrFiles, + }, + port: serverOptions.port, + strictPort: true, + host: serverOptions.host, + open: serverOptions.open, + allowedHosts: serverOptions.allowedHosts, + headers: serverOptions.headers, + // Disable the websocket if live reload is disabled (false/undefined are the only valid values) + ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, + // When server-side rendering (SSR) is enabled togather with SSL and Express is being used, + // we must configure Vite to use HTTP/1.1. + // This is necessary because Express does not support HTTP/2. + // We achieve this by defining an empty proxy. + // See: https://github.com/vitejs/vite/blob/c4b532cc900bf988073583511f57bd581755d5e3/packages/vite/src/node/http.ts#L106 + proxy: serverOptions.ssl && ssrMode === plugins_1.ServerSsrMode.ExternalSsrMiddleware + ? (proxy ?? {}) + : proxy, + cors: { + // This will add the header `Access-Control-Allow-Origin: http://example.com`, + // where `http://example.com` is the requesting origin. + origin: true, + // Allow preflight requests to be proxied. + preflightContinue: true, + }, + // File watching is handled by the build directly. `null` disables file watching for Vite. + watch: null, + fs: { + // Ensure cache directory, node modules, and all assets are accessible by the client. + // The first two are required for Vite to function in prebundling mode (the default) and to load + // the Vite client-side code for browser reloading. These would be available by default but when + // the `allow` option is explicitly configured, they must be included manually. + allow: [ + cacheDir, + (0, node_path_1.join)(serverOptions.workspaceRoot, 'node_modules'), + ...[...assets.values()].map(({ source }) => source), + ], + }, + }, + ssr: { + // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. + noExternal: /.*/, + // Exclude any Node.js built in module and provided dependencies (currently build defined externals) + external: externalMetadata.explicitServer, + optimizeDeps: (0, utils_1.getDepOptimizationConfig)({ + // Only enable with caching since it causes prebundle dependencies to be cached + disabled: serverOptions.prebundle === false, + // Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins) + exclude: externalMetadata.explicitServer, + // Include all implict dependencies from the external packages internal option + include: externalMetadata.implicitServer, + ssr: true, + prebundleTransformer, + zoneless, + target, + loader: prebundleLoaderExtensions, + thirdPartySourcemaps, + define, + }), + }, + plugins: [ + (0, plugins_1.createAngularSetupMiddlewaresPlugin)({ + outputFiles, + assets, + indexHtmlTransformer, + extensionMiddleware, + componentStyles, + templateUpdates, + ssrMode, + resetComponentUpdates: () => templateUpdates.clear(), + projectRoot: serverOptions.projectRoot, + }), + (0, plugins_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser), + await (0, plugins_1.createAngularSsrTransformPlugin)(serverOptions.workspaceRoot), + await (0, plugins_1.createAngularMemoryPlugin)({ + virtualProjectRoot, + outputFiles, + templateUpdates, + external: externalMetadata.explicitBrowser, + disableViteTransport: !serverOptions.liveReload, + }), + ], + // Browser only optimizeDeps. (This does not run for SSR dependencies). + optimizeDeps: (0, utils_1.getDepOptimizationConfig)({ + // Only enable with caching since it causes prebundle dependencies to be cached + disabled: serverOptions.prebundle === false, + // Exclude any explicitly defined dependencies (currently build defined externals) + exclude: externalMetadata.explicitBrowser, + // Include all implict dependencies from the external packages internal option + include: externalMetadata.implicitBrowser, + ssr: false, + prebundleTransformer, + target, + zoneless, + loader: prebundleLoaderExtensions, + thirdPartySourcemaps, + define, + }), + }; + if (serverOptions.ssl) { + if (serverOptions.sslCert && serverOptions.sslKey) { + configuration.server ??= {}; + // server configuration is defined above + configuration.server.https = { + cert: await (0, promises_1.readFile)(serverOptions.sslCert), + key: await (0, promises_1.readFile)(serverOptions.sslKey), + }; + } + else { + const { default: basicSslPlugin } = await Promise.resolve().then(() => __importStar(require('@vitejs/plugin-basic-ssl'))); + configuration.plugins ??= []; + configuration.plugins.push(basicSslPlugin()); + } + } + return configuration; +} +/** + * Checks if the given value is an absolute URL. + * + * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files. + * + * @param value - The URL or path to check. + * @returns `true` if the value is not an absolute URL; otherwise, `false`. + */ +function isAbsoluteUrl(value) { + return /^(?:https?:)?\/\//.test(value); +} diff --git a/src/builders/dev-server/vite/hmr.d.ts b/src/builders/dev-server/vite/hmr.d.ts deleted file mode 100644 index 5aef3454..00000000 --- a/src/builders/dev-server/vite/hmr.d.ts +++ /dev/null @@ -1,25 +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.dev/license - */ -import type { BuilderContext } from '@angular-devkit/architect'; -import type { ViteDevServer } from 'vite'; -import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; -import type { NormalizedDevServerOptions } from '../options'; -import type { OutputAssetRecord, OutputFileRecord } from './utils'; -/** - * Invalidates any updated asset or generated files and resets their `updated` state. - * This function also clears the server application cache when necessary. - * - * @returns A list of files that were updated and invalidated. - */ -export declare function invalidateUpdatedFiles(normalizePath: (id: string) => string, generatedFiles: Map, assetFiles: Map, server: ViteDevServer): Promise; -/** - * Handles updates for the client by sending HMR or full page reload commands - * based on the updated files. It also ensures proper tracking of component styles and determines if - * a full reload is needed. - */ -export declare function handleUpdate(server: ViteDevServer, serverOptions: NormalizedDevServerOptions, logger: BuilderContext['logger'], componentStyles: Map, updatedFiles: string[]): void; diff --git a/src/builders/dev-server/vite/hmr.js b/src/builders/dev-server/vite/hmr.js deleted file mode 100644 index 46e1e31a..00000000 --- a/src/builders/dev-server/vite/hmr.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.invalidateUpdatedFiles = invalidateUpdatedFiles; -exports.handleUpdate = handleUpdate; -const node_path_1 = require("node:path"); -const internal_1 = require("../internal"); -/** - * Invalidates any updated asset or generated files and resets their `updated` state. - * This function also clears the server application cache when necessary. - * - * @returns A list of files that were updated and invalidated. - */ -async function invalidateUpdatedFiles(normalizePath, generatedFiles, assetFiles, server) { - const updatedFiles = []; - // Invalidate any updated asset - for (const [file, record] of assetFiles) { - if (!record.updated) { - continue; - } - record.updated = false; - updatedFiles.push(file); - } - // Invalidate any updated files - let serverApplicationChanged = false; - for (const [file, record] of generatedFiles) { - if (!record.updated) { - continue; - } - record.updated = false; - updatedFiles.push(file); - serverApplicationChanged ||= record.type === internal_1.BuildOutputFileType.ServerApplication; - const updatedModules = server.moduleGraph.getModulesByFile(normalizePath((0, node_path_1.join)(server.config.root, file))); - updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m)); - } - if (serverApplicationChanged) { - // Clear the server app cache and - // trigger module evaluation before reload to initiate dependency optimization. - const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')); - ɵdestroyAngularServerApp(); - } - return updatedFiles; -} -/** - * Handles updates for the client by sending HMR or full page reload commands - * based on the updated files. It also ensures proper tracking of component styles and determines if - * a full reload is needed. - */ -function handleUpdate(server, serverOptions, logger, componentStyles, updatedFiles) { - if (!updatedFiles.length) { - return; - } - if (serverOptions.hmr) { - if (updatedFiles.every((f) => f.endsWith('.css'))) { - let requiresReload = false; - const timestamp = Date.now(); - const updates = updatedFiles.flatMap((filePath) => { - // For component styles, an HMR update must be sent for each one with the corresponding - // component identifier search parameter (`ngcomp`). The Vite client code will not keep - // the existing search parameters when it performs an update and each one must be - // specified explicitly. Typically, there is only one each though as specific style files - // are not typically reused across components. - const record = componentStyles.get(filePath); - if (record) { - if (record.reload) { - // Shadow DOM components currently require a full reload. - // Vite's CSS hot replacement does not support shadow root searching. - requiresReload = true; - return []; - } - return Array.from(record.used ?? []).map((id) => { - return { - type: 'css-update', - timestamp, - path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''), - acceptedPath: filePath, - }; - }); - } - return { - type: 'css-update', - timestamp, - path: filePath, - acceptedPath: filePath, - }; - }); - if (!requiresReload) { - server.ws.send({ - type: 'update', - updates, - }); - logger.info('Stylesheet update sent to client(s).'); - return; - } - } - } - // Send reload command to clients - if (serverOptions.liveReload) { - // Clear used component tracking on full reload - componentStyles.forEach((record) => record.used?.clear()); - server.ws.send({ - type: 'full-reload', - path: '*', - }); - logger.info('Page reload sent to client(s).'); - } -} diff --git a/src/builders/dev-server/vite/index.d.ts b/src/builders/dev-server/vite/index.d.ts deleted file mode 100644 index e6a455b3..00000000 --- a/src/builders/dev-server/vite/index.d.ts +++ /dev/null @@ -1,21 +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.dev/license - */ -import type { BuilderContext } from '@angular-devkit/architect'; -import type { Plugin } from 'esbuild'; -import type { Connect } from 'vite'; -import { Result } from '../../application/results'; -import { type ApplicationBuilderInternalOptions } from '../internal'; -import type { NormalizedDevServerOptions } from '../options'; -import type { DevServerBuilderOutput } from '../output'; -export type BuilderAction = (options: ApplicationBuilderInternalOptions, context: BuilderContext, plugins?: Plugin[]) => AsyncIterable; -export declare function serveWithVite(serverOptions: NormalizedDevServerOptions, builderName: string, builderAction: BuilderAction, context: BuilderContext, transformers?: { - indexHtml?: (content: string) => Promise; -}, extensions?: { - middleware?: Connect.NextHandleFunction[]; - buildPlugins?: Plugin[]; -}): AsyncIterableIterator; diff --git a/src/builders/dev-server/vite/server.d.ts b/src/builders/dev-server/vite/server.d.ts deleted file mode 100644 index 120aaf71..00000000 --- a/src/builders/dev-server/vite/server.d.ts +++ /dev/null @@ -1,15 +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.dev/license - */ -import type { Connect, InlineConfig } from 'vite'; -import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; -import { ServerSsrMode } from '../../../tools/vite/plugins'; -import { EsbuildLoaderOption } from '../../../tools/vite/utils'; -import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal'; -import type { NormalizedDevServerOptions } from '../options'; -import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils'; -export declare function setupServer(serverOptions: NormalizedDevServerOptions, outputFiles: Map, assets: Map, preserveSymlinks: boolean | undefined, externalMetadata: DevServerExternalResultMetadata, ssrMode: ServerSsrMode, prebundleTransformer: JavaScriptTransformer, target: string[], zoneless: boolean, componentStyles: Map, templateUpdates: Map, prebundleLoaderExtensions: EsbuildLoaderOption | undefined, define: ApplicationBuilderInternalOptions['define'], extensionMiddleware?: Connect.NextHandleFunction[], indexHtmlTransformer?: (content: string) => Promise, thirdPartySourcemaps?: boolean): Promise; diff --git a/src/builders/dev-server/vite/server.js b/src/builders/dev-server/vite/server.js deleted file mode 100644 index 03039f30..00000000 --- a/src/builders/dev-server/vite/server.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.setupServer = setupServer; -const promises_1 = require("node:fs/promises"); -const node_path_1 = require("node:path"); -const plugins_1 = require("../../../tools/vite/plugins"); -const utils_1 = require("../../../tools/vite/utils"); -const utils_2 = require("../../../utils"); -const load_esm_1 = require("../../../utils/load-esm"); -async function createServerConfig(serverOptions, assets, ssrMode, preTransformRequests, cacheDir) { - const proxy = await (0, utils_2.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig); - // Files used for SSR warmup. - let ssrFiles; - switch (ssrMode) { - case plugins_1.ServerSsrMode.InternalSsrMiddleware: - ssrFiles = ['./main.server.mjs']; - break; - case plugins_1.ServerSsrMode.ExternalSsrMiddleware: - ssrFiles = ['./main.server.mjs', './server.mjs']; - break; - } - const server = { - preTransformRequests, - warmup: { - ssrFiles, - }, - port: serverOptions.port, - strictPort: true, - host: serverOptions.host, - open: serverOptions.open, - allowedHosts: serverOptions.allowedHosts, - headers: serverOptions.headers, - // Disable the websocket if live reload is disabled (false/undefined are the only valid values) - ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, - // When server-side rendering (SSR) is enabled togather with SSL and Express is being used, - // we must configure Vite to use HTTP/1.1. - // This is necessary because Express does not support HTTP/2. - // We achieve this by defining an empty proxy. - // See: https://github.com/vitejs/vite/blob/c4b532cc900bf988073583511f57bd581755d5e3/packages/vite/src/node/http.ts#L106 - proxy: serverOptions.ssl && ssrMode === plugins_1.ServerSsrMode.ExternalSsrMiddleware ? (proxy ?? {}) : proxy, - cors: { - // This will add the header `Access-Control-Allow-Origin: http://example.com`, - // where `http://example.com` is the requesting origin. - origin: true, - // Allow preflight requests to be proxied. - preflightContinue: true, - }, - // File watching is handled by the build directly. `null` disables file watching for Vite. - watch: null, - fs: { - // Ensure cache directory, node modules, and all assets are accessible by the client. - // The first two are required for Vite to function in prebundling mode (the default) and to load - // the Vite client-side code for browser reloading. These would be available by default but when - // the `allow` option is explicitly configured, they must be included manually. - allow: [ - cacheDir, - (0, node_path_1.join)(serverOptions.workspaceRoot, 'node_modules'), - ...[...assets.values()].map(({ source }) => source), - ], - }, - }; - if (serverOptions.ssl) { - if (serverOptions.sslCert && serverOptions.sslKey) { - server.https = { - cert: await (0, promises_1.readFile)(serverOptions.sslCert), - key: await (0, promises_1.readFile)(serverOptions.sslKey), - }; - } - } - return server; -} -function createSsrConfig(externalMetadata, serverOptions, prebundleTransformer, zoneless, target, prebundleLoaderExtensions, thirdPartySourcemaps, define) { - return { - // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. - noExternal: /.*/, - // Exclude any Node.js built in module and provided dependencies (currently build defined externals) - external: externalMetadata.explicitServer, - optimizeDeps: (0, utils_1.getDepOptimizationConfig)({ - // Only enable with caching since it causes prebundle dependencies to be cached - disabled: serverOptions.prebundle === false, - // Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins) - exclude: externalMetadata.explicitServer, - // Include all implict dependencies from the external packages internal option - include: externalMetadata.implicitServer, - ssr: true, - prebundleTransformer, - zoneless, - target, - loader: prebundleLoaderExtensions, - thirdPartySourcemaps, - define, - }), - }; -} -async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, componentStyles, templateUpdates, prebundleLoaderExtensions, define, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) { - // dynamically import Vite for ESM compatibility - const { normalizePath } = await (0, load_esm_1.loadEsmModule)('vite'); - // Path will not exist on disk and only used to provide separate path for Vite requests - const virtualProjectRoot = normalizePath((0, node_path_1.join)(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project)); - /** - * Required when using `externalDependencies` to prevent Vite load errors. - * - * @note Can be removed if Vite introduces native support for externals. - * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments - * (e.g., 'foo/bar'), as they are not correctly re-based from the base href. - */ - const preTransformRequests = externalMetadata.explicitBrowser.length === 0 && ssrMode === plugins_1.ServerSsrMode.NoSsr; - const cacheDir = (0, node_path_1.join)(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite'); - const configuration = { - configFile: false, - envFile: false, - cacheDir, - root: virtualProjectRoot, - publicDir: false, - esbuild: false, - mode: 'development', - // We use custom as we do not rely on Vite's htmlFallbackMiddleware and indexHtmlMiddleware. - appType: 'custom', - css: { - devSourcemap: true, - }, - // Ensure custom 'file' loader build option entries are handled by Vite in application code that - // reference third-party libraries. Relative usage is handled directly by the build and not Vite. - // Only 'file' loader entries are currently supported directly by Vite. - assetsInclude: prebundleLoaderExtensions && - Object.entries(prebundleLoaderExtensions) - .filter(([, value]) => value === 'file') - // Create a file extension glob for each key - .map(([key]) => '*' + key), - // Vite will normalize the `base` option by adding a leading slash. - base: serverOptions.servePath, - resolve: { - mainFields: ['es2020', 'browser', 'module', 'main'], - preserveSymlinks, - }, - dev: { - preTransformRequests, - }, - server: await createServerConfig(serverOptions, assets, ssrMode, preTransformRequests, cacheDir), - ssr: createSsrConfig(externalMetadata, serverOptions, prebundleTransformer, zoneless, target, prebundleLoaderExtensions, thirdPartySourcemaps, define), - plugins: [ - (0, plugins_1.createAngularLocaleDataPlugin)(), - (0, plugins_1.createAngularSetupMiddlewaresPlugin)({ - outputFiles, - assets, - indexHtmlTransformer, - extensionMiddleware, - componentStyles, - templateUpdates, - ssrMode, - resetComponentUpdates: () => templateUpdates.clear(), - projectRoot: serverOptions.projectRoot, - }), - (0, plugins_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser), - await (0, plugins_1.createAngularSsrTransformPlugin)(serverOptions.workspaceRoot), - await (0, plugins_1.createAngularMemoryPlugin)({ - virtualProjectRoot, - outputFiles, - templateUpdates, - external: externalMetadata.explicitBrowser, - disableViteTransport: !serverOptions.liveReload, - }), - ], - // Browser only optimizeDeps. (This does not run for SSR dependencies). - optimizeDeps: (0, utils_1.getDepOptimizationConfig)({ - // Only enable with caching since it causes prebundle dependencies to be cached - disabled: serverOptions.prebundle === false, - // Exclude any explicitly defined dependencies (currently build defined externals) - exclude: externalMetadata.explicitBrowser, - // Include all implict dependencies from the external packages internal option - include: externalMetadata.implicitBrowser, - ssr: false, - prebundleTransformer, - target, - zoneless, - loader: prebundleLoaderExtensions, - thirdPartySourcemaps, - define, - }), - }; - if (serverOptions.ssl) { - if (!serverOptions.sslCert || !serverOptions.sslKey) { - const { default: basicSslPlugin } = await Promise.resolve().then(() => __importStar(require('@vitejs/plugin-basic-ssl'))); - configuration.plugins ??= []; - configuration.plugins.push(basicSslPlugin()); - } - } - return configuration; -} diff --git a/src/builders/dev-server/vite/utils.d.ts b/src/builders/dev-server/vite/utils.d.ts deleted file mode 100644 index 2aba8af9..00000000 --- a/src/builders/dev-server/vite/utils.d.ts +++ /dev/null @@ -1,36 +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.dev/license - */ -import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; -import type { ResultFile } from '../../application/results'; -import { BuildOutputFileType, type ExternalResultMetadata } from '../internal'; -export interface OutputFileRecord { - contents: Uint8Array; - size: number; - hash: string; - updated: boolean; - servable: boolean; - type: BuildOutputFileType; -} -export interface OutputAssetRecord { - source: string; - updated: boolean; -} -export interface DevServerExternalResultMetadata extends Omit { - explicitBrowser: string[]; - explicitServer: string[]; -} -export declare function updateResultRecord(outputPath: string, file: ResultFile, normalizePath: (id: string) => string, htmlIndexPath: string, generatedFiles: Map, assetFiles: Map, componentStyles: Map, initial?: boolean): void; -/** - * Checks if the given value is an absolute URL. - * - * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files. - * - * @param value - The URL or path to check. - * @returns `true` if the value is not an absolute URL; otherwise, `false`. - */ -export declare function isAbsoluteUrl(value: string): boolean; diff --git a/src/builders/dev-server/vite/utils.js b/src/builders/dev-server/vite/utils.js deleted file mode 100644 index 75c57664..00000000 --- a/src/builders/dev-server/vite/utils.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.updateResultRecord = updateResultRecord; -exports.isAbsoluteUrl = isAbsoluteUrl; -const internal_1 = require("../internal"); -function updateResultRecord(outputPath, file, normalizePath, htmlIndexPath, generatedFiles, assetFiles, componentStyles, initial = false) { - if (file.origin === 'disk') { - assetFiles.set('/' + normalizePath(outputPath), { - source: normalizePath(file.inputPath), - updated: !initial, - }); - return; - } - let filePath; - if (outputPath === htmlIndexPath) { - // Convert custom index output path to standard index path for dev-server usage. - // This mimics the Webpack dev-server behavior. - filePath = '/index.html'; - } - else { - filePath = '/' + normalizePath(outputPath); - } - const servable = file.type === internal_1.BuildOutputFileType.Browser || file.type === internal_1.BuildOutputFileType.Media; - // Skip analysis of sourcemaps - if (filePath.endsWith('.map')) { - generatedFiles.set(filePath, { - contents: file.contents, - servable, - size: file.contents.byteLength, - hash: file.hash, - type: file.type, - updated: false, - }); - return; - } - // New or updated file - generatedFiles.set(filePath, { - contents: file.contents, - size: file.contents.byteLength, - hash: file.hash, - // Consider the files updated except on the initial build result - updated: !initial, - type: file.type, - servable, - }); - // Record any external component styles - if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) { - const componentStyle = componentStyles.get(filePath); - if (componentStyle) { - componentStyle.rawContent = file.contents; - } - else { - componentStyles.set(filePath, { - rawContent: file.contents, - }); - } - } -} -/** - * Checks if the given value is an absolute URL. - * - * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files. - * - * @param value - The URL or path to check. - * @returns `true` if the value is not an absolute URL; otherwise, `false`. - */ -function isAbsoluteUrl(value) { - return /^(?:https?:)?\/\//.test(value); -} diff --git a/src/builders/karma/application_builder.js b/src/builders/karma/application_builder.js index f6258f2b..e123447e 100644 --- a/src/builders/karma/application_builder.js +++ b/src/builders/karma/application_builder.js @@ -46,6 +46,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.execute = execute; exports.writeTestFiles = writeTestFiles; const node_crypto_1 = require("node:crypto"); +const node_fs_1 = require("node:fs"); const fs = __importStar(require("node:fs/promises")); const node_module_1 = require("node:module"); const node_path_1 = __importDefault(require("node:path")); @@ -183,6 +184,8 @@ function injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controlle emitter; latestBuildFiles; static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN]; + // Needed for the karma reporter interface, see https://github.com/angular/angular-cli/issues/31629 + adapters = []; constructor(emitter, latestBuildFiles) { this.emitter = emitter; this.latestBuildFiles = latestBuildFiles; @@ -305,6 +308,11 @@ async function collectEntrypoints(options, context, projectSourceRoot) { async function initializeApplication(options, context, karmaOptions, transforms) { const outputPath = node_path_1.default.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)()); const projectSourceRoot = await getProjectSourceRoot(context); + // Setup exit cleanup for temporary directory + const handleProcessExit = () => (0, node_fs_1.rmSync)(outputPath, { recursive: true, force: true }); + process.once('exit', handleProcessExit); + process.once('SIGINT', handleProcessExit); + process.once('uncaughtException', handleProcessExit); const [karma, entryPoints] = await Promise.all([ Promise.resolve().then(() => __importStar(require('karma'))), collectEntrypoints(options, context, projectSourceRoot), diff --git a/src/builders/karma/find-tests.d.ts b/src/builders/karma/find-tests.d.ts index 44513a10..776febdb 100644 --- a/src/builders/karma/find-tests.d.ts +++ b/src/builders/karma/find-tests.d.ts @@ -9,8 +9,7 @@ export declare function findTests(include: string[], exclude: string[], workspac interface TestEntrypointsOptions { projectSourceRoot: string; workspaceRoot: string; - removeTestExtension?: boolean; } /** Generate unique bundle names for a set of test files. */ -export declare function getTestEntrypoints(testFiles: string[], { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions): Map; +export declare function getTestEntrypoints(testFiles: string[], { projectSourceRoot, workspaceRoot }: TestEntrypointsOptions): Map; export {}; diff --git a/src/builders/karma/find-tests.js b/src/builders/karma/find-tests.js index d267564a..27e3bbea 100644 --- a/src/builders/karma/find-tests.js +++ b/src/builders/karma/find-tests.js @@ -21,7 +21,7 @@ async function findTests(include, exclude, workspaceRoot, projectSourceRoot) { return [...new Set(files.flat())]; } /** Generate unique bundle names for a set of test files. */ -function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, removeTestExtension }) { +function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot }) { const seen = new Set(); return new Map(Array.from(testFiles, (testFile) => { const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot]) @@ -29,11 +29,7 @@ function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, remov .replace(/^[./\\]+/, '') // Replace any path separators with dashes. .replace(/[/\\]/g, '-'); - let fileName = (0, node_path_1.basename)(relativePath, (0, node_path_1.extname)(relativePath)); - if (removeTestExtension) { - fileName = fileName.replace(/\.(spec|test)$/, ''); - } - const baseName = `spec-${fileName}`; + const baseName = `spec-${(0, node_path_1.basename)(relativePath, (0, node_path_1.extname)(relativePath))}`; let uniqueName = baseName; let suffix = 2; while (seen.has(uniqueName)) { diff --git a/src/builders/unit-test/builder.d.ts b/src/builders/unit-test/builder.d.ts index 58395fea..fda30051 100644 --- a/src/builders/unit-test/builder.d.ts +++ b/src/builders/unit-test/builder.d.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import { type BuilderContext, type BuilderOutput } from '@angular-devkit/architect'; +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { ApplicationBuilderExtensions } from '../application/options'; import type { Schema as UnitTestBuilderOptions } from './schema'; export type { UnitTestBuilderOptions }; diff --git a/src/builders/unit-test/builder.js b/src/builders/unit-test/builder.js index 1f3d87d6..225bb467 100644 --- a/src/builders/unit-test/builder.js +++ b/src/builders/unit-test/builder.js @@ -6,218 +6,330 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { - if (value !== null && value !== void 0) { - if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); - var dispose, inner; - if (async) { - if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); - dispose = value[Symbol.asyncDispose]; - } - if (dispose === void 0) { - if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); - dispose = value[Symbol.dispose]; - if (async) inner = dispose; - } - if (typeof dispose !== "function") throw new TypeError("Object not disposable."); - if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; - env.stack.push({ value: value, dispose: dispose, async: async }); - } - else if (async) { - env.stack.push({ async: true }); - } - return value; -}; -var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { - return function (env) { - function fail(e) { - env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; - env.hasError = true; - } - var r, s = 0; - function next() { - while (r = env.stack.pop()) { - try { - if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); - if (r.dispose) { - var result = r.dispose.call(r.value); - if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); - } - else s |= 1; - } - catch (e) { - fail(e); - } - } - if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); - if (env.hasError) throw env.error; - } - return next(); - }; -})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; -}); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.execute = execute; -const architect_1 = require("@angular-devkit/architect"); const node_assert_1 = __importDefault(require("node:assert")); +const node_crypto_1 = require("node:crypto"); +const node_module_1 = require("node:module"); +const node_path_1 = __importDefault(require("node:path")); const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin"); const error_1 = require("../../utils/error"); +const load_esm_1 = require("../../utils/load-esm"); +const path_1 = require("../../utils/path"); const application_1 = require("../application"); const results_1 = require("../application/results"); +const schema_1 = require("../application/schema"); +const application_builder_1 = require("../karma/application_builder"); +const find_tests_1 = require("../karma/find-tests"); +const karma_bridge_1 = require("./karma-bridge"); const options_1 = require("./options"); -async function loadTestRunner(runnerName) { - // Harden against directory traversal - if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) { - throw new Error(`Invalid runner name "${runnerName}". Runner names can only contain alphanumeric characters and hyphens.`); +function adjustOutputHashing(hashing) { + switch (hashing) { + case schema_1.OutputHashing.All: + case schema_1.OutputHashing.Media: + // Ensure media is continued to be hashed to avoid overwriting of output media files + return schema_1.OutputHashing.Media; + default: + return schema_1.OutputHashing.None; } - let runnerModule; - try { - runnerModule = await Promise.resolve(`${`./runners/${runnerName}/index`}`).then(s => __importStar(require(s))); +} +/** + * @experimental Direct usage of this function is considered experimental. + */ +// eslint-disable-next-line max-lines-per-function +async function* execute(options, context, extensions = {}) { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + context.logger.error(`The "${context.builder.builderName}" builder requires a target to be specified.`); + return; } - catch (e) { - (0, error_1.assertIsError)(e); - if (e.code === 'ERR_MODULE_NOT_FOUND') { - throw new Error(`Unknown test runner "${runnerName}".`); - } - throw new Error(`Failed to load the '${runnerName}' test runner. The package may be corrupted or improperly installed.\n` + - `Error: ${e.message}`); + context.logger.warn(`NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`); + const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options); + const { projectSourceRoot, workspaceRoot, runnerName } = normalizedOptions; + // Translate options and use karma builder directly if specified + if (runnerName === 'karma') { + const karmaBridge = await (0, karma_bridge_1.useKarmaBuilder)(context, normalizedOptions); + yield* karmaBridge; + return; } - const runner = runnerModule.default; - if (!runner || - typeof runner.getBuildOptions !== 'function' || - typeof runner.createExecutor !== 'function') { - throw new Error(`The loaded test runner '${runnerName}' does not appear to be a valid TestRunner implementation.`); + if (runnerName !== 'vitest') { + context.logger.error('Unknown test runner: ' + runnerName); + return; } - return runner; -} -function prepareBuildExtensions(virtualFiles, projectSourceRoot, extensions) { - if (!virtualFiles) { - return extensions; + // Find test files + const testFiles = await (0, find_tests_1.findTests)(normalizedOptions.include, normalizedOptions.exclude, workspaceRoot, projectSourceRoot); + if (testFiles.length === 0) { + context.logger.error('No tests found.'); + return { success: false }; + } + const entryPoints = (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot }); + entryPoints.set('init-testbed', 'angular:test-bed-init'); + let vitestNodeModule; + try { + vitestNodeModule = await (0, load_esm_1.loadEsmModule)('vitest/node'); + } + catch (error) { + (0, error_1.assertIsError)(error); + if (error.code !== 'ERR_MODULE_NOT_FOUND') { + throw error; + } + context.logger.error('The `vitest` package was not found. Please install the package and rerun the test command.'); + return; } + const { startVitest } = vitestNodeModule; + // Setup test file build options based on application build target options + const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget))); + buildTargetOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildTargetOptions.polyfills); + const outputPath = (0, path_1.toPosixPath)(node_path_1.default.join(context.workspaceRoot, generateOutputPath())); + const buildOptions = { + ...buildTargetOptions, + watch: normalizedOptions.watch, + incrementalResults: normalizedOptions.watch, + outputPath, + index: false, + browser: undefined, + server: undefined, + outputMode: undefined, + localize: false, + budgets: [], + serviceWorker: false, + appShell: false, + ssr: false, + prerender: false, + sourceMap: { scripts: true, vendor: false, styles: false }, + outputHashing: adjustOutputHashing(buildTargetOptions.outputHashing), + optimization: false, + tsConfig: normalizedOptions.tsConfig, + entryPoints, + externalDependencies: [ + 'vitest', + '@vitest/browser/context', + ...(buildTargetOptions.externalDependencies ?? []), + ], + }; extensions ??= {}; extensions.codePlugins ??= []; - for (const [namespace, contents] of Object.entries(virtualFiles)) { - extensions.codePlugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({ - namespace, - loadContent: () => { - return { - contents, - loader: 'js', - resolveDir: projectSourceRoot, - }; - }, - })); - } - return extensions; -} -async function* runBuildAndTest(executor, applicationBuildOptions, context, extensions) { - for await (const buildResult of (0, application_1.buildApplicationInternal)(applicationBuildOptions, context, extensions)) { - if (buildResult.kind === results_1.ResultKind.Failure) { - yield { success: false }; - continue; - } - else if (buildResult.kind !== results_1.ResultKind.Full && - buildResult.kind !== results_1.ResultKind.Incremental) { - node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.'); - } - (0, node_assert_1.default)(buildResult.files, 'Builder did not provide result files.'); - // Pass the build artifacts to the executor - yield* executor.execute(buildResult); + const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({ + namespace: 'angular:test-bed-init', + loadContent: async () => { + const contents = [ + // Initialize the Angular testing environment + `import { NgModule } from '@angular/core';`, + `import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';`, + `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`, + '', + normalizedOptions.providersFile + ? `import providers from './${(0, path_1.toPosixPath)(node_path_1.default + .relative(projectSourceRoot, normalizedOptions.providersFile) + .replace(/.[mc]?ts$/, ''))}'` + : 'const providers = [];', + '', + // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29 + `beforeEach(getCleanupHook(false));`, + `afterEach(getCleanupHook(true));`, + '', + `@NgModule({`, + ` providers,`, + `})`, + `export class TestModule {}`, + '', + `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`, + ` errorOnUnknownElements: true,`, + ` errorOnUnknownProperties: true,`, + '});', + ]; + return { + contents: contents.join('\n'), + loader: 'js', + resolveDir: projectSourceRoot, + }; + }, + }); + extensions.codePlugins.unshift(virtualTestBedInit); + let instance; + // Setup vitest browser options if configured + const { browser, errors } = setupBrowserConfiguration(normalizedOptions.browsers, normalizedOptions.debug, projectSourceRoot); + if (errors?.length) { + errors.forEach((error) => context.logger.error(error)); + return { success: false }; } -} -/** - * @experimental Direct usage of this function is considered experimental. - */ -async function* execute(options, context, extensions) { - const env_1 = { stack: [], error: void 0, hasError: false }; - try { - // Determine project name from builder context target - const projectName = context.target?.project; - if (!projectName) { - context.logger.error(`The builder requires a target to be specified.`); - return; + // Add setup file entries for TestBed initialization and project polyfills + const setupFiles = ['init-testbed.js', ...normalizedOptions.setupFiles]; + if (buildTargetOptions?.polyfills?.length) { + // Placed first as polyfills may be required by the Testbed initialization + // or other project provided setup files (e.g., zone.js, ECMAScript polyfills). + setupFiles.unshift('polyfills.js'); + } + const debugOptions = normalizedOptions.debug + ? { + inspectBrk: true, + isolate: false, + fileParallelism: false, } - context.logger.warn(`NOTE: The "unit-test" builder is currently EXPERIMENTAL and not ready for production use.`); - const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options); - const runner = await loadTestRunner(normalizedOptions.runnerName); - const executor = __addDisposableResource(env_1, await runner.createExecutor(context, normalizedOptions), true); - if (runner.isStandalone) { - yield* executor.execute({ - kind: results_1.ResultKind.Full, - files: {}, + : {}; + try { + for await (const result of (0, application_1.buildApplicationInternal)(buildOptions, context, extensions)) { + if (result.kind === results_1.ResultKind.Failure) { + continue; + } + else if (result.kind !== results_1.ResultKind.Full && result.kind !== results_1.ResultKind.Incremental) { + node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.'); + } + (0, node_assert_1.default)(result.files, 'Builder did not provide result files.'); + await (0, application_builder_1.writeTestFiles)(result.files, outputPath); + instance ??= await startVitest('test', undefined /* cliFilters */, { + // Disable configuration file resolution/loading + config: false, + root: workspaceRoot, + project: ['base', projectName], + name: 'base', + include: [], + reporters: normalizedOptions.reporters ?? ['default'], + watch: normalizedOptions.watch, + coverage: generateCoverageOption(normalizedOptions.codeCoverage, workspaceRoot, outputPath), + ...debugOptions, + }, { + plugins: [ + { + name: 'angular:project-init', + async configureVitest(context) { + // Create a subproject that can be configured with plugins for browser mode. + // Plugins defined directly in the vite overrides will not be present in the + // browser specific Vite instance. + const [project] = await context.injectTestProjects({ + test: { + name: projectName, + root: outputPath, + globals: true, + setupFiles, + // Use `jsdom` if no browsers are explicitly configured. + // `node` is effectively no "environment" and the default. + environment: browser ? 'node' : 'jsdom', + browser, + }, + plugins: [ + { + name: 'angular:html-index', + transformIndexHtml() { + // Add all global stylesheets + return (Object.entries(result.files) + // TODO: Expand this to all configured global stylesheets + .filter(([file]) => file === 'styles.css') + .map(([styleUrl]) => ({ + tag: 'link', + attrs: { + 'href': styleUrl, + 'rel': 'stylesheet', + }, + injectTo: 'head', + }))); + }, + }, + ], + }); + // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests. + // Vite does this as a convenience but is problematic for the bundling strategy employed by the + // builder's test setup. To workaround this, the excludes are adjusted here to only automaticallyAdd commentMore actions + // exclude the TypeScript source test files. + project.config.coverage.exclude = [ + ...(normalizedOptions.codeCoverage?.exclude ?? []), + '**/*.{test,spec}.?(c|m)ts', + ]; + }, + }, + ], }); - return; + // Check if all the tests pass to calculate the result + const testModules = instance.state.getTestModules(); + yield { success: testModules.every((testModule) => testModule.ok()) }; } - // Get base build options from the buildTarget - let buildTargetOptions; - try { - buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget))); + } + finally { + if (normalizedOptions.watch) { + // Vitest will automatically close if not using watch mode + await instance?.close(); } - catch (e) { - (0, error_1.assertIsError)(e); - context.logger.error(`Could not load build target options for "${(0, architect_1.targetStringFromTarget)(normalizedOptions.buildTarget)}".\n` + - `Please check your 'angular.json' configuration.\n` + - `Error: ${e.message}`); - return; + } +} +function findBrowserProvider(projectResolver) { + // One of these must be installed in the project to use browser testing + const vitestBuiltinProviders = ['playwright', 'webdriverio']; + for (const providerName of vitestBuiltinProviders) { + try { + projectResolver(providerName); + return providerName; } - // Get runner-specific build options from the hook - const { buildOptions: runnerBuildOptions, virtualFiles } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions); - const finalExtensions = prepareBuildExtensions(virtualFiles, normalizedOptions.projectSourceRoot, extensions); - // Prepare and run the application build - const applicationBuildOptions = { - ...buildTargetOptions, - ...runnerBuildOptions, - watch: normalizedOptions.watch, - tsConfig: normalizedOptions.tsConfig, - progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, - }; - yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions); + catch { } } - catch (e_1) { - env_1.error = e_1; - env_1.hasError = true; +} +function normalizeBrowserName(browserName) { + // Normalize browser names to match Vitest's expectations for headless but also supports karma's names + // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' + // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'. + const normalized = browserName.toLowerCase(); + return normalized.replace(/headless$/, ''); +} +function setupBrowserConfiguration(browsers, debug, projectSourceRoot) { + if (browsers === undefined) { + return {}; } - finally { - const result_1 = __disposeResources(env_1); - if (result_1) - await result_1; + const projectResolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve; + let errors; + try { + projectResolver('@vitest/browser'); + } + catch { + errors ??= []; + errors.push('The "browsers" option requires the "@vitest/browser" package to be installed within the project.' + + ' Please install this package and rerun the test command.'); + } + const provider = findBrowserProvider(projectResolver); + if (!provider) { + errors ??= []; + errors.push('The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' + + ' Please install one of these packages and rerun the test command.'); + } + // Vitest current requires the playwright browser provider to use the inspect-brk option used by "debug" + if (debug && provider !== 'playwright') { + errors ??= []; + errors.push('Debugging browser mode tests currently requires the use of "playwright".' + + ' Please install this package and rerun the test command.'); } + if (errors) { + return { errors }; + } + const browser = { + enabled: true, + provider, + headless: browsers.some((name) => name.toLowerCase().includes('headless')), + instances: browsers.map((browserName) => ({ + browser: normalizeBrowserName(browserName), + })), + }; + return { browser }; +} +function generateOutputPath() { + const datePrefix = new Date().toISOString().replaceAll(/[-:.]/g, ''); + const uuidSuffix = (0, node_crypto_1.randomUUID)().slice(0, 8); + return node_path_1.default.join('dist', 'test-out', `${datePrefix}-${uuidSuffix}`); +} +function generateCoverageOption(codeCoverage, workspaceRoot, outputPath) { + if (!codeCoverage) { + return { + enabled: false, + }; + } + return { + enabled: true, + excludeAfterRemap: true, + include: [`${(0, path_1.toPosixPath)(node_path_1.default.relative(workspaceRoot, outputPath))}/**`], + // Special handling for `reporter` due to an undefined value causing upstream failures + ...(codeCoverage.reporters + ? { reporter: codeCoverage.reporters } + : {}), + }; } diff --git a/src/builders/unit-test/karma-bridge.d.ts b/src/builders/unit-test/karma-bridge.d.ts new file mode 100644 index 00000000..3f5db3e5 --- /dev/null +++ b/src/builders/unit-test/karma-bridge.d.ts @@ -0,0 +1,10 @@ +/** + * @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.dev/license + */ +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { type NormalizedUnitTestBuilderOptions } from './options'; +export declare function useKarmaBuilder(context: BuilderContext, unitTestOptions: NormalizedUnitTestBuilderOptions): Promise>; diff --git a/src/builders/unit-test/karma-bridge.js b/src/builders/unit-test/karma-bridge.js new file mode 100644 index 00000000..93363b5b --- /dev/null +++ b/src/builders/unit-test/karma-bridge.js @@ -0,0 +1,82 @@ +"use strict"; +/** + * @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.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useKarmaBuilder = useKarmaBuilder; +const options_1 = require("./options"); +async function useKarmaBuilder(context, unitTestOptions) { + if (unitTestOptions.debug) { + context.logger.warn('The "karma" test runner does not support the "debug" option. The option will be ignored.'); + } + if (unitTestOptions.setupFiles.length) { + context.logger.warn('The "karma" test runner does not support the "setupFiles" option. The option will be ignored.'); + } + const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget))); + buildTargetOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildTargetOptions.polyfills); + const options = { + tsConfig: unitTestOptions.tsConfig, + polyfills: buildTargetOptions.polyfills, + assets: buildTargetOptions.assets, + scripts: buildTargetOptions.scripts, + styles: buildTargetOptions.styles, + inlineStyleLanguage: buildTargetOptions.inlineStyleLanguage, + stylePreprocessorOptions: buildTargetOptions.stylePreprocessorOptions, + externalDependencies: buildTargetOptions.externalDependencies, + loader: buildTargetOptions.loader, + define: buildTargetOptions.define, + include: unitTestOptions.include, + exclude: unitTestOptions.exclude, + sourceMap: buildTargetOptions.sourceMap, + progress: buildTargetOptions.progress, + watch: unitTestOptions.watch, + poll: buildTargetOptions.poll, + preserveSymlinks: buildTargetOptions.preserveSymlinks, + browsers: unitTestOptions.browsers?.join(','), + codeCoverage: !!unitTestOptions.codeCoverage, + codeCoverageExclude: unitTestOptions.codeCoverage?.exclude, + fileReplacements: buildTargetOptions.fileReplacements, + reporters: unitTestOptions.reporters, + webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig, + aot: buildTargetOptions.aot, + }; + const { execute } = await Promise.resolve().then(() => __importStar(require('../karma'))); + return execute(options, context); +} diff --git a/src/builders/unit-test/options.d.ts b/src/builders/unit-test/options.d.ts index 2eda0cf5..e05c400e 100644 --- a/src/builders/unit-test/options.d.ts +++ b/src/builders/unit-test/options.d.ts @@ -15,14 +15,13 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s cacheOptions: import("../../utils/normalize-cache").NormalizedCachedOptions; buildTarget: import("@angular-devkit/architect").Target; include: string[]; - exclude: string[] | undefined; + exclude: string[]; runnerName: import("./schema").Runner; codeCoverage: { exclude: string[] | undefined; reporters: [string, Record][] | undefined; } | undefined; tsConfig: string; - buildProgress: boolean | undefined; reporters: string[] | undefined; browsers: string[] | undefined; watch: boolean; diff --git a/src/builders/unit-test/options.js b/src/builders/unit-test/options.js index 67d317d3..6a899f8c 100644 --- a/src/builders/unit-test/options.js +++ b/src/builders/unit-test/options.js @@ -28,7 +28,7 @@ async function normalizeOptions(context, projectName, options) { // Target specifier defaults to the current project's build target using a development configuration const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build'); - const { tsConfig, runner, reporters, browsers, progress } = options; + const { tsConfig, runner, reporters, browsers } = options; return { // Project/workspace information workspaceRoot, @@ -38,7 +38,7 @@ async function normalizeOptions(context, projectName, options) { // Target/configuration specified options buildTarget, include: options.include ?? ['**/*.spec.ts'], - exclude: options.exclude, + exclude: options.exclude ?? [], runnerName: runner, codeCoverage: options.codeCoverage ? { @@ -49,7 +49,6 @@ async function normalizeOptions(context, projectName, options) { } : undefined, tsConfig, - buildProgress: progress, reporters, browsers, watch: options.watch ?? (0, tty_1.isTTY)(), diff --git a/src/builders/unit-test/runners/api.d.ts b/src/builders/unit-test/runners/api.d.ts deleted file mode 100644 index 2660f274..00000000 --- a/src/builders/unit-test/runners/api.d.ts +++ /dev/null @@ -1,47 +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.dev/license - */ -import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import type { ApplicationBuilderInternalOptions } from '../../application/options'; -import type { FullResult, IncrementalResult } from '../../application/results'; -import type { NormalizedUnitTestBuilderOptions } from '../options'; -export interface RunnerOptions { - buildOptions: Partial; - virtualFiles?: Record; -} -/** - * Represents a stateful test execution session. - * An instance of this is created for each `ng test` command. - */ -export interface TestExecutor { - /** - * Executes tests using the artifacts from a specific build. - * This method can be called multiple times in watch mode. - * - * @param buildResult The output from the application builder. - * @returns An async iterable builder output stream. - */ - execute(buildResult: FullResult | IncrementalResult): AsyncIterable; - [Symbol.asyncDispose](): Promise; -} -/** - * Represents the metadata and hooks for a specific test runner. - */ -export interface TestRunner { - readonly name: string; - readonly isStandalone?: boolean; - getBuildOptions(options: NormalizedUnitTestBuilderOptions, baseBuildOptions: Partial): RunnerOptions | Promise; - /** - * Creates a stateful executor for a test session. - * This is called once at the start of the `ng test` command. - * - * @param context The Architect builder context. - * @param options The normalized unit test options. - * @returns A TestExecutor instance that will handle the test runs. - */ - createExecutor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions): Promise; -} diff --git a/src/builders/unit-test/runners/api.js b/src/builders/unit-test/runners/api.js deleted file mode 100644 index 7c2bf23c..00000000 --- a/src/builders/unit-test/runners/api.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/builders/unit-test/runners/karma/executor.d.ts b/src/builders/unit-test/runners/karma/executor.d.ts deleted file mode 100644 index 9897c98c..00000000 --- a/src/builders/unit-test/runners/karma/executor.d.ts +++ /dev/null @@ -1,17 +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.dev/license - */ -import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { NormalizedUnitTestBuilderOptions } from '../../options'; -import type { TestExecutor } from '../api'; -export declare class KarmaExecutor implements TestExecutor { - private context; - private options; - constructor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions); - execute(): AsyncIterable; - [Symbol.asyncDispose](): Promise; -} diff --git a/src/builders/unit-test/runners/karma/executor.js b/src/builders/unit-test/runners/karma/executor.js deleted file mode 100644 index 3f24bc45..00000000 --- a/src/builders/unit-test/runners/karma/executor.js +++ /dev/null @@ -1,93 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.KarmaExecutor = void 0; -class KarmaExecutor { - context; - options; - constructor(context, options) { - this.context = context; - this.options = options; - } - async *execute() { - const { context, options: unitTestOptions } = this; - if (unitTestOptions.debug) { - context.logger.warn('The "karma" test runner does not support the "debug" option. The option will be ignored.'); - } - if (unitTestOptions.setupFiles.length) { - context.logger.warn('The "karma" test runner does not support the "setupFiles" option. The option will be ignored.'); - } - const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget))); - const karmaOptions = { - tsConfig: unitTestOptions.tsConfig, - polyfills: buildTargetOptions.polyfills, - assets: buildTargetOptions.assets, - scripts: buildTargetOptions.scripts, - styles: buildTargetOptions.styles, - inlineStyleLanguage: buildTargetOptions.inlineStyleLanguage, - stylePreprocessorOptions: buildTargetOptions.stylePreprocessorOptions, - externalDependencies: buildTargetOptions.externalDependencies, - loader: buildTargetOptions.loader, - define: buildTargetOptions.define, - include: unitTestOptions.include, - exclude: unitTestOptions.exclude, - sourceMap: buildTargetOptions.sourceMap, - progress: unitTestOptions.buildProgress ?? buildTargetOptions.progress, - watch: unitTestOptions.watch, - poll: buildTargetOptions.poll, - preserveSymlinks: buildTargetOptions.preserveSymlinks, - browsers: unitTestOptions.browsers?.join(','), - codeCoverage: !!unitTestOptions.codeCoverage, - codeCoverageExclude: unitTestOptions.codeCoverage?.exclude, - fileReplacements: buildTargetOptions.fileReplacements, - reporters: unitTestOptions.reporters, - webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig, - aot: buildTargetOptions.aot, - }; - const { execute } = await Promise.resolve().then(() => __importStar(require('../../../karma'))); - yield* execute(karmaOptions, context); - } - async [Symbol.asyncDispose]() { - // The Karma builder handles its own teardown - } -} -exports.KarmaExecutor = KarmaExecutor; diff --git a/src/builders/unit-test/runners/karma/index.d.ts b/src/builders/unit-test/runners/karma/index.d.ts deleted file mode 100644 index 47ee6cda..00000000 --- a/src/builders/unit-test/runners/karma/index.d.ts +++ /dev/null @@ -1,13 +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.dev/license - */ -import type { TestRunner } from '../api'; -/** - * A declarative definition of the Karma test runner. - */ -declare const KarmaTestRunner: TestRunner; -export default KarmaTestRunner; diff --git a/src/builders/unit-test/runners/karma/index.js b/src/builders/unit-test/runners/karma/index.js deleted file mode 100644 index fa64709c..00000000 --- a/src/builders/unit-test/runners/karma/index.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const executor_1 = require("./executor"); -/** - * A declarative definition of the Karma test runner. - */ -const KarmaTestRunner = { - name: 'karma', - isStandalone: true, - getBuildOptions() { - return { - buildOptions: {}, - }; - }, - async createExecutor(context, options) { - return new executor_1.KarmaExecutor(context, options); - }, -}; -exports.default = KarmaTestRunner; diff --git a/src/builders/unit-test/runners/vitest/browser-provider.d.ts b/src/builders/unit-test/runners/vitest/browser-provider.d.ts deleted file mode 100644 index 418e67e5..00000000 --- a/src/builders/unit-test/runners/vitest/browser-provider.d.ts +++ /dev/null @@ -1,11 +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.dev/license - */ -export declare function setupBrowserConfiguration(browsers: string[] | undefined, debug: boolean, projectSourceRoot: string): { - browser?: import('vitest/node').BrowserConfigOptions; - errors?: string[]; -}; diff --git a/src/builders/unit-test/runners/vitest/browser-provider.js b/src/builders/unit-test/runners/vitest/browser-provider.js deleted file mode 100644 index dec4d68f..00000000 --- a/src/builders/unit-test/runners/vitest/browser-provider.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.setupBrowserConfiguration = setupBrowserConfiguration; -const node_module_1 = require("node:module"); -function findBrowserProvider(projectResolver) { - // One of these must be installed in the project to use browser testing - const vitestBuiltinProviders = ['playwright', 'webdriverio']; - for (const providerName of vitestBuiltinProviders) { - try { - projectResolver(providerName); - return providerName; - } - catch { } - } - return undefined; -} -function normalizeBrowserName(browserName) { - // Normalize browser names to match Vitest's expectations for headless but also supports karma's names - // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' -> 'firefox' - // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'. - const normalized = browserName.toLowerCase(); - return normalized.replace(/headless$/, ''); -} -function setupBrowserConfiguration(browsers, debug, projectSourceRoot) { - if (browsers === undefined) { - return {}; - } - const projectResolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve; - let errors; - try { - projectResolver('@vitest/browser'); - } - catch { - errors ??= []; - errors.push('The "browsers" option requires the "@vitest/browser" package to be installed within the project.' + - ' Please install this package and rerun the test command.'); - } - const provider = findBrowserProvider(projectResolver); - if (!provider) { - errors ??= []; - errors.push('The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' + - ' Please install one of these packages and rerun the test command.'); - } - // Vitest current requires the playwright browser provider to use the inspect-brk option used by "debug" - if (debug && provider !== 'playwright') { - errors ??= []; - errors.push('Debugging browser mode tests currently requires the use of "playwright".' + - ' Please install this package and rerun the test command.'); - } - if (errors) { - return { errors }; - } - const browser = { - enabled: true, - provider, - headless: browsers.some((name) => name.toLowerCase().includes('headless')), - instances: browsers.map((browserName) => ({ - browser: normalizeBrowserName(browserName), - })), - }; - return { browser }; -} diff --git a/src/builders/unit-test/runners/vitest/build-options.d.ts b/src/builders/unit-test/runners/vitest/build-options.d.ts deleted file mode 100644 index 59f66c6c..00000000 --- a/src/builders/unit-test/runners/vitest/build-options.d.ts +++ /dev/null @@ -1,11 +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.dev/license - */ -import type { ApplicationBuilderInternalOptions } from '../../../application/options'; -import { NormalizedUnitTestBuilderOptions } from '../../options'; -import { RunnerOptions } from '../api'; -export declare function getVitestBuildOptions(options: NormalizedUnitTestBuilderOptions, baseBuildOptions: Partial): Promise; diff --git a/src/builders/unit-test/runners/vitest/build-options.js b/src/builders/unit-test/runners/vitest/build-options.js deleted file mode 100644 index d7440e2c..00000000 --- a/src/builders/unit-test/runners/vitest/build-options.js +++ /dev/null @@ -1,101 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVitestBuildOptions = getVitestBuildOptions; -const node_path_1 = __importDefault(require("node:path")); -const path_1 = require("../../../../utils/path"); -const schema_1 = require("../../../application/schema"); -const options_1 = require("../../options"); -const test_discovery_1 = require("../../test-discovery"); -function createTestBedInitVirtualFile(providersFile, projectSourceRoot) { - let providersImport = 'const providers = [];'; - if (providersFile) { - const relativePath = node_path_1.default.relative(projectSourceRoot, providersFile); - const { dir, name } = node_path_1.default.parse(relativePath); - const importPath = (0, path_1.toPosixPath)(node_path_1.default.join(dir, name)); - providersImport = `import providers from './${importPath}';`; - } - return ` - // Initialize the Angular testing environment - import { NgModule } from '@angular/core'; - import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing'; - import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; - ${providersImport} - // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/srcs/test_hooks.ts#L21-L29 - beforeEach(getCleanupHook(false)); - afterEach(getCleanupHook(true)); - @NgModule({ - providers, - }) - export class TestModule {} - getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), { - errorOnUnknownElements: true, - errorOnUnknownProperties: true, - }); - `; -} -function adjustOutputHashing(hashing) { - switch (hashing) { - case schema_1.OutputHashing.All: - case schema_1.OutputHashing.Media: - // Ensure media is continued to be hashed to avoid overwriting of output media files - return schema_1.OutputHashing.Media; - default: - return schema_1.OutputHashing.None; - } -} -async function getVitestBuildOptions(options, baseBuildOptions) { - const { workspaceRoot, projectSourceRoot, include, exclude = [], watch, tsConfig, providersFile, } = options; - // Find test files - const testFiles = await (0, test_discovery_1.findTests)(include, exclude, workspaceRoot, projectSourceRoot); - if (testFiles.length === 0) { - throw new Error('No tests found matching the following patterns:\n' + - `- Included: ${include.join(', ')}\n` + - (exclude.length ? `- Excluded: ${exclude.join(', ')}\n` : '') + - `\nPlease check the 'test' target configuration in your project's 'angular.json' file.`); - } - const entryPoints = (0, test_discovery_1.getTestEntrypoints)(testFiles, { - projectSourceRoot, - workspaceRoot, - removeTestExtension: true, - }); - entryPoints.set('init-testbed', 'angular:test-bed-init'); - const buildOptions = { - ...baseBuildOptions, - watch, - incrementalResults: watch, - index: false, - browser: undefined, - server: undefined, - outputMode: undefined, - localize: false, - budgets: [], - serviceWorker: false, - appShell: false, - ssr: false, - prerender: false, - sourceMap: { scripts: true, vendor: false, styles: false }, - outputHashing: adjustOutputHashing(baseBuildOptions.outputHashing), - optimization: false, - tsConfig, - entryPoints, - externalDependencies: ['vitest', '@vitest/browser/context'], - }; - buildOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildOptions.polyfills); - const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot); - return { - buildOptions, - virtualFiles: { - 'angular:test-bed-init': testBedInitContents, - }, - }; -} diff --git a/src/builders/unit-test/runners/vitest/executor.d.ts b/src/builders/unit-test/runners/vitest/executor.d.ts deleted file mode 100644 index c69a02a0..00000000 --- a/src/builders/unit-test/runners/vitest/executor.d.ts +++ /dev/null @@ -1,25 +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.dev/license - */ -import type { BuilderOutput } from '@angular-devkit/architect'; -import { type FullResult, type IncrementalResult } from '../../../application/results'; -import { NormalizedUnitTestBuilderOptions } from '../../options'; -import type { TestExecutor } from '../api'; -export declare class VitestExecutor implements TestExecutor { - private vitest; - private readonly projectName; - private readonly options; - private buildResultFiles; - private testFileToEntryPoint; - private entryPointToTestFile; - constructor(projectName: string, options: NormalizedUnitTestBuilderOptions); - execute(buildResult: FullResult | IncrementalResult): AsyncIterable; - [Symbol.asyncDispose](): Promise; - private prepareSetupFiles; - private createVitestPlugins; - private initializeVitest; -} diff --git a/src/builders/unit-test/runners/vitest/executor.js b/src/builders/unit-test/runners/vitest/executor.js deleted file mode 100644 index de150d0b..00000000 --- a/src/builders/unit-test/runners/vitest/executor.js +++ /dev/null @@ -1,292 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.VitestExecutor = void 0; -const node_assert_1 = __importDefault(require("node:assert")); -const promises_1 = require("node:fs/promises"); -const node_path_1 = __importDefault(require("node:path")); -const error_1 = require("../../../../utils/error"); -const load_esm_1 = require("../../../../utils/load-esm"); -const path_1 = require("../../../../utils/path"); -const results_1 = require("../../../application/results"); -const test_discovery_1 = require("../../test-discovery"); -const browser_provider_1 = require("./browser-provider"); -class VitestExecutor { - vitest; - projectName; - options; - buildResultFiles = new Map(); - // This is a reverse map of the entry points created in `build-options.ts`. - // It is used by the in-memory provider plugin to map the requested test file - // path back to its bundled output path. - // Example: `Map<'/path/to/src/app.spec.ts', 'spec-src-app-spec'>` - testFileToEntryPoint = new Map(); - entryPointToTestFile = new Map(); - constructor(projectName, options) { - this.projectName = projectName; - this.options = options; - } - async *execute(buildResult) { - if (buildResult.kind === results_1.ResultKind.Full) { - this.buildResultFiles.clear(); - for (const [path, file] of Object.entries(buildResult.files)) { - this.buildResultFiles.set(path, file); - } - } - else { - for (const file of buildResult.removed) { - this.buildResultFiles.delete(file.path); - } - for (const [path, file] of Object.entries(buildResult.files)) { - this.buildResultFiles.set(path, file); - } - } - // The `getTestEntrypoints` function is used here to create the same mapping - // that was used in `build-options.ts` to generate the build entry points. - // This is a deliberate duplication to avoid a larger refactoring of the - // builder's core interfaces to pass the entry points from the build setup - // phase to the execution phase. - if (this.testFileToEntryPoint.size === 0) { - const { include, exclude = [], workspaceRoot, projectSourceRoot } = this.options; - const testFiles = await (0, test_discovery_1.findTests)(include, exclude, workspaceRoot, projectSourceRoot); - const entryPoints = (0, test_discovery_1.getTestEntrypoints)(testFiles, { - projectSourceRoot, - workspaceRoot, - removeTestExtension: true, - }); - for (const [entryPoint, testFile] of entryPoints) { - this.testFileToEntryPoint.set(testFile, entryPoint); - this.entryPointToTestFile.set(entryPoint + '.js', testFile); - } - } - // Initialize Vitest if not already present. - this.vitest ??= await this.initializeVitest(); - const vitest = this.vitest; - let testResults; - if (buildResult.kind === results_1.ResultKind.Incremental) { - // To rerun tests, Vitest needs the original test file paths, not the output paths. - const modifiedSourceFiles = new Set(); - for (const modifiedFile of buildResult.modified) { - // The `modified` files in the build result are the output paths. - // We need to find the original source file path to pass to Vitest. - const source = this.entryPointToTestFile.get(modifiedFile); - if (source) { - modifiedSourceFiles.add(source); - } - vitest.invalidateFile((0, path_1.toPosixPath)(node_path_1.default.join(this.options.workspaceRoot, modifiedFile))); - } - const specsToRerun = []; - for (const file of modifiedSourceFiles) { - vitest.invalidateFile(file); - const specs = vitest.getModuleSpecifications(file); - if (specs) { - specsToRerun.push(...specs); - } - } - if (specsToRerun.length > 0) { - testResults = await vitest.rerunTestSpecifications(specsToRerun); - } - } - // Check if all the tests pass to calculate the result - const testModules = testResults?.testModules ?? this.vitest.state.getTestModules(); - yield { success: testModules.every((testModule) => testModule.ok()) }; - } - async [Symbol.asyncDispose]() { - await this.vitest?.close(); - } - prepareSetupFiles() { - const { setupFiles } = this.options; - // Add setup file entries for TestBed initialization and project polyfills - const testSetupFiles = ['init-testbed.js', ...setupFiles]; - // TODO: Provide additional result metadata to avoid needing to extract based on filename - if (this.buildResultFiles.has('polyfills.js')) { - testSetupFiles.unshift('polyfills.js'); - } - return testSetupFiles; - } - createVitestPlugins(testSetupFiles, browserOptions) { - const { workspaceRoot } = this.options; - return [ - { - name: 'angular:project-init', - // Type is incorrect. This allows a Promise. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - configureVitest: async (context) => { - // Create a subproject that can be configured with plugins for browser mode. - // Plugins defined directly in the vite overrides will not be present in the - // browser specific Vite instance. - await context.injectTestProjects({ - test: { - name: this.projectName, - root: workspaceRoot, - globals: true, - setupFiles: testSetupFiles, - // Use `jsdom` if no browsers are explicitly configured. - // `node` is effectively no "environment" and the default. - environment: browserOptions.browser ? 'node' : 'jsdom', - browser: browserOptions.browser, - include: this.options.include, - ...(this.options.exclude ? { exclude: this.options.exclude } : {}), - }, - plugins: [ - { - name: 'angular:test-in-memory-provider', - enforce: 'pre', - resolveId: (id, importer) => { - if (importer && (id[0] === '.' || id[0] === '/')) { - let fullPath; - if (this.testFileToEntryPoint.has(importer)) { - fullPath = (0, path_1.toPosixPath)(node_path_1.default.join(this.options.workspaceRoot, id)); - } - else { - fullPath = (0, path_1.toPosixPath)(node_path_1.default.join(node_path_1.default.dirname(importer), id)); - } - const relativePath = node_path_1.default.relative(this.options.workspaceRoot, fullPath); - if (this.buildResultFiles.has((0, path_1.toPosixPath)(relativePath))) { - return fullPath; - } - } - if (this.testFileToEntryPoint.has(id)) { - return id; - } - (0, node_assert_1.default)(this.buildResultFiles.size > 0, 'buildResult must be available for resolving.'); - const relativePath = node_path_1.default.relative(this.options.workspaceRoot, id); - if (this.buildResultFiles.has((0, path_1.toPosixPath)(relativePath))) { - return id; - } - }, - load: async (id) => { - (0, node_assert_1.default)(this.buildResultFiles.size > 0, 'buildResult must be available for in-memory loading.'); - // Attempt to load as a source test file. - const entryPoint = this.testFileToEntryPoint.get(id); - let outputPath; - if (entryPoint) { - outputPath = entryPoint + '.js'; - // To support coverage exclusion of the actual test file, the virtual - // test entry point only references the built and bundled intermediate file. - return { - code: `import "./${outputPath}";`, - }; - } - else { - // Attempt to load as a built artifact. - const relativePath = node_path_1.default.relative(this.options.workspaceRoot, id); - outputPath = (0, path_1.toPosixPath)(relativePath); - } - const outputFile = this.buildResultFiles.get(outputPath); - if (outputFile) { - const sourceMapPath = outputPath + '.map'; - const sourceMapFile = this.buildResultFiles.get(sourceMapPath); - const code = outputFile.origin === 'memory' - ? Buffer.from(outputFile.contents).toString('utf-8') - : await (0, promises_1.readFile)(outputFile.inputPath, 'utf-8'); - const map = sourceMapFile - ? sourceMapFile.origin === 'memory' - ? Buffer.from(sourceMapFile.contents).toString('utf-8') - : await (0, promises_1.readFile)(sourceMapFile.inputPath, 'utf-8') - : undefined; - return { - code, - map: map ? JSON.parse(map) : undefined, - }; - } - }, - }, - { - name: 'angular:html-index', - transformIndexHtml: () => { - // Add all global stylesheets - if (this.buildResultFiles.has('styles.css')) { - return [ - { - tag: 'link', - attrs: { href: 'styles.css', rel: 'stylesheet' }, - injectTo: 'head', - }, - ]; - } - return []; - }, - }, - ], - }); - }, - }, - ]; - } - async initializeVitest() { - const { codeCoverage, reporters, workspaceRoot, browsers, debug, watch } = this.options; - let vitestNodeModule; - try { - vitestNodeModule = await (0, load_esm_1.loadEsmModule)('vitest/node'); - } - catch (error) { - (0, error_1.assertIsError)(error); - if (error.code !== 'ERR_MODULE_NOT_FOUND') { - throw error; - } - throw new Error('The `vitest` package was not found. Please install the package and rerun the test command.'); - } - const { startVitest } = vitestNodeModule; - // Setup vitest browser options if configured - const browserOptions = (0, browser_provider_1.setupBrowserConfiguration)(browsers, debug, this.options.projectSourceRoot); - if (browserOptions.errors?.length) { - throw new Error(browserOptions.errors.join('\n')); - } - (0, node_assert_1.default)(this.buildResultFiles.size > 0, 'buildResult must be available before initializing vitest'); - const testSetupFiles = this.prepareSetupFiles(); - const plugins = this.createVitestPlugins(testSetupFiles, browserOptions); - const debugOptions = debug - ? { - inspectBrk: true, - isolate: false, - fileParallelism: false, - } - : {}; - return startVitest('test', undefined, { - // Disable configuration file resolution/loading - config: false, - root: workspaceRoot, - project: ['base', this.projectName], - name: 'base', - include: [], - reporters: reporters ?? ['default'], - watch, - coverage: generateCoverageOption(codeCoverage), - ...debugOptions, - }, { - server: { - // Disable the actual file watcher. The boolean watch option above should still - // be enabled as it controls other internal behavior related to rerunning tests. - watch: null, - }, - plugins, - }); - } -} -exports.VitestExecutor = VitestExecutor; -function generateCoverageOption(codeCoverage) { - if (!codeCoverage) { - return { - enabled: false, - }; - } - return { - enabled: true, - excludeAfterRemap: true, - // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures - ...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}), - ...(codeCoverage.reporters - ? { reporter: codeCoverage.reporters } - : {}), - }; -} diff --git a/src/builders/unit-test/runners/vitest/index.d.ts b/src/builders/unit-test/runners/vitest/index.d.ts deleted file mode 100644 index 6b31a310..00000000 --- a/src/builders/unit-test/runners/vitest/index.d.ts +++ /dev/null @@ -1,13 +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.dev/license - */ -import type { TestRunner } from '../api'; -/** - * A declarative definition of the Vitest test runner. - */ -declare const VitestTestRunner: TestRunner; -export default VitestTestRunner; diff --git a/src/builders/unit-test/runners/vitest/index.js b/src/builders/unit-test/runners/vitest/index.js deleted file mode 100644 index 8a5a5cc7..00000000 --- a/src/builders/unit-test/runners/vitest/index.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const node_assert_1 = __importDefault(require("node:assert")); -const build_options_1 = require("./build-options"); -const executor_1 = require("./executor"); -/** - * A declarative definition of the Vitest test runner. - */ -const VitestTestRunner = { - name: 'vitest', - getBuildOptions(options, baseBuildOptions) { - return (0, build_options_1.getVitestBuildOptions)(options, baseBuildOptions); - }, - async createExecutor(context, options) { - const projectName = context.target?.project; - (0, node_assert_1.default)(projectName, 'The builder requires a target.'); - return new executor_1.VitestExecutor(projectName, options); - }, -}; -exports.default = VitestTestRunner; diff --git a/src/builders/unit-test/schema.d.ts b/src/builders/unit-test/schema.d.ts index dc2bea54..589d25f0 100644 --- a/src/builders/unit-test/schema.d.ts +++ b/src/builders/unit-test/schema.d.ts @@ -43,10 +43,6 @@ export type Schema = { * instead. */ include?: string[]; - /** - * Log progress to the console while building. Defaults to the build target's progress value. - */ - progress?: boolean; /** * TypeScript file that exports an array of Angular providers to use during test execution. * The array must be a default export. diff --git a/src/builders/unit-test/schema.json b/src/builders/unit-test/schema.json index f49eaeda..8628bb97 100644 --- a/src/builders/unit-test/schema.json +++ b/src/builders/unit-test/schema.json @@ -39,6 +39,7 @@ "items": { "type": "string" }, + "default": [], "description": "Globs of files to exclude, relative to the project root." }, "watch": { @@ -60,7 +61,8 @@ "description": "Globs to exclude from code coverage.", "items": { "type": "string" - } + }, + "default": [] }, "codeCoverageReporters": { "type": "array", @@ -104,10 +106,6 @@ "type": "string" }, "description": "A list of global setup and configuration files that are included before the test files. The application's polyfills are always included before these files. The Angular Testbed is also initialized prior to the execution of these files." - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console while building. Defaults to the build target's progress value." } }, "additionalProperties": false, diff --git a/src/builders/unit-test/test-discovery.d.ts b/src/builders/unit-test/test-discovery.d.ts deleted file mode 100644 index 77e627c1..00000000 --- a/src/builders/unit-test/test-discovery.d.ts +++ /dev/null @@ -1,8 +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.dev/license - */ -export { findTests, getTestEntrypoints } from '../karma/find-tests'; diff --git a/src/builders/unit-test/test-discovery.js b/src/builders/unit-test/test-discovery.js deleted file mode 100644 index b746ba0c..00000000 --- a/src/builders/unit-test/test-discovery.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTestEntrypoints = exports.findTests = void 0; -// TODO: This should eventually contain the implementations for these -var find_tests_1 = require("../karma/find-tests"); -Object.defineProperty(exports, "findTests", { enumerable: true, get: function () { return find_tests_1.findTests; } }); -Object.defineProperty(exports, "getTestEntrypoints", { enumerable: true, get: function () { return find_tests_1.getTestEntrypoints; } }); diff --git a/src/private.d.ts b/src/private.d.ts index 1f126c09..c58594e3 100644 --- a/src/private.d.ts +++ b/src/private.d.ts @@ -17,7 +17,7 @@ import { BundleStylesheetOptions } from './tools/esbuild/stylesheets/bundle-opti export { buildApplicationInternal } from './builders/application'; export type { ApplicationBuilderInternalOptions } from './builders/application/options'; export { type Result, type ResultFile, ResultKind } from './builders/application/results'; -export { serveWithVite } from './builders/dev-server/vite'; +export { serveWithVite } from './builders/dev-server/vite-server'; export * from './tools/babel/plugins'; export type { ExternalResultMetadata } from './tools/esbuild/bundler-execution-result'; export { emitFilesToDisk } from './tools/esbuild/utils'; diff --git a/src/private.js b/src/private.js index 50bb3825..4126633a 100644 --- a/src/private.js +++ b/src/private.js @@ -38,8 +38,8 @@ var application_1 = require("./builders/application"); Object.defineProperty(exports, "buildApplicationInternal", { enumerable: true, get: function () { return application_1.buildApplicationInternal; } }); var results_1 = require("./builders/application/results"); Object.defineProperty(exports, "ResultKind", { enumerable: true, get: function () { return results_1.ResultKind; } }); -var vite_1 = require("./builders/dev-server/vite"); -Object.defineProperty(exports, "serveWithVite", { enumerable: true, get: function () { return vite_1.serveWithVite; } }); +var vite_server_1 = require("./builders/dev-server/vite-server"); +Object.defineProperty(exports, "serveWithVite", { enumerable: true, get: function () { return vite_server_1.serveWithVite; } }); // Tools __exportStar(require("./tools/babel/plugins"), exports); var utils_1 = require("./tools/esbuild/utils"); diff --git a/src/tools/babel/plugins/pure-toplevel-functions.d.ts b/src/tools/babel/plugins/pure-toplevel-functions.d.ts index d613c914..cea439bb 100644 --- a/src/tools/babel/plugins/pure-toplevel-functions.d.ts +++ b/src/tools/babel/plugins/pure-toplevel-functions.d.ts @@ -8,7 +8,6 @@ import type { PluginObj } from '@babel/core'; /** * A babel plugin factory function for adding the PURE annotation to top-level new and call expressions. - * * @returns A babel plugin object instance. */ export default function (): PluginObj; diff --git a/src/tools/babel/plugins/pure-toplevel-functions.js b/src/tools/babel/plugins/pure-toplevel-functions.js index 6ca53933..cb6b2bcb 100644 --- a/src/tools/babel/plugins/pure-toplevel-functions.js +++ b/src/tools/babel/plugins/pure-toplevel-functions.js @@ -47,7 +47,11 @@ exports.default = default_1; const helper_annotate_as_pure_1 = __importDefault(require("@babel/helper-annotate-as-pure")); const tslib = __importStar(require("tslib")); /** - * A cached set of TypeScript helper function names used by the helper name matcher utility function. + * A set of constructor names that are considered to be side-effect free. + */ +const sideEffectFreeConstructors = new Set(['InjectionToken']); +/** + * A set of TypeScript helper function names used by the helper name matcher utility function. */ const tslibHelpers = new Set(Object.keys(tslib).filter((h) => h.startsWith('__'))); /** @@ -76,13 +80,16 @@ function isBabelHelperName(name) { } /** * A babel plugin factory function for adding the PURE annotation to top-level new and call expressions. - * * @returns A babel plugin object instance. */ function default_1() { return { visitor: { - CallExpression(path) { + CallExpression(path, state) { + const { topLevelSafeMode = false } = state.opts; + if (topLevelSafeMode) { + return; + } // If the expression has a function parent, it is not top-level if (path.getFunctionParent()) { return; @@ -100,9 +107,18 @@ function default_1() { } (0, helper_annotate_as_pure_1.default)(path); }, - NewExpression(path) { + NewExpression(path, state) { // If the expression has a function parent, it is not top-level - if (!path.getFunctionParent()) { + if (path.getFunctionParent()) { + return; + } + const { topLevelSafeMode = false } = state.opts; + if (!topLevelSafeMode) { + (0, helper_annotate_as_pure_1.default)(path); + return; + } + const callee = path.get('callee'); + if (callee.isIdentifier() && sideEffectFreeConstructors.has(callee.node.name)) { (0, helper_annotate_as_pure_1.default)(path); } }, diff --git a/src/tools/esbuild/application-code-bundle.js b/src/tools/esbuild/application-code-bundle.js index 6c5d8478..27b29dfb 100644 --- a/src/tools/esbuild/application-code-bundle.js +++ b/src/tools/esbuild/application-code-bundle.js @@ -418,8 +418,6 @@ function getEsBuildCommonOptions(options) { packages = 'external'; } } - const minifySyntax = optimizationOptions.scripts; - const minifyIdentifiers = minifySyntax && environment_options_1.allowMangle; return { absWorkingDir: workspaceRoot, format: 'esm', @@ -431,10 +429,9 @@ function getEsBuildCommonOptions(options) { metafile: true, legalComments: options.extractLicenses ? 'none' : 'eof', logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent', - keepNames: !minifyIdentifiers, - minifyIdentifiers, - minifySyntax, - minifyWhitespace: minifySyntax, + minifyIdentifiers: optimizationOptions.scripts && environment_options_1.allowMangle, + minifySyntax: optimizationOptions.scripts, + minifyWhitespace: optimizationOptions.scripts, pure: ['forwardRef'], outdir: workspaceRoot, outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined, @@ -451,7 +448,7 @@ function getEsBuildCommonOptions(options) { // Only set to false when script optimizations are enabled. It should not be set to true because // Angular turns `ngDevMode` into an object for development debugging purposes when not defined // which a constant true value would break. - ...(minifySyntax ? { 'ngDevMode': 'false' } : undefined), + ...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined), 'ngJitMode': jit ? 'true' : 'false', 'ngServerMode': 'false', 'ngHmrMode': options.templateUpdates ? 'true' : 'false', @@ -462,7 +459,7 @@ function getEsBuildCommonOptions(options) { }; } function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfillsAsRelative, loadResultCache) { - const { jit, workspaceRoot, i18nOptions } = options; + const { jit, workspaceRoot, i18nOptions, externalPackages } = options; const buildOptions = getEsBuildCommonOptions(options); buildOptions.splitting = false; buildOptions.plugins ??= []; @@ -475,8 +472,10 @@ function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfi // Locale data should go first so that project provided polyfill code can augment if needed. let needLocaleDataPlugin = false; if (i18nOptions.shouldInline) { - // Remove localize polyfill as this is not needed for build time i18n. - polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize')); + if (!externalPackages) { + // Remove localize polyfill when i18n inline transformation have been applied to all the packages. + polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize')); + } // Add locale data for all active locales // TODO: Inject each individually within the inlining process itself for (const locale of i18nOptions.inlineLocales) { @@ -490,7 +489,7 @@ function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfi needLocaleDataPlugin = true; } if (needLocaleDataPlugin) { - buildOptions.plugins.push((0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)()); + buildOptions.plugins.unshift((0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)()); } if (polyfills.length === 0) { return; diff --git a/src/tools/esbuild/i18n-locale-plugin.js b/src/tools/esbuild/i18n-locale-plugin.js index 5953e250..0ffd2861 100644 --- a/src/tools/esbuild/i18n-locale-plugin.js +++ b/src/tools/esbuild/i18n-locale-plugin.js @@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.LOCALE_DATA_BASE_MODULE = exports.LOCALE_DATA_NAMESPACE = void 0; exports.createAngularLocaleDataPlugin = createAngularLocaleDataPlugin; +const node_module_1 = require("node:module"); /** * The internal namespace used by generated locale import statements and Angular locale data plugin. */ @@ -26,16 +27,6 @@ function createAngularLocaleDataPlugin() { return { name: 'angular-locale-data', setup(build) { - // If packages are configured to be external then leave the original angular locale import path. - // This happens when using the development server with caching enabled to allow Vite prebundling to work. - // There currently is no option on the esbuild resolve function to resolve while disabling the option. To - // workaround the inability to resolve the full locale location here, the Vite dev server prebundling also - // contains a plugin to allow the locales to be correctly resolved when prebundling. - // NOTE: If esbuild eventually allows controlling the external package options in a build.resolve call, this - // workaround can be removed. - if (build.initialOptions.packages === 'external') { - return; - } build.onResolve({ filter: /^angular:locale\/data:/ }, async ({ path }) => { // Extract the locale from the path const rawLocaleTag = path.split(':', 3)[2]; @@ -57,6 +48,7 @@ function createAngularLocaleDataPlugin() { }; } let exact = true; + let localeRequire; while (partialLocaleTag) { // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. // The onLoad hook below for the locale data namespace has an `empty` loader that will prevent inclusion. @@ -69,11 +61,37 @@ function createAngularLocaleDataPlugin() { } // Attempt to resolve the locale tag data within the Angular base module location const potentialPath = `${exports.LOCALE_DATA_BASE_MODULE}/${partialLocaleTag}`; - const result = await build.resolve(potentialPath, { - kind: 'import-statement', - resolveDir: build.initialOptions.absWorkingDir, - }); - if (result.path) { + // If packages are configured to be external then leave the original angular locale import path. + // This happens when using the development server with caching enabled to allow Vite prebundling to work. + // There currently is no option on the esbuild resolve function to resolve while disabling the option. + // NOTE: If esbuild eventually allows controlling the external package options in a build.resolve call, this + // workaround can be removed. + let result; + const { packages, absWorkingDir } = build.initialOptions; + if (packages === 'external' && absWorkingDir) { + localeRequire ??= (0, node_module_1.createRequire)(absWorkingDir + '/'); + try { + localeRequire.resolve(potentialPath); + result = { + errors: [], + warnings: [], + external: true, + sideEffects: true, + namespace: '', + suffix: '', + pluginData: undefined, + path: potentialPath, + }; + } + catch { } + } + else { + result = await build.resolve(potentialPath, { + kind: 'import-statement', + resolveDir: absWorkingDir, + }); + } + if (result?.path) { if (exact) { return result; } diff --git a/src/tools/esbuild/javascript-transformer-worker.js b/src/tools/esbuild/javascript-transformer-worker.js index a11ec6f9..95a76930 100644 --- a/src/tools/esbuild/javascript-transformer-worker.js +++ b/src/tools/esbuild/javascript-transformer-worker.js @@ -84,16 +84,10 @@ async function transformWithBabel(filename, data, options) { plugins.push(linkerPlugin); } if (options.advancedOptimizations) { + const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } = await Promise.resolve().then(() => __importStar(require('../babel/plugins'))); const sideEffectFree = options.sideEffects === false; const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); - const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } = await Promise.resolve().then(() => __importStar(require('../babel/plugins'))); - if (safeAngularPackage) { - plugins.push(markTopLevelPure); - } - plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [ - adjustStaticMembers, - { wrapDecorators: sideEffectFree }, - ]); + plugins.push([markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }], elideAngularMetadata, adjustTypeScriptEnums, [adjustStaticMembers, { wrapDecorators: sideEffectFree }]); } // If no additional transformations are needed, return the data directly if (plugins.length === 0) { diff --git a/src/tools/esbuild/stylesheets/bundle-options.d.ts b/src/tools/esbuild/stylesheets/bundle-options.d.ts index b23dac97..0630bbae 100644 --- a/src/tools/esbuild/stylesheets/bundle-options.d.ts +++ b/src/tools/esbuild/stylesheets/bundle-options.d.ts @@ -29,10 +29,7 @@ export interface BundleStylesheetOptions { file: string; package: string; }; - postcssConfiguration?: { - config: PostcssConfiguration; - configPath: string; - }; + postcssConfiguration?: PostcssConfiguration; publicPath?: string; cacheOptions: NormalizedCachedOptions; } diff --git a/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.d.ts b/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.d.ts index dafeba00..a0faac61 100644 --- a/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.d.ts +++ b/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.d.ts @@ -46,10 +46,7 @@ export interface StylesheetPluginOptions { * initialized and used for every stylesheet. This overrides the tailwind integration * and any tailwind usage must be manually configured in the custom postcss usage. */ - postcssConfiguration?: { - config: PostcssConfiguration; - configPath: string; - }; + postcssConfiguration?: PostcssConfiguration; /** * Optional Options for configuring Sass behavior. */ diff --git a/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js b/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js index 7259aade..1f50b6a1 100644 --- a/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js +++ b/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js @@ -46,7 +46,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.StylesheetPluginFactory = void 0; const node_assert_1 = __importDefault(require("node:assert")); const promises_1 = require("node:fs/promises"); -const node_module_1 = require("node:module"); const node_path_1 = require("node:path"); const tinyglobby_1 = require("tinyglobby"); const error_1 = require("../../../utils/error"); @@ -144,15 +143,13 @@ class StylesheetPluginFactory { node_assert_1.default.equal(++this.initPostcssCallCount, 1, '`initPostcss` was called more than once.'); const { options } = this; if (options.postcssConfiguration) { - const { config, configPath } = options.postcssConfiguration; - const postCssInstanceKey = JSON.stringify(config); + const postCssInstanceKey = JSON.stringify(options.postcssConfiguration); let postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); if (!postcssProcessor) { postcss ??= (await Promise.resolve().then(() => __importStar(require('postcss')))).default; postcssProcessor = postcss(); - const postCssPluginRequire = (0, node_module_1.createRequire)((0, node_path_1.dirname)(configPath) + '/'); - for (const [pluginName, pluginOptions] of config.plugins) { - const plugin = postCssPluginRequire(pluginName); + for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) { + const { default: plugin } = await Promise.resolve(`${pluginName}`).then(s => __importStar(require(s))); if (typeof plugin !== 'function' || plugin.postcss !== true) { throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`); } diff --git a/src/tools/vite/middlewares/ssr-middleware.js b/src/tools/vite/middlewares/ssr-middleware.js index 270dfc6d..4a553789 100644 --- a/src/tools/vite/middlewares/ssr-middleware.js +++ b/src/tools/vite/middlewares/ssr-middleware.js @@ -23,11 +23,6 @@ function createAngularSsrInternalMiddleware(server, indexHtmlTransformer) { await (0, load_esm_1.loadEsmModule)('@angular/compiler'); const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } = await (0, load_esm_1.loadEsmModule)('@angular/ssr/node'); const { ɵgetOrCreateAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')); - // `ɵgetOrCreateAngularServerApp` can be undefined right after an error. - // See: https://github.com/angular/angular-cli/issues/29907 - if (!ɵgetOrCreateAngularServerApp) { - return next(); - } const angularServerApp = ɵgetOrCreateAngularServerApp({ allowStaticRouteRender: true, }); diff --git a/src/tools/vite/plugins/i18n-locale-plugin.d.ts b/src/tools/vite/plugins/i18n-locale-plugin.d.ts deleted file mode 100644 index 41704f63..00000000 --- a/src/tools/vite/plugins/i18n-locale-plugin.d.ts +++ /dev/null @@ -1,14 +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.dev/license - */ -import type { Plugin } from 'vite'; -/** - * Creates a Vite plugin that resolves Angular locale data files from `@angular/common`. - * - * @returns A Vite plugin. - */ -export declare function createAngularLocaleDataPlugin(): Plugin; diff --git a/src/tools/vite/plugins/i18n-locale-plugin.js b/src/tools/vite/plugins/i18n-locale-plugin.js deleted file mode 100644 index 33e367f6..00000000 --- a/src/tools/vite/plugins/i18n-locale-plugin.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -/** - * @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.dev/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAngularLocaleDataPlugin = createAngularLocaleDataPlugin; -/** - * The base module location used to search for locale specific data. - */ -const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global'; -/** - * Creates a Vite plugin that resolves Angular locale data files from `@angular/common`. - * - * @returns A Vite plugin. - */ -function createAngularLocaleDataPlugin() { - return { - name: 'angular-locale-data', - enforce: 'pre', - async resolveId(source) { - if (!source.startsWith('angular:locale/data:')) { - return; - } - // Extract the locale from the path - const originalLocale = source.split(':', 3)[2]; - // Remove any private subtags since these will never match - let partialLocale = originalLocale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, ''); - let exact = true; - while (partialLocale) { - const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocale}`; - const result = await this.resolve(potentialPath); - if (result) { - if (!exact) { - this.warn(`Locale data for '${originalLocale}' cannot be found. Using locale data for '${partialLocale}'.`); - } - return result; - } - // Remove the last subtag and try again with a less specific locale - const parts = partialLocale.split('-'); - partialLocale = parts.slice(0, -1).join('-'); - exact = false; - // The locales "en" and "en-US" are considered exact to retain existing behavior - if (originalLocale === 'en-US' && partialLocale === 'en') { - exact = true; - } - } - return null; - }, - }; -} diff --git a/src/tools/vite/plugins/index.d.ts b/src/tools/vite/plugins/index.d.ts index d6d583ec..2c157829 100644 --- a/src/tools/vite/plugins/index.d.ts +++ b/src/tools/vite/plugins/index.d.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ export { createAngularMemoryPlugin } from './angular-memory-plugin'; -export { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; export { createRemoveIdPrefixPlugin } from './id-prefix-plugin'; export { createAngularSetupMiddlewaresPlugin, ServerSsrMode } from './setup-middlewares-plugin'; export { createAngularSsrTransformPlugin } from './ssr-transform-plugin'; diff --git a/src/tools/vite/plugins/index.js b/src/tools/vite/plugins/index.js index 1f135500..37463501 100644 --- a/src/tools/vite/plugins/index.js +++ b/src/tools/vite/plugins/index.js @@ -7,11 +7,9 @@ * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAngularSsrTransformPlugin = exports.ServerSsrMode = exports.createAngularSetupMiddlewaresPlugin = exports.createRemoveIdPrefixPlugin = exports.createAngularLocaleDataPlugin = exports.createAngularMemoryPlugin = void 0; +exports.createAngularSsrTransformPlugin = exports.ServerSsrMode = exports.createAngularSetupMiddlewaresPlugin = exports.createRemoveIdPrefixPlugin = exports.createAngularMemoryPlugin = void 0; var angular_memory_plugin_1 = require("./angular-memory-plugin"); Object.defineProperty(exports, "createAngularMemoryPlugin", { enumerable: true, get: function () { return angular_memory_plugin_1.createAngularMemoryPlugin; } }); -var i18n_locale_plugin_1 = require("./i18n-locale-plugin"); -Object.defineProperty(exports, "createAngularLocaleDataPlugin", { enumerable: true, get: function () { return i18n_locale_plugin_1.createAngularLocaleDataPlugin; } }); var id_prefix_plugin_1 = require("./id-prefix-plugin"); Object.defineProperty(exports, "createRemoveIdPrefixPlugin", { enumerable: true, get: function () { return id_prefix_plugin_1.createRemoveIdPrefixPlugin; } }); var setup_middlewares_plugin_1 = require("./setup-middlewares-plugin"); diff --git a/src/tools/vite/utils.js b/src/tools/vite/utils.js index 22064ca0..57d28915 100644 --- a/src/tools/vite/utils.js +++ b/src/tools/vite/utils.js @@ -53,7 +53,6 @@ function getDepOptimizationConfig({ disabled, exclude, include, target, zoneless esbuildOptions: { // Set esbuild supported targets. target, - keepNames: true, supported: (0, utils_1.getFeatureSupport)(target, zoneless), plugins, loader, diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 00000000..a219622d --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1,19 @@ +/** + * @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.dev/license + */ + +// The `bundled_beasties` causes issues with module mappings in Bazel, +// leading to unexpected behavior with esbuild. Specifically, the problem occurs +// when esbuild resolves to a different module or version than expected, due to +// how Bazel handles module mappings. +// +// This change aims to resolve esbuild types correctly and maintain consistency +// in the Bazel build process. + +declare module 'esbuild' { + export * from 'esbuild-wasm'; +} diff --git a/src/utils/environment-options.d.ts b/src/utils/environment-options.d.ts index e1a63fea..2c80ed43 100644 --- a/src/utils/environment-options.d.ts +++ b/src/utils/environment-options.d.ts @@ -5,59 +5,17 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -/** - * Allows disabling of code mangling when the `NG_BUILD_MANGLE` environment variable is set to `0` or `false`. - * This is useful for debugging build output. - */ export declare const allowMangle: boolean; -/** - * Allows beautification of build output when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. - * This is useful for debugging build output. - */ export declare const shouldBeautify: boolean; -/** - * Allows disabling of code minification when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. - * This is useful for debugging build output. - */ export declare const allowMinify: boolean; -/** - * The maximum number of workers to use for parallel processing. - * This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable. - */ export declare const maxWorkers: number; -/** - * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. - */ export declare const useParallelTs: boolean; -/** - * When `NG_BUILD_DEBUG_PERF` is enabled, performance debugging information is printed. - */ export declare const debugPerformance: boolean; -/** - * When `NG_BUILD_WATCH_ROOT` is enabled, the build will watch the root directory for changes. - */ export declare const shouldWatchRoot: boolean; -/** - * When `NG_BUILD_TYPE_CHECK` is set to `0` or `false`, type checking is disabled. - */ export declare const useTypeChecking: boolean; -/** - * When `NG_BUILD_LOGS_JSON` is enabled, build logs will be output in JSON format. - */ export declare const useJSONBuildLogs: boolean; -/** - * When `NG_BUILD_OPTIMIZE_CHUNKS` is enabled, the build will optimize chunks. - */ export declare const shouldOptimizeChunks: boolean; -/** - * When `NG_HMR_CSTYLES` is enabled, component styles will be hot-reloaded. - */ export declare const useComponentStyleHmr: boolean; -/** - * When `NG_HMR_TEMPLATES` is set to `0` or `false`, component templates will not be hot-reloaded. - */ export declare const useComponentTemplateHmr: boolean; -/** - * When `NG_BUILD_PARTIAL_SSR` is enabled, a partial server-side rendering build will be performed. - */ export declare const usePartialSsrBuild: boolean; +export declare const bazelEsbuildPluginPath: string | undefined; diff --git a/src/utils/environment-options.js b/src/utils/environment-options.js index aceaef36..339a7545 100644 --- a/src/utils/environment-options.js +++ b/src/utils/environment-options.js @@ -7,44 +7,21 @@ * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.usePartialSsrBuild = exports.useComponentTemplateHmr = exports.useComponentStyleHmr = exports.shouldOptimizeChunks = exports.useJSONBuildLogs = exports.useTypeChecking = exports.shouldWatchRoot = exports.debugPerformance = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0; +exports.bazelEsbuildPluginPath = exports.usePartialSsrBuild = exports.useComponentTemplateHmr = exports.useComponentStyleHmr = exports.shouldOptimizeChunks = exports.useJSONBuildLogs = exports.useTypeChecking = exports.shouldWatchRoot = exports.debugPerformance = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0; const node_os_1 = require("node:os"); -/** A set of strings that are considered "truthy" when parsing environment variables. */ -const TRUTHY_VALUES = new Set(['1', 'true']); -/** A set of strings that are considered "falsy" when parsing environment variables. */ -const FALSY_VALUES = new Set(['0', 'false']); -/** - * Checks if an environment variable is present and has a non-empty value. - * @param variable The environment variable to check. - * @returns `true` if the variable is a non-empty string. - */ +function isDisabled(variable) { + return variable === '0' || variable.toLowerCase() === 'false'; +} +function isEnabled(variable) { + return variable === '1' || variable.toLowerCase() === 'true'; +} function isPresent(variable) { return typeof variable === 'string' && variable !== ''; } -/** - * Parses an environment variable into a boolean or undefined. - * @returns `true` if the variable is truthy ('1', 'true'). - * @returns `false` if the variable is falsy ('0', 'false'). - * @returns `undefined` if the variable is not present or has an unknown value. - */ -function parseTristate(variable) { - if (!isPresent(variable)) { - return undefined; - } - const value = variable.toLowerCase(); - if (TRUTHY_VALUES.has(value)) { - return true; - } - if (FALSY_VALUES.has(value)) { - return false; - } - // TODO: Consider whether a warning is useful in this case of a malformed value - return undefined; -} // Optimization and mangling const debugOptimizeVariable = process.env['NG_BUILD_DEBUG_OPTIMIZE']; const debugOptimize = (() => { - if (!isPresent(debugOptimizeVariable) || parseTristate(debugOptimizeVariable) === false) { + if (!isPresent(debugOptimizeVariable) || isDisabled(debugOptimizeVariable)) { return { mangle: true, minify: true, @@ -56,7 +33,7 @@ const debugOptimize = (() => { minify: false, beautify: true, }; - if (parseTristate(debugOptimizeVariable) === true) { + if (isEnabled(debugOptimizeVariable)) { return debugValue; } for (const part of debugOptimizeVariable.split(',')) { @@ -74,20 +51,11 @@ const debugOptimize = (() => { } return debugValue; })(); -/** - * Allows disabling of code mangling when the `NG_BUILD_MANGLE` environment variable is set to `0` or `false`. - * This is useful for debugging build output. - */ -exports.allowMangle = parseTristate(process.env['NG_BUILD_MANGLE']) ?? debugOptimize.mangle; -/** - * Allows beautification of build output when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. - * This is useful for debugging build output. - */ +const mangleVariable = process.env['NG_BUILD_MANGLE']; +exports.allowMangle = isPresent(mangleVariable) + ? !isDisabled(mangleVariable) + : debugOptimize.mangle; exports.shouldBeautify = debugOptimize.beautify; -/** - * Allows disabling of code minification when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. - * This is useful for debugging build output. - */ exports.allowMinify = debugOptimize.minify; /** * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. @@ -99,46 +67,29 @@ exports.allowMinify = debugOptimize.minify; * */ const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; -/** - * The maximum number of workers to use for parallel processing. - * This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable. - */ exports.maxWorkers = isPresent(maxWorkersVariable) ? +maxWorkersVariable : Math.min(4, Math.max((0, node_os_1.availableParallelism)() - 1, 1)); -/** - * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. - */ -exports.useParallelTs = parseTristate(process.env['NG_BUILD_PARALLEL_TS']) !== false; -/** - * When `NG_BUILD_DEBUG_PERF` is enabled, performance debugging information is printed. - */ -exports.debugPerformance = parseTristate(process.env['NG_BUILD_DEBUG_PERF']) === true; -/** - * When `NG_BUILD_WATCH_ROOT` is enabled, the build will watch the root directory for changes. - */ -exports.shouldWatchRoot = parseTristate(process.env['NG_BUILD_WATCH_ROOT']) === true; -/** - * When `NG_BUILD_TYPE_CHECK` is set to `0` or `false`, type checking is disabled. - */ -exports.useTypeChecking = parseTristate(process.env['NG_BUILD_TYPE_CHECK']) !== false; -/** - * When `NG_BUILD_LOGS_JSON` is enabled, build logs will be output in JSON format. - */ -exports.useJSONBuildLogs = parseTristate(process.env['NG_BUILD_LOGS_JSON']) === true; -/** - * When `NG_BUILD_OPTIMIZE_CHUNKS` is enabled, the build will optimize chunks. - */ -exports.shouldOptimizeChunks = parseTristate(process.env['NG_BUILD_OPTIMIZE_CHUNKS']) === true; -/** - * When `NG_HMR_CSTYLES` is enabled, component styles will be hot-reloaded. - */ -exports.useComponentStyleHmr = parseTristate(process.env['NG_HMR_CSTYLES']) === true; -/** - * When `NG_HMR_TEMPLATES` is set to `0` or `false`, component templates will not be hot-reloaded. - */ -exports.useComponentTemplateHmr = parseTristate(process.env['NG_HMR_TEMPLATES']) !== false; -/** - * When `NG_BUILD_PARTIAL_SSR` is enabled, a partial server-side rendering build will be performed. - */ -exports.usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SSR']) === true; +const parallelTsVariable = process.env['NG_BUILD_PARALLEL_TS']; +exports.useParallelTs = !isPresent(parallelTsVariable) || !isDisabled(parallelTsVariable); +const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF']; +exports.debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable); +const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT']; +exports.shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable); +const typeCheckingVariable = process.env['NG_BUILD_TYPE_CHECK']; +exports.useTypeChecking = !isPresent(typeCheckingVariable) || !isDisabled(typeCheckingVariable); +const buildLogsJsonVariable = process.env['NG_BUILD_LOGS_JSON']; +exports.useJSONBuildLogs = isPresent(buildLogsJsonVariable) && isEnabled(buildLogsJsonVariable); +const optimizeChunksVariable = process.env['NG_BUILD_OPTIMIZE_CHUNKS']; +exports.shouldOptimizeChunks = isPresent(optimizeChunksVariable) && isEnabled(optimizeChunksVariable); +const hmrComponentStylesVariable = process.env['NG_HMR_CSTYLES']; +exports.useComponentStyleHmr = isPresent(hmrComponentStylesVariable) && isEnabled(hmrComponentStylesVariable); +const hmrComponentTemplateVariable = process.env['NG_HMR_TEMPLATES']; +exports.useComponentTemplateHmr = !isPresent(hmrComponentTemplateVariable) || !isDisabled(hmrComponentTemplateVariable); +const partialSsrBuildVariable = process.env['NG_BUILD_PARTIAL_SSR']; +exports.usePartialSsrBuild = isPresent(partialSsrBuildVariable) && isEnabled(partialSsrBuildVariable); +const bazelBinDirectory = process.env['BAZEL_BINDIR']; +const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; +exports.bazelEsbuildPluginPath = bazelBinDirectory && bazelExecRoot + ? process.env['NG_INTERNAL_ESBUILD_PLUGINS_DO_NOT_USE'] + : undefined; diff --git a/src/utils/normalize-cache.js b/src/utils/normalize-cache.js index b35a27a7..8a6f7b6d 100644 --- a/src/utils/normalize-cache.js +++ b/src/utils/normalize-cache.js @@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.normalizeCacheOptions = normalizeCacheOptions; const node_path_1 = require("node:path"); /** Version placeholder is replaced during the build process with actual package version */ -const VERSION = '21.0.0-next.2+sha-9749ec6'; +const VERSION = '20.3.13+sha-948869d'; function hasCacheMetadata(value) { return (!!value && typeof value === 'object' && diff --git a/src/utils/postcss-configuration.d.ts b/src/utils/postcss-configuration.d.ts index cd7f788d..35dc3991 100644 --- a/src/utils/postcss-configuration.d.ts +++ b/src/utils/postcss-configuration.d.ts @@ -14,7 +14,4 @@ export interface SearchDirectory { } export declare function generateSearchDirectories(roots: string[]): Promise; export declare function findTailwindConfiguration(searchDirectories: SearchDirectory[]): string | undefined; -export declare function loadPostcssConfiguration(searchDirectories: SearchDirectory[]): Promise<{ - configPath: string; - config: PostcssConfiguration; -} | undefined>; +export declare function loadPostcssConfiguration(searchDirectories: SearchDirectory[]): Promise; diff --git a/src/utils/postcss-configuration.js b/src/utils/postcss-configuration.js index 090c6759..9bcf2c3d 100644 --- a/src/utils/postcss-configuration.js +++ b/src/utils/postcss-configuration.js @@ -67,7 +67,7 @@ async function loadPostcssConfiguration(searchDirectories) { config.plugins.push(element); } } - return { config, configPath }; + return config; } // Normalize plugin object map form const entries = Object.entries(raw.plugins); @@ -81,5 +81,5 @@ async function loadPostcssConfiguration(searchDirectories) { } config.plugins.push([name, options]); } - return { config, configPath }; + return config; } diff --git a/src/utils/server-rendering/load-esm-from-memory.d.ts b/src/utils/server-rendering/load-esm-from-memory.d.ts index c34a409a..bb038915 100644 --- a/src/utils/server-rendering/load-esm-from-memory.d.ts +++ b/src/utils/server-rendering/load-esm-from-memory.d.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ import type { ApplicationRef, Type } from '@angular/core'; +import type { BootstrapContext } from '@angular/platform-browser'; import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr'; /** * Represents the exports available from the main server bundle. */ interface MainServerBundleExports { - default: (() => Promise) | Type; + default: ((context: BootstrapContext) => Promise) | Type; ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree; ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp; } diff --git a/src/utils/server-rendering/prerender.js b/src/utils/server-rendering/prerender.js index 3d22012c..dce0af44 100644 --- a/src/utils/server-rendering/prerender.js +++ b/src/utils/server-rendering/prerender.js @@ -19,6 +19,7 @@ const worker_pool_1 = require("../worker-pool"); const utils_1 = require("./esm-in-memory-loader/utils"); const manifest_1 = require("./manifest"); const models_1 = require("./models"); +const utils_2 = require("./utils"); async function prerenderPages(workspaceRoot, baseHref, appShellOptions, prerenderOptions, outputFiles, assets, outputMode, sourcemap = false, maxThreads = 1) { const outputFilesForWorker = {}; const serverBundlesSourceMaps = new Map(); @@ -50,7 +51,7 @@ async function prerenderPages(workspaceRoot, baseHref, appShellOptions, prerende serverBundlesSourceMaps.clear(); const assetsReversed = {}; for (const { source, destination } of assets) { - assetsReversed[addLeadingSlash((0, path_1.toPosixPath)(destination))] = source; + assetsReversed[(0, url_1.addLeadingSlash)((0, path_1.toPosixPath)(destination))] = source; } // Get routes to prerender const { errors: extractionErrors, serializedRouteTree: serializableRouteTreeNode, appShellRoute, } = await getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, assetsReversed, appShellOptions, prerenderOptions, sourcemap, outputMode).catch((err) => { @@ -128,16 +129,16 @@ async function renderPages(baseHref, sourcemap, serializableRouteTreeNode, maxTh }); try { const renderingPromises = []; - const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute); + const appShellRouteWithLeadingSlash = appShellRoute && (0, url_1.addLeadingSlash)(appShellRoute); const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname; for (const { route, redirectTo } of serializableRouteTreeNode) { // Remove the base href from the file output path. - const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefPathnameWithLeadingSlash) - ? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length)) + const routeWithoutBaseHref = (0, url_1.addTrailingSlash)(route).startsWith(baseHrefPathnameWithLeadingSlash) + ? (0, url_1.addLeadingSlash)(route.slice(baseHrefPathnameWithLeadingSlash.length)) : route; - const outPath = node_path_1.posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html'); + const outPath = (0, url_1.stripLeadingSlash)(node_path_1.posix.join(routeWithoutBaseHref, 'index.html')); if (typeof redirectTo === 'string') { - output[outPath] = { content: generateRedirectStaticPage(redirectTo), appShellRoute: false }; + output[outPath] = { content: (0, utils_2.generateRedirectStaticPage)(redirectTo), appShellRoute: false }; continue; } const render = renderWorker.run({ url: route }); @@ -171,7 +172,7 @@ async function getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, asset const routes = []; let appShellRoute; if (appShellOptions) { - appShellRoute = (0, url_1.urlJoin)(baseHref, appShellOptions.route); + appShellRoute = (0, url_1.joinUrlParts)(baseHref, appShellOptions.route); routes.push({ renderMode: models_1.RouteRenderMode.Prerender, route: appShellRoute, @@ -182,7 +183,7 @@ async function getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, asset for (const route of routesFromFile) { routes.push({ renderMode: models_1.RouteRenderMode.Prerender, - route: (0, url_1.urlJoin)(baseHref, route.trim()), + route: (0, url_1.joinUrlParts)(baseHref, route.trim()), }); } } @@ -232,36 +233,3 @@ async function getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, asset void renderWorker.destroy(); } } -function addLeadingSlash(value) { - return value[0] === '/' ? value : '/' + value; -} -function addTrailingSlash(url) { - return url[url.length - 1] === '/' ? url : `${url}/`; -} -function removeLeadingSlash(value) { - return value[0] === '/' ? value.slice(1) : value; -} -/** - * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. - * - * This function creates a simple HTML page that performs a redirect using a meta tag. - * It includes a fallback link in case the meta-refresh doesn't work. - * - * @param url - The URL to which the page should redirect. - * @returns The HTML content of the static redirect page. - */ -function generateRedirectStaticPage(url) { - return ` - - - - - Redirecting - - - -
Redirecting to ${url}
- - -`.trim(); -} diff --git a/src/utils/server-rendering/render-worker.js b/src/utils/server-rendering/render-worker.js index 6f5b1835..d66b2f4a 100644 --- a/src/utils/server-rendering/render-worker.js +++ b/src/utils/server-rendering/render-worker.js @@ -11,6 +11,7 @@ const node_worker_threads_1 = require("node:worker_threads"); const fetch_patch_1 = require("./fetch-patch"); const launch_server_1 = require("./launch-server"); const load_esm_from_memory_1 = require("./load-esm-from-memory"); +const utils_1 = require("./utils"); /** * This is passed as workerData when setting up the worker via the `piscina` package. */ @@ -25,7 +26,11 @@ async function renderPage({ url }) { allowStaticRouteRender: true, }); const response = await angularServerApp.handle(new Request(new URL(url, serverURL), { signal: AbortSignal.timeout(30_000) })); - return response ? response.text() : null; + if (!response) { + return null; + } + const location = response.headers.get('Location'); + return location ? (0, utils_1.generateRedirectStaticPage)(location) : response.text(); } async function initialize() { if (outputMode !== undefined && hasSsrEntry) { diff --git a/src/utils/server-rendering/utils.d.ts b/src/utils/server-rendering/utils.d.ts index 8eb3598e..2da381c5 100644 --- a/src/utils/server-rendering/utils.d.ts +++ b/src/utils/server-rendering/utils.d.ts @@ -9,3 +9,13 @@ import type { createRequestHandler } from '@angular/ssr'; import type { createNodeRequestHandler } from '@angular/ssr/node'; export declare function isSsrNodeRequestHandler(value: unknown): value is ReturnType; export declare function isSsrRequestHandler(value: unknown): value is ReturnType; +/** + * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. + * + * This function creates a simple HTML page that performs a redirect using a meta tag. + * It includes a fallback link in case the meta-refresh doesn't work. + * + * @param url - The URL to which the page should redirect. + * @returns The HTML content of the static redirect page. + */ +export declare function generateRedirectStaticPage(url: string): string; diff --git a/src/utils/server-rendering/utils.js b/src/utils/server-rendering/utils.js index 8a64731c..ca255ee1 100644 --- a/src/utils/server-rendering/utils.js +++ b/src/utils/server-rendering/utils.js @@ -9,9 +9,34 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.isSsrNodeRequestHandler = isSsrNodeRequestHandler; exports.isSsrRequestHandler = isSsrRequestHandler; +exports.generateRedirectStaticPage = generateRedirectStaticPage; function isSsrNodeRequestHandler(value) { return typeof value === 'function' && '__ng_node_request_handler__' in value; } function isSsrRequestHandler(value) { return typeof value === 'function' && '__ng_request_handler__' in value; } +/** + * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. + * + * This function creates a simple HTML page that performs a redirect using a meta tag. + * It includes a fallback link in case the meta-refresh doesn't work. + * + * @param url - The URL to which the page should redirect. + * @returns The HTML content of the static redirect page. + */ +function generateRedirectStaticPage(url) { + return ` + + + + + Redirecting + + + +
Redirecting to ${url}
+ + +`.trim(); +} diff --git a/src/utils/url.d.ts b/src/utils/url.d.ts index d6e19160..8c3b16db 100644 --- a/src/utils/url.d.ts +++ b/src/utils/url.d.ts @@ -5,4 +5,77 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -export declare function urlJoin(...parts: string[]): string; +/** + * Removes the trailing slash from a URL if it exists. + * + * @param url - The URL string from which to remove the trailing slash. + * @returns The URL string without a trailing slash. + * + * @example + * ```js + * stripTrailingSlash('path/'); // 'path' + * stripTrailingSlash('/path'); // '/path' + * stripTrailingSlash('/'); // '/' + * stripTrailingSlash(''); // '' + * ``` + */ +export declare function stripTrailingSlash(url: string): string; +/** + * Removes the leading slash from a URL if it exists. + * + * @param url - The URL string from which to remove the leading slash. + * @returns The URL string without a leading slash. + * + * @example + * ```js + * stripLeadingSlash('/path'); // 'path' + * stripLeadingSlash('/path/'); // 'path/' + * stripLeadingSlash('/'); // '/' + * stripLeadingSlash(''); // '' + * ``` + */ +export declare function stripLeadingSlash(url: string): string; +/** + * Adds a leading slash to a URL if it does not already have one. + * + * @param url - The URL string to which the leading slash will be added. + * @returns The URL string with a leading slash. + * + * @example + * ```js + * addLeadingSlash('path'); // '/path' + * addLeadingSlash('/path'); // '/path' + * ``` + */ +export declare function addLeadingSlash(url: string): string; +/** + * Adds a trailing slash to a URL if it does not already have one. + * + * @param url - The URL string to which the trailing slash will be added. + * @returns The URL string with a trailing slash. + * + * @example + * ```js + * addTrailingSlash('path'); // 'path/' + * addTrailingSlash('path/'); // 'path/' + * ``` + */ +export declare function addTrailingSlash(url: string): string; +/** + * Joins URL parts into a single URL string. + * + * This function takes multiple URL segments, normalizes them by removing leading + * and trailing slashes where appropriate, and then joins them into a single URL. + * + * @param parts - The parts of the URL to join. Each part can be a string with or without slashes. + * @returns The joined URL string, with normalized slashes. + * + * @example + * ```js + * joinUrlParts('path/', '/to/resource'); // '/path/to/resource' + * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource' + * joinUrlParts('http://localhost/path/', 'to/resource'); // 'http://localhost/path/to/resource' + * joinUrlParts('', ''); // '/' + * ``` + */ +export declare function joinUrlParts(...parts: string[]): string; diff --git a/src/utils/url.js b/src/utils/url.js index 6674de1f..a41d632b 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -7,11 +7,115 @@ * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.urlJoin = urlJoin; -function urlJoin(...parts) { - const [p, ...rest] = parts; - // Remove trailing slash from first part - // Join all parts with `/` - // Dedupe double slashes from path names - return p.replace(/\/$/, '') + ('/' + rest.join('/')).replace(/\/\/+/g, '/'); +exports.stripTrailingSlash = stripTrailingSlash; +exports.stripLeadingSlash = stripLeadingSlash; +exports.addLeadingSlash = addLeadingSlash; +exports.addTrailingSlash = addTrailingSlash; +exports.joinUrlParts = joinUrlParts; +/** + * Removes the trailing slash from a URL if it exists. + * + * @param url - The URL string from which to remove the trailing slash. + * @returns The URL string without a trailing slash. + * + * @example + * ```js + * stripTrailingSlash('path/'); // 'path' + * stripTrailingSlash('/path'); // '/path' + * stripTrailingSlash('/'); // '/' + * stripTrailingSlash(''); // '' + * ``` + */ +function stripTrailingSlash(url) { + // Check if the last character of the URL is a slash + return url.length > 1 && url[url.length - 1] === '/' ? url.slice(0, -1) : url; +} +/** + * Removes the leading slash from a URL if it exists. + * + * @param url - The URL string from which to remove the leading slash. + * @returns The URL string without a leading slash. + * + * @example + * ```js + * stripLeadingSlash('/path'); // 'path' + * stripLeadingSlash('/path/'); // 'path/' + * stripLeadingSlash('/'); // '/' + * stripLeadingSlash(''); // '' + * ``` + */ +function stripLeadingSlash(url) { + // Check if the first character of the URL is a slash + return url.length > 1 && url[0] === '/' ? url.slice(1) : url; +} +/** + * Adds a leading slash to a URL if it does not already have one. + * + * @param url - The URL string to which the leading slash will be added. + * @returns The URL string with a leading slash. + * + * @example + * ```js + * addLeadingSlash('path'); // '/path' + * addLeadingSlash('/path'); // '/path' + * ``` + */ +function addLeadingSlash(url) { + // Check if the URL already starts with a slash + return url[0] === '/' ? url : `/${url}`; +} +/** + * Adds a trailing slash to a URL if it does not already have one. + * + * @param url - The URL string to which the trailing slash will be added. + * @returns The URL string with a trailing slash. + * + * @example + * ```js + * addTrailingSlash('path'); // 'path/' + * addTrailingSlash('path/'); // 'path/' + * ``` + */ +function addTrailingSlash(url) { + // Check if the URL already end with a slash + return url[url.length - 1] === '/' ? url : `${url}/`; +} +/** + * Joins URL parts into a single URL string. + * + * This function takes multiple URL segments, normalizes them by removing leading + * and trailing slashes where appropriate, and then joins them into a single URL. + * + * @param parts - The parts of the URL to join. Each part can be a string with or without slashes. + * @returns The joined URL string, with normalized slashes. + * + * @example + * ```js + * joinUrlParts('path/', '/to/resource'); // '/path/to/resource' + * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource' + * joinUrlParts('http://localhost/path/', 'to/resource'); // 'http://localhost/path/to/resource' + * joinUrlParts('', ''); // '/' + * ``` + */ +function joinUrlParts(...parts) { + const normalizeParts = []; + for (const part of parts) { + if (part === '') { + // Skip any empty parts + continue; + } + let normalizedPart = part; + if (part[0] === '/') { + normalizedPart = normalizedPart.slice(1); + } + if (part[part.length - 1] === '/') { + normalizedPart = normalizedPart.slice(0, -1); + } + if (normalizedPart !== '') { + normalizeParts.push(normalizedPart); + } + } + const protocolMatch = normalizeParts.length && /^https?:\/\//.test(normalizeParts[0]); + const joinedParts = normalizeParts.join('/'); + return protocolMatch ? joinedParts : addLeadingSlash(joinedParts); } diff --git a/src/utils/version.js b/src/utils/version.js index 736e613f..ad05586d 100644 --- a/src/utils/version.js +++ b/src/utils/version.js @@ -28,7 +28,7 @@ function assertCompatibleAngularVersion(projectRoot) { 'This likely indicates a corrupted local installation. Please try reinstalling your packages.'); process.exit(2); } - const supportedAngularSemver = '^21.0.0-next.0'; + const supportedAngularSemver = '^20.0.0'; if (angularPkgJson['version'] === '0.0.0' || supportedAngularSemver.startsWith('0.0.0')) { // Internal CLI and FW testing version. return; diff --git a/uniqueId b/uniqueId index 9a9d8859..8fbc32b3 100644 --- a/uniqueId +++ b/uniqueId @@ -1 +1 @@ -Tue Sep 09 2025 16:40:00 GMT+0000 (Coordinated Universal Time) \ No newline at end of file +Wed Dec 03 2025 14:12:21 GMT+0000 (Coordinated Universal Time) \ No newline at end of file