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 1facd525d53d..af06c813e3dd 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -444,14 +444,12 @@ function getEsBuildCommonPolyfillsOptions( namespace, cache: sourceFileCache?.loadResultCache, loadContent: async (_, build) => { - let hasLocalizePolyfill = false; let polyfillPaths = polyfills; let warnings: PartialMessage[] | undefined; if (tryToResolvePolyfillsAsRelative) { polyfillPaths = await Promise.all( polyfills.map(async (path) => { - hasLocalizePolyfill ||= path.startsWith('@angular/localize'); if (path.startsWith('zone.js') || !extname(path)) { return path; } @@ -465,32 +463,6 @@ function getEsBuildCommonPolyfillsOptions( return result.path ? potentialPathRelative : path; }), ); - } else { - hasLocalizePolyfill = polyfills.some((p) => p.startsWith('@angular/localize')); - } - - // Add localize polyfill if needed. - // TODO: remove in version 19 or later. - if (!i18nOptions.shouldInline && !hasLocalizePolyfill) { - const result = await build.resolve('@angular/localize', { - kind: 'import-statement', - resolveDir: workspaceRoot, - }); - - if (result.path) { - polyfillPaths.push('@angular/localize/init'); - - (warnings ??= []).push({ - text: 'Polyfill for "@angular/localize/init" was added automatically.', - notes: [ - { - text: - 'In the future, this functionality will be removed. ' + - 'Please add this polyfill in the "polyfills" section of your "angular.json" instead.', - }, - ], - }); - } } // Generate module contents with an import statement per defined polyfill diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 825081ab4fce..0c2b31ad1ae5 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -1,11 +1,16 @@ { "schematics": { "use-application-builder": { - "version": "18.0.0", + "version": "19.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, "documentation": "tools/cli/build-system-migration" + }, + "update-workspace-config": { + "version": "19.0.0", + "factory": "./update-workspace-config/migration", + "description": "Update the workspace configuration by replacing deprecated options in 'angular.json' for compatibility with the latest Angular CLI changes." } } } 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..1668e5d2e044 --- /dev/null +++ b/packages/schematics/angular/migrations/update-workspace-config/migration.ts @@ -0,0 +1,61 @@ +/** + * @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 { Rule } from '@angular-devkit/schematics'; +import { allTargetOptions, updateWorkspace } from '../../utility/workspace'; +import { Builders, ProjectType } from '../../utility/workspace-models'; + +/** + * Main entry point for the migration rule. + * + * This migration performs the following tasks: + * - Loops through all application projects in the workspace. + * - Identifies the build target for each application. + * - If the `localize` option is enabled but the polyfill `@angular/localize/init` is not present, + * it adds the polyfill to the `polyfills` option of the build target. + * + * This migration is specifically for application projects that use either the `application` or `browser-esbuild` builders. + */ +export default function (): Rule { + return updateWorkspace((workspace) => { + for (const project of workspace.projects.values()) { + if (project.extensions.projectType !== ProjectType.Application) { + continue; + } + + const buildTarget = project.targets.get('build'); + if ( + !buildTarget || + (buildTarget.builder !== Builders.BuildApplication && + buildTarget.builder !== Builders.Application && + buildTarget.builder !== Builders.BrowserEsbuild) + ) { + continue; + } + + const polyfills = buildTarget.options?.['polyfills']; + if ( + Array.isArray(polyfills) && + polyfills.some( + (polyfill) => typeof polyfill === 'string' && polyfill.startsWith('@angular/localize'), + ) + ) { + // Skip the polyfill is already added + continue; + } + + for (const [, options] of allTargetOptions(buildTarget, false)) { + if (options['localize']) { + buildTarget.options ??= {}; + ((buildTarget.options['polyfills'] ??= []) as string[]).push('@angular/localize/init'); + break; + } + } + } + }); +} 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..fb4f12715140 --- /dev/null +++ b/packages/schematics/angular/migrations/update-workspace-config/migration_spec.ts @@ -0,0 +1,92 @@ +/** + * @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 { ProjectType } from '../../utility/workspace-models'; + +function createWorkSpaceConfig(tree: UnitTestTree) { + const angularConfig = { + version: 1, + projects: { + app: { + root: '/project/app', + sourceRoot: '/project/app/src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + build: { + builder: '@angular/build:application', + options: { + localize: true, + polyfills: [], + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); +} + +describe(`Migration to update the 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()); + createWorkSpaceConfig(tree); + }); + + it(`should add '@angular/localize/init' to polyfills if localize is enabled`, async () => { + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = newTree.readJson('/angular.json') as any; + + expect(app.architect.build.options.polyfills).toContain('@angular/localize/init'); + }); + + it(`should not add '@angular/localize/init' to polyfills if it already exists`, async () => { + // Add '@angular/localize/init' manually + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const config = tree.readJson('/angular.json') as any; + config.projects.app.architect.build.options.polyfills.push('@angular/localize/init'); + tree.overwrite('/angular.json', JSON.stringify(config, undefined, 2)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = newTree.readJson('/angular.json') as any; + + const polyfills = app.architect.build.options.polyfills; + expect(polyfills.filter((p: string) => p === '@angular/localize/init').length).toBe(1); + }); + + it(`should not add polyfills if localize is not enabled`, async () => { + // Disable 'localize' + const config = JSON.parse(tree.readContent('/angular.json')); + config.projects.app.architect.build.options.localize = false; + tree.overwrite('/angular.json', JSON.stringify(config, undefined, 2)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = newTree.readJson('/angular.json') as any; + + expect(app.architect.build.options.polyfills).not.toContain('@angular/localize/init'); + }); +});