From 1e04700f903fd5ec6e36ee8153f73d95fae273ae Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Thu, 2 Jul 2020 18:06:36 -0700 Subject: [PATCH 1/2] Implement support for the new "import type" TypeScript construct --- apps/api-extractor/src/analyzer/AstImport.ts | 15 +++ .../src/analyzer/ExportAnalyzer.ts | 31 ++++- .../src/generators/DtsEmitHelpers.ts | 18 ++- .../config/build-config.json | 1 + .../api-extractor-scenarios.api.json | 108 ++++++++++++++++++ .../importType/api-extractor-scenarios.api.md | 25 ++++ .../etc/test-outputs/importType/rollup.d.ts | 16 +++ .../src/importType/index.ts | 16 +++ 8 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.json create mode 100644 build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.md create mode 100644 build-tests/api-extractor-scenarios/etc/test-outputs/importType/rollup.d.ts create mode 100644 build-tests/api-extractor-scenarios/src/importType/index.ts diff --git a/apps/api-extractor/src/analyzer/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts index 7d7a6b5fb7a..92454ee4e2b 100644 --- a/apps/api-extractor/src/analyzer/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -42,6 +42,7 @@ export interface IAstImportOptions { readonly importKind: AstImportKind; readonly modulePath: string; readonly exportName: string; + readonly isTypeOnly: boolean; } /** @@ -82,6 +83,17 @@ export class AstImport { */ public readonly exportName: string; + /** + * Whether it is a type-only import, for example: + * + * ```ts + * import type { X } from "y"; + * ``` + * + * This is set to true ONLY if the type-only form is used in *every* reference to this AstImport. + */ + public isTypeOnlyEverywhere: boolean; + /** * If this import statement refers to an API from an external package that is tracked by API Extractor * (according to `PackageMetadataManager.isAedocSupportedFor()`), then this property will return the @@ -102,6 +114,9 @@ export class AstImport { this.modulePath = options.modulePath; this.exportName = options.exportName; + // We start with this assumption, but it may get changed later if non-type-only import is encountered. + this.isTypeOnlyEverywhere = options.isTypeOnly; + this.key = AstImport.getKey(options); } diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index f87c1840c6f..7ac791ea39a 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -444,7 +444,8 @@ export class ExportAnalyzer { return this._fetchAstImport(declarationSymbol, { importKind: AstImportKind.NamedImport, modulePath: externalModulePath, - exportName: exportName + exportName: exportName, + isTypeOnly: false }); } @@ -498,7 +499,8 @@ export class ExportAnalyzer { return this._fetchAstImport(undefined, { importKind: AstImportKind.StarImport, exportName: declarationSymbol.name, - modulePath: externalModulePath + modulePath: externalModulePath, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration) }); } @@ -530,7 +532,8 @@ export class ExportAnalyzer { return this._fetchAstImport(declarationSymbol, { importKind: AstImportKind.NamedImport, modulePath: externalModulePath, - exportName: exportName + exportName: exportName, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration) }); } @@ -563,7 +566,8 @@ export class ExportAnalyzer { return this._fetchAstImport(declarationSymbol, { importKind: AstImportKind.DefaultImport, modulePath: externalModulePath, - exportName + exportName, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration) }); } @@ -604,7 +608,8 @@ export class ExportAnalyzer { return this._fetchAstImport(declarationSymbol, { importKind: AstImportKind.EqualsImport, modulePath: externalModuleName, - exportName: variableName + exportName: variableName, + isTypeOnly: false }); } } @@ -624,6 +629,13 @@ export class ExportAnalyzer { return undefined; } + private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean { + if (importDeclaration.importClause) { + return !!importDeclaration.importClause.isTypeOnly; + } + return false; + } + private _getExportOfSpecifierAstModule( exportName: string, importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration, @@ -700,7 +712,8 @@ export class ExportAnalyzer { return this._fetchAstImport(astSymbol.followedSymbol, { importKind: AstImportKind.NamedImport, modulePath: starExportedModule.externalModulePath, - exportName: exportName + exportName: exportName, + isTypeOnly: false }); } @@ -815,6 +828,12 @@ export class ExportAnalyzer { addIfMissing: true }); } + } else { + // If we encounter at least one import that does not use the type-only form, + // then the .d.ts rollup will NOT use "import type". + if (!options.isTypeOnly) { + astImport.isTypeOnlyEverywhere = false; + } } return astImport; diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index fe05d8fc057..6a08aff563f 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -18,28 +18,34 @@ export class DtsEmitHelpers { collectorEntity: CollectorEntity, astImport: AstImport ): void { + const importPrefix: string = astImport.isTypeOnlyEverywhere ? 'import type' : 'import'; + switch (astImport.importKind) { case AstImportKind.DefaultImport: if (collectorEntity.nameForEmit !== astImport.exportName) { - stringWriter.write(`import { default as ${collectorEntity.nameForEmit} }`); + stringWriter.write(`${importPrefix} { default as ${collectorEntity.nameForEmit} }`); } else { - stringWriter.write(`import ${astImport.exportName}`); + stringWriter.write(`${importPrefix} ${astImport.exportName}`); } stringWriter.writeLine(` from '${astImport.modulePath}';`); break; case AstImportKind.NamedImport: if (collectorEntity.nameForEmit !== astImport.exportName) { - stringWriter.write(`import { ${astImport.exportName} as ${collectorEntity.nameForEmit} }`); + stringWriter.write(`${importPrefix} { ${astImport.exportName} as ${collectorEntity.nameForEmit} }`); } else { - stringWriter.write(`import { ${astImport.exportName} }`); + stringWriter.write(`${importPrefix} { ${astImport.exportName} }`); } stringWriter.writeLine(` from '${astImport.modulePath}';`); break; case AstImportKind.StarImport: - stringWriter.writeLine(`import * as ${collectorEntity.nameForEmit} from '${astImport.modulePath}';`); + stringWriter.writeLine( + `${importPrefix} * as ${collectorEntity.nameForEmit} from '${astImport.modulePath}';` + ); break; case AstImportKind.EqualsImport: - stringWriter.writeLine(`import ${collectorEntity.nameForEmit} = require('${astImport.modulePath}');`); + stringWriter.writeLine( + `${importPrefix} ${collectorEntity.nameForEmit} = require('${astImport.modulePath}');` + ); break; default: throw new InternalError('Unimplemented AstImportKind'); diff --git a/build-tests/api-extractor-scenarios/config/build-config.json b/build-tests/api-extractor-scenarios/config/build-config.json index f8ad516b6bd..5ccfcc657ed 100644 --- a/build-tests/api-extractor-scenarios/config/build-config.json +++ b/build-tests/api-extractor-scenarios/config/build-config.json @@ -23,6 +23,7 @@ "exportStar3", "functionOverload", "importEquals", + "importType", "inconsistentReleaseTags", "internationalCharacters", "preapproved", diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..28990cfed4b --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.json @@ -0,0 +1,108 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1003, + "oldestForwardsCompatibleVersion": 1001 + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "members": [ + { + "kind": "Interface", + "canonicalReference": "api-extractor-scenarios!A:interface", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface A extends " + }, + { + "kind": "Reference", + "text": "Lib1Class", + "canonicalReference": "api-extractor-lib1-test!Lib1Class:class" + }, + { + "kind": "Content", + "text": " " + } + ], + "releaseTag": "Public", + "name": "A", + "members": [], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 3 + } + ] + }, + { + "kind": "Interface", + "canonicalReference": "api-extractor-scenarios!B:interface", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface B extends " + }, + { + "kind": "Reference", + "text": "Lib1Interface", + "canonicalReference": "api-extractor-lib1-test!Lib1Interface:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "releaseTag": "Public", + "name": "B", + "members": [], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 3 + } + ] + }, + { + "kind": "Interface", + "canonicalReference": "api-extractor-scenarios!C:interface", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface C extends " + }, + { + "kind": "Reference", + "text": "Renamed", + "canonicalReference": "api-extractor-lib1-test!Lib1Interface:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "releaseTag": "Public", + "name": "C", + "members": [], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 3 + } + ] + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..eb8d113f635 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/api-extractor-scenarios.api.md @@ -0,0 +1,25 @@ +## API Report File for "api-extractor-scenarios" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Lib1Class } from 'api-extractor-lib1-test'; +import { Lib1Interface } from 'api-extractor-lib1-test'; + +// @public (undocumented) +export interface A extends Lib1Class { +} + +// @public (undocumented) +export interface B extends Lib1Interface { +} + +// @public (undocumented) +export interface C extends Lib1Interface { +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/importType/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/rollup.d.ts new file mode 100644 index 00000000000..79be312a695 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/importType/rollup.d.ts @@ -0,0 +1,16 @@ +import type { Lib1Class } from 'api-extractor-lib1-test'; +import { Lib1Interface } from 'api-extractor-lib1-test'; + +/** @public */ +export declare interface A extends Lib1Class { +} + +/** @public */ +export declare interface B extends Lib1Interface { +} + +/** @public */ +export declare interface C extends Lib1Interface { +} + +export { } diff --git a/build-tests/api-extractor-scenarios/src/importType/index.ts b/build-tests/api-extractor-scenarios/src/importType/index.ts new file mode 100644 index 00000000000..e9f48f45d1e --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/importType/index.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { Lib1Class, Lib1Interface } from 'api-extractor-lib1-test'; + +// This should prevent Lib1Interface from being emitted as a type-only import, even though B uses it that way. +import { Lib1Interface as Renamed } from 'api-extractor-lib1-test'; + +/** @public */ +export interface A extends Lib1Class {} + +/** @public */ +export interface B extends Lib1Interface {} + +/** @public */ +export interface C extends Renamed {} From d0eef699ac36dc349408febb7488734b52bcd708 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Thu, 2 Jul 2020 21:05:48 -0700 Subject: [PATCH 2/2] rush change --- .../octogonz-import-type_2020-07-03-04-05.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/api-extractor/octogonz-import-type_2020-07-03-04-05.json diff --git a/common/changes/@microsoft/api-extractor/octogonz-import-type_2020-07-03-04-05.json b/common/changes/@microsoft/api-extractor/octogonz-import-type_2020-07-03-04-05.json new file mode 100644 index 00000000000..ece18cd32c7 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/octogonz-import-type_2020-07-03-04-05.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Add support for \"import type\" imports (new in TypeScript 3.8)", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "4673363+octogonz@users.noreply.github.com" +} \ No newline at end of file