@rsc-kit/core 0.11.0 → 0.13.0
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 +15 -10
- package/dist/js/Form.d.ts +152 -3
- package/dist/js/Form.js +293 -29
- package/dist/js/Form.js.map +1 -1
- package/dist/js/formEncoding.d.ts +8 -0
- package/dist/js/formEncoding.js +37 -0
- package/dist/js/formEncoding.js.map +1 -0
- package/dist/js/formStore.d.ts +9 -0
- package/dist/js/formStore.js +43 -0
- package/dist/js/formStore.js.map +1 -0
- package/dist/js/updateStore.d.ts +17 -0
- package/dist/js/updateStore.js +44 -0
- package/dist/js/updateStore.js.map +1 -0
- package/dist/js/useAppUpdate.d.ts +4 -0
- package/dist/js/useAppUpdate.js +29 -0
- package/dist/js/useAppUpdate.js.map +1 -0
- package/dist/routes.d.ts +28 -0
- package/dist/routes.js +15 -1
- package/dist/routes.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +147 -19
- package/dist/vite.js.map +1 -1
- package/package.json +14 -6
- package/dist/js/useForm.d.ts +0 -42
- package/dist/js/useForm.js +0 -164
- package/dist/js/useForm.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rsc-kit/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React Server Components
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "React Server Components as a Vite plugin. Routing, streaming, server actions, typed urls and build-time prerendering — deploy to Bun, Node, Workers, Vercel or Deno.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -61,10 +61,6 @@
|
|
|
61
61
|
"types": "./dist/js/router.d.ts",
|
|
62
62
|
"default": "./dist/js/router.js"
|
|
63
63
|
},
|
|
64
|
-
"./useForm": {
|
|
65
|
-
"types": "./dist/js/useForm.d.ts",
|
|
66
|
-
"default": "./dist/js/useForm.js"
|
|
67
|
-
},
|
|
68
64
|
"./usePathname": {
|
|
69
65
|
"types": "./dist/js/usePathname.d.ts",
|
|
70
66
|
"default": "./dist/js/usePathname.js"
|
|
@@ -176,6 +172,18 @@
|
|
|
176
172
|
"./manifest-file": {
|
|
177
173
|
"types": "./dist/webManifest.d.ts",
|
|
178
174
|
"default": "./dist/webManifest.js"
|
|
175
|
+
},
|
|
176
|
+
"./useAppUpdate": {
|
|
177
|
+
"types": "./dist/js/useAppUpdate.d.ts",
|
|
178
|
+
"default": "./dist/js/useAppUpdate.js"
|
|
179
|
+
},
|
|
180
|
+
"./form-encoding": {
|
|
181
|
+
"types": "./dist/js/formEncoding.d.ts",
|
|
182
|
+
"default": "./dist/js/formEncoding.js"
|
|
183
|
+
},
|
|
184
|
+
"./useField": {
|
|
185
|
+
"types": "./dist/js/Form.d.ts",
|
|
186
|
+
"default": "./dist/js/Form.js"
|
|
179
187
|
}
|
|
180
188
|
},
|
|
181
189
|
"peerDependencies": {
|
package/dist/js/useForm.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "./standardSchema";
|
|
2
|
-
type SetDataFn<T> = {
|
|
3
|
-
<K extends keyof T>(field: K, value: T[K]): void;
|
|
4
|
-
(values: Partial<T>): void;
|
|
5
|
-
};
|
|
6
|
-
export interface UseFormReturn<T extends Record<string, unknown>> {
|
|
7
|
-
data: T;
|
|
8
|
-
setData: SetDataFn<T>;
|
|
9
|
-
errors: Partial<Record<keyof T & string, string[]>>;
|
|
10
|
-
error: (field: keyof T & string) => string | undefined;
|
|
11
|
-
hasErrors: boolean;
|
|
12
|
-
pending: boolean;
|
|
13
|
-
processing: boolean;
|
|
14
|
-
wasSuccessful: boolean;
|
|
15
|
-
recentlySuccessful: boolean;
|
|
16
|
-
clearErrors: (...fields: (keyof T & string)[]) => void;
|
|
17
|
-
reset: (...fields: (keyof T)[]) => void;
|
|
18
|
-
setDefaults: (values?: Partial<T>) => void;
|
|
19
|
-
transform: (fn: (data: T) => Record<string, unknown>) => void;
|
|
20
|
-
submit: (action: (formData: FormData) => Promise<unknown>, optimistic?: () => void) => Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Serialize form state into FormData for a server action.
|
|
24
|
-
*
|
|
25
|
-
* Exported for testing: this is the contract between useForm and the action —
|
|
26
|
-
* booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
|
|
27
|
-
* `key[]`, Files pass through untouched for native uploads, and null/undefined
|
|
28
|
-
* are dropped rather than sent as the string "null".
|
|
29
|
-
*/
|
|
30
|
-
export declare function buildFormData(data: Record<string, unknown>): FormData;
|
|
31
|
-
export declare function useForm<T extends Record<string, unknown>>(initialValues: T, options?: {
|
|
32
|
-
/**
|
|
33
|
-
* Check the fields before submitting, with any Standard Schema — Zod,
|
|
34
|
-
* Valibot, ArkType.
|
|
35
|
-
*
|
|
36
|
-
* A failure fills `errors` and the action is never called. A courtesy, not
|
|
37
|
-
* a control: the action is reachable without this form, so the server
|
|
38
|
-
* still has to check.
|
|
39
|
-
*/
|
|
40
|
-
schema?: StandardSchemaV1;
|
|
41
|
-
}): UseFormReturn<T>;
|
|
42
|
-
export {};
|
package/dist/js/useForm.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useState, useRef, useCallback, useTransition } from "react";
|
|
3
|
-
import { ServerValidationError, ServerDumpError } from "./errors";
|
|
4
|
-
import { validateWith } from "./standardSchema";
|
|
5
|
-
/**
|
|
6
|
-
* Serialize form state into FormData for a server action.
|
|
7
|
-
*
|
|
8
|
-
* Exported for testing: this is the contract between useForm and the action —
|
|
9
|
-
* booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
|
|
10
|
-
* `key[]`, Files pass through untouched for native uploads, and null/undefined
|
|
11
|
-
* are dropped rather than sent as the string "null".
|
|
12
|
-
*/
|
|
13
|
-
export function buildFormData(data) {
|
|
14
|
-
const formData = new FormData();
|
|
15
|
-
for (const [key, val] of Object.entries(data)) {
|
|
16
|
-
if (val === null || val === undefined) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
if (val instanceof File) {
|
|
20
|
-
formData.append(key, val);
|
|
21
|
-
}
|
|
22
|
-
else if (typeof val === "boolean") {
|
|
23
|
-
formData.append(key, val ? "1" : "0");
|
|
24
|
-
}
|
|
25
|
-
else if (Array.isArray(val)) {
|
|
26
|
-
for (const item of val) {
|
|
27
|
-
formData.append(`${key}[]`, String(item));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
formData.append(key, String(val));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return formData;
|
|
35
|
-
}
|
|
36
|
-
export function useForm(initialValues, options = {}) {
|
|
37
|
-
const [data, setDataState] = useState(initialValues);
|
|
38
|
-
const [errors, setErrors] = useState({});
|
|
39
|
-
const [wasSuccessful, setWasSuccessful] = useState(false);
|
|
40
|
-
const [recentlySuccessful, setRecentlySuccessful] = useState(false);
|
|
41
|
-
const [isPending, startTransition] = useTransition();
|
|
42
|
-
const defaultsRef = useRef({ ...initialValues });
|
|
43
|
-
const transformRef = useRef(null);
|
|
44
|
-
const recentTimerRef = useRef(undefined);
|
|
45
|
-
const setData = useCallback((fieldOrValues, value) => {
|
|
46
|
-
if (typeof fieldOrValues === "object") {
|
|
47
|
-
setDataState((prev) => ({ ...prev, ...fieldOrValues }));
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
setDataState((prev) => ({ ...prev, [fieldOrValues]: value }));
|
|
51
|
-
}
|
|
52
|
-
}, []);
|
|
53
|
-
const error = useCallback((field) => errors[field]?.[0], [errors]);
|
|
54
|
-
const clearErrors = useCallback((...fields) => {
|
|
55
|
-
if (fields.length === 0) {
|
|
56
|
-
setErrors({});
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
setErrors((prev) => {
|
|
60
|
-
const next = { ...prev };
|
|
61
|
-
for (const f of fields) {
|
|
62
|
-
delete next[f];
|
|
63
|
-
}
|
|
64
|
-
return next;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}, []);
|
|
68
|
-
const reset = useCallback((...fields) => {
|
|
69
|
-
if (fields.length === 0) {
|
|
70
|
-
setDataState({ ...defaultsRef.current });
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
setDataState((prev) => {
|
|
74
|
-
const next = { ...prev };
|
|
75
|
-
for (const f of fields) {
|
|
76
|
-
next[f] = defaultsRef.current[f];
|
|
77
|
-
}
|
|
78
|
-
return next;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
clearErrors();
|
|
82
|
-
}, [clearErrors]);
|
|
83
|
-
const setDefaults = useCallback((values) => {
|
|
84
|
-
if (values) {
|
|
85
|
-
defaultsRef.current = { ...defaultsRef.current, ...values };
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
defaultsRef.current = { ...data };
|
|
89
|
-
}
|
|
90
|
-
}, [data, options.schema]);
|
|
91
|
-
const transformFn = useCallback((fn) => {
|
|
92
|
-
transformRef.current = fn;
|
|
93
|
-
}, []);
|
|
94
|
-
const submit = useCallback((action, optimistic) => {
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
startTransition(async () => {
|
|
97
|
-
try {
|
|
98
|
-
// Before the optimistic update, so a rejected form never shows the
|
|
99
|
-
// row it was going to add.
|
|
100
|
-
const invalid = await validateWith(options.schema, data);
|
|
101
|
-
if (invalid) {
|
|
102
|
-
setErrors(invalid);
|
|
103
|
-
resolve();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// Call optimistic updater inside the transition so React's
|
|
107
|
-
// useOptimistic picks it up and auto-reverts on settle.
|
|
108
|
-
optimistic?.();
|
|
109
|
-
const payload = transformRef.current ? transformRef.current(data) : data;
|
|
110
|
-
const formData = buildFormData(payload);
|
|
111
|
-
setErrors({});
|
|
112
|
-
const result = await action(formData);
|
|
113
|
-
const answer = result;
|
|
114
|
-
// An action built with createActionClient returns its failures
|
|
115
|
-
// rather than throwing them, because a rejected server action is
|
|
116
|
-
// serialised opaquely and the fields it named would be lost.
|
|
117
|
-
if (answer?.validationErrors) {
|
|
118
|
-
setErrors(answer.validationErrors);
|
|
119
|
-
resolve();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (answer?.serverError) {
|
|
123
|
-
reject(new Error(answer.serverError));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
setWasSuccessful(true);
|
|
127
|
-
setRecentlySuccessful(true);
|
|
128
|
-
if (recentTimerRef.current) {
|
|
129
|
-
clearTimeout(recentTimerRef.current);
|
|
130
|
-
}
|
|
131
|
-
recentTimerRef.current = setTimeout(() => setRecentlySuccessful(false), 2000);
|
|
132
|
-
resolve();
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
if (err instanceof ServerValidationError) {
|
|
136
|
-
setErrors(err.errors);
|
|
137
|
-
}
|
|
138
|
-
if (err instanceof ServerDumpError) {
|
|
139
|
-
resolve();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
reject(err);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
}, [data]);
|
|
147
|
-
return {
|
|
148
|
-
data,
|
|
149
|
-
setData,
|
|
150
|
-
errors: errors,
|
|
151
|
-
error,
|
|
152
|
-
hasErrors: Object.keys(errors).length > 0,
|
|
153
|
-
pending: isPending,
|
|
154
|
-
processing: isPending,
|
|
155
|
-
wasSuccessful,
|
|
156
|
-
recentlySuccessful,
|
|
157
|
-
clearErrors,
|
|
158
|
-
reset,
|
|
159
|
-
setDefaults,
|
|
160
|
-
transform: transformFn,
|
|
161
|
-
submit,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
//# sourceMappingURL=useForm.js.map
|
package/dist/js/useForm.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.js","sourceRoot":"","sources":["../../src/js/useForm.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useState, useRef, useCallback, useTransition } from \"react\";\nimport { ServerValidationError, ServerDumpError } from \"./errors\";\nimport { validateWith } from \"./standardSchema\";\nimport type { StandardSchemaV1 } from \"./standardSchema\";\n\ntype SetDataFn<T> = {\n <K extends keyof T>(field: K, value: T[K]): void;\n (values: Partial<T>): void;\n};\n\nexport interface UseFormReturn<T extends Record<string, unknown>> {\n data: T;\n setData: SetDataFn<T>;\n errors: Partial<Record<keyof T & string, string[]>>;\n error: (field: keyof T & string) => string | undefined;\n hasErrors: boolean;\n pending: boolean;\n processing: boolean;\n wasSuccessful: boolean;\n recentlySuccessful: boolean;\n clearErrors: (...fields: (keyof T & string)[]) => void;\n reset: (...fields: (keyof T)[]) => void;\n setDefaults: (values?: Partial<T>) => void;\n transform: (fn: (data: T) => Record<string, unknown>) => void;\n submit: (action: (formData: FormData) => Promise<unknown>, optimistic?: () => void) => Promise<void>;\n}\n\n/**\n * Serialize form state into FormData for a server action.\n *\n * Exported for testing: this is the contract between useForm and the action —\n * booleans become \"1\"/\"0\" so PHP sees something truthy, arrays repeat under\n * `key[]`, Files pass through untouched for native uploads, and null/undefined\n * are dropped rather than sent as the string \"null\".\n */\nexport function buildFormData(data: Record<string, unknown>): FormData {\n const formData = new FormData();\n\n for (const [key, val] of Object.entries(data)) {\n if (val === null || val === undefined) {\n continue;\n }\n if (val instanceof File) {\n formData.append(key, val);\n } else if (typeof val === \"boolean\") {\n formData.append(key, val ? \"1\" : \"0\");\n } else if (Array.isArray(val)) {\n for (const item of val) {\n formData.append(`${key}[]`, String(item));\n }\n } else {\n formData.append(key, String(val));\n }\n }\n\n return formData;\n}\n\nexport function useForm<T extends Record<string, unknown>>(\n initialValues: T,\n options: {\n /**\n * Check the fields before submitting, with any Standard Schema — Zod,\n * Valibot, ArkType.\n *\n * A failure fills `errors` and the action is never called. A courtesy, not\n * a control: the action is reachable without this form, so the server\n * still has to check.\n */\n schema?: StandardSchemaV1\n } = {},\n): UseFormReturn<T> {\n const [data, setDataState] = useState<T>(initialValues);\n const [errors, setErrors] = useState<Record<string, string[]>>({});\n const [wasSuccessful, setWasSuccessful] = useState(false);\n const [recentlySuccessful, setRecentlySuccessful] = useState(false);\n const [isPending, startTransition] = useTransition();\n\n const defaultsRef = useRef<T>({ ...initialValues });\n const transformRef = useRef<((data: T) => Record<string, unknown>) | null>(null);\n const recentTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const setData: SetDataFn<T> = useCallback(\n (fieldOrValues: keyof T | Partial<T>, value?: unknown) => {\n if (typeof fieldOrValues === \"object\") {\n setDataState((prev) => ({ ...prev, ...fieldOrValues }));\n } else {\n setDataState((prev) => ({ ...prev, [fieldOrValues]: value as T[keyof T] }));\n }\n },\n []\n );\n\n const error = useCallback(\n (field: keyof T & string): string | undefined => errors[field]?.[0],\n [errors]\n );\n\n const clearErrors = useCallback(\n (...fields: (keyof T & string)[]) => {\n if (fields.length === 0) {\n setErrors({});\n } else {\n setErrors((prev) => {\n const next = { ...prev };\n for (const f of fields) {\n delete next[f];\n }\n return next;\n });\n }\n },\n []\n );\n\n const reset = useCallback(\n (...fields: (keyof T)[]) => {\n if (fields.length === 0) {\n setDataState({ ...defaultsRef.current });\n } else {\n setDataState((prev) => {\n const next = { ...prev };\n for (const f of fields) {\n next[f] = defaultsRef.current[f];\n }\n return next;\n });\n }\n clearErrors();\n },\n [clearErrors]\n );\n\n const setDefaults = useCallback(\n (values?: Partial<T>) => {\n if (values) {\n defaultsRef.current = { ...defaultsRef.current, ...values };\n } else {\n defaultsRef.current = { ...data };\n }\n },\n [data, options.schema]\n );\n\n const transformFn = useCallback(\n (fn: (data: T) => Record<string, unknown>) => {\n transformRef.current = fn;\n },\n []\n );\n\n const submit = useCallback(\n (action: (formData: FormData) => Promise<unknown>, optimistic?: () => void): Promise<void> => {\n return new Promise<void>((resolve, reject) => {\n startTransition(async () => {\n try {\n // Before the optimistic update, so a rejected form never shows the\n // row it was going to add.\n const invalid = await validateWith(options.schema, data);\n\n if (invalid) {\n setErrors(invalid);\n resolve();\n\n return;\n }\n\n // Call optimistic updater inside the transition so React's\n // useOptimistic picks it up and auto-reverts on settle.\n optimistic?.();\n\n const payload = transformRef.current ? transformRef.current(data) : data;\n const formData = buildFormData(payload);\n\n setErrors({});\n\n const result = await action(formData);\n const answer = result as\n | { validationErrors?: Record<string, string[]>; serverError?: string }\n | undefined\n\n // An action built with createActionClient returns its failures\n // rather than throwing them, because a rejected server action is\n // serialised opaquely and the fields it named would be lost.\n if (answer?.validationErrors) {\n setErrors(answer.validationErrors);\n resolve();\n\n return;\n }\n\n if (answer?.serverError) {\n reject(new Error(answer.serverError));\n\n return;\n }\n\n setWasSuccessful(true);\n setRecentlySuccessful(true);\n\n if (recentTimerRef.current) {\n clearTimeout(recentTimerRef.current);\n }\n recentTimerRef.current = setTimeout(() => setRecentlySuccessful(false), 2000);\n\n resolve();\n } catch (err) {\n if (err instanceof ServerValidationError) {\n setErrors(err.errors);\n }\n if (err instanceof ServerDumpError) {\n resolve();\n return;\n }\n reject(err);\n }\n });\n });\n },\n [data]\n );\n\n return {\n data,\n setData,\n errors: errors as Partial<Record<keyof T & string, string[]>>,\n error,\n hasErrors: Object.keys(errors).length > 0,\n pending: isPending,\n processing: isPending,\n wasSuccessful,\n recentlySuccessful,\n clearErrors,\n reset,\n setDefaults,\n transform: transformFn,\n submit,\n };\n}\n"]}
|