Skip to content

Commit 01acb1c

Browse files
authored
Ban default imports (#10)
* Ban default imports * add changeset
1 parent 89e0b75 commit 01acb1c

File tree

5 files changed

+221
-12
lines changed

5 files changed

+221
-12
lines changed

.changeset/happy-papers-accept.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"eslint-plugin-import-zod": minor
3+
---
4+
5+
Default imports are now converted to namespace imports
6+
7+
```ts
8+
import z from "zod";
9+
```
10+
11+
to
12+
13+
```ts
14+
import * as z from "zod";
15+
```

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<a href="https://www.npmjs.com/package/eslint-plugin-import-zod"><img src="https://img.shields.io/npm/v/eslint-plugin-import-zod"/></a>
44
<a href="https://nodejs.org/en/"><img src="https://img.shields.io/node/v/eslint-plugin-import-zod"/></a>
55

6-
ESLint plugin to enforce namespace imports for zod. This plugin provides a rule that ensures all imports of zod use the namespace import style (`import * as z from "zod";`) instead of named imports to promote better tree-shaking and reduce bundle sizes. See [this Zod issue comment](https://github.com/colinhacks/zod/issues/4433#issuecomment-2921500831) for a more detailed explanation.
6+
ESLint plugin to enforce namespace imports for zod. This plugin provides a rule that ensures all imports of zod use the namespace import style (`import * as z from "zod";`) instead of named imports or default imports to promote better tree-shaking and reduce bundle sizes. See [this Zod issue comment](https://github.com/colinhacks/zod/issues/4433#issuecomment-2921500831) for a more detailed explanation.
77

88
## Installation
99

@@ -37,16 +37,30 @@ export default [
3737

3838
### [`prefer-zod-namespace`](docs/rules/prefer-zod-namespace.md)
3939

40-
This rule enforces using namespace imports for zod instead of named imports.
40+
This rule enforces using namespace imports for zod instead of named imports or default imports.
4141

4242
#### ❌ Invalid
4343

4444
```js
4545
// Importing z directly
4646
import { z } from "zod";
4747

48+
// Default imports (any name)
49+
import z from "zod";
50+
import zod from "zod";
51+
import zodSchema from "zod";
52+
53+
// Type default imports
54+
import type z from "zod";
55+
import type zod from "zod";
56+
57+
// Mixed default and named imports
58+
import z, { toJSONSchema } from "zod";
59+
import zod, { ZodError } from "zod";
60+
4861
// Importing z from 'zod/v4'
4962
import { z } from "zod/v4";
63+
import z from "zod/v4";
5064

5165
// Importing z from 'zod/v4-mini'
5266
import { z } from "zod/v4-mini";
@@ -73,6 +87,10 @@ import { core } from "zod/v4";
7387
// Using namespace import
7488
import * as z from "zod";
7589

90+
// Using namespace import with any name
91+
import * as zod from "zod";
92+
import * as zodSchema from "zod";
93+
7694
// Importing z from 'zod/v4'
7795
import * as z from "zod/v4";
7896

@@ -81,6 +99,7 @@ import * as z from "zod/v4-mini";
8199

82100
// Using type namespace import
83101
import type * as z from "zod";
102+
import type * as zod from "zod";
84103

85104
// Other imports from zod that don't include 'z'
86105
import { ZodError } from "zod";
@@ -97,13 +116,21 @@ import * as core from "zod/v4/core";
97116
This rule is automatically fixable. When using `--fix`, ESLint will:
98117

99118
- Convert `import { z } from 'zod';` to `import * as z from 'zod';`
119+
- Convert `import z from 'zod';` to `import * as z from 'zod';`
120+
- Convert `import zod from 'zod';` to `import * as zod from 'zod';`
121+
- Convert `import type z from 'zod';` to `import type * as z from 'zod';`
100122
- Convert `import type { z } from 'zod';` to `import type * as z from 'zod';`
101123
- Convert `import { core } from 'zod/v4';` to `import * as core from 'zod/v4/core';`
102124
- Split mixed imports like `import { ZodError, z } from 'zod';` into:
103125
```js
104126
import * as z from "zod";
105127
import { ZodError } from "zod";
106128
```
129+
- Split default and named imports like `import z, { toJSONSchema } from 'zod';` into:
130+
```js
131+
import * as z from "zod";
132+
import { toJSONSchema } from "zod";
133+
```
107134
- Split type imports like `import type { ZodError, z } from 'zod';` into:
108135
```js
109136
import type * as z from "zod";

docs/rules/prefer-zod-namespace.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Enforce namespace imports for zod (prefer-zod-namespace)
22

3-
This rule enforces using namespace imports for zod instead of named imports. Using namespace imports results in better tree-shaking and reduced bundle sizes.
3+
This rule enforces using namespace imports for zod instead of named imports or default imports. Using namespace imports results in better tree-shaking and reduced bundle sizes. All default imports from 'zod' are converted to namespace imports regardless of the import name.
44

55
## Rule Details
66

@@ -12,6 +12,19 @@ This rule aims to enforce a consistent pattern for importing zod.
1212
// Importing z directly
1313
import { z } from "zod";
1414

15+
// Default import
16+
import z from "zod";
17+
18+
// Type default import
19+
import type z from "zod";
20+
21+
// Mixed default and named imports
22+
import z, { toJSONSchema } from "zod";
23+
24+
// Default imports with any name
25+
import zod from "zod";
26+
import zodSchema from "zod";
27+
1528
// Type imports
1629
import type { z } from "zod";
1730

src/rules/prefer-zod-namespace.ts

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default createRule({
3737
schema: [], // No options
3838
messages: {
3939
preferNamespaceImport:
40-
'Import zod as a namespace (import * as z from "zod") instead of destructuring its exports',
40+
'Import zod as a namespace (import * as z from "zod") instead of destructuring its exports or using default imports',
4141
},
4242
},
4343

@@ -61,12 +61,18 @@ export default createRule({
6161
specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier
6262
);
6363

64-
// If there are no named imports, there's nothing to check
65-
if (namedSpecifiers.length === 0) {
64+
// Check if it's a default import (e.g., import z from 'zod')
65+
const defaultSpecifiers = node.specifiers.filter(
66+
(specifier) =>
67+
specifier.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier
68+
);
69+
70+
// If there are no named or default imports, there's nothing to check
71+
if (namedSpecifiers.length === 0 && defaultSpecifiers.length === 0) {
6672
return;
6773
}
6874

69-
// Find 'z' or 'core' imports specifically
75+
// Find 'z' or 'core' imports specifically in named imports
7076
const zodSpecifiers = namedSpecifiers.filter(
7177
(
7278
specifier: TSESTree.ImportSpecifier
@@ -78,12 +84,101 @@ export default createRule({
7884
specifier.imported.name === "core")
7985
);
8086

81-
// If there's no 'z' or 'core' import, we don't need to do anything
82-
if (zodSpecifiers.length === 0) {
87+
// Find all default imports (any name)
88+
const zodDefaultSpecifiers = defaultSpecifiers;
89+
90+
// If there's no 'z' or 'core' named import and no default imports, we don't need to do anything
91+
if (zodSpecifiers.length === 0 && zodDefaultSpecifiers.length === 0) {
8392
return;
8493
}
8594

86-
// Handle each zod specifier
95+
// Handle each zod default specifier
96+
for (const zodDefaultSpecifier of zodDefaultSpecifiers) {
97+
const localName = zodDefaultSpecifier.local.name;
98+
const importSource = node.source.value;
99+
100+
// Report the issue for default import
101+
context.report({
102+
node: zodDefaultSpecifier,
103+
messageId: "preferNamespaceImport",
104+
fix(fixer) {
105+
// If this is the only specifier
106+
if (node.specifiers.length === 1) {
107+
// Simple case: just replace with a namespace import
108+
const isTypeOnlyImport = node.importKind === "type";
109+
const typePrefix = isTypeOnlyImport ? "type " : "";
110+
return fixer.replaceText(
111+
node,
112+
`import ${typePrefix}* as ${localName} from '${importSource}';`
113+
);
114+
} else {
115+
// Handle the case where we need to split imports
116+
const otherSpecifiers = node.specifiers.filter(
117+
(s) => s !== zodDefaultSpecifier
118+
);
119+
120+
// Check if this is a type-only import
121+
const isTypeOnlyImport = node.importKind === "type";
122+
123+
// Create a namespace import for the zod default specifier
124+
const typePrefix = isTypeOnlyImport ? "type " : "";
125+
const namespaceImport = `import ${typePrefix}* as ${localName} from '${importSource}';\n`;
126+
127+
// Create a new import for the other specifiers
128+
const otherImportParts: string[] = [];
129+
130+
// Handle other default imports
131+
const otherDefaultSpecifiers = otherSpecifiers.filter(
132+
(s) =>
133+
s.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier
134+
);
135+
136+
// Handle other named imports
137+
const otherNamedSpecifiers = otherSpecifiers.filter(
138+
(s) => s.type === TSESTree.AST_NODE_TYPES.ImportSpecifier
139+
);
140+
141+
if (otherDefaultSpecifiers.length > 0) {
142+
const defaultName = otherDefaultSpecifiers[0].local.name;
143+
otherImportParts.push(defaultName);
144+
}
145+
146+
if (otherNamedSpecifiers.length > 0) {
147+
const namedPart = `{ ${otherNamedSpecifiers
148+
.map((s) => {
149+
const specifierLocalName = s.local.name;
150+
const specifierImportedName =
151+
s.imported.type === TSESTree.AST_NODE_TYPES.Identifier
152+
? s.imported.name
153+
: "";
154+
// Preserve individual type imports when not a type-only import
155+
const typeModifier =
156+
!isTypeOnlyImport && s.importKind === "type"
157+
? "type "
158+
: "";
159+
return specifierLocalName === specifierImportedName
160+
? `${typeModifier}${specifierImportedName}`
161+
: `${typeModifier}${specifierImportedName} as ${specifierLocalName}`;
162+
})
163+
.join(", ")} }`;
164+
otherImportParts.push(namedPart);
165+
}
166+
167+
const otherImport = `import ${
168+
isTypeOnlyImport ? "type " : ""
169+
}${otherImportParts.join(", ")} from '${importSource}';`;
170+
171+
// Replace the entire import declaration
172+
return fixer.replaceText(
173+
node,
174+
`${namespaceImport}${otherImport}`
175+
);
176+
}
177+
},
178+
});
179+
}
180+
181+
// Handle each zod named specifier (existing logic)
87182
for (const zodSpecifier of zodSpecifiers) {
88183
// Get the local name of the import (in case it's renamed)
89184
const localName = zodSpecifier.local.name;

tests/prefer-zod-namespace.test.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ ruleTester.run("prefer-zod-namespace-import", rule, {
3030
"import { toJSONSchema } from 'zod';",
3131
// Other type imports from zod that don't include 'z' are valid
3232
"import type { toJSONSchema } from 'zod';",
33-
// Default import would be valid (though zod doesn't have one)
34-
"import zod from 'zod';",
33+
3534
// Imports from other modules are valid
3635
"import { z } from 'not-zod';",
3736
],
@@ -58,6 +57,66 @@ ruleTester.run("prefer-zod-namespace-import", rule, {
5857
output: "import * as z from 'zod/v3';",
5958
errors: [{ messageId: "preferNamespaceImport" }],
6059
},
60+
// Default import cases
61+
{
62+
code: "import z from 'zod';",
63+
output: "import * as z from 'zod';",
64+
errors: [{ messageId: "preferNamespaceImport" }],
65+
},
66+
{
67+
code: "import z from 'zod/v4';",
68+
output: "import * as z from 'zod/v4';",
69+
errors: [{ messageId: "preferNamespaceImport" }],
70+
},
71+
{
72+
code: "import z from 'zod/v3';",
73+
output: "import * as z from 'zod/v3';",
74+
errors: [{ messageId: "preferNamespaceImport" }],
75+
},
76+
{
77+
code: "import type z from 'zod';",
78+
output: "import type * as z from 'zod';",
79+
errors: [{ messageId: "preferNamespaceImport" }],
80+
},
81+
// Mixed default and named imports
82+
{
83+
code: "import z, { toJSONSchema } from 'zod';",
84+
output: "import * as z from 'zod';\nimport { toJSONSchema } from 'zod';",
85+
errors: [{ messageId: "preferNamespaceImport" }],
86+
},
87+
{
88+
code: "import type z, { toJSONSchema } from 'zod';",
89+
output:
90+
"import type * as z from 'zod';\nimport type { toJSONSchema } from 'zod';",
91+
errors: [{ messageId: "preferNamespaceImport" }],
92+
},
93+
// Default imports with any name should be converted
94+
{
95+
code: "import zod from 'zod';",
96+
output: "import * as zod from 'zod';",
97+
errors: [{ messageId: "preferNamespaceImport" }],
98+
},
99+
{
100+
code: "import zodSchema from 'zod';",
101+
output: "import * as zodSchema from 'zod';",
102+
errors: [{ messageId: "preferNamespaceImport" }],
103+
},
104+
{
105+
code: "import Z from 'zod';",
106+
output: "import * as Z from 'zod';",
107+
errors: [{ messageId: "preferNamespaceImport" }],
108+
},
109+
{
110+
code: "import type zod from 'zod';",
111+
output: "import type * as zod from 'zod';",
112+
errors: [{ messageId: "preferNamespaceImport" }],
113+
},
114+
{
115+
code: "import zod, { toJSONSchema } from 'zod';",
116+
output:
117+
"import * as zod from 'zod';\nimport { toJSONSchema } from 'zod';",
118+
errors: [{ messageId: "preferNamespaceImport" }],
119+
},
61120
// Submodule imports from zod/v4
62121
{
63122
code: "import { core } from 'zod/v4';",

0 commit comments

Comments
 (0)