@beignet/react-hook-form 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @beignet/react-hook-form
2
2
 
3
+ ## 0.0.5
4
+
5
+ ## 0.0.4
6
+
7
+ ### Patch Changes
8
+
9
+ - 8bcb31f: Mark package READMEs with Beignet's experimental alpha status and 0.0.x stability expectations.
10
+ - c1a834d: Generate an app-owned client error helper and align form error handling docs with
11
+ the canonical React Query and React Hook Form mutation path.
12
+ - d137044: Declare `@beignet/core` as a peer dependency with a lockstep version range in
13
+ every integration and provider package instead of a regular `"*"` dependency.
14
+ Installs now always resolve a single shared copy of core, so `instanceof`
15
+ checks such as `isContractError` and upload error identity keep working, and
16
+ mixed Beignet versions fail loudly at install time instead of at runtime.
17
+
18
+ If your package manager does not install peer dependencies automatically, add
19
+ `@beignet/core` to your app alongside these packages. `@beignet/nuqs` now also
20
+ declares `@beignet/react-query` as a peer dependency, and
21
+ `@beignet/provider-storage-s3` now expects you to install
22
+ `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` yourself, matching
23
+ how other providers treat their SDKs.
24
+
25
+ - 1a79090: Emit Node-compatible ESM: all relative imports in published packages now carry explicit .js extensions, fixing ERR_MODULE_NOT_FOUND when running the CLI or importing package dist files under plain Node.
26
+ - 9d1bf0b: Clarify React Hook Form submit-error handling and align generated form examples with Beignet client error semantics.
27
+ - 89390fe: Type form values with the contract body schema's input/output split. Live field
28
+ values (`register`, `watch`, `setValue`, `getValues`, `defaultValues`) now use
29
+ the schema input, and `handleSubmit` callbacks receive the parsed schema
30
+ output, so coercing, transforming, and defaulting body schemas type correctly
31
+ end to end.
32
+ - d6ad8bb: Standardize generated client helpers around `client/index.ts`, add a typed
33
+ React Query invalidation helper, and align package docs with the canonical app
34
+ client entrypoint.
35
+ - 8063d38: Rename the contract front door to `defineContract`/`defineContractGroup`, rename operational commands to tasks (`@beignet/core/tasks`, `defineTasks`, `runTask`, `beignet task run`, `beignet make task`, `server/tasks.ts`, `features/<feature>/tasks/`, `paths.tasks`), and standardize context binding: context-free declarations stay top-level (`defineEvent`), while context-bound definitions come from per-capability factories (`createListeners`, `createJobs`, `createSchedules`, `createNotifications`, `createTasks`) called once in `lib/`. Top-level context-generic `defineListener`, `defineJob`, `defineSchedule`, and `defineNotification` are removed.
36
+ - 192c6ad: Typed clients now attach idempotency keys automatically from contract metadata (override with `idempotencyKey`), React Query mutations keep the key stable across retry attempts, and the shared client error helpers `contractErrorMessage` and `rootFormError` are now exported by @beignet/core/client and @beignet/react-hook-form.
37
+
3
38
  ## 0.0.3
4
39
 
5
40
  ### Patch Changes
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > React Hook Form integration for Beignet
4
4
 
5
+ > [!CAUTION]
6
+ > Beignet is experimental alpha software. The `0.0.x` package line is for early
7
+ > evaluation, and APIs may change between releases while the framework settles.
8
+
5
9
  This package provides automatic form validation using your contract's body schema. Works with any [Standard Schema](https://github.com/standard-schema/standard-schema) library (Zod, Valibot, ArkType, etc.).
6
10
 
7
11
  ## Installation
@@ -10,7 +14,6 @@ This package provides automatic form validation using your contract's body schem
10
14
  npm install @beignet/react-hook-form @beignet/core react-hook-form @hookform/resolvers react
11
15
  ```
12
16
 
13
-
14
17
  ## TypeScript requirements
15
18
 
16
19
  This package requires TypeScript 5.0 or higher for proper type inference.
@@ -35,7 +38,7 @@ function CreateTodoForm() {
35
38
  });
36
39
 
37
40
  const onSubmit = form.handleSubmit((values) => {
38
- // values is typed as: { title: string; completed?: boolean }
41
+ // values is the parsed schema output: { title: string; completed?: boolean }
39
42
  console.log("Creating todo:", values);
40
43
  });
41
44
 
@@ -65,9 +68,9 @@ function CreateTodoForm() {
65
68
  ### With React Query mutation
66
69
 
67
70
  ```tsx
68
- import { createReactHookForm } from "@beignet/react-hook-form";
71
+ import { createReactHookForm, rootFormError } from "@beignet/react-hook-form";
69
72
  import { useMutation } from "@tanstack/react-query";
70
- import { rq } from "@/client/rq";
73
+ import { rq } from "@/client";
71
74
  import { createTodo } from "@/features/todos/contracts";
72
75
 
73
76
  const rhf = createReactHookForm();
@@ -79,10 +82,16 @@ function CreateTodoForm() {
79
82
  });
80
83
 
81
84
  const mutation = useMutation(
82
- rq(createTodo).mutationOptions()
85
+ rq(createTodo).mutationOptions({
86
+ onSuccess: () => form.reset(),
87
+ onError: (error) => {
88
+ form.setError("root", rootFormError(error, "Could not create the todo."));
89
+ },
90
+ }),
83
91
  );
84
92
 
85
93
  const onSubmit = form.handleSubmit((values) => {
94
+ form.clearErrors("root");
86
95
  mutation.mutate({ body: values });
87
96
  });
88
97
 
@@ -92,19 +101,25 @@ function CreateTodoForm() {
92
101
  {form.formState.errors.title && (
93
102
  <p>{form.formState.errors.title.message}</p>
94
103
  )}
104
+ {form.formState.errors.root && (
105
+ <p>{form.formState.errors.root.message}</p>
106
+ )}
95
107
 
96
108
  <button type="submit" disabled={mutation.isPending}>
97
109
  {mutation.isPending ? "Creating..." : "Create"}
98
110
  </button>
99
-
100
- {mutation.isError && (
101
- <p className="error">{mutation.error.message}</p>
102
- )}
103
111
  </form>
104
112
  );
105
113
  }
106
114
  ```
107
115
 
116
+ React Hook Form only owns request body fields. Pass path params, query params,
117
+ headers, and auth-derived values to the endpoint call or mutation variables.
118
+ Submit failures come back as Beignet client errors, so map route-owned catalog
119
+ errors, framework validation errors, network failures, and contract drift
120
+ through `rootFormError(...)`, then set `form.setError("root", ...)` or a
121
+ specific field when your server response identifies one.
122
+
108
123
  ### Disabling validation
109
124
 
110
125
  If you need to disable the schema resolver (e.g., for partial form handling):
@@ -164,9 +179,33 @@ const form = adapter.useForm({
164
179
  });
165
180
  ```
166
181
 
182
+ ### `rootFormError(error, fallback, overrides?)`
183
+
184
+ Maps a failed endpoint call or mutation error to the
185
+ `form.setError("root", ...)` shape. Wraps `contractErrorMessage` from
186
+ `@beignet/core/client`: non-contract errors return the fallback copy,
187
+ client-side input validation failures return a generic "check the highlighted
188
+ fields" message, and catalog codes can override copy per form.
189
+
190
+ ```ts
191
+ form.setError(
192
+ "root",
193
+ rootFormError(error, "Could not update profile.", {
194
+ HANDLE_UNAVAILABLE: "That handle is already taken.",
195
+ }),
196
+ );
197
+ ```
198
+
167
199
  ## Type inference
168
200
 
169
- Form values are automatically typed based on the contract's body schema:
201
+ Form types come from the contract's body schema and follow React Hook Form's
202
+ input/output split:
203
+
204
+ - Live field values — `register`, `watch`, `setValue`, `getValues`, and
205
+ `defaultValues` — use the schema **input**: what the user edits before
206
+ validation runs.
207
+ - `handleSubmit` callbacks receive the schema **output**: the parsed values
208
+ after coercion, transforms, and defaults run.
170
209
 
171
210
  ```ts
172
211
  // Contract definition
@@ -187,6 +226,45 @@ form.register("description"); // ✓ Valid
187
226
  form.register("invalid"); // ✗ Type error
188
227
  ```
189
228
 
