Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(core): add static-query template strategy
Introduces a new strategy for the `static-query` schematic that is enabled by default. In order to provide a migration that works for the most Angular applications and makes the upgrade as easy as possible, the template strategy leverages the view engine Angular compiler logic in order to determine the query timing that is currently used within applications using view engine.
- Loading branch information
1 parent
d5995b0
commit 73ec857
Showing
11 changed files
with
755 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
...re/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 {AotCompiler, CompileDirectiveMetadata, CompileMetadataResolver, CompileNgModuleMetadata, NgAnalyzedModules, StaticSymbol, TemplateAst, findStaticQueryIds, staticViewQueryIds} from '@angular/compiler'; | ||
import {Diagnostic, createProgram, readConfiguration} from '@angular/compiler-cli'; | ||
import {resolve} from 'path'; | ||
import * as ts from 'typescript'; | ||
|
||
import {hasPropertyNameText} from '../../../../utils/typescript/property_name'; | ||
import {ClassMetadataMap} from '../../angular/ng_query_visitor'; | ||
import {NgQueryDefinition, QueryTiming, QueryType} from '../../angular/query-definition'; | ||
import {TimingResult, TimingStrategy} from '../timing-strategy'; | ||
|
||
const QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE = 'Timing could not be determined. This happens ' + | ||
'if the query is not declared in any component.'; | ||
|
||
export class QueryTemplateStrategy implements TimingStrategy { | ||
private compiler: AotCompiler; | ||
private metadataResolver: CompileMetadataResolver; | ||
private analyzedQueries = new Map<string, QueryTiming>(); | ||
|
||
constructor( | ||
private projectPath: string, private classMetadata: ClassMetadataMap, host: ts.CompilerHost) { | ||
const {rootNames, options} = readConfiguration(projectPath); | ||
const aotProgram = createProgram({rootNames, options, host}); | ||
|
||
// The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it | ||
// expose the logic that is necessary to analyze the determined modules. We work around | ||
// this by just accessing the necessary private properties using the bracket notation. | ||
this.compiler = aotProgram['compiler']; | ||
this.metadataResolver = this.compiler['_metadataResolver']; | ||
const analyzedModules = aotProgram['analyzedModules'] as NgAnalyzedModules; | ||
|
||
const ngDiagnostics = [ | ||
...aotProgram.getNgStructuralDiagnostics(), | ||
...aotProgram.getNgSemanticDiagnostics(), | ||
]; | ||
|
||
if (ngDiagnostics.length) { | ||
throw this._createDiagnosticsError(ngDiagnostics); | ||
} | ||
|
||
analyzedModules.files.forEach(file => { | ||
file.directives.forEach(directive => this._analyzeDirective(directive, analyzedModules)); | ||
}); | ||
} | ||
|
||
/** Analyzes a given directive by determining the timing of all matched view queries. */ | ||
private _analyzeDirective(symbol: StaticSymbol, analyzedModules: NgAnalyzedModules) { | ||
const metadata = this.metadataResolver.getDirectiveMetadata(symbol); | ||
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(symbol); | ||
|
||
if (!metadata.isComponent || !ngModule) { | ||
return; | ||
} | ||
|
||
const parsedTemplate = this._parseTemplate(metadata, ngModule); | ||
const queryTimingMap = findStaticQueryIds(parsedTemplate); | ||
const {staticQueryIds} = staticViewQueryIds(queryTimingMap); | ||
|
||
metadata.viewQueries.forEach((query, index) => { | ||
// Query ids are computed by adding "one" to the index. This is done within | ||
// the "view_compiler.ts" in order to support using a bloom filter for queries. | ||
const queryId = index + 1; | ||
const queryKey = | ||
this._getViewQueryUniqueKey(symbol.filePath, symbol.name, query.propertyName); | ||
this.analyzedQueries.set( | ||
queryKey, staticQueryIds.has(queryId) ? QueryTiming.STATIC : QueryTiming.DYNAMIC); | ||
}); | ||
} | ||
|
||
/** Detects the timing of the query definition. */ | ||
detectTiming(query: NgQueryDefinition): TimingResult { | ||
if (query.type === QueryType.ContentChild) { | ||
return {timing: null, message: 'Content queries cannot be migrated automatically.'}; | ||
} else if (!hasPropertyNameText(query.property.name)) { | ||
// In case the query property name is not statically analyzable, we mark this | ||
// query as unresolved. NGC currently skips these view queries as well. | ||
return {timing: null, message: 'Query is not statically analyzable.'}; | ||
} | ||
|
||
const propertyName = query.property.name.text; | ||
const classMetadata = this.classMetadata.get(query.container); | ||
|
||
// In case there is no class metadata or there are no derived classes that | ||
// could access the current query, we just look for the query analysis of | ||
// the class that declares the query. e.g. only the template of the class | ||
// that declares the view query affects the query timing. | ||
if (!classMetadata || !classMetadata.derivedClasses.length) { | ||
const timing = this._getQueryTimingFromClass(query.container, propertyName); | ||
|
||
if (timing === null) { | ||
return {timing: null, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE}; | ||
} | ||
|
||
return {timing}; | ||
} | ||
|
||
let resolvedTiming: QueryTiming|null = null; | ||
let timingMismatch = false; | ||
|
||
// In case there are multiple components that use the same query (e.g. through inheritance), | ||
// we need to check if all components use the query with the same timing. If that is not | ||
// the case, the query timing is ambiguous and the developer needs to fix the query manually. | ||
[query.container, ...classMetadata.derivedClasses].forEach(classDecl => { | ||
const classTiming = this._getQueryTimingFromClass(classDecl, propertyName); | ||
|
||
if (classTiming === null) { | ||
return; | ||
} | ||
|
||
// In case there is no resolved timing yet, save the new timing. Timings from other | ||
// components that use the query with a different timing, cause the timing to be | ||
// mismatched. In that case we can't detect a working timing for all components. | ||
if (resolvedTiming === null) { | ||
resolvedTiming = classTiming; | ||
} else if (resolvedTiming !== classTiming) { | ||
timingMismatch = true; | ||
} | ||
}); | ||
|
||
if (resolvedTiming === null) { | ||
return {timing: QueryTiming.DYNAMIC, message: QUERY_NOT_DECLARED_IN_COMPONENT_MESSAGE}; | ||
} else if (timingMismatch) { | ||
return {timing: null, message: 'Multiple components use the query with different timings.'}; | ||
} | ||
return {timing: resolvedTiming}; | ||
} | ||
|
||
/** | ||
* Gets the timing that has been resolved for a given query when it's used within the | ||
* specified class declaration. e.g. queries from an inherited class can be used. | ||
*/ | ||
private _getQueryTimingFromClass(classDecl: ts.ClassDeclaration, queryName: string): QueryTiming | ||
|null { | ||
if (!classDecl.name) { | ||
return null; | ||
} | ||
const filePath = classDecl.getSourceFile().fileName; | ||
const queryKey = this._getViewQueryUniqueKey(filePath, classDecl.name.text, queryName); | ||
|
||
if (this.analyzedQueries.has(queryKey)) { | ||
return this.analyzedQueries.get(queryKey) !; | ||
} | ||
return null; | ||
} | ||
|
||
private _parseTemplate(component: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata): | ||
TemplateAst[] { | ||
return this | ||
.compiler['_parseTemplate'](component, ngModule, ngModule.transitiveModule.directives) | ||
.template; | ||
} | ||
|
||
private _createDiagnosticsError(diagnostics: (ts.Diagnostic|Diagnostic)[]) { | ||
return new Error( | ||
`Could not create Angular AOT compiler to determine query timing.\n` + | ||
`The following diagnostics were detected:\n` + | ||
diagnostics.map(d => d.messageText).join(`\n`)); | ||
} | ||
|
||
private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) { | ||
return `${resolve(filePath)}#${className}-${propName}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.