/** * @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 * as ts from 'typescript'; /** * Describes a TypeScript transformation context with the internal emit * resolver exposed. There are requests upstream in TypeScript to expose * that as public API: https://github.com/microsoft/TypeScript/issues/17516.. */ interface TransformationContextWithResolver extends ts.TransformationContext { getEmitResolver: () => EmitResolver; } /** Describes a subset of the TypeScript internal emit resolver. */ interface EmitResolver { isReferencedAliasDeclaration?(node: ts.Node, checkChildren?: boolean): void; } /** * Patches the alias declaration reference resolution for a given transformation context * so that TypeScript knows about the specified alias declarations being referenced. * * This exists because TypeScript performs analysis of import usage before transformers * run and doesn't refresh its state after transformations. This means that imports * for symbols used as constructor types are elided due to their original type-only usage. * * In reality though, since we downlevel decorators and constructor parameters, we want * these symbols to be retained in the JavaScript output as they will be used as values * at runtime. We can instruct TypeScript to preserve imports for such identifiers by * creating a mutable clone of a given import specifier/clause or namespace, but that * has the downside of preserving the full import in the JS output. See: * https://github.com/microsoft/TypeScript/blob/3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/src/compiler/transformers/ts.ts#L242-L250. * * This is a trick the CLI used in the past for constructor parameter downleveling in JIT: * https://github.com/angular/angular-cli/blob/b3f84cc5184337666ce61c07b7b9df418030106f/packages/ngtools/webpack/src/transformers/ctor-parameters.ts#L323-L325 * The trick is not ideal though as it preserves the full import (as outlined before), and it * results in a slow-down due to the type checker being involved multiple times. The CLI * worked around this import preserving issue by having another complex post-process step that * detects and elides unused imports. Note that these unused imports could cause unused chunks * being generated by Webpack if the application or library is not marked as side-effect free. * * This is not ideal though, as we basically re-implement the complex import usage resolution * from TypeScript. We can do better by letting TypeScript do the import eliding, but providing * information about the alias declarations (e.g. import specifiers) that should not be elided * because they are actually referenced (as they will now appear in static properties). * * More information about these limitations with transformers can be found in: * 1. https://github.com/Microsoft/TypeScript/issues/17552. * 2. https://github.com/microsoft/TypeScript/issues/17516. * 3. https://github.com/angular/tsickle/issues/635. * * The patch we apply to tell TypeScript about actual referenced aliases (i.e. imported symbols), * matches conceptually with the logic that runs internally in TypeScript when the * `emitDecoratorMetadata` flag is enabled. TypeScript basically surfaces the same problem and * solves it conceptually the same way, but obviously doesn't need to access an `@internal` API. * * See below. Note that this uses sourcegraph as the TypeScript checker file doesn't display on * Github. * https://sourcegraph.com/github.com/microsoft/TypeScript@3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/-/blob/src/compiler/checker.ts#L31219-31257 */ export function patchAliasReferenceResolutionOrDie( context: ts.TransformationContext, referencedAliases: Set): void { // If the `getEmitResolver` method is not available, TS most likely changed the // internal structure of the transformation context. We will abort gracefully. if (!isTransformationContextWithEmitResolver(context)) { throwIncompatibleTransformationContextError(); return; } const emitResolver = context.getEmitResolver(); const originalReferenceResolution = emitResolver.isReferencedAliasDeclaration; // If the emit resolver does not have a function called `isReferencedAliasDeclaration`, then // we abort gracefully as most likely TS changed the internal structure of the emit resolver. if (originalReferenceResolution === undefined) { throwIncompatibleTransformationContextError(); return; } emitResolver.isReferencedAliasDeclaration = function(node, ...args) { if (isAliasImportDeclaration(node) && referencedAliases.has(node)) { return true; } return originalReferenceResolution.call(emitResolver, node, ...args); }; } /** * Gets whether a given node corresponds to an import alias declaration. Alias * declarations can be import specifiers, namespace imports or import clauses * as these do not declare an actual symbol but just point to a target declaration. */ export function isAliasImportDeclaration(node: ts.Node): node is ts.ImportSpecifier| ts.NamespaceImport|ts.ImportClause { return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node); } /** Whether the transformation context exposes its emit resolver. */ function isTransformationContextWithEmitResolver(context: ts.TransformationContext): context is TransformationContextWithResolver { return (context as Partial).getEmitResolver !== undefined; } /** * Throws an error about an incompatible TypeScript version for which the alias * declaration reference resolution could not be monkey-patched. The error will * also propose potential solutions that can be applied by developers. */ function throwIncompatibleTransformationContextError() { throw Error( 'Unable to downlevel Angular decorators due to an incompatible TypeScript ' + 'version.\nIf you recently updated TypeScript and this issue surfaces now, consider ' + 'downgrading.\n\n' + 'Please report an issue on the Angular repositories when this issue ' + 'surfaces and you are using a supposedly compatible TypeScript version.'); }