@rsc-kit/press 0.30.47 → 0.30.49

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.
@@ -1,31 +1,47 @@
1
- import type { FieldOverride, FormSchema, ValuesOf } from '../resource.js';
1
+ import type { ReactNode } from 'react';
2
+ import type { Field, FieldOverride, FormSchema, ValuesOf } from '../resource.js';
3
+ import type { HostAction, HostFields } from './host.js';
2
4
  import type { FormsTheme } from './theme.js';
3
- export interface FormDefinition<Schema extends FormSchema> {
5
+ interface FormOptions {
4
6
  /** Unique in the app: the form is found by it when it is sent. Lowercase letters, digits and dashes. */
5
7
  name: string;
8
+ /** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */
9
+ theme: FormsTheme;
10
+ /** The submit button's words. Send by default. */
11
+ submit?: string;
12
+ /** What the visitor reads in place of the form once it is sent. */
13
+ done?: string;
14
+ /** Where the visitor goes once it is sent, instead of `done`. */
15
+ redirect?: string;
16
+ }
17
+ export interface FormDefinition<Schema extends FormSchema> extends FormOptions {
6
18
  /** Any Standard Schema of an object: z.object(), v.object(), type({...}). It checks every submission, on the server. */
7
19
  form: Schema;
8
20
  /** Labels, descriptions and controls the schema cannot say - or every field, for a schema that does not describe itself as JSON Schema. */
9
21
  fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>;
10
- /** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */
11
- theme: FormsTheme;
12
22
  /**
13
23
  * What a submission that passed the schema does: store it, send it, call a
14
24
  * backend. Throw an ActionValidationError - or let a backend's 422 through -
15
25
  * and its messages land under the fields.
16
26
  */
17
27
  handle: (values: ValuesOf<Schema>) => void | Promise<void>;
18
- /** The submit button's words. Send by default. */
19
- submit?: string;
20
- /** What the visitor reads in place of the form once it is sent. */
21
- done?: string;
22
- /** Where the visitor goes once it is sent, instead of `done`. */
23
- redirect?: string;
28
+ }
29
+ export interface HostFormDefinition<Action extends HostAction> extends FormOptions {
30
+ /**
31
+ * The backend action the form calls, as the build generated it:
32
+ * `import { contactSend } from '../server-actions.generated'`. Its fields are
33
+ * drawn from what the backend says it takes, and it does all the checking:
34
+ * a Laravel FormRequest, a Go struct. Its refusals land under the fields.
35
+ */
36
+ action: Action;
37
+ /** Labels, descriptions and controls the backend's type cannot say. */
38
+ fields?: Partial<Record<HostFields<Action>, FieldOverride>>;
24
39
  }
25
40
  /**
26
- * A form anyone may fill in - a contact form, a sign-up, a survey - from a
27
- * Standard Schema, checked by the browser first and by the schema on the
28
- * server always. Place it in any page:
41
+ * A form anyone may fill in - a contact form, a sign-up, a survey. Either from
42
+ * a Standard Schema, checked by the browser first and by the schema on the
43
+ * server always, or from a backend function, checked by the browser first and
44
+ * by the backend always. Place it in any page:
29
45
  *
30
46
  * export default function Contact() {
31
47
  * return <main><h1>Contact us</h1><contact.Form /></main>
@@ -34,9 +50,12 @@ export interface FormDefinition<Schema extends FormSchema> {
34
50
  * The form reads nothing from the request, so the page around it can still
35
51
  * be stored by the build.
36
52
  */
37
- export declare function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>): {
53
+ export declare function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>): PublicFormHandle;
54
+ export declare function defineForm<Action extends HostAction>(definition: HostFormDefinition<Action>): PublicFormHandle;
55
+ export interface PublicFormHandle {
38
56
  name: string;
39
- fields: import("../resource.js").Field[];
40
- Form: () => import("react").JSX.Element;
41
- };
42
- export type PublicForm = ReturnType<typeof defineForm>;
57
+ fields: Field[];
58
+ Form: () => ReactNode;
59
+ }
60
+ export type PublicForm = PublicFormHandle;
61
+ export {};
@@ -1,20 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { fieldsOf } from '../resource.js';
3
3
  import { submitForm } from './actions.js';
