@rsc-kit/core 0.30.3 → 0.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/action.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type StandardSchemaV1 } from './js/standardSchema.js';
2
2
  import { type QueryOptions } from './query.js';
3
+ import { type Redirected } from './js/errors.js';
3
4
  /**
4
5
  * What an action answers with. One of `data`, `validationErrors`,
5
6
  * `serverError` and `redirected` is set; `refusal` only ever beside
@@ -55,6 +56,17 @@ export declare function isActionValidationError(error: unknown): error is Action
55
56
  * particular, which is where the form already looks for one.
56
57
  */
57
58
  export type FieldErrorsFor<Input> = Partial<Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>>;
59
+ /**
60
+ * What a handler's data is, without the redirect a generated stub's type adds.
61
+ *
62
+ * A stub is typed `Promise<T | Redirected>` for the browser, where a redirected
63
+ * call resolves with `{ redirected }`. Called from a handler it runs on the
64
+ * server, where the redirect is thrown and travels as one, so its result is
65
+ * only ever `T` - and `data` would say `T | Redirected`, a case the client
66
+ * reports beside `data` and never inside it. Only a member that is exactly
67
+ * `Redirected` goes: data that merely has a `redirected` field stays.
68
+ */
69
+ export type Delivered<T> = T extends Redirected ? (Redirected extends T ? never : T) : T;
58
70
  /**
59
71
  * An action declining to do what it was asked, on purpose, with a message for
60
72
  * the person who asked and - optionally - data the page can act on: the
@@ -190,7 +202,7 @@ export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw =
190
202
  * reading `result.validationErrors` after a redirect reads undefined and
191
203
  * not a TypeError inside a transition, which unmounts the root.
192
204
  */
