diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 4f0d1295a7e3..99d5d67efbfd 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -28,11 +28,11 @@ import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project import { addTrailingSlash, joinUrlParts, stripLeadingSlash } from '../../utils/url'; import { Schema as ApplicationBuilderOptions, - ExperimentalPlatform, I18NTranslation, OutputHashing, OutputMode, OutputPathClass, + Platform, } from './schema'; /** @@ -292,11 +292,11 @@ export async function normalizeOptions( if (options.ssr === true) { ssrOptions = {}; } else if (typeof options.ssr === 'object') { - const { entry, experimentalPlatform = ExperimentalPlatform.Node } = options.ssr; + const { entry, platform = Platform.Node } = options.ssr; ssrOptions = { entry: entry && path.join(workspaceRoot, entry), - platform: experimentalPlatform, + platform, }; } diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 5498a21fe004..8bf30aec8298 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -603,8 +603,8 @@ "type": "string", "description": "The server entry-point that when executed will spawn the web server." }, - "experimentalPlatform": { - "description": "Specifies the platform for which the server bundle is generated. This affects the APIs and modules available in the server-side code. \n\n- `node`: (Default) Generates a bundle optimized for Node.js environments. \n- `neutral`: Generates a platform-neutral bundle suitable for environments like edge workers, and other serverless platforms. This option avoids using Node.js-specific APIs, making the bundle more portable. \n\nPlease note that this feature does not provide polyfills for Node.js modules. Additionally, it is experimental, and the schematics may undergo changes in future versions.", + "platform": { + "description": "Specifies the platform for which the server bundle is generated. This affects the APIs and modules available in the server-side code. \n\n- `node`: (Default) Generates a bundle optimized for Node.js environments. \n- `neutral`: Generates a platform-neutral bundle suitable for environments like edge workers, and other serverless platforms. This option avoids using Node.js-specific APIs, making the bundle more portable. \n\nPlease note that this feature does not provide polyfills for Node.js modules.", "default": "node", "enum": ["node", "neutral"] } diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index 257a98fc0f45..08da63744c97 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { BuildOptions, PartialMessage, Plugin } from 'esbuild'; +import type { BuildOptions, Plugin } from 'esbuild'; import assert from 'node:assert'; import { createHash } from 'node:crypto'; import { extname, relative } from 'node:path'; import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; -import { ExperimentalPlatform } from '../../builders/application/schema'; +import { Platform } from '../../builders/application/schema'; import { allowMangle } from '../../utils/environment-options'; import { toPosixPath } from '../../utils/path'; import { @@ -158,7 +158,7 @@ export function createServerPolyfillBundleOptions( ): BundlerOptionsFactory | undefined { const serverPolyfills: string[] = []; const polyfillsFromConfig = new Set(options.polyfills); - const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; + const isNodePlatform = options.ssrOptions?.platform !== Platform.Neutral; if (!isZonelessApp(options.polyfills)) { serverPolyfills.push(isNodePlatform ? 'zone.js/node' : 'zone.js'); @@ -295,7 +295,7 @@ export function createServerMainCodeBundleOptions( // Mark manifest and polyfills file as external as these are generated by a different bundle step. (buildOptions.external ??= []).push(...SERVER_GENERATED_EXTERNALS); - const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; + const isNodePlatform = options.ssrOptions?.platform !== Platform.Neutral; if (!isNodePlatform) { // `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client. @@ -387,7 +387,7 @@ export function createSsrEntryCodeBundleOptions( const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadResultCache); const ssrEntryNamespace = 'angular:ssr-entry'; const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest'; - const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; + const isNodePlatform = options.ssrOptions?.platform !== Platform.Neutral; const jsBanner: string[] = []; if (options.externalDependencies?.length) { @@ -505,7 +505,7 @@ export function createSsrEntryCodeBundleOptions( } function getEsBuildServerCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions { - const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; + const isNodePlatform = options.ssrOptions?.platform !== Platform.Neutral; const commonOptions = getEsBuildCommonOptions(options); commonOptions.define ??= {}; diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 72d2cdef4030..3745792eb6cc 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -2,7 +2,7 @@ "encapsulation": false, "schematics": { "use-application-builder": { - "version": "21.0.0", + "version": "22.0.0", "factory": "./use-application-builder/migration", "description": "Migrate application projects to the new build system. Application projects that are using the '@angular-devkit/build-angular' package's 'browser' and/or 'browser-esbuild' builders will be migrated to use the new 'application' builder. You can read more about this, including known issues and limitations, here: https://angular.dev/tools/cli/build-system-migration", "optional": true, @@ -13,6 +13,11 @@ "version": "21.0.0", "factory": "./karma/migration", "description": "Remove any karma configuration files that only contain the default content. The default configuration is automatically available without a specific project file." + }, + "update-workspace-config": { + "version": "22.0.0", + "factory": "./update-workspace-config/migration", + "description": "Update the angular workspace configuration." } } } diff --git a/packages/schematics/angular/migrations/update-workspace-config/migration.ts b/packages/schematics/angular/migrations/update-workspace-config/migration.ts new file mode 100644 index 000000000000..3abd89d35a80 --- /dev/null +++ b/packages/schematics/angular/migrations/update-workspace-config/migration.ts @@ -0,0 +1,47 @@ +/** + * @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 { isJsonObject } from '@angular-devkit/core'; +import { Rule } from '@angular-devkit/schematics'; +import { allTargetOptions, updateWorkspace } from '../../utility/workspace'; +import { Builders, ProjectType } from '../../utility/workspace-models'; + +/** + * Migration to update the angular workspace configuration. + */ +export default function (): Rule { + return updateWorkspace((workspace) => { + for (const project of workspace.projects.values()) { + if (project.extensions['projectType'] !== ProjectType.Application) { + continue; + } + + for (const target of project.targets.values()) { + if ( + target.builder !== Builders.Application && + target.builder !== Builders.BuildApplication + ) { + continue; + } + + for (const [, options] of allTargetOptions(target)) { + const ssr = options['ssr']; + if (!ssr || !isJsonObject(ssr)) { + continue; + } + + const platform = ssr['experimentalPlatform']; + if (platform) { + ssr['platform'] = platform; + delete ssr['experimentalPlatform']; + } + } + } + } + }); +} diff --git a/packages/schematics/angular/migrations/update-workspace-config/migration_spec.ts b/packages/schematics/angular/migrations/update-workspace-config/migration_spec.ts new file mode 100644 index 000000000000..a56247cf3a1a --- /dev/null +++ b/packages/schematics/angular/migrations/update-workspace-config/migration_spec.ts @@ -0,0 +1,103 @@ +/** + * @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 { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType } from '../../utility/workspace-models'; + +describe('Migration to update the angular workspace configuration', () => { + const schematicName = 'update-workspace-config'; + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + }); + + it('should rename experimentalPlatform to platform in application builder', async () => { + const angularConfig = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + build: { + builder: Builders.Application, + options: { + ssr: { + entry: 'src/server.ts', + experimentalPlatform: 'neutral', + }, + }, + configurations: { + production: { + ssr: { + experimentalPlatform: 'node', + }, + }, + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const config = newTree.readJson('/angular.json') as any; + const options = config.projects.app.architect.build.options; + const prodOptions = config.projects.app.architect.build.configurations.production; + + expect(options.ssr.platform).toBe('neutral'); + expect(options.ssr.experimentalPlatform).toBeUndefined(); + expect(prodOptions.ssr.platform).toBe('node'); + expect(prodOptions.ssr.experimentalPlatform).toBeUndefined(); + }); + + it('should not change other builders', async () => { + const angularConfig = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + build: { + builder: Builders.Browser, + options: { + ssr: { + experimentalPlatform: 'neutral', + }, + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const config = newTree.readJson('/angular.json') as any; + const options = config.projects.app.architect.build.options; + + expect(options.ssr.experimentalPlatform).toBe('neutral'); + expect(options.ssr.platform).toBeUndefined(); + }); +}); diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts index 6fdd1998cdd2..dff15d055943 100644 --- a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts @@ -95,7 +95,7 @@ export default async function () { await updateJsonFile('angular.json', (json) => { const buildTarget = json['projects']['test-project']['architect']['build']; const options = buildTarget['options']; - options['ssr']['experimentalPlatform'] = 'neutral'; + options['ssr']['platform'] = 'neutral'; options['outputMode'] = 'server'; options['security'] ??= {}; options['security']['allowedHosts'] = ['localhost'];