229
+ For plain schemas like the one above, input and output are identical. For
230
+ coercing or transforming schemas they differ:
231
+
232
+ ```ts
233
+ const createPayment = payments
234
+ .post("/api/payments")
235
+ .body(z.object({
236
+ amount: z.string().transform(Number),
237
+ note: z.string().optional(),
238
+ }))
239
+ .responses({ 201: PaymentSchema });
240
+
241
+ const form = rhf(createPayment).useForm({
242
+ defaultValues: { amount: "" }, // input: string
243
+ });
244
+
245
+ form.watch("amount"); // string (input)
246
+
247
+ form.handleSubmit((values) => {
248
+ values.amount; // number (output)
249
+ });
250
+ ```
251
+
252
+ ### Submitting transforming schemas
253
+
254
+ The typed client posts the schema input — the server validates and transforms
255
+ the body when it receives the request. Parsed output from `handleSubmit` is
256
+ still valid input for plain, defaulted, and coerced schemas, so
257
+ `mutation.mutate({ body: values })` keeps working for those. When a transform
258
+ changes a field's type, the parsed output no longer matches the contract body
259
+ and TypeScript rejects it. Send the raw field values instead — validation has
260
+ already passed by the time the submit handler runs:
261
+
262
+ ```tsx
263
+ const onSubmit = form.handleSubmit(() => {
264
+ mutation.mutate({ body: form.getValues() });
265
+ });
266
+ ```
267
+
190
268
  ## Standard Schema support
191
269
 
192
270
  This package uses the `@hookform/resolvers/standard-schema` resolver, which works with any Standard Schema compatible library:
@@ -197,11 +275,11 @@ This package uses the `@hookform/resolvers/standard-schema` resolver, which work
197
275
 
198
276
  ## Validation behavior
199
277
 
200
- The resolver validates:
201
-
202
- 1. **On blur** - When a field loses focus
203
- 2. **On change** - After first submission attempt
204
- 3. **On submit** - Before calling your submit handler
278
+ React Hook Form controls when the generated resolver runs. With the default
279
+ React Hook Form settings, the resolver validates before submit and then
280
+ revalidates changed fields after a failed submit. Pass normal React Hook Form
281
+ options such as `mode: "onBlur"` or `reValidateMode: "onChange"` when a form
282
+ needs different timing.
205
283
 
206
284
  Validation errors are available via `form.formState.errors`:
207
285
 
