UNPKG

typescript-transform-paths

Version:

Transforms module resolution paths using TypeScript path mapping and/or custom paths

169 lines 8.32 kB
/** * UPDATE: * * TODO - In next major version, we can remove this file entirely due to TS PR 57223 * https://github.com/microsoft/TypeScript/pull/57223 * * This file and its contents are due to an issue in TypeScript (affecting _at least_ up to 4.1) which causes type * elision to break during emit for nodes which have been transformed. Specifically, if the 'original' property is set, * elision functionality no longer works. * * This results in module specifiers for types being output in import/export declarations in the compiled _JS files_ * * The logic herein compensates for that issue by recreating type elision separately so that the transformer can update * the clause with the properly elided information * * Issues: * * - See https://github.com/LeDDGroup/typescript-transform-paths/issues/184 * - See https://github.com/microsoft/TypeScript/issues/40603 * - See https://github.com/microsoft/TypeScript/issues/31446 * * @example * // a.ts * export type A = string; * export const B = 2; * * // b.ts * import { A, B } from "./b"; * export { A } from "./b"; * * // Expected output for b.js * import { B } from "./b"; * * // Actual output for b.js * import { A, B } from "./b"; * export { A } from "./b"; */ import { Debug, ImportsNotUsedAsValues, isInJSFile, } from "typescript"; export function elideImportOrExportDeclaration(context, node, newModuleSpecifier, resolver) { const { tsInstance, factory } = context; const { compilerOptions } = context; const { visitNode, isNamedImportBindings, isImportSpecifier, SyntaxKind, visitNodes, isNamedExportBindings, // 3.8 does not have this, so we have to define it ourselves // isNamespaceExport, isIdentifier, isExportSpecifier, } = tsInstance; const isNamespaceExport = tsInstance.isNamespaceExport ?? ((node) => node.kind === SyntaxKind.NamespaceExport); if (tsInstance.isImportDeclaration(node)) { // Do not elide a side-effect only import declaration. // import "foo"; if (!node.importClause) return node.importClause; // Always elide type-only imports if (node.importClause.isTypeOnly) return undefined; const importClause = visitNode(node.importClause, visitImportClause); if (importClause || compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve || compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error) return factory.updateImportDeclaration(node, /*modifiers*/ undefined, importClause, newModuleSpecifier, // This will be changed in the next release of TypeScript, but by that point we can drop elision entirely // @ts-expect-error TS(2339) FIXME: Property 'attributes' does not exist on type 'ImportDeclaration'. node.attributes || node.assertClause); else return undefined; } else { if (node.isTypeOnly) return undefined; if (!node.exportClause || node.exportClause.kind === SyntaxKind.NamespaceExport) { // never elide `export <whatever> from <whereever>` declarations - // they should be kept for sideffects/untyped exports, even when the // type checker doesn't know about any exports return node; } const allowEmpty = !!compilerOptions["verbatimModuleSyntax"] || (!!node.moduleSpecifier && (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve || compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error)); const exportClause = visitNode(node.exportClause, ((bindings) => visitNamedExportBindings(bindings, allowEmpty)), isNamedExportBindings); return exportClause ? factory.updateExportDeclaration(node, /*modifiers*/ undefined, node.isTypeOnly, exportClause, newModuleSpecifier, // This will be changed in the next release of TypeScript, but by that point we can drop elision entirely // @ts-expect-error TS(2339) FIXME: Property 'attributes' does not exist on type 'ExportDeclaration'. node.attributes || node.assertClause) : undefined; } /* ********************************************************* * * Helpers * ********************************************************* */ // The following visitors are adapted from the TS source-base src/compiler/transformers/ts /** * Visits an import clause, eliding it if it is not referenced. * * @param node The import clause node. */ function visitImportClause(node) { // Elide the import clause if we elide both its name and its named bindings. const name = shouldEmitAliasDeclaration(node) ? node.name : undefined; const namedBindings = visitNode(node.namedBindings, visitNamedImportBindings, isNamedImportBindings); return name || namedBindings ? factory.updateImportClause(node, /*isTypeOnly*/ false, name, namedBindings) : undefined; } /** * Visits named import bindings, eliding it if it is not referenced. * * @param node The named import bindings node. */ function visitNamedImportBindings(node) { if (node.kind === SyntaxKind.NamespaceImport) { // Elide a namespace import if it is not referenced. return shouldEmitAliasDeclaration(node) ? node : undefined; } else { // Elide named imports if all of its import specifiers are elided. const allowEmpty = compilerOptions["verbatimModuleSyntax"] || (compilerOptions.preserveValueImports && (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve || compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error)); const elements = visitNodes(node.elements, visitImportSpecifier, isImportSpecifier); return allowEmpty || tsInstance.some(elements) ? factory.updateNamedImports(node, elements) : undefined; } } /** * Visits an import specifier, eliding it if it is not referenced. * * @param node The import specifier node. */ function visitImportSpecifier(node) { // Elide an import specifier if it is not referenced. return !node.isTypeOnly && shouldEmitAliasDeclaration(node) ? node : undefined; } /** Visits named exports, eliding it if it does not contain an export specifier that resolves to a value. */ function visitNamedExports(node, allowEmpty) { // Elide the named exports if all of its export specifiers were elided. const elements = visitNodes(node.elements, visitExportSpecifier, isExportSpecifier); return allowEmpty || tsInstance.some(elements) ? factory.updateNamedExports(node, elements) : undefined; } function visitNamedExportBindings(node, allowEmpty) { return isNamespaceExport(node) ? visitNamespaceExports(node) : visitNamedExports(node, allowEmpty); } function visitNamespaceExports(node) { // Note: This may not work entirely properly, more likely it's just extraneous, but this won't matter soon, // as we'll be removing elision entirely return factory.updateNamespaceExport(node, Debug.checkDefined(visitNode(node.name, (n) => n, isIdentifier))); } /** * Visits an export specifier, eliding it if it does not resolve to a value. * * @param node The export specifier node. */ function visitExportSpecifier(node) { // Elide an export specifier if it does not reference a value. return !node.isTypeOnly && (compilerOptions["verbatimModuleSyntax"] || resolver.isValueAliasDeclaration(node)) ? node : undefined; } function shouldEmitAliasDeclaration(node) { return (!!compilerOptions["verbatimModuleSyntax"] || isInJSFile(node) || (compilerOptions.preserveValueImports ? resolver.isValueAliasDeclaration(node) : resolver.isReferencedAliasDeclaration(node))); } } // endregion //# sourceMappingURL=elide-import-export.js.map