Skip to content

feat(ACT-4873): add require-zod-import-in-contracts ESLint rule#696

Merged
poldridge merged 1 commit into
mainfrom
pauloldridge-act-4873-add-require-zod-import-in-contracts-rule
Jun 5, 2026
Merged

feat(ACT-4873): add require-zod-import-in-contracts ESLint rule#696
poldridge merged 1 commit into
mainfrom
pauloldridge-act-4873-add-require-zod-import-in-contracts-rule

Conversation

@poldridge
Copy link
Copy Markdown
Contributor

Summary

New ESLint rule @clipboard-health/require-zod-import-in-contracts that requires every *.contract.ts file to reference "zod" in its own module scope — either via an import (named, default, namespace, or side-effect) or via a re-export (export { z } from "zod", export * from "zod").

Auto-applies to all *.contract.ts files via @clipboard-health/eslint-config, mirroring how require-http-module-factory is applied to *.module.ts.

Why

Prevents the silent type-safety regression documented in ACT-4870 (clipboard-health#25570).

When a contract file infers zod-typed schemas from sibling files but doesn't reference zod in its own scope, TypeScript's declaration emit can't reuse a clean "zod" specifier and falls back to synthesizing inline imports as import("node_modules/zod/index.cjs").ZodObject<...>. That malformed module specifier fails to resolve for downstream consumers, so every type that references it silently collapses to any — and tsc does not error.

It happened once already (@clipboard-health/contract-backend-main's shiftV3.contract.ts shipped 441 leaked paths in v60.15.6, silently breaking shiftClient.list()'s response type for worker-app-bff). This rule prevents the next occurrence.

What's in the PR

  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.ts — rule implementation. Visits Program once, checks for any ImportDeclaration / ExportAllDeclaration / ExportNamedDeclaration with source.value === "zod". Reports on the Program node if none found.
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.test.ts — vitest tests covering 6 valid forms (named import, default import, namespace import, export { z } from "zod", export * from "zod", side-effect import) and 3 invalid forms (the shiftV3 case, no zod at all, similarly-named-but-not-zod package).
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/README.md — explains the underlying TypeScript declaration-emit pitfall, why the rule exists, and shows both the regular import form and the export { z } from "zod" workaround for files that compose schemas from sibling files only.
  • packages/eslint-plugin/src/index.ts — registers the rule alongside existing rules.
  • packages/eslint-config/src/index.js — auto-applies the rule to **/*.contract.ts.

Verification

  • nx test eslint-plugin: all 42 tests pass (33 existing + 9 new).
  • nx lint eslint-plugin: clean.
  • End-to-end against the live motivating file: ran the rule against the actual shiftV3.contract.ts source from clipboard-health checked out at ../clipboard-health. With the current export { z } from "zod" line present → no error. With that line stripped (the pre-fix state that shipped v60.15.6) → fires missingZodReference. (Temporary verification test was deleted before commit.)

Test plan

  • CI green
  • After this lands and @clipboard-health/eslint-plugin + @clipboard-health/eslint-config are republished, follow up by bumping both in clipboard-health (and any other consumer) and verifying the rule is active. The rule should produce no errors against current *.contract.ts files in clipboard-health/packages/contract-backend-main/ since they all already have a zod import — shiftV3.contract.ts is the only file that previously didn't, and ACT-4870 fixed it.

Linear: ACT-4873

Posted by Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77f38db8-f192-45a5-854d-0c06f9d99883

📥 Commits

Reviewing files that changed from the base of the PR and between 47e692a and 8e3127e.

📒 Files selected for processing (5)
  • packages/eslint-config/src/index.js
  • packages/eslint-plugin/src/index.ts
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/README.md
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.test.ts
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/eslint-plugin/src/index.ts
  • packages/eslint-config/src/index.js
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.ts
  • packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.test.ts

📝 Walkthrough

Walkthrough

This PR introduces a new ESLint rule require-zod-import-in-contracts that enforces contract files to explicitly import or re-export the zod module at the top level. The rule implementation scans TypeScript contract files for Zod references, the plugin exports the rule, and the ESLint config enables it for all *.contract.ts files outside the core-utils monorepo.

Changes

Require Zod import in contract files

Layer / File(s) Summary
Rule implementation
packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.ts
The require-zod-import-in-contracts rule inspects top-level import/export statements and reports missingZodReference when no statement references the zod module.
Rule testing and documentation
packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.test.ts, packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/README.md
Test suite validates rule behavior against valid cases (named, default, namespace, and export-form Zod references) and invalid cases (missing imports, unrelated packages), and README documents the rule's purpose, configuration, and usage examples.
Plugin and config integration
packages/eslint-plugin/src/index.ts, packages/eslint-config/src/index.js
Rule is registered in the plugin's exported rules map and wired into the ESLint config as an error-level override for **/*.contract.ts files outside core-utils.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a new ESLint rule require-zod-import-in-contracts, which directly reflects the primary purpose of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the rule's purpose, implementation, motivation, and verification details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pauloldridge-act-4873-add-require-zod-import-in-contracts-rule

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/eslint-config/src/index.js

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/eslint-plugin/src/index.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/eslint-plugin/src/lib/rules/require-zod-import-in-contracts/index.test.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 1 others

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 30, 2026

View your CI Pipeline Execution ↗ for commit 8e3127e

Command Status Duration Result
nx affected --configuration ci --parallel 8 --t... ✅ Succeeded 9s View ↗
nx build ai-rules ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-31 14:21:09 UTC

mendral-app[bot]

This comment was marked as outdated.

New ESLint rule that requires every `*.contract.ts` file to reference
"zod" in its own module scope -- either via an import (named, default,
namespace, or side-effect) or via a re-export.

Prevents the silent type-collapse regression documented in ACT-4870:
when a contract file infers zod-typed schemas from sibling files but
doesn't reference zod in its own scope, TypeScript's declaration emit
synthesizes inline imports as `import("node_modules/zod/index.cjs")`.
That malformed specifier fails to resolve for downstream consumers,
so every type that references it silently collapses to `any` --
without any `tsc` error. The fix is to give TS a clean "zod"
specifier in the source-level scope of the file being emitted.

Auto-applies to all `*.contract.ts` files via `@clipboard-health/eslint-config`,
mirroring how `require-http-module-factory` is auto-applied to `*.module.ts`.
@poldridge poldridge force-pushed the pauloldridge-act-4873-add-require-zod-import-in-contracts-rule branch from 47e692a to 8e3127e Compare May 31, 2026 14:19
@poldridge poldridge requested a review from cbh-clayton June 5, 2026 16:28
@poldridge poldridge merged commit 4ca4f2c into main Jun 5, 2026
8 checks passed
@poldridge poldridge deleted the pauloldridge-act-4873-add-require-zod-import-in-contracts-rule branch June 5, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants