From ebe23521d86d4cf35170f9e48fec337e133f94db Mon Sep 17 00:00:00 2001 From: termorey Date: Sun, 7 Sep 2025 17:06:49 +0300 Subject: [PATCH 01/16] chore: init zod package --- packages/zod/CHANGELOG.md | 1 + packages/zod/README.md | 3 + packages/zod/package.json | 49 +++ packages/zod/src/__tests__/contract.test-d.ts | 218 +++++++++++ packages/zod/src/__tests__/contract.test.ts | 363 ++++++++++++++++++ packages/zod/src/index.ts | 1 + packages/zod/src/zod_contract.ts | 61 +++ packages/zod/tsconfig.json | 16 + packages/zod/vite.config.ts | 26 ++ pnpm-lock.yaml | 12 + 10 files changed, 750 insertions(+) create mode 100644 packages/zod/CHANGELOG.md create mode 100644 packages/zod/README.md create mode 100644 packages/zod/package.json create mode 100644 packages/zod/src/__tests__/contract.test-d.ts create mode 100644 packages/zod/src/__tests__/contract.test.ts create mode 100644 packages/zod/src/index.ts create mode 100644 packages/zod/src/zod_contract.ts create mode 100644 packages/zod/tsconfig.json create mode 100644 packages/zod/vite.config.ts diff --git a/packages/zod/CHANGELOG.md b/packages/zod/CHANGELOG.md new file mode 100644 index 0000000..57e9a3b --- /dev/null +++ b/packages/zod/CHANGELOG.md @@ -0,0 +1 @@ +# @withease/zod diff --git a/packages/zod/README.md b/packages/zod/README.md new file mode 100644 index 0000000..7194a3d --- /dev/null +++ b/packages/zod/README.md @@ -0,0 +1,3 @@ +# @withease/zod + +Read documentation [here](). diff --git a/packages/zod/package.json b/packages/zod/package.json new file mode 100644 index 0000000..007655a --- /dev/null +++ b/packages/zod/package.json @@ -0,0 +1,49 @@ +{ + "name": "@withease/zod", + "version": "0.1.0", + "license": "MIT", + "repository": "https://github.com/igorkamyshev/withease", + "scripts": { + "test:run": "vitest run --typecheck", + "build": "vite build", + "size": "size-limit", + "publint": "node ../../tools/publint.mjs", + "typelint": "attw --pack" + }, + "peerDependencies": { + "@withease/contracts": "workspace:*", + "zod": "^3.25.0 || ^4.0.0" + }, + "devDependencies": { + "@withease/contracts": "workspace:*", + "zod": "^3.25.0 || ^4.0.0" + }, + "type": "module", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "main": "./dist/zod.cjs", + "module": "./dist/zod.js", + "types": "./dist/zod.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/zod.d.ts", + "default": "./dist/zod.js" + }, + "require": { + "types": "./dist/zod.d.cts", + "default": "./dist/zod.cjs" + } + } + }, + "size-limit": [ + { + "path": "./dist/zod.js", + "limit": "231 B" + } + ] +} diff --git a/packages/zod/src/__tests__/contract.test-d.ts b/packages/zod/src/__tests__/contract.test-d.ts new file mode 100644 index 0000000..ef8c378 --- /dev/null +++ b/packages/zod/src/__tests__/contract.test-d.ts @@ -0,0 +1,218 @@ +import { describe, test, expectTypeOf } from 'vitest'; +import { z as zodV3 } from 'zod/v3'; +import { z as zodV4 } from 'zod/v4'; +import { z as zodV4mini } from 'zod/v4-mini'; + +import { zodContract } from '../zod_contract'; + +describe('zodContract (zod v3)', () => { + test('string', () => { + const stringContract = zodContract(zodV3.string()); + + const smth: unknown = null; + + if (stringContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf(); + expectTypeOf(smth).not.toEqualTypeOf(); + } + }); + + test('complex object', () => { + const complexContract = zodContract( + zodV3.tuple([ + zodV3.object({ + x: zodV3.number(), + y: zodV3.literal(false), + k: zodV3.set(zodV3.string()), + }), + zodV3.literal('literal'), + zodV3.literal(42), + ]) + ); + + const smth: unknown = null; + + if (complexContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf< + [ + { + x: number; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + + expectTypeOf(smth).not.toEqualTypeOf(); + + expectTypeOf(smth).not.toEqualTypeOf< + [ + { + x: string; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + } + }); + + test('branded type', () => { + const BrandedContainer = zodV3.object({ + branded: zodV3.string().brand<'Branded'>(), + }); + const brandedContract = zodContract(BrandedContainer); + + const smth: unknown = { branded: 'branded' }; + + if (brandedContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf>(); + } + }); +}); + +describe('zodContract (zod v4)', () => { + test('string', () => { + const stringContract = zodContract(zodV4.string()); + + const smth: unknown = null; + + if (stringContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf(); + expectTypeOf(smth).not.toEqualTypeOf(); + } + }); + + test('complex object', () => { + const complexContract = zodContract( + zodV4.tuple([ + zodV4.object({ + x: zodV4.number(), + y: zodV4.literal(false), + k: zodV4.set(zodV4.string()), + }), + zodV4.literal('literal'), + zodV4.literal(42), + ]) + ); + + const smth: unknown = null; + + if (complexContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf< + [ + { + x: number; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + + expectTypeOf(smth).not.toEqualTypeOf(); + + expectTypeOf(smth).not.toEqualTypeOf< + [ + { + x: string; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + } + }); + + test('branded type', () => { + const BrandedContainer = zodV4.object({ + branded: zodV4.string().brand<'Branded'>(), + }); + const brandedContract = zodContract(BrandedContainer); + + const smth: unknown = { branded: 'branded' }; + + if (brandedContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf>(); + } + }); +}); + +describe('zodContract (zod v4-mini)', () => { + test('string', () => { + const stringContract = zodContract(zodV4mini.string()); + + const smth: unknown = null; + + if (stringContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf(); + expectTypeOf(smth).not.toEqualTypeOf(); + } + }); + + test('complex object', () => { + const complexContract = zodContract( + zodV4mini.tuple([ + zodV4mini.object({ + x: zodV4mini.number(), + y: zodV4mini.literal(false), + k: zodV4mini.set(zodV4mini.string()), + }), + zodV4mini.literal('literal'), + zodV4mini.literal(42), + ]) + ); + + const smth: unknown = null; + + if (complexContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf< + [ + { + x: number; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + + expectTypeOf(smth).not.toEqualTypeOf(); + + expectTypeOf(smth).not.toEqualTypeOf< + [ + { + x: string; + y: false; + k: Set; + }, + 'literal', + 42, + ] + >(); + } + }); + + test('branded type', () => { + const BrandedContainer = zodV4mini.object({ + branded: zodV4mini.string().brand<'Branded'>(), + }); + const brandedContract = zodContract(BrandedContainer); + + const smth: unknown = { branded: 'branded' }; + + if (brandedContract.isData(smth)) { + expectTypeOf(smth).toEqualTypeOf< + zodV4mini.infer + >(); + } + }); +}); diff --git a/packages/zod/src/__tests__/contract.test.ts b/packages/zod/src/__tests__/contract.test.ts new file mode 100644 index 0000000..025315a --- /dev/null +++ b/packages/zod/src/__tests__/contract.test.ts @@ -0,0 +1,363 @@ +import { z as zod_v3 } from 'zod/v3'; +import { z as zod_v4 } from 'zod/v4'; +import { z as zod_v4mini } from 'zod/v4-mini'; +import { describe, test, expect } from 'vitest'; + +import { zodContract } from '../zod_contract'; + +describe('zod/zodContract short (zod v3)', () => { + const zod = zod_v3; + + test('interprets invalid response as error', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(` + [ + "Expected string, received number", + ] + `); + }); + + test('passes valid data', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages('foo')).toEqual([]); + }); + + test('isData passes for valid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + x: 42, + y: 'answer', + }) + ).toEqual(true); + }); + + test('isData does not pass for invalid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + 42: 'x', + answer: 'y', + }) + ).toEqual(false); + }); + + test('interprets complex invalid response as error', () => { + const contract = zodContract( + zod.tuple([ + zod.object({ + x: zod.number(), + y: zod.literal(true), + k: zod + .set(zod.string()) + .nonempty('Invalid set, expected set of strings'), + }), + zod.literal('Uhm?'), + zod.literal(42), + ]) + ); + + expect( + contract.getErrorMessages([ + { + x: 456, + y: false, + k: new Set(), + }, + 'Answer is:', + '42', + ]) + ).toMatchInlineSnapshot(` + [ + "Invalid literal value, expected true, path: 0.y", + "Invalid set, expected set of strings, path: 0.k", + "Invalid literal value, expected "Uhm?", path: 1", + "Invalid literal value, expected 42, path: 2", + ] + `); + }); + + test('path from original zod error included in final message', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.object({ + z: zod.string(), + k: zod.object({ + j: zod.boolean(), + }), + }), + }) + ); + + expect( + contract.getErrorMessages({ + x: '42', + y: { + z: 123, + k: { + j: new Map(), + }, + }, + }) + ).toMatchInlineSnapshot(` + [ + "Expected number, received string, path: x", + "Expected string, received number, path: y.z", + "Expected boolean, received map, path: y.k.j", + ] + `); + }); +}); + +describe('zod/zodContract short (zod v4)', () => { + const zod = zod_v4; + + test('interprets invalid response as error', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(` + [ + "Invalid input: expected string, received number", + ] + `); + }); + + test('passes valid data', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages('foo')).toEqual([]); + }); + + test('isData passes for valid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + x: 42, + y: 'answer', + }) + ).toEqual(true); + }); + + test('isData does not pass for invalid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + 42: 'x', + answer: 'y', + }) + ).toEqual(false); + }); + + test('interprets complex invalid response as error', () => { + const contract = zodContract( + zod.tuple([ + zod.object({ + x: zod.number(), + y: zod.literal(true), + k: zod + .set(zod.string()) + .nonempty('Invalid input: expected set of strings'), + }), + zod.literal('Uhm?'), + zod.literal(42), + ]) + ); + + expect( + contract.getErrorMessages([ + { + x: 456, + y: false, + k: new Set(), + }, + 'Answer is:', + '42', + ]) + ).toMatchInlineSnapshot(` + [ + "Invalid input: expected true, path: 0.y", + "Invalid input: expected set of strings, path: 0.k", + "Invalid input: expected "Uhm?", path: 1", + "Invalid input: expected 42, path: 2", + ] + `); + }); + + test('path from original zod error included in final message', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.object({ + z: zod.string(), + k: zod.object({ + j: zod.boolean(), + }), + }), + }) + ); + + expect( + contract.getErrorMessages({ + x: '42', + y: { + z: 123, + k: { + j: new Map(), + }, + }, + }) + ).toMatchInlineSnapshot(` + [ + "Invalid input: expected number, received string, path: x", + "Invalid input: expected string, received number, path: y.z", + "Invalid input: expected boolean, received Map, path: y.k.j", + ] + `); + }); +}); + +describe('zod/zodContract short (zod v4-mini)', () => { + const zod = zod_v4mini; + + test('interprets invalid response as error', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(` + [ + "Invalid input: expected string, received number", + ] + `); + }); + + test('passes valid data', () => { + const contract = zodContract(zod.string()); + + expect(contract.getErrorMessages('foo')).toEqual([]); + }); + + test('isData passes for valid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + x: 42, + y: 'answer', + }) + ).toEqual(true); + }); + + test('isData does not pass for invalid data', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.string(), + }) + ); + + expect( + contract.isData({ + 42: 'x', + answer: 'y', + }) + ).toEqual(false); + }); + + test('interprets complex invalid response as error', () => { + const contract = zodContract( + zod.tuple([ + zod.object({ + x: zod.number(), + y: zod.literal(true), + k: zod.set(zod.string(), { + error: 'Invalid input: expected set of strings', + }), + }), + zod.literal('Uhm?'), + zod.literal(42), + ]) + ); + + expect( + contract.getErrorMessages([ + { + x: 456, + y: false, + k: new Map(), + }, + 'Answer is:', + '42', + ]) + ).toMatchInlineSnapshot(` + [ + "Invalid input: expected true, path: 0.y", + "Invalid input: expected set of strings, path: 0.k", + "Invalid input: expected "Uhm?", path: 1", + "Invalid input: expected 42, path: 2", + ] + `); + }); + + test('path from original zod error included in final message', () => { + const contract = zodContract( + zod.object({ + x: zod.number(), + y: zod.object({ + z: zod.string(), + k: zod.object({ + j: zod.boolean(), + }), + }), + }) + ); + + expect( + contract.getErrorMessages({ + x: '42', + y: { + z: 123, + k: { + j: new Map(), + }, + }, + }) + ).toMatchInlineSnapshot(` + [ + "Invalid input: expected number, received string, path: x", + "Invalid input: expected string, received number, path: y.z", + "Invalid input: expected boolean, received Map, path: y.k.j", + ] + `); + }); +}); diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts new file mode 100644 index 0000000..44de771 --- /dev/null +++ b/packages/zod/src/index.ts @@ -0,0 +1 @@ +export { zodContract } from './zod_contract'; diff --git a/packages/zod/src/zod_contract.ts b/packages/zod/src/zod_contract.ts new file mode 100644 index 0000000..5d8b58b --- /dev/null +++ b/packages/zod/src/zod_contract.ts @@ -0,0 +1,61 @@ +import { type ZodType as ZodTypeV3, type ZodError as ZodErrorV3, type TypeOf as TypeOfV3 } from 'zod/v3'; +import { + type $ZodType as ZodTypeV4, + type output as TypeOfV4, + type $ZodError as ZodErrorV4, + safeParse, +} from 'zod/v4/core'; +import { type Contract } from '@withease/contracts'; + +type ZodAnyType = ZodTypeV3 | ZodTypeV4; +type ZodAnyError = ZodErrorV3 | ZodErrorV4; + +type Output = T extends ZodTypeV4 + ? TypeOfV4 + : T extends ZodTypeV3 + ? TypeOfV3 + : never; + +type ErrorTransformer = (issues: ZodAnyError) => ReturnType['getErrorMessages']>; + +function isZodV4(schema: unknown): schema is ZodTypeV4 { + return !!schema && typeof schema === 'object' && '_zod' in schema; +} + +const standardErrorTransformer: ErrorTransformer = (error) => { + return error.issues.map((e) => { + const path = e.path.join('.'); + return path !== '' ? `${e.message}, path: ${path}` : e.message; + }); +} + +/** + * Transforms Zod contracts for `data` to internal Contract. + * Any response which does not conform to `data` will be treated as error. + * + * @param {ZodTypeV3 | ZodTypeV4} data Zod Contract for valid data + */ +function zodContract( + data: T +): Contract> { + function isData(prepared: unknown): prepared is Output { + if (isZodV4(data)) return safeParse(data, prepared).success; + return data.safeParse(prepared).success; + } + + return { + isData, + getErrorMessages(raw) { + const validation = isZodV4(data) + ? safeParse(data, raw) + : data.safeParse(raw); + if (validation.success) { + return []; + } + + return standardErrorTransformer(validation.error); + }, + }; +} + +export { zodContract }; diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json new file mode 100644 index 0000000..467180e --- /dev/null +++ b/packages/zod/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "types": ["node"], + "outDir": "dist", + "rootDir": "src", + "baseUrl": "src" + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../core" + } + ] +} diff --git a/packages/zod/vite.config.ts b/packages/zod/vite.config.ts new file mode 100644 index 0000000..e32b736 --- /dev/null +++ b/packages/zod/vite.config.ts @@ -0,0 +1,26 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; + +import dts from '../../tools/vite/types'; + +export default { + plugins: [tsconfigPaths(), dts()], + test: { + pool: 'threads', + poolOptions: { + threads: { + useAtomics: true, + }, + }, + }, + build: { + lib: { + entry: 'src/index.ts', + name: '@farfetched/zod', + fileName: 'zod', + formats: ['es', 'cjs'], + }, + rollupOptions: { + external: ['zod', 'zod/v3', 'zod/v4/core'], + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14dc770..9485a51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,14 @@ importers: packages/web-api: specifiers: {} + packages/zod: + specifiers: + '@withease/contracts': workspace:* + zod: ^3.25.0 || ^4.0.0 + devDependencies: + '@withease/contracts': link:../contracts + zod: 4.1.5 + packages: /@algolia/autocomplete-core/1.9.3_andexh5lxsyy34kfg3pekcyz5e: @@ -4696,3 +4704,7 @@ packages: optionalDependencies: commander: 9.5.0 dev: true + + /zod/4.1.5: + resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + dev: true From 1b2b6cb68646e63ed38d45bd5c513f3e2d8ae723 Mon Sep 17 00:00:00 2001 From: termorey Date: Wed, 10 Sep 2025 15:58:59 +0300 Subject: [PATCH 02/16] chore: added ignored IDE directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 587668d..c7a3725 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ dist node_modules .DS_Store +.idea *.log # Playwright From 3da187fdc1be80abdedfb5bb8965c0d15a0cbadf Mon Sep 17 00:00:00 2001 From: termorey Date: Wed, 10 Sep 2025 17:24:02 +0300 Subject: [PATCH 03/16] chore: added docs article --- apps/website/docs/zod/index.md | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 apps/website/docs/zod/index.md diff --git a/apps/website/docs/zod/index.md b/apps/website/docs/zod/index.md new file mode 100644 index 0000000..850f521 --- /dev/null +++ b/apps/website/docs/zod/index.md @@ -0,0 +1,95 @@ + + +# @withease/zod + +Extremely small adapter (less than **{{maxSize}}** controlled by CI) for Zod package support, initially created for [_Contracts_](/protocols/contract) that allows you to introduce data validation on edges of the application. + +## Installation + +First, you need to install package: + +::: code-group + +```sh [pnpm] +pnpm install @withease/zod +``` + +```sh [yarn] +yarn add @withease/zod +``` + +```sh [npm] +npm install @withease/zod +``` + +::: + +## Version compatibility + +Best way of usage - use supported zod version. zod@4 is preferred, but we understand the need to maintain a transitional versions help you with migrations. + +## Before zod@3.25.0 + +You can use adapter from [Farfetched](https://ff.effector/) + +```ts +import { z } from "zod"; +import { zodContract } from '@farfetched/zod'; +``` + +## With zod@^3.25.0 + +You still can use latest versions of zod@3 for migrations we support it. + +```ts +import { z } from "zod/v3"; +import { zodContract } from '@withease/zod'; +``` + +## Usage as a _Contract_ + +`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocol/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). + +### Farfetched + +[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract). + +```ts +import { createQuery } from '@farfetched/core'; +import { z } from "zod"; +import { zodContract } from "@withease/zod"; + +const CharacterSchema = z.object({ + id: z.string(), + name: z.string(), + status: StatusSchema, + species: z.string(), + type: z.string(), + gender: GenderSchema, + origin: z.object({ name: z.string(), url: z.string() }), + location: z.object({ name: z.string(), url: z.string() }), + image: z.string(), + episode: z.array(z.string()), +}); + +const characterQuery = createQuery({ + effect: createEffect(async (config: { id: number; }) => { + const response = await fetch(`https://rickandmortyapi.com/api/character/${config.id}`); + return response.json(); + }), + // after receiving data from the server + // check if it is conforms the Contract to ensure + // API does not return something unexpected + contract: zodContract(CharacterSchema), +}); +``` + +### Integration with other libraries + +Since *zodContract* (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. + +The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). From d50ab178498e8382c22cfcb846085529dbb2e0ee Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 15:17:37 +0300 Subject: [PATCH 04/16] fix: removed incorrect reference --- packages/zod/tsconfig.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index 467180e..65b1ff2 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -7,10 +7,5 @@ "rootDir": "src", "baseUrl": "src" }, - "include": ["src/**/*.ts"], - "references": [ - { - "path": "../core" - } - ] + "include": ["src/**/*.ts"] } From 7bb80177cf4a466902f96502f2457d2d8ff0533d Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:12:47 +0300 Subject: [PATCH 05/16] feat: added zod docs pages --- apps/website/docs/.vitepress/config.mjs | 11 ++++ apps/website/docs/zod/api/contract/index.md | 47 ++++++++++++++ apps/website/docs/zod/compatibility.md | 30 +++++++++ apps/website/docs/zod/index.md | 68 ++------------------- 4 files changed, 94 insertions(+), 62 deletions(-) create mode 100644 apps/website/docs/zod/api/contract/index.md create mode 100644 apps/website/docs/zod/compatibility.md diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs index b847f05..741d5cf 100644 --- a/apps/website/docs/.vitepress/config.mjs +++ b/apps/website/docs/.vitepress/config.mjs @@ -49,6 +49,7 @@ export default defineConfig({ { text: 'web-api', link: '/web-api/' }, { text: 'factories', link: '/factories/' }, { text: 'contracts', link: '/contracts/' }, + { text: 'zod', link: '/zod/' }, ], }, { text: 'Magazine', link: '/magazine/' }, @@ -140,6 +141,16 @@ export default defineConfig({ }, { text: 'APIs', link: '/contracts/api' }, ]), + ...createSidebar('zod', [ + { text: 'Get Started', link: '/zod/' }, + { text: 'Compatibility', link: '/zod/compatibility' }, + { + text: "API", + items: [ + { text: "zodContract", link: '/zod/api/contract/' } + ] + } + ]), '/magazine/': [ { text: 'Architecture', diff --git a/apps/website/docs/zod/api/contract/index.md b/apps/website/docs/zod/api/contract/index.md new file mode 100644 index 0000000..735ca00 --- /dev/null +++ b/apps/website/docs/zod/api/contract/index.md @@ -0,0 +1,47 @@ +# zodContract + +Adapter, initially created for [_Contracts_](/protocols/contract) that allows you to introduce data validation of the application. + +## Usage as a _Contract_ + +`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocol/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). + +### Farfetched + +[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract). + +```ts +import { createQuery } from '@farfetched/core'; +import { z } from "zod"; +import { zodContract } from "@withease/zod"; + +const CharacterSchema = z.object({ + id: z.string(), + name: z.string(), + status: StatusSchema, + species: z.string(), + type: z.string(), + gender: GenderSchema, + origin: z.object({ name: z.string(), url: z.string() }), + location: z.object({ name: z.string(), url: z.string() }), + image: z.string(), + episode: z.array(z.string()), +}); + +const characterQuery = createQuery({ + effect: createEffect(async (config: { id: number; }) => { + const response = await fetch(`https://rickandmortyapi.com/api/character/${config.id}`); + return response.json(); + }), + // after receiving data from the server + // check if it is conforms the Contract to ensure + // API does not return something unexpected + contract: zodContract(CharacterSchema), +}); +``` + +### Integration with other libraries + +Since *zodContract* (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. + +The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). diff --git a/apps/website/docs/zod/compatibility.md b/apps/website/docs/zod/compatibility.md new file mode 100644 index 0000000..4ecbb06 --- /dev/null +++ b/apps/website/docs/zod/compatibility.md @@ -0,0 +1,30 @@ +# Compatibility + +The best practice is to use a supported version of Zod. Preferably, opt for Zod@4. However, we recognize the need to support transitional versions to assist with migrations. + +## With zod@^4 + +You can use standard zod import. + +```ts +import { z } from "zod"; +import { zodContract } from '@withease/zod'; +``` + +## With zod@^3.25.0 + +You still can use latest versions of zod@3 for migrations we support it. + +```ts +import { z } from "zod/v3"; +import { zodContract } from '@withease/zod'; +``` + +## Before zod@3.25.0 + +You can use adapter from [Farfetched](https://ff.effector/) + +```ts +import { z } from "zod"; +import { zodContract } from '@farfetched/zod'; +``` diff --git a/apps/website/docs/zod/index.md b/apps/website/docs/zod/index.md index 850f521..f0d3782 100644 --- a/apps/website/docs/zod/index.md +++ b/apps/website/docs/zod/index.md @@ -6,7 +6,7 @@ const maxSize = pkg['size-limit'].at(0).limit; # @withease/zod -Extremely small adapter (less than **{{maxSize}}** controlled by CI) for Zod package support, initially created for [_Contracts_](/protocols/contract) that allows you to introduce data validation on edges of the application. +Small adapters (less than **{{maxSize}}** summary controlled by CI) for Zod package support. ## Installation @@ -30,66 +30,10 @@ npm install @withease/zod ## Version compatibility -Best way of usage - use supported zod version. zod@4 is preferred, but we understand the need to maintain a transitional versions help you with migrations. +Best way of usage - use a supported zod version. zod@4 is preferred.\ +But we understand the need to maintain a transitional versions to assist with migrations.\ +[Read more](./compatibility) -## Before zod@3.25.0 +## API -You can use adapter from [Farfetched](https://ff.effector/) - -```ts -import { z } from "zod"; -import { zodContract } from '@farfetched/zod'; -``` - -## With zod@^3.25.0 - -You still can use latest versions of zod@3 for migrations we support it. - -```ts -import { z } from "zod/v3"; -import { zodContract } from '@withease/zod'; -``` - -## Usage as a _Contract_ - -`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocol/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). - -### Farfetched - -[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract). - -```ts -import { createQuery } from '@farfetched/core'; -import { z } from "zod"; -import { zodContract } from "@withease/zod"; - -const CharacterSchema = z.object({ - id: z.string(), - name: z.string(), - status: StatusSchema, - species: z.string(), - type: z.string(), - gender: GenderSchema, - origin: z.object({ name: z.string(), url: z.string() }), - location: z.object({ name: z.string(), url: z.string() }), - image: z.string(), - episode: z.array(z.string()), -}); - -const characterQuery = createQuery({ - effect: createEffect(async (config: { id: number; }) => { - const response = await fetch(`https://rickandmortyapi.com/api/character/${config.id}`); - return response.json(); - }), - // after receiving data from the server - // check if it is conforms the Contract to ensure - // API does not return something unexpected - contract: zodContract(CharacterSchema), -}); -``` - -### Integration with other libraries - -Since *zodContract* (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. - -The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). +- [zodContract](api/contract) From 438ca6765cd15e207b79771fbcb9e6ebe04ec325 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:20:06 +0300 Subject: [PATCH 06/16] fix: incorrect name --- packages/zod/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zod/vite.config.ts b/packages/zod/vite.config.ts index e32b736..c50b531 100644 --- a/packages/zod/vite.config.ts +++ b/packages/zod/vite.config.ts @@ -15,7 +15,7 @@ export default { build: { lib: { entry: 'src/index.ts', - name: '@farfetched/zod', + name: '@withease/zod', fileName: 'zod', formats: ['es', 'cjs'], }, From f04e64a2b30e844069e3051ece91cc19a7e86871 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:20:29 +0300 Subject: [PATCH 07/16] chore: updated vite config --- packages/zod/vite.config.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/zod/vite.config.ts b/packages/zod/vite.config.ts index c50b531..983728e 100644 --- a/packages/zod/vite.config.ts +++ b/packages/zod/vite.config.ts @@ -1,17 +1,13 @@ import tsconfigPaths from 'vite-tsconfig-paths'; - import dts from '../../tools/vite/types'; export default { - plugins: [tsconfigPaths(), dts()], test: { - pool: 'threads', - poolOptions: { - threads: { - useAtomics: true, - }, + typecheck: { + ignoreSourceErrors: true, }, }, + plugins: [tsconfigPaths(), dts()], build: { lib: { entry: 'src/index.ts', From 58ef4156badd0f324548ec7db85fd2f5d2625a70 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:22:06 +0300 Subject: [PATCH 08/16] chore: added docs link --- packages/zod/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zod/README.md b/packages/zod/README.md index 7194a3d..b8b3663 100644 --- a/packages/zod/README.md +++ b/packages/zod/README.md @@ -1,3 +1,3 @@ # @withease/zod -Read documentation [here](). +Read documentation [here](https://withease.effector.dev/zod/). From 1d441ce453f3187185b0a483a6a815b49e9d405b Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:25:05 +0300 Subject: [PATCH 09/16] chore: added missed script --- packages/zod/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zod/package.json b/packages/zod/package.json index 007655a..c3d886a 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -5,6 +5,7 @@ "repository": "https://github.com/igorkamyshev/withease", "scripts": { "test:run": "vitest run --typecheck", + "test:watch": "vitest --typecheck", "build": "vite build", "size": "size-limit", "publint": "node ../../tools/publint.mjs", From 1e7ef3ff759d26bbc74edeb5a0f819cf0a7055c3 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:28:15 +0300 Subject: [PATCH 10/16] chore: removed default changelog file --- packages/zod/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/zod/CHANGELOG.md diff --git a/packages/zod/CHANGELOG.md b/packages/zod/CHANGELOG.md deleted file mode 100644 index 57e9a3b..0000000 --- a/packages/zod/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -# @withease/zod From 6cf326feb46f21e008122c408807d7d698421382 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:41:58 +0300 Subject: [PATCH 11/16] chore: updated links --- apps/website/docs/zod/api/contract/index.md | 2 +- apps/website/docs/zod/compatibility.md | 2 +- apps/website/docs/zod/index.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/website/docs/zod/api/contract/index.md b/apps/website/docs/zod/api/contract/index.md index 735ca00..1cb7b80 100644 --- a/apps/website/docs/zod/api/contract/index.md +++ b/apps/website/docs/zod/api/contract/index.md @@ -4,7 +4,7 @@ Adapter, initially created for [_Contracts_](/protocols/contract) that allows yo ## Usage as a _Contract_ -`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocol/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). +`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocols/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). ### Farfetched diff --git a/apps/website/docs/zod/compatibility.md b/apps/website/docs/zod/compatibility.md index 4ecbb06..a41239f 100644 --- a/apps/website/docs/zod/compatibility.md +++ b/apps/website/docs/zod/compatibility.md @@ -1,6 +1,6 @@ # Compatibility -The best practice is to use a supported version of Zod. Preferably, opt for Zod@4. However, we recognize the need to support transitional versions to assist with migrations. +The best practice is to use a supported version of [Zod](https://zod.dev/). Preferably, opt for Zod@4. However, we recognize the need to support transitional versions to assist with migrations. ## With zod@^4 diff --git a/apps/website/docs/zod/index.md b/apps/website/docs/zod/index.md index f0d3782..350b589 100644 --- a/apps/website/docs/zod/index.md +++ b/apps/website/docs/zod/index.md @@ -6,7 +6,7 @@ const maxSize = pkg['size-limit'].at(0).limit; # @withease/zod -Small adapters (less than **{{maxSize}}** summary controlled by CI) for Zod package support. +Small adapters (less than **{{maxSize}}** summary controlled by CI) for [Zod](https://zod.dev/) package support. ## Installation @@ -36,4 +36,4 @@ But we understand the need to maintain a transitional versions to assist with mi ## API -- [zodContract](api/contract) +- [zodContract](./api/contract/) From 71e792d9f9a21af1b21e5ba2302ae4497b71105c Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 16:46:10 +0300 Subject: [PATCH 12/16] chore: apply prettier format --- apps/website/docs/zod/api/contract/index.md | 12 +++++++----- apps/website/docs/zod/compatibility.md | 6 +++--- packages/zod/src/__tests__/contract.test-d.ts | 12 ++++++------ packages/zod/src/zod_contract.ts | 16 +++++++++++----- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/website/docs/zod/api/contract/index.md b/apps/website/docs/zod/api/contract/index.md index 1cb7b80..53a0836 100644 --- a/apps/website/docs/zod/api/contract/index.md +++ b/apps/website/docs/zod/api/contract/index.md @@ -12,8 +12,8 @@ Adapter, initially created for [_Contracts_](/protocols/contract) that allows yo ```ts import { createQuery } from '@farfetched/core'; -import { z } from "zod"; -import { zodContract } from "@withease/zod"; +import { z } from 'zod'; +import { zodContract } from '@withease/zod'; const CharacterSchema = z.object({ id: z.string(), @@ -29,8 +29,10 @@ const CharacterSchema = z.object({ }); const characterQuery = createQuery({ - effect: createEffect(async (config: { id: number; }) => { - const response = await fetch(`https://rickandmortyapi.com/api/character/${config.id}`); + effect: createEffect(async (config: { id: number }) => { + const response = await fetch( + `https://rickandmortyapi.com/api/character/${config.id}` + ); return response.json(); }), // after receiving data from the server @@ -42,6 +44,6 @@ const characterQuery = createQuery({ ### Integration with other libraries -Since *zodContract* (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. +Since _zodContract_ (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). diff --git a/apps/website/docs/zod/compatibility.md b/apps/website/docs/zod/compatibility.md index a41239f..3a637d5 100644 --- a/apps/website/docs/zod/compatibility.md +++ b/apps/website/docs/zod/compatibility.md @@ -7,7 +7,7 @@ The best practice is to use a supported version of [Zod](https://zod.dev/). Pref You can use standard zod import. ```ts -import { z } from "zod"; +import { z } from 'zod'; import { zodContract } from '@withease/zod'; ``` @@ -16,7 +16,7 @@ import { zodContract } from '@withease/zod'; You still can use latest versions of zod@3 for migrations we support it. ```ts -import { z } from "zod/v3"; +import { z } from 'zod/v3'; import { zodContract } from '@withease/zod'; ``` @@ -25,6 +25,6 @@ import { zodContract } from '@withease/zod'; You can use adapter from [Farfetched](https://ff.effector/) ```ts -import { z } from "zod"; +import { z } from 'zod'; import { zodContract } from '@farfetched/zod'; ``` diff --git a/packages/zod/src/__tests__/contract.test-d.ts b/packages/zod/src/__tests__/contract.test-d.ts index ef8c378..7da907e 100644 --- a/packages/zod/src/__tests__/contract.test-d.ts +++ b/packages/zod/src/__tests__/contract.test-d.ts @@ -41,7 +41,7 @@ describe('zodContract (zod v3)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); @@ -55,7 +55,7 @@ describe('zodContract (zod v3)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); } @@ -111,7 +111,7 @@ describe('zodContract (zod v4)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); @@ -125,7 +125,7 @@ describe('zodContract (zod v4)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); } @@ -181,7 +181,7 @@ describe('zodContract (zod v4-mini)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); @@ -195,7 +195,7 @@ describe('zodContract (zod v4-mini)', () => { k: Set; }, 'literal', - 42, + 42 ] >(); } diff --git a/packages/zod/src/zod_contract.ts b/packages/zod/src/zod_contract.ts index 5d8b58b..139d453 100644 --- a/packages/zod/src/zod_contract.ts +++ b/packages/zod/src/zod_contract.ts @@ -1,4 +1,8 @@ -import { type ZodType as ZodTypeV3, type ZodError as ZodErrorV3, type TypeOf as TypeOfV3 } from 'zod/v3'; +import { + type ZodType as ZodTypeV3, + type ZodError as ZodErrorV3, + type TypeOf as TypeOfV3, +} from 'zod/v3'; import { type $ZodType as ZodTypeV4, type output as TypeOfV4, @@ -13,10 +17,12 @@ type ZodAnyError = ZodErrorV3 | ZodErrorV4; type Output = T extends ZodTypeV4 ? TypeOfV4 : T extends ZodTypeV3 - ? TypeOfV3 - : never; + ? TypeOfV3 + : never; -type ErrorTransformer = (issues: ZodAnyError) => ReturnType['getErrorMessages']>; +type ErrorTransformer = ( + issues: ZodAnyError +) => ReturnType['getErrorMessages']>; function isZodV4(schema: unknown): schema is ZodTypeV4 { return !!schema && typeof schema === 'object' && '_zod' in schema; @@ -27,7 +33,7 @@ const standardErrorTransformer: ErrorTransformer = (error) => { const path = e.path.join('.'); return path !== '' ? `${e.message}, path: ${path}` : e.message; }); -} +}; /** * Transforms Zod contracts for `data` to internal Contract. From a040cb117f235dce4bb9da42a1dcc83940ec51e0 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 17:21:12 +0300 Subject: [PATCH 13/16] chore: added async schemas usage warning --- apps/website/docs/zod/api/contract/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/website/docs/zod/api/contract/index.md b/apps/website/docs/zod/api/contract/index.md index 53a0836..49ed6e2 100644 --- a/apps/website/docs/zod/api/contract/index.md +++ b/apps/website/docs/zod/api/contract/index.md @@ -2,6 +2,11 @@ Adapter, initially created for [_Contracts_](/protocols/contract) that allows you to introduce data validation of the application. +::: warning +Async schemas has not supported yet - related with [_Contracts protocol_](/protocols/contract) returned types:\ +Async schemas validation return Promise that can't be used with current [_Contracts protocol_](/protocols/contract) +::: + ## Usage as a _Contract_ `@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocols/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract). From 13fe002b747dd81cbe3182d4c4a309ad66aa1489 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 17:28:43 +0300 Subject: [PATCH 14/16] Initial release: Zod package adapter package --- .changeset/sharp-trees-obey.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-trees-obey.md diff --git a/.changeset/sharp-trees-obey.md b/.changeset/sharp-trees-obey.md new file mode 100644 index 0000000..85fef46 --- /dev/null +++ b/.changeset/sharp-trees-obey.md @@ -0,0 +1,5 @@ +--- +'@withease/zod': major +--- + +Initial release: Zod package adapter package From c0f7f3b0c8f9ec9f157b877ea1f6ffa55e46b744 Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 17:46:08 +0300 Subject: [PATCH 15/16] chore: changed size limit --- packages/zod/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zod/package.json b/packages/zod/package.json index c3d886a..680f473 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -44,7 +44,7 @@ "size-limit": [ { "path": "./dist/zod.js", - "limit": "231 B" + "limit": "315 B" } ] } From 7b47b03f07f41af6bfe2a877c31307ebc88daf3b Mon Sep 17 00:00:00 2001 From: termorey Date: Sat, 20 Sep 2025 17:48:12 +0300 Subject: [PATCH 16/16] Increased package size limit --- .changeset/dirty-buckets-roll.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-buckets-roll.md diff --git a/.changeset/dirty-buckets-roll.md b/.changeset/dirty-buckets-roll.md new file mode 100644 index 0000000..dab8192 --- /dev/null +++ b/.changeset/dirty-buckets-roll.md @@ -0,0 +1,5 @@ +--- +'@withease/zod': patch +--- + +Increased package size limit