mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
This commit is contained in:
committed by
atscott
parent
0a1d078a74
commit
401ef71ae5
@@ -7,26 +7,28 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, core, createAotCompiler, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, getParseErrors, isFormattedError, isSyntaxError, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, StaticSymbol, TypeScriptEmitter, Xliff, Xliff2, Xmb} from '@angular/compiler';
|
||||
import {AotCompiler, AotCompilerOptions, core, createAotCompiler, FormattedMessageChain, GeneratedFile, getParseErrors, isFormattedError, isSyntaxError, MessageBundle, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Serializer, Xliff, Xliff2, Xmb} from '@angular/compiler';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {translateDiagnostics, TypeCheckHost} from '../diagnostics/translate_diagnostics';
|
||||
import {createBundleIndexHost, MetadataCollector, ModuleMetadata} from '../metadata';
|
||||
import {translateDiagnostics} from '../diagnostics/translate_diagnostics';
|
||||
import {createBundleIndexHost, MetadataCollector} from '../metadata';
|
||||
import {isAngularCorePackage} from '../ngtsc/core/src/compiler';
|
||||
import {NgtscProgram} from '../ngtsc/program';
|
||||
import {TypeScriptReflectionHost} from '../ngtsc/reflection';
|
||||
import {verifySupportedTypeScriptVersion} from '../typescript_support';
|
||||
|
||||
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api';
|
||||
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitCallback, TsMergeEmitResultsCallback} from './api';
|
||||
import {CodeGenerator, getOriginalReferences, TsCompilerAotCompilerTypeCheckHostAdapter} from './compiler_host';
|
||||
import {getDownlevelDecoratorsTransform} from './downlevel_decorators_transform';
|
||||
import {getInlineResourcesTransformFactory, InlineResourcesMetadataTransformer} from './inline_resources';
|
||||
import {getExpressionLoweringTransformFactory, LowerMetadataTransform} from './lower_expressions';
|
||||
import {MetadataCache, MetadataTransformer} from './metadata_cache';
|
||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
|
||||
import {getDecoratorStripTransformerFactory, StripDecoratorsMetadataTransformer} from './r3_strip_decorators';
|
||||
import {getAngularClassTransformerFactory} from './r3_transform';
|
||||
import {createMessageDiagnostic, DTS, GENERATED_FILES, isInRootDir, ngToTsDiagnostic, StructureIsReused, TS, tsStructureIsReused, userError} from './util';
|
||||
import {createMessageDiagnostic, DTS, GENERATED_FILES, isInRootDir, ngToTsDiagnostic, StructureIsReused, TS, tsStructureIsReused} from './util';
|
||||
|
||||
|
||||
/**
|
||||
@@ -46,14 +48,6 @@ const LOWER_FIELDS = ['useValue', 'useFactory', 'data', 'id', 'loadChildren'];
|
||||
*/
|
||||
const R3_LOWER_FIELDS = [...LOWER_FIELDS, 'providers', 'imports', 'exports'];
|
||||
|
||||
const R3_REIFIED_DECORATORS = [
|
||||
'Component',
|
||||
'Directive',
|
||||
'Injectable',
|
||||
'NgModule',
|
||||
'Pipe',
|
||||
];
|
||||
|
||||
const emptyModules: NgAnalyzedModules = {
|
||||
ngModules: [],
|
||||
ngModuleByPipeOrDirective: new Map(),
|
||||
@@ -99,8 +93,7 @@ class AngularCompilerProgram implements Program {
|
||||
private _structuralDiagnostics: Diagnostic[]|undefined;
|
||||
private _programWithStubs: ts.Program|undefined;
|
||||
private _optionsDiagnostics: Diagnostic[] = [];
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private _reifiedDecorators!: Set<StaticSymbol>;
|
||||
private _transformTsDiagnostics: ts.Diagnostic[] = [];
|
||||
|
||||
constructor(
|
||||
rootNames: ReadonlyArray<string>, private options: CompilerOptions,
|
||||
@@ -263,72 +256,6 @@ class AngularCompilerProgram implements Program {
|
||||
return this._emitRender2(parameters);
|
||||
}
|
||||
|
||||
private _emitRender3({
|
||||
emitFlags = EmitFlags.Default,
|
||||
cancellationToken,
|
||||
customTransformers,
|
||||
emitCallback = defaultEmitCallback,
|
||||
mergeEmitResultsCallback = mergeEmitResults,
|
||||
}: {
|
||||
emitFlags?: EmitFlags,
|
||||
cancellationToken?: ts.CancellationToken,
|
||||
customTransformers?: CustomTransformers,
|
||||
emitCallback?: TsEmitCallback,
|
||||
mergeEmitResultsCallback?: TsMergeEmitResultsCallback,
|
||||
} = {}): ts.EmitResult {
|
||||
const emitStart = Date.now();
|
||||
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
|
||||
0) {
|
||||
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
|
||||
}
|
||||
|
||||
// analyzedModules and analyzedInjectables are created together. If one exists, so does the
|
||||
// other.
|
||||
const modules =
|
||||
this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables!);
|
||||
|
||||
const writeTsFile: ts.WriteFileCallback =
|
||||
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
|
||||
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
|
||||
};
|
||||
|
||||
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
|
||||
|
||||
const tsCustomTransformers = this.calculateTransforms(
|
||||
/* genFiles */ undefined, /* partialModules */ modules,
|
||||
/* stripDecorators */ this.reifiedDecorators, customTransformers);
|
||||
|
||||
|
||||
// Restore the original references before we emit so TypeScript doesn't emit
|
||||
// a reference to the .d.ts file.
|
||||
const augmentedReferences = new Map<ts.SourceFile, ReadonlyArray<ts.FileReference>>();
|
||||
for (const sourceFile of this.tsProgram.getSourceFiles()) {
|
||||
const originalReferences = getOriginalReferences(sourceFile);
|
||||
if (originalReferences) {
|
||||
augmentedReferences.set(sourceFile, sourceFile.referencedFiles);
|
||||
sourceFile.referencedFiles = originalReferences;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return emitCallback({
|
||||
program: this.tsProgram,
|
||||
host: this.host,
|
||||
options: this.options,
|
||||
writeFile: writeTsFile,
|
||||
emitOnlyDtsFiles,
|
||||
customTransformers: tsCustomTransformers
|
||||
});
|
||||
} finally {
|
||||
// Restore the references back to the augmented value to ensure that the
|
||||
// checks that TypeScript makes for project structure reuse will succeed.
|
||||
for (const [sourceFile, references] of Array.from(augmentedReferences)) {
|
||||
// TODO(chuckj): Remove any cast after updating build to 2.6
|
||||
(sourceFile as any).referencedFiles = references;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _emitRender2({
|
||||
emitFlags = EmitFlags.Default,
|
||||
cancellationToken,
|
||||
@@ -367,6 +294,7 @@ class AngularCompilerProgram implements Program {
|
||||
const genFileByFileName = new Map<string, GeneratedFile>();
|
||||
genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile));
|
||||
this.emittedLibrarySummaries = [];
|
||||
this._transformTsDiagnostics = [];
|
||||
const emittedSourceFiles = [] as ts.SourceFile[];
|
||||
const writeTsFile: ts.WriteFileCallback =
|
||||
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
|
||||
@@ -389,8 +317,8 @@ class AngularCompilerProgram implements Program {
|
||||
const modules = this._analyzedInjectables &&
|
||||
this.compiler.emitAllPartialModules2(this._analyzedInjectables);
|
||||
|
||||
const tsCustomTransformers = this.calculateTransforms(
|
||||
genFileByFileName, modules, /* stripDecorators */ undefined, customTransformers);
|
||||
const tsCustomTransformers =
|
||||
this.calculateTransforms(genFileByFileName, modules, customTransformers);
|
||||
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
|
||||
// Restore the original references before we emit so TypeScript doesn't emit
|
||||
// a reference to the .d.ts file.
|
||||
@@ -548,22 +476,23 @@ class AngularCompilerProgram implements Program {
|
||||
return this._tsProgram!;
|
||||
}
|
||||
|
||||
private get reifiedDecorators(): Set<StaticSymbol> {
|
||||
if (!this._reifiedDecorators) {
|
||||
const reflector = this.compiler.reflector;
|
||||
this._reifiedDecorators = new Set(
|
||||
R3_REIFIED_DECORATORS.map(name => reflector.findDeclaration('@angular/core', name)));
|
||||
/** Whether the program is compiling the Angular core package. */
|
||||
private get isCompilingAngularCore(): boolean {
|
||||
if (this._isCompilingAngularCore !== null) {
|
||||
return this._isCompilingAngularCore;
|
||||
}
|
||||
return this._reifiedDecorators;
|
||||
return this._isCompilingAngularCore = isAngularCorePackage(this.tsProgram);
|
||||
}
|
||||
private _isCompilingAngularCore: boolean|null = null;
|
||||
|
||||
private calculateTransforms(
|
||||
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
|
||||
stripDecorators: Set<StaticSymbol>|undefined,
|
||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||
const beforeTs: Array<ts.TransformerFactory<ts.SourceFile>> = [];
|
||||
const metadataTransforms: MetadataTransformer[] = [];
|
||||
const flatModuleMetadataTransforms: MetadataTransformer[] = [];
|
||||
const annotateForClosureCompiler = this.options.annotateForClosureCompiler || false;
|
||||
|
||||
if (this.options.enableResourceInlining) {
|
||||
beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter));
|
||||
const transformer = new InlineResourcesMetadataTransformer(this.hostAdapter);
|
||||
@@ -576,7 +505,6 @@ class AngularCompilerProgram implements Program {
|
||||
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
|
||||
metadataTransforms.push(this.loweringMetadataTransform);
|
||||
}
|
||||
const annotateForClosureCompiler = this.options.annotateForClosureCompiler || false;
|
||||
if (genFiles) {
|
||||
beforeTs.push(getAngularEmitterTransformFactory(
|
||||
genFiles, this.getTsProgram(), annotateForClosureCompiler));
|
||||
@@ -591,18 +519,26 @@ class AngularCompilerProgram implements Program {
|
||||
flatModuleMetadataTransforms.push(transformer);
|
||||
}
|
||||
|
||||
if (stripDecorators) {
|
||||
beforeTs.push(getDecoratorStripTransformerFactory(
|
||||
stripDecorators, this.compiler.reflector, this.getTsProgram().getTypeChecker()));
|
||||
const transformer =
|
||||
new StripDecoratorsMetadataTransformer(stripDecorators, this.compiler.reflector);
|
||||
metadataTransforms.push(transformer);
|
||||
flatModuleMetadataTransforms.push(transformer);
|
||||
}
|
||||
|
||||
if (customTransformers && customTransformers.beforeTs) {
|
||||
beforeTs.push(...customTransformers.beforeTs);
|
||||
}
|
||||
|
||||
// If decorators should be converted to static fields (enabled by default), we set up
|
||||
// the decorator downlevel transform. Note that we set it up as last transform as that
|
||||
// allows custom transformers to strip Angular decorators without having to deal with
|
||||
// identifying static properties. e.g. it's more difficult handling `<..>.decorators`
|
||||
// or `<..>.ctorParameters` compared to the `ts.Decorator` AST nodes.
|
||||
if (this.options.annotationsAs !== 'decorators') {
|
||||
const typeChecker = this.getTsProgram().getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(typeChecker);
|
||||
// Similarly to how we handled tsickle decorator downleveling in the past, we just
|
||||
// ignore diagnostics that have been collected by the transformer. These are
|
||||
// non-significant failures that shouldn't prevent apps from compiling.
|
||||
beforeTs.push(getDownlevelDecoratorsTransform(
|
||||
typeChecker, reflectionHost, [], this.isCompilingAngularCore,
|
||||
annotateForClosureCompiler));
|
||||
}
|
||||
|
||||
if (metadataTransforms.length > 0) {
|
||||
this.metadataCache = this.createMetadataCache(metadataTransforms);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user