Skip to content

Commit b20f587

Browse files
committed
Add option to transform import names in the new path
This makes it possible to debarrel Cloudscape, which uses lowercase paths for direct imports (as the updated README shows).
1 parent b9f7c05 commit b20f587

File tree

3 files changed

+195
-9
lines changed

3 files changed

+195
-9
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
An ESLint plugin to transform barrel imports into direct imports. For example:
44

55
```typescript
6-
import { Button, Dialog } from "@material-ui/core";
6+
import { Box, Button } from "@cloudscape-design/components";
77
```
88

99
Will be transformed to:
1010

1111
```typescript
12-
import Button from "@material-ui/core/Button";
13-
import Dialog from "@material-ui/core/Dialog";
12+
import Box from "@cloudscape-design/components/box";
13+
import Button from "@cloudscape-design/components/button";
1414
```
1515

1616
## Why?
@@ -50,9 +50,10 @@ export default [
5050
{
5151
patterns: [
5252
{
53-
barrel: "@material-ui/core",
54-
transformPattern: "@material-ui/core/{{importName}}",
55-
},
53+
"barrel": "@cloudscape-design/components",
54+
"transformPattern": "@cloudscape-design/components/{{importName}}",
55+
"transformImportName": "lowercase"
56+
}
5657
],
5758
},
5859
],
@@ -81,8 +82,9 @@ Then configure the rule under the rules section:
8182
{
8283
"patterns": [
8384
{
84-
"barrel": "@material-ui/core",
85-
"transformPattern": "@material-ui/core/{{importName}}"
85+
"barrel": "@cloudscape-design/components",
86+
"transformPattern": "@cloudscape-design/components/{{importName}}",
87+
"transformImportName": "lowercase"
8688
}
8789
]
8890
}
@@ -97,3 +99,4 @@ For each barrel file you want to avoid, add an object to the `patterns` list wit
9799

98100
- `barrel`: The import path of the barrel file
99101
- `transformPattern`: The path that each named import should be imported from as a default import instead, using `{{importName}}` as a placeholder
102+
- `transformImportName` (optional): Transform the original import name before using it in the `transformPattern`. The transform can be either a string-to-string function or one of the following: `"lowercase" | "kebab-case" | "camelCase"`.

src/rules/debarrel.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,145 @@ import Dialog from "@material-ui/core/Dialog";`,
7272
],
7373
});
7474
});
75+
76+
it("transforms import names to lowercase", () => {
77+
ruleTester.run("debarrel", rule, {
78+
valid: [],
79+
invalid: [
80+
{
81+
code: 'import { Box } from "@cloudscape-design/components";',
82+
options: [
83+
{
84+
patterns: [
85+
{
86+
barrel: "@cloudscape-design/components",
87+
transformPattern: "@cloudscape-design/components/{{importName}}",
88+
transformImportName: "lowercase"
89+
},
90+
],
91+
},
92+
],
93+
errors: [
94+
{
95+
message: "Barrel imports should be transformed into direct imports",
96+
},
97+
],
98+
output: 'import Box from "@cloudscape-design/components/box";',
99+
},
100+
],
101+
});
102+
});
103+
104+
it("transforms import names to kebab-case", () => {
105+
ruleTester.run("debarrel", rule, {
106+
valid: [],
107+
invalid: [
108+
{
109+
code: 'import { MyComponent } from "@my-lib/components";',
110+
options: [
111+
{
112+
patterns: [
113+
{
114+
barrel: "@my-lib/components",
115+
transformPattern: "@my-lib/components/{{importName}}",
116+
transformImportName: "kebab-case"
117+
},
118+
],
119+
},
120+
],
121+
errors: [
122+
{
123+
message: "Barrel imports should be transformed into direct imports",
124+
},
125+
],
126+
output: 'import MyComponent from "@my-lib/components/my-component";',
127+
},
128+
],
129+
});
130+
});
131+
132+
it("transforms import names to camelCase", () => {
133+
ruleTester.run("debarrel", rule, {
134+
valid: [],
135+
invalid: [
136+
{
137+
code: 'import { MyComponent } from "@my-lib/components";',
138+
options: [
139+
{
140+
patterns: [
141+
{
142+
barrel: "@my-lib/components",
143+
transformPattern: "@my-lib/components/{{importName}}",
144+
transformImportName: "camelCase"
145+
},
146+
],
147+
},
148+
],
149+
errors: [
150+
{
151+
message: "Barrel imports should be transformed into direct imports",
152+
},
153+
],
154+
output: 'import MyComponent from "@my-lib/components/myComponent";',
155+
},
156+
],
157+
});
158+
});
159+
160+
it("transforms import names using a custom function", () => {
161+
ruleTester.run("debarrel", rule, {
162+
valid: [],
163+
invalid: [
164+
{
165+
code: 'import { MyComponent } from "@my-lib/components";',
166+
options: [
167+
{
168+
patterns: [
169+
{
170+
barrel: "@my-lib/components",
171+
transformPattern: "@my-lib/components/{{importName}}",
172+
transformImportName: (name) => `custom-${name.toLowerCase()}`
173+
},
174+
],
175+
},
176+
],
177+
errors: [
178+
{
179+
message: "Barrel imports should be transformed into direct imports",
180+
},
181+
],
182+
output: 'import MyComponent from "@my-lib/components/custom-mycomponent";',
183+
},
184+
],
185+
});
186+
});
187+
188+
it("handles multiple imports with custom function transformer", () => {
189+
ruleTester.run("debarrel", rule, {
190+
valid: [],
191+
invalid: [
192+
{
193+
code: 'import { ButtonA, ButtonB } from "@my-lib/components";',
194+
options: [
195+
{
196+
patterns: [
197+
{
198+
barrel: "@my-lib/components",
199+
transformPattern: "@my-lib/components/{{importName}}",
200+
transformImportName: (name) => name.replace("Button", "btn-").toLowerCase()
201+
},
202+
],
203+
},
204+
],
205+
errors: [
206+
{
207+
message: "Barrel imports should be transformed into direct imports",
208+
},
209+
],
210+
output: `import ButtonA from "@my-lib/components/btn-a";
211+
import ButtonB from "@my-lib/components/btn-b";`,
212+
},
213+
],
214+
});
215+
});
75216
});

src/rules/debarrel.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
11
import type { Rule } from "eslint";
22
import type { ImportDeclaration, ImportSpecifier, Identifier } from "estree";
33

4+
function toKebabCase(str: string): string {
5+
return str
6+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
7+
.replace(/([A-Z])([A-Z])(?=[a-z])/g, "$1-$2")
8+
.toLowerCase();
9+
}
10+
11+
function toCamelCase(str: string): string {
12+
return str.charAt(0).toLowerCase() + str.slice(1);
13+
}
14+
15+
function transformImportName(name: string, transform?: PatternConfig["transformImportName"]): string {
16+
if (!transform) return name;
17+
18+
if (typeof transform === "function") {
19+
return transform(name);
20+
}
21+
22+
switch (transform) {
23+
case "lowercase":
24+
return name.toLowerCase();
25+
case "kebab-case":
26+
return toKebabCase(name);
27+
case "camelCase":
28+
return toCamelCase(name);
29+
default:
30+
return name;
31+
}
32+
}
33+
434
interface PatternConfig {
535
barrel: string;
636
transformPattern: string;
37+
transformImportName?: "lowercase" | "kebab-case" | "camelCase" | ((importName: string) => string);
738
}
839

940
interface RuleOptions {
@@ -29,6 +60,16 @@ const rule: Rule.RuleModule = {
2960
properties: {
3061
barrel: { type: "string" },
3162
transformPattern: { type: "string" },
63+
transformImportName: {
64+
anyOf: [
65+
{
66+
enum: ["lowercase", "kebab-case", "camelCase"]
67+
},
68+
{
69+
not: { type: "string" }
70+
}
71+
]
72+
},
3273
},
3374
required: ["barrel", "transformPattern"],
3475
additionalProperties: false,
@@ -69,9 +110,10 @@ const rule: Rule.RuleModule = {
69110
const newImports = specifiers.map((specifier) => {
70111
const importName = (specifier.imported as Identifier).name;
71112
const localName = specifier.local.name;
113+
const transformedName = transformImportName(importName, matchingPattern.transformImportName);
72114
const newPath = matchingPattern.transformPattern.replace(
73115
"{{importName}}",
74-
importName,
116+
transformedName,
75117
);
76118
return `import ${localName} from "${newPath}";`;
77119
});

0 commit comments

Comments
 (0)