Skip to content

Commit 55929f2

Browse files
authored
Transform mixed type/value imports to type-only imports (Khan#273)
* Transform mixed type/value imports to type-only imports ```ts import { type A, B } from "./a" ``` is invalid TypeScript, so we transform it into two statements, one being a [type-only import](https://www.typescriptlang.org/docs/handbook/modules.html#importing-types). Also updated `trackComments` to handle multiple nodes, which you might have passed to `replaceWithMultiple`. * Fixed type checking errors Sorry, my editor wasn't configured to shout at me.
1 parent 04e5e73 commit 55929f2

File tree

5 files changed

+118
-26
lines changed

5 files changed

+118
-26
lines changed

src/transform.ts

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import * as t from "../babel/packages/babel-types/src/index";
1+
import * as t from "@babel/types";
2+
import { ImportSpecifier, ImportDeclaration } from "@babel/types";
23

34
import * as declare from "./transforms/declare";
45
import * as reactTypes from "./transforms/react-types";
56
import * as objectType from "./transforms/object-type";
67
import * as utilityTypes from "./transforms/utility-types";
78

8-
import { trackComments } from "./util";
9+
import { trackComments, partition, returning } from "./util";
910

1011
const locToString = (loc) =>
1112
`${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
@@ -575,31 +576,89 @@ export const transform = {
575576
ImportDeclaration: {
576577
exit(path, state) {
577578
stripSuffixFromImportSource(path);
579+
let replacementNode = null;
580+
581+
const {
582+
importKind,
583+
specifiers,
584+
source,
585+
leadingComments,
586+
trailingComments,
587+
loc,
588+
} = path.node as ImportDeclaration;
589+
578590
if (
579-
path.node.importKind === "typeof" &&
580-
t.isImportDefaultSpecifier(path.node.specifiers[0])
591+
importKind === "typeof" &&
592+
t.isImportDefaultSpecifier(specifiers[0])
581593
) {
582-
const {
583-
specifiers,
584-
source,
585-
leadingComments,
586-
trailingComments,
587-
loc,
588-
} = path.node;
589-
const replacementNode = t.tsTypeAliasDeclaration(
594+
replacementNode = t.tsTypeAliasDeclaration(
590595
specifiers[0].local,
591596
undefined,
592597
t.tsTypeQuery(t.tsImportType(source, t.identifier("default")))
593598
);
594599
replacementNode.leadingComments = leadingComments;
595600
replacementNode.trailingComments = trailingComments;
596601
replacementNode.loc = loc;
602+
} else if (importKind === "value") {
603+
// find any "type" imports within the ImportSpecifier list
604+
// and pull them out to separate ImportDeclarations
597605

598-
trackComments(replacementNode, state);
606+
const [valSpecs, typeSpecs] = partition(
607+
specifiers,
608+
(s): s is ImportSpecifier =>
609+
s.type === "ImportSpecifier" && s.importKind === "type"
610+
);
599611

600-
path.replaceWith(replacementNode);
601-
} else {
602-
trackComments(path.node, state);
612+
if (typeSpecs.length > 0) {
613+
const typeNode = t.importDeclaration(
614+
typeSpecs.map((s) =>
615+
t.importSpecifier(
616+
returning(t.identifier(s.local.name), (n) => {
617+
n.loc = s.local.loc;
618+
}),
619+
returning(
620+
t.identifier(
621+
s.imported.type === "Identifier"
622+
? s.imported.name
623+
: s.imported.value
624+
),
625+
(n) => {
626+
n.loc = s.imported.loc;
627+
}
628+
)
629+
)
630+
),
631+
source
632+
);
633+
typeNode.leadingComments = leadingComments;
634+
typeNode.importKind = "type";
635+
typeNode.loc = loc;
636+
637+
const valNode = t.importDeclaration(valSpecs, source);
638+
valNode.trailingComments = trailingComments;
639+
valNode.loc = {
640+
start: {
641+
line: loc.start.line + 1,
642+
column: 0,
643+
},
644+
end: {
645+
line: loc.start.line + 1,
646+
column: 0,
647+
},
648+
};
649+
650+
replacementNode = [typeNode, valNode];
651+
}
652+
}
653+
654+
trackComments(replacementNode ?? path.node, state);
655+
656+
if (replacementNode != null) {
657+
if (Array.isArray(replacementNode)) {
658+
path.replaceWithMultiple(replacementNode);
659+
} else {
660+
path.replaceWith(replacementNode);
661+
}
603662
}
604663
},
605664
},

src/util.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,53 @@
2020
* @param {*} state
2121
*/
2222
export const trackComments = (node, state) => {
23-
if (node.leadingComments) {
24-
for (const comment of node.leadingComments) {
23+
let leadingNode = node,
24+
trailingNode = node;
25+
26+
if (Array.isArray(node)) {
27+
leadingNode = node[0];
28+
trailingNode = node[node.length - 1];
29+
}
30+
31+
if (leadingNode.leadingComments) {
32+
for (const comment of leadingNode.leadingComments) {
2533
const { start, end } = comment;
2634
const key = `${start}:${end}`;
2735

2836
if (state.commentsToNodesMap.has(key)) {
29-
state.commentsToNodesMap.get(key).leading = node;
37+
state.commentsToNodesMap.get(key).leading = leadingNode;
3038
} else {
31-
state.commentsToNodesMap.set(key, { leading: node });
39+
state.commentsToNodesMap.set(key, { leading: leadingNode });
3240
}
3341
}
3442
}
35-
if (node.trailingComments) {
36-
for (const comment of node.trailingComments) {
43+
if (trailingNode.trailingComments) {
44+
for (const comment of trailingNode.trailingComments) {
3745
const { start, end } = comment;
3846
const key = `${start}:${end}`;
3947

4048
if (state.commentsToNodesMap.has(key)) {
41-
state.commentsToNodesMap.get(key).trailing = node;
49+
state.commentsToNodesMap.get(key).trailing = trailingNode;
4250
} else {
43-
state.commentsToNodesMap.set(key, { trailing: node });
51+
state.commentsToNodesMap.set(key, { trailing: trailingNode });
4452
}
4553
}
4654
}
4755
};
56+
57+
export function partition<T, U extends T>(
58+
iter: Iterable<T>,
59+
fn: (val: T) => val is U
60+
): [T[], U[]] {
61+
const l = [],
62+
r = [];
63+
for (const v of iter) {
64+
(fn(v) ? r : l).push(v);
65+
}
66+
return [l, r];
67+
}
68+
69+
export function returning<T>(v: T, fn: (arg: T) => unknown): T {
70+
fn(v);
71+
return v;
72+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// comment before
2+
import { type A, B } from "./a.js";
3+
// comment after
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// comment before
2+
import type { A } from "./a";
3+
import { B } from "./a"; // comment after
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
import { type A, type B, C } from "./depA";
2-
import { type D, type E, F } from "../depB";
1+
import type { A, B } from "./depA";
2+
import { C } from "./depA";
3+
import type { D, E } from "../depB";
4+
import { F } from "../depB";

0 commit comments

Comments
 (0)