Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions README.md

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions eslint-rules/enforce-zod-v4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use strict";
import path from "path";

// The file that is allowed to import from zod/v4
const configFilePath = path.resolve(import.meta.dirname, "../src/common/config.ts");

// Ref: https://eslint.org/docs/latest/extend/custom-rules
export default {
meta: {
type: "problem",
docs: {
description:
"Only allow importing 'zod/v4' in config.ts, all other imports are allowed elsewhere. We should only adopt zod v4 for tools and resources once https://github.com/modelcontextprotocol/typescript-sdk/issues/555 is resolved.",
recommended: true,
},
fixable: null,
messages: {
enforceZodV4:
"Only 'zod/v4' imports are allowed in config.ts. Found import from '{{importPath}}'. Use 'zod/v4' instead.",
},
},
create(context) {
const currentFilePath = path.resolve(context.getFilename());

// Only allow zod v4 import in config.ts
if (currentFilePath === configFilePath) {
return {};
}

return {
ImportDeclaration(node) {
const importPath = node.source.value;

// Check if this is a zod import
if (typeof importPath !== "string") {
return;
}

const isZodV4Import = importPath === "zod/v4";

if (isZodV4Import) {
context.report({
node,
messageId: "enforceZodV4",
data: {
importPath,
},
});
}
},
};
},
};
153 changes: 153 additions & 0 deletions eslint-rules/enforce-zod-v4.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import path from "path";
import { RuleTester } from "eslint";
import { describe, it } from "vitest";
import tsParser from "@typescript-eslint/parser";
import rule from "./enforce-zod-v4.js";

const ROOT = process.cwd();
const resolve = (p) => path.resolve(ROOT, p);

const ruleTester = new RuleTester({
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
},
});

describe("enforce-zod-v4", () => {
it("should allow zod/v4 imports in config.ts", () => {
ruleTester.run("enforce-zod-v4", rule, {
valid: [
{
filename: resolve("src/common/config.ts"),
code: 'import { z } from "zod/v4";\n',
},
{
filename: resolve("src/common/config.ts"),
code: 'import * as z from "zod/v4";\n',
},
{
filename: resolve("src/common/config.ts"),
code: 'import type { ZodType } from "zod/v4";\n',
},
],
invalid: [],
});
});

it("should allow regular zod imports in other files", () => {
ruleTester.run("enforce-zod-v4", rule, {
valid: [
{
filename: resolve("src/tools/tool.ts"),
code: 'import { z } from "zod";\n',
},
{
filename: resolve("src/resources/resource.ts"),
code: 'import * as z from "zod";\n',
},
{
filename: resolve("src/some/module.ts"),
code: 'import type { ZodType } from "zod";\n',
},
],
invalid: [],
});
});

it("should allow non-zod imports in any file", () => {
ruleTester.run("enforce-zod-v4", rule, {
valid: [
{
filename: resolve("src/tools/tool.ts"),
code: 'import { something } from "some-package";\n',
},
{
filename: resolve("src/common/config.ts"),
code: 'import path from "path";\n',
},
{
filename: resolve("src/resources/resource.ts"),
code: 'import { Logger } from "./logger.js";\n',
},
],
invalid: [],
});
});

it("should report error when zod/v4 is imported in files other than config.ts", () => {
ruleTester.run("enforce-zod-v4", rule, {
valid: [],
invalid: [
{
filename: resolve("src/tools/tool.ts"),
code: 'import { z } from "zod/v4";\n',
errors: [
{
messageId: "enforceZodV4",
data: { importPath: "zod/v4" },
},
],
},
{
filename: resolve("src/resources/resource.ts"),
code: 'import * as z from "zod/v4";\n',
errors: [
{
messageId: "enforceZodV4",
data: { importPath: "zod/v4" },
},
],
},
{
filename: resolve("src/some/module.ts"),
code: 'import type { ZodType } from "zod/v4";\n',
errors: [
{
messageId: "enforceZodV4",
data: { importPath: "zod/v4" },
},
],
},
{
filename: resolve("tests/unit/toolBase.test.ts"),
code: 'import { z } from "zod/v4";\n',
errors: [
{
messageId: "enforceZodV4",
data: { importPath: "zod/v4" },
},
],
},
],
});
});

it("should handle multiple imports in a single file", () => {
ruleTester.run("enforce-zod-v4", rule, {
valid: [
{
filename: resolve("src/common/config.ts"),
code: `import { z } from "zod/v4";
import path from "path";
import type { UserConfig } from "./types.js";
`,
},
],
invalid: [
{
filename: resolve("src/tools/tool.ts"),
code: `import { z } from "zod/v4";
import path from "path";
`,
errors: [
{
messageId: "enforceZodV4",
data: { importPath: "zod/v4" },
},
],
},
],
});
});
});
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import tseslint from "typescript-eslint";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import vitestPlugin from "@vitest/eslint-plugin";
import noConfigImports from "./eslint-rules/no-config-imports.js";
import enforceZodV4 from "./eslint-rules/enforce-zod-v4.js";

const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"];

Expand Down Expand Up @@ -72,9 +73,15 @@ export default defineConfig([
"no-config-imports": noConfigImports,
},
},
"enforce-zod-v4": {
rules: {
"enforce-zod-v4": enforceZodV4,
},
},
},
rules: {
"no-config-imports/no-config-imports": "error",
"enforce-zod-v4/enforce-zod-v4": "error",
},
},
globalIgnores([
Expand Down
Loading
Loading