mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
09d325a9e6
commit
ab3de40ba3
22 changed files with 146 additions and 271 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue