@rsc-kit/core 0.12.0 → 0.13.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rsc-kit/core",
3
- "version": "0.12.0",
4
- "description": "React Server Components on any JavaScript server: a Vite plugin that discovers the route tree, and a host adapter that serves it.",
3
+ "version": "0.13.1",
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"
@@ -180,6 +176,14 @@
180
176
  "./useAppUpdate": {
181
177
  "types": "./dist/js/useAppUpdate.d.ts",
182
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"
183
187
  }
184
188
  },
185
189
  "peerDependencies": {
@@ -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 {};
@@ -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
@@ -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"]}