mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
e6e5d29e83
commit
aa8eb15ddf
19 changed files with 9 additions and 2505 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -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())]
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -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 {}
|
||||
```
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 });`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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>`',
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue