diff --git a/README.md b/README.md
index e42e330e..fc762459 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@
- [ArkType](#arktype)
- [Valibot](#valibot)
- [effect-ts](#effect-ts)
+ - [VineJS](#vinejs)
- [Backers](#backers)
- [Sponsors](#sponsors)
- [Contributors](#contributors)
@@ -629,6 +630,41 @@ function TestComponent({ onSubmit }: Props) {
}
```
+### [VineJS](https://github.com/vinejs/vine)
+
+VineJS is a form data validation library for Node.js
+
+[![npm](https://img.shields.io/bundlephobia/minzip/@vinejs/vine?style=for-the-badge)](https://bundlephobia.com/result?p=@vinejs/vine)
+
+```typescript jsx
+import { useForm } from 'react-hook-form';
+import { vineResolver } from '@hookform/resolvers/vine';
+import vine from '@vinejs/vine';
+
+const schema = vine.compile(
+ vine.object({
+ username: vine.string().minLength(1),
+ password: vine.string().minLength(1),
+ }),
+);
+
+const App = () => {
+ const { register, handleSubmit } = useForm({
+ resolver: vineResolver(schema),
+ });
+
+ return (
+
+ );
+};
+```
+
## Backers
Thanks go to all our backers! [[Become a backer](https://opencollective.com/react-hook-form#backer)].
diff --git a/config/node-13-exports.js b/config/node-13-exports.js
index d7f8ccd5..173155d2 100644
--- a/config/node-13-exports.js
+++ b/config/node-13-exports.js
@@ -17,6 +17,7 @@ const subRepositories = [
'arktype',
'valibot',
'effect-ts',
+ 'vine',
];
const copySrc = () => {
diff --git a/package.json b/package.json
index c553687a..e22104eb 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "@hookform/resolvers",
"amdName": "hookformResolvers",
"version": "2.9.1",
- "description": "React Hook Form validation resolvers: Yup, Joi, Superstruct, Zod, Vest, Class Validator, io-ts, Nope, computed-types, TypeBox, arktype, Typanion and Effect-TS",
+ "description": "React Hook Form validation resolvers: Yup, Joi, Superstruct, Zod, Vest, Class Validator, io-ts, Nope, computed-types, TypeBox, arktype, Typanion, Effect-TS and VineJS",
"main": "dist/resolvers.js",
"module": "dist/resolvers.module.js",
"umd:main": "dist/resolvers.umd.js",
@@ -105,6 +105,12 @@
"import": "./effect-ts/dist/effect-ts.mjs",
"require": "./effect-ts/dist/effect-ts.js"
},
+ "./vine": {
+ "types": "./vine/dist/index.d.ts",
+ "umd": "./vine/dist/vine.umd.js",
+ "import": "./vine/dist/vine.mjs",
+ "require": "./vine/dist/vine.js"
+ },
"./package.json": "./package.json",
"./*": "./*"
},
@@ -154,7 +160,10 @@
"valibot/dist",
"effect-ts/package.json",
"effect-ts/src",
- "effect-ts/dist"
+ "effect-ts/dist",
+ "vine/package.json",
+ "vine/src",
+ "vine/dist"
],
"publishConfig": {
"access": "public"
@@ -178,6 +187,7 @@
"build:arktype": "microbundle --cwd arktype --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
"build:valibot": "microbundle --cwd valibot --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
"build:effect-ts": "microbundle --cwd effect-ts --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@effect/schema=EffectSchema,@effect/schema/AST=EffectSchemaAST,@effect/schema/ArrayFormatter=EffectSchemaArrayFormatter",
+ "build:vine": "microbundle --cwd vine --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@vinejs/vine=vine",
"postbuild": "node ./config/node-13-exports.js",
"lint": "eslint . --ext .ts,.js --ignore-path .gitignore",
"lint:types": "tsc",
@@ -207,7 +217,8 @@
"typanion",
"ajv",
"TypeBox",
- "arktype"
+ "arktype",
+ "vine"
],
"repository": {
"type": "git",
@@ -230,6 +241,7 @@
"@types/react": "^18.2.20",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
+ "@vinejs/vine": "^2.0.0",
"@vitejs/plugin-react": "^4.0.4",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 551f218b..fe537467 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@typescript-eslint/parser':
specifier: ^6.4.1
version: 6.4.1(eslint@8.47.0)(typescript@5.1.6)
+ '@vinejs/vine':
+ specifier: ^2.0.0
+ version: 2.0.0
'@vitejs/plugin-react':
specifier: ^4.0.4
version: 4.0.4(vite@4.4.9(@types/node@20.5.2)(terser@5.19.2))
@@ -1024,6 +1027,10 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@poppinss/macroable@1.0.2':
+ resolution: {integrity: sha512-xhhEcEvhQC8mP5oOr5hbE4CmUgmw/IPV1jhpGg2xSkzoFrt9i8YVqBQt9744EFesi5F7pBheWozg63RUBM/5JA==}
+ engines: {node: '>=18.16.0'}
+
'@rollup/plugin-alias@3.1.9':
resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==}
engines: {node: '>=8.0.0'}
@@ -1174,6 +1181,9 @@ packages:
'@types/validator@13.11.1':
resolution: {integrity: sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==}
+ '@types/validator@13.11.10':
+ resolution: {integrity: sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==}
+
'@typescript-eslint/eslint-plugin@6.4.1':
resolution: {integrity: sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -1232,6 +1242,14 @@ packages:
resolution: {integrity: sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==}
engines: {node: ^16.0.0 || >=18.0.0}
+ '@vinejs/compiler@2.5.0':
+ resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@vinejs/vine@2.0.0':
+ resolution: {integrity: sha512-NqgT4B2uo4mMsGI8LJdpuXNnan7F3xm10+kHaXpqI0PCYpn7+Xiic6av586mmj747/qZ3iR8o4C9cL54WU1fWw==}
+ engines: {node: '>=18.16.0'}
+
'@vitejs/plugin-react@4.0.4':
resolution: {integrity: sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -1441,6 +1459,10 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
+ camelcase@8.0.0:
+ resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
+ engines: {node: '>=16'}
+
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
@@ -1626,6 +1648,9 @@ packages:
resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==}
engines: {node: '>=14'}
+ dayjs@1.11.11:
+ resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
+
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -1676,6 +1701,9 @@ packages:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
+ dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+
doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
@@ -2508,6 +2536,10 @@ packages:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
+ normalize-url@8.0.1:
+ resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
+ engines: {node: '>=14.16'}
+
npm-run-all@4.1.5:
resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==}
engines: {node: '>= 4'}
@@ -4622,6 +4654,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
+ '@poppinss/macroable@1.0.2': {}
+
'@rollup/plugin-alias@3.1.9(rollup@2.79.1)':
dependencies:
rollup: 2.79.1
@@ -4771,6 +4805,8 @@ snapshots:
'@types/validator@13.11.1': {}
+ '@types/validator@13.11.10': {}
+
'@typescript-eslint/eslint-plugin@6.4.1(@typescript-eslint/parser@6.4.1(eslint@8.47.0)(typescript@5.1.6))(eslint@8.47.0)(typescript@5.1.6)':
dependencies:
'@eslint-community/regexpp': 4.7.0
@@ -4856,6 +4892,19 @@ snapshots:
'@typescript-eslint/types': 6.4.1
eslint-visitor-keys: 3.4.3
+ '@vinejs/compiler@2.5.0': {}
+
+ '@vinejs/vine@2.0.0':
+ dependencies:
+ '@poppinss/macroable': 1.0.2
+ '@types/validator': 13.11.10
+ '@vinejs/compiler': 2.5.0
+ camelcase: 8.0.0
+ dayjs: 1.11.11
+ dlv: 1.1.3
+ normalize-url: 8.0.1
+ validator: 13.11.0
+
'@vitejs/plugin-react@4.0.4(vite@4.4.9(@types/node@20.5.2)(terser@5.19.2))':
dependencies:
'@babel/core': 7.22.10
@@ -5083,6 +5132,8 @@ snapshots:
camelcase@6.3.0: {}
+ camelcase@8.0.0: {}
+
caniuse-api@3.0.0:
dependencies:
browserslist: 4.21.10
@@ -5313,6 +5364,8 @@ snapshots:
whatwg-mimetype: 3.0.0
whatwg-url: 12.0.1
+ dayjs@1.11.11: {}
+
debug@4.3.4:
dependencies:
ms: 2.1.2
@@ -5365,6 +5418,8 @@ snapshots:
dependencies:
path-type: 4.0.0
+ dlv@1.1.3: {}
+
doctrine@3.0.0:
dependencies:
esutils: 2.0.3
@@ -6326,6 +6381,8 @@ snapshots:
normalize-url@6.1.0: {}
+ normalize-url@8.0.1: {}
+
npm-run-all@4.1.5:
dependencies:
ansi-styles: 3.2.1
diff --git a/vine/package.json b/vine/package.json
new file mode 100644
index 00000000..473737df
--- /dev/null
+++ b/vine/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@hookform/resolvers/vine",
+ "amdName": "hookformResolversVine",
+ "version": "1.0.0",
+ "private": true,
+ "description": "React Hook Form validation resolver: vine",
+ "main": "dist/vine.js",
+ "module": "dist/vine.module.js",
+ "umd:main": "dist/vine.umd.js",
+ "source": "src/index.ts",
+ "types": "dist/index.d.ts",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0",
+ "@hookform/resolvers": "^2.0.0",
+ "@vinejs/vine": "^2.0.0"
+ }
+}
diff --git a/vine/src/__tests__/Form-native-validation.tsx b/vine/src/__tests__/Form-native-validation.tsx
new file mode 100644
index 00000000..e7ec4a4b
--- /dev/null
+++ b/vine/src/__tests__/Form-native-validation.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+import { vineResolver } from '..';
+import vine from '@vinejs/vine';
+import { Infer } from '@vinejs/vine/build/src/types';
+
+const schema = vine.compile(
+ vine.object({
+ username: vine.string().minLength(1),
+ password: vine.string().minLength(1),
+ }),
+);
+
+type FormData = Infer & { unusedProperty: string };
+
+interface Props {
+ onSubmit: (data: FormData) => void;
+}
+
+function TestComponent({ onSubmit }: Props) {
+ const { register, handleSubmit } = useForm({
+ resolver: vineResolver(schema),
+ shouldUseNativeValidation: true,
+ });
+
+ return (
+
+ );
+}
+
+test("form's native validation with Zod", async () => {
+ const handleSubmit = vi.fn();
+ render();
+
+ // username
+ let usernameField = screen.getByPlaceholderText(
+ /username/i,
+ ) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(true);
+ expect(usernameField.validationMessage).toBe('');
+
+ // password
+ let passwordField = screen.getByPlaceholderText(
+ /password/i,
+ ) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(true);
+ expect(passwordField.validationMessage).toBe('');
+
+ await user.click(screen.getByText(/submit/i));
+
+ // username
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(false);
+ expect(usernameField.validationMessage).toBe('The username field must have at least 1 characters');
+
+ // password
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(false);
+ expect(passwordField.validationMessage).toBe('The password field must have at least 1 characters');
+
+ await user.type(screen.getByPlaceholderText(/username/i), 'joe');
+ await user.type(screen.getByPlaceholderText(/password/i), 'password');
+
+ // username
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(true);
+ expect(usernameField.validationMessage).toBe('');
+
+ // password
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(true);
+ expect(passwordField.validationMessage).toBe('');
+});
diff --git a/vine/src/__tests__/Form.tsx b/vine/src/__tests__/Form.tsx
new file mode 100644
index 00000000..736f9c4d
--- /dev/null
+++ b/vine/src/__tests__/Form.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+import { useForm } from 'react-hook-form';
+import { vineResolver } from '..';
+import vine from '@vinejs/vine';
+import { Infer } from '@vinejs/vine/build/src/types';
+
+const schema = vine.compile(
+ vine.object({
+ username: vine.string().minLength(1),
+ password: vine.string().minLength(1),
+ }),
+);
+
+type FormData = Infer & { unusedProperty: string };
+
+interface Props {
+ onSubmit: (data: FormData) => void;
+}
+
+function TestComponent({ onSubmit }: Props) {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: vineResolver(schema), // Useful to check TypeScript regressions
+ });
+
+ return (
+
+ );
+}
+
+test("form's validation with Vine and TypeScript's integration", async () => {
+ const handleSubmit = vi.fn();
+ render();
+
+ expect(screen.queryAllByRole('alert')).toHaveLength(0);
+
+ await user.click(screen.getByText(/submit/i));
+
+ expect(screen.getByText(/The username field must have at least 1 characters/i)).toBeInTheDocument();
+ expect(screen.getByText(/The password field must have at least 1 characters/i)).toBeInTheDocument();
+ expect(handleSubmit).not.toHaveBeenCalled();
+});
diff --git a/vine/src/__tests__/__fixtures__/data.ts b/vine/src/__tests__/__fixtures__/data.ts
new file mode 100644
index 00000000..97717525
--- /dev/null
+++ b/vine/src/__tests__/__fixtures__/data.ts
@@ -0,0 +1,74 @@
+import { Field, InternalFieldName } from 'react-hook-form';
+import vine from '@vinejs/vine';
+import { Infer } from '@vinejs/vine/build/src/types';
+
+export const schema = vine.compile(
+ vine.object({
+ username: vine.string().regex(/^\w+$/).minLength(3).maxLength(30),
+ password: vine
+ .string()
+ .regex(new RegExp('.*[A-Z].*'))
+ .regex(new RegExp('.*[a-z].*'))
+ .regex(new RegExp('.*\\d.*'))
+ .regex(new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'))
+ .minLength(8)
+ .confirmed({ confirmationField: 'repeatPassword' }),
+ repeatPassword: vine.string().sameAs('password'),
+ accessToken: vine.unionOfTypes([vine.string(), vine.number()]),
+ birthYear: vine.number().min(1900).max(2013),
+ email: vine.string().email().optional(),
+ tags: vine.array(vine.string()),
+ enabled: vine.boolean(),
+ like: vine.array(
+ vine.object({
+ id: vine.number(),
+ name: vine.string().fixedLength(4),
+ }),
+ ),
+ dateStr: vine.string().transform((value: string) => new Date(value).toISOString()),
+ }),
+);
+
+export const validData: Infer = {
+ username: 'Doe',
+ password: 'Password123_',
+ repeatPassword: 'Password123_',
+ birthYear: 2000,
+ email: 'john@doe.com',
+ tags: ['tag1', 'tag2'],
+ enabled: true,
+ accessToken: 'accessToken',
+ like: [
+ {
+ id: 1,
+ name: 'name',
+ },
+ ],
+ dateStr: '2020-01-01T00:00:00.000Z',
+};
+
+export const invalidData = {
+ password: '___',
+ email: '',
+ birthYear: 'birthYear',
+ like: [{ id: 'z' }],
+};
+
+export const fields: Record = {
+ username: {
+ ref: { name: 'username' },
+ name: 'username',
+ },
+ password: {
+ ref: { name: 'password' },
+ name: 'password',
+ },
+ email: {
+ ref: { name: 'email' },
+ name: 'email',
+ },
+ birthday: {
+ ref: { name: 'birthday' },
+ name: 'birthday',
+ },
+};
diff --git a/vine/src/__tests__/__snapshots__/vine.ts.snap b/vine/src/__tests__/__snapshots__/vine.ts.snap
new file mode 100644
index 00000000..b139736b
--- /dev/null
+++ b/vine/src/__tests__/__snapshots__/vine.ts.snap
@@ -0,0 +1,180 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`vineResolver > should return a single error from vineResolver when validation fails 1`] = `
+{
+ "errors": {
+ "accessToken": {
+ "message": "Invalid value provided for accessToken field",
+ "ref": undefined,
+ "type": "unionOfTypes",
+ },
+ "birthYear": {
+ "message": "The birthYear field must be a number",
+ "ref": undefined,
+ "type": "number",
+ },
+ "dateStr": {
+ "message": "The dateStr field must be defined",
+ "ref": undefined,
+ "type": "required",
+ },
+ "email": {
+ "message": "The email field must be a valid email address",
+ "ref": {
+ "name": "email",
+ },
+ "type": "email",
+ },
+ "enabled": {
+ "message": "The enabled field must be defined",
+ "ref": undefined,
+ "type": "required",
+ },
+ "like": [
+ {
+ "id": {
+ "message": "The id field must be a number",
+ "ref": undefined,
+ "type": "number",
+ },
+ "name": {
+ "message": "The name field must be defined",
+ "ref": undefined,
+ "type": "required",
+ },
+ },
+ ],
+ "password": {
+ "message": "The password field format is invalid",
+ "ref": {
+ "name": "password",
+ },
+ "type": "regex",
+ },
+ "repeatPassword": {
+ "message": "The repeatPassword field must be defined",
+ "ref": undefined,
+ "type": "required",
+ },
+ "tags": {
+ "message": "The tags field must be defined",
+ "ref": undefined,
+ "type": "required",
+ },
+ "username": {
+ "message": "The username field must be defined",
+ "ref": {
+ "name": "username",
+ },
+ "type": "required",
+ },
+ },
+ "values": {},
+}
+`;
+
+exports[`vineResolver > should return all the errors from vineResolver when validation fails with \`validateAllFieldCriteria\` set to true 1`] = `
+{
+ "errors": {
+ "accessToken": {
+ "message": "Invalid value provided for accessToken field",
+ "ref": undefined,
+ "type": "unionOfTypes",
+ "types": {
+ "unionOfTypes": "Invalid value provided for accessToken field",
+ },
+ },
+ "birthYear": {
+ "message": "The birthYear field must be a number",
+ "ref": undefined,
+ "type": "number",
+ "types": {
+ "number": "The birthYear field must be a number",
+ },
+ },
+ "dateStr": {
+ "message": "The dateStr field must be defined",
+ "ref": undefined,
+ "type": "required",
+ "types": {
+ "required": "The dateStr field must be defined",
+ },
+ },
+ "email": {
+ "message": "The email field must be a valid email address",
+ "ref": {
+ "name": "email",
+ },
+ "type": "email",
+ "types": {
+ "email": "The email field must be a valid email address",
+ },
+ },
+ "enabled": {
+ "message": "The enabled field must be defined",
+ "ref": undefined,
+ "type": "required",
+ "types": {
+ "required": "The enabled field must be defined",
+ },
+ },
+ "like": [
+ {
+ "id": {
+ "message": "The id field must be a number",
+ "ref": undefined,
+ "type": "number",
+ "types": {
+ "number": "The id field must be a number",
+ },
+ },
+ "name": {
+ "message": "The name field must be defined",
+ "ref": undefined,
+ "type": "required",
+ "types": {
+ "required": "The name field must be defined",
+ },
+ },
+ },
+ ],
+ "password": {
+ "message": "The password field format is invalid",
+ "ref": {
+ "name": "password",
+ },
+ "type": "regex",
+ "types": {
+ "regex": "The password field format is invalid",
+ },
+ },
+ "repeatPassword": {
+ "message": "The repeatPassword field must be defined",
+ "ref": undefined,
+ "type": "required",
+ "types": {
+ "required": "The repeatPassword field must be defined",
+ },
+ },
+ "tags": {
+ "message": "The tags field must be defined",
+ "ref": undefined,
+ "type": "required",
+ "types": {
+ "required": "The tags field must be defined",
+ },
+ },
+ "username": {
+ "message": "The username field must be defined",
+ "ref": {
+ "name": "username",
+ },
+ "type": "required",
+ "types": {
+ "required": "The username field must be defined",
+ },
+ },
+ },
+ "values": {},
+}
+`;
diff --git a/vine/src/__tests__/vine.ts b/vine/src/__tests__/vine.ts
new file mode 100644
index 00000000..a95f7dd3
--- /dev/null
+++ b/vine/src/__tests__/vine.ts
@@ -0,0 +1,53 @@
+import { vineResolver } from '..';
+import { schema, validData, fields, invalidData } from './__fixtures__/data';
+
+const shouldUseNativeValidation = false;
+
+describe('vineResolver', () => {
+ it('should return values from vineResolver when validation pass', async () => {
+ const schemaSpy = vi.spyOn(schema, 'validate');
+
+ const result = await vineResolver(schema)(validData, undefined, {
+ fields,
+ shouldUseNativeValidation,
+ });
+
+ expect(schemaSpy).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({ errors: {}, values: validData });
+ });
+
+ it('should return a single error from vineResolver when validation fails', async () => {
+ const result = await vineResolver(schema)(invalidData, undefined, {
+ fields,
+ shouldUseNativeValidation,
+ });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should return all the errors from vineResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
+ const result = await vineResolver(schema)(invalidData, undefined, {
+ fields,
+ criteriaMode: 'all',
+ shouldUseNativeValidation,
+ });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should return values from vineResolver when validation pass & raw=true', async () => {
+ const schemaSpy = vi.spyOn(schema, 'validate');
+
+ const result = await vineResolver(schema, undefined, { raw: true })(
+ validData,
+ undefined,
+ {
+ fields,
+ shouldUseNativeValidation,
+ },
+ );
+
+ expect(schemaSpy).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({ errors: {}, values: validData });
+ });
+});
diff --git a/vine/src/index.ts b/vine/src/index.ts
new file mode 100644
index 00000000..6266c98e
--- /dev/null
+++ b/vine/src/index.ts
@@ -0,0 +1,2 @@
+export * from './vine';
+export * from './types';
diff --git a/vine/src/types.ts b/vine/src/types.ts
new file mode 100644
index 00000000..e7d50bdd
--- /dev/null
+++ b/vine/src/types.ts
@@ -0,0 +1,13 @@
+import { FieldValues, ResolverResult, ResolverOptions } from 'react-hook-form';
+import { VineValidator } from '@vinejs/vine';
+import { ValidationOptions } from '@vinejs/vine/build/src/types';
+
+export type Resolver = >(
+ schema: T,
+ schemaOptions?: ValidationOptions,
+ resolverOptions?: { raw?: boolean },
+) => (
+ values: TFieldValues,
+ context: TContext | undefined,
+ options: ResolverOptions,
+) => Promise>;
diff --git a/vine/src/vine.ts b/vine/src/vine.ts
new file mode 100644
index 00000000..28f9afa9
--- /dev/null
+++ b/vine/src/vine.ts
@@ -0,0 +1,66 @@
+import { appendErrors, FieldError, FieldErrors } from 'react-hook-form';
+import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
+import { SimpleErrorReporter, errors } from '@vinejs/vine';
+import type { Resolver } from './types';
+
+const parseErrorSchema = (
+ vineErrors: SimpleErrorReporter['errors'],
+ validateAllFieldCriteria: boolean,
+) => {
+ const schemaErrors: Record = {};
+
+ for (const error of vineErrors) {
+ const { field, rule, message } = error;
+ const path = field;
+
+ if (!(path in schemaErrors)) {
+ schemaErrors[path] = { message, type: rule };
+ }
+
+ if (validateAllFieldCriteria) {
+ const { types } = schemaErrors[path];
+ const messages = types && types[rule];
+
+ schemaErrors[path] = appendErrors(
+ path,
+ validateAllFieldCriteria,
+ schemaErrors,
+ rule,
+ messages ? [...(messages as string[]), message] : message,
+ ) as FieldError;
+ }
+ }
+
+ return schemaErrors;
+};
+
+export const vineResolver: Resolver =
+ (schema, schemaOptions, resolverOptions = {}) =>
+ async (values, _, options) => {
+ try {
+ const data = await schema.validate(values, schemaOptions);
+
+ options.shouldUseNativeValidation && validateFieldsNatively({}, options);
+
+ return {
+ errors: {} as FieldErrors,
+ values: resolverOptions.raw ? values : data,
+ };
+ } catch (error: any) {
+ if (error instanceof errors.E_VALIDATION_ERROR) {
+ return {
+ values: {},
+ errors: toNestErrors(
+ parseErrorSchema(
+ error.messages,
+ !options.shouldUseNativeValidation &&
+ options.criteriaMode === 'all',
+ ),
+ options,
+ ),
+ };
+ }
+
+ throw error;
+ }
+ };