@rsc-kit/core 0.30.6 → 0.30.8
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 +8 -1
- package/dist/action.js.map +1 -1
- package/dist/headers.d.ts +6 -0
- package/dist/headers.js +6 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.js +30 -5
- package/dist/host.js.map +1 -1
- package/dist/js/Form.js +8 -1
- package/dist/js/Form.js.map +1 -1
- package/dist/js/RedirectBoundary.d.ts +9 -0
- package/dist/js/RedirectBoundary.js +60 -9
- package/dist/js/RedirectBoundary.js.map +1 -1
- package/dist/js/RouteErrorBoundary.js +6 -2
- package/dist/js/RouteErrorBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +16 -3
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +32 -0
- package/dist/js/errors.js +43 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.d.ts +6 -0
- package/dist/js/navigate.js +11 -3
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/refreshOn.js +7 -0
- package/dist/js/refreshOn.js.map +1 -1
- package/dist/js/router.d.ts +1 -0
- package/dist/js/router.js.map +1 -1
- package/dist/manifest.d.ts +10 -0
- package/dist/manifest.js.map +1 -1
- package/dist/notFound.d.ts +2 -2
- package/dist/notFound.js +5 -4
- package/dist/notFound.js.map +1 -1
- package/dist/vite.js +80 -22
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/action.d.ts
CHANGED
|
@@ -177,6 +177,13 @@ type InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown;
|
|
|
177
177
|
* it, which is how a form calls the same action. Without a schema, anything.
|
|
178
178
|
*/
|
|
179
179
|
export type Action<Raw, Data, Refusal = never> = unknown extends Raw ? (input?: unknown) => Promise<ActionResult<Data, Refusal>> : undefined extends Raw ? (input?: Raw | FormData) => Promise<ActionResult<Data, Refusal>> : (input: Raw | FormData) => Promise<ActionResult<Data, Refusal>>;
|
|
180
|
+
/**
|
|
181
|
+
* What `.query()` returns: the same three cases as an action, without the
|
|
182
|
+
* FormData a form posts. A query is read with arguments, so a key the schema
|
|
183
|
+
* does not have fails the typecheck rather than the read - it was
|
|
184
|
+
* `(input?: unknown)`, and `getLogs({ team: 1, nonsense: true })` compiled.
|
|
185
|
+
*/
|
|
186
|
+
export type Query<Raw, Data> = unknown extends Raw ? (input?: unknown) => Promise<Data> : undefined extends Raw ? (input?: Raw) => Promise<Data> : (input: Raw) => Promise<Data>;
|
|
180
187
|
export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown, Refusal = never> {
|
|
181
188
|
/** Add a step, and whatever context it contributes. */
|
|
182
189
|
use<Extra extends Record<string, unknown> = Record<string, never>>(middleware: (args: {
|
|
@@ -222,7 +229,7 @@ export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw =
|
|
|
222
229
|
query<Data>(fn: (args: {
|
|
223
230
|
input: Input;
|
|
224
231
|
ctx: Ctx;
|
|
225
|
-
}) => Promise<Data> | Data, options?: QueryOptions):
|
|
232
|
+
}) => Promise<Data> | Data, options?: QueryOptions): Query<Raw, Delivered<Data>>;
|
|
226
233
|
}
|
|
227
234
|
export interface ActionClientOptions {
|
|
228
235
|
/**
|
package/dist/action.js.map
CHANGED
|
@@ -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, 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"]}
|
|
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\n/**\n * What `.query()` returns: the same three cases as an action, without the\n * FormData a form posts. A query is read with arguments, so a key the schema\n * does not have fails the typecheck rather than the read - it was\n * `(input?: unknown)`, and `getLogs({ team: 1, nonsense: true })` compiled.\n */\nexport type Query<Raw, Data> = unknown extends Raw\n ? (input?: unknown) => Promise<Data>\n : undefined extends Raw\n ? (input?: Raw) => Promise<Data>\n : (input: Raw) => Promise<Data>\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 ): Query<Raw, 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 never\n },\n }\n }\n\n return build([], null)\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export declare const HEADER: {
|
|
|
10
10
|
readonly segmentDepth: 'X-RSC-Segment-Depth';
|
|
11
11
|
/** The chain to send back next time. */
|
|
12
12
|
readonly layouts: 'X-RSC-Layouts';
|
|
13
|
+
/**
|
|
14
|
+
* On a payload request: this url's page said it does not exist, after the
|
|
15
|
+
* shell went out, and the client is asking for the not-found page to put
|
|
16
|
+
* where it was. RedirectBoundary sends it.
|
|
17
|
+
*/
|
|
18
|
+
readonly notFound: 'X-RSC-Not-Found';
|
|
13
19
|
/** Identifies the build, so a client can notice it is talking to an old one. */
|
|
14
20
|
readonly version: 'X-RSC-Version';
|
|
15
21
|
/** The slot an intercepted navigation is targeting. */
|
package/dist/headers.js
CHANGED
|
@@ -19,6 +19,12 @@ export const HEADER = {
|
|
|
19
19
|
segmentDepth: 'X-RSC-Segment-Depth',
|
|
20
20
|
/** The chain to send back next time. */
|
|
21
21
|
layouts: 'X-RSC-Layouts',
|
|
22
|
+
/**
|
|
23
|
+
* On a payload request: this url's page said it does not exist, after the
|
|
24
|
+
* shell went out, and the client is asking for the not-found page to put
|
|
25
|
+
* where it was. RedirectBoundary sends it.
|
|
26
|
+
*/
|
|
27
|
+
notFound: 'X-RSC-Not-Found',
|
|
22
28
|
/** Identifies the build, so a client can notice it is talking to an old one. */
|
|
23
29
|
version: 'X-RSC-Version',
|
|
24
30
|
/** The slot an intercepted navigation is targeting. */
|
package/dist/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,wEAAwE;AACxE,2EAA2E;AAC3E,gCAAgC;AAChC,EAAE;AACF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB;;;OAGG;IACH,GAAG,EAAE,OAAO;IAEZ,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB;IAE1B,kEAAkE;IAClE,YAAY,EAAE,qBAAqB;IAEnC,wCAAwC;IACxC,OAAO,EAAE,eAAe;IAExB,gFAAgF;IAChF,OAAO,EAAE,eAAe;IAExB,uDAAuD;IACvD,SAAS,EAAE,iBAAiB;IAE5B,gDAAgD;IAChD,OAAO,EAAE,eAAe;IAExB,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAE9B,0CAA0C;IAC1C,MAAM,EAAE,cAAc;IAEtB;;;;;;;OAOG;IACH,QAAQ,EAAE,gBAAgB;IAE1B;;;;;;OAMG;IACH,WAAW,EAAE,oBAAoB;IAEjC;;;;;OAKG;IACH,UAAU,EAAE,cAAc;IAC1B;;;;;;;;;OASG;IACH;;;;;;;OAOG;IACH,KAAK,EAAE,aAAa;IAEpB,SAAS,EAAE,aAAa;IACxB,mEAAmE;IACnE,YAAY,EAAE,iBAAiB;IAC/B,uDAAuD;IACvD,aAAa,EAAE,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,eAAe;IAC1B,+EAA+E;IAC/E,WAAW,EAAE,eAAe;IAC5B,8FAA8F;IAC9F,UAAU,EAAE,cAAc;CAClB,CAAA;AAEV,sBAAsB;AACtB,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAE5D,2BAA2B;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,CAAC,GAAG;IACV,MAAM,CAAC,QAAQ;IACf,MAAM,CAAC,UAAU;IACjB,MAAM,CAAC,SAAS;IAChB,MAAM,CAAC,OAAO;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,oCAAoC,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qCAAqC,CAAA","sourcesContent":["// The wire protocol, as names rather than string literals.\n//\n// A host writing these out by hand is how they drift. Two of them are not\n// negotiable at all — the client sends them or asks for them at fixed\n// addresses, and a host that spells one differently fails silently: the\n// request falls through, the decoder never runs, and a button does nothing\n// with no error on either side.\n//\n// PROTOCOL.md describes what each one means. This is the same list, importable.\n\nexport const HEADER = {\n /**\n * Present on a request for a Flight payload rather than the document. One\n * url serves both, which is why every answer must Vary on it.\n */\n rsc: 'X-RSC',\n\n /** The layout chain the client has mounted, outermost first, comma-separated. */\n segments: 'X-RSC-Segments',\n\n /** The boundary the answer replaces. 0 means a whole document. */\n segmentDepth: 'X-RSC-Segment-Depth',\n\n /** The chain to send back next time. */\n layouts: 'X-RSC-Layouts',\n\n /** Identifies the build, so a client can notice it is talking to an old one. */\n version: 'X-RSC-Version',\n\n /** The slot an intercepted navigation is targeting. */\n intercept: 'X-RSC-Intercept',\n\n /** Where the navigation or action came from. */\n referer: 'X-RSC-Referer',\n\n /** One named region of the current page to render and return alone. */\n revalidate: 'X-RSC-Revalidate',\n\n /** The server reference being invoked. */\n action: 'X-RSC-Action',\n\n /**\n * Go here instead.\n *\n * Used for a redirect the client has to perform itself: a failed action, and\n * a navigation whose render redirected. Never a 3xx for those — `fetch`\n * follows one transparently, so the client would get the destination's HTML\n * where it expected a Flight payload and decode it as one.\n */\n redirect: 'X-RSC-Redirect',\n\n /**\n * The body's real type.\n *\n * The body itself goes out as application/octet-stream so a host that parses\n * multipart cannot consume it before the action does — reading Content-Type\n * instead leaves an upload undecodable.\n */\n contentType: 'X-RSC-Content-Type',\n\n /**\n * Where the browser posts server actions.\n *\n * Not a convention this can choose: the client runtime posts here\n * unconditionally.\n */\n actionPath: '/_rsc/action',\n /**\n * Where the browser reads queries.\n *\n * A GET, and that is the whole point of it being a separate address: a POST\n * is cacheable by nothing, so a read routed through the action endpoint can\n * never be kept by an HTTP cache, a CDN or a service worker. Only functions\n * declared with `query()` are reachable here — the endpoint checks the mark\n * before invoking anything, because otherwise this address would run any\n * registered action over GET.\n */\n /**\n * Sent on every read, and required by the query endpoint.\n *\n * A GET with no unusual header is a simple request, so any page could\n * trigger one with an <img> and the visitor's cookies would ride along.\n * This header is not CORS-safelisted, so the browser preflights it — the\n * same thing that makes a POST carrying X-RSC-Action safe.\n */\n query: 'X-RSC-Query',\n\n queryPath: '/_rsc/query',\n /** Build-time shell for a PPR route, for an edge cache to hold. */\n pprShellPath: '/_rsc/ppr-shell',\n /** Finish a shell an edge cache is already serving. */\n pprResumePath: '/_rsc/ppr-resume',\n /**\n * On a refresh a change triggered: which names moved, to which versions\n * (`stock@5,orders@2`). A shared section renders once per such change, and\n * every tab asking with the same one gets that render.\n */\n changedBy: 'X-RSC-Changed',\n /** The stream a tab hears on when a name its page refreshes on has changed. */\n changesPath: '/_rsc/changes',\n /** Whether this renderer can serve: up, and its backend answering. GET only, never cached. */\n healthPath: '/_rsc/health',\n} as const\n\n/** Flight payload. */\nexport const FLIGHT_TYPE = 'text/x-component; charset=utf-8'\n\n/** A rendered document. */\nexport const HTML_TYPE = 'text/html; charset=utf-8'\n\n/**\n * Every request header that changes what a response body contains.\n *\n * One url answers with a whole document, a partial payload, a named region or\n * an interceptor depending on these — so a cache keyed on the url alone, or on\n * `X-RSC` alone, will hand one client another's answer. They are materially\n * different bodies, not variations in presentation.\n */\nexport const VARY_ON_RSC = [\n HEADER.rsc,\n HEADER.segments,\n HEADER.revalidate,\n HEADER.intercept,\n HEADER.referer,\n].join(', ')\n\n/**\n * What a response says about being stored, when it must not be.\n *\n * `Vary` alone is not enough on the platform this targets. Cloudflare ignores\n * `Vary` for everything except `Accept-Encoding`, and this package ships a\n * Workers story as a headline feature — so one url answering with a document,\n * a Flight payload, a segment and an interceptor, keyed only on the url, hands\n * one visitor another's body. Anything narrowed by a request header, and\n * anything behind a guard, says outright that it is not to be stored.\n */\nexport const PER_CLIENT = 'private, no-store'\n\n/**\n * And what a frozen, unguarded page says.\n *\n * Explicit rather than absent: with no directive at all, whether an\n * intermediary stores it comes down to that intermediary's heuristics, which\n * is not a decision this package should be leaving to chance in either\n * direction.\n */\nexport const REVALIDATE = 'public, max-age=0, must-revalidate'\n\n/**\n * And what a page rendered for one request says.\n *\n * It may have read the visitor's cookies, and nothing in the headers - which\n * leave before the render has finished - can know whether it did. So a shared\n * cache may not keep it; the visitor's own browser may, and must ask again.\n */\nexport const PER_REQUEST = 'private, max-age=0, must-revalidate'\n"]}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,wEAAwE;AACxE,2EAA2E;AAC3E,gCAAgC;AAChC,EAAE;AACF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB;;;OAGG;IACH,GAAG,EAAE,OAAO;IAEZ,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB;IAE1B,kEAAkE;IAClE,YAAY,EAAE,qBAAqB;IAEnC,wCAAwC;IACxC,OAAO,EAAE,eAAe;IAExB;;;;OAIG;IACH,QAAQ,EAAE,iBAAiB;IAE3B,gFAAgF;IAChF,OAAO,EAAE,eAAe;IAExB,uDAAuD;IACvD,SAAS,EAAE,iBAAiB;IAE5B,gDAAgD;IAChD,OAAO,EAAE,eAAe;IAExB,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAE9B,0CAA0C;IAC1C,MAAM,EAAE,cAAc;IAEtB;;;;;;;OAOG;IACH,QAAQ,EAAE,gBAAgB;IAE1B;;;;;;OAMG;IACH,WAAW,EAAE,oBAAoB;IAEjC;;;;;OAKG;IACH,UAAU,EAAE,cAAc;IAC1B;;;;;;;;;OASG;IACH;;;;;;;OAOG;IACH,KAAK,EAAE,aAAa;IAEpB,SAAS,EAAE,aAAa;IACxB,mEAAmE;IACnE,YAAY,EAAE,iBAAiB;IAC/B,uDAAuD;IACvD,aAAa,EAAE,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,eAAe;IAC1B,+EAA+E;IAC/E,WAAW,EAAE,eAAe;IAC5B,8FAA8F;IAC9F,UAAU,EAAE,cAAc;CAClB,CAAA;AAEV,sBAAsB;AACtB,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAE5D,2BAA2B;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,CAAC,GAAG;IACV,MAAM,CAAC,QAAQ;IACf,MAAM,CAAC,UAAU;IACjB,MAAM,CAAC,SAAS;IAChB,MAAM,CAAC,OAAO;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,oCAAoC,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qCAAqC,CAAA","sourcesContent":["// The wire protocol, as names rather than string literals.\n//\n// A host writing these out by hand is how they drift. Two of them are not\n// negotiable at all — the client sends them or asks for them at fixed\n// addresses, and a host that spells one differently fails silently: the\n// request falls through, the decoder never runs, and a button does nothing\n// with no error on either side.\n//\n// PROTOCOL.md describes what each one means. This is the same list, importable.\n\nexport const HEADER = {\n /**\n * Present on a request for a Flight payload rather than the document. One\n * url serves both, which is why every answer must Vary on it.\n */\n rsc: 'X-RSC',\n\n /** The layout chain the client has mounted, outermost first, comma-separated. */\n segments: 'X-RSC-Segments',\n\n /** The boundary the answer replaces. 0 means a whole document. */\n segmentDepth: 'X-RSC-Segment-Depth',\n\n /** The chain to send back next time. */\n layouts: 'X-RSC-Layouts',\n\n /**\n * On a payload request: this url's page said it does not exist, after the\n * shell went out, and the client is asking for the not-found page to put\n * where it was. RedirectBoundary sends it.\n */\n notFound: 'X-RSC-Not-Found',\n\n /** Identifies the build, so a client can notice it is talking to an old one. */\n version: 'X-RSC-Version',\n\n /** The slot an intercepted navigation is targeting. */\n intercept: 'X-RSC-Intercept',\n\n /** Where the navigation or action came from. */\n referer: 'X-RSC-Referer',\n\n /** One named region of the current page to render and return alone. */\n revalidate: 'X-RSC-Revalidate',\n\n /** The server reference being invoked. */\n action: 'X-RSC-Action',\n\n /**\n * Go here instead.\n *\n * Used for a redirect the client has to perform itself: a failed action, and\n * a navigation whose render redirected. Never a 3xx for those — `fetch`\n * follows one transparently, so the client would get the destination's HTML\n * where it expected a Flight payload and decode it as one.\n */\n redirect: 'X-RSC-Redirect',\n\n /**\n * The body's real type.\n *\n * The body itself goes out as application/octet-stream so a host that parses\n * multipart cannot consume it before the action does — reading Content-Type\n * instead leaves an upload undecodable.\n */\n contentType: 'X-RSC-Content-Type',\n\n /**\n * Where the browser posts server actions.\n *\n * Not a convention this can choose: the client runtime posts here\n * unconditionally.\n */\n actionPath: '/_rsc/action',\n /**\n * Where the browser reads queries.\n *\n * A GET, and that is the whole point of it being a separate address: a POST\n * is cacheable by nothing, so a read routed through the action endpoint can\n * never be kept by an HTTP cache, a CDN or a service worker. Only functions\n * declared with `query()` are reachable here — the endpoint checks the mark\n * before invoking anything, because otherwise this address would run any\n * registered action over GET.\n */\n /**\n * Sent on every read, and required by the query endpoint.\n *\n * A GET with no unusual header is a simple request, so any page could\n * trigger one with an <img> and the visitor's cookies would ride along.\n * This header is not CORS-safelisted, so the browser preflights it — the\n * same thing that makes a POST carrying X-RSC-Action safe.\n */\n query: 'X-RSC-Query',\n\n queryPath: '/_rsc/query',\n /** Build-time shell for a PPR route, for an edge cache to hold. */\n pprShellPath: '/_rsc/ppr-shell',\n /** Finish a shell an edge cache is already serving. */\n pprResumePath: '/_rsc/ppr-resume',\n /**\n * On a refresh a change triggered: which names moved, to which versions\n * (`stock@5,orders@2`). A shared section renders once per such change, and\n * every tab asking with the same one gets that render.\n */\n changedBy: 'X-RSC-Changed',\n /** The stream a tab hears on when a name its page refreshes on has changed. */\n changesPath: '/_rsc/changes',\n /** Whether this renderer can serve: up, and its backend answering. GET only, never cached. */\n healthPath: '/_rsc/health',\n} as const\n\n/** Flight payload. */\nexport const FLIGHT_TYPE = 'text/x-component; charset=utf-8'\n\n/** A rendered document. */\nexport const HTML_TYPE = 'text/html; charset=utf-8'\n\n/**\n * Every request header that changes what a response body contains.\n *\n * One url answers with a whole document, a partial payload, a named region or\n * an interceptor depending on these — so a cache keyed on the url alone, or on\n * `X-RSC` alone, will hand one client another's answer. They are materially\n * different bodies, not variations in presentation.\n */\nexport const VARY_ON_RSC = [\n HEADER.rsc,\n HEADER.segments,\n HEADER.revalidate,\n HEADER.intercept,\n HEADER.referer,\n].join(', ')\n\n/**\n * What a response says about being stored, when it must not be.\n *\n * `Vary` alone is not enough on the platform this targets. Cloudflare ignores\n * `Vary` for everything except `Accept-Encoding`, and this package ships a\n * Workers story as a headline feature — so one url answering with a document,\n * a Flight payload, a segment and an interceptor, keyed only on the url, hands\n * one visitor another's body. Anything narrowed by a request header, and\n * anything behind a guard, says outright that it is not to be stored.\n */\nexport const PER_CLIENT = 'private, no-store'\n\n/**\n * And what a frozen, unguarded page says.\n *\n * Explicit rather than absent: with no directive at all, whether an\n * intermediary stores it comes down to that intermediary's heuristics, which\n * is not a decision this package should be leaving to chance in either\n * direction.\n */\nexport const REVALIDATE = 'public, max-age=0, must-revalidate'\n\n/**\n * And what a page rendered for one request says.\n *\n * It may have read the visitor's cookies, and nothing in the headers - which\n * leave before the render has finished - can know whether it did. So a shared\n * cache may not keep it; the visitor's own browser may, and must ask again.\n */\nexport const PER_REQUEST = 'private, max-age=0, must-revalidate'\n"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The component of the route whose page or guard answered with notFound(), or
|
|
3
|
+
* undefined for a url no route owns. The entry in front asks, to find the
|
|
4
|
+
* nearest not-found.tsx above it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function notFoundRouteOf(request: Request): string | undefined;
|
|
1
7
|
/** Whether a page or guard of this app answered the request with notFound(). */
|
|
2
8
|
export declare function pageSaidNotFound(request: Request): boolean;
|
|
3
9
|
export { revalidate } from "./revalidate.js";
|
package/dist/host.js
CHANGED
|
@@ -38,10 +38,26 @@ const RENDER_NOT_FOUND = Symbol("render-not-found");
|
|
|
38
38
|
* showed. Asked with pageSaidNotFound().
|
|
39
39
|
*/
|
|
40
40
|
const notFoundRequests = new WeakSet();
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The route that said it, so not-found.tsx can be the nearest one above it.
|
|
43
|
+
*
|
|
44
|
+
* A url no route owns has none, and is answered by the root one.
|
|
45
|
+
*/
|
|
46
|
+
const notFoundFrom = new WeakMap();
|
|
47
|
+
function saidNotFound(request, route) {
|
|
42
48
|
notFoundRequests.add(request);
|
|
49
|
+
if (route !== undefined)
|
|
50
|
+
notFoundFrom.set(request, route);
|
|
43
51
|
return null;
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The component of the route whose page or guard answered with notFound(), or
|
|
55
|
+
* undefined for a url no route owns. The entry in front asks, to find the
|
|
56
|
+
* nearest not-found.tsx above it.
|
|
57
|
+
*/
|
|
58
|
+
export function notFoundRouteOf(request) {
|
|
59
|
+
return notFoundFrom.get(request);
|
|
60
|
+
}
|
|
45
61
|
/** Whether a page or guard of this app answered the request with notFound(). */
|
|
46
62
|
export function pageSaidNotFound(request) {
|
|
47
63
|
return notFoundRequests.has(request);
|
|
@@ -834,6 +850,15 @@ export function createRscHandler(options) {
|
|
|
834
850
|
headers: withVersion({ "X-RSC-Location": url.pathname + url.search, "Cache-Control": "no-store" }),
|
|
835
851
|
});
|
|
836
852
|
}
|
|
853
|
+
// The page said, after its shell was sent, that this url names nothing.
|
|
854
|
+
// The browser's boundary caught the mark and asks for the not-found page
|
|
855
|
+
// to show where the page was - the same answer a url no route owns gets.
|
|
856
|
+
// After the version check, so a client of another build is sent to load
|
|
857
|
+
// the document instead of being handed a tree its manifest cannot read.
|
|
858
|
+
// Anyone may ask for it: not-found.tsx is public by construction.
|
|
859
|
+
if (request.headers.get(HEADER.notFound) !== null && request.headers.get(HEADER.rsc) !== null) {
|
|
860
|
+
return saidNotFound(request, matchPage(routes, url)?.route.component);
|
|
861
|
+
}
|
|
837
862
|
// One named region of this page, asked for without mutating anything to
|
|
838
863
|
// earn it. What an action invalidated does not come through here — that
|
|
839
864
|
// travels back inside the action's own answer, which is the whole point of
|
|
@@ -864,7 +889,7 @@ export function createRscHandler(options) {
|
|
|
864
889
|
// Null, as a render that calls notFound() answers: the caller in front
|
|
865
890
|
// serves not-found.tsx with a 404, the same for a guard as for a page.
|
|
866
891
|
if (refusal === RENDER_NOT_FOUND)
|
|
867
|
-
return saidNotFound(request);
|
|
892
|
+
return saidNotFound(request, match?.route.component);
|
|
868
893
|
if (refusal)
|
|
869
894
|
return refusal;
|
|
870
895
|
const frozen = await servePrerendered(request, url, match, options.prerendered);
|
|
@@ -927,7 +952,7 @@ export function createRscHandler(options) {
|
|
|
927
952
|
// route are indistinguishable to whoever is asking — which is the
|
|
928
953
|
// point of a 404.
|
|
929
954
|
if (currentNotFound())
|
|
930
|
-
return saidNotFound(request);
|
|
955
|
+
return saidNotFound(request, match.route.component);
|
|
931
956
|
// A guard refusing is not a failed render. Without this a visitor
|
|
932
957
|
// who may not see the page gets a 500, which reads as the
|
|
933
958
|
// application being broken rather than them being turned away —
|
|
@@ -949,7 +974,7 @@ export function createRscHandler(options) {
|
|
|
949
974
|
// stays 200 because the status line has gone.
|
|
950
975
|
if (currentNotFound()) {
|
|
951
976
|
letGo();
|
|
952
|
-
return saidNotFound(request);
|
|
977
|
+
return saidNotFound(request, match.route.component);
|
|
953
978
|
}
|
|
954
979
|
// The payload beside the HTML: the browser hydrates from it rather
|
|
955
980
|
// than asking for a second render.
|
|
@@ -1000,7 +1025,7 @@ export function createRscHandler(options) {
|
|
|
1000
1025
|
// Same answer the document path gives, so a client navigating to a
|
|
1001
1026
|
// url and a browser loading it fresh agree about whether it exists.
|
|
1002
1027
|
if (currentNotFound())
|
|
1003
|
-
return saidNotFound(request);
|
|
1028
|
+
return saidNotFound(request, match.route.component);
|
|
1004
1029
|
// A payload request is guarded exactly as the document is. Narrowing
|
|
1005
1030
|
// a request must never narrow what is checked.
|
|
1006
1031
|
const status = refusalStatus(error);
|