Revert "refactor(migrations): support use of an ESM @angular/compiler package (#43627)"

This reverts commit c008e0fa90. This commit
breaks in g3. We will need to plan a mitigation first.
This commit is contained in:
Alex Rickabaugh 2021-09-29 11:35:57 -07:00
parent 09d325a9e6
commit ab3de40ba3
22 changed files with 146 additions and 271 deletions

View file

@ -6,7 +6,6 @@ ts_library(
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
deps = [
"//packages/compiler",
"//packages/core/schematics/migrations/activated-route-snapshot-fragment",
"//packages/core/schematics/migrations/can-activate-with-redirect-to",
"//packages/core/schematics/migrations/dynamic-queries",

View file

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as compiler from '@angular/compiler';
import {Replacement, RuleFailure, Rules} from 'tslint';
import * as ts from 'typescript';
@ -50,7 +49,7 @@ export class Rule extends Rules.TypedRule {
});
const queries = resolvedQueries.get(sourceFile);
const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker, compiler);
const usageStrategy = new QueryUsageStrategy(classMetadata, typeChecker);
// No queries detected for the given source file.
if (!queries) {

View file

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as compiler from '@angular/compiler';
import {RuleFailure, Rules} from 'tslint';
import * as ts from 'typescript';
@ -35,7 +34,7 @@ export class Rule extends Rules.TypedRule {
// template variables.
resolvedTemplates.forEach(template => {
const filePath = template.filePath;
const nodes = analyzeResolvedTemplate(template, compiler);
const nodes = analyzeResolvedTemplate(template);
const templateFile =
template.inline ? sourceFile : createHtmlSourceFile(filePath, template.content);

View file

@ -6,23 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {TmplAstBoundAttribute} from '@angular/compiler';
import {TmplAstBoundAttribute} from '@angular/compiler';
import {ResolvedTemplate} from '../../utils/ng_component_template';
import {parseHtmlGracefully} from '../../utils/parse_html';
import {RouterLinkEmptyExprVisitor} from './angular/html_routerlink_empty_expr_visitor';
export function analyzeResolvedTemplate(
template: ResolvedTemplate,
compilerModule: typeof import('@angular/compiler')): TmplAstBoundAttribute[]|null {
const templateNodes = parseHtmlGracefully(template.content, template.filePath, compilerModule);
export function analyzeResolvedTemplate(template: ResolvedTemplate): TmplAstBoundAttribute[]|null {
const templateNodes = parseHtmlGracefully(template.content, template.filePath);
if (!templateNodes) {
return null;
}
const visitor = new RouterLinkEmptyExprVisitor(compilerModule);
const visitor = new RouterLinkEmptyExprVisitor();
// Analyze the Angular Render3 HTML AST and collect all template variable assignments.
visitor.visitAll(templateNodes);

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {TmplAstBoundAttribute, TmplAstElement, TmplAstTemplate} from '@angular/compiler';
import {ASTWithSource, EmptyExpr, TmplAstBoundAttribute, TmplAstElement, TmplAstTemplate} from '@angular/compiler';
import {TemplateAstVisitor} from '../../../utils/template_ast_visitor';
/**
@ -27,8 +27,8 @@ export class RouterLinkEmptyExprVisitor extends TemplateAstVisitor {
}
override visitBoundAttribute(node: TmplAstBoundAttribute) {
if (node.name === 'routerLink' && node.value instanceof this.compilerModule.ASTWithSource &&
node.value.ast instanceof this.compilerModule.EmptyExpr) {
if (node.name === 'routerLink' && node.value instanceof ASTWithSource &&
node.value.ast instanceof EmptyExpr) {
this.emptyRouterLinkExpressions.push(node);
}
}

View file

@ -8,9 +8,8 @@
import {logging, normalize} from '@angular-devkit/core';
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import type {TmplAstBoundAttribute} from '@angular/compiler';
import {EmptyExpr, TmplAstBoundAttribute} from '@angular/compiler';
import {relative} from 'path';
import {loadEsmModule} from '../../utils/load_esm';
import {NgComponentTemplateVisitor, ResolvedTemplate} from '../../utils/ng_component_template';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
@ -45,20 +44,8 @@ export default function(): Rule {
'Could not find any tsconfig file. Cannot check templates for empty routerLinks.');
}
let compilerModule;
try {
// Load ESM `@angular/compiler` 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.
compilerModule = await loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler');
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
}
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
runEmptyRouterLinkExpressionMigration(
tree, tsconfigPath, basePath, context.logger, compilerModule);
runEmptyRouterLinkExpressionMigration(tree, tsconfigPath, basePath, context.logger);
}
};
}
@ -68,8 +55,7 @@ export default function(): Rule {
* which templates received updates.
*/
function runEmptyRouterLinkExpressionMigration(
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger,
compilerModule: typeof import('@angular/compiler')) {
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger) {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const typeChecker = program.getTypeChecker();
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
@ -80,15 +66,13 @@ function runEmptyRouterLinkExpressionMigration(
sourceFiles.forEach(sourceFile => templateVisitor.visitNode(sourceFile));
const {resolvedTemplates} = templateVisitor;
fixEmptyRouterlinks(resolvedTemplates, tree, logger, compilerModule);
fixEmptyRouterlinks(resolvedTemplates, tree, logger);
}
function fixEmptyRouterlinks(
resolvedTemplates: ResolvedTemplate[], tree: Tree, logger: Logger,
compilerModule: typeof import('@angular/compiler')) {
function fixEmptyRouterlinks(resolvedTemplates: ResolvedTemplate[], tree: Tree, logger: Logger) {
const basePath = process.cwd();
const collectedFixes: string[] = [];
const fixesByFile = getFixesByFile(resolvedTemplates, compilerModule);
const fixesByFile = getFixesByFile(resolvedTemplates);
for (const [absFilePath, templateFixes] of fixesByFile) {
const treeFilePath = relative(normalize(basePath), normalize(absFilePath));
@ -132,12 +116,10 @@ function fixEmptyRouterlinks(
/**
* Returns fixes for nodes in templates which contain empty routerLink assignments, grouped by file.
*/
function getFixesByFile(
templates: ResolvedTemplate[],
compilerModule: typeof import('@angular/compiler')): Map<string, FixedTemplate[]> {
function getFixesByFile(templates: ResolvedTemplate[]): Map<string, FixedTemplate[]> {
const fixesByFile = new Map<string, FixedTemplate[]>();
for (const template of templates) {
const templateFix = fixEmptyRouterlinksInTemplate(template, compilerModule);
const templateFix = fixEmptyRouterlinksInTemplate(template);
if (templateFix === null) {
continue;
}
@ -159,10 +141,8 @@ function getFixesByFile(
return fixesByFile;
}
function fixEmptyRouterlinksInTemplate(
template: ResolvedTemplate, compilerModule: typeof import('@angular/compiler')): FixedTemplate|
null {
const emptyRouterlinkExpressions = analyzeResolvedTemplate(template, compilerModule);
function fixEmptyRouterlinksInTemplate(template: ResolvedTemplate): FixedTemplate|null {
const emptyRouterlinkExpressions = analyzeResolvedTemplate(template);
if (!emptyRouterlinkExpressions || emptyRouterlinkExpressions.length === 0) {
return null;

View file

@ -11,7 +11,6 @@ import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit
import {relative} from 'path';
import * as ts from 'typescript';
import {loadEsmModule} from '../../utils/load_esm';
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
@ -70,21 +69,9 @@ async function runMigration(tree: Tree, context: SchematicContext) {
}
}
let compilerModule;
try {
// Load ESM `@angular/compiler` 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.
compilerModule = await loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler');
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
}
if (buildProjects.size) {
for (let project of Array.from(buildProjects.values())) {
failures.push(
...await runStaticQueryMigration(tree, project, strategy, logger, compilerModule));
failures.push(...await runStaticQueryMigration(tree, project, strategy, logger));
}
}
@ -93,8 +80,8 @@ async function runMigration(tree: Tree, context: SchematicContext) {
for (const tsconfigPath of testPaths) {
const project = await analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger);
if (project) {
failures.push(...await runStaticQueryMigration(
tree, project, SELECTED_STRATEGY.TESTS, logger, compilerModule));
failures.push(
...await runStaticQueryMigration(tree, project, SELECTED_STRATEGY.TESTS, logger));
}
}
@ -163,8 +150,7 @@ function analyzeProject(
*/
async function runStaticQueryMigration(
tree: Tree, project: AnalyzedProject, selectedStrategy: SELECTED_STRATEGY,
logger: logging.LoggerApi,
compilerModule: typeof import('@angular/compiler')): Promise<string[]> {
logger: logging.LoggerApi): Promise<string[]> {
const {sourceFiles, typeChecker, host, queryVisitor, tsconfigPath, basePath} = project;
const printer = ts.createPrinter();
const failureMessages: string[] = [];
@ -191,11 +177,11 @@ async function runStaticQueryMigration(
let strategy: TimingStrategy;
if (selectedStrategy === SELECTED_STRATEGY.USAGE) {
strategy = new QueryUsageStrategy(classMetadata, typeChecker, compilerModule);
strategy = new QueryUsageStrategy(classMetadata, typeChecker);
} else if (selectedStrategy === SELECTED_STRATEGY.TESTS) {
strategy = new QueryTestStrategy();
} else {
strategy = new QueryTemplateStrategy(tsconfigPath, classMetadata, host, compilerModule);
strategy = new QueryTemplateStrategy(tsconfigPath, classMetadata, host);
}
try {

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompileStylesheetMetadata, ElementAst, EmbeddedTemplateAst, NgAnalyzedModules, QueryMatch, StaticSymbol, TemplateAst} from '@angular/compiler';
import {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompileStylesheetMetadata, ElementAst, EmbeddedTemplateAst, NgAnalyzedModules, QueryMatch, StaticSymbol, TemplateAst} from '@angular/compiler';
import {createProgram, Diagnostic, readConfiguration} from '@angular/compiler-cli';
import {resolve} from 'path';
import * as ts from 'typescript';
@ -25,7 +25,7 @@ export class QueryTemplateStrategy implements TimingStrategy {
constructor(
private projectPath: string, private classMetadata: ClassMetadataMap,
private host: ts.CompilerHost, private compilerModule: typeof import('@angular/compiler')) {}
private host: ts.CompilerHost) {}
/**
* Sets up the template strategy by creating the AngularCompilerProgram. Returns false if
@ -54,8 +54,8 @@ export class QueryTemplateStrategy implements TimingStrategy {
// breaks the analysis of the project because we instantiate a standalone AOT compiler
// program which does not contain the custom logic by the Angular CLI Webpack compiler plugin.
const directiveNormalizer = this.metadataResolver!['_directiveNormalizer'];
directiveNormalizer['_normalizeStylesheet'] = (metadata: CompileStylesheetMetadata) => {
return new this.compilerModule.CompileStylesheetMetadata(
directiveNormalizer['_normalizeStylesheet'] = function(metadata: CompileStylesheetMetadata) {
return new CompileStylesheetMetadata(
{styles: metadata.styles, styleUrls: [], moduleUrl: metadata.moduleUrl!});
};
@ -83,7 +83,7 @@ export class QueryTemplateStrategy implements TimingStrategy {
}
const parsedTemplate = this._parseTemplate(metadata, ngModule);
const queryTimingMap = this.findStaticQueryIds(parsedTemplate);
const queryTimingMap = findStaticQueryIds(parsedTemplate);
const {staticQueryIds} = staticViewQueryIds(queryTimingMap);
metadata.viewQueries.forEach((query, index) => {
@ -187,40 +187,6 @@ export class QueryTemplateStrategy implements TimingStrategy {
private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) {
return `${resolve(filePath)}#${className}-${propName}`;
}
/** Figures out which queries are static and which ones are dynamic. */
private findStaticQueryIds(
nodes: TemplateAst[], result = new Map<TemplateAst, StaticAndDynamicQueryIds>()):
Map<TemplateAst, StaticAndDynamicQueryIds> {
nodes.forEach((node) => {
const staticQueryIds = new Set<number>();
const dynamicQueryIds = new Set<number>();
let queryMatches: QueryMatch[] = undefined!;
if (node instanceof this.compilerModule.ElementAst) {
this.findStaticQueryIds(node.children, result);
node.children.forEach((child) => {
const childData = result.get(child)!;
childData.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId));
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
});
queryMatches = node.queryMatches;
} else if (node instanceof this.compilerModule.EmbeddedTemplateAst) {
this.findStaticQueryIds(node.children, result);
node.children.forEach((child) => {
const childData = result.get(child)!;
childData.staticQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
});
queryMatches = node.queryMatches;
}
if (queryMatches) {
queryMatches.forEach((match) => staticQueryIds.add(match.queryId));
}
dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId));
result.set(node, {staticQueryIds, dynamicQueryIds});
});
return result;
}
}
interface StaticAndDynamicQueryIds {
@ -228,6 +194,40 @@ interface StaticAndDynamicQueryIds {
dynamicQueryIds: Set<number>;
}
/** Figures out which queries are static and which ones are dynamic. */
function findStaticQueryIds(
nodes: TemplateAst[], result = new Map<TemplateAst, StaticAndDynamicQueryIds>()):
Map<TemplateAst, StaticAndDynamicQueryIds> {
nodes.forEach((node) => {
const staticQueryIds = new Set<number>();
const dynamicQueryIds = new Set<number>();
let queryMatches: QueryMatch[] = undefined!;
if (node instanceof ElementAst) {
findStaticQueryIds(node.children, result);
node.children.forEach((child) => {
const childData = result.get(child)!;
childData.staticQueryIds.forEach(queryId => staticQueryIds.add(queryId));
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
});
queryMatches = node.queryMatches;
} else if (node instanceof EmbeddedTemplateAst) {
findStaticQueryIds(node.children, result);
node.children.forEach((child) => {
const childData = result.get(child)!;
childData.staticQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
childData.dynamicQueryIds.forEach(queryId => dynamicQueryIds.add(queryId));
});
queryMatches = node.queryMatches;
}
if (queryMatches) {
queryMatches.forEach((match) => staticQueryIds.add(match.queryId));
}
dynamicQueryIds.forEach(queryId => staticQueryIds.delete(queryId));
result.set(node, {staticQueryIds, dynamicQueryIds});
});
return result;
}
/** Splits queries into static and dynamic. */
function staticViewQueryIds(nodeStaticQueryIds: Map<TemplateAst, StaticAndDynamicQueryIds>):
StaticAndDynamicQueryIds {

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {ParseSourceSpan, PropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
import {ImplicitReceiver, ParseSourceSpan, PropertyRead, RecursiveAstVisitor, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
import {TemplateAstVisitor} from '../../../../utils/template_ast_visitor';
/**
@ -15,33 +15,10 @@ import {TemplateAstVisitor} from '../../../../utils/template_ast_visitor';
*/
export class TemplateUsageVisitor extends TemplateAstVisitor {
private hasQueryTemplateReference = false;
private expressionAstVisitor;
private expressionAstVisitor = new ExpressionAstVisitor(this.queryPropertyName);
constructor(
public queryPropertyName: string, compilerModule: typeof import('@angular/compiler')) {
super(compilerModule);
// AST visitor that checks if the given expression contains property reads that
// refer to the specified query property name.
// This class must be defined within the template visitor due to the need to extend from a class
// value found within `@angular/compiler` which is dynamically imported and provided to the
// visitor.
this.expressionAstVisitor = new (class extends compilerModule.RecursiveAstVisitor {
hasQueryPropertyRead = false;
override visitPropertyRead(node: PropertyRead, span: ParseSourceSpan): any {
// The receiver of the property read needs to be "implicit" as queries are accessed
// from the component instance and not from other objects.
if (node.receiver instanceof compilerModule.ImplicitReceiver &&
node.name === queryPropertyName) {
this.hasQueryPropertyRead = true;
return;
}
super.visitPropertyRead(node, span);
}
})();
constructor(public queryPropertyName: string) {
super();
}
/** Checks whether the given query is statically accessed within the specified HTML nodes. */
@ -92,3 +69,26 @@ export class TemplateUsageVisitor extends TemplateAstVisitor {
node.handler.visit(this.expressionAstVisitor, node.handlerSpan);
}
}
/**
* AST visitor that checks if the given expression contains property reads that
* refer to the specified query property name.
*/
class ExpressionAstVisitor extends RecursiveAstVisitor {
hasQueryPropertyRead = false;
constructor(private queryPropertyName: string) {
super();
}
override visitPropertyRead(node: PropertyRead, span: ParseSourceSpan): any {
// The receiver of the property read needs to be "implicit" as queries are accessed
// from the component instance and not from other objects.
if (node.receiver instanceof ImplicitReceiver && node.name === this.queryPropertyName) {
this.hasQueryPropertyRead = true;
return;
}
super.visitPropertyRead(node, span);
}
}

View file

@ -35,9 +35,7 @@ const STATIC_QUERY_LIFECYCLE_HOOKS = {
* this strategy here: https://hackmd.io/s/Hymvc2OKE
*/
export class QueryUsageStrategy implements TimingStrategy {
constructor(
private classMetadata: ClassMetadataMap, private typeChecker: ts.TypeChecker,
private compilerModule: typeof import('@angular/compiler')) {}
constructor(private classMetadata: ClassMetadataMap, private typeChecker: ts.TypeChecker) {}
setup() {}
@ -103,9 +101,8 @@ export class QueryUsageStrategy implements TimingStrategy {
// can be marked as static.
if (classMetadata.template && hasPropertyNameText(query.property!.name)) {
const template = classMetadata.template;
const parsedHtml =
parseHtmlGracefully(template.content, template.filePath, this.compilerModule);
const htmlVisitor = new TemplateUsageVisitor(query.property!.name.text, this.compilerModule);
const parsedHtml = parseHtmlGracefully(template.content, template.filePath);
const htmlVisitor = new TemplateUsageVisitor(query.property!.name.text);
if (parsedHtml && htmlVisitor.isQueryUsedStatically(parsedHtml)) {
return ResolvedUsage.SYNCHRONOUS;

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {PropertyWrite} from '@angular/compiler';
import {PropertyWrite} from '@angular/compiler';
import {ResolvedTemplate} from '../../utils/ng_component_template';
import {parseHtmlGracefully} from '../../utils/parse_html';
import {HtmlVariableAssignmentVisitor} from './angular/html_variable_assignment_visitor';
@ -21,16 +21,15 @@ export interface TemplateVariableAssignment {
* Analyzes a given resolved template by looking for property assignments to local
* template variables within bound events.
*/
export function analyzeResolvedTemplate(
template: ResolvedTemplate,
compilerModule: typeof import('@angular/compiler')): TemplateVariableAssignment[]|null {
const templateNodes = parseHtmlGracefully(template.content, template.filePath, compilerModule);
export function analyzeResolvedTemplate(template: ResolvedTemplate): TemplateVariableAssignment[]|
null {
const templateNodes = parseHtmlGracefully(template.content, template.filePath);
if (!templateNodes) {
return null;
}
const visitor = new HtmlVariableAssignmentVisitor(compilerModule);
const visitor = new HtmlVariableAssignmentVisitor();
// Analyze the Angular Render3 HTML AST and collect all template variable assignments.
visitor.visitAll(templateNodes);

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {ImplicitReceiver, ParseSourceSpan, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstElement, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import {ImplicitReceiver, ParseSourceSpan, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstElement, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import {TemplateAstVisitor} from '../../../utils/template_ast_visitor';
@ -24,35 +24,8 @@ export class HtmlVariableAssignmentVisitor extends TemplateAstVisitor {
variableAssignments: TemplateVariableAssignment[] = [];
private currentVariables: TmplAstVariable[] = [];
private expressionAstVisitor;
constructor(compilerModule: typeof import('@angular/compiler')) {
super(compilerModule);
// AST visitor that resolves all variable assignments within a given expression AST.
// This class must be defined within the template visitor due to the need to extend from a class
// value found within `@angular/compiler` which is dynamically imported and provided to the
// visitor.
this.expressionAstVisitor = new (class extends compilerModule.RecursiveAstVisitor {
constructor(
private variableAssignments: TemplateVariableAssignment[],
private currentVariables: TmplAstVariable[]) {
super();
}
override visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) {
if (node.receiver instanceof compilerModule.ImplicitReceiver &&
this.currentVariables.some(v => v.name === node.name)) {
this.variableAssignments.push({
node: node,
start: span.start.offset,
end: span.end.offset,
});
}
super.visitPropertyWrite(node, span);
}
})(this.variableAssignments, this.currentVariables);
}
private expressionAstVisitor =
new ExpressionAstVisitor(this.variableAssignments, this.currentVariables);
override visitElement(element: TmplAstElement): void {
this.visitAll(element.outputs);
@ -84,3 +57,24 @@ export class HtmlVariableAssignmentVisitor extends TemplateAstVisitor {
node.handler.visit(this.expressionAstVisitor, node.handlerSpan);
}
}
/** AST visitor that resolves all variable assignments within a given expression AST. */
class ExpressionAstVisitor extends RecursiveAstVisitor {
constructor(
private variableAssignments: TemplateVariableAssignment[],
private currentVariables: TmplAstVariable[]) {
super();
}
override visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) {
if (node.receiver instanceof ImplicitReceiver &&
this.currentVariables.some(v => v.name === node.name)) {
this.variableAssignments.push({
node: node,
start: span.start.offset,
end: span.end.offset,
});
}
super.visitPropertyWrite(node, span);
}
}

View file

@ -10,7 +10,6 @@ import {logging, normalize} from '@angular-devkit/core';
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {relative} from 'path';
import {loadEsmModule} from '../../utils/load_esm';
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
@ -34,20 +33,8 @@ export default function(): Rule {
'assignments.');
}
let compilerModule;
try {
// Load ESM `@angular/compiler` 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.
compilerModule = await loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler');
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
}
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
runTemplateVariableAssignmentCheck(
tree, tsconfigPath, basePath, context.logger, compilerModule);
runTemplateVariableAssignmentCheck(tree, tsconfigPath, basePath, context.logger);
}
};
}
@ -57,8 +44,7 @@ export default function(): Rule {
* if values are assigned to template variables within output bindings.
*/
function runTemplateVariableAssignmentCheck(
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger,
compilerModule: typeof import('@angular/compiler')) {
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger) {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const typeChecker = program.getTypeChecker();
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
@ -75,7 +61,7 @@ function runTemplateVariableAssignmentCheck(
// template variables.
resolvedTemplates.forEach(template => {
const filePath = template.filePath;
const nodes = analyzeResolvedTemplate(template, compilerModule);
const nodes = analyzeResolvedTemplate(template);
if (!nodes) {
return;

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {AotCompiler} from '@angular/compiler';
import {AotCompiler} from '@angular/compiler';
import {CompilerHost, createProgram, readConfiguration} from '@angular/compiler-cli';
import * as ts from 'typescript';

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {StaticSymbol} from '@angular/compiler';
import {StaticSymbol} from '@angular/compiler';
import * as ts from 'typescript';
/** Error that will be thrown if an unexpected value needs to be converted. */
@ -17,8 +17,7 @@ export class UnexpectedMetadataValueError extends Error {}
* if metadata cannot be cleanly converted.
*/
export function convertDirectiveMetadataToExpression(
compilerModule: typeof import('@angular/compiler'), metadata: any,
resolveSymbolImport: (symbol: StaticSymbol) => string | null,
metadata: any, resolveSymbolImport: (symbol: StaticSymbol) => string | null,
createImport: (moduleName: string, name: string) => ts.Expression,
convertProperty?: (key: string, value: any) => ts.Expression | null): ts.Expression {
if (typeof metadata === 'string') {
@ -26,7 +25,7 @@ export function convertDirectiveMetadataToExpression(
} else if (Array.isArray(metadata)) {
return ts.createArrayLiteral(metadata.map(
el => convertDirectiveMetadataToExpression(
compilerModule, el, resolveSymbolImport, createImport, convertProperty)));
el, resolveSymbolImport, createImport, convertProperty)));
} else if (typeof metadata === 'number') {
return ts.createNumericLiteral(metadata.toString());
} else if (typeof metadata === 'boolean') {
@ -39,7 +38,7 @@ export function convertDirectiveMetadataToExpression(
// In case there is a static symbol object part of the metadata, try to resolve
// the import expression of the symbol. If no import path could be resolved, an
// error will be thrown as the symbol cannot be converted into TypeScript AST.
if (metadata instanceof compilerModule.StaticSymbol) {
if (metadata instanceof StaticSymbol) {
const resolvedImport = resolveSymbolImport(metadata);
if (resolvedImport === null) {
throw new UnexpectedMetadataValueError();
@ -64,7 +63,7 @@ export function convertDirectiveMetadataToExpression(
// the resolved metadata value into a TypeScript expression.
if (propertyValue === null) {
propertyValue = convertDirectiveMetadataToExpression(
compilerModule, metadataValue, resolveSymbolImport, createImport, convertProperty);
metadataValue, resolveSymbolImport, createImport, convertProperty);
}
literalProperties.push(ts.createPropertyAssignment(getPropertyName(key), propertyValue));

View file

@ -6,7 +6,7 @@
* 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 type {AotCompiler} from '@angular/compiler';
import {AotCompiler} from '@angular/compiler';
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import * as ts from 'typescript';

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {AotCompilerHost} from '@angular/compiler';
import {AotCompilerHost} from '@angular/compiler';
import {dirname, resolve} from 'path';
import * as ts from 'typescript';

View file

@ -8,14 +8,13 @@
import {logging} from '@angular-devkit/core';
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import type {AotCompiler} from '@angular/compiler';
import {AotCompiler} from '@angular/compiler';
import {Diagnostic as NgDiagnostic} from '@angular/compiler-cli';
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
import {relative} from 'path';
import * as ts from 'typescript';
import {loadEsmModule} from '../../utils/load_esm';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationCompilerHost} from '../../utils/typescript/compiler_host';
@ -45,20 +44,8 @@ export default function(): Rule {
'undecorated base classes which use DI.');
}
let compilerModule;
try {
// Load ESM `@angular/compiler` 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.
compilerModule = await loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler');
} catch (e) {
throw new SchematicsException(
`Unable to load the '@angular/compiler' package. Details: ${e.message}`);
}
for (const tsconfigPath of buildPaths) {
const result =
runUndecoratedClassesMigration(tree, tsconfigPath, basePath, ctx.logger, compilerModule);
const result = runUndecoratedClassesMigration(tree, tsconfigPath, basePath, ctx.logger);
failures.push(...result.failures);
programError = programError || !!result.programError;
}
@ -83,9 +70,8 @@ export default function(): Rule {
}
function runUndecoratedClassesMigration(
tree: Tree, tsconfigPath: string, basePath: string, logger: logging.LoggerApi,
compilerModule: typeof import('@angular/compiler')):
{failures: string[], programError?: boolean} {
tree: Tree, tsconfigPath: string, basePath: string,
logger: logging.LoggerApi): {failures: string[], programError?: boolean} {
const failures: string[] = [];
const programData = gracefullyCreateProgram(tree, basePath, tsconfigPath, logger);
@ -106,8 +92,8 @@ function runUndecoratedClassesMigration(
sourceFiles.forEach(sourceFile => declarationCollector.visitNode(sourceFile));
const {decoratedDirectives, decoratedProviders, undecoratedDeclarations} = declarationCollector;
const transform = new UndecoratedClassesTransform(
typeChecker, compiler, partialEvaluator, getUpdateRecorder, compilerModule);
const transform =
new UndecoratedClassesTransform(typeChecker, compiler, partialEvaluator, getUpdateRecorder);
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
// Run the migrations for decorated providers and both decorated and undecorated

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {AotCompiler, AotCompilerHost, CompileMetadataResolver, StaticSymbol, StaticSymbolResolver, SummaryResolver} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, CompileMetadataResolver, StaticSymbol, StaticSymbolResolver, SummaryResolver} from '@angular/compiler';
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import * as ts from 'typescript';
@ -58,8 +58,7 @@ export class UndecoratedClassesTransform {
constructor(
private typeChecker: ts.TypeChecker, private compiler: AotCompiler,
private evaluator: PartialEvaluator,
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
private compilerModule: typeof import('@angular/compiler')) {
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {
this.symbolResolver = compiler['_symbolResolver'];
this.compilerHost = compiler['_host'];
this.metadataResolver = compiler['_metadataResolver'];
@ -329,7 +328,7 @@ export class UndecoratedClassesTransform {
directiveMetadata: DeclarationMetadata, targetSourceFile: ts.SourceFile): ts.Decorator|null {
try {
const decoratorExpr = convertDirectiveMetadataToExpression(
this.compilerModule, directiveMetadata.metadata,
directiveMetadata.metadata,
staticSymbol =>
this.compilerHost
.fileNameToModuleName(staticSymbol.filePath, targetSourceFile.fileName)

View file

@ -1,35 +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 {URL} from 'url';
/**
* This uses a dynamic import to load a module which may be ESM.
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
* will currently, unconditionally downlevel dynamic import into a require call.
* require calls cannot load ESM code and will result in a runtime error. To workaround
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
* Once TypeScript provides support for keeping the dynamic import this workaround can
* be dropped.
* This is only intended to be used with Angular framework packages.
*
* @param modulePath The path of the module to load.
* @returns A Promise that resolves to the dynamically imported module.
*/
export async function loadEsmModule<T>(modulePath: string|URL): Promise<T> {
const namespaceObject =
(await new Function('modulePath', `return import(modulePath);`)(modulePath));
// If it is not ESM then the values needed will be stored in the `default` property.
// TODO_ESM: This can be removed once `@angular/*` packages are ESM only.
if (namespaceObject.default) {
return namespaceObject.default;
} else {
return namespaceObject;
}
}

View file

@ -6,17 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {TmplAstNode} from '@angular/compiler';
import {parseTemplate, TmplAstNode} from '@angular/compiler';
/**
* Parses the given HTML content using the Angular compiler. In case the parsing
* fails, null is being returned.
*/
export function parseHtmlGracefully(
htmlContent: string, filePath: string,
compilerModule: typeof import('@angular/compiler')): TmplAstNode[]|null {
export function parseHtmlGracefully(htmlContent: string, filePath: string): TmplAstNode[]|null {
try {
return compilerModule.parseTemplate(htmlContent, filePath).nodes;
return parseTemplate(htmlContent, filePath).nodes;
} catch {
// Do nothing if the template couldn't be parsed. We don't want to throw any
// exception if a template is syntactically not valid. e.g. template could be

View file

@ -24,15 +24,6 @@ import type {TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAst
* interface `Visitor<T>` is not exported.
*/
export class TemplateAstVisitor implements TmplAstRecursiveVisitor {
/**
* Creates a new Render3 Template AST visitor using an instance of the `@angular/compiler`
* package. Passing in the compiler is required due to the need to dynamically import the
* ESM `@angular/compiler` into a CommonJS schematic.
*
* @param compilerModule The compiler instance that should be used within the visitor.
*/
constructor(protected readonly compilerModule: typeof import('@angular/compiler')) {}
visitElement(element: TmplAstElement): void {}
visitTemplate(template: TmplAstTemplate): void {}
visitContent(content: TmplAstContent): void {}