@@ -218,7 +296,8 @@ Validation errors are available via `form.formState.errors`:
218
296
  ```tsx
219
297
  import { createReactHookForm } from "@beignet/react-hook-form";
220
298
  import { useMutation } from "@tanstack/react-query";
221
- import { rq } from "@/client/rq";
299
+ import { rq } from "@/client";
300
+ import { rootFormError } from "@/client/errors";
222
301
  import { updateProfile } from "@/features/profile/contracts";
223
302
 
224
303
  const rhf = createReactHookForm();
@@ -238,10 +317,14 @@ function ProfileForm({ profile }) {
238
317
  onSuccess: () => {
239
318
  toast.success("Profile updated!");
240
319
  },
320
+ onError: (error) => {
321
+ form.setError("root", rootFormError(error, "Could not update the profile."));
322
+ },
241
323
  })
242
324
  );
243
325
 
244
326
  const onSubmit = form.handleSubmit((values) => {
327
+ form.clearErrors("root");
245
328
  mutation.mutate({ body: values });
246
329
  });
247
330
 
@@ -270,6 +353,8 @@ function ProfileForm({ profile }) {
270
353
  <button type="submit" disabled={!isDirty || isSubmitting}>
271
354
  {isSubmitting ? "Saving..." : "Save Changes"}
272
355
  </button>
356
+
357
+ {errors.root && <span className="error">{errors.root.message}</span>}
273
358
  </form>
274
359
  );
275
360
  }
@@ -277,9 +362,9 @@ function ProfileForm({ profile }) {
277
362
 
278
363
  ## Related packages
279
364
 
280
- - [`@beignet/core/contracts`](https://beignet.dev/contracts) - Core contract definitions
281
- - [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
282
- - [`@beignet/core/client`](https://beignet.dev/client) - HTTP client
365
+ - [`@beignet/core/contracts`](https://beignetjs.com/contracts) - Core contract definitions
366
+ - [`@beignet/react-query`](https://beignetjs.com/react-query) - TanStack Query integration
367
+ - [`@beignet/core/client`](https://beignetjs.com/client) - HTTP client
283
368
 
284
369
  ## License
285
370
 
package/dist/index.d.ts CHANGED
@@ -1,19 +1,34 @@
1
- import { type ContractLike, type HttpContractConfig, type InferOutput, type ResolveContract, type StandardSchemaV1 } from "@beignet/core/contracts";
1
+ import { type ErrorMessageOverrides } from "@beignet/core/client";
2
+ import { type ContractLike, type HttpContractConfig, type InferInput, type InferOutput, type ResolveContract, type StandardSchemaV1 } from "@beignet/core/contracts";
2
3
  import { type FieldValues, type UseFormProps, type UseFormReturn } from "react-hook-form";
3
4
  /**
4
- * Infer React Hook Form values from a contract body schema.
5
+ * Constrain a schema input type to React Hook Form field values without
6
+ * widening field name inference for object inputs.
5
7
  */
6
- type InferBody<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? InferOutput<TContract["body"]> & FieldValues : FieldValues;
7
- type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
8
+ type AsFieldValues<T> = T extends FieldValues ? T : T & FieldValues;
9
+ /**
10
+ * Live form field values inferred from the contract body schema input.
11
+ *
12
+ * React Hook Form field values (`register`, `watch`, `setValue`,
13
+ * `defaultValues`) hold pre-validation input, so coercing or transforming body
14
+ * schemas type fields as what the user edits, not what the schema produces.
15
+ */
16
+ type FormInput<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? AsFieldValues<InferInput<TContract["body"]>> : FieldValues;
17
+ /**
18
+ * Validated submit values inferred from the contract body schema output.
19
+ *
20
+ * `handleSubmit` callbacks receive the resolver-parsed output.
21
+ */
22
+ type FormOutput<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? InferOutput<TContract["body"]> : FieldValues;
8
23
  /**
9
24
  * React Hook Form options accepted by the Beignet adapter.
10
25
  */
11
- type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<FormValues<TContract>>, "resolver"> & {
26
+ type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>, "resolver"> & {
12
27
  /**
13
28
  * Optional override for the resolver. If supplied, this is used instead of
14
29
  * the default contract-based resolver.
15
30
  */
16
- resolver?: UseFormProps<FormValues<TContract>>["resolver"];
31
+ resolver?: UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>["resolver"];
17
32
  /**
18
33
  * Enables or disables automatic resolver generation from the contract body schema.
19
34
  * Defaults to true.
@@ -27,11 +42,11 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
27
42
  /**
28
43
  * Generate `UseFormProps` for any React Hook Form usage.
29
44
  */
30
- formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<FormValues<TContract>>;
45
+ formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>;
31
46
  /**
32
47
  * Convenience wrapper around `useForm(formOptions(props))`.
33
48
  */
34
- useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<FormValues<TContract>>;
49
+ useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<FormInput<TContract>, unknown, FormOutput<TContract>>;
35
50
  };
36
51
  /**
37
52
  * Create a React Hook Form adapter factory.
@@ -42,5 +57,28 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
42
57
  * disable the generated resolver through `formOptions(...)` or `useForm(...)`.
43
58
  */
44
59
  export declare function createReactHookForm(): <TContractLike extends ContractLike>(contract: TContractLike) => ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
60
+ /**
61
+ * Root-level form error for `form.setError("root", ...)`.
62
+ */
63
+ export type RootFormError = {
64
+ type: "server";
65
+ message: string;
66
+ };
67
+ /**
68
+ * Map a failed endpoint call or mutation error to a root-level form error.
69
+ *
70
+ * Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
71
+ * errors get the fallback copy and catalog codes can be overridden per form:
72
+ *
73
+ * ```ts
74
+ * form.setError(
75
+ * "root",
76
+ * rootFormError(error, "Could not update profile.", {
77
+ * HANDLE_UNAVAILABLE: "That handle is already taken.",
78
+ * }),
79
+ * );
80
+ * ```
81
+ */
82
+ export declare function rootFormError(error: unknown, fallback: string, overrides?: ErrorMessageOverrides): RootFormError;
45
83
  export {};
46
84
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,KAAK,SAAS,CAAC,SAAS,SAAS,kBAAkB,IACjD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,GAC5C,WAAW,CAAC;AAElB,KAAK,UAAU,CAAC,SAAS,SAAS,kBAAkB,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC;AAE7E;;GAEG;AACH,KAAK,cAAc,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CAC9D,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EACnC,UAAU,CACX,GAAG;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAE3D;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,CAAC,SAAS,SAAS,kBAAkB,IAC3E;IACE;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEzC;;OAEG;IACH,OAAO,EAAE,CACP,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;CAC3C,CAAC;AAqDJ;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,KACb,aAAa,SAAS,YAAY,EACpD,UAAU,aAAa,KACtB,4BAA4B,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAGhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB;;;GAGG;AACH,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,SAAS,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC;AAEpE;;;;;;GAMG;AACH,KAAK,SAAS,CAAC,SAAS,SAAS,kBAAkB,IACjD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,GAC5C,WAAW,CAAC;AAElB;;;;GAIG;AACH,KAAK,UAAU,CAAC,SAAS,SAAS,kBAAkB,IAClD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAC9B,WAAW,CAAC;AAElB;;GAEG;AACH,KAAK,cAAc,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CAC9D,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,EAClE,UAAU,CACX,GAAG;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CACrB,SAAS,CAAC,SAAS,CAAC,EACpB,OAAO,EACP,UAAU,CAAC,SAAS,CAAC,CACtB,CAAC,UAAU,CAAC,CAAC;IAEd;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,CAAC,SAAS,SAAS,kBAAkB,IAC3E;IACE;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAExE;;OAEG;IACH,OAAO,EAAE,CACP,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;CAC1E,CAAC;AAsDJ;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,KACb,aAAa,SAAS,YAAY,EACpD,UAAU,aAAa,KACtB,4BAA4B,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAGhE;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,qBAAqB,GAChC,aAAa,CAKf"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { contractErrorMessage, } from "@beignet/core/client";
1
2
  import { resolveContract, } from "@beignet/core/contracts";
2
3
  import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
3
4
  import { useForm as rhfUseForm, } from "react-hook-form";
@@ -39,4 +40,25 @@ export function createReactHookForm() {
39
40
  return createReactHookFormAdapter(contract);
40
41
  };
41
42
  }
43
+ /**
44
+ * Map a failed endpoint call or mutation error to a root-level form error.
45
+ *
46
+ * Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
47
+ * errors get the fallback copy and catalog codes can be overridden per form:
48
+ *
49
+ * ```ts
50
+ * form.setError(
51
+ * "root",
52
+ * rootFormError(error, "Could not update profile.", {
53
+ * HANDLE_UNAVAILABLE: "That handle is already taken.",
54
+ * }),
55
+ * );
56
+ * ```
57
+ */
58
+ export function rootFormError(error, fallback, overrides) {
59
+ return {
60
+ type: "server",
61
+ message: contractErrorMessage(error, fallback, overrides),
62
+ };
63
+ }
42
64
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,eAAe,GAEhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAEL,OAAO,IAAI,UAAU,GAGtB,MAAM,iBAAiB,CAAC;AAoDzB,SAAS,0BAA0B,CACjC,QAAuB;IAEvB,MAAM,gBAAgB,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,OAAO,gBAAgB,CAAC,IAAI,kCAAkC;YAC5D,iFAAiF;YACjF,6EAA6E,CAChF,CAAC;IACJ,CAAC;IAID,SAAS,WAAW,CAClB,KAAsD;QAEtD,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAGvB,CAAC;QAEd,MAAM,EACJ,eAAe,GAAG,IAAI,EACtB,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,IAAI,EACR,GAAG,KAAK,IAAI,EAAE,CAAC;QAEhB,MAAM,QAAQ,GACZ,gBAAgB;YAChB,CAAC,UAAU,IAAI,eAAe;gBAC5B,CAAC,CAAC,sBAAsB,CAAC,UAAU,CAAC;gBACpC,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,OAAO;YACL,GAAI,IAA6B;YACjC,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,KAAsD;QAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,UAAU,CAAS,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,SAAS,GAAG,CACjB,QAAuB;QAEvB,OAAO,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAML,eAAe,GAEhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAEL,OAAO,IAAI,UAAU,GAGtB,MAAM,iBAAiB,CAAC;AA0EzB,SAAS,0BAA0B,CACjC,QAAuB;IAEvB,MAAM,gBAAgB,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,OAAO,gBAAgB,CAAC,IAAI,kCAAkC;YAC5D,iFAAiF;YACjF,6EAA6E,CAChF,CAAC;IACJ,CAAC;IAKD,SAAS,WAAW,CAClB,KAAsD;QAEtD,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAGvB,CAAC;QAEd,MAAM,EACJ,eAAe,GAAG,IAAI,EACtB,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,IAAI,EACR,GAAG,KAAK,IAAI,EAAE,CAAC;QAEhB,MAAM,QAAQ,GACZ,gBAAgB;YAChB,CAAC,UAAU,IAAI,eAAe;gBAC5B,CAAC,CAAC,sBAAsB,CAAyB,UAAU,CAAC;gBAC5D,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,OAAO;YACL,GAAI,IAA6C;YACjD,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,KAAsD;QAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,UAAU,CAAyB,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,SAAS,GAAG,CACjB,QAAuB;QAEvB,OAAO,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC;AAUD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAc,EACd,QAAgB,EAChB,SAAiC;IAEjC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC;KAC1D,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/react-hook-form",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "React Hook Form integration for Beignet with Standard Schema support",
6
6
  "main": "./dist/index.js",
@@ -24,8 +24,9 @@
24
24
  "build": "tsc",
25
25
  "dev": "tsc --watch",
26
26
  "clean": "rm -rf dist coverage .turbo",
27
- "test": "bun test",
27
+ "test": "bun test && bun run typecheck:types",
28
28
  "test:coverage": "bun test --coverage",
29
+ "typecheck:types": "tsc -p tsconfig.type-tests.json",
29
30
  "lint": "biome check ."
30
31
  },
31
32
  "keywords": [
@@ -52,13 +53,11 @@
52
53
  "node": ">=18.0.0"
53
54
  },
54
55
  "peerDependencies": {
56
+ "@beignet/core": ">=0.0.3 <1.0.0",
55
57
  "@hookform/resolvers": "^5.0.0",
56
58
  "react": "^18.0.0 || ^19.0.0",
57
59
  "react-hook-form": "^7.0.0"
58
60
  },
59
- "dependencies": {
60
- "@beignet/core": "*"
61
- },
62
61
  "devDependencies": {
63
62
  "@hookform/resolvers": "^5.2.2",
64
63
  "@testing-library/dom": "^9.3.4",
package/src/index.ts CHANGED
@@ -1,6 +1,11 @@
1
+ import {
2
+ contractErrorMessage,
3
+ type ErrorMessageOverrides,
4
+ } from "@beignet/core/client";
1
5
  import {
2
6
  type ContractLike,
3
7
  type HttpContractConfig,
8
+ type InferInput,
4
9
  type InferOutput,
5
10
  type ResolveContract,
6
11
  resolveContract,
@@ -15,27 +20,49 @@ import {
15
20
  } from "react-hook-form";
16
21
 
17
22
  /**
18
- * Infer React Hook Form values from a contract body schema.
23
+ * Constrain a schema input type to React Hook Form field values without
24
+ * widening field name inference for object inputs.
25
+ */
26
+ type AsFieldValues<T> = T extends FieldValues ? T : T & FieldValues;
27
+
28
+ /**
29
+ * Live form field values inferred from the contract body schema input.
30
+ *
31
+ * React Hook Form field values (`register`, `watch`, `setValue`,
32
+ * `defaultValues`) hold pre-validation input, so coercing or transforming body
33
+ * schemas type fields as what the user edits, not what the schema produces.
19
34
  */
20
- type InferBody<TContract extends HttpContractConfig> =
35
+ type FormInput<TContract extends HttpContractConfig> =
21
36
  TContract["body"] extends StandardSchemaV1
22
- ? InferOutput<TContract["body"]> & FieldValues
37
+ ? AsFieldValues<InferInput<TContract["body"]>>
23
38
  : FieldValues;
24
39
 
25
- type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
40
+ /**
41
+ * Validated submit values inferred from the contract body schema output.
42
+ *
43
+ * `handleSubmit` callbacks receive the resolver-parsed output.
44
+ */
45
+ type FormOutput<TContract extends HttpContractConfig> =
46
+ TContract["body"] extends StandardSchemaV1
47
+ ? InferOutput<TContract["body"]>
48
+ : FieldValues;
26
49
 
27
50
  /**
28
51
  * React Hook Form options accepted by the Beignet adapter.
29
52
  */
30
53
  type RhfFormOptions<TContract extends HttpContractConfig> = Omit<
31
- UseFormProps<FormValues<TContract>>,
54
+ UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>,
32
55
  "resolver"
33
56
  > & {
34
57
  /**
35
58
  * Optional override for the resolver. If supplied, this is used instead of
36
59
  * the default contract-based resolver.
37
60
  */
38
- resolver?: UseFormProps<FormValues<TContract>>["resolver"];
61
+ resolver?: UseFormProps<
62
+ FormInput<TContract>,
63
+ unknown,
64
+ FormOutput<TContract>
65
+ >["resolver"];
39
66
 
40
67
  /**
41
68
  * Enables or disables automatic resolver generation from the contract body schema.
@@ -54,14 +81,14 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
54
81
  */
55
82
  formOptions: (
56
83
  props?: RhfFormOptions<TContract>,
57
- ) => UseFormProps<FormValues<TContract>>;
84
+ ) => UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>;
58
85
 
59
86
  /**
60
87
  * Convenience wrapper around `useForm(formOptions(props))`.
61
88
  */
62
89
  useForm: (
63
90
  props?: RhfFormOptions<TContract>,
64
- ) => UseFormReturn<FormValues<TContract>>;
91
+ ) => UseFormReturn<FormInput<TContract>, unknown, FormOutput<TContract>>;
65
92
  };
66
93
 
67
94
  function createReactHookFormAdapter<TContractLike extends ContractLike>(
@@ -77,13 +104,14 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
77
104
  );
78
105
  }
79
106
 
80
- type Values = FormValues<ResolveContract<TContractLike>>;
107
+ type Input = FormInput<ResolveContract<TContractLike>>;
108
+ type Output = FormOutput<ResolveContract<TContractLike>>;
81
109
 
82
110
  function formOptions(
83
111
  props?: RhfFormOptions<ResolveContract<TContractLike>>,
84
- ): UseFormProps<Values> {
112
+ ): UseFormProps<Input, unknown, Output> {
85
113
  const bodySchema = resolvedContract.body as
86
- | StandardSchemaV1<FieldValues>
114
+ | StandardSchemaV1<Input, Output>
87
115
  | null
88
116
  | undefined;
89
117
 
@@ -96,20 +124,20 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
96
124
  const resolver =
97
125
  resolverOverride ??
98
126
  (bodySchema && resolverEnabled
99
- ? standardSchemaResolver(bodySchema)
127
+ ? standardSchemaResolver<Input, unknown, Output>(bodySchema)
100
128
  : undefined);
101
129
 
102
130
  return {
103
- ...(rest as UseFormProps<Values>),
131
+ ...(rest as UseFormProps<Input, unknown, Output>),
104
132
  resolver,
105
133
  };
106
134
  }
107
135
 
108
136
  function useForm(
109
137
  props?: RhfFormOptions<ResolveContract<TContractLike>>,
110
- ): UseFormReturn<Values> {
138
+ ): UseFormReturn<Input, unknown, Output> {
111
139
  const options = formOptions(props);
112
- return rhfUseForm<Values>(options);
140
+ return rhfUseForm<Input, unknown, Output>(options);
113
141
  }
114
142
 
115
143
  return { formOptions, useForm };
@@ -130,3 +158,37 @@ export function createReactHookForm() {
130
158
  return createReactHookFormAdapter(contract);
131
159
  };
132
160
  }
161
+
162
+ /**
163
+ * Root-level form error for `form.setError("root", ...)`.
164
+ */
165
+ export type RootFormError = {
166
+ type: "server";
167
+ message: string;
168
+ };
169
+
170
+ /**
171
+ * Map a failed endpoint call or mutation error to a root-level form error.
172
+ *
173
+ * Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
174
+ * errors get the fallback copy and catalog codes can be overridden per form:
175
+ *
176
+ * ```ts
177
+ * form.setError(
178
+ * "root",
179
+ * rootFormError(error, "Could not update profile.", {
180
+ * HANDLE_UNAVAILABLE: "That handle is already taken.",
181
+ * }),
182
+ * );
183
+ * ```
184
+ */
185
+ export function rootFormError(
186
+ error: unknown,
187
+ fallback: string,
188
+ overrides?: ErrorMessageOverrides,
189
+ ): RootFormError {
190
+ return {
191
+ type: "server",
192
+ message: contractErrorMessage(error, fallback, overrides),
193
+ };
194
+ }