Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/bazel/test/ng_package/core_package.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ describe('@angular/core ng_package', () => {
node: './fesm2015/testing.mjs',
default: './fesm2020/testing.mjs',
},
'./schematics/*': {
'default': './schematics/*.js',
},
}
}));
});
Expand Down
5 changes: 5 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"engines": {
"node": "^12.14.1 || >=14.0.0"
},
"exports": {
"./schematics/*": {
"default": "./schematics/*.js"
}
},
"dependencies": {
"tslib": "^2.3.0"
},
Expand Down
5 changes: 4 additions & 1 deletion packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ exports_files([

pkg_npm(
name = "npm_package",
srcs = ["migrations.json"],
srcs = [
"migrations.json",
"package.json",
],
visibility = ["//packages/core:__pkg__"],
deps = [
"//packages/core/schematics/migrations/abstract-control-parent",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/schematics/migrations/google3/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ ts_library(
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/core/schematics/migrations/activated-route-snapshot-fragment",
"//packages/core/schematics/migrations/can-activate-with-redirect-to",
"//packages/core/schematics/migrations/dynamic-queries",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/

import {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations';
import {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter';
import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
import {RuleFailure, Rules} from 'tslint';
import ts from 'typescript';

Expand Down Expand Up @@ -34,7 +39,15 @@ export class Rule extends Rules.TypedRule {
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));

const {resolvedModules, resolvedDirectives} = definitionCollector;
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder);
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder, {
Reference,
DynamicValue,
PartialEvaluator,
StaticInterpreter,
TypeScriptReflectionHost,
forwardRefResolver,
reflectObjectLiteral,
});
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();

[...transformer.migrateModules(resolvedModules),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* 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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations';
import {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
import {DynamicValue, PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import {StaticInterpreter} from '@angular/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter';
import {reflectObjectLiteral, TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
import {RuleFailure, Rules} from 'tslint';
import ts from 'typescript';
import {TslintUpdateRecorder} from '../undecorated-classes-with-decorated-fields/google3/tslint_update_recorder';
Expand All @@ -23,7 +27,15 @@ export class Rule extends Rules.TypedRule {
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
const transform =
new UndecoratedClassesWithDecoratedFieldsTransform(typeChecker, getUpdateRecorder);
new UndecoratedClassesWithDecoratedFieldsTransform(typeChecker, getUpdateRecorder, {
Reference,
DynamicValue,
PartialEvaluator,
StaticInterpreter,
TypeScriptReflectionHost,
forwardRefResolver,
reflectObjectLiteral,
});

// Migrate all source files in the project.
transform.migrate(sourceFiles);
Expand Down
23 changes: 20 additions & 3 deletions packages/core/schematics/migrations/missing-injectable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {relative} from 'path';
import ts from 'typescript';

import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
import {NgDefinitionCollector} from './definition_collector';
Expand All @@ -28,8 +30,20 @@ export default function(): Rule {
'which don\'t have that decorator set.');
}

let compilerCliMigrationsModule;
try {
// Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import
// workaround. Once TypeScript provides support for keeping the dynamic import this workaround
// can be changed to a direct dynamic import.
compilerCliMigrationsModule = await loadCompilerCliMigrationsModule();
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`);
}

for (const tsconfigPath of [...buildPaths, ...testPaths]) {
failures.push(...runMissingInjectableMigration(tree, tsconfigPath, basePath));
failures.push(...runMissingInjectableMigration(
tree, tsconfigPath, basePath, compilerCliMigrationsModule));
}

if (failures.length) {
Expand All @@ -41,7 +55,9 @@ export default function(): Rule {
}

function runMissingInjectableMigration(
tree: Tree, tsconfigPath: string, basePath: string): string[] {
tree: Tree, tsconfigPath: string, basePath: string,
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')):
string[] {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const failures: string[] = [];
const typeChecker = program.getTypeChecker();
Expand All @@ -53,7 +69,8 @@ function runMissingInjectableMigration(
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));

const {resolvedModules, resolvedDirectives} = definitionCollector;
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder);
const transformer =
new MissingInjectableTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule);
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();

[...transformer.migrateModules(resolvedModules),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,69 @@
* found in the LICENSE file at https://angular.io/license
*/

import {forwardRefResolver, ResolvedValue, StaticInterpreter} from '@angular/compiler-cli/private/migrations';
import ts from 'typescript';

import type {ResolvedValue, TypeScriptReflectionHost} from '@angular/compiler-cli/private/migrations';

export interface ProviderLiteral {
node: ts.ObjectLiteralExpression;
resolvedValue: ResolvedValue;
}

/**
* Providers evaluator that extends the ngtsc static interpreter. This is necessary because
* the static interpreter by default only exposes the resolved value, but we are also interested
* in the TypeScript nodes that declare providers. It would be possible to manually traverse the
* AST to collect these nodes, but that would mean that we need to re-implement the static
* interpreter in order to handle all possible scenarios. (e.g. spread operator, function calls,
* callee scope). This can be avoided by simply extending the static interpreter and intercepting
* the "visitObjectLiteralExpression" method.
* A factory function to create an evaluator for providers. This is required to be a
* factory function because the underlying class extends a class that is only available
* from within a dynamically imported module (`@angular/compiler-cli/private/migrations`)
* and is therefore not available at module evaluation time.
*/
export class ProvidersEvaluator extends StaticInterpreter {
private _providerLiterals: ProviderLiteral[] = [];
export function createProvidersEvaluator(
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations'),
host: TypeScriptReflectionHost, checker: ts.TypeChecker): {
evaluate:
(expr: ts.Expression) => {
resolvedValue: ResolvedValue, literals: ProviderLiteral[]
}
} {
/**
* Providers evaluator that extends the ngtsc static interpreter. This is necessary because
* the static interpreter by default only exposes the resolved value, but we are also interested
* in the TypeScript nodes that declare providers. It would be possible to manually traverse the
* AST to collect these nodes, but that would mean that we need to re-implement the static
* interpreter in order to handle all possible scenarios. (e.g. spread operator, function calls,
* callee scope). This can be avoided by simply extending the static interpreter and intercepting
* the "visitObjectLiteralExpression" method.
*/
class ProvidersEvaluator extends compilerCliMigrationsModule.StaticInterpreter {
private _providerLiterals: ProviderLiteral[] = [];

override visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: any) {
const resolvedValue =
super.visitObjectLiteralExpression(node, {...context, insideProviderDef: true});
// do not collect nested object literals. e.g. a provider could use a
// spread assignment (which resolves to another object literal). In that
// case the referenced object literal is not a provider object literal.
if (!context.insideProviderDef) {
this._providerLiterals.push({node, resolvedValue});
override visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: any) {
const resolvedValue =
super.visitObjectLiteralExpression(node, {...context, insideProviderDef: true});
// do not collect nested object literals. e.g. a provider could use a
// spread assignment (which resolves to another object literal). In that
// case the referenced object literal is not a provider object literal.
if (!context.insideProviderDef) {
this._providerLiterals.push({node, resolvedValue});
}
return resolvedValue;
}
return resolvedValue;
}

/**
* Evaluates the given expression and returns its statically resolved value
* and a list of object literals which define Angular providers.
*/
evaluate(expr: ts.Expression) {
this._providerLiterals = [];
const resolvedValue = this.visit(expr, {
originatingFile: expr.getSourceFile(),
absoluteModuleName: null,
resolutionContext: expr.getSourceFile().fileName,
scope: new Map(),
foreignFunctionResolver: forwardRefResolver
});
return {resolvedValue, literals: this._providerLiterals};
/**
* Evaluates the given expression and returns its statically resolved value
* and a list of object literals which define Angular providers.
*/
evaluate(expr: ts.Expression) {
this._providerLiterals = [];
const resolvedValue = this.visit(expr, {
originatingFile: expr.getSourceFile(),
absoluteModuleName: null,
resolutionContext: expr.getSourceFile().fileName,
scope: new Map(),
foreignFunctionResolver: compilerCliMigrationsModule.forwardRefResolver
});
return {resolvedValue, literals: this._providerLiterals};
}
}

return new ProvidersEvaluator(host, checker, /* dependencyTracker */ null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/

import {DynamicValue, Reference, ResolvedValue, TypeScriptReflectionHost} from '@angular/compiler-cli/private/migrations';
import type {ResolvedValue} from '@angular/compiler-cli/private/migrations';
import ts from 'typescript';

import {ImportManager} from '../../utils/import_manager';
import {getAngularDecorators} from '../../utils/ng_decorators';

import {ResolvedDirective, ResolvedNgModule} from './definition_collector';
import {ProviderLiteral, ProvidersEvaluator} from './providers_evaluator';
import {createProvidersEvaluator, ProviderLiteral} from './providers_evaluator';
import {UpdateRecorder} from './update_recorder';

/**
Expand All @@ -33,7 +33,7 @@ export interface AnalysisFailure {
export class MissingInjectableTransform {
private printer = ts.createPrinter();
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
private providersEvaluator: ProvidersEvaluator;
private providersEvaluator;

/** Set of provider class declarations which were already checked or migrated. */
private visitedProviderClasses = new Set<ts.ClassDeclaration>();
Expand All @@ -43,9 +43,12 @@ export class MissingInjectableTransform {

constructor(
private typeChecker: ts.TypeChecker,
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {
this.providersEvaluator = new ProvidersEvaluator(
new TypeScriptReflectionHost(typeChecker), typeChecker, /* dependencyTracker */ null);
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
private compilerCliMigrationsModule:
typeof import('@angular/compiler-cli/private/migrations')) {
this.providersEvaluator = createProvidersEvaluator(
compilerCliMigrationsModule,
new compilerCliMigrationsModule.TypeScriptReflectionHost(typeChecker), typeChecker);
}

recordChanges() {
Expand Down Expand Up @@ -214,7 +217,8 @@ export class MissingInjectableTransform {
*/
private _visitProviderResolvedValue(value: ResolvedValue, module: ResolvedNgModule):
AnalysisFailure[] {
if (value instanceof Reference && ts.isClassDeclaration(value.node)) {
if (value instanceof this.compilerCliMigrationsModule.Reference &&
ts.isClassDeclaration(value.node)) {
this.migrateProviderClass(value.node, module);
} else if (value instanceof Map) {
// If a "ClassProvider" has the "deps" property set, then we do not need to
Expand All @@ -227,7 +231,7 @@ export class MissingInjectableTransform {
return value.reduce(
(res, v) => res.concat(this._visitProviderResolvedValue(v, module)),
[] as AnalysisFailure[]);
} else if (value instanceof DynamicValue) {
} else if (value instanceof this.compilerCliMigrationsModule.DynamicValue) {
return [{node: value.node, message: `Provider is not statically analyzable.`}];
}
return [];
Expand Down
22 changes: 19 additions & 3 deletions packages/core/schematics/migrations/module-with-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Rule, SchematicContext, SchematicsException, Tree, UpdateRecorder} from
import {relative} from 'path';
import ts from 'typescript';

import {loadCompilerCliMigrationsModule, loadEsmModule} from '../../utils/load_esm';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';

Expand All @@ -32,8 +33,20 @@ export default function(): Rule {
'Could not find any tsconfig file. Cannot migrate ModuleWithProviders.');
}

let compilerCliMigrationsModule;
try {
// Load ESM `@angular/compiler/private/migrations` using the TypeScript dynamic import
// workaround. Once TypeScript provides support for keeping the dynamic import this workaround
// can be changed to a direct dynamic import.
compilerCliMigrationsModule = await loadCompilerCliMigrationsModule();
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler-cli' package. Details: ${e.message}`);
}

for (const tsconfigPath of allPaths) {
failures.push(...runModuleWithProvidersMigration(tree, tsconfigPath, basePath));
failures.push(...runModuleWithProvidersMigration(
tree, tsconfigPath, basePath, compilerCliMigrationsModule));
}

if (failures.length) {
Expand All @@ -44,7 +57,9 @@ export default function(): Rule {
};
}

function runModuleWithProvidersMigration(tree: Tree, tsconfigPath: string, basePath: string) {
function runModuleWithProvidersMigration(
tree: Tree, tsconfigPath: string, basePath: string,
compilerCliMigrationsModule: typeof import('@angular/compiler-cli/private/migrations')) {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const failures: string[] = [];
const typeChecker = program.getTypeChecker();
Expand All @@ -56,7 +71,8 @@ function runModuleWithProvidersMigration(tree: Tree, tsconfigPath: string, baseP
sourceFiles.forEach(sourceFile => collector.visitNode(sourceFile));

const {resolvedModules, resolvedNonGenerics} = collector;
const transformer = new ModuleWithProvidersTransform(typeChecker, getUpdateRecorder);
const transformer =
new ModuleWithProvidersTransform(typeChecker, getUpdateRecorder, compilerCliMigrationsModule);
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();

[...resolvedModules.reduce(
Expand Down
Loading