193
- handler<Data>(fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data): Action<Raw, Data, Refusal>;
205
+ handler<Data>(fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data): Action<Raw, Delivered<Data>, Refusal>;
194
206
  /**
195
207
  * The body of a READ, sharing this client's middleware and schema.
196
208
  *
@@ -210,7 +222,7 @@ export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw =
210
222
  query<Data>(fn: (args: {
211
223
  input: Input;
212
224
  ctx: Ctx;
213
- }) => Promise<Data> | Data, options?: QueryOptions): (input?: unknown) => Promise<Data>;
225
+ }) => Promise<Data> | Data, options?: QueryOptions): (input?: unknown) => Promise<Delivered<Data>>;
214
226
  }
215
227
  export interface ActionClientOptions {
216
228
  /**
package/dist/action.js CHANGED
@@ -248,6 +248,8 @@ export function createActionClient(options = {}) {
248
248
  return build(middlewares, schema, next);
249
249
  },
250
250
  handler(fn) {
251
+ // Delivered is a type-only difference from what the body returns:
252
+ // the value is passed through as it is.
251
253
  return markClientBuilt(async (raw) => {
252
254
  try {
253
255
  return { data: (await pipeline(raw, fn)) };
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"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","sourcesContent":["// Server actions with a schema, middleware, and types that follow from both.\n//\n// export const action = createActionClient({ onError: report })\n//\n// export const createPost = action\n// .use(async ({ next }) => next({ ctx: { user: await currentUser() } }))\n// .input(z.object({ title: z.string().min(3) }))\n// .handler(async ({ input, ctx }) => savePost(ctx.user.id, input.title))\n//\n// `input` is typed from the schema and `ctx` from every middleware that ran, so\n// the handler is checked against both without either being written down twice.\n//\n// The built action RETURNS its failures rather than throwing them, and that is\n// not a style choice. React serialises a rejected server action opaquely —\n// production strips the message and leaves a digest — so a thrown validation\n// error reaches the browser as \"an error occurred\" and the fields it named are\n// gone. A returned object crosses the boundary intact.\n//\n// Distinct from `middleware.ts` in a route directory, which decides whether a\n// page may render. This wraps one action. They are different questions: an\n// action is reachable without any page, which is why it defends itself.\n\nimport { validateWith, type StandardSchemaV1 } from './js/standardSchema.js'\nimport { decodeFormData } from './js/formEncoding.js'\nimport { markQuery, QueryValidationError, type QueryOptions } from './query.js'\nimport { isRedirectSignal } from './redirectDigest.js'\nimport { isNotFoundSignal } from './notFound.js'\nimport { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js'\n\n/**\n * What an action answers with. One of `data`, `validationErrors`,\n * `serverError` and `redirected` is set; `refusal` only ever beside\n * `serverError`.\n */\nexport interface ActionResult<Data, Refusal = never> {\n /** What the handler returned. */\n data?: Data\n /** Field name to messages, in the shape a form already renders. */\n validationErrors?: Record<string, string[]>\n /**\n * The action did not happen, as a message the browser may see: a refusal's\n * own message - see `refuse()` - or something unexpected, reduced by\n * `onError` to one that says nothing a stranger should not see.\n */\n serverError?: string\n /**\n * What a refusal said beside its message - the things blocking a delete,\n * say - checked against the schema the action declared with `.refusal()`\n * and typed from it. Set only by `refuse()`, here or on the backend.\n */\n refusal?: Refusal\n /**\n * Where the action sent the visitor. Set by the client, which has already\n * started the navigation: the page is on its way there, and there is\n * nothing for the caller to do. Never set on the server side of the call.\n */\n redirected?: string\n}\n\n/**\n * A middleware that neither continued nor refused.\n *\n * Never reported through `onError`: that reduces an error to a message for the\n * browser, and this one is for whoever wrote the middleware. A check that\n * forgot to call `next()` would otherwise look exactly like a check that\n * passed.\n */\nexport class ActionMisuse extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ActionMisuse'\n }\n}\n\n/** Refuse from inside a handler, naming the fields. */\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * A property rather than `instanceof`, for the reason this project keeps\n * running into: an app's actions are bundled separately from the engine, so\n * each gets its own copy of this module and its own copy of the class.\n * `instanceof` compares identity across that seam and is simply false — and\n * the refusal is then reported as a server error, so the form shows \"Something\n * went wrong\" instead of naming the fields. Everything works; nothing logs.\n *\n * Symbol.for, so the two copies agree on the key as well as the value.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.action-validation')\n\n/**\n * Set on every function a client builds, so the build can tell them from a\n * bare \"use server\" export. The difference is the middleware: a bare export\n * runs with nothing checking who called it, and nothing else in the app\n * would ever say so. Symbol.for, because the build reads the mark from the\n * bundled copy of this module and the app's actions were built by another.\n */\nconst CLIENT_MARK = Symbol.for('@rsc-kit/core.action-client')\n\n/** Whether a server action was built by createActionClient, and so ran its chain. */\nexport function isClientBuilt(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[CLIENT_MARK] === true\n}\n\nfunction markClientBuilt<F extends Function>(fn: F): F {\n Object.defineProperty(fn, CLIENT_MARK, { value: true })\n\n return fn\n}\n\nexport class ActionValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'ActionValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionValidationError(error: unknown): error is ActionValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * The field errors a handler may report, keyed by its own input's fields.\n *\n * `''` is the whole submission — for a refusal that is about no field in\n * particular, which is where the form already looks for one.\n */\nexport type FieldErrorsFor<Input> = Partial<\n Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>\n>\n\nconst REFUSAL_MARK = Symbol.for('@rsc-kit/core.action-refusal')\n\n/**\n * An action declining to do what it was asked, on purpose, with a message for\n * the person who asked and - optionally - data the page can act on: the\n * records blocking a delete, the plan a quota belongs to.\n *\n * Not a validation error: the input was fine, the state of things says no.\n * And not a failure: its message is meant to be seen, so it is never\n * replaced by `onError`'s \"Something went wrong.\". It arrives as the result's\n * `serverError`, which is where a form's `formError` already reads, and its\n * data as `refusal`.\n */\nexport class ActionRefusal extends Error {\n public readonly data: unknown\n /** The status a request that is not an action - a page, a form posted without javascript - answers with. */\n public readonly refusalStatus: number\n\n constructor(message: string, data?: unknown, status = 409) {\n super(message)\n this.name = 'ActionRefusal'\n this.data = data\n this.refusalStatus = status\n ;(this as unknown as Record<symbol, boolean>)[REFUSAL_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionRefusal(error: unknown): error is ActionRefusal {\n return typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[REFUSAL_MARK] === true\n}\n\n/**\n * Decline, with a message to show and optional data to act on. Throws.\n *\n * if (attached.length) refuse('Still in use', { blockers: attached })\n *\n * From a handler, a middleware or anything they call. The data reaches the\n * page only when the action declares its shape with `.refusal(schema)`, and\n * only once it matches - see ActionResult.refusal. Inside a handler, the\n * `refuse` it is given is typed to that schema; this one takes anything.\n */\nexport function refuse(message: string, data?: unknown, options: { status?: number } = {}): never {\n throw new ActionRefusal(message, data, options.status)\n}\n\n/**\n * What a handler is given.\n *\n * `fieldErrors` is here as well as exported on its own, and the one here is\n * the one to use: it is typed to this handler's input, so a field the schema\n * does not have is a type error rather than an error the form never shows.\n * The bare export takes any string, for the rare check that runs outside a\n * handler.\n */\nexport interface HandlerArgs<Input, Ctx, Refusal = never> {\n input: Input\n ctx: Ctx\n /**\n * Decline, with a message and the data the action declared with\n * `.refusal(schema)`. Throws; write `return refuse(...)`, as with\n * fieldErrors.\n */\n refuse: [Refusal] extends [never]\n ? (message: string) => never\n : (message: string, data: Refusal, options?: { status?: number }) => never\n /**\n * Fail with errors on this input's fields. Throws; nothing after it runs.\n *\n * Write `return fieldErrors(...)`. It throws either way, but TypeScript does\n * not treat a never-return as terminating when the callee is a destructured\n * binding — only a declaration or an explicitly annotated variable — so\n * without the return, a value checked on the line above is still possibly\n * undefined on the line below. The return is what lets the type narrow.\n */\n fieldErrors: (errors: FieldErrorsFor<Input>) => never\n}\n\n/**\n * Fail with field errors the form can show.\n *\n * For what a schema cannot know — a name already taken, a balance too low.\n * Throws, so the handler stops where it is; the action turns it into a\n * returned result on the way out.\n *\n * Untyped by field, because it has no handler to take the input from. Inside\n * one, use the `fieldErrors` the handler is given instead.\n */\nexport function fieldErrors(errors: Record<string, string[] | string>): never {\n const normalised: Record<string, string[]> = {}\n\n for (const [field, message] of Object.entries(errors)) {\n normalised[field] = Array.isArray(message) ? message : [message]\n }\n\n throw new ActionValidationError(normalised)\n}\n\n/**\n * What `next()` hands back, carrying what the step added.\n *\n * The context a middleware contributes cannot be inferred from the arguments\n * it passes to `next` — TypeScript infers from a function's return, not from a\n * call inside it. So `next` returns this, the middleware returns it, and the\n * addition is read off the middleware's own return type.\n */\nexport interface MiddlewareResult<Extra> {\n readonly ctx: Extra\n readonly value: unknown\n}\n\n/**\n * A step that runs before the handler.\n *\n * It calls `next` to continue, optionally adding to the context, and what it\n * adds shows up in the handler's types. Returning without calling `next` — or\n * throwing — stops the action, which is how a check refuses.\n */\nexport type ActionMiddleware<Ctx, Extra extends Record<string, unknown>> = (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n}) => Promise<MiddlewareResult<Extra>>\n\ntype Output<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never\n\n/** What a caller may pass, before the schema parses it. */\ntype InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown\n\n/**\n * A built action, as its callers see it.\n *\n * Typed from the schema's input when there is one, so a call with the wrong\n * shape fails the typecheck rather than the validation - and FormData beside\n * it, which is how a form calls the same action. Without a schema, anything.\n */\nexport type Action<Raw, Data, Refusal = never> = unknown extends Raw\n ? (input?: unknown) => Promise<ActionResult<Data, Refusal>>\n : undefined extends Raw\n ? (input?: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n : (input: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n\nexport interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown, Refusal = never> {\n /** Add a step, and whatever context it contributes. */\n use<Extra extends Record<string, unknown> = Record<string, never>>(\n middleware: (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n }) => Promise<MiddlewareResult<Extra>>,\n ): ActionBuilder<Ctx & Extra, Input, Raw, Refusal>\n /** Parse and check what the caller sent. The handler's `input` follows. */\n input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>, Refusal>\n /**\n * The shape of what a refusal may carry - see `refuse()`. The page gets\n * `result.refusal` typed from it, and only data that matches it: data that\n * does not is a bug in the server, answered as one.\n */\n refusal<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Input, Raw, Output<S>>\n /**\n * The body.\n *\n * What the caller gets is always the result object: `data`, or a\n * refusal, or - when the action redirected - `redirected`, set by the\n * client once the navigation is under way. Always an object, so a caller\n * reading `result.validationErrors` after a redirect reads undefined and\n * not a TypeError inside a transition, which unmounts the root.\n */\n handler<Data>(\n fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data,\n ): Action<Raw, Data, Refusal>\n /**\n * The body of a READ, sharing this client's middleware and schema.\n *\n * export const getPosts = client.input(filter).query(async ({ input, ctx }) =>\n * db.posts(ctx.user.id, input))\n *\n * The same builder as `handler`, and deliberately so: an app configures its\n * auth check and its error reporting once, and both a mutation and a read go\n * through them.\n *\n * It fails differently, though, and that is not an oversight. An action\n * RETURNS its failures because React serialises a rejection opaquely. A query\n * is handed to a cache library as a fetcher, and every one of them reports\n * failure by rejection — so this returns the data directly and throws, and\n * the endpoint carries the message across for it.\n */\n query<Data>(\n fn: (args: { input: Input; ctx: Ctx }) => Promise<Data> | Data,\n options?: QueryOptions,\n ): (input?: unknown) => Promise<Data>\n}\n\nexport interface ActionClientOptions {\n /**\n * What the browser is told when something unexpected throws.\n *\n * Everything reaching here is a bug or an outage, and its message may say\n * more than a stranger should see — a query, a path, a host. Returning a\n * fixed string is the safe default; return the message only for errors you\n * raised deliberately.\n */\n onError?: (error: unknown) => string\n}\n\nconst GENERIC = 'Something went wrong.'\n\n/**\n * Whether the backend turned this caller away: not signed in, not allowed, or\n * nothing there for them. Each carries a message written for the person.\n */\nfunction turnedAway(error: unknown): error is Error {\n return (\n error instanceof ServerAuthenticationError || error instanceof ServerAuthorizationError || isNotFoundSignal(error)\n )\n}\n\n/**\n * FormData in, a plain object out — what a schema expects to be handed.\n *\n * The form's own decoder, so the object validated here is the object the\n * browser validated. This had a flat decoder of its own: `items[0].name`\n * stayed a key spelled exactly that, `<Form>` had already parsed it into an\n * array, and a nested form passed in the browser and failed on the server\n * with the fields it named gone.\n */\nconst fromFormData = (body: FormData, schema: unknown): Record<string, unknown> => decodeFormData(body, schema)\n\nexport function createActionClient(\n options: ActionClientOptions = {},\n): ActionBuilder<Record<never, never>, undefined> {\n const report = options.onError ?? (() => GENERIC)\n\n function build<Ctx extends Record<string, unknown>, Input>(\n middlewares: ActionMiddleware<never, never>[],\n schema: StandardSchemaV1 | null,\n refusalSchema: StandardSchemaV1 | null = null,\n ): ActionBuilder<Ctx, Input> {\n /**\n * Validate, run the chain, call the body.\n *\n * Shared by both terminals, which is the point of putting a read on this\n * builder at all: one set of middleware, one schema, one place the auth\n * check lives. Refusals leave by throwing, and each terminal decides what\n * that should look like from the outside.\n */\n const pipeline = async (\n raw: unknown,\n fn: (args: HandlerArgs<never, never>) => unknown,\n ): Promise<unknown> => {\n const value = raw instanceof FormData ? fromFormData(raw, schema) : raw\n\n if (schema) {\n const invalid = await validateWith(schema, value)\n\n if (invalid) throw new ActionValidationError(invalid)\n }\n\n const parsed = schema\n ? ((await schema['~standard'].validate(value)).value as Input)\n : (value as Input)\n\n // Composed inside-out so the first `use` is the outermost — it sees the\n // others run, which is what makes timing and cleanup possible rather\n // than only checks.\n let ctx = {} as Ctx\n let index = 0\n\n const run = async (): Promise<unknown> => {\n const middleware = middlewares[index++]\n\n if (!middleware) {\n return await fn({\n input: parsed as never,\n ctx: ctx as never,\n // The same function as the export; the type on the way in is what\n // is different, and the type is the handler's input.\n fieldErrors: fieldErrors as never,\n refuse: refuse as never,\n })\n }\n\n let continued = false\n\n const result = await (middleware as unknown as ActionMiddleware<Ctx, Record<string, unknown>>)({\n ctx,\n next: (async (opts?: { ctx?: Record<string, unknown> }) => {\n continued = true\n ctx = { ...ctx, ...(opts?.ctx ?? {}) } as Ctx\n\n return { ctx, value: await run() }\n }) as never,\n })\n\n // Silence is not refusal. A middleware that neither called next() nor\n // threw has done nothing, and guessing which it meant turns a\n // forgotten `return` into a check that quietly passes.\n if (!continued) {\n throw new ActionMisuse(\n 'A middleware returned without calling next(). Call it to continue, or throw to refuse.',\n )\n }\n\n return (result as { value?: unknown })?.value\n }\n\n return await run()\n }\n\n /**\n * A refusal as the page gets it: its message, always - it was written to\n * be seen - and its data only once the declared schema has checked it.\n * Data with no schema to check it, or that fails the schema, never\n * reaches the browser: the first is said in the server's log, the second\n * is a bug in the server, returned as the error each terminal reports.\n */\n const refusalResult = async (error: ActionRefusal): Promise<ActionResult<never, unknown> | Error> => {\n if (error.data === undefined) return { serverError: error.message }\n\n if (!refusalSchema) {\n console.error(\n `[rsc-kit] An action refused with data, and declares no .refusal(schema) for it - the message was sent, the data was not: ${error.message}`,\n )\n\n return { serverError: error.message }\n }\n\n const checked = await refusalSchema['~standard'].validate(error.data)\n\n if (checked.issues) {\n return new Error(\n `An action refused with data its .refusal(schema) does not accept: ${checked.issues.map((issue) => issue.message).join('; ')}`,\n )\n }\n\n return { serverError: error.message, refusal: checked.value }\n }\n\n return {\n use(middleware) {\n return build([...middlewares, middleware as never], schema, refusalSchema) as never\n },\n input(next) {\n return build(middlewares, next, refusalSchema) as never\n },\n refusal(next) {\n return build(middlewares, schema, next) as never\n },\n handler(fn) {\n return markClientBuilt(async (raw?: unknown) => {\n try {\n return { data: (await pipeline(raw, fn)) as Awaited<ReturnType<typeof fn>> }\n } catch (error) {\n // Past onError deliberately — see ActionMisuse.\n if (error instanceof ActionMisuse) throw error\n\n // A redirect is an instruction, not a failure: it has to reach\n // the host, which answers the action with the destination. Caught\n // here it became { serverError: 'Something went wrong.' } - a\n // login that succeeded and then showed an error.\n if (isRedirectSignal(error)) throw error\n\n if (isActionValidationError(error)) {\n return { validationErrors: error.errors }\n }\n\n if (isActionRefusal(error)) {\n const refused = await refusalResult(error)\n\n return (refused instanceof Error ? { serverError: report(refused) } : refused) as ActionResult<never>\n }\n\n // Turned away by the backend - not signed in, not allowed, no such\n // record - is an answer too, and its message is meant to be seen.\n // Not rethrown as a redirect is: the action endpoint answers a\n // throw with a 500, and the form would show nothing at all.\n if (turnedAway(error)) return { serverError: error.message }\n\n return { serverError: report(error) }\n }\n })\n },\n query(fn, options) {\n const read = async (raw?: unknown) => {\n try {\n return await pipeline(raw, fn)\n } catch (error) {\n if (error instanceof ActionMisuse) throw error\n if (isRedirectSignal(error)) throw error\n\n // Left as they are, as a redirect is. A page that reads through a\n // query answers 401, 403 or its not-found page, as it does calling\n // the backend directly; the read endpoint answers the same statuses.\n if (turnedAway(error)) throw error\n\n // Thrown, not returned. A cache library reports failure by\n // rejection, so a query that answered with an error-shaped object\n // would look like a successful read of something odd.\n if (isActionValidationError(error)) {\n throw new QueryValidationError(error.errors)\n }\n\n // Its own message, as an action's is, and its checked data on\n // the error for a fetcher that wants it.\n if (isActionRefusal(error)) {\n const refused = await refusalResult(error)\n\n if (refused instanceof Error) throw new Error(report(refused))\n\n throw Object.assign(new Error(refused.serverError), {\n refusal: refused.refusal,\n refusalStatus: error.refusalStatus,\n })\n }\n\n throw new Error(report(error))\n }\n }\n\n return markClientBuilt(markQuery(read, options)) as (input?: unknown) => Promise<never>\n },\n }\n }\n\n return build([], null)\n}\n"]}
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"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","sourcesContent":["// Server actions with a schema, middleware, and types that follow from both.\n//\n// export const action = createActionClient({ onError: report })\n//\n// export const createPost = action\n// .use(async ({ next }) => next({ ctx: { user: await currentUser() } }))\n// .input(z.object({ title: z.string().min(3) }))\n// .handler(async ({ input, ctx }) => savePost(ctx.user.id, input.title))\n//\n// `input` is typed from the schema and `ctx` from every middleware that ran, so\n// the handler is checked against both without either being written down twice.\n//\n// The built action RETURNS its failures rather than throwing them, and that is\n// not a style choice. React serialises a rejected server action opaquely —\n// production strips the message and leaves a digest — so a thrown validation\n// error reaches the browser as \"an error occurred\" and the fields it named are\n// gone. A returned object crosses the boundary intact.\n//\n// Distinct from `middleware.ts` in a route directory, which decides whether a\n// page may render. This wraps one action. They are different questions: an\n// action is reachable without any page, which is why it defends itself.\n\nimport { validateWith, type StandardSchemaV1 } from './js/standardSchema.js'\nimport { decodeFormData } from './js/formEncoding.js'\nimport { markQuery, QueryValidationError, type QueryOptions } from './query.js'\nimport { isRedirectSignal } from './redirectDigest.js'\nimport { isNotFoundSignal } from './notFound.js'\nimport { ServerAuthenticationError, ServerAuthorizationError, type Redirected } from './js/errors.js'\n\n/**\n * What an action answers with. One of `data`, `validationErrors`,\n * `serverError` and `redirected` is set; `refusal` only ever beside\n * `serverError`.\n */\nexport interface ActionResult<Data, Refusal = never> {\n /** What the handler returned. */\n data?: Data\n /** Field name to messages, in the shape a form already renders. */\n validationErrors?: Record<string, string[]>\n /**\n * The action did not happen, as a message the browser may see: a refusal's\n * own message - see `refuse()` - or something unexpected, reduced by\n * `onError` to one that says nothing a stranger should not see.\n */\n serverError?: string\n /**\n * What a refusal said beside its message - the things blocking a delete,\n * say - checked against the schema the action declared with `.refusal()`\n * and typed from it. Set only by `refuse()`, here or on the backend.\n */\n refusal?: Refusal\n /**\n * Where the action sent the visitor. Set by the client, which has already\n * started the navigation: the page is on its way there, and there is\n * nothing for the caller to do. Never set on the server side of the call.\n */\n redirected?: string\n}\n\n/**\n * A middleware that neither continued nor refused.\n *\n * Never reported through `onError`: that reduces an error to a message for the\n * browser, and this one is for whoever wrote the middleware. A check that\n * forgot to call `next()` would otherwise look exactly like a check that\n * passed.\n */\nexport class ActionMisuse extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ActionMisuse'\n }\n}\n\n/** Refuse from inside a handler, naming the fields. */\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * A property rather than `instanceof`, for the reason this project keeps\n * running into: an app's actions are bundled separately from the engine, so\n * each gets its own copy of this module and its own copy of the class.\n * `instanceof` compares identity across that seam and is simply false — and\n * the refusal is then reported as a server error, so the form shows \"Something\n * went wrong\" instead of naming the fields. Everything works; nothing logs.\n *\n * Symbol.for, so the two copies agree on the key as well as the value.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.action-validation')\n\n/**\n * Set on every function a client builds, so the build can tell them from a\n * bare \"use server\" export. The difference is the middleware: a bare export\n * runs with nothing checking who called it, and nothing else in the app\n * would ever say so. Symbol.for, because the build reads the mark from the\n * bundled copy of this module and the app's actions were built by another.\n */\nconst CLIENT_MARK = Symbol.for('@rsc-kit/core.action-client')\n\n/** Whether a server action was built by createActionClient, and so ran its chain. */\nexport function isClientBuilt(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[CLIENT_MARK] === true\n}\n\nfunction markClientBuilt<F extends Function>(fn: F): F {\n Object.defineProperty(fn, CLIENT_MARK, { value: true })\n\n return fn\n}\n\nexport class ActionValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'ActionValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionValidationError(error: unknown): error is ActionValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * The field errors a handler may report, keyed by its own input's fields.\n *\n * `''` is the whole submission — for a refusal that is about no field in\n * particular, which is where the form already looks for one.\n */\nexport type FieldErrorsFor<Input> = Partial<\n Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>\n>\n\nconst REFUSAL_MARK = Symbol.for('@rsc-kit/core.action-refusal')\n\n/**\n * What a handler's data is, without the redirect a generated stub's type adds.\n *\n * A stub is typed `Promise<T | Redirected>` for the browser, where a redirected\n * call resolves with `{ redirected }`. Called from a handler it runs on the\n * server, where the redirect is thrown and travels as one, so its result is\n * only ever `T` - and `data` would say `T | Redirected`, a case the client\n * reports beside `data` and never inside it. Only a member that is exactly\n * `Redirected` goes: data that merely has a `redirected` field stays.\n */\nexport type Delivered<T> = T extends Redirected ? (Redirected extends T ? never : T) : T\n\n/**\n * An action declining to do what it was asked, on purpose, with a message for\n * the person who asked and - optionally - data the page can act on: the\n * records blocking a delete, the plan a quota belongs to.\n *\n * Not a validation error: the input was fine, the state of things says no.\n * And not a failure: its message is meant to be seen, so it is never\n * replaced by `onError`'s \"Something went wrong.\". It arrives as the result's\n * `serverError`, which is where a form's `formError` already reads, and its\n * data as `refusal`.\n */\nexport class ActionRefusal extends Error {\n public readonly data: unknown\n /** The status a request that is not an action - a page, a form posted without javascript - answers with. */\n public readonly refusalStatus: number\n\n constructor(message: string, data?: unknown, status = 409) {\n super(message)\n this.name = 'ActionRefusal'\n this.data = data\n this.refusalStatus = status\n ;(this as unknown as Record<symbol, boolean>)[REFUSAL_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionRefusal(error: unknown): error is ActionRefusal {\n return typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[REFUSAL_MARK] === true\n}\n\n/**\n * Decline, with a message to show and optional data to act on. Throws.\n *\n * if (attached.length) refuse('Still in use', { blockers: attached })\n *\n * From a handler, a middleware or anything they call. The data reaches the\n * page only when the action declares its shape with `.refusal(schema)`, and\n * only once it matches - see ActionResult.refusal. Inside a handler, the\n * `refuse` it is given is typed to that schema; this one takes anything.\n */\nexport function refuse(message: string, data?: unknown, options: { status?: number } = {}): never {\n throw new ActionRefusal(message, data, options.status)\n}\n\n/**\n * What a handler is given.\n *\n * `fieldErrors` is here as well as exported on its own, and the one here is\n * the one to use: it is typed to this handler's input, so a field the schema\n * does not have is a type error rather than an error the form never shows.\n * The bare export takes any string, for the rare check that runs outside a\n * handler.\n */\nexport interface HandlerArgs<Input, Ctx, Refusal = never> {\n input: Input\n ctx: Ctx\n /**\n * Decline, with a message and the data the action declared with\n * `.refusal(schema)`. Throws; write `return refuse(...)`, as with\n * fieldErrors.\n */\n refuse: [Refusal] extends [never]\n ? (message: string) => never\n : (message: string, data: Refusal, options?: { status?: number }) => never\n /**\n * Fail with errors on this input's fields. Throws; nothing after it runs.\n *\n * Write `return fieldErrors(...)`. It throws either way, but TypeScript does\n * not treat a never-return as terminating when the callee is a destructured\n * binding — only a declaration or an explicitly annotated variable — so\n * without the return, a value checked on the line above is still possibly\n * undefined on the line below. The return is what lets the type narrow.\n */\n fieldErrors: (errors: FieldErrorsFor<Input>) => never\n}\n\n/**\n * Fail with field errors the form can show.\n *\n * For what a schema cannot know — a name already taken, a balance too low.\n * Throws, so the handler stops where it is; the action turns it into a\n * returned result on the way out.\n *\n * Untyped by field, because it has no handler to take the input from. Inside\n * one, use the `fieldErrors` the handler is given instead.\n */\nexport function fieldErrors(errors: Record<string, string[] | string>): never {\n const normalised: Record<string, string[]> = {}\n\n for (const [field, message] of Object.entries(errors)) {\n normalised[field] = Array.isArray(message) ? message : [message]\n }\n\n throw new ActionValidationError(normalised)\n}\n\n/**\n * What `next()` hands back, carrying what the step added.\n *\n * The context a middleware contributes cannot be inferred from the arguments\n * it passes to `next` — TypeScript infers from a function's return, not from a\n * call inside it. So `next` returns this, the middleware returns it, and the\n * addition is read off the middleware's own return type.\n */\nexport interface MiddlewareResult<Extra> {\n readonly ctx: Extra\n readonly value: unknown\n}\n\n/**\n * A step that runs before the handler.\n *\n * It calls `next` to continue, optionally adding to the context, and what it\n * adds shows up in the handler's types. Returning without calling `next` — or\n * throwing — stops the action, which is how a check refuses.\n */\nexport type ActionMiddleware<Ctx, Extra extends Record<string, unknown>> = (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n}) => Promise<MiddlewareResult<Extra>>\n\ntype Output<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never\n\n/** What a caller may pass, before the schema parses it. */\ntype InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown\n\n/**\n * A built action, as its callers see it.\n *\n * Typed from the schema's input when there is one, so a call with the wrong\n * shape fails the typecheck rather than the validation - and FormData beside\n * it, which is how a form calls the same action. Without a schema, anything.\n */\nexport type Action<Raw, Data, Refusal = never> = unknown extends Raw\n ? (input?: unknown) => Promise<ActionResult<Data, Refusal>>\n : undefined extends Raw\n ? (input?: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n : (input: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n\nexport interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown, Refusal = never> {\n /** Add a step, and whatever context it contributes. */\n use<Extra extends Record<string, unknown> = Record<string, never>>(\n middleware: (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n }) => Promise<MiddlewareResult<Extra>>,\n ): ActionBuilder<Ctx & Extra, Input, Raw, Refusal>\n /** Parse and check what the caller sent. The handler's `input` follows. */\n input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>, Refusal>\n /**\n * The shape of what a refusal may carry - see `refuse()`. The page gets\n * `result.refusal` typed from it, and only data that matches it: data that\n * does not is a bug in the server, answered as one.\n */\n refusal<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Input, Raw, Output<S>>\n /**\n * The body.\n *\n * What the caller gets is always the result object: `data`, or a\n * refusal, or - when the action redirected - `redirected`, set by the\n * client once the navigation is under way. Always an object, so a caller\n * reading `result.validationErrors` after a redirect reads undefined and\n * not a TypeError inside a transition, which unmounts the root.\n */\n handler<Data>(\n fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data,\n ): Action<Raw, Delivered<Data>, Refusal>\n /**\n * The body of a READ, sharing this client's middleware and schema.\n *\n * export const getPosts = client.input(filter).query(async ({ input, ctx }) =>\n * db.posts(ctx.user.id, input))\n *\n * The same builder as `handler`, and deliberately so: an app configures its\n * auth check and its error reporting once, and both a mutation and a read go\n * through them.\n *\n * It fails differently, though, and that is not an oversight. An action\n * RETURNS its failures because React serialises a rejection opaquely. A query\n * is handed to a cache library as a fetcher, and every one of them reports\n * failure by rejection — so this returns the data directly and throws, and\n * the endpoint carries the message across for it.\n */\n query<Data>(\n fn: (args: { input: Input; ctx: Ctx }) => Promise<Data> | Data,\n options?: QueryOptions,\n ): (input?: unknown) => Promise<Delivered<Data>>\n}\n\nexport interface ActionClientOptions {\n /**\n * What the browser is told when something unexpected throws.\n *\n * Everything reaching here is a bug or an outage, and its message may say\n * more than a stranger should see — a query, a path, a host. Returning a\n * fixed string is the safe default; return the message only for errors you\n * raised deliberately.\n */\n onError?: (error: unknown) => string\n}\n\nconst GENERIC = 'Something went wrong.'\n\n/**\n * Whether the backend turned this caller away: not signed in, not allowed, or\n * nothing there for them. Each carries a message written for the person.\n */\nfunction turnedAway(error: unknown): error is Error {\n return (\n error instanceof ServerAuthenticationError || error instanceof ServerAuthorizationError || isNotFoundSignal(error)\n )\n}\n\n/**\n * FormData in, a plain object out — what a schema expects to be handed.\n *\n * The form's own decoder, so the object validated here is the object the\n * browser validated. This had a flat decoder of its own: `items[0].name`\n * stayed a key spelled exactly that, `<Form>` had already parsed it into an\n * array, and a nested form passed in the browser and failed on the server\n * with the fields it named gone.\n */\nconst fromFormData = (body: FormData, schema: unknown): Record<string, unknown> => decodeFormData(body, schema)\n\nexport function createActionClient(\n options: ActionClientOptions = {},\n): ActionBuilder<Record<never, never>, undefined> {\n const report = options.onError ?? (() => GENERIC)\n\n function build<Ctx extends Record<string, unknown>, Input>(\n middlewares: ActionMiddleware<never, never>[],\n schema: StandardSchemaV1 | null,\n refusalSchema: StandardSchemaV1 | null = null,\n ): ActionBuilder<Ctx, Input> {\n /**\n * Validate, run the chain, call the body.\n *\n * Shared by both terminals, which is the point of putting a read on this\n * builder at all: one set of middleware, one schema, one place the auth\n * check lives. Refusals leave by throwing, and each terminal decides what\n * that should look like from the outside.\n */\n const pipeline = async (\n raw: unknown,\n fn: (args: HandlerArgs<never, never>) => unknown,\n ): Promise<unknown> => {\n const value = raw instanceof FormData ? fromFormData(raw, schema) : raw\n\n if (schema) {\n const invalid = await validateWith(schema, value)\n\n if (invalid) throw new ActionValidationError(invalid)\n }\n\n const parsed = schema\n ? ((await schema['~standard'].validate(value)).value as Input)\n : (value as Input)\n\n // Composed inside-out so the first `use` is the outermost — it sees the\n // others run, which is what makes timing and cleanup possible rather\n // than only checks.\n let ctx = {} as Ctx\n let index = 0\n\n const run = async (): Promise<unknown> => {\n const middleware = middlewares[index++]\n\n if (!middleware) {\n return await fn({\n input: parsed as never,\n ctx: ctx as never,\n // The same function as the export; the type on the way in is what\n // is different, and the type is the handler's input.\n fieldErrors: fieldErrors as never,\n refuse: refuse as never,\n })\n }\n\n let continued = false\n\n const result = await (middleware as unknown as ActionMiddleware<Ctx, Record<string, unknown>>)({\n ctx,\n next: (async (opts?: { ctx?: Record<string, unknown> }) => {\n continued = true\n ctx = { ...ctx, ...(opts?.ctx ?? {}) } as Ctx\n\n return { ctx, value: await run() }\n }) as never,\n })\n\n // Silence is not refusal. A middleware that neither called next() nor\n // threw has done nothing, and guessing which it meant turns a\n // forgotten `return` into a check that quietly passes.\n if (!continued) {\n throw new ActionMisuse(\n 'A middleware returned without calling next(). Call it to continue, or throw to refuse.',\n )\n }\n\n return (result as { value?: unknown })?.value\n }\n\n return await run()\n }\n\n /**\n * A refusal as the page gets it: its message, always - it was written to\n * be seen - and its data only once the declared schema has checked it.\n * Data with no schema to check it, or that fails the schema, never\n * reaches the browser: the first is said in the server's log, the second\n * is a bug in the server, returned as the error each terminal reports.\n */\n const refusalResult = async (error: ActionRefusal): Promise<ActionResult<never, unknown> | Error> => {\n if (error.data === undefined) return { serverError: error.message }\n\n if (!refusalSchema) {\n console.error(\n `[rsc-kit] An action refused with data, and declares no .refusal(schema) for it - the message was sent, the data was not: ${error.message}`,\n )\n\n return { serverError: error.message }\n }\n\n const checked = await refusalSchema['~standard'].validate(error.data)\n\n if (checked.issues) {\n return new Error(\n `An action refused with data its .refusal(schema) does not accept: ${checked.issues.map((issue) => issue.message).join('; ')}`,\n )\n }\n\n return { serverError: error.message, refusal: checked.value }\n }\n\n return {\n use(middleware) {\n return build([...middlewares, middleware as never], schema, refusalSchema) as never\n },\n input(next) {\n return build(middlewares, next, refusalSchema) as never\n },\n refusal(next) {\n return build(middlewares, schema, next) as never\n },\n handler(fn) {\n // Delivered is a type-only difference from what the body returns:\n // the value is passed through as it is.\n return markClientBuilt(async (raw?: unknown) => {\n try {\n return { data: (await pipeline(raw, fn)) as Awaited<ReturnType<typeof fn>> }\n } catch (error) {\n // Past onError deliberately — see ActionMisuse.\n if (error instanceof ActionMisuse) throw error\n\n // A redirect is an instruction, not a failure: it has to reach\n // the host, which answers the action with the destination. Caught\n // here it became { serverError: 'Something went wrong.' } - a\n // login that succeeded and then showed an error.\n if (isRedirectSignal(error)) throw error\n\n if (isActionValidationError(error)) {\n return { validationErrors: error.errors }\n }\n\n if (isActionRefusal(error)) {\n const refused = await refusalResult(error)\n\n return (refused instanceof Error ? { serverError: report(refused) } : refused) as ActionResult<never>\n }\n\n // Turned away by the backend - not signed in, not allowed, no such\n // record - is an answer too, and its message is meant to be seen.\n // Not rethrown as a redirect is: the action endpoint answers a\n // throw with a 500, and the form would show nothing at all.\n if (turnedAway(error)) return { serverError: error.message }\n\n return { serverError: report(error) }\n }\n }) as never\n },\n query(fn, options) {\n const read = async (raw?: unknown) => {\n try {\n return await pipeline(raw, fn)\n } catch (error) {\n if (error instanceof ActionMisuse) throw error\n if (isRedirectSignal(error)) throw error\n\n // Left as they are, as a redirect is. A page that reads through a\n // query answers 401, 403 or its not-found page, as it does calling\n // the backend directly; the read endpoint answers the same statuses.\n if (turnedAway(error)) throw error\n\n // Thrown, not returned. A cache library reports failure by\n // rejection, so a query that answered with an error-shaped object\n // would look like a successful read of something odd.\n if (isActionValidationError(error)) {\n throw new QueryValidationError(error.errors)\n }\n\n // Its own message, as an action's is, and its checked data on\n // the error for a fetcher that wants it.\n if (isActionRefusal(error)) {\n const refused = await refusalResult(error)\n\n if (refused instanceof Error) throw new Error(report(refused))\n\n throw Object.assign(new Error(refused.serverError), {\n refusal: refused.refusal,\n refusalStatus: error.refusalStatus,\n })\n }\n\n throw new Error(report(error))\n }\n }\n\n return markClientBuilt(markQuery(read, options)) as (input?: unknown) => Promise<never>\n },\n }\n }\n\n return build([], null)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/core",
3
- "version": "0.30.3",
3
+ "version": "0.30.4",
4
4
  "description": "React Server Components as a Vite plugin. Routing, streaming, server actions, typed urls and build-time prerendering — deploy to Bun, Node, Workers, Vercel or Deno.",
5
5
  "license": "MIT",
6
6
  "type": "module",