build: delete v18 migrations (#57603)

We don't need to ship the migrations for v18 once we're in v19.

PR Close #57603
This commit is contained in:
Kristiyan Kostadinov 2024-08-30 13:11:30 +02:00 committed by Andrew Kushnir
parent e6e5d29e83
commit aa8eb15ddf
19 changed files with 9 additions and 2505 deletions

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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"],
)

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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"],
)

View file

@ -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())]
})
})
})
```

View file

@ -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);
}
}
}

View file

@ -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<string, Set<string>>([
[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<string>,
commonHttpTestingIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>,
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<ts.CallExpression>();
// 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<string>,
commonHttpTestingIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>,
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<string>,
commonHttpTestingIdentifiers: Set<string>,
) {
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),
]);
}

View file

@ -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"],
)

View file

@ -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: `<input [(ngModel)]="a && b"/>`
})
export class MyComp {}
```
#### After
```ts
import {Component} from '@angular/core';
@Component({
template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`
})
export class MyComp {}
```

View file

@ -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<string, AnalyzedFile>, 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<string, AnalyzedFile>) {
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);
});
}

View file

@ -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<string, AnalyzedFile>();
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);
}
}

View file

@ -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<string>();
// 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);
}
}
}
}

View file

@ -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",

View file

@ -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 });`,
);
});
});

View file

@ -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 () => {

View file

@ -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');
});
});

View file

@ -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: \`<input [(ngModel)]="a && b.c"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"/>`',
);
});
it('should migrate a two-way binding with a single unary expression', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="!a.b"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="!a.b" (ngModelChange)="a.b = $event"/>`',
);
});
it('should migrate a two-way binding with a nested unary expression', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="!!!!!!!a.b"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="!!!!!!!a.b" (ngModelChange)="a.b = $event"/>`',
);
});
it('should migrate a two-way binding with a conditional expression', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a ? b : c.d"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a ? b : c.d" (ngModelChange)="a ? b : c.d = $event"/>`',
);
});
it('should migrate multiple inline templates in the same file', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a && b"/>\`
})
class Comp {}
@Component({
template: \`<input [(ngModel)]="a || b"/>\`
})
class Comp2 {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`',
);
expect(content).toContain(
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`',
);
});
it('should migrate an external template', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './comp.html'
})
class Comp {}
`,
);
writeFile(
'/comp.html',
[`<div>`, `hello`, `<span>`, `<input [(ngModel)]="a && b"/>`, `</span>`, `</div>`].join('\n'),
);
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe(
[
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].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',
[`<div>`, `hello`, `<span>`, `<input [(ngModel)]="a && b"/>`, `</span>`, `</div>`].join('\n'),
);
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe(
[
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].join('\n'),
);
});
it('should migrate multiple two-way bindings on the same element', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(foo)]="a && b" bar="123" [(baz)]="!!a"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [foo]="a && b" (fooChange)="a && (b = $event)" ' +
'bar="123" [baz]="!!a" (bazChange)="a = $event"/>`',
);
});
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', '<input [(ngModel)]="a || b"/>');
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe('<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>');
});
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: \`<input [(ngModel)]="a || b"/>\`
})
class Comp {}
}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`',
);
});
it('should preserve a valid expression', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a.b.c"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain('template: `<input [(ngModel)]="a.b.c"/>`');
});
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: \`<input [(ngModel)]="a || b" (ngModelChange)="foo($event)"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [(ngModel)]="a || b" (ngModelChange)="foo($event)"/>`',
);
});
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: \`<input [(ngModel)]="a || b" [ngModel]="foo"/>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain('template: `<input [(ngModel)]="a || b" [ngModel]="foo"/>`');
});
it('should migrate a two-way binding on an ng-template', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<ng-template [(ngModel)]="a && b.c"></ng-template>\`
})
class Comp {}
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<ng-template [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"></ng-template>`',
);
});
});