Skip to content

Commit a289fa5

Browse files
committed
fix(compiler-cli): downlevel angular decorators to static properties
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output and SSR: (1): TypeScript has a limitation where `forwardRef` breaks in es2015 due to `forwardRef` calls being invoked immediately as part of decorator metadata. See: angular/angular-cli#14424 and angular#30106. (2): TypeScript preserves type information for class members, and references the type values immediately (as with `forwardRef` above). This means that using native DOM globals as property types could break SSR. This is because loading such JS file requires these DOM globals to exist in NodeJS globally too (and there is no support for DI mocking). See: angular#30586. This is especially relevant for libraries that do not want to use tsickle but ship SSR-compatible code. The root cause for (1) is a TypeScript limitation as mentioned. This is the related upstream ticket: microsoft/TypeScript#27519. Downleveling decorators to static properties fixes the issues, as outlined in (1) and (2), because we can defer metadata evaluation to avoid direct evaluation on file load. Additionally, we have more control and can discard unnnecessary metadata information, like class member types that are not needed by Angular at all see (2). One might wonder why this hasn't been an issue in the past since we disabled this as part of version 7. These issues didn't surface at a large scale because we added a custom transformer to CLI projects and to `ng-packagr`. Those transformers downleveled constructor parameters/ore removed decorators at all to fix (1) and (2). Also `emitMetadataDecorator` has been disabled by default in CLI projects. For bazel release output this didn't surface either because tsickle still ran by default in prodmode output. This was never an ideal solution though, and we'd also like to not run tsickle by default in the Bazel prodmode output. It was not ideal because we just applied workarounds at Angular compiler derivatives. Ideally, TypeScript would just emit proper metadata that isn't evaluated at top-level, but given they marked it as limitation and the decorator proposal is still stage 2, this won't happen any time soon (if at all). The ideal solution is that we downlevel decorators (as previously done with tsickle by default) as part of the Angular compiler (a level higher; and one below the actual TypeScript compiler limitation). This fixes the issues with the common `forwardRef` pattern (1), and also fixes (2). It also allows us to reduce code duplication in the compiler derivatives (e.g. ng-packagr), fixes the left-behind standalone ngc comsumers, and we can disable tsickle in Angular bazel (as already done with this commit). Fixes angular#30106. Fixes angular#30586. Fixes angular#30141. Resolves FW-2196. Resolves FW-2199.
1 parent 1b55da1 commit a289fa5

File tree

12 files changed

+1129
-342
lines changed

12 files changed

+1129
-342
lines changed

packages/bazel/src/ngc-wrapped/index.ts

