Skip to content

Commit

Permalink
feat(dts): add relative specifier remapping for import type, exports,…
Browse files Browse the repository at this point in the history
… and module declarations
  • Loading branch information
jacob-alford committed Feb 13, 2024
1 parent c15ea89 commit 7884ec8
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 7 deletions.
124 changes: 117 additions & 7 deletions src/TypesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,18 @@ export class TypesService {
}
}

type NodeMapper = (node: ts.Node, context: ts.TransformationContext) => ts.Node

const mapSourceFile =
(mapNode: NodeMapper) =>
(mapNode: Endomorphism<ts.Node>) =>
(context: ts.TransformationContext) =>
(sourceFile: ts.SourceFile | ts.Bundle): ts.SourceFile => {
const mapNodeAndChildren = (node: ts.Node): ts.Node => {
return ts.visitEachChild(mapNode(node, context), mapNodeAndChildren, context)
return ts.visitEachChild(mapNode(node), mapNodeAndChildren, context)
}
return mapNodeAndChildren(sourceFile) as ts.SourceFile
}

const isRelativePath = (path: string): boolean => /^\.\.?\//.test(path)
// See: https://github.com/microsoft/TypeScript/blob/a6414052a3eb66e30670f20c6597ee4b74067c73/src/compiler/path.ts#L101C12-L101C41
const isRelativePath = (path: string): boolean => /^\.\.?($|[\\/])/.test(path)

export const mapFileAndExtension: Endomorphism<Endomorphism<string>> =
remapExtenion => importPath => {
Expand All @@ -83,9 +82,33 @@ export const mapFileAndExtension: Endomorphism<Endomorphism<string>> =
return rewrittenPath.startsWith('.') ? rewrittenPath : `./${rewrittenPath}`
}

const rewriteRelativeImportSpecifier: (
export const rewriteRelativeImportSpecifier: (
remapExtension: Endomorphism<string>,
) => NodeMapper = remapExtension => node => {
) => Endomorphism<ts.Node> = remapExtension => node => {
// --- Import Declaration ----
// --------- from ------------
// import { foo } from './foo'
// ---------- to -------------
// import { foo } from './foo.(m|c)js'
// ---------------------------
// -- Import Namespace Decl --
// --------- from ------------
// import * as foo from './foo'
// ---------- to -------------
// import * as foo './foo.(m|c)js'
// ---------------------------
// ----- Default Import ------
// --------- from ------------
// import Foo from './foo'
// ---------- to -------------
// import Foo from './foo.(m|c)js'
// ---------------------------
// --- Import Declaration ----
// --------- from ------------
// import './foo'
// ---------- to -------------
// import './foo.(m|c)js'
// ---------------------------
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const importPath = node.moduleSpecifier.text

Expand All @@ -102,6 +125,93 @@ const rewriteRelativeImportSpecifier: (
)
}
}

// --- Import Type Node ------
// --------- from ------------
// import("./foo")
// ---------- to -------------
// import("./foo.(m|c)js")
// ---------------------------
if (
ts.isImportTypeNode(node) &&
ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteral(node.argument.literal)
) {
const importPath = node.argument.literal.text
if (isRelativePath(importPath)) {
const rewrittenPath = mapFileAndExtension(remapExtension)(importPath)
return ts.factory.updateImportTypeNode(
node,
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral(
rewrittenPath.startsWith('.') ? rewrittenPath : `./${rewrittenPath}`,
),
),
node.attributes,
node.qualifier,
node.typeArguments,
node.isTypeOf,
)
}
}

// --- Module Declaration ----
// --------- from ------------
// declare module './foo'
// ---------- to -------------
// declare module './foo.(m|c)js'
// ---------------------------
if (ts.isModuleDeclaration(node)) {
const name = node.name.text
const rewrittenPath = mapFileAndExtension(remapExtension)(name)
return ts.factory.updateModuleDeclaration(
node,
node.modifiers,
ts.factory.createStringLiteral(
rewrittenPath.startsWith('.') ? rewrittenPath : `./${rewrittenPath}`,
),
node.body,
)
}
// ----- Named exports -------
// --------- from ------------
// export { foo } from './foo'
// ---------- to -------------
// export { foo } from './foo.(m|c)js
// ---------------------------
// --- Export Declarations ---
// --------- from ------------
// export * from './foo'
// ---------- to -------------
// export * from './foo.(m|c)js'
// ---------------------------
// ---- Export Namespace -----
// --------- from ------------
// export * as foo from './foo'
// ---------- to -------------
// export * as foo from './foo.(m|c)js'
// ---------------------------
if (
ts.isExportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
const importPath = node.moduleSpecifier.text
if (isRelativePath(importPath)) {
const rewrittenPath = mapFileAndExtension(remapExtension)(importPath)
return ts.factory.updateExportDeclaration(
node,
node.modifiers,
node.isTypeOnly,
node.exportClause,
ts.factory.createStringLiteral(
rewrittenPath.startsWith('.') ? rewrittenPath : `./${rewrittenPath}`,
),
node.attributes,
)
}
}

return node
}

Expand Down
118 changes: 118 additions & 0 deletions tests/TypesService.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { printNode, ts } from 'ts-morph'

import * as Types from '../src/TypesService'

const t = <A, B>(a: A, b: B): readonly [A, B] => [a, b]
Expand All @@ -24,5 +26,121 @@ describe('relative import remapping', () => {
`../../../lib/foo/Foo.${ext}`,
)
})
describe('node remapping', () => {
it('rewrites named import declarations', () => {
const testNode = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports([
ts.factory.createImportSpecifier(
true,
undefined,
ts.factory.createIdentifier('foo'),
),
]),
),
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`import { type foo } from "./foo.${ext}";`)
})
it('rewrites namepsace import declarations', () => {
const testNode = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamespaceImport(ts.factory.createIdentifier('foo')),
),
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`import * as foo from "./foo.${ext}";`)
})
it('rewrites default import declarations', () => {
const testNode = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
ts.factory.createIdentifier('foo'),
undefined,
),
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`import foo from "./foo.${ext}";`)
})
it('rewrites effectful import declarations', () => {
const testNode = ts.factory.createImportDeclaration(
undefined,
undefined,
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`import "./foo.${ext}";`)
})
it('rewrites import types', () => {
const testNode = ts.factory.createImportTypeNode(
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('./foo')),
undefined,
undefined,
undefined,
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`import("./foo.${ext}")`)
})
it('rewrites module declaration names', () => {
const testNode = ts.factory.createModuleDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createIdentifier('./foo'),
ts.factory.createModuleBlock([]),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`declare module "./foo.${ext}" { }`)
})
it('rewrites export declarations', () => {
const testNode = ts.factory.createExportDeclaration(
[],
true,
undefined,
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`export type * from "./foo.${ext}";`)
})
it('rewrites export namespace declarations', () => {
const testNode = ts.factory.createExportDeclaration(
[],
false,
ts.factory.createNamespaceExport(ts.factory.createIdentifier('foo')),
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`export * as foo from "./foo.${ext}";`)
})
it('rewrites export assignment', () => {
const testNode = ts.factory.createExportDeclaration(
[],
false,
ts.factory.createNamedExports([
ts.factory.createExportSpecifier(true, 'foo', 'foo2'),
]),
ts.factory.createStringLiteral('./foo'),
)
const transformed = Types.rewriteRelativeImportSpecifier(rewrite)(testNode)
const result = printNode(transformed)
expect(result).toBe(`export { type foo as foo2 } from "./foo.${ext}";`)
})
})
})
})

0 comments on commit 7884ec8

Please sign in to comment.