@nifrajs/web 1.0.0 → 1.1.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/dist/forms.d.ts +53 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +16 -0
- package/dist/forms.js.map +1 -0
- package/package.json +8 -2
- package/src/forms.ts +86 -0
package/dist/forms.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/forms` — a typed form ↔ backend-schema binding. The three artifacts that must agree —
|
|
3
|
+
* a form's `<input name>`, the action's `formData.get`, and the typed-client payload — all key off the
|
|
4
|
+
* ONE route body schema, derived purely from `typeof backend`. A typo'd field name, or a field the
|
|
5
|
+
* schema doesn't have, becomes a COMPILE error instead of a silent runtime empty.
|
|
6
|
+
*
|
|
7
|
+
* import type { backend } from "../backend"
|
|
8
|
+
* import { formFor } from "@nifrajs/web/forms"
|
|
9
|
+
*
|
|
10
|
+
* const f = formFor<typeof backend, "/todos">() // method defaults to "post"
|
|
11
|
+
* // In the component (any framework — spread the props onto an input):
|
|
12
|
+
* <input {...f.field("text")} /> // "text" ∈ the body's keys, else a compile error
|
|
13
|
+
* // In the action:
|
|
14
|
+
* const text = f.read(await request.formData(), "text") // an orphan key won't compile
|
|
15
|
+
* await api.todos.post({ text }) // payload already typed by nifra
|
|
16
|
+
*
|
|
17
|
+
* Framework-agnostic + dependency-free: no codegen, no schema is bundled into the client, and the
|
|
18
|
+
* runtime is a trivial pass-through — all the work is in the types. It checks the field KEY, never the
|
|
19
|
+
* MEANING (a `field("email")` collecting a phone is still valid here); that stays an app/AI concern.
|
|
20
|
+
*/
|
|
21
|
+
import type { RouteInfo, Server } from "@nifrajs/core";
|
|
22
|
+
type FormEntry = ReturnType<FormData["get"]>;
|
|
23
|
+
type FormEntries = ReturnType<FormData["getAll"]>;
|
|
24
|
+
/** The route registry baked into a server's type (`typeof app`). */
|
|
25
|
+
type RegistryOf<App> = App extends Server<infer R, infer _Ctx> ? R : never;
|
|
26
|
+
/** Every route path the app declares — constrains `Path`, so a wrong path is itself a type error. */
|
|
27
|
+
export type RoutePaths<App> = keyof RegistryOf<App> & string;
|
|
28
|
+
/** The body object type of `App`'s `Method Path` route (`never` when the route declares no body). */
|
|
29
|
+
export type RouteBody<App, Path extends string, Method extends string> = Path extends keyof RegistryOf<App> ? Uppercase<Method> extends keyof RegistryOf<App>[Path] ? (RegistryOf<App>[Path][Uppercase<Method>] & RouteInfo)["body"] : never : never;
|
|
30
|
+
/** The valid field names for that route's body — the schema's keys as a string union. */
|
|
31
|
+
export type FieldKey<App, Path extends string, Method extends string> = [
|
|
32
|
+
RouteBody<App, Path, Method>
|
|
33
|
+
] extends [never] ? never : keyof RouteBody<App, Path, Method> & string;
|
|
34
|
+
/** Extra attributes merged into the returned input props (id, type, defaultValue, placeholder, …). */
|
|
35
|
+
export type FieldProps = Record<string, unknown>;
|
|
36
|
+
export interface FormHandle<App, Path extends string, Method extends string> {
|
|
37
|
+
/** Input props for a schema field. `name` is constrained to the body's keys — a typo, or a field the
|
|
38
|
+
* schema doesn't have, is a COMPILE error. Spread onto an element: `<input {...f.field("text")} />`. */
|
|
39
|
+
field<K extends FieldKey<App, Path, Method>>(name: K, props?: FieldProps): FieldProps & {
|
|
40
|
+
name: K;
|
|
41
|
+
};
|
|
42
|
+
/** `formData.get`, key-constrained to the body's fields — reading an orphan key won't compile. */
|
|
43
|
+
read<K extends FieldKey<App, Path, Method>>(form: FormData, name: K): FormEntry;
|
|
44
|
+
/** `formData.getAll`, key-checked the same way. */
|
|
45
|
+
readAll<K extends FieldKey<App, Path, Method>>(form: FormData, name: K): FormEntries;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bind a form to a backend route's body schema at the type level. `App` is `typeof backend`; `Path` is
|
|
49
|
+
* constrained to the app's real routes (a wrong path is a type error); `Method` defaults to `"post"`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formFor<App, Path extends RoutePaths<App>, Method extends string = "post">(): FormHandle<App, Path, Method>;
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=forms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms.d.ts","sourceRoot":"","sources":["../src/forms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAItD,KAAK,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;AAC5C,KAAK,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;AAEjD,oEAAoE;AACpE,KAAK,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAE1E,qGAAqG;AACrG,MAAM,MAAM,UAAU,CAAC,GAAG,IAAI,MAAM,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,CAAA;AAE5D,qGAAqG;AACrG,MAAM,MAAM,SAAS,CACnB,GAAG,EACH,IAAI,SAAS,MAAM,EACnB,MAAM,SAAS,MAAM,IACnB,IAAI,SAAS,MAAM,UAAU,CAAC,GAAG,CAAC,GAClC,SAAS,CAAC,MAAM,CAAC,SAAS,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GACnD,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,MAAM,CAAC,GAC9D,KAAK,GACP,KAAK,CAAA;AAET,yFAAyF;AACzF,MAAM,MAAM,QAAQ,CAAC,GAAG,EAAE,IAAI,SAAS,MAAM,EAAE,MAAM,SAAS,MAAM,IAAI;IACtE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC;CAC7B,SAAS,CAAC,KAAK,CAAC,GACb,KAAK,GACL,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;AAE/C,sGAAsG;AACtG,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEhD,MAAM,WAAW,UAAU,CAAC,GAAG,EAAE,IAAI,SAAS,MAAM,EAAE,MAAM,SAAS,MAAM;IACzE;4GACwG;IACxG,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EACzC,IAAI,EAAE,CAAC,EACP,KAAK,CAAC,EAAE,UAAU,GACjB,UAAU,GAAG;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAA;IAC3B,kGAAkG;IAClG,IAAI,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,CAAA;IAC/E,mDAAmD;IACnD,OAAO,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,WAAW,CAAA;CACrF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,GAAG,EACH,IAAI,SAAS,UAAU,CAAC,GAAG,CAAC,EAC5B,MAAM,SAAS,MAAM,GAAG,MAAM,KAC3B,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAUjC"}
|
package/dist/forms.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind a form to a backend route's body schema at the type level. `App` is `typeof backend`; `Path` is
|
|
3
|
+
* constrained to the app's real routes (a wrong path is a type error); `Method` defaults to `"post"`.
|
|
4
|
+
*/
|
|
5
|
+
export function formFor() {
|
|
6
|
+
const handle = {
|
|
7
|
+
field: (name, props) => ({
|
|
8
|
+
...(props ?? {}),
|
|
9
|
+
name,
|
|
10
|
+
}),
|
|
11
|
+
read: (form, name) => form.get(name),
|
|
12
|
+
readAll: (form, name) => form.getAll(name),
|
|
13
|
+
};
|
|
14
|
+
return handle;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms.js","sourceRoot":"","sources":["../src/forms.ts"],"names":[],"mappings":"AAmEA;;;GAGG;AACH,MAAM,UAAU,OAAO;IAKrB,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,CAAC,IAAY,EAAE,KAAkB,EAAiC,EAAE,CAAC,CAAC;YAC3E,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI;SACL,CAAC;QACF,IAAI,EAAE,CAAC,IAAc,EAAE,IAAY,EAAa,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;QACjE,OAAO,EAAE,CAAC,IAAc,EAAE,IAAY,EAAe,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;KAC1E,CAAA;IACD,OAAO,MAAkD,CAAA;AAC3D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Framework-agnostic SSR core for nifra — the render seam + HTML document orchestration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
"types": "./dist/client.d.ts",
|
|
40
40
|
"default": "./dist/client.js"
|
|
41
41
|
},
|
|
42
|
+
"./forms": {
|
|
43
|
+
"bun": "./src/forms.ts",
|
|
44
|
+
"types": "./dist/forms.d.ts",
|
|
45
|
+
"default": "./dist/forms.js"
|
|
46
|
+
},
|
|
42
47
|
"./islands": {
|
|
43
48
|
"bun": "./src/islands.ts",
|
|
44
49
|
"types": "./dist/islands.d.ts",
|
|
@@ -102,7 +107,7 @@
|
|
|
102
107
|
"access": "public"
|
|
103
108
|
},
|
|
104
109
|
"dependencies": {
|
|
105
|
-
"@nifrajs/core": "1.
|
|
110
|
+
"@nifrajs/core": "1.1.0"
|
|
106
111
|
},
|
|
107
112
|
"peerDependencies": {
|
|
108
113
|
"sass": "^1.0.0",
|
|
@@ -117,6 +122,7 @@
|
|
|
117
122
|
}
|
|
118
123
|
},
|
|
119
124
|
"devDependencies": {
|
|
125
|
+
"@nifrajs/schema": "workspace:*",
|
|
120
126
|
"sass": "^1.77.0",
|
|
121
127
|
"vite": "^8.0.0"
|
|
122
128
|
},
|
package/src/forms.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/forms` — a typed form ↔ backend-schema binding. The three artifacts that must agree —
|
|
3
|
+
* a form's `<input name>`, the action's `formData.get`, and the typed-client payload — all key off the
|
|
4
|
+
* ONE route body schema, derived purely from `typeof backend`. A typo'd field name, or a field the
|
|
5
|
+
* schema doesn't have, becomes a COMPILE error instead of a silent runtime empty.
|
|
6
|
+
*
|
|
7
|
+
* import type { backend } from "../backend"
|
|
8
|
+
* import { formFor } from "@nifrajs/web/forms"
|
|
9
|
+
*
|
|
10
|
+
* const f = formFor<typeof backend, "/todos">() // method defaults to "post"
|
|
11
|
+
* // In the component (any framework — spread the props onto an input):
|
|
12
|
+
* <input {...f.field("text")} /> // "text" ∈ the body's keys, else a compile error
|
|
13
|
+
* // In the action:
|
|
14
|
+
* const text = f.read(await request.formData(), "text") // an orphan key won't compile
|
|
15
|
+
* await api.todos.post({ text }) // payload already typed by nifra
|
|
16
|
+
*
|
|
17
|
+
* Framework-agnostic + dependency-free: no codegen, no schema is bundled into the client, and the
|
|
18
|
+
* runtime is a trivial pass-through — all the work is in the types. It checks the field KEY, never the
|
|
19
|
+
* MEANING (a `field("email")` collecting a phone is still valid here); that stays an app/AI concern.
|
|
20
|
+
*/
|
|
21
|
+
import type { RouteInfo, Server } from "@nifrajs/core"
|
|
22
|
+
|
|
23
|
+
// Derived from `FormData` itself (not the `FormDataEntryValue` global) so this file stays DOM-lib-free:
|
|
24
|
+
// `@nifrajs/web`'s server surface is typechecked by the root config, which has no DOM lib.
|
|
25
|
+
type FormEntry = ReturnType<FormData["get"]>
|
|
26
|
+
type FormEntries = ReturnType<FormData["getAll"]>
|
|
27
|
+
|
|
28
|
+
/** The route registry baked into a server's type (`typeof app`). */
|
|
29
|
+
type RegistryOf<App> = App extends Server<infer R, infer _Ctx> ? R : never
|
|
30
|
+
|
|
31
|
+
/** Every route path the app declares — constrains `Path`, so a wrong path is itself a type error. */
|
|
32
|
+
export type RoutePaths<App> = keyof RegistryOf<App> & string
|
|
33
|
+
|
|
34
|
+
/** The body object type of `App`'s `Method Path` route (`never` when the route declares no body). */
|
|
35
|
+
export type RouteBody<
|
|
36
|
+
App,
|
|
37
|
+
Path extends string,
|
|
38
|
+
Method extends string,
|
|
39
|
+
> = Path extends keyof RegistryOf<App>
|
|
40
|
+
? Uppercase<Method> extends keyof RegistryOf<App>[Path]
|
|
41
|
+
? (RegistryOf<App>[Path][Uppercase<Method>] & RouteInfo)["body"]
|
|
42
|
+
: never
|
|
43
|
+
: never
|
|
44
|
+
|
|
45
|
+
/** The valid field names for that route's body — the schema's keys as a string union. */
|
|
46
|
+
export type FieldKey<App, Path extends string, Method extends string> = [
|
|
47
|
+
RouteBody<App, Path, Method>,
|
|
48
|
+
] extends [never]
|
|
49
|
+
? never
|
|
50
|
+
: keyof RouteBody<App, Path, Method> & string
|
|
51
|
+
|
|
52
|
+
/** Extra attributes merged into the returned input props (id, type, defaultValue, placeholder, …). */
|
|
53
|
+
export type FieldProps = Record<string, unknown>
|
|
54
|
+
|
|
55
|
+
export interface FormHandle<App, Path extends string, Method extends string> {
|
|
56
|
+
/** Input props for a schema field. `name` is constrained to the body's keys — a typo, or a field the
|
|
57
|
+
* schema doesn't have, is a COMPILE error. Spread onto an element: `<input {...f.field("text")} />`. */
|
|
58
|
+
field<K extends FieldKey<App, Path, Method>>(
|
|
59
|
+
name: K,
|
|
60
|
+
props?: FieldProps,
|
|
61
|
+
): FieldProps & { name: K }
|
|
62
|
+
/** `formData.get`, key-constrained to the body's fields — reading an orphan key won't compile. */
|
|
63
|
+
read<K extends FieldKey<App, Path, Method>>(form: FormData, name: K): FormEntry
|
|
64
|
+
/** `formData.getAll`, key-checked the same way. */
|
|
65
|
+
readAll<K extends FieldKey<App, Path, Method>>(form: FormData, name: K): FormEntries
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bind a form to a backend route's body schema at the type level. `App` is `typeof backend`; `Path` is
|
|
70
|
+
* constrained to the app's real routes (a wrong path is a type error); `Method` defaults to `"post"`.
|
|
71
|
+
*/
|
|
72
|
+
export function formFor<
|
|
73
|
+
App,
|
|
74
|
+
Path extends RoutePaths<App>,
|
|
75
|
+
Method extends string = "post",
|
|
76
|
+
>(): FormHandle<App, Path, Method> {
|
|
77
|
+
const handle = {
|
|
78
|
+
field: (name: string, props?: FieldProps): FieldProps & { name: string } => ({
|
|
79
|
+
...(props ?? {}),
|
|
80
|
+
name,
|
|
81
|
+
}),
|
|
82
|
+
read: (form: FormData, name: string): FormEntry => form.get(name),
|
|
83
|
+
readAll: (form: FormData, name: string): FormEntries => form.getAll(name),
|
|
84
|
+
}
|
|
85
|
+
return handle as unknown as FormHandle<App, Path, Method>
|
|
86
|
+
}
|