@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.
@@ -1 +1 @@
1
- {"version":3,"file":"Form.js","sourceRoot":"","sources":["../../src/js/Form.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { Href } from \"../routes.js\";\nimport {\n type FormHTMLAttributes,\n type FormEvent,\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n useTransition,\n} from \"react\";\nimport { ServerValidationError, ServerDumpError } from \"./errors\";\nimport { validateWith } from \"./standardSchema\";\nimport type { StandardSchemaV1 } from \"./standardSchema\";\n\ntype PrefetchStrategy = \"hover\" | \"mount\" | \"none\";\n\ninterface FormRenderProps<T extends Record<string, unknown> = Record<string, unknown>> {\n pending: boolean;\n data: T;\n errors: Record<string, string[]>;\n error: (field: keyof T & string) => string | undefined;\n clearErrors: (...fields: (keyof T & string)[]) => void;\n reset: () => void;\n}\n\ninterface FormProps<T extends Record<string, unknown> = Record<string, unknown>>\n extends Omit<FormHTMLAttributes<HTMLFormElement>, \"action\" | \"method\" | \"children\" | \"onSubmit\" | \"onError\"> {\n action: Href | ((formData: FormData) => Promise<unknown>);\n method?: \"get\" | \"post\";\n prefetch?: PrefetchStrategy;\n cacheFor?: number;\n replace?: boolean;\n preserveScroll?: boolean;\n resetOnSuccess?: boolean;\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, so a mistake\n * costs no round trip. It is a courtesy and not a control: the same action\n * is reachable without this form, so the server still has to check.\n */\n schema?: StandardSchemaV1;\n /** Transform form data before submitting to the server action. */\n transform?: (data: T) => Record<string, unknown>;\n /** Called inside the transition with typed form data. Use it to call your useOptimistic setter. */\n optimistic?: (data: T) => void;\n onSuccess?: (result: unknown) => void;\n /**\n * Called when a submit does not succeed.\n *\n * Validation failures arrive as field errors, with no second argument.\n * Anything else arrives as an empty error map and the thrown value — there\n * are no field errors to report, but the form still has to say so.\n */\n onError?: (errors: Record<string, string[]>, error?: unknown) => void;\n onSubmit?: (formData: FormData) => void | false;\n children: ReactNode | ((form: FormRenderProps<T>) => ReactNode);\n}\n\n/**\n * What an action built with createActionClient answers with.\n *\n * Recognised rather than thrown, because React serialises a rejected server\n * action opaquely: production strips the message and the fields it named are\n * gone. A returned object crosses intact, so a form reads it.\n */\nfunction resultOf(value: unknown): { errors?: Record<string, string[]>; serverError?: string } | null {\n if (typeof value !== 'object' || value === null) return null\n\n const result = value as { validationErrors?: Record<string, string[]>; serverError?: string }\n\n if (result.validationErrors) return { errors: result.validationErrors }\n if (result.serverError) return { serverError: result.serverError }\n\n return null\n}\n\nconst FormStatusContext = createContext<FormRenderProps>({\n pending: false,\n data: {},\n errors: {},\n error: () => undefined,\n clearErrors: () => {},\n reset: () => {},\n});\n\nexport function useFormStatus<T extends Record<string, unknown> = Record<string, unknown>>(): FormRenderProps<T> {\n return useContext(FormStatusContext) as FormRenderProps<T>;\n}\n\nfunction formDataToObject<T extends Record<string, unknown>>(formData: FormData): T {\n const obj: Record<string, unknown> = {};\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\") {\n obj[key] = value;\n }\n }\n return obj as T;\n}\n\n/**\n * Also exported by name, and re-exported below, because both spellings are in\n * use: `import Form from` and `import { Form } from`.\n */\nexport default function Form<T extends Record<string, unknown> = Record<string, unknown>>({\n action,\n method: methodProp,\n prefetch = \"hover\",\n cacheFor,\n replace = false,\n preserveScroll = false,\n resetOnSuccess = true,\n schema,\n transform,\n optimistic,\n onSuccess,\n onError,\n onSubmit,\n children,\n ...rest\n}: FormProps<T>) {\n const isGetForm = typeof action === \"string\";\n const method = methodProp ?? (isGetForm ? \"get\" : \"post\");\n const [errors, setErrors] = useState<Record<string, string[]>>({});\n const [currentData, setCurrentData] = useState<T>({} as T);\n const [isPending, startTransition] = useTransition();\n const formRef = useRef<HTMLFormElement>(null);\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 resetForm = useCallback(() => {\n formRef.current?.reset();\n setErrors({});\n setCurrentData({} as T);\n }, []);\n\n useEffect(() => {\n if (isGetForm && prefetch === \"mount\") {\n const fn = (window as any).__rsc_prefetch;\n fn?.(action, cacheFor);\n }\n }, [isGetForm, prefetch, action, cacheFor]);\n\n const doPrefetch = useCallback(() => {\n if (!isGetForm) return;\n const fn = (window as any).__rsc_prefetch;\n fn?.(action as string, cacheFor);\n }, [isGetForm, action, cacheFor]);\n\n const handleSubmit = useCallback(\n async (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n const formData = new FormData(e.currentTarget);\n const data = formDataToObject<T>(formData);\n setCurrentData(data);\n\n if (onSubmit?.(formData) === false) {\n return;\n }\n\n if (isGetForm && method === \"get\") {\n const url = new URL(action as string, window.location.origin);\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\" && value !== \"\") {\n url.searchParams.set(key, value);\n }\n }\n\n const path = url.pathname + url.search;\n const shell = action as string;\n const nav = (window as any).__rsc_navigate;\n const prefetched = (window as any).__rsc_is_prefetched;\n\n // The route without its query is that route's shell: the layout, the\n // chrome, and whatever it renders with nothing to show yet. Prefetching\n // on hover put it in the cache, so going there first costs no request\n // and puts the page on screen while the real query is still running.\n //\n // The query itself is never prefetched. It is the expensive half, and\n // hovering a search button is not a reason to run someone's search.\n if (path !== shell && prefetched?.(shell)) {\n // Awaited rather than raced: navigate() aborts whatever is in flight,\n // so starting the real one first would cancel the shell before it\n // could render. It is a cache hit, so this is a render, not a wait.\n Promise.resolve(nav?.(shell, { replace, preserveScroll })).then(() =>\n // Replaces, so the shell does not become a back-button stop of its\n // own — the pair leaves exactly one entry behind.\n nav?.(path, { replace: true, preserveScroll })\n );\n\n return;\n }\n\n nav?.(path, { replace, preserveScroll });\n return;\n }\n\n const serverAction = action as (formData: FormData) => Promise<unknown>;\n\n // Apply transform — rebuild FormData from transformed values\n if (transform) {\n const transformed = transform(data);\n for (const key of [...formData.keys()]) {\n formData.delete(key);\n }\n for (const [key, val] of Object.entries(transformed)) {\n if (val === null || val === undefined) continue;\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\n setErrors({});\n\n // Before the transition, so a failure neither runs the optimistic\n // update nor leaves the form looking like it is submitting.\n const invalid = await validateWith(schema, data);\n\n if (invalid) {\n setErrors(invalid);\n onError?.(invalid);\n\n return;\n }\n\n startTransition(async () => {\n try {\n // Call optimistic updater inside the transition so React's\n // useOptimistic picks it up and auto-reverts on settle.\n optimistic?.(data);\n\n const result = await serverAction(formData);\n const refused = resultOf(result);\n\n if (refused) {\n if (refused.errors) {\n setErrors(refused.errors);\n onError?.(refused.errors);\n } else {\n onError?.({}, new Error(refused.serverError));\n }\n\n return;\n }\n\n if (resetOnSuccess) {\n formRef.current?.reset();\n setCurrentData({} as T);\n }\n\n setErrors({});\n onSuccess?.(result);\n } catch (err) {\n if (err instanceof ServerValidationError) {\n setErrors(err.errors);\n onError?.(err.errors);\n } else if (err instanceof ServerDumpError) {\n // Dump overlay is already shown — silently swallow\n } else if (onError) {\n // Not rethrown. A rejected action never settles, and until it\n // settles React keeps the optimistic update on screen — so an\n // unexpected failure left the row showing as though the write had\n // worked. Settling is what takes it back.\n onError({}, err);\n } else {\n // Nothing is handling it, and swallowing here would lose it\n // entirely — the reason this used to rethrow.\n console.error('[rsc-kit] form submit failed', err);\n }\n }\n });\n },\n [action, isGetForm, method, replace, preserveScroll, resetOnSuccess, schema, transform, optimistic, onSubmit, onSuccess, onError]\n );\n\n const formStatus: FormRenderProps<T> = {\n pending: isPending,\n data: currentData,\n errors,\n error,\n clearErrors,\n reset: resetForm,\n };\n\n return (\n <FormStatusContext.Provider value={formStatus as FormRenderProps}>\n <form\n ref={formRef}\n onSubmit={handleSubmit}\n onMouseEnter={prefetch === \"hover\" ? doPrefetch : undefined}\n data-pending={isPending ? \"\" : undefined}\n {...rest}\n >\n {typeof children === \"function\" ? children(formStatus) : children}\n </form>\n </FormStatusContext.Provider>\n );\n}\n\n// Also by name, because both spellings are in use: `import Form` and\n// `import { Form }`. This was a re-export of \"./Form\" — from inside Form.tsx,\n// so the module named itself. It survived on the bundler tolerating a circular\n// self-reference, left over from when a separate barrel re-exported a\n// FormComponent.tsx that a case-insensitive filesystem would not let be called\n// Form.ts. There is one file now, and it can just export what it declares.\nexport { Form };\nexport { useForm } from \"./useForm\";\n"]}
1
+ {"version":3,"file":"Form.js","sourceRoot":"","sources":["../../src/js/Form.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { Href } from \"../routes.js\";\nimport {\n type FormHTMLAttributes,\n type FormEvent,\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n useTransition,\n} from \"react\";\nimport { ServerValidationError, ServerDumpError } from \"./errors\";\nimport { buildFormData } from \"./formEncoding\";\nimport { createFormStore } from \"./formStore\";\nimport type { FormStore } from \"./formStore\";\nimport { validateWith } from \"./standardSchema\";\nimport type { StandardSchemaV1 } from \"./standardSchema\";\n\ntype PrefetchStrategy = \"hover\" | \"mount\" | \"none\";\n\n/**\n * What `field(name)` hands a control, spread straight onto it.\n *\n * The same four things react-hook-form's `<Controller>` gives, because it is\n * the same job: a component with no native control behind it needs a value and\n * a way to report a new one.\n */\ninterface FieldBinding<V> {\n name: string;\n value: V;\n /**\n * Either shape: a DOM event, or the value itself.\n *\n * A native input passes the event; a Radix select or a rich editor passes\n * what was chosen. A binder understanding only one of them would work on\n * half the controls anyone actually uses.\n */\n onChange: (next: V | { target: { value: V } }) => void;\n onBlur: () => void;\n}\n\n/**\n * What is known about one field, separately from what is spread onto it.\n *\n * Two objects rather than one, which is react-hook-form's split and it is right\n * for a mechanical reason: `touched` and `invalid` are not DOM attributes, so a\n * single spreadable object would put them on the element and React would warn\n * about every one.\n */\ninterface FieldState {\n /** Whether it has been left at least once. */\n touched: boolean;\n /** Whether it currently has errors. */\n invalid: boolean;\n /** Its messages, ready for a `<FieldError>`. */\n errors: string[];\n}\n\ninterface FormRenderProps<T extends Record<string, unknown> = Record<string, unknown>> {\n pending: boolean;\n data: T;\n /**\n * Keyed by field name, so a typo is a type error rather than undefined.\n *\n * Partial because most fields have none, and nested paths join with dots —\n * `errors['address.city']`, which is the key a Standard Schema issue for that\n * field produces.\n */\n errors: Partial<Record<keyof T & string, string[]>> & Record<string, string[] | undefined>;\n error: (field: keyof T & string) => string | undefined;\n clearErrors: (...fields: (keyof T & string)[]) => void;\n reset: () => void;\n /** Whether the last submit was accepted. */\n succeeded: boolean;\n /**\n * The same thing, for two seconds.\n *\n * The \"Saved ✓\" that appears and fades. Worth having as state rather than a\n * timer in every form that wants one, because the timer has to be cleared\n * when the component goes away and that is the part people forget.\n */\n recentlySucceeded: boolean;\n /**\n * Bind one field so this component holds its value.\n *\n * Most fields need nothing: they are uncontrolled, the DOM holds the value,\n * and it is read back as FormData on submit. Reach for this when the DOM\n * cannot hold it for you — a control with no native element behind it, or a\n * value you want to read as it is typed:\n *\n * <Input {...field('title')} />\n * <span>{field('body').value.length}/100</span>\n *\n * A bound field is still an ordinary named input, so it arrives in FormData\n * with everything else. Nothing merges; there is one source of truth.\n */\n field: <K extends keyof T & string>(name: K) => FieldBinding<T[K]>;\n /**\n * What is known about a field, for deciding how to show it.\n *\n * const title = fieldState('title')\n *\n * <Field data-invalid={title.invalid}>\n * <Input {...field('title')} aria-invalid={title.invalid} />\n * <FieldError errors={title.errors.map((message) => ({ message }))} />\n * </Field>\n *\n * `touched` is what separates \"not filled in yet\" from \"filled in wrongly\" —\n * an error on a field nobody has visited is a form shouting before anyone\n * has done anything.\n */\n fieldState: (name: string) => FieldState;\n}\n\ninterface FormProps<T extends Record<string, unknown> = Record<string, unknown>>\n extends Omit<FormHTMLAttributes<HTMLFormElement>, \"action\" | \"method\" | \"children\" | \"onSubmit\" | \"onError\"> {\n action: Href | ((formData: FormData) => Promise<unknown>);\n method?: \"get\" | \"post\";\n /**\n * Starting values for fields bound with `field()`.\n *\n * Uncontrolled fields do not need this — they take React's own\n * `defaultValue`, and the DOM keeps whatever is typed into them.\n */\n defaultValues?: Partial<T>;\n /**\n * A store created above this form, from `useFormStore()`.\n *\n * For the one case the context cannot reach: something that is not a\n * descendant — a top bar showing unsaved changes, a sidebar preview — needs\n * the values to exist above both of them. Create the store where they share\n * an ancestor and hand it down.\n *\n * Without it the form makes its own, which is what almost every form wants.\n */\n store?: FormStore;\n prefetch?: PrefetchStrategy;\n cacheFor?: number;\n replace?: boolean;\n preserveScroll?: boolean;\n resetOnSuccess?: boolean;\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, so a mistake\n * costs no round trip. It is a courtesy and not a control: the same action\n * is reachable without this form, so the server still has to check.\n */\n schema?: StandardSchemaV1;\n /** Transform form data before submitting to the server action. */\n transform?: (data: T) => Record<string, unknown>;\n /** Called inside the transition with typed form data. Use it to call your useOptimistic setter. */\n optimistic?: (data: T) => void;\n onSuccess?: (result: unknown) => void;\n /**\n * Called when a submit does not succeed.\n *\n * Validation failures arrive as field errors, with no second argument.\n * Anything else arrives as an empty error map and the thrown value — there\n * are no field errors to report, but the form still has to say so.\n */\n onError?: (errors: Record<string, string[]>, error?: unknown) => void;\n onSubmit?: (formData: FormData) => void | false;\n children: ReactNode | ((form: FormRenderProps<T>) => ReactNode);\n}\n\n/**\n * What an action built with createActionClient answers with.\n *\n * Recognised rather than thrown, because React serialises a rejected server\n * action opaquely: production strips the message and the fields it named are\n * gone. A returned object crosses intact, so a form reads it.\n */\nfunction resultOf(value: unknown): { errors?: Record<string, string[]>; serverError?: string } | null {\n if (typeof value !== 'object' || value === null) return null\n\n const result = value as { validationErrors?: Record<string, string[]>; serverError?: string }\n\n if (result.validationErrors) return { errors: result.validationErrors }\n if (result.serverError) return { serverError: result.serverError }\n\n return null\n}\n\nconst FormStatusContext = createContext<FormRenderProps>({\n pending: false,\n data: {},\n errors: {},\n error: () => undefined,\n clearErrors: () => {},\n reset: () => {},\n succeeded: false,\n recentlySucceeded: false,\n // Outside a Form there is nothing holding a value, so a binding that reported\n // one would be lying. Name only, which is the part that is still true.\n field: ((name: string) => ({\n name,\n value: \"\",\n onChange: () => {},\n onBlur: () => {},\n })) as FormRenderProps[\"field\"],\n fieldState: () => ({ touched: false, invalid: false, errors: [] }),\n});\n\n/**\n * The store, on its own context.\n *\n * Separate from the status context because that one holds a fresh object every\n * render, so anything reading it re-renders with the form. The store is stable\n * for the life of the form, which is what lets a subscriber below it re-render\n * alone.\n */\nconst FormStoreContext = createContext<{ store: FormStore; touch: (name: string) => void } | null>(\n null,\n);\n\n/**\n * A value store created above the form rather than by it.\n *\n * const store = useFormStore({ title: '' })\n *\n * <TopBar store={store} /> // not inside the form\n * <Form action={save} store={store}>…</Form>\n *\n * For the one case the context cannot reach. `<Form>` makes its own otherwise,\n * and almost every form should let it — this exists so that a component which\n * is not a descendant can still read the values, which is the flexibility\n * react-hook-form and TanStack Form get from `useForm()` being yours to call.\n *\n * Deliberately not reactive itself: creating the store does not subscribe to\n * it, so the component holding it does not re-render on every keystroke and\n * take the whole subtree with it. Read it with `useFormValues(store)`.\n */\nexport function useFormStore<T extends Record<string, unknown>>(\n initial: Partial<T> = {},\n): FormStore {\n const ref = useRef<FormStore | null>(null);\n\n ref.current ??= createFormStore({ ...initial });\n\n return ref.current;\n}\n\n/**\n * One field, subscribed on its own.\n *\n * The same thing `field()` gives, from a component that re-renders when this\n * field changes and at no other time. Reach for it when a form is large enough\n * that re-rendering all of it per keystroke is real:\n *\n * function Title() {\n * const { field, invalid, errors } = useField('title')\n *\n * return <Input {...field} aria-invalid={invalid} />\n * }\n *\n * Which is react-hook-form's `<Controller>` without the render prop: the\n * component you already had to write is the subscription boundary.\n */\nexport function useField(\n name: string,\n store?: FormStore,\n): FieldBinding<string> & FieldState {\n const ctx = useContext(FormStoreContext);\n const status = useContext(FormStatusContext);\n\n if (!ctx && !store) {\n throw new Error(\n \"useField() was called outside a <Form>. It reads that form's values, so there has to be one above it.\",\n );\n }\n\n const source = store ?? ctx!.store;\n const touch = ctx?.touch;\n\n const value = useSyncExternalStore(\n source.subscribe,\n () => (source.get(name) ?? \"\") as string,\n () => \"\",\n );\n\n const errors = status.errors[name] ?? [];\n\n return {\n name,\n value,\n onChange: (next) => {\n source.set(\n name,\n typeof next === \"object\" && next !== null && \"target\" in next\n ? (next as { target: { value: string } }).target.value\n : next,\n );\n },\n onBlur: () => void touch?.(name),\n touched: status.fieldState(name).touched,\n invalid: errors.length > 0,\n errors,\n };\n}\n\n/**\n * Every bound value, from anywhere inside the form.\n *\n * For a summary, a preview, a count of what has changed — something that reads\n * the form without being a field in it. Only values bound through `field()` or\n * `useField` are here: an uncontrolled input's value belongs to the DOM, and\n * this has no way to know it changed.\n */\nexport function useFormValues<T extends Record<string, unknown>>(\n store?: FormStore,\n): Partial<T> {\n const ctx = useContext(FormStoreContext);\n\n if (!ctx && !store) {\n throw new Error(\n \"useFormValues() was called outside a <Form>. It reads that form's values, so there has to be one above it.\",\n );\n }\n\n const source = store ?? ctx!.store;\n\n return useSyncExternalStore(\n source.subscribe,\n () => source.all() as Partial<T>,\n () => ({}) as Partial<T>,\n );\n}\n\nexport function useFormStatus<T extends Record<string, unknown> = Record<string, unknown>>(): FormRenderProps<T> {\n return useContext(FormStatusContext) as FormRenderProps<T>;\n}\n\n/**\n * The pieces of a field name: `items[0].name` is items, 0, name.\n *\n * Both spellings, because both are in use and a form should not care which one\n * a person reached for: `items[0].name` and `items[0][name]` are the same\n * field. A trailing `[]` is a piece of its own — see below.\n */\nfunction pathOf(name: string): string[] {\n return name\n .replace(/\\[(\\w*)\\]/g, \".$1\")\n .split(\".\")\n .filter((piece, index, all) => piece !== \"\" || index === all.length - 1);\n}\n\n/** Whether a piece names an array index rather than a property. */\nconst isIndex = (piece: string): boolean => /^\\d+$/.test(piece);\n\n/**\n * Put one value at one path, making the containers it passes through.\n *\n * Whether a container is an array or an object is decided by the NEXT piece, so\n * `items[0].name` makes an array holding an object without being told which is\n * which.\n */\nfunction place(root: Record<string, unknown>, path: string[], value: unknown): void {\n let node: Record<string, unknown> | unknown[] = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const container = node as Record<string, unknown>;\n\n if (container[key] === undefined || typeof container[key] !== \"object\") {\n // An index makes an array, and so does the empty piece a trailing `[]`\n // leaves — `tags[]` has to reach an array to be pushed into, and building\n // an object there is how this first went wrong.\n const next = path[i + 1];\n\n container[key] = isIndex(next) || next === \"\" ? [] : {};\n }\n\n node = container[key] as Record<string, unknown> | unknown[];\n }\n\n const last = path[path.length - 1];\n\n // The empty piece a trailing `[]` leaves: push rather than assign, so\n // `tags[]` twice is two entries rather than one overwriting the other.\n if (last === \"\") (node as unknown[]).push(value);\n else (node as Record<string, unknown>)[last] = value;\n}\n\n/**\n * A FormData as the object a schema expects.\n *\n * Four things beyond copying entries across, and each of them was a bug or a\n * gap someone would meet on their first non-trivial form:\n *\n * A repeated name is an array. Three checkboxes sharing a name, a multiple\n * select, a list of tags — this used to keep the LAST one and drop the rest\n * silently, so a schema validated an object the person had not submitted.\n *\n * A name ending in `[]` is always an array, even with one value selected.\n * Otherwise a list of checkboxes is a string when one is ticked and an array\n * when two are, and no schema can describe both. It is also what `useForm`\n * writes when it serialises an array, so the two round-trip.\n *\n * Nested names nest. `address.city` and `items[0].name` build the object they\n * describe, which is the shape the schema was written against — and the shape\n * whose validation errors come back keyed the same way, because Standard\n * Schema issue paths are joined with dots too.\n *\n * Files are kept. They were dropped for being non-strings, which meant a schema\n * checking an upload was handed undefined and refused a file that was there.\n */\nfunction formDataToObject<T extends Record<string, unknown>>(formData: FormData): T {\n const obj: Record<string, unknown> = {};\n\n for (const name of new Set(formData.keys())) {\n const all = formData.getAll(name);\n const path = pathOf(name);\n\n // A plain name used more than once is the array case, and it has no\n // brackets to say so — `tags` twice is `['a', 'b']`.\n if (path.length === 1 && path[0] !== \"\" && all.length > 1) {\n obj[path[0]] = all;\n continue;\n }\n\n for (const value of all) place(obj, path, value);\n }\n\n return obj as T;\n}\n\n/**\n * Also exported by name, and re-exported below, because both spellings are in\n * use: `import Form from` and `import { Form } from`.\n */\nexport default function Form<T extends Record<string, unknown> = Record<string, unknown>>({\n action,\n method: methodProp,\n defaultValues,\n store: providedStore,\n prefetch = \"hover\",\n cacheFor,\n replace = false,\n preserveScroll = false,\n resetOnSuccess = true,\n schema,\n transform,\n optimistic,\n onSuccess,\n onError,\n onSubmit,\n children,\n ...rest\n}: FormProps<T>) {\n const isGetForm = typeof action === \"string\";\n const method = methodProp ?? (isGetForm ? \"get\" : \"post\");\n const [errors, setErrors] = useState<Record<string, string[]>>({});\n const [touched, setTouched] = useState<Record<string, boolean>>({});\n\n /**\n * Mark a field visited, and check it.\n *\n * Checking on blur rather than on every keystroke, because an error that\n * appears while someone is halfway through typing an email address is a form\n * arguing with them. Leaving the field is the moment they have finished\n * saying what they meant.\n *\n * The whole object is validated and only this field's issues are kept: a\n * Standard Schema has no notion of one field, and filtering by path is the\n * honest way to ask it about one. Everything else's errors are left as they\n * were, so blurring an empty field does not light up the rest of the form.\n */\n const touch = useCallback(\n async (name: string) => {\n setTouched((prev) => (prev[name] ? prev : { ...prev, [name]: true }));\n\n if (!schema || !formRef.current) return;\n\n const invalid = await validateWith(schema, formDataToObject(new FormData(formRef.current)));\n\n setErrors((prev) => {\n const next = { ...prev };\n\n if (invalid?.[name]) next[name] = invalid[name];\n else delete next[name];\n\n return next;\n });\n },\n [schema],\n );\n\n const [succeeded, setSucceeded] = useState(false);\n const [recentlySucceeded, setRecentlySucceeded] = useState(false);\n const recentTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n // Cleared on unmount: a timer that fires into a component that has gone is\n // the warning nobody reads and the leak nobody finds.\n useEffect(() => () => clearTimeout(recentTimer.current), []);\n\n // Only the fields someone bound. Everything else is the DOM's.\n //\n // A store rather than state, so a component using `useField` can re-render\n // for one field while this one does not. See formStore.ts.\n const storeRef = useRef<FormStore | null>(null);\n\n storeRef.current ??= createFormStore({ ...(defaultValues as Record<string, unknown> | undefined) });\n\n const store = providedStore ?? storeRef.current;\n\n // Which names the render prop read through `field()`. A change to one of\n // those has to re-render this component, because that is where the value is\n // being displayed; a change to anything else does not, which is what lets a\n // `useField` child stand on its own.\n const readHere = useRef(new Set<string>());\n const [, bump] = useState(0);\n\n useEffect(\n () => store.subscribe((name) => {\n if (name === \"\" || readHere.current.has(name)) bump((n) => n + 1);\n }),\n [store],\n );\n const [currentData, setCurrentData] = useState<T>({} as T);\n const [isPending, startTransition] = useTransition();\n const formRef = useRef<HTMLFormElement>(null);\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 resetForm = useCallback(() => {\n formRef.current?.reset();\n setErrors({});\n setCurrentData({} as T);\n }, []);\n\n useEffect(() => {\n if (isGetForm && prefetch === \"mount\") {\n const fn = (window as any).__rsc_prefetch;\n fn?.(action, cacheFor);\n }\n }, [isGetForm, prefetch, action, cacheFor]);\n\n const doPrefetch = useCallback(() => {\n if (!isGetForm) return;\n const fn = (window as any).__rsc_prefetch;\n fn?.(action as string, cacheFor);\n }, [isGetForm, action, cacheFor]);\n\n const handleSubmit = useCallback(\n async (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n const formData = new FormData(e.currentTarget);\n const data = formDataToObject<T>(formData);\n setCurrentData(data);\n\n if (onSubmit?.(formData) === false) {\n return;\n }\n\n if (isGetForm && method === \"get\") {\n const url = new URL(action as string, window.location.origin);\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\" && value !== \"\") {\n url.searchParams.set(key, value);\n }\n }\n\n const path = url.pathname + url.search;\n const shell = action as string;\n const nav = (window as any).__rsc_navigate;\n const prefetched = (window as any).__rsc_is_prefetched;\n\n // The route without its query is that route's shell: the layout, the\n // chrome, and whatever it renders with nothing to show yet. Prefetching\n // on hover put it in the cache, so going there first costs no request\n // and puts the page on screen while the real query is still running.\n //\n // The query itself is never prefetched. It is the expensive half, and\n // hovering a search button is not a reason to run someone's search.\n if (path !== shell && prefetched?.(shell)) {\n // Awaited rather than raced: navigate() aborts whatever is in flight,\n // so starting the real one first would cancel the shell before it\n // could render. It is a cache hit, so this is a render, not a wait.\n Promise.resolve(nav?.(shell, { replace, preserveScroll })).then(() =>\n // Replaces, so the shell does not become a back-button stop of its\n // own — the pair leaves exactly one entry behind.\n nav?.(path, { replace: true, preserveScroll })\n );\n\n return;\n }\n\n nav?.(path, { replace, preserveScroll });\n return;\n }\n\n const serverAction = action as (formData: FormData) => Promise<unknown>;\n\n // Rebuilt rather than edited: transform returns the values to send, so\n // whatever was in the form and is not in the result should not go.\n if (transform) {\n for (const key of [...formData.keys()]) formData.delete(key);\n\n for (const [key, value] of buildFormData(transform(data))) {\n formData.append(key, value);\n }\n }\n\n setErrors({});\n\n // Before the transition, so a failure neither runs the optimistic\n // update nor leaves the form looking like it is submitting.\n const invalid = await validateWith(schema, data);\n\n if (invalid) {\n setErrors(invalid);\n onError?.(invalid);\n\n return;\n }\n\n startTransition(async () => {\n try {\n // Call optimistic updater inside the transition so React's\n // useOptimistic picks it up and auto-reverts on settle.\n optimistic?.(data);\n\n const result = await serverAction(formData);\n const refused = resultOf(result);\n\n if (refused) {\n if (refused.errors) {\n setErrors(refused.errors);\n onError?.(refused.errors);\n } else {\n onError?.({}, new Error(refused.serverError));\n }\n\n return;\n }\n\n if (resetOnSuccess) {\n formRef.current?.reset();\n setTouched({});\n setCurrentData({} as T);\n }\n\n setErrors({});\n setSucceeded(true);\n setRecentlySucceeded(true);\n\n clearTimeout(recentTimer.current);\n recentTimer.current = setTimeout(() => setRecentlySucceeded(false), 2_000);\n\n onSuccess?.(result);\n } catch (err) {\n if (err instanceof ServerValidationError) {\n setErrors(err.errors);\n onError?.(err.errors);\n } else if (err instanceof ServerDumpError) {\n // Dump overlay is already shown — silently swallow\n } else if (onError) {\n // Not rethrown. A rejected action never settles, and until it\n // settles React keeps the optimistic update on screen — so an\n // unexpected failure left the row showing as though the write had\n // worked. Settling is what takes it back.\n onError({}, err);\n } else {\n // Nothing is handling it, and swallowing here would lose it\n // entirely — the reason this used to rethrow.\n console.error('[rsc-kit] form submit failed', err);\n }\n }\n });\n },\n [action, isGetForm, method, replace, preserveScroll, resetOnSuccess, schema, transform, optimistic, onSubmit, onSuccess, onError]\n );\n\n const field = useCallback(\n <K extends keyof T & string>(name: K): FieldBinding<T[K]> => {\n // Recorded during render, deliberately: this is how the form knows which\n // values it is the one displaying. Idempotent, so a double render in\n // development records the same name twice and means the same thing.\n readHere.current.add(name);\n\n return {\n name,\n value: (store.get(name) ?? \"\") as T[K],\n onChange: (next: unknown) => {\n const value =\n typeof next === \"object\" && next !== null && \"target\" in next\n ? (next as { target: { value: string } }).target.value\n : next;\n\n store.set(name, value);\n },\n onBlur: () => void touch(name),\n };\n },\n [store, touch],\n );\n\n const fieldState = useCallback(\n (name: string): FieldState => ({\n touched: touched[name] === true,\n invalid: (errors[name]?.length ?? 0) > 0,\n errors: errors[name] ?? [],\n }),\n [touched, errors],\n );\n\n // Stable, so a subscriber below does not re-render because this one did.\n const storeContext = useMemo(() => ({ store, touch }), [store, touch]);\n\n const formStatus: FormRenderProps<T> = {\n pending: isPending,\n data: currentData,\n succeeded,\n recentlySucceeded,\n field,\n fieldState,\n errors: errors as FormRenderProps<T>[\"errors\"],\n error,\n clearErrors,\n reset: resetForm,\n };\n\n return (\n <FormStoreContext.Provider value={storeContext}>\n <FormStatusContext.Provider value={formStatus as FormRenderProps}>\n <form\n ref={formRef}\n // On the element as well as in the handler, which is what makes this\n // work before hydration. React emits a form a browser can submit on its\n // own for a server action, and an ordinary action/method pair for a\n // url — so a submit that happens before the javascript arrives still\n // reaches the server.\n //\n // The two do not fight: handleSubmit calls preventDefault() first, and\n // React does not run a form action when the submit event was cancelled.\n // So the enhanced path wins whenever there is one, and the native path\n // is what is left when there is not.\n action={action as never}\n // Only for a url. React sets the method itself for a server action, and\n // passing one alongside is what it warns about.\n method={isGetForm ? method : undefined}\n onSubmit={handleSubmit}\n // On the form, not only on the bound fields. `focusout` bubbles where\n // `blur` does not, so React's onBlur here sees every control that was\n // left — including the uncontrolled ones, which are most of them and\n // would otherwise never be marked touched at all.\n onBlur={(event) => {\n const name = (event.target as { name?: string }).name;\n\n if (name) void touch(name);\n }}\n onMouseEnter={prefetch === \"hover\" ? doPrefetch : undefined}\n data-pending={isPending ? \"\" : undefined}\n {...rest}\n >\n {typeof children === \"function\" ? children(formStatus) : children}\n </form>\n </FormStatusContext.Provider>\n </FormStoreContext.Provider>\n );\n}\n\n// Also by name, because both spellings are in use: `import Form` and\n// `import { Form }`. This was a re-export of \"./Form\" — from inside Form.tsx,\n// so the module named itself. It survived on the bundler tolerating a circular\n// self-reference, left over from when a separate barrel re-exported a\n// FormComponent.tsx that a case-insensitive filesystem would not let be called\n// Form.ts. There is one file now, and it can just export what it declares.\nexport { Form };\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Serialize form state into FormData for a server action.
3
+ *
4
+ * The contract between a form and the action it calls — booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
5
+ * `key[]`, Files pass through untouched for native uploads, and null/undefined
6
+ * are dropped rather than sent as the string "null".
7
+ */
8
+ export declare function buildFormData(data: Record<string, unknown>): FormData;
@@ -0,0 +1,37 @@
1
+ // Turning an object back into FormData.
2
+ //
3
+ // The one place the encoding decisions live, so a value sent through `<Form>`'s
4
+ // `transform` is encoded exactly as one sent any other way. It used to exist
5
+ // twice — here and inline in Form.tsx — which is two places for a boolean to
6
+ // start meaning something different.
7
+ /**
8
+ * Serialize form state into FormData for a server action.
9
+ *
10
+ * The contract between a form and the action it calls — booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
11
+ * `key[]`, Files pass through untouched for native uploads, and null/undefined
12
+ * are dropped rather than sent as the string "null".
13
+ */
14
+ export function buildFormData(data) {
15
+ const formData = new FormData();
16
+ for (const [key, val] of Object.entries(data)) {
17
+ if (val === null || val === undefined) {
18
+ continue;
19
+ }
20
+ if (val instanceof File) {
21
+ formData.append(key, val);
22
+ }
23
+ else if (typeof val === "boolean") {
24
+ formData.append(key, val ? "1" : "0");
25
+ }
26
+ else if (Array.isArray(val)) {
27
+ for (const item of val) {
28
+ formData.append(`${key}[]`, String(item));
29
+ }
30
+ }
31
+ else {
32
+ formData.append(key, String(val));
33
+ }
34
+ }
35
+ return formData;
36
+ }
37
+ //# sourceMappingURL=formEncoding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formEncoding.js","sourceRoot":"","sources":["../../src/js/formEncoding.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,6EAA6E;AAC7E,qCAAqC;AAErC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAA6B;IACzD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAEhC,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtC,SAAS;QACX,CAAC;QACD,IAAI,GAAG,YAAY,IAAI,EAAE,CAAC;YACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,OAAO,GAAG,KAAK,SAAS,EAAE,CAAC;YACpC,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;gBACvB,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["// Turning an object back into FormData.\n//\n// The one place the encoding decisions live, so a value sent through `<Form>`'s\n// `transform` is encoded exactly as one sent any other way. It used to exist\n// twice — here and inline in Form.tsx — which is two places for a boolean to\n// start meaning something different.\n\n/**\n * Serialize form state into FormData for a server action.\n *\n * The contract between a form and the action it calls — 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"]}
@@ -0,0 +1,9 @@
1
+ export interface FormStore {
2
+ get(name: string): unknown;
3
+ all(): Record<string, unknown>;
4
+ set(name: string, value: unknown): void;
5
+ reset(values?: Record<string, unknown>): void;
6
+ /** Called for every change, with the name that changed. */
7
+ subscribe(listener: (name: string) => void): () => void;
8
+ }
9
+ export declare function createFormStore(initial?: Record<string, unknown>): FormStore;
@@ -0,0 +1,43 @@
1
+ // Where a form's bound values live.
2
+ //
3
+ // Not React state, and that is the whole point. State in <Form> means every
4
+ // change re-renders everything the render prop returned — fine for the one or
5
+ // two fields a form usually binds, and the reason a field could not re-render
6
+ // on its own.
7
+ //
8
+ // A store can be subscribed to per field. A component calling useField('title')
9
+ // re-renders when `title` changes and at no other time, which is what
10
+ // react-hook-form's <Controller> achieves with a render prop and what this
11
+ // achieves without one.
12
+ //
13
+ // The form still re-renders for fields read through `field()` in the render
14
+ // prop, because that value is being read *in* the render prop and there is
15
+ // nowhere else for it to come from. Which of the two a field uses is the
16
+ // caller's choice, and the store is what makes the choice available.
17
+ export function createFormStore(initial = {}) {
18
+ let values = { ...initial };
19
+ const listeners = new Set();
20
+ return {
21
+ get: (name) => values[name],
22
+ all: () => values,
23
+ set(name, value) {
24
+ // Identity matters: useSyncExternalStore compares snapshots, and handing
25
+ // back a value that did not change would still be a render.
26
+ if (Object.is(values[name], value))
27
+ return;
28
+ values = { ...values, [name]: value };
29
+ listeners.forEach((fn) => fn(name));
30
+ },
31
+ reset(next) {
32
+ values = { ...(next ?? initial) };
33
+ // No name, so everything listening hears it. `reset` is the one change
34
+ // that is not about a field.
35
+ listeners.forEach((fn) => fn(""));
36
+ },
37
+ subscribe(listener) {
38
+ listeners.add(listener);
39
+ return () => listeners.delete(listener);
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=formStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formStore.js","sourceRoot":"","sources":["../../src/js/formStore.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,8EAA8E;AAC9E,cAAc;AACd,EAAE;AACF,gFAAgF;AAChF,sEAAsE;AACtE,2EAA2E;AAC3E,wBAAwB;AACxB,EAAE;AACF,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,qEAAqE;AAWrE,MAAM,UAAU,eAAe,CAAC,OAAO,GAA4B,EAAE;IACnE,IAAI,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEpD,OAAO;QACL,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;QAC3B,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM;QACjB,GAAG,CAAC,IAAI,EAAE,KAAK;YACb,yEAAyE;YACzE,4DAA4D;YAC5D,IAAI,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;gBAAE,OAAO;YAE3C,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC;YACtC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,KAAK,CAAC,IAAI;YACR,MAAM,GAAG,EAAE,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,EAAE,CAAC;YAClC,uEAAuE;YACvE,6BAA6B;YAC7B,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,SAAS,CAAC,QAAQ;YAChB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAExB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["// Where a form's bound values live.\n//\n// Not React state, and that is the whole point. State in <Form> means every\n// change re-renders everything the render prop returned — fine for the one or\n// two fields a form usually binds, and the reason a field could not re-render\n// on its own.\n//\n// A store can be subscribed to per field. A component calling useField('title')\n// re-renders when `title` changes and at no other time, which is what\n// react-hook-form's <Controller> achieves with a render prop and what this\n// achieves without one.\n//\n// The form still re-renders for fields read through `field()` in the render\n// prop, because that value is being read *in* the render prop and there is\n// nowhere else for it to come from. Which of the two a field uses is the\n// caller's choice, and the store is what makes the choice available.\n\nexport interface FormStore {\n get(name: string): unknown;\n all(): Record<string, unknown>;\n set(name: string, value: unknown): void;\n reset(values?: Record<string, unknown>): void;\n /** Called for every change, with the name that changed. */\n subscribe(listener: (name: string) => void): () => void;\n}\n\nexport function createFormStore(initial: Record<string, unknown> = {}): FormStore {\n let values = { ...initial };\n const listeners = new Set<(name: string) => void>();\n\n return {\n get: (name) => values[name],\n all: () => values,\n set(name, value) {\n // Identity matters: useSyncExternalStore compares snapshots, and handing\n // back a value that did not change would still be a render.\n if (Object.is(values[name], value)) return;\n\n values = { ...values, [name]: value };\n listeners.forEach((fn) => fn(name));\n },\n reset(next) {\n values = { ...(next ?? initial) };\n // No name, so everything listening hears it. `reset` is the one change\n // that is not about a field.\n listeners.forEach((fn) => fn(\"\"));\n },\n subscribe(listener) {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n };\n}\n"]}
package/dist/routes.d.ts CHANGED
@@ -33,4 +33,32 @@ type OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` |
33
33
  * `href={path as Href}`.
34
34
  */
35
35
  export type Href = Unregistered extends true ? string : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | OffRoute;
36
+ /** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
37
+ export interface RegisterApi {
38
+ }
39
+ /** The api route patterns this app declared: `'/api/orders/[id]'`. */
40
+ export type ApiPattern = RegisterApi extends {
41
+ apis: infer R extends string;
42
+ } ? R : string;
43
+ type NoApis = string extends ApiPattern ? true : false;
44
+ /**
45
+ * A url an api route in this app answers.
46
+ *
47
+ * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed
48
+ * because that is how a GET is parameterised.
49
+ */
50
+ export type ApiHref = NoApis extends true ? string : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;
51
+ /**
52
+ * An api url, checked against the routes the build found.
53
+ *
54
+ * await fetch(apiUrl(`/api/orders/${id}`))
55
+ *
56
+ * A function rather than a bare type so it can be used inline at a call site
57
+ * that is typed `string` — `fetch` takes any string, so nothing would check the
58
+ * argument without somewhere to put the type. It returns what it was given.
59
+ *
60
+ * Wrong path, and it stops compiling. Renamed the directory, and every call
61
+ * site says so rather than one of them 404ing in production.
62
+ */
63
+ export declare function apiUrl(href: ApiHref): string;
36
64
  export {};
package/dist/routes.js CHANGED
@@ -24,5 +24,19 @@
24
24
  // nothing, `RoutePattern` stays `string`, and every url-taking API is exactly
25
25
  // as permissive as it was before. That fallback is the reason this can ship
26
26
  // without a flag.
27
- export {};
27
+ /**
28
+ * An api url, checked against the routes the build found.
29
+ *
30
+ * await fetch(apiUrl(`/api/orders/${id}`))
31
+ *
32
+ * A function rather than a bare type so it can be used inline at a call site
33
+ * that is typed `string` — `fetch` takes any string, so nothing would check the
34
+ * argument without somewhere to put the type. It returns what it was given.
35
+ *
36
+ * Wrong path, and it stops compiling. Renamed the directory, and every call
37
+ * site says so rather than one of them 404ing in production.
38
+ */
39
+ export function apiUrl(href) {
40
+ return href;
41
+ }
28
42
  //# sourceMappingURL=routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,kEAAkE;AAClE,6EAA6E;AAC7E,8EAA8E;AAC9E,8CAA8C;AAC9C,EAAE;AACF,8EAA8E;AAC9E,sCAAsC;AACtC,EAAE;AACF,4CAA4C;AAC5C,2DAA2D;AAC3D,MAAM;AACN,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,kBAAkB","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string } ? R : string\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` | `#${string}` | `?${string}`\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Href}`.\n */\nexport type Href = Unregistered extends true\n ? string\n : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | OffRoute\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,kEAAkE;AAClE,6EAA6E;AAC7E,8EAA8E;AAC9E,8CAA8C;AAC9C,EAAE;AACF,8EAA8E;AAC9E,sCAAsC;AACtC,EAAE;AACF,4CAA4C;AAC5C,2DAA2D;AAC3D,MAAM;AACN,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,kBAAkB;AAwElB;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,MAAM,CAAC,IAAa;IAClC,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string } ? R : string\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` | `#${string}` | `?${string}`\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Href}`.\n */\nexport type Href = Unregistered extends true\n ? string\n : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | OffRoute\n\n// ── Api routes ───────────────────────────────────────────────────────────────\n//\n// Their own union rather than part of Href, because they are not pages and a\n// link to one is almost always a mistake — an <a href=\"/api/orders\"> navigates\n// the browser away to a json document. Keeping them apart means `Link` refuses\n// an api url and `apiUrl()` refuses a page, which is the pair of mistakes worth\n// catching.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterApi {}\n\n/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string } ? R : string\n\ntype NoApis = string extends ApiPattern ? true : false\n\n/**\n * A url an api route in this app answers.\n *\n * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed\n * because that is how a GET is parameterised.\n */\nexport type ApiHref = NoApis extends true\n ? string\n : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`\n\n/**\n * An api url, checked against the routes the build found.\n *\n * await fetch(apiUrl(`/api/orders/${id}`))\n *\n * A function rather than a bare type so it can be used inline at a call site\n * that is typed `string` — `fetch` takes any string, so nothing would check the\n * argument without somewhere to put the type. It returns what it was given.\n *\n * Wrong path, and it stops compiling. Renamed the directory, and every call\n * site says so rather than one of them 404ing in production.\n */\nexport function apiUrl(href: ApiHref): string {\n return href\n}\n"]}
package/dist/vite.js CHANGED
@@ -1240,6 +1240,7 @@ function patternOf(segments) {
1240
1240
  */
1241
1241
  function renderRouteTypes(manifest) {
1242
1242
  const patterns = [...new Set(manifest.routes.map((route) => patternOf(route.segments)))].sort();
1243
+ const apis = [...new Set((manifest.apis ?? []).map((route) => patternOf(route.segments)))].sort();
1243
1244
  return [
1244
1245
  '// @generated — do not edit. Written by the RSC build from the route tree.',
1245
1246
  '//',
@@ -1258,6 +1259,14 @@ function renderRouteTypes(manifest) {
1258
1259
  ? ' routes:\n' + patterns.map((p) => ' | ' + JSON.stringify(p)).join('\n')
1259
1260
  : ' // No routes found under the source directory.\n routes: never',
1260
1261
  ' }',
1262
+ // Api routes are a separate union, so Link refuses an api url and apiUrl()
1263
+ // refuses a page. Linking to an api route navigates the browser away to a
1264
+ // json document, which is the mistake worth catching.
1265
+ ' interface RegisterApi {',
1266
+ apis.length > 0
1267
+ ? ' apis:\n' + apis.map((p) => ' | ' + JSON.stringify(p)).join('\n')
1268
+ : ' // No route.ts files found under the source directory.\n apis: never',
1269
+ ' }',
1261
1270
  '}',
1262
1271
  '',
1263
1272
  ].join('\n');
@@ -3713,7 +3722,16 @@ export function rscKit(options = {}) {
3713
3722
  // routing bug rather than as this line. In dev the pages are the root;
3714
3723
  // the assets come from the same origin either way.
3715
3724
  base: '/',
3716
- root: outDir,
3725
+ // Where the generated entries live, so Vite resolves them as its own
3726
+ // source. A build concern only.
3727
+ //
3728
+ // Not during preview. `vite preview` serves what was built, and Nitro's
3729
+ // preview reads its build info from `<vite root>/node_modules/.nitro` —
3730
+ // so pointing the root at this directory sent it looking somewhere no
3731
+ // build ever writes, and every preview failed with "Cannot load nitro
3732
+ // build info. Make sure to build first." after a build that had just
3733
+ // succeeded.
3734
+ ...(env.isPreview ? {} : { root: outDir }),
3717
3735
  // Force single instances of React/RSC runtime — critical when the
3718
3736
  // package is symlinked (local dev / monorepo), else "use client"
3719
3737
  // components SSR against a second React copy and hooks throw.