Skip to content

Commit c07aeca

Browse files
authored
Add support for TypeScript import assignments (#149)
Closes #144
1 parent c641891 commit c07aeca

File tree

7 files changed

+390
-28
lines changed

7 files changed

+390
-28
lines changed

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ import g from ".";
254254
import h from "./constants";
255255
import i from "./styles";
256256

257+
// TypeScript import assignments.
258+
import J = require("../parent");
259+
import K = require("./sibling");
260+
export import L = require("an-npm-package");
261+
import M = require("different-npm-package");
262+
import N = Namespace;
263+
export import O = Namespace.A.B.C;
264+
import P = Namespace.A.C;
265+
257266
// Different types of exports:
258267
export { a } from "../..";
259268
export { b } from "/";
@@ -365,6 +374,8 @@ Side effect imports have `\u0000` _prepended_ to their `from` string (starts wit
365374

366375
Type imports have `\u0000` _appended_ to their `from` string (ends with `\u0000`). You can match them with `"\\u0000$"` – but you probably need more than that to avoid them also being matched by other regexes.
367376

377+
TypeScript import assignments have `\u0001` (for `import A = require("A")`) or `\u0002` (for `import A = B.C.D`) prepended to their `from` string (starts with `\u0001` or `\u0002`). It is _not_ possible to distinguish `export import A =` and `import A =`.
378+
368379
All imports that match the same regex are sorted internally as mentioned in [Sort order].
369380

370381
This is the default value for the `groups` option:
@@ -384,6 +395,8 @@ This is the default value for the `groups` option:
384395
// Relative imports.
385396
// Anything that starts with a dot.
386397
["^\\."],
398+
// TypeScript import assignments.
399+
["^\\u0001", "^\\u0002"],
387400
];
388401
```
389402

@@ -502,6 +515,8 @@ The final whitespace rule is that this plugin puts one import/export per line. I
502515

503516
No. This is intentional to keep things simple. Use some other sorting rule, such as [import/order], for sorting `require`. Or consider migrating your code using `require` to `import`. `import` is well supported these days.
504517

518+
The only `require`-like thing supported is TypeScript import assignments like `import Thing = require("something")`. They’re much easier to support since they are very restricted: The thing to the left of the `=` has to be a single identifier, and inside `require()` there has to be a single string literal. This makes it sortable as if it was `import Thing from "something"`.
519+
505520
### Why sort on `from`?
506521

507522
Some other import sorting rules sort based on the first name after `import`, rather than the string after `from`. This plugin intentionally sorts on the `from` string to be `git diff` friendly.
@@ -677,7 +692,7 @@ Use [custom grouping], setting the `groups` option to only have a single inner a
677692
For example, here’s the default value but changed to a single inner array:
678693

679694
```js
680-
[["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]];
695+
[["^\\u0000", "^node:", "^@?\\w", "^", "^\\.", "^\\u0001", "^\\u0002"]];
681696
```
682697

683698
(By default, each string is in its _own_ array (that’s 5 inner arrays) – causing a blank line between each.)

examples/.eslintrc.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ module.exports = {
103103
"error",
104104
{
105105
// The default grouping, but with no blank lines.
106-
groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]],
106+
groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\.", "^\\u0001", "^\\u0002"]],
107107
},
108108
],
109109
},
@@ -115,7 +115,7 @@ module.exports = {
115115
"error",
116116
{
117117
// The default grouping, but in reverse.
118-
groups: [["^\\."], ["^"], ["^@?\\w"], ["^node:"], ["^\\u0000"]],
118+
groups: [["^\\u0001", "^\\u0002"], ["^\\."], ["^"], ["^@?\\w"], ["^node:"], ["^\\u0000"]],
119119
},
120120
],
121121
},
@@ -128,7 +128,7 @@ module.exports = {
128128
"error",
129129
{
130130
// The default grouping, but with type imports first as a separate group.
131-
groups: [["^.*\\u0000$"], ["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."]],
131+
groups: [["^.*\\u0000$"], ["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^\\u0001", "^\\u0002"]],
132132
},
133133
],
134134
},
@@ -141,7 +141,7 @@ module.exports = {
141141
"error",
142142
{
143143
// The default grouping, but with type imports last as a separate group.
144-
groups: [["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^.+\\u0000$"]],
144+
groups: [["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^\\u0001", "^\\u0002"], ["^.+\\u0000$"]],
145145
},
146146
],
147147
},
@@ -162,6 +162,7 @@ module.exports = {
162162
["^@?\\w"],
163163
["^"],
164164
["^\\."],
165+
["^\\u0001", "^\\u0002"],
165166
],
166167
},
167168
],
@@ -182,6 +183,7 @@ module.exports = {
182183
["^@?\\w"],
183184
["^"],
184185
["^\\."],
186+
["^\\u0001", "^\\u0002"],
185187
["^node:.*\\u0000$", "^@?\\w.*\\u0000$", "^[^.].*\\u0000$", "^\\..*\\u0000$"],
186188
],
187189
},
@@ -202,6 +204,7 @@ module.exports = {
202204
["^@?\\w.*\\u0000$", "^@?\\w"],
203205
["(?<=\\u0000)$", "^"],
204206
["^\\..*\\u0000$", "^\\."],
207+
["^\\u0001", "^\\u0002"],
205208
],
206209
},
207210
],

examples/readme-order.prettier.ts

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import g from ".";
2525
import h from "./constants";
2626
import i from "./styles";
2727

28+
// TypeScript import assignments.
29+
import J = require("../parent");
30+
import K = require("./sibling");
31+
export import L = require("an-npm-package");
32+
import M = require("different-npm-package");
33+
import N = Namespace;
34+
export import O = Namespace.A.B.C;
35+
import P = Namespace.A.C;
36+
2837
// Different types of exports:
2938
export { a } from "../..";
3039
export { b } from "/";

src/imports.js

+57-14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const defaultGroups = [
1616
// Relative imports.
1717
// Anything that starts with a dot.
1818
["^\\."],
19+
// TypeScript import assignments.
20+
["^\\u0001", "^\\u0002"],
1921
];
2022

2123
module.exports = {
@@ -56,7 +58,7 @@ module.exports = {
5658
const parents = new Set();
5759

5860
return {
59-
ImportDeclaration: (node) => {
61+
"ImportDeclaration,TSImportEqualsDeclaration": (node) => {
6062
parents.add(node.parent);
6163
},
6264

@@ -97,14 +99,16 @@ function makeSortedItems(items, outerGroups) {
9799

98100
for (const item of items) {
99101
const { originalSource } = item.source;
100-
const source = item.isSideEffectImport
101-
? `\0${originalSource}`
102-
: item.source.kind !== "value"
103-
? `${originalSource}\0`
104-
: originalSource;
102+
const sourceWithControlCharacter = getSourceWithControlCharacter(
103+
originalSource,
104+
item
105+
);
105106
const [matchedGroup] = shared
106107
.flatMap(itemGroups, (groups) =>
107-
groups.map((group) => [group, group.regex.exec(source)])
108+
groups.map((group) => [
109+
group,
110+
group.regex.exec(sourceWithControlCharacter),
111+
])
108112
)
109113
.reduce(
110114
([group, longestMatch], [nextGroup, nextMatch]) =>
@@ -130,14 +134,41 @@ function makeSortedItems(items, outerGroups) {
130134
);
131135
}
132136

137+
function getSourceWithControlCharacter(originalSource, item) {
138+
if (item.isSideEffectImport) {
139+
return `\0${originalSource}`;
140+
}
141+
switch (item.source.kind) {
142+
case shared.KIND_VALUE:
143+
return originalSource;
144+
case shared.KIND_TS_IMPORT_ASSIGNMENT_REQUIRE:
145+
return `\u0001${originalSource}`;
146+
case shared.KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE:
147+
return `\u0002${originalSource}`;
148+
default: // `type` and `typeof`.
149+
return `${originalSource}\u0000`;
150+
}
151+
}
152+
133153
// Exclude "ImportDefaultSpecifier" – the "def" in `import def, {a, b}`.
134154
function getSpecifiers(importNode) {
135-
return importNode.specifiers.filter((node) => isImportSpecifier(node));
155+
switch (importNode.type) {
156+
case "ImportDeclaration":
157+
return importNode.specifiers.filter((node) => isImportSpecifier(node));
158+
case "TSImportEqualsDeclaration":
159+
return [];
160+
// istanbul ignore next
161+
default:
162+
throw new Error(`Unsupported import node type: ${importNode.type}`);
163+
}
136164
}
137165

138166
// Full import statement.
139167
function isImport(node) {
140-
return node.type === "ImportDeclaration";
168+
return (
169+
node.type === "ImportDeclaration" ||
170+
node.type === "TSImportEqualsDeclaration"
171+
);
141172
}
142173

143174
// import def, { a, b as c, type d } from "A"
@@ -150,9 +181,21 @@ function isImportSpecifier(node) {
150181
// But not: import {} from "setup"
151182
// And not: import type {} from "setup"
152183
function isSideEffectImport(importNode, sourceCode) {
153-
return (
154-
importNode.specifiers.length === 0 &&
155-
(!importNode.importKind || importNode.importKind === "value") &&
156-
!shared.isPunctuator(sourceCode.getFirstToken(importNode, { skip: 1 }), "{")
157-
);
184+
switch (importNode.type) {
185+
case "ImportDeclaration":
186+
return (
187+
importNode.specifiers.length === 0 &&
188+
(!importNode.importKind ||
189+
importNode.importKind === shared.KIND_VALUE) &&
190+
!shared.isPunctuator(
191+
sourceCode.getFirstToken(importNode, { skip: 1 }),
192+
"{"
193+
)
194+
);
195+
case "TSImportEqualsDeclaration":
196+
return false;
197+
// istanbul ignore next
198+
default:
199+
throw new Error(`Unsupported import node type: ${importNode.type}`);
200+
}
158201
}

src/shared.js

+81-9
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ function getImportExportItems(
164164
const [start] = all[0].range;
165165
const [, end] = all[all.length - 1].range;
166166

167-
const source = getSource(node);
167+
const source = getSource(sourceCode, node);
168168

169169
return {
170170
node,
@@ -795,8 +795,8 @@ function isNewline(node) {
795795
return node.type === "Newline";
796796
}
797797

798-
function getSource(node) {
799-
const source = node.source.value;
798+
function getSource(sourceCode, node) {
799+
const [source, kind] = getSourceTextAndKind(sourceCode, node);
800800

801801
return {
802802
// Sort by directory level rather than by string length.
@@ -806,7 +806,7 @@ function getSource(node) {
806806
// Make `../` sort after `../../` but before `../a` etc.
807807
// Why a comma? See the next comment.
808808
.replace(/^[./]*\/$/, "$&,")
809-
// Make `.` and `/` sort before any other punctation.
809+
// Make `.` and `/` sort before any other punctuation.
810810
// The default order is: _ - , x x x . x x x / x x x
811811
// We’re changing it to: . / , x x x _ x x x - x x x
812812
.replace(/[./_-]/g, (char) => {
@@ -825,16 +825,85 @@ function getSource(node) {
825825
}
826826
}),
827827
originalSource: source,
828-
kind: getImportExportKind(node),
828+
kind,
829829
};
830830
}
831831

832+
function getSourceTextAndKind(sourceCode, node) {
833+
switch (node.type) {
834+
case "ImportDeclaration":
835+
case "ExportNamedDeclaration":
836+
case "ExportAllDeclaration":
837+
return [node.source.value, getImportExportKind(node)];
838+
case "TSImportEqualsDeclaration":
839+
return getSourceTextAndKindFromModuleReference(
840+
sourceCode,
841+
node.moduleReference
842+
);
843+
// istanbul ignore next
844+
default:
845+
throw new Error(`Unsupported import/export node type: ${node.type}`);
846+
}
847+
}
848+
849+
const KIND_VALUE = "value";
850+
const KIND_TS_IMPORT_ASSIGNMENT_REQUIRE = "z_require";
851+
const KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE = "z_namespace";
852+
853+
function getSourceTextAndKindFromModuleReference(sourceCode, node) {
854+
switch (node.type) {
855+
case "TSExternalModuleReference":
856+
// Only string literals inside `require()` are allowed by
857+
// TypeScript, but the parser supports anything. Sorting
858+
// is defined for string literals only. For other expressions,
859+
// we just make sure not to crash.
860+
switch (node.expression.type) {
861+
case "Literal":
862+
return [
863+
typeof node.expression.value === "string"
864+
? node.expression.value
865+
: node.expression.raw,
866+
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
867+
];
868+
default: {
869+
const [start, end] = node.expression.range;
870+
return [
871+
sourceCode.text.slice(start, end),
872+
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
873+
];
874+
}
875+
}
876+
case "TSQualifiedName":
877+
return [
878+
getSourceTextFromTSQualifiedName(sourceCode, node),
879+
KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE,
880+
];
881+
case "Identifier":
882+
return [node.name, KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE];
883+
// istanbul ignore next
884+
default:
885+
throw new Error(`Unsupported module reference node type: ${node.type}`);
886+
}
887+
}
888+
889+
function getSourceTextFromTSQualifiedName(sourceCode, node) {
890+
switch (node.left.type) {
891+
case "Identifier":
892+
return `${node.left.name}.${node.right.name}`;
893+
case "TSQualifiedName":
894+
return `${getSourceTextFromTSQualifiedName(sourceCode, node.left)}.${
895+
node.right.name
896+
}`;
897+
// istanbul ignore next
898+
default:
899+
throw new Error(`Unsupported TS qualified name node type: ${node.type}`);
900+
}
901+
}
902+
832903
function getImportExportKind(node) {
833904
// `type` and `typeof` imports, as well as `type` exports (there are no
834-
// `typeof` exports). In Flow, import specifiers can also have a kind. Default
835-
// to "value" (like TypeScript) to make regular imports/exports come after the
836-
// type imports/exports.
837-
return node.importKind || node.exportKind || "value";
905+
// `typeof` exports).
906+
return node.importKind || node.exportKind || KIND_VALUE;
838907
}
839908

840909
// Like `Array.prototype.findIndex`, but searches from the end.
@@ -859,6 +928,9 @@ module.exports = {
859928
flatMap,
860929
getImportExportItems,
861930
isPunctuator,
931+
KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE,
932+
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
933+
KIND_VALUE,
862934
maybeReportSorting,
863935
printSortedItems,
864936
printWithSortedSpecifiers,

test/__snapshots__/examples.test.js.snap

+9
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,15 @@ import g from ".";
496496
import h from "./constants";
497497
import i from "./styles";
498498
499+
// TypeScript import assignments.
500+
import J = require("../parent");
501+
import K = require("./sibling");
502+
export import L = require("an-npm-package");
503+
import M = require("different-npm-package");
504+
import N = Namespace;
505+
export import O = Namespace.A.B.C;
506+
import P = Namespace.A.C;
507+
499508
// Different types of exports:
500509
export { a } from "../..";
501510
export { b } from "/";

0 commit comments

Comments
 (0)