4
+ import { hostSchema, requireDrawable } from './host.js';
4
5
  import { HONEYPOT } from './honeypot.js';
5
6
  import { registerForm } from './registry.js';
6
- /**
7
- * A form anyone may fill in - a contact form, a sign-up, a survey - from a
8
- * Standard Schema, checked by the browser first and by the schema on the
9
- * server always. Place it in any page:
10
- *
11
- * export default function Contact() {
12
- * return <main><h1>Contact us</h1><contact.Form /></main>
13
- * }
14
- *
15
- * The form reads nothing from the request, so the page around it can still
16
- * be stored by the build.
17
- */
18
7
  export function defineForm(definition) {
19
8
  if (!/^[a-z0-9-]+$/.test(definition.name)) {
20
9
  throw new Error(`[forms] a form's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`);
@@ -22,23 +11,56 @@ export function defineForm(definition) {
22
11
  if (definition.done !== undefined && definition.redirect !== undefined) {
23
12
  throw new Error(`[forms] the form ${definition.name} has both done and redirect; a sent form shows one or goes to the other`);
24
13
  }
25
- const fields = fieldsOf(definition.form, definition.fields ?? {});
26
- if (fields.some((field) => field.name === HONEYPOT)) {
27
- throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`);
14
+ const { action } = definition;
15
+ if (typeof action === 'string') {
16
+ throw new Error(`[forms] the form ${definition.name}'s action is the generated action now, not its name: ` +
17
+ `import { contactSend } from '../server-actions.generated', then action: contactSend, for ${JSON.stringify(action)}`);
28
18
  }
19
+ if ((action === undefined) === (definition.form === undefined)) {
20
+ throw new Error(`[forms] the form ${definition.name} needs a form schema with handle, or an action, and not both`);
21
+ }
22
+ const overrides = (definition.fields ?? {});
23
+ const schema = action === undefined ? definition.form : hostSchema(action);
24
+ let drawn = null;
25
+ function fields() {
26
+ if (drawn)
27
+ return drawn;
28
+ if (action !== undefined)
29
+ requireDrawable(definition.name, action, overrides);
30
+ const fields = fieldsOf(schema, overrides);
31
+ if (fields.some((field) => field.name === HONEYPOT)) {
32
+ throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`);
33
+ }
34
+ return (drawn = fields);
35
+ }
36
+ // A backend's types arrive with the server entry, which runs after this
37
+ // module: those fields wait for the first render. A schema's fail here.
38
+ if (action === undefined)
39
+ fields();
40
+ const handle = action === undefined
41
+ ? definition.handle
42
+ : async (values) => void (await action(values));
29
43
  registerForm({
30
44
  name: definition.name,
31
- schema: definition.form,
32
- fields,
33
- handle: definition.handle,
45
+ schema,
46
+ get fields() {
47
+ return fields();
48
+ },
49
+ handle,
34
50
  redirect: definition.redirect ?? null,
35
51
  });
36
52
  const theme = definition.theme;
37
53
  const submit = definition.submit ?? 'Send';
38
54
  const done = definition.redirect === undefined ? (definition.done ?? 'Thank you. It was sent.') : undefined;
39
55
  function Form() {
40
- return (_jsx(theme.PublicForm, { action: submitForm.bind(null, definition.name), fields: fields, submit: submit, ...(done === undefined ? {} : { done }), honeypot: HONEYPOT }));
56
+ return (_jsx(theme.PublicForm, { action: submitForm.bind(null, definition.name), fields: fields(), submit: submit, ...(done === undefined ? {} : { done }), honeypot: HONEYPOT }));
41
57
  }
42
- return { name: definition.name, fields, Form };
58
+ return {
59
+ name: definition.name,
60
+ get fields() {
61
+ return fields();
62
+ },
63
+ Form,
64
+ };
43
65
  }
44
66
  //# sourceMappingURL=form.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"form.js","sourceRoot":"","sources":["../../src/forms/form.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA0B5C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAA4B,UAAkC;IACtF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,kEAAkE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtH,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS,IAAI,UAAU,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,CAAC,IAAI,yEAAyE,CAAC,CAAA;IAC/H,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IAEjE,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,CAAC,IAAI,sBAAsB,QAAQ,uCAAuC,CAAC,CAAA;IAC3H,CAAC;IAED,YAAY,CAAC;QACX,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,MAAM,EAAE,UAAU,CAAC,IAAI;QACvB,MAAM;QACN,MAAM,EAAE,UAAU,CAAC,MAAmE;QACtF,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,IAAI;KACtC,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,IAAI,MAAM,CAAA;IAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,IAAI,yBAAyB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAE3G,SAAS,IAAI;QACX,OAAO,CACL,KAAC,KAAK,CAAC,UAAU,IACf,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,KACV,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EACxC,QAAQ,EAAE,QAAQ,GAClB,CACH,CAAA;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAChD,CAAC","sourcesContent":["import { fieldsOf } from '../resource.js'\nimport type { FieldOverride, FormSchema, ValuesOf } from '../resource.js'\nimport { submitForm } from './actions.js'\nimport { HONEYPOT } from './honeypot.js'\nimport { registerForm } from './registry.js'\nimport type { FormsTheme } from './theme.js'\n\nexport interface FormDefinition<Schema extends FormSchema> {\n /** Unique in the app: the form is found by it when it is sent. Lowercase letters, digits and dashes. */\n name: string\n /** Any Standard Schema of an object: z.object(), v.object(), type({...}). It checks every submission, on the server. */\n form: Schema\n /** Labels, descriptions and controls the schema cannot say - or every field, for a schema that does not describe itself as JSON Schema. */\n fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>\n /** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */\n theme: FormsTheme\n /**\n * What a submission that passed the schema does: store it, send it, call a\n * backend. Throw an ActionValidationError - or let a backend's 422 through -\n * and its messages land under the fields.\n */\n handle: (values: ValuesOf<Schema>) => void | Promise<void>\n /** The submit button's words. Send by default. */\n submit?: string\n /** What the visitor reads in place of the form once it is sent. */\n done?: string\n /** Where the visitor goes once it is sent, instead of `done`. */\n redirect?: string\n}\n\n/**\n * A form anyone may fill in - a contact form, a sign-up, a survey - from a\n * Standard Schema, checked by the browser first and by the schema on the\n * server always. Place it in any page:\n *\n * export default function Contact() {\n * return <main><h1>Contact us</h1><contact.Form /></main>\n * }\n *\n * The form reads nothing from the request, so the page around it can still\n * be stored by the build.\n */\nexport function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>) {\n if (!/^[a-z0-9-]+$/.test(definition.name)) {\n throw new Error(`[forms] a form's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`)\n }\n\n if (definition.done !== undefined && definition.redirect !== undefined) {\n throw new Error(`[forms] the form ${definition.name} has both done and redirect; a sent form shows one or goes to the other`)\n }\n\n const fields = fieldsOf(definition.form, definition.fields ?? {})\n\n if (fields.some((field) => field.name === HONEYPOT)) {\n throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`)\n }\n\n registerForm({\n name: definition.name,\n schema: definition.form,\n fields,\n handle: definition.handle as (values: Record<string, unknown>) => void | Promise<void>,\n redirect: definition.redirect ?? null,\n })\n\n const theme = definition.theme\n const submit = definition.submit ?? 'Send'\n const done = definition.redirect === undefined ? (definition.done ?? 'Thank you. It was sent.') : undefined\n\n function Form() {\n return (\n <theme.PublicForm\n action={submitForm.bind(null, definition.name)}\n fields={fields}\n submit={submit}\n {...(done === undefined ? {} : { done })}\n honeypot={HONEYPOT}\n />\n )\n }\n\n return { name: definition.name, fields, Form }\n}\n\nexport type PublicForm = ReturnType<typeof defineForm>\n"]}
1
+ {"version":3,"file":"form.js","sourceRoot":"","sources":["../../src/forms/form.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport { fieldsOf } from '../resource.js'\nimport type { Field, FieldOverride, FormSchema, ValuesOf } from '../resource.js'\nimport { submitForm } from './actions.js'\nimport { hostSchema, requireDrawable } from './host.js'\nimport type { HostAction, HostFields } from './host.js'\nimport { HONEYPOT } from './honeypot.js'\nimport { registerForm } from './registry.js'\nimport type { FormsTheme } from './theme.js'\n\ninterface FormOptions {\n /** Unique in the app: the form is found by it when it is sent. Lowercase letters, digits and dashes. */\n name: string\n /** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */\n theme: FormsTheme\n /** The submit button's words. Send by default. */\n submit?: string\n /** What the visitor reads in place of the form once it is sent. */\n done?: string\n /** Where the visitor goes once it is sent, instead of `done`. */\n redirect?: string\n}\n\nexport interface FormDefinition<Schema extends FormSchema> extends FormOptions {\n /** Any Standard Schema of an object: z.object(), v.object(), type({...}). It checks every submission, on the server. */\n form: Schema\n /** Labels, descriptions and controls the schema cannot say - or every field, for a schema that does not describe itself as JSON Schema. */\n fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>\n /**\n * What a submission that passed the schema does: store it, send it, call a\n * backend. Throw an ActionValidationError - or let a backend's 422 through -\n * and its messages land under the fields.\n */\n handle: (values: ValuesOf<Schema>) => void | Promise<void>\n}\n\nexport interface HostFormDefinition<Action extends HostAction> extends FormOptions {\n /**\n * The backend action the form calls, as the build generated it:\n * `import { contactSend } from '../server-actions.generated'`. Its fields are\n * drawn from what the backend says it takes, and it does all the checking:\n * a Laravel FormRequest, a Go struct. Its refusals land under the fields.\n */\n action: Action\n /** Labels, descriptions and controls the backend's type cannot say. */\n fields?: Partial<Record<HostFields<Action>, FieldOverride>>\n}\n\n/**\n * A form anyone may fill in - a contact form, a sign-up, a survey. Either from\n * a Standard Schema, checked by the browser first and by the schema on the\n * server always, or from a backend function, checked by the browser first and\n * by the backend always. Place it in any page:\n *\n * export default function Contact() {\n * return <main><h1>Contact us</h1><contact.Form /></main>\n * }\n *\n * The form reads nothing from the request, so the page around it can still\n * be stored by the build.\n */\nexport function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>): PublicFormHandle\nexport function defineForm<Action extends HostAction>(definition: HostFormDefinition<Action>): PublicFormHandle\nexport function defineForm(definition: (FormDefinition<FormSchema> | HostFormDefinition<HostAction>) & { form?: FormSchema; action?: HostAction }): PublicFormHandle {\n if (!/^[a-z0-9-]+$/.test(definition.name)) {\n throw new Error(`[forms] a form's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`)\n }\n\n if (definition.done !== undefined && definition.redirect !== undefined) {\n throw new Error(`[forms] the form ${definition.name} has both done and redirect; a sent form shows one or goes to the other`)\n }\n\n const { action } = definition\n\n if (typeof action === 'string') {\n throw new Error(\n `[forms] the form ${definition.name}'s action is the generated action now, not its name: ` +\n `import { contactSend } from '../server-actions.generated', then action: contactSend, for ${JSON.stringify(action)}`,\n )\n }\n\n if ((action === undefined) === (definition.form === undefined)) {\n throw new Error(`[forms] the form ${definition.name} needs a form schema with handle, or an action, and not both`)\n }\n\n const overrides = (definition.fields ?? {}) as Record<string, FieldOverride>\n const schema = action === undefined ? definition.form! : hostSchema(action)\n let drawn: Field[] | null = null\n\n function fields(): Field[] {\n if (drawn) return drawn\n\n if (action !== undefined) requireDrawable(definition.name, action, overrides)\n\n const fields = fieldsOf(schema, overrides)\n\n if (fields.some((field) => field.name === HONEYPOT)) {\n throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`)\n }\n\n return (drawn = fields)\n }\n\n // A backend's types arrive with the server entry, which runs after this\n // module: those fields wait for the first render. A schema's fail here.\n if (action === undefined) fields()\n\n const handle = action === undefined\n ? ((definition as FormDefinition<FormSchema>).handle as (values: Record<string, unknown>) => void | Promise<void>)\n : async (values: Record<string, unknown>) => void (await (action as (values: Record<string, unknown>) => Promise<unknown>)(values))\n\n registerForm({\n name: definition.name,\n schema,\n get fields() {\n return fields()\n },\n handle,\n redirect: definition.redirect ?? null,\n })\n\n const theme = definition.theme\n const submit = definition.submit ?? 'Send'\n const done = definition.redirect === undefined ? (definition.done ?? 'Thank you. It was sent.') : undefined\n\n function Form() {\n return (\n <theme.PublicForm\n action={submitForm.bind(null, definition.name)}\n fields={fields()}\n submit={submit}\n {...(done === undefined ? {} : { done })}\n honeypot={HONEYPOT}\n />\n )\n }\n\n return {\n name: definition.name,\n get fields() {\n return fields()\n },\n Form,\n }\n}\n\nexport interface PublicFormHandle {\n name: string\n fields: Field[]\n Form: () => ReactNode\n}\n\nexport type PublicForm = PublicFormHandle\n"]}
@@ -0,0 +1,24 @@
1
+ import type { FormFields } from '@rsc-kit/core/form';
2
+ import type { FieldOverride, FormSchema } from '../resource.js';
3
+ /**
4
+ * A form with no schema of its own: defineForm({ action: contactSend }), with
5
+ * the action the build generated for the backend's Contact.send. The backend
6
+ * said what the function takes when it wrote rsc-host.json, and the build put
7
+ * that in the server entry, so the fields come from there and every check is
8
+ * the backend's.
9
+ */
10
+ /** A backend action the build generated: import { contactSend } from '../server-actions.generated'. */
11
+ export type HostAction = (...args: never[]) => Promise<unknown>;
12
+ /** The names of the fields an action's form has, as its generated stub declares them. */
13
+ export type HostFields<Action> = Action extends (form: FormFields<infer Name>) => Promise<unknown> ? string extends Name ? string : Name : string;
14
+ /** The backend function a generated action calls, or null for any other function. */
15
+ export declare function targetOf(action: HostAction): string | null;
16
+ /**
17
+ * A Standard Schema that describes the backend's input and refuses nothing:
18
+ * the rest of the form is the same as for any schema, and the checking is the
19
+ * function's. The browser's checks come from the same description. Read when
20
+ * the form first draws, by which time the server entry has run.
21
+ */
22
+ export declare function hostSchema(action: HostAction): FormSchema;
23
+ /** A form must be able to draw what it was given, or say so rather than draw a text box. */
24
+ export declare function requireDrawable(form: string, action: HostAction, overrides: Record<string, FieldOverride>): void;
@@ -0,0 +1,47 @@
1
+ function hostForms() {
2
+ const forms = globalThis[Symbol.for('rsc-kit.host-forms')];
3
+ if (!forms)
4
+ throw new Error('[forms] a form with an action needs the rsc-kit server entry, which says what the backend takes');
5
+ return forms;
6
+ }
7
+ /** The backend function a generated action calls, or null for any other function. */
8
+ export function targetOf(action) {
9
+ return hostForms().actions.get(action) ?? null;
10
+ }
11
+ /**
12
+ * A Standard Schema that describes the backend's input and refuses nothing:
13
+ * the rest of the form is the same as for any schema, and the checking is the
14
+ * function's. The browser's checks come from the same description. Read when
15
+ * the form first draws, by which time the server entry has run.
16
+ */
17
+ export function hostSchema(action) {
18
+ return {
19
+ '~standard': {
20
+ version: 1,
21
+ vendor: 'rsc-kit',
22
+ validate: (value) => ({ value: value }),
23
+ jsonSchema: { input: () => hostForms().inputs[targetOf(action) ?? ''] },
24
+ },
25
+ };
26
+ }
27
+ /** A form must be able to draw what it was given, or say so rather than draw a text box. */
28
+ export function requireDrawable(form, action, overrides) {
29
+ const target = targetOf(action);
30
+ if (target === null) {
31
+ throw new Error(`[forms] the form ${form}'s action ${action.name || 'function'} is not one the build generated for the backend. ` +
32
+ "Import it from the generated server-actions module: import { contactSend } from '../server-actions.generated'.");
33
+ }
34
+ const input = hostForms().inputs[target];
35
+ if (!input) {
36
+ if (Object.keys(overrides).length > 0)
37
+ return;
38
+ throw new Error(`[forms] the form ${form} calls ${target}, and the backend does not say it takes one object with named fields. ` +
39
+ 'Take a FormRequest in Laravel or a struct in Go, or list every field in fields.');
40
+ }
41
+ for (const [field, schema] of Object.entries(input.properties ?? {})) {
42
+ if (!schema.type && !schema.enum && !schema.anyOf && !overrides[field]?.type) {
43
+ throw new Error(`[forms] the form ${form} cannot tell what ${target}'s ${field} is, which a file or an open type would be. Say it in fields.`);
44
+ }
45
+ }
46
+ }
47
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/forms/host.ts"],"names":[],"mappings":"AA4BA,SAAS,SAAS;IAChB,MAAM,KAAK,GAAI,UAAsC,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAA0B,CAAA;IAEhH,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iGAAiG,CAAC,CAAA;IAE9H,OAAO,KAAK,CAAA;AACd,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,MAAkB;IACzC,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAA;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,OAAO;QACL,WAAW,EAAE;YACX,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,SAAS;YACjB,QAAQ,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAgC,EAAE,CAAC;YAC3E,UAAU,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE;SACxE;KACuB,CAAA;AAC5B,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,MAAkB,EAAE,SAAwC;IACxG,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;IAE/B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,aAAa,MAAM,CAAC,IAAI,IAAI,UAAU,mDAAmD;YAC/G,gHAAgH,CACnH,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAExC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QAE7C,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,UAAU,MAAM,wEAAwE;YAC9G,iFAAiF,CACpF,CAAA;IACH,CAAC;IAED,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;YAC7E,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,qBAAqB,MAAM,MAAM,KAAK,+DAA+D,CAAC,CAAA;QAChJ,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type { FormFields } from '@rsc-kit/core/form'\nimport type { FieldOverride, FormSchema } from '../resource.js'\n\n/**\n * A form with no schema of its own: defineForm({ action: contactSend }), with\n * the action the build generated for the backend's Contact.send. The backend\n * said what the function takes when it wrote rsc-host.json, and the build put\n * that in the server entry, so the fields come from there and every check is\n * the backend's.\n */\n\n/** A backend action the build generated: import { contactSend } from '../server-actions.generated'. */\nexport type HostAction = (...args: never[]) => Promise<unknown>\n\n/** The names of the fields an action's form has, as its generated stub declares them. */\nexport type HostFields<Action> = Action extends (form: FormFields<infer Name>) => Promise<unknown>\n ? string extends Name\n ? string\n : Name\n : string\n\ninterface HostForms {\n /** Each generated action, and the backend function it calls. */\n actions: Map<unknown, string>\n /** What each backend function takes, by its name. */\n inputs: Record<string, { properties?: Record<string, Record<string, unknown>> }>\n}\n\nfunction hostForms(): HostForms {\n const forms = (globalThis as Record<symbol, unknown>)[Symbol.for('rsc-kit.host-forms')] as HostForms | undefined\n\n if (!forms) throw new Error('[forms] a form with an action needs the rsc-kit server entry, which says what the backend takes')\n\n return forms\n}\n\n/** The backend function a generated action calls, or null for any other function. */\nexport function targetOf(action: HostAction): string | null {\n return hostForms().actions.get(action) ?? null\n}\n\n/**\n * A Standard Schema that describes the backend's input and refuses nothing:\n * the rest of the form is the same as for any schema, and the checking is the\n * function's. The browser's checks come from the same description. Read when\n * the form first draws, by which time the server entry has run.\n */\nexport function hostSchema(action: HostAction): FormSchema {\n return {\n '~standard': {\n version: 1,\n vendor: 'rsc-kit',\n validate: (value: unknown) => ({ value: value as Record<string, unknown> }),\n jsonSchema: { input: () => hostForms().inputs[targetOf(action) ?? ''] },\n },\n } as unknown as FormSchema\n}\n\n/** A form must be able to draw what it was given, or say so rather than draw a text box. */\nexport function requireDrawable(form: string, action: HostAction, overrides: Record<string, FieldOverride>): void {\n const target = targetOf(action)\n\n if (target === null) {\n throw new Error(\n `[forms] the form ${form}'s action ${action.name || 'function'} is not one the build generated for the backend. ` +\n \"Import it from the generated server-actions module: import { contactSend } from '../server-actions.generated'.\",\n )\n }\n\n const input = hostForms().inputs[target]\n\n if (!input) {\n if (Object.keys(overrides).length > 0) return\n\n throw new Error(\n `[forms] the form ${form} calls ${target}, and the backend does not say it takes one object with named fields. ` +\n 'Take a FormRequest in Laravel or a struct in Go, or list every field in fields.',\n )\n }\n\n for (const [field, schema] of Object.entries(input.properties ?? {})) {\n if (!schema.type && !schema.enum && !schema.anyOf && !overrides[field]?.type) {\n throw new Error(`[forms] the form ${form} cannot tell what ${target}'s ${field} is, which a file or an open type would be. Say it in fields.`)\n }\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
1
  export { defineForm } from './form.js';
2
- export type { FormDefinition, PublicForm } from './form.js';
2
+ export type { FormDefinition, HostFormDefinition, PublicForm } from './form.js';
3
3
  export type { FormsTheme, PublicFormProps } from './theme.js';
4
+ export type { HostAction, HostFields } from './host.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA","sourcesContent":["export { defineForm } from './form.js'\nexport type { FormDefinition, PublicForm } from './form.js'\nexport type { FormsTheme, PublicFormProps } from './theme.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA","sourcesContent":["export { defineForm } from './form.js'\nexport type { FormDefinition, HostFormDefinition, PublicForm } from './form.js'\nexport type { FormsTheme, PublicFormProps } from './theme.js'\nexport type { HostAction, HostFields } from './host.js'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/press",
3
- "version": "0.30.47",
3
+ "version": "0.30.49",
4
4
  "description": "Admin panels, public forms and pages built from blocks for rsc-kit: resources, data sources, tenants and plugins, with the drawing in a theme - shadcn/ui is the one that ships.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "prepack": "bun run build"
51
51
  },
52
52
  "peerDependencies": {
53
- "@rsc-kit/core": "^0.30.47",
53
+ "@rsc-kit/core": "^0.30.49",
54
54
  "react": "^19.2.0",
55
55
  "react-dom": "^19.2.0",
56
56
  "tailwindcss": "^4.0.0"