From aa8eb15ddf35f8430fdb7ef627f044d95daa0562 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 30 Aug 2024 13:11:30 +0200 Subject: [PATCH] build: delete v18 migrations (#57603) We don't need to ship the migrations for v18 once we're in v19. PR Close #57603 --- packages/core/schematics/BUILD.bazel | 3 - packages/core/schematics/migrations.json | 18 +- .../migrations/after-render-phase/BUILD.bazel | 33 -- .../migrations/after-render-phase/index.ts | 58 -- .../after-render-phase/migration.ts | 95 ---- .../migrations/http-providers/BUILD.bazel | 33 -- .../migrations/http-providers/README.md | 93 ---- .../migrations/http-providers/index.ts | 59 -- .../migrations/http-providers/utils.ts | 521 ------------------ .../invalid-two-way-bindings/BUILD.bazel | 34 -- .../invalid-two-way-bindings/README.md | 38 -- .../invalid-two-way-bindings/analysis.ts | 118 ---- .../invalid-two-way-bindings/index.ts | 72 --- .../invalid-two-way-bindings/migration.ts | 263 --------- packages/core/schematics/test/BUILD.bazel | 6 - .../test/after_render_phase_spec.ts | 182 ------ .../schematics/test/all-migrations.spec.ts | 14 +- .../schematics/test/http_providers_spec.ts | 478 ---------------- .../test/invalid_two_way_bindings_spec.ts | 396 ------------- 19 files changed, 9 insertions(+), 2505 deletions(-) delete mode 100644 packages/core/schematics/migrations/after-render-phase/BUILD.bazel delete mode 100644 packages/core/schematics/migrations/after-render-phase/index.ts delete mode 100644 packages/core/schematics/migrations/after-render-phase/migration.ts delete mode 100644 packages/core/schematics/migrations/http-providers/BUILD.bazel delete mode 100644 packages/core/schematics/migrations/http-providers/README.md delete mode 100644 packages/core/schematics/migrations/http-providers/index.ts delete mode 100644 packages/core/schematics/migrations/http-providers/utils.ts delete mode 100644 packages/core/schematics/migrations/invalid-two-way-bindings/BUILD.bazel delete mode 100644 packages/core/schematics/migrations/invalid-two-way-bindings/README.md delete mode 100644 packages/core/schematics/migrations/invalid-two-way-bindings/analysis.ts delete mode 100644 packages/core/schematics/migrations/invalid-two-way-bindings/index.ts delete mode 100644 packages/core/schematics/migrations/invalid-two-way-bindings/migration.ts delete mode 100644 packages/core/schematics/test/after_render_phase_spec.ts delete mode 100644 packages/core/schematics/test/http_providers_spec.ts delete mode 100644 packages/core/schematics/test/invalid_two_way_bindings_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index ae6dfe31902..12006deb4d7 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -20,9 +20,6 @@ pkg_npm( validate = False, visibility = ["//packages/core:__pkg__"], deps = [ - "//packages/core/schematics/migrations/after-render-phase:bundle", - "//packages/core/schematics/migrations/http-providers:bundle", - "//packages/core/schematics/migrations/invalid-two-way-bindings:bundle", "//packages/core/schematics/ng-generate/control-flow-migration:bundle", "//packages/core/schematics/ng-generate/inject-migration:bundle", "//packages/core/schematics/ng-generate/route-lazy-loading:bundle", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 1ef729caa9b..63001b44588 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -1,19 +1,3 @@ { - "schematics": { - "invalid-two-way-bindings": { - "version": "18.0.0", - "description": "Updates two-way bindings that have an invalid expression to use the longform expression instead.", - "factory": "./migrations/invalid-two-way-bindings/bundle" - }, - "migration-http-providers": { - "version": "18.0.0", - "description": "Replace deprecated HTTP related modules with provider functions", - "factory": "./migrations/http-providers/bundle" - }, - "migration-after-render-phase": { - "version": "18.1.0", - "description": "Updates calls to afterRender with an explicit phase to the new API", - "factory": "./migrations/after-render-phase/bundle" - } - } + "schematics": {} } diff --git a/packages/core/schematics/migrations/after-render-phase/BUILD.bazel b/packages/core/schematics/migrations/after-render-phase/BUILD.bazel deleted file mode 100644 index b3ab8460a55..00000000000 --- a/packages/core/schematics/migrations/after-render-phase/BUILD.bazel +++ /dev/null @@ -1,33 +0,0 @@ -load("//tools:defaults.bzl", "esbuild_no_sourcemaps", "ts_library") - -package( - default_visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], -) - -ts_library( - name = "after-render-phase", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) - -esbuild_no_sourcemaps( - name = "bundle", - entry_point = ":index.ts", - external = [ - "@angular-devkit/*", - "typescript", - ], - format = "cjs", - platform = "node", - deps = [":after-render-phase"], -) diff --git a/packages/core/schematics/migrations/after-render-phase/index.ts b/packages/core/schematics/migrations/after-render-phase/index.ts deleted file mode 100644 index 46bc036ce1e..00000000000 --- a/packages/core/schematics/migrations/after-render-phase/index.ts +++ /dev/null @@ -1,58 +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.io/license - */ - -import {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics'; -import {relative} from 'path'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; -import {migrateFile} from './migration'; - -export default function (): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run the afterRender phase migration.', - ); - } - - for (const tsconfigPath of allPaths) { - runMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const program = createMigrationProgram(tree, tsconfigPath, basePath); - const sourceFiles = program - .getSourceFiles() - .filter((sourceFile) => canMigrateFile(basePath, sourceFile, program)); - - for (const sourceFile of sourceFiles) { - let update: UpdateRecorder | null = null; - - const rewriter = (startPos: number, width: number, text: string | null) => { - if (update === null) { - // Lazily initialize update, because most files will not require migration. - update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - } - update.remove(startPos, width); - if (text !== null) { - update.insertLeft(startPos, text); - } - }; - migrateFile(sourceFile, program.getTypeChecker(), rewriter); - - if (update !== null) { - tree.commitUpdate(update); - } - } -} diff --git a/packages/core/schematics/migrations/after-render-phase/migration.ts b/packages/core/schematics/migrations/after-render-phase/migration.ts deleted file mode 100644 index 0037e1ae292..00000000000 --- a/packages/core/schematics/migrations/after-render-phase/migration.ts +++ /dev/null @@ -1,95 +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.io/license - */ - -import ts from 'typescript'; -import {ChangeTracker} from '../../utils/change_tracker'; -import { - getImportOfIdentifier, - getImportSpecifier, - getNamedImports, -} from '../../utils/typescript/imports'; - -const CORE = '@angular/core'; -const AFTER_RENDER_PHASE_ENUM = 'AfterRenderPhase'; -const AFTER_RENDER_FNS = new Set(['afterRender', 'afterNextRender']); - -type RewriteFn = (startPos: number, width: number, text: string) => void; - -export function migrateFile( - sourceFile: ts.SourceFile, - typeChecker: ts.TypeChecker, - rewriteFn: RewriteFn, -) { - const changeTracker = new ChangeTracker(ts.createPrinter()); - // Check if there are any imports of the `AfterRenderPhase` enum. - const coreImports = getNamedImports(sourceFile, CORE); - if (!coreImports) { - return; - } - const phaseEnum = getImportSpecifier(sourceFile, CORE, AFTER_RENDER_PHASE_ENUM); - if (!phaseEnum) { - return; - } - - // Remove the `AfterRenderPhase` enum import. - const newCoreImports = ts.factory.updateNamedImports(coreImports, [ - ...coreImports.elements.filter((current) => phaseEnum !== current), - ]); - changeTracker.replaceNode(coreImports, newCoreImports); - ts.forEachChild(sourceFile, function visit(node: ts.Node) { - ts.forEachChild(node, visit); - - // Check if this is a function call of `afterRender` or `afterNextRender`. - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - AFTER_RENDER_FNS.has(getImportOfIdentifier(typeChecker, node.expression)?.name || '') - ) { - let phase: string | undefined; - const [callback, options] = node.arguments; - // Check if any `AfterRenderOptions` options were specified. - if (ts.isObjectLiteralExpression(options)) { - const phaseProp = options.properties.find((p) => p.name?.getText() === 'phase'); - // Check if the `phase` options is set. - if ( - phaseProp && - ts.isPropertyAssignment(phaseProp) && - ts.isPropertyAccessExpression(phaseProp.initializer) && - phaseProp.initializer.expression.getText() === AFTER_RENDER_PHASE_ENUM - ) { - phaseProp.initializer.expression; - phase = phaseProp.initializer.name.getText(); - // Remove the `phase` option. - if (options.properties.length === 1) { - changeTracker.removeNode(options); - } else { - const newOptions = ts.factory.createObjectLiteralExpression( - options.properties.filter((p) => p !== phaseProp), - ); - changeTracker.replaceNode(options, newOptions); - } - } - } - // If we found a phase, update the callback. - if (phase) { - phase = phase.substring(0, 1).toLocaleLowerCase() + phase.substring(1); - const spec = ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment(ts.factory.createIdentifier(phase), callback), - ]); - changeTracker.replaceNode(callback, spec); - } - } - }); - - // Write the changes. - for (const changesInFile of changeTracker.recordChanges().values()) { - for (const change of changesInFile) { - rewriteFn(change.start, change.removeLength ?? 0, change.text); - } - } -} diff --git a/packages/core/schematics/migrations/http-providers/BUILD.bazel b/packages/core/schematics/migrations/http-providers/BUILD.bazel deleted file mode 100644 index cf351f8b227..00000000000 --- a/packages/core/schematics/migrations/http-providers/BUILD.bazel +++ /dev/null @@ -1,33 +0,0 @@ -load("//tools:defaults.bzl", "esbuild_no_sourcemaps", "ts_library") - -package( - default_visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], -) - -ts_library( - name = "http-providers", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - deps = [ - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) - -esbuild_no_sourcemaps( - name = "bundle", - entry_point = ":index.ts", - external = [ - "@angular-devkit/*", - "typescript", - ], - format = "cjs", - platform = "node", - deps = [":http-providers"], -) diff --git a/packages/core/schematics/migrations/http-providers/README.md b/packages/core/schematics/migrations/http-providers/README.md deleted file mode 100644 index b16718aa3f6..00000000000 --- a/packages/core/schematics/migrations/http-providers/README.md +++ /dev/null @@ -1,93 +0,0 @@ -## Replace Http modules from `@angular/common/http` with provider functions - -`HttpClientModule`, `HttpClientXsrfModule`, `HttpClientJsonpModule` are deprecated in favor of `provideHttpClient` and its options. -`HttpClientTestingModule` is deprecated in favor or `provideHttpClientTesting()` - -This migration updates any `@NgModule`, `@Component`, `@Directive` that imports those modules. - -### Http Modules - -#### Before -```ts - -import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule } from '@angular/common/http'; - -@NgModule({ - imports: [CommonModule, HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule] -}) -export class AppModule {} -``` - -#### After -```ts -import { provideHttpClient, withJsonpSupport, withXsrfConfiguration } from '@angular/common/http'; - -@NgModule({ - imports: [CommonModule], - providers: [provideHttpClient(withJsonpSupport(), withXsrfConfiguration())] -}) -export class AppModule {} -``` - -### Testing - -#### Before - -```ts -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -describe('some test', () => { - - it('...', () => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] - }) - }) -}) -``` - -#### After - -```ts -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('some test', () => { - - it('...', () => { - TestBed.configureTestingModule({ - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] - }) - }) -}) -``` - -#### Before - -```ts -import { HttpClientTesting } from '@angular/common/http'; - -describe('some test', () => { - - it('...', () => { - TestBed.configureTestingModule({ - imports: [HttpClientTesting], - }) - }) -}); -``` - -#### After - -```ts -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('some test', () => { - - it('...', () => { - TestBed.configureTestingModule({ - providers: [provideHttpClient(withInterceptorsFromDi())] - }) - }) -}) -``` diff --git a/packages/core/schematics/migrations/http-providers/index.ts b/packages/core/schematics/migrations/http-providers/index.ts deleted file mode 100644 index 91c81433728..00000000000 --- a/packages/core/schematics/migrations/http-providers/index.ts +++ /dev/null @@ -1,59 +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.io/license - */ -import {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics'; -import {relative} from 'path'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {migrateFile} from './utils'; - -export default function (): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run the http providers migration.', - ); - } - - for (const tsconfigPath of allPaths) { - runMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const program = createMigrationProgram(tree, tsconfigPath, basePath); - const sourceFiles = program - .getSourceFiles() - .filter((sourceFile) => canMigrateFile(basePath, sourceFile, program)); - - for (const sourceFile of sourceFiles) { - let update: UpdateRecorder | null = null; - - const rewriter = (startPos: number, width: number, text: string | null) => { - if (update === null) { - // Lazily initialize update, because most files will not require migration. - update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); - } - update.remove(startPos, width); - if (text !== null) { - update.insertLeft(startPos, text); - } - }; - migrateFile(sourceFile, program.getTypeChecker(), rewriter); - - if (update !== null) { - tree.commitUpdate(update); - } - } -} diff --git a/packages/core/schematics/migrations/http-providers/utils.ts b/packages/core/schematics/migrations/http-providers/utils.ts deleted file mode 100644 index 37fcd1fff92..00000000000 --- a/packages/core/schematics/migrations/http-providers/utils.ts +++ /dev/null @@ -1,521 +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.io/license - */ - -import ts from 'typescript'; - -import {ChangeTracker} from '../../utils/change_tracker'; -import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; -import {getImportSpecifiers, getNamedImports} from '../../utils/typescript/imports'; - -const HTTP_CLIENT_MODULE = 'HttpClientModule'; -const HTTP_CLIENT_XSRF_MODULE = 'HttpClientXsrfModule'; -const HTTP_CLIENT_JSONP_MODULE = 'HttpClientJsonpModule'; -const HTTP_CLIENT_TESTING_MODULE = 'HttpClientTestingModule'; -const WITH_INTERCEPTORS_FROM_DI = 'withInterceptorsFromDi'; -const WITH_JSONP_SUPPORT = 'withJsonpSupport'; -const WITH_NOXSRF_PROTECTION = 'withNoXsrfProtection'; -const WITH_XSRF_CONFIGURATION = 'withXsrfConfiguration'; -const PROVIDE_HTTP_CLIENT = 'provideHttpClient'; -const PROVIDE_HTTP_CLIENT_TESTING = 'provideHttpClientTesting'; - -const COMMON_HTTP = '@angular/common/http'; -const COMMON_HTTP_TESTING = '@angular/common/http/testing'; - -const HTTP_MODULES = new Set([ - HTTP_CLIENT_MODULE, - HTTP_CLIENT_XSRF_MODULE, - HTTP_CLIENT_JSONP_MODULE, -]); -const HTTP_TESTING_MODULES = new Set([HTTP_CLIENT_TESTING_MODULE]); - -export type RewriteFn = (startPos: number, width: number, text: string) => void; - -export function migrateFile( - sourceFile: ts.SourceFile, - typeChecker: ts.TypeChecker, - rewriteFn: RewriteFn, -) { - const changeTracker = new ChangeTracker(ts.createPrinter()); - const addedImports = new Map>([ - [COMMON_HTTP, new Set()], - [COMMON_HTTP_TESTING, new Set()], - ]); - - const commonHttpIdentifiers = new Set( - getImportSpecifiers(sourceFile, COMMON_HTTP, [...HTTP_MODULES]).map((specifier) => - specifier.getText(), - ), - ); - const commonHttpTestingIdentifiers = new Set( - getImportSpecifiers(sourceFile, COMMON_HTTP_TESTING, [...HTTP_TESTING_MODULES]).map( - (specifier) => specifier.getText(), - ), - ); - - ts.forEachChild(sourceFile, function visit(node: ts.Node) { - ts.forEachChild(node, visit); - - if (ts.isClassDeclaration(node)) { - const decorators = getAngularDecorators(typeChecker, ts.getDecorators(node) || []); - decorators.forEach((decorator) => { - migrateDecorator( - decorator, - commonHttpIdentifiers, - commonHttpTestingIdentifiers, - addedImports, - changeTracker, - sourceFile, - ); - }); - } - - migrateTestingModuleImports( - node, - commonHttpIdentifiers, - commonHttpTestingIdentifiers, - addedImports, - changeTracker, - ); - }); - - // Imports are for the whole file - // We handle them separately - - // Remove the HttpModules imports from common/http - const commonHttpImports = getNamedImports(sourceFile, COMMON_HTTP); - if (commonHttpImports) { - const symbolImportsToRemove = getImportSpecifiers(sourceFile, COMMON_HTTP, [...HTTP_MODULES]); - - const newImports = ts.factory.updateNamedImports(commonHttpImports, [ - ...commonHttpImports.elements.filter((current) => !symbolImportsToRemove.includes(current)), - ...[...(addedImports.get(COMMON_HTTP) ?? [])].map((entry) => { - return ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(entry), - ); - }), - ]); - changeTracker.replaceNode(commonHttpImports, newImports); - } - // If there are no imports for common/http, and we need to add some - else if (addedImports.get(COMMON_HTTP)?.size) { - // Then we add a new import statement for common/http - addedImports.get(COMMON_HTTP)?.forEach((entry) => { - changeTracker.addImport(sourceFile, entry, COMMON_HTTP); - }); - } - - // Remove the HttpModules imports from common/http/testing - const commonHttpTestingImports = getNamedImports(sourceFile, COMMON_HTTP_TESTING); - if (commonHttpTestingImports) { - const symbolImportsToRemove = getImportSpecifiers(sourceFile, COMMON_HTTP_TESTING, [ - ...HTTP_TESTING_MODULES, - ]); - - const newHttpTestingImports = ts.factory.updateNamedImports(commonHttpTestingImports, [ - ...commonHttpTestingImports.elements.filter( - (current) => !symbolImportsToRemove.includes(current), - ), - ...[...(addedImports.get(COMMON_HTTP_TESTING) ?? [])].map((entry) => { - return ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(entry), - ); - }), - ]); - changeTracker.replaceNode(commonHttpTestingImports, newHttpTestingImports); - } - - // Writing the changes - for (const changesInFile of changeTracker.recordChanges().values()) { - for (const change of changesInFile) { - rewriteFn(change.start, change.removeLength ?? 0, change.text); - } - } -} - -function migrateDecorator( - decorator: NgDecorator, - commonHttpIdentifiers: Set, - commonHttpTestingIdentifiers: Set, - addedImports: Map>, - changeTracker: ChangeTracker, - sourceFile: ts.SourceFile, -) { - // Only @NgModule and @Component support `imports`. - // Also skip decorators with no arguments. - if ( - (decorator.name !== 'NgModule' && decorator.name !== 'Component') || - decorator.node.expression.arguments.length < 1 - ) { - return; - } - - // Does the decorator have any imports? - const metadata = decorator.node.expression.arguments[0]; - if (!ts.isObjectLiteralExpression(metadata)) { - return; - } - - const moduleImports = getImportsProp(metadata); - if (!moduleImports) { - return; - } - - // Does the decorator import any of the HTTP modules? - const importedModules = getImportedHttpModules( - moduleImports, - commonHttpIdentifiers, - commonHttpTestingIdentifiers, - ); - if (!importedModules) { - return; - } - - // HttpClient imported in component is actually a mistake - // Schematics will be no-op but add a TODO - const isComponent = decorator.name === 'Component'; - if (isComponent && importedModules.client) { - const httpClientModuleIdentifier = importedModules.client; - const commentText = - '\n// TODO: `HttpClientModule` should not be imported into a component directly.\n' + - '// Please refactor the code to add `provideHttpClient()` call to the provider list in the\n' + - '// application bootstrap logic and remove the `HttpClientModule` import from this component.\n'; - ts.addSyntheticLeadingComment( - httpClientModuleIdentifier, - ts.SyntaxKind.SingleLineCommentTrivia, - commentText, - true, - ); - changeTracker.insertText(sourceFile, httpClientModuleIdentifier.getStart(), commentText); - return; - } - - const addedProviders = new Set(); - - // Handle the different imported Http modules - const commonHttpAddedImports = addedImports.get(COMMON_HTTP); - commonHttpAddedImports?.add(PROVIDE_HTTP_CLIENT); - if (importedModules.client || importedModules.clientTesting) { - commonHttpAddedImports?.add(WITH_INTERCEPTORS_FROM_DI); - addedProviders.add(createCallExpression(WITH_INTERCEPTORS_FROM_DI)); - } - if (importedModules.clientJsonp) { - commonHttpAddedImports?.add(WITH_JSONP_SUPPORT); - addedProviders.add(createCallExpression(WITH_JSONP_SUPPORT)); - } - if (importedModules.xsrf) { - // HttpClientXsrfModule is the only module with Class methods. - // They correspond to different provider functions - if (importedModules.xsrfOptions === 'disable') { - commonHttpAddedImports?.add(WITH_NOXSRF_PROTECTION); - addedProviders.add(createCallExpression(WITH_NOXSRF_PROTECTION)); - } else { - commonHttpAddedImports?.add(WITH_XSRF_CONFIGURATION); - addedProviders.add( - createCallExpression( - WITH_XSRF_CONFIGURATION, - importedModules.xsrfOptions?.options ? [importedModules.xsrfOptions.options] : [], - ), - ); - } - } - - // Removing the imported Http modules from the imports list - const newImports = ts.factory.createArrayLiteralExpression([ - ...moduleImports.elements.filter( - (item) => - item !== importedModules.client && - item !== importedModules.clientJsonp && - item !== importedModules.xsrf && - item !== importedModules.clientTesting, - ), - ]); - - // Adding the new providers - const providers = getProvidersFromLiteralExpr(metadata); - - // handle the HttpClientTestingModule - let provideHttpClientTestingExpr: ts.CallExpression | undefined; - if (importedModules.clientTesting) { - const commonHttpTestingAddedImports = addedImports.get(COMMON_HTTP_TESTING); - commonHttpTestingAddedImports?.add(PROVIDE_HTTP_CLIENT_TESTING); - provideHttpClientTestingExpr = createCallExpression(PROVIDE_HTTP_CLIENT_TESTING); - } - const provideHttpExpr = createCallExpression(PROVIDE_HTTP_CLIENT, [...addedProviders]); - const providersToAppend = provideHttpClientTestingExpr - ? [provideHttpExpr, provideHttpClientTestingExpr] - : [provideHttpExpr]; - - let newProviders: ts.ArrayLiteralExpression; - if (!providers) { - // No existing providers, we add a property to the literal - newProviders = ts.factory.createArrayLiteralExpression(providersToAppend); - } else { - // We add the provider to the existing provider array - newProviders = ts.factory.updateArrayLiteralExpression( - providers, - ts.factory.createNodeArray( - [...providers.elements, ...providersToAppend], - providers.elements.hasTrailingComma, - ), - ); - } - - // Replacing the existing decorator with the new one (with the new imports and providers) - const newDecoratorArgs = ts.factory.createObjectLiteralExpression([ - ...metadata.properties.filter( - (property) => - property.name?.getText() !== 'imports' && property.name?.getText() !== 'providers', - ), - ts.factory.createPropertyAssignment('imports', newImports), - ts.factory.createPropertyAssignment('providers', newProviders), - ]); - changeTracker.replaceNode(metadata, newDecoratorArgs); -} - -function migrateTestingModuleImports( - node: ts.Node, - commonHttpIdentifiers: Set, - commonHttpTestingIdentifiers: Set, - addedImports: Map>, - changeTracker: ChangeTracker, -) { - // Look for calls to `TestBed.configureTestingModule` with at least one argument. - // TODO: this won't work if `TestBed` is aliased or type cast. - if ( - !ts.isCallExpression(node) || - node.arguments.length < 1 || - !ts.isPropertyAccessExpression(node.expression) || - !ts.isIdentifier(node.expression.expression) || - node.expression.expression.text !== 'TestBed' || - node.expression.name.text !== 'configureTestingModule' - ) { - return; - } - - // Do we have any arguments for configureTestingModule ? - const configureTestingModuleArgs = node.arguments[0]; - if (!ts.isObjectLiteralExpression(configureTestingModuleArgs)) { - return; - } - - // Do we have an imports property with an array ? - const importsArray = getImportsProp(configureTestingModuleArgs); - if (!importsArray) { - return; - } - - const commonHttpAddedImports = addedImports.get(COMMON_HTTP); - - // Does the imports array contain the HttpClientModule? - const httpClient = importsArray.elements.find((elt) => elt.getText() === HTTP_CLIENT_MODULE); - if (httpClient && commonHttpIdentifiers.has(HTTP_CLIENT_MODULE)) { - // We add the imports for provideHttpClient(withInterceptorsFromDi()) - commonHttpAddedImports?.add(PROVIDE_HTTP_CLIENT); - commonHttpAddedImports?.add(WITH_INTERCEPTORS_FROM_DI); - - const newImports = ts.factory.createArrayLiteralExpression([ - ...importsArray.elements.filter((item) => item !== httpClient), - ]); - - const provideHttpClient = createCallExpression(PROVIDE_HTTP_CLIENT, [ - createCallExpression(WITH_INTERCEPTORS_FROM_DI), - ]); - - // Adding the new provider - const providers = getProvidersFromLiteralExpr(configureTestingModuleArgs); - - let newProviders: ts.ArrayLiteralExpression; - if (!providers) { - // No existing providers, we add a property to the literal - newProviders = ts.factory.createArrayLiteralExpression([provideHttpClient]); - } else { - // We add the provider to the existing provider array - newProviders = ts.factory.updateArrayLiteralExpression( - providers, - ts.factory.createNodeArray( - [...providers.elements, provideHttpClient], - providers.elements.hasTrailingComma, - ), - ); - } - - // Replacing the existing configuration with the new one (with the new imports and providers) - const newTestingModuleArgs = updateTestBedConfiguration( - configureTestingModuleArgs, - newImports, - newProviders, - ); - changeTracker.replaceNode(configureTestingModuleArgs, newTestingModuleArgs); - } - - // Does the imports array contain the HttpClientTestingModule? - const httpClientTesting = importsArray.elements.find( - (elt) => elt.getText() === HTTP_CLIENT_TESTING_MODULE, - ); - if (httpClientTesting && commonHttpTestingIdentifiers.has(HTTP_CLIENT_TESTING_MODULE)) { - // We add the imports for provideHttpClient(withInterceptorsFromDi()) and provideHttpClientTesting() - commonHttpAddedImports?.add(PROVIDE_HTTP_CLIENT); - commonHttpAddedImports?.add(WITH_INTERCEPTORS_FROM_DI); - addedImports.get(COMMON_HTTP_TESTING)?.add(PROVIDE_HTTP_CLIENT_TESTING); - - const newImports = ts.factory.createArrayLiteralExpression([ - ...importsArray.elements.filter((item) => item !== httpClientTesting), - ]); - - const provideHttpClient = createCallExpression(PROVIDE_HTTP_CLIENT, [ - createCallExpression(WITH_INTERCEPTORS_FROM_DI), - ]); - const provideHttpClientTesting = createCallExpression(PROVIDE_HTTP_CLIENT_TESTING); - - // Adding the new providers - const providers = getProvidersFromLiteralExpr(configureTestingModuleArgs); - - let newProviders: ts.ArrayLiteralExpression; - if (!providers) { - // No existing providers, we add a property to the literal - newProviders = ts.factory.createArrayLiteralExpression([ - provideHttpClient, - provideHttpClientTesting, - ]); - } else { - // We add the provider to the existing provider array - newProviders = ts.factory.updateArrayLiteralExpression( - providers, - ts.factory.createNodeArray( - [...providers.elements, provideHttpClient, provideHttpClientTesting], - providers.elements.hasTrailingComma, - ), - ); - } - - // Replacing the existing configuration with the new one (with the new imports and providers) - const newTestingModuleArgs = updateTestBedConfiguration( - configureTestingModuleArgs, - newImports, - newProviders, - ); - changeTracker.replaceNode(configureTestingModuleArgs, newTestingModuleArgs); - } -} - -function getImportsProp(literal: ts.ObjectLiteralExpression) { - const properties = literal.properties; - const importProp = properties.find((property) => property.name?.getText() === 'imports'); - if (!importProp || !ts.hasOnlyExpressionInitializer(importProp)) { - return null; - } - - if (ts.isArrayLiteralExpression(importProp.initializer)) { - return importProp.initializer; - } - - return null; -} - -function getProvidersFromLiteralExpr(literal: ts.ObjectLiteralExpression) { - const properties = literal.properties; - const providersProp = properties.find((property) => property.name?.getText() === 'providers'); - if (!providersProp || !ts.hasOnlyExpressionInitializer(providersProp)) { - return null; - } - - if (ts.isArrayLiteralExpression(providersProp.initializer)) { - return providersProp.initializer; - } - - return null; -} - -function getImportedHttpModules( - imports: ts.ArrayLiteralExpression, - commonHttpIdentifiers: Set, - commonHttpTestingIdentifiers: Set, -) { - let client: ts.Identifier | ts.CallExpression | null = null; - let clientJsonp: ts.Identifier | ts.CallExpression | null = null; - let xsrf: ts.Identifier | ts.CallExpression | null = null; - let clientTesting: ts.Identifier | ts.CallExpression | null = null; - - // represents respectively: - // HttpClientXsrfModule.disable() - // HttpClientXsrfModule.withOptions(options) - // base HttpClientXsrfModule - let xsrfOptions: 'disable' | {options: ts.Expression} | null = null; - - // Handling the http modules from @angular/common/http and the http testing module from @angular/common/http/testing - // and skipping the rest - for (const item of imports.elements) { - if (ts.isIdentifier(item)) { - const moduleName = item.getText(); - - // We only care about the modules from @angular/common/http and @angular/common/http/testing - if (!commonHttpIdentifiers.has(moduleName) && !commonHttpTestingIdentifiers.has(moduleName)) { - continue; - } - - if (moduleName === HTTP_CLIENT_MODULE) { - client = item; - } else if (moduleName === HTTP_CLIENT_JSONP_MODULE) { - clientJsonp = item; - } else if (moduleName === HTTP_CLIENT_XSRF_MODULE) { - xsrf = item; - } else if (moduleName === HTTP_CLIENT_TESTING_MODULE) { - clientTesting = item; - } - } else if (ts.isCallExpression(item) && ts.isPropertyAccessExpression(item.expression)) { - const moduleName = item.expression.expression.getText(); - - // We only care about the modules from @angular/common/http - if (!commonHttpIdentifiers.has(moduleName)) { - continue; - } - - if (moduleName === HTTP_CLIENT_XSRF_MODULE) { - xsrf = item; - if (item.expression.getText().includes('withOptions') && item.arguments.length === 1) { - xsrfOptions = {options: item.arguments[0]}; - } else if (item.expression.getText().includes('disable')) { - xsrfOptions = 'disable'; - } - } - } - } - - if (client !== null || clientJsonp !== null || xsrf !== null || clientTesting !== null) { - return {client, clientJsonp, xsrf, xsrfOptions, clientTesting}; - } - - return null; -} - -function createCallExpression(functionName: string, args: ts.Expression[] = []) { - return ts.factory.createCallExpression( - ts.factory.createIdentifier(functionName), - undefined, - args, - ); -} - -function updateTestBedConfiguration( - configureTestingModuleArgs: ts.ObjectLiteralExpression, - newImports: ts.ArrayLiteralExpression, - newProviders: ts.ArrayLiteralExpression, -): ts.ObjectLiteralExpression { - return ts.factory.updateObjectLiteralExpression(configureTestingModuleArgs, [ - ...configureTestingModuleArgs.properties.filter( - (property) => - property.name?.getText() !== 'imports' && property.name?.getText() !== 'providers', - ), - ts.factory.createPropertyAssignment('imports', newImports), - ts.factory.createPropertyAssignment('providers', newProviders), - ]); -} diff --git a/packages/core/schematics/migrations/invalid-two-way-bindings/BUILD.bazel b/packages/core/schematics/migrations/invalid-two-way-bindings/BUILD.bazel deleted file mode 100644 index 58e23012a72..00000000000 --- a/packages/core/schematics/migrations/invalid-two-way-bindings/BUILD.bazel +++ /dev/null @@ -1,34 +0,0 @@ -load("//tools:defaults.bzl", "esbuild_no_sourcemaps", "ts_library") - -package( - default_visibility = [ - "//packages/core/schematics:__pkg__", - "//packages/core/schematics/migrations/google3:__pkg__", - "//packages/core/schematics/test:__pkg__", - ], -) - -ts_library( - name = "invalid-two-way-bindings", - srcs = glob(["**/*.ts"]), - tsconfig = "//packages/core/schematics:tsconfig.json", - deps = [ - "//packages/compiler", - "//packages/core/schematics/utils", - "@npm//@angular-devkit/schematics", - "@npm//@types/node", - "@npm//typescript", - ], -) - -esbuild_no_sourcemaps( - name = "bundle", - entry_point = ":index.ts", - external = [ - "@angular-devkit/*", - "typescript", - ], - format = "cjs", - platform = "node", - deps = [":invalid-two-way-bindings"], -) diff --git a/packages/core/schematics/migrations/invalid-two-way-bindings/README.md b/packages/core/schematics/migrations/invalid-two-way-bindings/README.md deleted file mode 100644 index cc7e074340c..00000000000 --- a/packages/core/schematics/migrations/invalid-two-way-bindings/README.md +++ /dev/null @@ -1,38 +0,0 @@ -## Invalid two-way bindings migration - -Due to a quirk in the template parser, Angular previously allowed some unassignable expressions -to be passed into two-way bindings which may produce incorrect results. This migration will -replace the invalid two-way bindings with their input/output pair while preserving the original -behavior. Note that the migrated expression may not be the original intent of the code as it was -written, but they match what the Angular runtime would've executed. - -The invalid bindings will become errors in a future version of Angular. - -Some examples of invalid expressions include: -* Binary expressions like `[(ngModel)]="a || b"`. Previously Angular would append `= $event` to -the right-hand-side of the expression (e.g. `(ngModelChange)="a || (b = $event)"`). -* Unary expressions like `[(ngModel)]="!a"` which Angular would wrap in a parentheses and execute -(e.g. `(ngModelChange)="!(a = $event)"`). -* Conditional expressions like `[(ngModel)]="a ? b : c"` where Angular would add `= $event` to -the false case, e.g. `(ngModelChange)="a ? b : c = $event"`. - -#### Before -```ts -import {Component} from '@angular/core'; - -@Component({ - template: `` -}) -export class MyComp {} -``` - - -#### After -```ts -import {Component} from '@angular/core'; - -@Component({ - template: `` -}) -export class MyComp {} -``` diff --git a/packages/core/schematics/migrations/invalid-two-way-bindings/analysis.ts b/packages/core/schematics/migrations/invalid-two-way-bindings/analysis.ts deleted file mode 100644 index e8723989d64..00000000000 --- a/packages/core/schematics/migrations/invalid-two-way-bindings/analysis.ts +++ /dev/null @@ -1,118 +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.io/license - */ - -import {dirname, join} from 'path'; -import ts from 'typescript'; - -/** - * Represents a range of text within a file. Omitting the end - * means that it's until the end of the file. - */ -type Range = [start: number, end?: number]; - -/** Represents a file that was analyzed by the migration. */ -export class AnalyzedFile { - private ranges: Range[] = []; - - /** Returns the ranges in the order in which they should be migrated. */ - getSortedRanges(): Range[] { - return this.ranges.slice().sort(([aStart], [bStart]) => bStart - aStart); - } - - /** - * Adds a text range to an `AnalyzedFile`. - * @param path Path of the file. - * @param analyzedFiles Map keeping track of all the analyzed files. - * @param range Range to be added. - */ - static addRange(path: string, analyzedFiles: Map, range: Range): void { - let analysis = analyzedFiles.get(path); - - if (!analysis) { - analysis = new AnalyzedFile(); - analyzedFiles.set(path, analysis); - } - - const duplicate = analysis.ranges.find( - (current) => current[0] === range[0] && current[1] === range[1], - ); - - if (!duplicate) { - analysis.ranges.push(range); - } - } -} - -/** - * Analyzes a source file to find file that need to be migrated and the text ranges within them. - * @param sourceFile File to be analyzed. - * @param analyzedFiles Map in which to store the results. - */ -export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map) { - forEachClass(sourceFile, (node) => { - // Note: we have a utility to resolve the Angular decorators from a class declaration already. - // We don't use it here, because it requires access to the type checker which makes it more - // time-consuming to run internally. - const decorator = ts.getDecorators(node)?.find((dec) => { - return ( - ts.isCallExpression(dec.expression) && - ts.isIdentifier(dec.expression.expression) && - dec.expression.expression.text === 'Component' - ); - }) as (ts.Decorator & {expression: ts.CallExpression}) | undefined; - - const metadata = - decorator && - decorator.expression.arguments.length > 0 && - ts.isObjectLiteralExpression(decorator.expression.arguments[0]) - ? decorator.expression.arguments[0] - : null; - - if (!metadata) { - return; - } - - for (const prop of metadata.properties) { - // All the properties we care about should have static - // names and be initialized to a static string. - if ( - !ts.isPropertyAssignment(prop) || - !ts.isStringLiteralLike(prop.initializer) || - (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name)) - ) { - continue; - } - - switch (prop.name.text) { - case 'template': - // +1/-1 to exclude the opening/closing characters from the range. - AnalyzedFile.addRange(sourceFile.fileName, analyzedFiles, [ - prop.initializer.getStart() + 1, - prop.initializer.getEnd() - 1, - ]); - break; - - case 'templateUrl': - // Leave the end as undefined which means that the range is until the end of the file. - const path = join(dirname(sourceFile.fileName), prop.initializer.text); - AnalyzedFile.addRange(path, analyzedFiles, [0]); - break; - } - } - }); -} - -/** Executes a callback on each class declaration in a file. */ -function forEachClass(sourceFile: ts.SourceFile, callback: (node: ts.ClassDeclaration) => void) { - sourceFile.forEachChild(function walk(node) { - if (ts.isClassDeclaration(node)) { - callback(node); - } - node.forEachChild(walk); - }); -} diff --git a/packages/core/schematics/migrations/invalid-two-way-bindings/index.ts b/packages/core/schematics/migrations/invalid-two-way-bindings/index.ts deleted file mode 100644 index 799a50aba56..00000000000 --- a/packages/core/schematics/migrations/invalid-two-way-bindings/index.ts +++ /dev/null @@ -1,72 +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.io/license - */ - -import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics'; -import {relative} from 'path'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; - -import {analyze, AnalyzedFile} from './analysis'; -import {migrateTemplate} from './migration'; - -export default function (): Rule { - return async (tree: Tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - const basePath = process.cwd(); - const allPaths = [...buildPaths, ...testPaths]; - - if (!allPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run the invalid two-way bindings migration.', - ); - } - - for (const tsconfigPath of allPaths) { - runInvalidTwoWayBindingsMigration(tree, tsconfigPath, basePath); - } - }; -} - -function runInvalidTwoWayBindingsMigration(tree: Tree, tsconfigPath: string, basePath: string) { - const program = createMigrationProgram(tree, tsconfigPath, basePath); - const sourceFiles = program - .getSourceFiles() - .filter((sourceFile) => canMigrateFile(basePath, sourceFile, program)); - const analysis = new Map(); - - for (const sourceFile of sourceFiles) { - analyze(sourceFile, analysis); - } - - for (const [path, file] of analysis) { - const ranges = file.getSortedRanges(); - const relativePath = relative(basePath, path); - - // Don't interrupt the entire migration if a file can't be read. - if (!tree.exists(relativePath)) { - continue; - } - - const content = tree.readText(relativePath); - const update = tree.beginUpdate(relativePath); - - for (const [start, end] of ranges) { - const template = content.slice(start, end); - const length = (end ?? content.length) - start; - const migrated = migrateTemplate(template); - - if (migrated !== null) { - update.remove(start, length); - update.insertLeft(start, migrated); - } - } - - tree.commitUpdate(update); - } -} diff --git a/packages/core/schematics/migrations/invalid-two-way-bindings/migration.ts b/packages/core/schematics/migrations/invalid-two-way-bindings/migration.ts deleted file mode 100644 index 79aec36f0fb..00000000000 --- a/packages/core/schematics/migrations/invalid-two-way-bindings/migration.ts +++ /dev/null @@ -1,263 +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.io/license - */ - -import { - ASTWithSource, - BindingType, - ParsedEventType, - parseTemplate, - ReadKeyExpr, - ReadPropExpr, - TmplAstBoundAttribute, - TmplAstElement, - TmplAstNode, - TmplAstRecursiveVisitor, - TmplAstTemplate, -} from '@angular/compiler'; -import ts from 'typescript'; - -/** - * Migrates a template to replace the invalid usages of two-way bindings with their long form. - * Returns null if no changes had to be made to the file. - * @param template Template to be migrated. - */ -export function migrateTemplate(template: string): string | null { - // Don't attempt to parse templates that don't contain two-way bindings. - if (!template.includes(')]=')) { - return null; - } - - let rootNodes: TmplAstNode[] | null = null; - - try { - const parsed = parseTemplate(template, '', {allowInvalidAssignmentEvents: true}); - - if (parsed.errors === null) { - rootNodes = parsed.nodes; - } - } catch {} - - // Don't migrate invalid templates. - if (rootNodes === null) { - return null; - } - - const visitor = new InvalidTwoWayBindingCollector(); - const bindings = visitor - .collectInvalidBindings(rootNodes) - .sort((a, b) => b.sourceSpan.start.offset - a.sourceSpan.start.offset); - - if (bindings.length === 0) { - return null; - } - - let result = template; - const printer = ts.createPrinter(); - - for (const binding of bindings) { - const valueText = result.slice(binding.value.sourceSpan.start, binding.value.sourceSpan.end); - const outputText = migrateTwoWayEvent(valueText, binding, printer); - - if (outputText === null) { - continue; - } - - const before = result.slice(0, binding.sourceSpan.start.offset); - const after = result.slice(binding.sourceSpan.end.offset); - const inputText = migrateTwoWayInput(binding, valueText); - result = before + inputText + ' ' + outputText + after; - } - - return result; -} - -/** - * Creates the string for the input side of an invalid two-way bindings. - * @param binding Invalid two-way binding to be migrated. - * @param value String value of the binding. - */ -function migrateTwoWayInput(binding: TmplAstBoundAttribute, value: string): string { - return `[${binding.name}]="${value}"`; -} - -/** - * Creates the string for the output side of an invalid two-way bindings. - * @param binding Invalid two-way binding to be migrated. - * @param value String value of the binding. - */ -function migrateTwoWayEvent( - value: string, - binding: TmplAstBoundAttribute, - printer: ts.Printer, -): string | null { - // Note that we use the TypeScript parser, as opposed to our own, because even though we have - // an expression AST here already, our AST is harder to work with in a migration context. - // To use it here, we would have to solve the following: - // 1. Expose the internal converter that turns it from an event AST to an output AST. - // 2. The process of converting to an output AST also transforms some expressions - // (e.g. `foo.bar` becomes `ctx.foo.bar`). We would have to strip away those transformations here - // which introduces room for mistakes. - // 3. We'd still need a way to convert the output AST back into a string. We have such a utility - // for JIT compilation, but it also includes JIT-specific logic we might not want. - // Given these issues and the fact that the kinds of expressions we're migrating is fairly narrow, - // we can get away with using the TypeScript AST instead. - const sourceFile = ts.createSourceFile('temp.ts', value, ts.ScriptTarget.Latest); - const expression = - sourceFile.statements.length === 1 && ts.isExpressionStatement(sourceFile.statements[0]) - ? sourceFile.statements[0].expression - : null; - - if (expression === null) { - return null; - } - - let migrated: ts.Expression | null = null; - - // Historically the expression parser was handling two-way events by appending `=$event` - // to the raw string before attempting to parse it. This has led to bugs over the years (see - // #37809) and to unintentionally supporting unassignable events in the two-way binding. The - // logic below aims to emulate the old behavior. Note that the generated code doesn't necessarily - // make sense based on what the user wrote, for example the event binding for `[(value)]="a ? b : - // c"` would produce `ctx.a ? ctx.b : ctx.c = $event`. We aim to reproduce what the parser used to - // generate before #54154. - if (ts.isBinaryExpression(expression) && isReadExpression(expression.right)) { - // `a && b` -> `a && (b = $event)` - migrated = ts.factory.updateBinaryExpression( - expression, - expression.left, - expression.operatorToken, - wrapInEventAssignment(expression.right), - ); - } else if (ts.isConditionalExpression(expression) && isReadExpression(expression.whenFalse)) { - // `a ? b : c` -> `a ? b : c = $event` - migrated = ts.factory.updateConditionalExpression( - expression, - expression.condition, - expression.questionToken, - expression.whenTrue, - expression.colonToken, - wrapInEventAssignment(expression.whenFalse), - ); - } else if (isPrefixNot(expression)) { - // `!!a` -> `a = $event` - let innerExpression = expression.operand; - while (true) { - if (isPrefixNot(innerExpression)) { - innerExpression = innerExpression.operand; - } else { - if (isReadExpression(innerExpression)) { - migrated = wrapInEventAssignment(innerExpression); - } - - break; - } - } - } - - if (migrated === null) { - return null; - } - - const newValue = printer.printNode(ts.EmitHint.Expression, migrated, sourceFile); - return `(${binding.name}Change)="${newValue}"`; -} - -/** Wraps an expression in an assignment to `$event`, e.g. `foo.bar = $event`. */ -function wrapInEventAssignment(node: ts.Expression): ts.Expression { - return ts.factory.createBinaryExpression( - node, - ts.factory.createToken(ts.SyntaxKind.EqualsToken), - ts.factory.createIdentifier('$event'), - ); -} - -/** - * Checks whether an expression is a valid read expression. Note that identifiers - * are considered read expressions in Angular templates as well. - */ -function isReadExpression( - node: ts.Expression, -): node is ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression { - return ( - ts.isIdentifier(node) || - ts.isPropertyAccessExpression(node) || - ts.isElementAccessExpression(node) - ); -} - -/** Checks whether an expression is in the form of `!x`. */ -function isPrefixNot(node: ts.Expression): node is ts.PrefixUnaryExpression { - return ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken; -} - -/** Traverses a template AST and collects any invalid two-way bindings. */ -class InvalidTwoWayBindingCollector extends TmplAstRecursiveVisitor { - private invalidBindings: TmplAstBoundAttribute[] | null = null; - - collectInvalidBindings(rootNodes: TmplAstNode[]): TmplAstBoundAttribute[] { - const result = (this.invalidBindings = []); - rootNodes.forEach((node) => node.visit(this)); - this.invalidBindings = null; - return result; - } - - override visitElement(element: TmplAstElement): void { - this.visitNodeWithBindings(element); - super.visitElement(element); - } - - override visitTemplate(template: TmplAstTemplate): void { - this.visitNodeWithBindings(template); - super.visitTemplate(template); - } - - private visitNodeWithBindings(node: TmplAstElement | TmplAstTemplate) { - const seenOneWayBindings = new Set(); - - // Collect all of the regular event and input binding - // names so we can easily check for their presence. - for (const output of node.outputs) { - if (output.type === ParsedEventType.Regular) { - seenOneWayBindings.add(output.name); - } - } - - for (const input of node.inputs) { - if (input.type === BindingType.Property) { - seenOneWayBindings.add(input.name); - } - } - - // Make a second pass only over the two-way bindings. - for (const input of node.inputs) { - // Skip over non-two-way bindings or two-way bindings where the user is also binding - // to the input/output side. We can't migrate the latter, because we may end up converting - // something like `[(ngModel)]="invalid" (ngModelChange)="foo()"` to - // `[ngModel]="invalid" (ngModelChange)="invalid = $event" (ngModelChange)="foo()"` which - // would break the app. - if ( - input.type !== BindingType.TwoWay || - seenOneWayBindings.has(input.name) || - seenOneWayBindings.has(input.name + 'Change') - ) { - continue; - } - - let value = input.value; - - if (value instanceof ASTWithSource) { - value = value.ast; - } - - // The only supported expression types are property reads and keyed reads. - if (!(value instanceof ReadPropExpr) && !(value instanceof ReadKeyExpr)) { - this.invalidBindings!.push(input); - } - } - } -} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index d0b17bdcd82..004222dfe88 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -19,12 +19,6 @@ jasmine_node_test( data = [ "//packages/core/schematics:collection.json", "//packages/core/schematics:migrations.json", - "//packages/core/schematics/migrations/after-render-phase", - "//packages/core/schematics/migrations/after-render-phase:bundle", - "//packages/core/schematics/migrations/http-providers", - "//packages/core/schematics/migrations/http-providers:bundle", - "//packages/core/schematics/migrations/invalid-two-way-bindings", - "//packages/core/schematics/migrations/invalid-two-way-bindings:bundle", "//packages/core/schematics/ng-generate/control-flow-migration", "//packages/core/schematics/ng-generate/control-flow-migration:bundle", "//packages/core/schematics/ng-generate/control-flow-migration:static_files", diff --git a/packages/core/schematics/test/after_render_phase_spec.ts b/packages/core/schematics/test/after_render_phase_spec.ts deleted file mode 100644 index ad4b7532a97..00000000000 --- a/packages/core/schematics/test/after_render_phase_spec.ts +++ /dev/null @@ -1,182 +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.io/license - */ - -import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; -import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; -import {HostTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; -import {runfiles} from '@bazel/runfiles'; -import shx from 'shelljs'; - -describe('afterRender phase migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematic('migration-after-render-phase', {}, tree); - } - - beforeEach(() => { - runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile( - '/tsconfig.json', - JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - }), - ); - - writeFile( - '/angular.json', - JSON.stringify({ - version: 1, - projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, - }), - ); - - previousWorkingDir = shx.pwd(); - tmpDirPath = getSystemPath(host.root); - - // Switch into the temporary directory path. This allows us to run - // the schematic against our custom unit test tree. - shx.cd(tmpDirPath); - }); - - it('should update afterRender phase flag', async () => { - writeFile( - '/index.ts', - ` - import { AfterRenderPhase, Directive, afterRender } from '@angular/core'; - - @Directive({ - selector: '[someDirective]' - }) - export class SomeDirective { - constructor() { - afterRender(() => { - console.log('read'); - }, {phase: AfterRenderPhase.Read}); - } - }`, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); - expect(content).not.toContain('AfterRenderPhase'); - expect(content).toContain("import { Directive, afterRender } from '@angular/core';"); - expect(content).toContain(`afterRender({ read: () => { console.log('read'); } }, );`); - }); - - it('should update afterNextRender phase flag', async () => { - writeFile( - '/index.ts', - ` - import { AfterRenderPhase, Directive, afterNextRender } from '@angular/core'; - - @Directive({ - selector: '[someDirective]' - }) - export class SomeDirective { - constructor() { - afterNextRender(() => { - console.log('earlyRead'); - }, {phase: AfterRenderPhase.EarlyRead}); - } - }`, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); - expect(content).not.toContain('AfterRenderPhase'); - expect(content).toContain("import { Directive, afterNextRender } from '@angular/core';"); - expect(content).toContain( - `afterNextRender({ earlyRead: () => { console.log('earlyRead'); } }, );`, - ); - }); - - it('should not update calls that do not specify phase flag', async () => { - const originalContent = ` - import { Directive, Injector, afterRender, afterNextRender, inject } from '@angular/core'; - - @Directive({ - selector: '[someDirective]' - }) - export class SomeDirective { - injector = inject(Injector); - - constructor() { - afterRender(() => { - console.log('default phase'); - }); - afterNextRender(() => { - console.log('default phase'); - }); - afterRender(() => { - console.log('default phase'); - }, {injector: this.injector}); - afterNextRender(() => { - console.log('default phase'); - }, {injector: this.injector}); - } - }`; - writeFile('/index.ts', originalContent); - - await runMigration(); - - const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); - expect(content).toEqual(originalContent.replace(/\s+/g, ' ')); - }); - - it('should not change options other than phase', async () => { - writeFile( - '/index.ts', - ` - import { Directive, Injector, afterRender, AfterRenderPhase, inject } from '@angular/core'; - - @Directive({ - selector: '[someDirective]' - }) - export class SomeDirective { - injector = inject(Injector); - - constructor() { - afterRender(() => { - console.log('earlyRead'); - }, { - phase: AfterRenderPhase.EarlyRead, - injector: this.injector - }); - } - }`, - ); - - await runMigration(); - const content = tree.readContent('/index.ts').replace(/\s+/g, ' '); - expect(content).not.toContain('AfterRenderPhase'); - expect(content).toContain( - "import { Directive, Injector, afterRender, inject } from '@angular/core';", - ); - expect(content).toContain( - `afterRender({ earlyRead: () => { console.log('earlyRead'); } }, { injector: this.injector });`, - ); - }); -}); diff --git a/packages/core/schematics/test/all-migrations.spec.ts b/packages/core/schematics/test/all-migrations.spec.ts index ce16f1d2f59..cc93dccb3c8 100644 --- a/packages/core/schematics/test/all-migrations.spec.ts +++ b/packages/core/schematics/test/all-migrations.spec.ts @@ -62,14 +62,16 @@ describe('all migrations', () => { await runner.runSchematic(migrationName, undefined, tree); } - if (!allMigrationSchematics.length) { - throw Error('No migration schematics found.'); + if (allMigrationSchematics.length) { + allMigrationSchematics.forEach((name) => { + describe(name, () => createTests(name)); + }); + } else { + it('should pass', () => { + expect(true).toBe(true); + }); } - allMigrationSchematics.forEach((name) => { - describe(name, () => createTests(name)); - }); - function createTests(migrationName: string) { // Regression test for: https://github.com/angular/angular/issues/36346. it('should not throw if non-existent symbols are imported with rootDirs', async () => { diff --git a/packages/core/schematics/test/http_providers_spec.ts b/packages/core/schematics/test/http_providers_spec.ts deleted file mode 100644 index d6111c0bad3..00000000000 --- a/packages/core/schematics/test/http_providers_spec.ts +++ /dev/null @@ -1,478 +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.io/license - */ - -import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; -import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; -import {HostTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; -import {runfiles} from '@bazel/runfiles'; -import shx from 'shelljs'; - -describe('Http providers migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematic('migration-http-providers', {}, tree); - } - - beforeEach(() => { - runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile( - '/tsconfig.json', - JSON.stringify({ - compilerOptions: { - lib: ['es2015'], - strictNullChecks: true, - }, - }), - ); - - writeFile( - '/angular.json', - JSON.stringify({ - version: 1, - projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, - }), - ); - - previousWorkingDir = shx.pwd(); - tmpDirPath = getSystemPath(host.root); - - // Switch into the temporary directory path. This allows us to run - // the schematic against our custom unit test tree. - shx.cd(tmpDirPath); - }); - - afterEach(() => { - shx.cd(previousWorkingDir); - shx.rm('-r', tmpDirPath); - }); - - it('should replace HttpClientModule', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http'; - import { CommonModule } from '@angular/common'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [AppComponent], - imports: [ - CommonModule, - HttpClientModule,HttpClientJsonpModule, - RouterModule.forRoot([]), - HttpClientXsrfModule.withOptions({cookieName: 'foobar'}) - ], - }) - export class AppModule {}`, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientXsrfModule`); - expect(content).not.toContain(`HttpClientJsonpModule`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toMatch(/import.*provideHttpClient/); - expect(content).toMatch(/import.*withInterceptorsFromDi/); - expect(content).toMatch(/import.*withJsonpSupport/); - expect(content).toMatch(/import.*withXsrfConfiguration/); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`, - ); - expect(content).toContain(`RouterModule.forRoot([])`); - expect(content).toContain(`declarations: [AppComponent]`); - }); - - it('should replace HttpClientModule with existing providers ', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http'; - - @NgModule({ - imports: [ - CommonModule, - HttpClientModule, - HttpClientJsonpModule, - RouterModule.forRoot([]), - HttpClientXsrfModule.withOptions({cookieName: 'foobar'}) - ], - providers: [provideConfig({ someConfig: 'foobar'})] - }) - export class AppModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientXsrfModule`); - expect(content).not.toContain(`HttpClientJsonpModule`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`, - ); - }); - - it('should replace HttpClientModule & HttpClientXsrfModule.disable()', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http'; - - @NgModule({ - imports: [ - CommonModule, - HttpClientModule, - HttpClientJsonpModule, - RouterModule.forRoot([]), - HttpClientXsrfModule.disable() - ], - providers: [provideConfig({ someConfig: 'foobar'})] - }) - export class AppModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientXsrfModule`); - expect(content).not.toContain(`HttpClientJsonpModule`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withNoXsrfProtection())`, - ); - }); - - it('should replace HttpClientModule & base HttpClientXsrfModule', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http'; - - @NgModule({ - imports: [ - CommonModule, - HttpClientModule, - RouterModule.forRoot([]), - HttpClientXsrfModule - ], - providers: [provideConfig({ someConfig: 'foobar'})] - }) - export class AppModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientXsrfModule`); - expect(content).not.toContain(`HttpClientJsonpModule`); - expect(content).not.toContain(`withJsonpSupport`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi(), withXsrfConfiguration())`, - ); - }); - - it('should handle a migration with 2 modules in the same file ', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http'; - - @NgModule({ - imports: [CommonModule, HttpClientModule, HttpClientJsonpModule], - providers: [provideConfig({ someConfig: 'foobar'})] - }) - export class AppModule {} - - @NgModule({ - imports: [CommonModule, HttpClientModule, HttpClientXsrfModule.disable()], - providers: [provideConfig({ someConfig: 'foobar'})] - }) - export class AppModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientXsrfModule`); - expect(content).not.toContain(`HttpClientJsonpModule`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`); - expect(content).toContain(`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`, - ); - }); - - it('should handle a migration for a component', async () => { - writeFile( - '/index.ts', - ` - import { Component } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http'; - - @Component({ - template: '', - imports: [HttpClientModule,HttpClientJsonpModule], - }) - export class MyComponent {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).toContain(`HttpClientModule`); - expect(content).not.toContain( - `provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`, - ); - expect(content).toContain('// TODO: `HttpClientModule` should not be imported'); - expect(content).toContain(`template: ''`); - }); - - it('should handle a migration of HttpClientModule in a test', async () => { - writeFile( - '/index.ts', - ` - import { HttpClientModule } from '@angular/common/http'; - - describe('MyComponent', () => { - beforeEach(() => - TestBed.configureTestingModule({ - imports: [HttpClientModule] - }) - ); - }); - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).not.toContain(`'@angular/common/http/testing'`); - expect(content).toContain(`'@angular/common/http'`); - expect(content).toMatch(/import.*provideHttpClient.*withInterceptorsFromDi.*from/); - expect(content).not.toContain(`HttpClientModule`); - expect(content).toContain(`provideHttpClient(withInterceptorsFromDi())`); - }); - - it('should not migrate HttpClientModule from another package', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@not-angular/common/http'; - - @NgModule({ - imports: [CommonModule,HttpClientModule,HttpClientJsonpModule], - providers: [provideConfig({ someConfig: 'foobar' })] - }) - export class AppModule {} - - @NgModule({ - imports: [CommonModule,HttpClientModule,HttpClientXsrfModule.disable()], - providers: [provideConfig({ someConfig: 'foobar' })] - }) - export class AppModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@not-angular/common/http`); - expect(content).toContain(`HttpClientModule`); - expect(content).toContain(`HttpClientXsrfModule`); - expect(content).toContain(`HttpClientJsonpModule`); - expect(content).toContain(`HttpTransferCacheOptions`); - expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`); - expect(content).not.toContain( - `provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`, - ); - expect(content).not.toContain( - `provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`, - ); - }); - - it('should migrate HttpClientTestingModule', async () => { - writeFile( - '/index.ts', - ` - import { TestBed } from '@angular/core/testing'; - import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - import { AppComponent } from './app.component'; - - TestBed.configureTestingModule({ - declarations: [AppComponent], - imports: [HttpClientTestingModule], - }); - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`'@angular/common/http/testing'`); - expect(content).toContain(`'@angular/common/http'`); - expect(content).toMatch(/import.*provideHttpClient.*withInterceptorsFromDi.*from/); - expect(content).not.toContain(`HttpClientTestingModule`); - expect(content).toMatch(/import.*provideHttpClientTesting/); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()`, - ); - expect(content).toContain(`declarations: [AppComponent]`); - }); - - it('should not migrate HttpClientTestingModule from outside package', async () => { - writeFile( - '/index.ts', - ` - import { TestBed } from '@angular/core/testing'; - import { HttpClientTestingModule, HttpTestingController } from '@not-angular/common/http/testing'; - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - }); - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@not-angular/common/http/testing`); - expect(content).toContain(`HttpClientTestingModule`); - expect(content).not.toContain('provideHttpClientTesting'); - }); - - it('should migrate NgModule + TestBed.configureTestingModule in the same file', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { TestBed } from '@angular/core/testing'; - import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http'; - - @NgModule({ - template: '', - imports: [HttpClientModule,HttpClientJsonpModule], - }) - export class MyModule {} - - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - }); - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toContain(`@angular/common/http`); - expect(content).toContain(`@angular/common/http/testing`); - expect(content).not.toContain(`HttpClientModule`); - expect(content).not.toContain(`HttpClientTestingModule`); - expect(content).toContain('provideHttpClientTesting'); - expect(content).toContain('provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())'); - expect(content).toContain( - 'provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()', - ); - - expect(content).toContain( - `import { provideHttpClient, withInterceptorsFromDi, withJsonpSupport } from '@angular/common/http';`, - ); - expect(content).toContain( - `import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';`, - ); - }); - - it('should migrate HttpClientTestingModule in NgModule', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { TestBed } from '@angular/core/testing'; - import { HttpClientTestingModule } from '@angular/common/http/testing'; - - @NgModule({ - declarations: [AppComponent], - imports: [HttpClientTestingModule], - }) - export class TestModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).toMatch(/import.*provideHttpClient.*withInterceptorsFromDi.*from/); - expect(content).not.toContain(`HttpClientTestingModule`); - expect(content).toMatch(/import.*provideHttpClientTesting/); - expect(content).toContain( - `provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()`, - ); - expect(content).toContain(`declarations: [AppComponent]`); - }); - - it('should not change a decorator with no arguments', async () => { - writeFile( - '/index.ts', - ` - import { NgModule } from '@angular/core'; - import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http'; - - @NgModule() - export class MyModule {} - `, - ); - - await runMigration(); - - const content = tree.readContent('/index.ts'); - expect(content).not.toContain('HttpClientModule'); - expect(content).not.toContain('provideHttpClient'); - }); -}); diff --git a/packages/core/schematics/test/invalid_two_way_bindings_spec.ts b/packages/core/schematics/test/invalid_two_way_bindings_spec.ts deleted file mode 100644 index 02bd22429bb..00000000000 --- a/packages/core/schematics/test/invalid_two_way_bindings_spec.ts +++ /dev/null @@ -1,396 +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.io/license - */ - -import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; -import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; -import {HostTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; -import {runfiles} from '@bazel/runfiles'; -import shx from 'shelljs'; - -describe('Invalid two-way bindings migration', () => { - let runner: SchematicTestRunner; - let host: TempScopedNodeJsSyncHost; - let tree: UnitTestTree; - let tmpDirPath: string; - let previousWorkingDir: string; - - function writeFile(filePath: string, contents: string) { - host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); - } - - function runMigration() { - return runner.runSchematic('invalid-two-way-bindings', {}, tree); - } - - beforeEach(() => { - runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json')); - host = new TempScopedNodeJsSyncHost(); - tree = new UnitTestTree(new HostTree(host)); - - writeFile('/tsconfig.json', '{}'); - writeFile( - '/angular.json', - JSON.stringify({ - version: 1, - projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, - }), - ); - - previousWorkingDir = shx.pwd(); - tmpDirPath = getSystemPath(host.root); - - // Switch into the temporary directory path. This allows us to run - // the schematic against our custom unit test tree. - shx.cd(tmpDirPath); - }); - - afterEach(() => { - shx.cd(previousWorkingDir); - shx.rm('-r', tmpDirPath); - }); - - it('should migrate a two-way binding with a binary expression', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should migrate a two-way binding with a single unary expression', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should migrate a two-way binding with a nested unary expression', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should migrate a two-way binding with a conditional expression', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should migrate multiple inline templates in the same file', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - - @Component({ - template: \`\` - }) - class Comp2 {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should migrate an external template', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './comp.html' - }) - class Comp {} - `, - ); - - writeFile( - '/comp.html', - [`
`, `hello`, ``, ``, ``, `
`].join('\n'), - ); - - await runMigration(); - const content = tree.readContent('/comp.html'); - - expect(content).toBe( - [ - `
`, - `hello`, - ``, - ``, - ``, - `
`, - ].join('\n'), - ); - }); - - it('should migrate a template referenced by multiple components', async () => { - writeFile( - '/comp-a.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './comp.html' - }) - class CompA {} - `, - ); - - writeFile( - '/comp-b.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './comp.html' - }) - class CompB {} - `, - ); - - writeFile( - '/comp.html', - [`
`, `hello`, ``, ``, ``, `
`].join('\n'), - ); - - await runMigration(); - const content = tree.readContent('/comp.html'); - - expect(content).toBe( - [ - `
`, - `hello`, - ``, - ``, - ``, - `
`, - ].join('\n'), - ); - }); - - it('should migrate multiple two-way bindings on the same element', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should not stop the migration if a file cannot be read', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './does-not-exist.html' - }) - class BrokenComp {} - `, - ); - - writeFile( - '/other-comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - templateUrl: './comp.html' - }) - class Comp {} - `, - ); - - writeFile('/comp.html', ''); - - await runMigration(); - const content = tree.readContent('/comp.html'); - - expect(content).toBe(''); - }); - - it('should migrate a component that is not at the top level', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - function foo() { - @Component({ - template: \`\` - }) - class Comp {} - } - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - - expect(content).toContain( - 'template: ``', - ); - }); - - it('should preserve a valid expression', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain('template: ``'); - }); - - it('should not migrate an invalid expression if an event listener for the same binding exists', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); - - it('should not migrate an invalid expression if a property binding for the same binding exists', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain('template: ``'); - }); - - it('should migrate a two-way binding on an ng-template', async () => { - writeFile( - '/comp.ts', - ` - import {Component} from '@angular/core'; - - @Component({ - template: \`\` - }) - class Comp {} - `, - ); - - await runMigration(); - const content = tree.readContent('/comp.ts'); - expect(content).toContain( - 'template: ``', - ); - }); -});