+14-21
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,8 @@ export function compile({
191191
fileLoader = new UncachedFileLoader();
192192
}
193193

194-
compilerOpts.annotationsAs = 'static fields';
195-
if (!bazelOpts.es5Mode) {
196-
if (bazelOpts.workspaceName === 'google3') {
197-
compilerOpts.annotateForClosureCompiler = true;
198-
} else {
199-
compilerOpts.annotateForClosureCompiler = false;
200-
}
201-
}
202-
203194
// Detect from compilerOpts whether the entrypoint is being invoked in Ivy mode.
204195
const isInIvyMode = !!compilerOpts.enableIvy;
205-
206-
// Disable downleveling and Closure annotation if in Ivy mode.
207-
if (isInIvyMode) {
208-
compilerOpts.annotationsAs = 'decorators';
209-
}
210-
211196
if (!compilerOpts.rootDirs) {
212197
throw new Error('rootDirs is not set!');
213198
}
@@ -264,9 +249,6 @@ export function compile({
264249
}
265250

266251
if (isInIvyMode) {
267-
// Also need to disable decorator downleveling in the BazelHost in Ivy mode.
268-
bazelHost.transformDecorators = false;
269-
270252
const delegate = bazelHost.shouldSkipTsickleProcessing.bind(bazelHost);
271253
bazelHost.shouldSkipTsickleProcessing = (fileName: string) => {
272254
// The base implementation of shouldSkipTsickleProcessing checks whether `fileName` is part of
@@ -277,12 +259,23 @@ export function compile({
277259
};
278260
}
279261

262+
// Always disable tsickle decorator transforming in the tsickle compiler host. The
263+
// Angular compilers have their own logic for decorator processing and we wouldn't
264+
// want tsickle to interfere with that.
265+
bazelHost.transformDecorators = false;
266+
267+
// By default, annotations for closure compiler are disabled in prodmode,
268+
// unless the workspace is known to be `google3`.
269+
if (!bazelOpts.es5Mode && compilerOpts.annotateForClosureCompiler !== undefined) {
270+
compilerOpts.annotateForClosureCompiler = bazelOpts.workspaceName === 'google3';
271+
}
272+
273+
// The `annotateForClosureCompiler` Angular compiler option is not respected by default
274+
// as ngc-wrapped handles tsickle emit on its own. This means that we need to update
275+
// the tsickle compiler host based on the `annotateForClosureCompiler` flag.
280276
if (compilerOpts.annotateForClosureCompiler) {
281277
bazelHost.transformTypesToClosure = true;
282278
}
283-
if (compilerOpts.annotateForClosureCompiler || compilerOpts.annotationsAs === 'static fields') {
284-
bazelHost.transformDecorators = true;
285-
}
286279

287280
const origBazelHostFileExist = bazelHost.fileExists;
288281
bazelHost.fileExists = (fileName: string) => {

packages/bazel/test/ng_package/example_package.golden

+1-4
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,6 @@ Hello
274274
MyService.decorators = [
275275
{ type: i0.Injectable, args: [{ providedIn: 'root' },] }
276276
];
277-
/** @nocollapse */
278277
MyService.ctorParameters = function () { return [
279278
{ type: MySecondService }
280279
]; };
@@ -605,15 +604,14 @@ let MyService = /** @class */ (() => {
605604
MyService.decorators = [
606605
{ type: Injectable, args: [{ providedIn: 'root' },] }
607606
];
608-
/** @nocollapse */
609607
MyService.ctorParameters = () => [
610608
{ type: MySecondService }
611609
];
612610
MyService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(i0.ɵɵinject(i1.MySecondService)); }, token: MyService, providedIn: "root" });
613611
return MyService;
614612
})();
615613
export { MyService };
616-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL2JhemVsL3Rlc3QvbmdfcGFja2FnZS9leGFtcGxlL2ltcG9ydHMvcHVibGljLWFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxVQUFVLENBQUM7OztBQUV6QztJQUFBLE1BQ2EsU0FBUztRQUNwQixZQUFtQixhQUE4QjtZQUE5QixrQkFBYSxHQUFiLGFBQWEsQ0FBaUI7UUFBRyxDQUFDOzs7Z0JBRnRELFVBQVUsU0FBQyxFQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUM7Ozs7Z0JBRnhCLGVBQWU7OztvQkFUdkI7S0FjQztTQUZZLFNBQVMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEBsaWNlbnNlXG4gKiBDb3B5cmlnaHQgR29vZ2xlIExMQyBBbGwgUmlnaHRzIFJlc2VydmVkLlxuICpcbiAqIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IGFuIE1JVC1zdHlsZSBsaWNlbnNlIHRoYXQgY2FuIGJlXG4gKiBmb3VuZCBpbiB0aGUgTElDRU5TRSBmaWxlIGF0IGh0dHBzOi8vYW5ndWxhci5pby9saWNlbnNlXG4gKi9cblxuaW1wb3J0IHtJbmplY3RhYmxlfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB7TXlTZWNvbmRTZXJ2aWNlfSBmcm9tICcuL3NlY29uZCc7XG5cbkBJbmplY3RhYmxlKHtwcm92aWRlZEluOiAncm9vdCd9KVxuZXhwb3J0IGNsYXNzIE15U2VydmljZSB7XG4gIGNvbnN0cnVjdG9yKHB1YmxpYyBzZWNvbmRTZXJ2aWNlOiBNeVNlY29uZFNlcnZpY2UpIHt9XG59XG4iXX0=
614+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL2JhemVsL3Rlc3QvbmdfcGFja2FnZS9leGFtcGxlL2ltcG9ydHMvcHVibGljLWFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxVQUFVLENBQUM7OztBQUV6QztJQUFBLE1BQ2EsU0FBUztRQUNwQixZQUFtQixhQUE4QjtZQUE5QixrQkFBYSxHQUFiLGFBQWEsQ0FBaUI7UUFBRyxDQUFDOzs7Z0JBRnRELFVBQVUsU0FBQyxFQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUM7OztnQkFFSSxlQUFlOzs7b0JBYm5EO0tBY0M7U0FGWSxTQUFTIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBAbGljZW5zZVxuICogQ29weXJpZ2h0IEdvb2dsZSBMTEMgQWxsIFJpZ2h0cyBSZXNlcnZlZC5cbiAqXG4gKiBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSBhbiBNSVQtc3R5bGUgbGljZW5zZSB0aGF0IGNhbiBiZVxuICogZm91bmQgaW4gdGhlIExJQ0VOU0UgZmlsZSBhdCBodHRwczovL2FuZ3VsYXIuaW8vbGljZW5zZVxuICovXG5cbmltcG9ydCB7SW5qZWN0YWJsZX0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQge015U2Vjb25kU2VydmljZX0gZnJvbSAnLi9zZWNvbmQnO1xuXG5ASW5qZWN0YWJsZSh7cHJvdmlkZWRJbjogJ3Jvb3QnfSlcbmV4cG9ydCBjbGFzcyBNeVNlcnZpY2Uge1xuICBjb25zdHJ1Y3RvcihwdWJsaWMgc2Vjb25kU2VydmljZTogTXlTZWNvbmRTZXJ2aWNlKSB7fVxufVxuIl19
617615

618616
--- esm2015/imports/second.js ---
619617

@@ -835,7 +833,6 @@ let MyService = /** @class */ (() => {
835833
MyService.decorators = [
836834
{ type: Injectable, args: [{ providedIn: 'root' },] }
837835
];
838-
/** @nocollapse */
839836
MyService.ctorParameters = () => [
840837
{ type: MySecondService }
841838
];

packages/compiler-cli/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ts_library(
2929
"//packages/compiler-cli/src/ngtsc/file_system",
3030
"//packages/compiler-cli/src/ngtsc/indexer",
3131
"//packages/compiler-cli/src/ngtsc/perf",
32+
"//packages/compiler-cli/src/ngtsc/reflection",
3233
"//packages/compiler-cli/src/ngtsc/typecheck",
3334
"@npm//@bazel/typescript",
3435
"@npm//@types/node",

packages/compiler-cli/src/main.ts

+22-43
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,9 @@ export function mainDiagnosticsForTest(
8989
}
9090

9191
function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined {
92-
const transformDecorators =
93-
(options.enableIvy === false && options.annotationsAs !== 'decorators');
94-
const transformTypesToClosure = options.annotateForClosureCompiler;
95-
if (!transformDecorators && !transformTypesToClosure) {
92+
if (!options.annotateForClosureCompiler) {
9693
return undefined;
9794
}
98-
if (transformDecorators) {
99-
// This is needed as a workaround for https://github.com./angular/tsickle/issues/635
100-
// Otherwise tsickle might emit references to non imported values
101-
// as TypeScript elided the import.
102-
options.emitDecoratorMetadata = true;
103-
}
10495
const tsickleHost: Pick<
10596
tsickle.TsickleHost,
10697
'shouldSkipTsickleProcessing'|'pathToModuleName'|'shouldIgnoreWarningsForPath'|
@@ -115,41 +106,29 @@ function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|un
115106
googmodule: false,
116107
untyped: true,
117108
convertIndexImportShorthand: false,
118-
transformDecorators,
119-
transformTypesToClosure,
109+
// Decorators are transformed as part of the Angular compiler programs. To avoid
110+
// conflicts, we disable decorator transformations for tsickle.
111+
transformDecorators: false,
112+
transformTypesToClosure: true,
120113
};
121114

122-
if (options.annotateForClosureCompiler || options.annotationsAs === 'static fields') {
123-
return ({
124-
program,
125-
targetSourceFile,
126-
writeFile,
127-
cancellationToken,
128-
emitOnlyDtsFiles,
129-
customTransformers = {},
130-
host,
131-
options
132-
}) =>
133-
// tslint:disable-next-line:no-require-imports only depend on tsickle if requested
134-
require('tsickle').emitWithTsickle(
135-
program, {...tsickleHost, options, host, moduleResolutionHost: host}, host, options,
136-
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, {
137-
beforeTs: customTransformers.before,
138-
afterTs: customTransformers.after,
139-
});
140-
} else {
141-
return ({
142-
program,
143-
targetSourceFile,
144-
writeFile,
145-
cancellationToken,
146-
emitOnlyDtsFiles,
147-
customTransformers = {},
148-
}) =>
149-
program.emit(
150-
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles,
151-
{after: customTransformers.after, before: customTransformers.before});
152-
}
115+
return ({
116+
program,
117+
targetSourceFile,
118+
writeFile,
119+
cancellationToken,
120+
emitOnlyDtsFiles,
121+
customTransformers = {},
122+
host,
123+
options
124+
}) =>
125+
// tslint:disable-next-line:no-require-imports only depend on tsickle if requested
126+
require('tsickle').emitWithTsickle(
127+
program, {...tsickleHost, options, host, moduleResolutionHost: host}, host, options,
128+
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, {
129+
beforeTs: customTransformers.before,
130+
afterTs: customTransformers.after,
131+
});
153132
}
154133

155134
export interface NgcParsedConfiguration extends ParsedConfiguration {

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ export class NgCompiler {
754754
/**
755755
* Determine if the given `Program` is @angular/core.
756756
*/
757-
function isAngularCorePackage(program: ts.Program): boolean {
757+
export function isAngularCorePackage(program: ts.Program): boolean {
758758
// Look for its_just_angular.ts somewhere in the program.
759759
const r3Symbols = getR3SymbolsFile(program);
760760
if (r3Symbols === null) {

0 commit comments

Comments
 (0)