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..577525f1 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.0-next.0+sha-e21bd5c", "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#e21bd5c", + "@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", + "rolldown": "1.0.0-beta.32", + "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.2", "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#e21bd5c", "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": { @@ -123,5 +123,6 @@ "bugs": { "url": "https://github.com/angular/angular-cli/issues" }, - "homepage": "https://github.com/angular/angular-cli" + "homepage": "https://github.com/angular/angular-cli", + "__ngDevExceptionalMinor__": true } diff --git a/src/builders/application/chunk-optimizer.js b/src/builders/application/chunk-optimizer.js index 06353085..3e780be8 100644 --- a/src/builders/application/chunk-optimizer.js +++ b/src/builders/application/chunk-optimizer.js @@ -198,8 +198,7 @@ async function optimizeChunks(original, sourcemap) { ], }); const result = await bundle.generate({ - minify: { mangle: false, compress: false }, - advancedChunks: { minSize: 8192 }, + minify: { mangle: false, compress: false, removeWhitespace: true }, sourcemap, chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, }); 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/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..9fb7f767 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. + 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).'); + } +} +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.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) { + 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/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/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/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..2769cc0f 100644 --- a/src/utils/environment-options.d.ts +++ b/src/utils/environment-options.d.ts @@ -5,59 +5,16 @@ * 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; diff --git a/src/utils/environment-options.js b/src/utils/environment-options.js index aceaef36..ac113cd1 100644 --- a/src/utils/environment-options.js +++ b/src/utils/environment-options.js @@ -9,42 +9,19 @@ 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; 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,24 @@ 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); diff --git a/src/utils/normalize-cache.js b/src/utils/normalize-cache.js index b35a27a7..6e67eac0 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.0-next.0+sha-e21bd5c'; 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/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..0f2e1678 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 +Tue Sep 09 2025 17:28:04 GMT+0000 (Coordinated Universal Time) \ No newline at end of file