diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index d5306f9ca76a..14815a63d5a6 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -1,22 +1,12 @@ { "schematics": { "use-application-builder": { - "version": "19.0.0", + "version": "20.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, "recommended": 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." - }, - "update-ssr-imports": { - "version": "19.0.0", - "factory": "./update-ssr-imports/migration", - "description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected." } } } diff --git a/packages/schematics/angular/migrations/update-ssr-imports/migration.ts b/packages/schematics/angular/migrations/update-ssr-imports/migration.ts deleted file mode 100644 index 7e17888a1c54..000000000000 --- a/packages/schematics/angular/migrations/update-ssr-imports/migration.ts +++ /dev/null @@ -1,97 +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 { DirEntry, Rule, UpdateRecorder } from '@angular-devkit/schematics'; -import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import { getPackageJsonDependency } from '../../utility/dependencies'; - -function* visit(directory: DirEntry): IterableIterator { - for (const path of directory.subfiles) { - if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { - const entry = directory.file(path); - if (entry) { - const content = entry.content; - if (content.includes('CommonEngine') && !content.includes('@angular/ssr/node')) { - const source = ts.createSourceFile( - entry.path, - content.toString().replace(/^\uFEFF/, ''), - ts.ScriptTarget.Latest, - true, - ); - - yield source; - } - } - } - } - - for (const path of directory.subdirs) { - if (path === 'node_modules' || path.startsWith('.')) { - continue; - } - - yield* visit(directory.dir(path)); - } -} - -/** - * Schematics rule that identifies and updates import declarations in TypeScript files. - * Specifically, it modifies imports of '@angular/ssr' by appending '/node' if the - * `CommonEngine` is used from the old entry point. - * - */ -export default function (): Rule { - return (tree) => { - if (!getPackageJsonDependency(tree, '@angular/ssr')) { - return; - } - - for (const sourceFile of visit(tree.root)) { - let recorder: UpdateRecorder | undefined; - - const allImportDeclarations = sourceFile.statements.filter((n) => ts.isImportDeclaration(n)); - if (allImportDeclarations.length === 0) { - continue; - } - - const ssrImports = allImportDeclarations.filter( - (n) => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === '@angular/ssr', - ); - for (const ssrImport of ssrImports) { - const ssrNamedBinding = getNamedImports(ssrImport); - if (ssrNamedBinding) { - const isUsingOldEntryPoint = ssrNamedBinding.elements.some((e) => - e.name.text.startsWith('CommonEngine'), - ); - - if (!isUsingOldEntryPoint) { - continue; - } - - recorder ??= tree.beginUpdate(sourceFile.fileName); - recorder.insertRight(ssrImport.moduleSpecifier.getEnd() - 1, '/node'); - } - } - - if (recorder) { - tree.commitUpdate(recorder); - } - } - }; -} - -function getNamedImports( - importDeclaration: ts.ImportDeclaration | undefined, -): ts.NamedImports | undefined { - const namedBindings = importDeclaration?.importClause?.namedBindings; - if (namedBindings && ts.isNamedImports(namedBindings)) { - return namedBindings; - } - - return undefined; -} diff --git a/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts b/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts deleted file mode 100644 index 9c8919b0febe..000000000000 --- a/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts +++ /dev/null @@ -1,75 +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 { tags } from '@angular-devkit/core'; -import { EmptyTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; - -describe('CommonEngine migration', () => { - const schematicRunner = new SchematicTestRunner( - 'migrations', - require.resolve('../migration-collection.json'), - ); - - let tree: UnitTestTree; - beforeEach(() => { - tree = new UnitTestTree(new EmptyTree()); - tree.create( - 'package.json', - JSON.stringify({ - dependencies: { - '@angular/ssr': '0.0.0', - }, - }), - ); - }); - - function runMigration(): Promise { - return schematicRunner.runSchematic('update-ssr-imports', {}, tree); - } - - it(`should replace 'CommonEngine*' imports from '@angular/ssr' to '@angular/ssr/node'`, async () => { - tree.create( - '/index.ts', - tags.stripIndents` - import { CommonEngine } from '@angular/ssr'; - import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr'; - `, - ); - - const newTree = await runMigration(); - expect(newTree.readContent('/index.ts')).toBe(tags.stripIndents` - import { CommonEngine } from '@angular/ssr/node'; - import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr/node'; - `); - }); - - it(`should not replace 'CommonEngine*' imports from '@angular/ssr/node'`, async () => { - const input = tags.stripIndents` - import { CommonEngine } from '@angular/ssr/node'; - import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr/node'; - `; - - tree.create('/index.ts', input); - - const newTree = await runMigration(); - expect(newTree.readContent('/index.ts')).toBe(input); - }); - - it(`should not replace 'CommonEngine*' imports from other package`, async () => { - const input = tags.stripIndents` - import { CommonEngine } from 'unknown'; - import type { CommonEngineOptions, CommonEngineRenderOptions } from 'unknown'; - `; - - tree.create('/index.ts', input); - - const newTree = await runMigration(); - expect(newTree.readContent('/index.ts')).toBe(input); - }); -}); diff --git a/packages/schematics/angular/migrations/update-workspace-config/migration.ts b/packages/schematics/angular/migrations/update-workspace-config/migration.ts deleted file mode 100644 index 852a4065101c..000000000000 --- a/packages/schematics/angular/migrations/update-workspace-config/migration.ts +++ /dev/null @@ -1,89 +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 { 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 schematic migration performs updates to the Angular workspace configuration - * to ensure that application projects are properly configured with polyfills - * required for internationalization (`localize`). - * - * It specifically targets application projects that use either the `application` - * or `browser-esbuild` builders. - * - * The migration process involves: - * - * 1. Iterating over all projects in the workspace. - * 2. Checking each project to determine if it is an application-type project. - * 3. For each application project, examining the associated build targets. - * 4. If a build target's `localize` option is enabled but the polyfill - * `@angular/localize/init` is missing from the `polyfills` array, the polyfill - * is automatically added to ensure proper internationalization support. - * - * Additionally, this migration updates projects that use the `dev-server` or `extract-i18n` - * builders to ensure that deprecated `browserTarget` options are migrated to the - * newer `buildTarget` field. - * - */ -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.DevServer || target.builder === Builders.ExtractI18n) { - // Migrate `browserTarget` to `buildTarget` - - for (const [, options] of allTargetOptions(target, false)) { - if (options['browserTarget'] && !options['buildTarget']) { - options['buildTarget'] = options['browserTarget']; - } - - delete options['browserTarget']; - } - } - - // Check if the target uses application-related builders - if ( - target.builder !== Builders.BuildApplication && - target.builder !== Builders.Application && - target.builder !== Builders.BrowserEsbuild - ) { - continue; - } - - // Check if polyfills include '@angular/localize/init' - const polyfills = target.options?.['polyfills']; - if ( - Array.isArray(polyfills) && - polyfills.some( - (polyfill) => typeof polyfill === 'string' && polyfill.startsWith('@angular/localize'), - ) - ) { - // Skip if the polyfill is already present - continue; - } - - // Add '@angular/localize/init' polyfill if localize option is enabled - for (const [, options] of allTargetOptions(target, false)) { - if (options['localize']) { - target.options ??= {}; - ((target.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 deleted file mode 100644 index fb4f12715140..000000000000 --- a/packages/schematics/angular/migrations/update-workspace-config/migration_spec.ts +++ /dev/null @@ -1,92 +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 { 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'); - }); -});