react-server-actions 1.0.3 → 1.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/README.md +312 -58
- package/dist/client/helpers.d.ts +28 -1
- package/dist/client/helpers.d.ts.map +1 -1
- package/dist/client/helpers.js +58 -9
- package/dist/client/helpers.js.map +1 -1
- package/dist/client/index.d.ts +6 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +35 -9
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
package/README.md
CHANGED
|
@@ -1,16 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# React Server Actions
|
|
2
2
|
|
|
3
|
-
A lightweight library for handling
|
|
3
|
+
A lightweight library for handling server actions in React and Next.js applications using server side zod validation and native html client side validation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Intro
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
React Server Actions represent a groundbreaking advancement in how we handle server-side operations in React applications. However, while the APIs provided by the React team are powerful, developers often face challenges in establishing consistent patterns for handling responses, validation, and error management. This is where this library comes in.
|
|
8
|
+
|
|
9
|
+
React Server Actions was born out of the need to address these gaps in the implementation of React's new server actions feature. Rather than replacing the native functionality, this library builds upon it by providing a structured approach to server actions. The core idea is to establish a well-defined shape for server action responses, making them predictable and easier to work with.
|
|
10
|
+
|
|
11
|
+
This is how we define a server action with the help of react-server-actions library.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
export const zodSchema = z.object({
|
|
15
|
+
name: z.string().min(1),
|
|
16
|
+
...
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
'use server';
|
|
8
22
|
|
|
9
|
-
|
|
23
|
+
// the action wrapper will take care of validate data and will wrap the response into a ActionResult type!
|
|
24
|
+
export const someAction = action(zodSchema, async (data) => {
|
|
25
|
+
// data is already validated and is typed
|
|
26
|
+
return {
|
|
27
|
+
something: 'whatever',
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
By providing this structure, the library enables developers to create more robust client-side components that can confidently interact with server actions. It combines the power of Zod for server-side validation with native HTML validation attributes, creating a seamless development experience while maintaining type safety throughout the application. The result is a more organized and maintainable codebase that follows best practices for handling server-client interactions.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
'use client'
|
|
36
|
+
export default function Form() {
|
|
37
|
+
const [state, action, pending] = useActionState(someAction, initialState());
|
|
38
|
+
|
|
39
|
+
console.log(state.formData?.name); // state will be always of type ActionResult
|
|
40
|
+
console.log(state.invalid?.name); // it will contain the data and eventually the validation problems
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Form state={state} action={action} schema={schema}>
|
|
44
|
+
<FormField
|
|
45
|
+
name="name"
|
|
46
|
+
render={(field) => (
|
|
47
|
+
<div>
|
|
48
|
+
<label htmlFor={field.input.id} className={field.invalid ? 'text-red-500' : ''}>Name</label>
|
|
49
|
+
<input {...field.input} type="text" />
|
|
50
|
+
{field.invalid && <p className="text-red-500">{field.invalid}</p>}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
</Form>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// The ActionResult type
|
|
61
|
+
export type ActionResult<Schema> = {
|
|
62
|
+
success: boolean;
|
|
63
|
+
formData: z.infer<Schema> | undefined; // Data submitted
|
|
64
|
+
successData: any; // Data returned from the user
|
|
65
|
+
invalid: [key in keyof Partial<z.TypeOf<Schema>>]: string[] | undefined; // Validation fields
|
|
66
|
+
error: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Features
|
|
10
71
|
|
|
11
|
-
|
|
12
|
-
- No additional server-side middleware required
|
|
13
|
-
- Type-safe action handling
|
|
72
|
+
### Server-Side Features
|
|
14
73
|
|
|
15
74
|
#### Structured Action Responses
|
|
16
75
|
|
|
@@ -33,7 +92,7 @@ A lightweight library for handling form actions in Next.js applications using na
|
|
|
33
92
|
|
|
34
93
|
#### Form State Management
|
|
35
94
|
|
|
36
|
-
-
|
|
95
|
+
- Form data persistence between submissions
|
|
37
96
|
- Ability to reset form data after successful submission
|
|
38
97
|
- State management utilities for handling loading and error states
|
|
39
98
|
|
|
@@ -41,56 +100,119 @@ A lightweight library for handling form actions in Next.js applications using na
|
|
|
41
100
|
|
|
42
101
|
#### Framework Agnostic
|
|
43
102
|
|
|
103
|
+
- Use React standard hook useActionState
|
|
44
104
|
- Works with any form management library of your choice
|
|
45
105
|
- Native support for React Hook Form, Formik, or plain HTML forms
|
|
46
106
|
- Zero client-side dependencies required
|
|
47
107
|
|
|
48
108
|
#### Native HTML Validation
|
|
49
109
|
|
|
50
|
-
- Automatic HTML5 validation attributes from Zod schemas
|
|
110
|
+
- Automatic HTML5 validation attributes inferred from Zod schemas
|
|
51
111
|
- Client-side validation before server submission
|
|
52
112
|
- Improved user experience with instant feedback
|
|
53
113
|
- Accessibility-friendly validation messages
|
|
54
114
|
|
|
55
115
|
## Installation
|
|
56
116
|
|
|
57
|
-
bash
|
|
58
|
-
npm install
|
|
117
|
+
```bash
|
|
118
|
+
npm install react-server-actions
|
|
119
|
+
```
|
|
59
120
|
|
|
60
121
|
## Basic Usage
|
|
61
122
|
|
|
62
123
|
### 1. Define your schema and action
|
|
63
124
|
|
|
64
|
-
|
|
125
|
+
Define your zod schema and create a server action. The server action must be wrapped into the `action` method to be sure that it will return a correct `ActionResult` type.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
65
128
|
import { z } from 'zod';
|
|
66
|
-
import { createAction } from 'next-native-actions';
|
|
67
129
|
const userSchema = z.object({
|
|
68
|
-
name: z.string().min(2),
|
|
69
|
-
email: z.string().email(),
|
|
70
|
-
age: z.number().min(18)
|
|
130
|
+
name: z.string().min(2),
|
|
131
|
+
email: z.string().email(),
|
|
132
|
+
age: z.number().min(18),
|
|
71
133
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { userSchema } from './schema';
|
|
138
|
+
import { action } from 'react-server-actions';
|
|
139
|
+
export const createUser = action(userSchema, async (data) => {
|
|
140
|
+
// Your server logic here
|
|
141
|
+
return { success: true, data };
|
|
75
142
|
});
|
|
143
|
+
```
|
|
76
144
|
|
|
77
145
|
### 2. Use in your form component
|
|
78
146
|
|
|
79
|
-
|
|
147
|
+
Connect the server action with the React hook `useStateAction`.
|
|
148
|
+
For having a correct initial data we provide a `initialState` method
|
|
149
|
+
After that you will have a typed `state` with all the informations about data submitted and validation state.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
'use client';
|
|
153
|
+
import { useActionState } from 'react';
|
|
154
|
+
import { createUser } from './action';
|
|
155
|
+
import { initialState } from 'react-server-actions';
|
|
156
|
+
export function UserForm() {
|
|
157
|
+
const [state, action, pending] = useActionState(createUser, initialData({
|
|
158
|
+
// can be an object that satisfies userSchema or undefined
|
|
159
|
+
...
|
|
160
|
+
}));
|
|
161
|
+
...
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 3. Define the form
|
|
166
|
+
|
|
167
|
+
You can now create the form with native `<form>` or with any other library, taking advantage of having a structured and typed `state`.
|
|
168
|
+
This library will, anyway, provide two useful component:
|
|
169
|
+
|
|
170
|
+
- `<Form>` component - it will provide the ability to reset the form on successful submission, define a `onSuccess` and `onError` callback
|
|
171
|
+
- `<FormField>` component - it will provide a render method, which will expose a `field` property with the correct informations to build your inputs. **It will also inject the HTML5 validation rule based on the zod schema**!
|
|
172
|
+
- Inside the `render` method of the `<FormField>` component you can use whatever ui library you want.
|
|
173
|
+
- The `field` property will have this structure
|
|
174
|
+
```typescript
|
|
175
|
+
{
|
|
176
|
+
invalid: string[] | undefined;
|
|
177
|
+
value: any;
|
|
178
|
+
input: {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
'aria-invalid': boolean;
|
|
182
|
+
autoComplete: 'on' | 'off' | undefined;
|
|
183
|
+
defaultValue?: string;
|
|
184
|
+
...html5ValidationRules (like required, min, etc...)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
80
190
|
'use client';
|
|
81
|
-
import {
|
|
191
|
+
import { useActionState } from 'react';
|
|
192
|
+
import { createUser } from './action'
|
|
82
193
|
export function UserForm() {
|
|
83
|
-
const
|
|
84
|
-
return (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
194
|
+
const [state, action, pending] = useActionState(createUser);
|
|
195
|
+
return (
|
|
196
|
+
<Form state={state} action={action} schema={userSchema}>
|
|
197
|
+
|
|
198
|
+
<FormField
|
|
199
|
+
name="name"
|
|
200
|
+
render={(field) => (
|
|
201
|
+
<div>
|
|
202
|
+
<label htmlFor={field.input.id} className={field.invalid ? 'text-red-500' : ''}>
|
|
203
|
+
Name
|
|
204
|
+
</label>
|
|
205
|
+
<input {...field.input} type="text" />
|
|
206
|
+
{field.invalid && <p className="text-red-500">{field.invalid}</p>}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<button type="submit" disabled={pending}>Save</button>
|
|
212
|
+
</Form>
|
|
213
|
+
);
|
|
93
214
|
}
|
|
215
|
+
```
|
|
94
216
|
|
|
95
217
|
## Advanced Features
|
|
96
218
|
|
|
@@ -98,39 +220,131 @@ return (
|
|
|
98
220
|
|
|
99
221
|
The library works seamlessly with popular form management libraries:
|
|
100
222
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
223
|
+
** Docs still missing **
|
|
224
|
+
|
|
225
|
+
### Build your component around the library
|
|
226
|
+
|
|
227
|
+
** Docs still missing **
|
|
228
|
+
|
|
229
|
+
If you want to create your custom components around react-server-action library you can use the provided `useField` hook.
|
|
230
|
+
|
|
231
|
+
The `<Form>` and `<FormField>` components are providing a context in which you can call the `useField` for getting all the required information about a single field you need to build a component.
|
|
232
|
+
|
|
233
|
+
For example, we can recreate the standard Shadcn Form components in this way:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
"use client";
|
|
237
|
+
|
|
238
|
+
import { useField } from "@native-actions/client";
|
|
239
|
+
import React from "react";
|
|
240
|
+
|
|
241
|
+
export function FormLabel({ children }: { children: React.ReactNode }) {
|
|
242
|
+
const field = useField();
|
|
243
|
+
return (
|
|
244
|
+
<label
|
|
245
|
+
className={`text-sm font-medium leading-none ${field.invalid && "text-destructive"}`}
|
|
246
|
+
htmlFor={field.input.id}
|
|
247
|
+
>
|
|
248
|
+
{children}
|
|
249
|
+
</label>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function FormDescription({ children }: { children: React.ReactNode }) {
|
|
254
|
+
const field = useField();
|
|
255
|
+
return (
|
|
256
|
+
<p
|
|
257
|
+
id={field.input.id + "-description"}
|
|
258
|
+
className="text-sm text-muted-foreground"
|
|
259
|
+
>
|
|
260
|
+
{children}
|
|
261
|
+
</p>
|
|
262
|
+
);
|
|
110
263
|
}
|
|
111
264
|
|
|
265
|
+
export function FormMessage({ children }: { children?: React.ReactNode }) {
|
|
266
|
+
const field = useField();
|
|
267
|
+
const error = field.invalid || children;
|
|
268
|
+
if (!error) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return (
|
|
272
|
+
<p
|
|
273
|
+
id={field.input.id + "-message"}
|
|
274
|
+
className={`text-sm font-medium ${error && "text-destructive"}`}
|
|
275
|
+
>
|
|
276
|
+
{error}
|
|
277
|
+
</p>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
And them can use them inside the form for having a better developer experience
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
export default function ShadcnForm() {
|
|
286
|
+
const [state, action, pending] = useActionState(formAction, initialState(undefined));
|
|
287
|
+
return (
|
|
288
|
+
<Form state={state} action={action} schema={schema} className="space-y-4">
|
|
289
|
+
<FormField
|
|
290
|
+
name="name"
|
|
291
|
+
render={(field) => (
|
|
292
|
+
<div className="flex flex-col space-y-2">
|
|
293
|
+
<FormLabel>Name</FormLabel>
|
|
294
|
+
<FormDescription>Insert your name</FormDescription>
|
|
295
|
+
<Input {...field.input} />
|
|
296
|
+
<FormMessage />
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
/>
|
|
300
|
+
...
|
|
301
|
+
</Form>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
112
306
|
### Error Handling
|
|
113
307
|
|
|
114
|
-
|
|
308
|
+
In the `ActionResult` object there is an `error` property exposed. This property will be sent back only in case of an error during the action execution.
|
|
309
|
+
You can handle this error state in several ways
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
115
312
|
'use client';
|
|
313
|
+
import { useActionState } from 'react';
|
|
314
|
+
import { createUser } from './action'
|
|
116
315
|
export function UserForm() {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
316
|
+
const [state, action, pending] = useActionState(createUser);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
// You can subscribe to state variable
|
|
319
|
+
if (state.error) {
|
|
320
|
+
console.log(state.error);
|
|
321
|
+
}
|
|
322
|
+
}, [state]);
|
|
323
|
+
return (
|
|
324
|
+
<Form state={state} action={action} schema={userSchema} onError={(err) => {
|
|
325
|
+
// You can use the <Form> onError callback
|
|
326
|
+
toast.error(err);
|
|
327
|
+
}}>
|
|
328
|
+
// You can render some component if state error is present
|
|
329
|
+
{state.error && <p>An error occurred: {state.error}</p>}
|
|
330
|
+
<FormField
|
|
331
|
+
name="name"
|
|
332
|
+
render={(field) => (
|
|
333
|
+
<div>
|
|
334
|
+
<label htmlFor={field.input.id} className={field.invalid ? 'text-red-500' : ''}>
|
|
335
|
+
Name
|
|
336
|
+
</label>
|
|
337
|
+
<input {...field.input} type="text" />
|
|
338
|
+
{field.invalid && <p className="text-red-500">{field.invalid}</p>}
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<button type="submit" disabled={pending}>Save</button>
|
|
344
|
+
</Form>
|
|
345
|
+
);
|
|
125
346
|
}
|
|
126
|
-
|
|
127
|
-
## Best Practices
|
|
128
|
-
|
|
129
|
-
1. Always define your schemas in a separate file for better reusability
|
|
130
|
-
2. Use type inference from your schemas for better type safety
|
|
131
|
-
3. Implement proper error handling both on client and server
|
|
132
|
-
4. Consider using progressive enhancement for better user experience
|
|
133
|
-
5. Follow accessibility guidelines when displaying validation messages
|
|
347
|
+
```
|
|
134
348
|
|
|
135
349
|
## TypeScript Support
|
|
136
350
|
|
|
@@ -141,6 +355,46 @@ The library is written in TypeScript and provides full type safety:
|
|
|
141
355
|
- Proper error typing
|
|
142
356
|
- IDE autocompletion support
|
|
143
357
|
|
|
358
|
+
## Caveats
|
|
359
|
+
|
|
360
|
+
### datetime-local input type
|
|
361
|
+
|
|
362
|
+
In the standard html (as the time of writing) we have two kinds of date input
|
|
363
|
+
|
|
364
|
+
- `<input type="date">` which represent a yyyy-MM-dd date
|
|
365
|
+
- `<input type="datetime-local">` which represent a yyyy-MM-ddTHH:mm
|
|
366
|
+
Since from a zod rule like `z.coerce.date()` we cannot know if the user is using a `<input type="date">` or `<input type="datetime-local">` we are, by default, assuming you are using a type="date" field.
|
|
367
|
+
So, inside the `render` method of the `<FormField>` component, we are returning a yyyy-MM-dd formatted string inside the `field.input.defaultValue`
|
|
368
|
+
|
|
369
|
+
So this will work
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
<FormField
|
|
373
|
+
name="dateField"
|
|
374
|
+
render={(field) => (
|
|
375
|
+
<input {...field.input} type="date" />
|
|
376
|
+
)}
|
|
377
|
+
/>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Instead for working with `datetime-local` inputs we are exposing a helper method to retrieve the defaultValue to assing to the input to be sure that we will not loose the field state across submits
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { datetimeToInputDefaultValue } from 'react-server-actions';
|
|
384
|
+
...
|
|
385
|
+
<FormField
|
|
386
|
+
name="dateField"
|
|
387
|
+
render={(field) => (
|
|
388
|
+
<input {...field.input} type="datetime-local" defaultValue={field.value ? datetimeToInputDefaultValue(field.value) : ''} />
|
|
389
|
+
)}
|
|
390
|
+
/>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### select defaultValue
|
|
394
|
+
|
|
395
|
+
In react 19 there is an open issue https://github.com/facebook/react/issues/30580 that prevents the defaultValue to correctly set the select value.
|
|
396
|
+
According to this comment https://github.com/facebook/react/issues/30580#issuecomment-2537962675 there is a workaround by setting the 'key' attribute of the select.
|
|
397
|
+
|
|
144
398
|
## Contributing
|
|
145
399
|
|
|
146
400
|
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
|
package/dist/client/helpers.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Get the html5 validation attributes from a Zod schema field
|
|
4
|
+
* @param schema - The Zod schema
|
|
5
|
+
* @param path - The path to the field in the schema
|
|
6
|
+
* @returns The validation attributes for the field
|
|
7
|
+
* @note It would be cool to infer also the "type" attribute of the field, but this would not be consistent because a zod rule is not a 1-1 relation with an html input.
|
|
8
|
+
* For example, a zod.number() could be an <input type="number" /> but also a <select>. A z.date() could be represented by a <input type="date" /> but also a <input type="datetime-local" />.
|
|
9
|
+
* We could make a "guess" based on the zod rule, and then could be overriden by the user, but this would lead to confusion.
|
|
10
|
+
* So we leave it as a parameter for now.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getZodValidationAttributes(schema: z.ZodTypeAny, path: string[], options?: {
|
|
13
|
+
inferTypeAttr?: boolean;
|
|
14
|
+
}): {
|
|
15
|
+
type: 'string' | 'number' | 'date' | 'boolean' | 'enum';
|
|
16
|
+
attrs: Record<string, string | number | boolean>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Convert a date to an <input type="date"> default value
|
|
20
|
+
* @param date - The date to convert
|
|
21
|
+
* @returns The input default value
|
|
22
|
+
*/
|
|
23
|
+
export declare const dateToInputDefaultValue: (date: Date) => string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Convert a date to an <input type="datetime-local"> default value
|
|
26
|
+
* @param date - The date to convert
|
|
27
|
+
* @returns The input default value
|
|
28
|
+
*/
|
|
29
|
+
export declare const datetimeToInputDefaultValue: (date: Date) => string;
|
|
3
30
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/client/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,CAAC,CAAC,UAAU,EACpB,IAAI,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/client/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,CAAC,CAAC,UAAU,EACpB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IACR,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GACA;IACD,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACxD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAClD,CAgHA;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,SAAU,IAAI,uBAKjD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,SAAU,IAAI,WAKrD,CAAC"}
|
package/dist/client/helpers.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Get the html5 validation attributes from a Zod schema field
|
|
4
|
+
* @param schema - The Zod schema
|
|
5
|
+
* @param path - The path to the field in the schema
|
|
6
|
+
* @returns The validation attributes for the field
|
|
7
|
+
* @note It would be cool to infer also the "type" attribute of the field, but this would not be consistent because a zod rule is not a 1-1 relation with an html input.
|
|
8
|
+
* For example, a zod.number() could be an <input type="number" /> but also a <select>. A z.date() could be represented by a <input type="date" /> but also a <input type="datetime-local" />.
|
|
9
|
+
* We could make a "guess" based on the zod rule, and then could be overriden by the user, but this would lead to confusion.
|
|
10
|
+
* So we leave it as a parameter for now.
|
|
11
|
+
*/
|
|
12
|
+
export function getZodValidationAttributes(schema, path, options) {
|
|
3
13
|
const def = schema._def;
|
|
14
|
+
let type = 'string';
|
|
4
15
|
const attrs = {};
|
|
5
16
|
// First handle object type to get to the actual field
|
|
6
17
|
if (def.typeName === 'ZodObject' && path.length > 0) {
|
|
7
18
|
const shape = def.shape();
|
|
8
19
|
const field = shape[path[0]];
|
|
9
|
-
return field
|
|
20
|
+
return field
|
|
21
|
+
? getZodValidationAttributes(field, path.slice(1))
|
|
22
|
+
: { type, attrs };
|
|
10
23
|
}
|
|
11
24
|
// Now we're at the actual field, check if it's optional/nullable
|
|
12
25
|
const isOptionalType = schema instanceof z.ZodOptional;
|
|
@@ -14,19 +27,20 @@ export function getZodValidationAttributes(schema, path) {
|
|
|
14
27
|
// If it's an optional/nullable type, get attributes from the inner type but don't set required
|
|
15
28
|
if (isOptionalType || isNullableType) {
|
|
16
29
|
const innerAttrs = getZodValidationAttributes(def.innerType, path);
|
|
17
|
-
delete innerAttrs.required;
|
|
30
|
+
delete innerAttrs.attrs.required;
|
|
18
31
|
return innerAttrs;
|
|
19
32
|
}
|
|
20
33
|
// Set required by default for non-optional/nullable fields
|
|
21
34
|
attrs.required = true;
|
|
22
35
|
if (def.typeName === 'ZodString') {
|
|
36
|
+
type = 'string';
|
|
23
37
|
attrs.type = 'text';
|
|
24
38
|
if (def.checks) {
|
|
25
39
|
for (const check of def.checks) {
|
|
26
|
-
if (check.kind === '
|
|
40
|
+
if (check.kind === 'minlength') {
|
|
27
41
|
attrs.minLength = check.value;
|
|
28
42
|
}
|
|
29
|
-
if (check.kind === '
|
|
43
|
+
if (check.kind === 'maxlength') {
|
|
30
44
|
attrs.maxLength = check.value;
|
|
31
45
|
}
|
|
32
46
|
if (check.kind === 'email') {
|
|
@@ -35,14 +49,12 @@ export function getZodValidationAttributes(schema, path) {
|
|
|
35
49
|
if (check.kind === 'url') {
|
|
36
50
|
attrs.type = 'url';
|
|
37
51
|
}
|
|
38
|
-
if (check.kind === 'password') {
|
|
39
|
-
attrs.type = 'password';
|
|
40
|
-
}
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
if (def.typeName === 'ZodNumber' ||
|
|
45
56
|
(def.typeName === 'ZodCoerce' && def.schema._def.typeName === 'ZodNumber')) {
|
|
57
|
+
type = 'number';
|
|
46
58
|
attrs.type = 'number';
|
|
47
59
|
if (def.checks || (def.schema && def.schema._def.checks)) {
|
|
48
60
|
const checks = def.checks || def.schema._def.checks;
|
|
@@ -61,6 +73,7 @@ export function getZodValidationAttributes(schema, path) {
|
|
|
61
73
|
}
|
|
62
74
|
if (def.typeName === 'ZodDate' ||
|
|
63
75
|
(def.typeName === 'ZodCoerce' && def.schema._def.typeName === 'ZodDate')) {
|
|
76
|
+
type = 'date';
|
|
64
77
|
attrs.type = 'date';
|
|
65
78
|
if (def.checks || (def.schema && def.schema._def.checks)) {
|
|
66
79
|
const checks = def.checks || def.schema._def.checks;
|
|
@@ -74,6 +87,42 @@ export function getZodValidationAttributes(schema, path) {
|
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
|
-
|
|
90
|
+
if (def.typeName === 'ZodBoolean' ||
|
|
91
|
+
(def.typeName === 'ZodCoerce' && def.schema._def.typeName === 'ZodBoolean')) {
|
|
92
|
+
type = 'boolean';
|
|
93
|
+
attrs.type = 'checkbox';
|
|
94
|
+
}
|
|
95
|
+
if (def.typeName === 'ZodEnum' ||
|
|
96
|
+
(def.typeName === 'ZodCoerce' && def.schema._def.typeName === 'ZodEnum')) {
|
|
97
|
+
type = 'enum';
|
|
98
|
+
attrs.type = 'radio';
|
|
99
|
+
}
|
|
100
|
+
// If not specified, remove the type attribute
|
|
101
|
+
if (!options?.inferTypeAttr) {
|
|
102
|
+
delete attrs.type;
|
|
103
|
+
}
|
|
104
|
+
return { type, attrs };
|
|
78
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Convert a date to an <input type="date"> default value
|
|
108
|
+
* @param date - The date to convert
|
|
109
|
+
* @returns The input default value
|
|
110
|
+
*/
|
|
111
|
+
export const dateToInputDefaultValue = (date) => {
|
|
112
|
+
const newDate = date ? new Date(date) : new Date();
|
|
113
|
+
return new Date(newDate.getTime() - newDate.getTimezoneOffset() * 60000)
|
|
114
|
+
.toISOString()
|
|
115
|
+
.split('T')[0];
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Convert a date to an <input type="datetime-local"> default value
|
|
119
|
+
* @param date - The date to convert
|
|
120
|
+
* @returns The input default value
|
|
121
|
+
*/
|
|
122
|
+
export const datetimeToInputDefaultValue = (date) => {
|
|
123
|
+
const newDate = date ? new Date(date) : new Date();
|
|
124
|
+
return new Date(newDate.getTime() - newDate.getTimezoneOffset() * 60000)
|
|
125
|
+
.toISOString()
|
|
126
|
+
.slice(0, -1);
|
|
127
|
+
};
|
|
79
128
|
//# sourceMappingURL=helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/client/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,UAAU,0BAA0B,CACxC,MAAoB,EACpB,IAAc;
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/client/helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { ActionResult } from '../index.js';
|
|
4
|
-
|
|
4
|
+
type UseFieldReturn = {
|
|
5
5
|
invalid: string[] | undefined;
|
|
6
6
|
value: any;
|
|
7
7
|
input: {
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
10
|
-
defaultValue: any;
|
|
11
10
|
'aria-invalid': boolean;
|
|
11
|
+
autoComplete: 'on' | 'off' | undefined;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
defaultChecked?: boolean;
|
|
12
14
|
};
|
|
13
15
|
};
|
|
16
|
+
export declare const useField: <Schema extends z.AnyZodObject>() => UseFieldReturn;
|
|
14
17
|
export declare function Form<Schema extends z.AnyZodObject>({ children, action, state, schema, className, reset, onSuccess, onError, }: {
|
|
15
18
|
children: React.ReactNode;
|
|
16
19
|
action: (payload: FormData) => void;
|
|
@@ -25,4 +28,5 @@ export declare function FormField<Schema extends z.AnyZodObject>({ render, name,
|
|
|
25
28
|
render: (field: ReturnType<typeof useField>) => React.ReactNode;
|
|
26
29
|
name: keyof z.TypeOf<Schema>;
|
|
27
30
|
}): React.JSX.Element;
|
|
31
|
+
export {};
|
|
28
32
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0BhD,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,GAAG,CAAC;IACX,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QAEb,cAAc,EAAE,OAAO,CAAC;QACxB,YAAY,EAAE,IAAI,GAAG,KAAK,GAAG,SAAS,CAAC;QACvC,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;CACH,CAAC;AACF,eAAO,MAAM,QAAQ,GAAI,MAAM,SAAS,CAAC,CAAC,YAAY,OAAK,cA4C1D,CAAC;AAEF,wBAAgB,IAAI,CAAC,MAAM,SAAS,CAAC,CAAC,YAAY,EAAE,EAClD,QAAQ,EACR,MAAM,EACN,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,SAAS,EACT,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,CACV,WAAW,EAAE,GAAG,EAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,SAAS,KACnC,IAAI,CAAC;IACV,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,qBAsBA;AAED,wBAAgB,SAAS,CAAC,MAAM,SAAS,CAAC,CAAC,YAAY,EAAE,EACvD,MAAM,EACN,IAAI,GACL,EAAE;IACD,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAChE,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CAC9B,qBAOA"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useEffect, useRef } from 'react';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { getZodValidationAttributes } from './helpers.js';
|
|
4
|
+
import { dateToInputDefaultValue, getZodValidationAttributes, } from './helpers.js';
|
|
5
5
|
const FormContext = React.createContext({ state: undefined, schema: undefined });
|
|
6
6
|
const useForm = () => {
|
|
7
7
|
const context = React.useContext(FormContext);
|
|
@@ -12,27 +12,52 @@ const useForm = () => {
|
|
|
12
12
|
schema: context.schema,
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
-
const FieldContext = React.createContext({
|
|
15
|
+
const FieldContext = React.createContext({
|
|
16
|
+
name: '',
|
|
17
|
+
id: '',
|
|
18
|
+
});
|
|
16
19
|
export const useField = () => {
|
|
17
20
|
'use no memo'; // the useField hook should not be memoized because the value will change
|
|
18
|
-
const { name } = React.useContext(FieldContext);
|
|
21
|
+
const { name, id } = React.useContext(FieldContext);
|
|
19
22
|
const { state, schema } = useForm();
|
|
20
23
|
if (!state || !schema)
|
|
21
24
|
throw new Error('<FormField> must be used within a <Form>');
|
|
22
|
-
const id = React.useId();
|
|
23
25
|
// Get validation attributes from schema
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
+
const { type, attrs } = getZodValidationAttributes(schema, [name]);
|
|
27
|
+
// Create the field object
|
|
28
|
+
const field = {
|
|
26
29
|
invalid: state.invalid?.[name],
|
|
27
30
|
value: state.formData?.[name],
|
|
28
31
|
input: {
|
|
29
32
|
id: id,
|
|
30
33
|
name: name,
|
|
31
|
-
defaultValue: state.formData?.[name],
|
|
32
34
|
'aria-invalid': !!state.invalid?.[name],
|
|
33
|
-
|
|
35
|
+
autoComplete: undefined,
|
|
36
|
+
...attrs,
|
|
34
37
|
},
|
|
35
38
|
};
|
|
39
|
+
// Set the default value for mantaining the state across submissions
|
|
40
|
+
let defaultValue = state.formData?.[name];
|
|
41
|
+
if (defaultValue && defaultValue instanceof Date) {
|
|
42
|
+
defaultValue = dateToInputDefaultValue(defaultValue);
|
|
43
|
+
}
|
|
44
|
+
if (type === 'enum') {
|
|
45
|
+
field.input.defaultValue = defaultValue;
|
|
46
|
+
// TODO: This is not working if the input is a radio
|
|
47
|
+
// if the input is a radio, we don't know which of the inputs is this one
|
|
48
|
+
}
|
|
49
|
+
else if (type === 'boolean') {
|
|
50
|
+
field.input.defaultChecked = defaultValue;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
field.input.defaultValue = defaultValue;
|
|
54
|
+
}
|
|
55
|
+
// Set autocomplete for string inputs
|
|
56
|
+
if (type === 'string') {
|
|
57
|
+
field.input.autoComplete = 'on';
|
|
58
|
+
}
|
|
59
|
+
console.log(field);
|
|
60
|
+
return field;
|
|
36
61
|
};
|
|
37
62
|
export function Form({ children, action, state, schema, className, reset, onSuccess, onError, }) {
|
|
38
63
|
const formRef = useRef(null);
|
|
@@ -53,7 +78,8 @@ export function Form({ children, action, state, schema, className, reset, onSucc
|
|
|
53
78
|
React.createElement("form", { action: action, ref: formRef, className: className }, children)));
|
|
54
79
|
}
|
|
55
80
|
export function FormField({ render, name, }) {
|
|
56
|
-
|
|
81
|
+
const id = React.useId();
|
|
82
|
+
return (React.createElement(FieldContext.Provider, { value: { name: name, id } },
|
|
57
83
|
React.createElement(FormFieldRenderer, { render: render })));
|
|
58
84
|
}
|
|
59
85
|
function FormFieldRenderer({ render, }) {
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { dateToInputDefaultValue, datetimeToInputDefaultValue } from './client/helpers.js';
|
|
1
2
|
import { Form, FormField, useField } from './client/index.js';
|
|
2
3
|
import { action, actionWithParam } from './server/actions.js';
|
|
3
4
|
import { initialState, setInvalid } from './server/helpers.js';
|
|
4
5
|
import { type ActionResult, type ErrorActionResult, type FieldErrors, type IdleActionResult, type InvalidActionResult, type SuccessActionResult } from './server/types.js';
|
|
5
|
-
export { action, actionWithParam,
|
|
6
|
+
export { Form, FormField, action, actionWithParam, dateToInputDefaultValue, datetimeToInputDefaultValue, initialState, setInvalid, useField, };
|
|
6
7
|
export type { ActionResult, ErrorActionResult, FieldErrors, IdleActionResult, InvalidActionResult, SuccessActionResult, };
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,MAAM,EACN,eAAe,EACf,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,IAAI,EACJ,SAAS,EACT,MAAM,EACN,eAAe,EACf,uBAAuB,EACvB,2BAA2B,EAC3B,YAAY,EACZ,UAAU,EACV,QAAQ,GACT,CAAC;AACF,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,GACpB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { dateToInputDefaultValue, datetimeToInputDefaultValue, } from './client/helpers.js';
|
|
1
2
|
import { Form, FormField, useField } from './client/index.js';
|
|
2
3
|
import { action, actionWithParam } from './server/actions.js';
|
|
3
4
|
import { initialState, setInvalid } from './server/helpers.js';
|
|
4
5
|
import {} from './server/types.js';
|
|
5
|
-
export { action, actionWithParam,
|
|
6
|
+
export { Form, FormField, action, actionWithParam, dateToInputDefaultValue, datetimeToInputDefaultValue, initialState, setInvalid, useField, };
|
|
6
7
|
//# 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,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAON,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,MAAM,EACN,eAAe,EACf,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAON,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,IAAI,EACJ,SAAS,EACT,MAAM,EACN,eAAe,EACf,uBAAuB,EACvB,2BAA2B,EAC3B,YAAY,EACZ,UAAU,EACV,QAAQ,GACT,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-server-actions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A package for working with actions in React and Next.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"next",
|
|
@@ -24,14 +24,6 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
29
|
-
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
|
|
30
|
-
"check-format": "prettier --check .",
|
|
31
|
-
"ci": "npm run build && npm run check-format && npm run check-exports",
|
|
32
|
-
"format": "prettier --write .",
|
|
33
|
-
"prepublishOnly": "npm run ci"
|
|
34
|
-
},
|
|
35
27
|
"devDependencies": {
|
|
36
28
|
"@arethetypeswrong/cli": "^0.17.4",
|
|
37
29
|
"prettier": "^3.3.3",
|
|
@@ -41,5 +33,12 @@
|
|
|
41
33
|
"@types/react": ">=18.0.0",
|
|
42
34
|
"react": ">=18.0.0",
|
|
43
35
|
"zod": ">=3.22.4"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
|
|
40
|
+
"check-format": "prettier --check .",
|
|
41
|
+
"ci": "npm run build && npm run check-format && npm run check-exports",
|
|
42
|
+
"format": "prettier --write ."
|
|
44
43
|
}
|
|
45
|
-
}
|
|
44
|
+
}
|