@rsc-kit/core 0.30.26 → 0.30.28
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 +11 -0
- package/dist/action.js +20 -1
- package/dist/action.js.map +1 -1
- package/dist/edgeShells.d.ts +52 -0
- package/dist/edgeShells.js +98 -0
- package/dist/edgeShells.js.map +1 -0
- package/dist/headers.d.ts +17 -4
- package/dist/headers.js +17 -4
- package/dist/headers.js.map +1 -1
- package/dist/host.js +147 -84
- package/dist/host.js.map +1 -1
- package/dist/vite.js +32 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/action.d.ts
CHANGED
|
@@ -242,5 +242,16 @@ export interface ActionClientOptions {
|
|
|
242
242
|
*/
|
|
243
243
|
onError?: (error: unknown) => string;
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* The status the backend turned this caller away with - 401, 403 or 404 - or
|
|
247
|
+
* null for anything else.
|
|
248
|
+
*
|
|
249
|
+
* The action endpoint answers a plain function's throw with it, as it answers
|
|
250
|
+
* a refusal: the stub the build writes for a backend action is a plain
|
|
251
|
+
* function, and a record the platform could not find reached the dashboard as
|
|
252
|
+
* a 500 and "Something went wrong.", where createActionClient showed the
|
|
253
|
+
* backend's message.
|
|
254
|
+
*/
|
|
255
|
+
export declare function turnedAwayStatus(error: unknown): 401 | 403 | 404 | null;
|
|
245
256
|
export declare function createActionClient(options?: ActionClientOptions): ActionBuilder<Record<never, never>, undefined>;
|
|
246
257
|
export {};
|
package/dist/action.js
CHANGED
|
@@ -148,7 +148,26 @@ const GENERIC = 'Something went wrong.';
|
|
|
148
148
|
* nothing there for them. Each carries a message written for the person.
|
|
149
149
|
*/
|
|
150
150
|
function turnedAway(error) {
|
|
151
|
-
return (error
|
|
151
|
+
return turnedAwayStatus(error) !== null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The status the backend turned this caller away with - 401, 403 or 404 - or
|
|
155
|
+
* null for anything else.
|
|
156
|
+
*
|
|
157
|
+
* The action endpoint answers a plain function's throw with it, as it answers
|
|
158
|
+
* a refusal: the stub the build writes for a backend action is a plain
|
|
159
|
+
* function, and a record the platform could not find reached the dashboard as
|
|
160
|
+
* a 500 and "Something went wrong.", where createActionClient showed the
|
|
161
|
+
* backend's message.
|
|
162
|
+
*/
|
|
163
|
+
export function turnedAwayStatus(error) {
|
|
164
|
+
if (error instanceof ServerAuthenticationError)
|
|
165
|
+
return 401;
|
|
166
|
+
if (error instanceof ServerAuthorizationError)
|
|
167
|
+
return 403;
|
|
168
|
+
if (isNotFoundSignal(error))
|
|
169
|
+
return 404;
|
|
170
|
+
return null;
|
|
152
171
|
}
|
|
153
172
|
/**
|
|
154
173
|
* FormData in, a plain object out — what a schema expects to be handed.
|
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\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"]}
|
|
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 turnedAwayStatus(error) !== null\n}\n\n/**\n * The status the backend turned this caller away with - 401, 403 or 404 - or\n * null for anything else.\n *\n * The action endpoint answers a plain function's throw with it, as it answers\n * a refusal: the stub the build writes for a backend action is a plain\n * function, and a record the platform could not find reached the dashboard as\n * a 500 and \"Something went wrong.\", where createActionClient showed the\n * backend's message.\n */\nexport function turnedAwayStatus(error: unknown): 401 | 403 | 404 | null {\n if (error instanceof ServerAuthenticationError) return 401\n if (error instanceof ServerAuthorizationError) return 403\n if (isNotFoundSignal(error)) return 404\n\n return null\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"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The url patterns of the pages an edge may serve from a cached shell, for
|
|
3
|
+
* `.output/ppr.json` - the marker of the PPR contract, which an edge reads the
|
|
4
|
+
* same way for every framework that implements it.
|
|
5
|
+
*
|
|
6
|
+
* An edge in front of the app asks one question of each request - is this a
|
|
7
|
+
* PPR page? - and answers it from a list it was handed at deploy time. Reading
|
|
8
|
+
* `rsc-static/` for it does not work: a guarded page has a shell on disk too,
|
|
9
|
+
* served from a cache it skips its guard; `_team_` cannot say whether it was a
|
|
10
|
+
* parameter or a folder of that name; and the layout is ours to change.
|
|
11
|
+
*
|
|
12
|
+
* So the list is the build's, by the same rule the shell endpoint applies: a
|
|
13
|
+
* page with a shell, on a route with no middleware - neither a `middleware.ts`
|
|
14
|
+
* above it nor the host's `export const middleware`. The endpoint refuses
|
|
15
|
+
* exactly those, so the list and the origin cannot disagree.
|
|
16
|
+
*/
|
|
17
|
+
import type { RouteSegment } from './manifest.js';
|
|
18
|
+
/** What an edge is promised, and what it can hold. */
|
|
19
|
+
export declare const MAX_SHELLS = 200;
|
|
20
|
+
export declare const MAX_SHELL_BYTES = 200;
|
|
21
|
+
/**
|
|
22
|
+
* The marker. `bypass` names this framework's own requests on a page's path
|
|
23
|
+
* that are not a visitor's document: every payload fetch - a navigation, a
|
|
24
|
+
* prefetch, a refresh - carries X-RSC, and an action is a POST, which an edge
|
|
25
|
+
* never answers from a shell anyway.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pprMarker(build: string, shells: string[]): {
|
|
28
|
+
v: number;
|
|
29
|
+
build: string;
|
|
30
|
+
shells: string[];
|
|
31
|
+
bypass: {
|
|
32
|
+
headers: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export interface ShellRoute {
|
|
36
|
+
component: string;
|
|
37
|
+
segments: RouteSegment[];
|
|
38
|
+
middleware?: readonly string[];
|
|
39
|
+
hostMiddleware?: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The patterns, `/` for the index, `[name]` for one segment and a final
|
|
43
|
+
* `[...name]` for one or more - and what was left out of the list, and why,
|
|
44
|
+
* for the build to say.
|
|
45
|
+
*/
|
|
46
|
+
export declare function edgeShells(results: readonly {
|
|
47
|
+
type: string;
|
|
48
|
+
component: string;
|
|
49
|
+
}[], routes: readonly ShellRoute[]): {
|
|
50
|
+
shells: string[];
|
|
51
|
+
left: string[];
|
|
52
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The url patterns of the pages an edge may serve from a cached shell, for
|
|
3
|
+
* `.output/ppr.json` - the marker of the PPR contract, which an edge reads the
|
|
4
|
+
* same way for every framework that implements it.
|
|
5
|
+
*
|
|
6
|
+
* An edge in front of the app asks one question of each request - is this a
|
|
7
|
+
* PPR page? - and answers it from a list it was handed at deploy time. Reading
|
|
8
|
+
* `rsc-static/` for it does not work: a guarded page has a shell on disk too,
|
|
9
|
+
* served from a cache it skips its guard; `_team_` cannot say whether it was a
|
|
10
|
+
* parameter or a folder of that name; and the layout is ours to change.
|
|
11
|
+
*
|
|
12
|
+
* So the list is the build's, by the same rule the shell endpoint applies: a
|
|
13
|
+
* page with a shell, on a route with no middleware - neither a `middleware.ts`
|
|
14
|
+
* above it nor the host's `export const middleware`. The endpoint refuses
|
|
15
|
+
* exactly those, so the list and the origin cannot disagree.
|
|
16
|
+
*/
|
|
17
|
+
/** What an edge is promised, and what it can hold. */
|
|
18
|
+
export const MAX_SHELLS = 200;
|
|
19
|
+
export const MAX_SHELL_BYTES = 200;
|
|
20
|
+
/**
|
|
21
|
+
* The marker. `bypass` names this framework's own requests on a page's path
|
|
22
|
+
* that are not a visitor's document: every payload fetch - a navigation, a
|
|
23
|
+
* prefetch, a refresh - carries X-RSC, and an action is a POST, which an edge
|
|
24
|
+
* never answers from a shell anyway.
|
|
25
|
+
*/
|
|
26
|
+
export function pprMarker(build, shells) {
|
|
27
|
+
return { v: 1, build, shells, bypass: { headers: ['X-RSC'] } };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The patterns, `/` for the index, `[name]` for one segment and a final
|
|
31
|
+
* `[...name]` for one or more - and what was left out of the list, and why,
|
|
32
|
+
* for the build to say.
|
|
33
|
+
*/
|
|
34
|
+
export function edgeShells(results, routes) {
|
|
35
|
+
const byComponent = new Map(routes.map((route) => [route.component, route]));
|
|
36
|
+
const shells = [];
|
|
37
|
+
const left = [];
|
|
38
|
+
for (const component of new Set(results.filter((r) => r.type === 'shell').map((r) => r.component))) {
|
|
39
|
+
const route = byComponent.get(component);
|
|
40
|
+
if (!route)
|
|
41
|
+
continue;
|
|
42
|
+
if (route.middleware?.length || route.hostMiddleware?.length)
|
|
43
|
+
continue;
|
|
44
|
+
// Bound from the request's host, which a path pattern cannot say: the
|
|
45
|
+
// same pattern would match every tenant's page on every domain.
|
|
46
|
+
if (route.segments.some((segment) => segment.type === 'host'))
|
|
47
|
+
continue;
|
|
48
|
+
const pattern = patternOf(route.segments);
|
|
49
|
+
if (pattern === null)
|
|
50
|
+
continue;
|
|
51
|
+
if (new TextEncoder().encode(pattern).length > MAX_SHELL_BYTES) {
|
|
52
|
+
left.push(`${pattern.slice(0, 60)}… is over ${MAX_SHELL_BYTES} bytes`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!shells.includes(pattern))
|
|
56
|
+
shells.push(pattern);
|
|
57
|
+
}
|
|
58
|
+
// Over the cap, the deepest are the ones not listed: an edge that does not
|
|
59
|
+
// know a page is a PPR page sends it to the origin, which answers it
|
|
60
|
+
// correctly, and the shallow pages are the ones most visited.
|
|
61
|
+
if (shells.length > MAX_SHELLS) {
|
|
62
|
+
shells.sort((a, b) => depth(a) - depth(b) || (a < b ? -1 : 1));
|
|
63
|
+
left.push(`${shells.length - MAX_SHELLS} past the shallowest ${MAX_SHELLS}`);
|
|
64
|
+
shells.length = MAX_SHELLS;
|
|
65
|
+
}
|
|
66
|
+
shells.sort();
|
|
67
|
+
return { shells, left };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A route's segments as the edge matches them, or null when it cannot: a
|
|
71
|
+
* catch-all anywhere but last, or a name that is itself bracketed - an
|
|
72
|
+
* optional catch-all, `[[...name]]`, matches no url at all.
|
|
73
|
+
*/
|
|
74
|
+
function patternOf(segments) {
|
|
75
|
+
const parts = [];
|
|
76
|
+
for (const [index, segment] of segments.entries()) {
|
|
77
|
+
// Not a path an edge can match: the contract has no empty, `.` or `..`
|
|
78
|
+
// segment, and a name with a slash would be two.
|
|
79
|
+
if (['', '.', '..'].includes(segment.value) || segment.value.includes('/'))
|
|
80
|
+
return null;
|
|
81
|
+
if (segment.type === 'static')
|
|
82
|
+
parts.push(segment.value);
|
|
83
|
+
else if (segment.value.includes('[') || segment.value.includes(']'))
|
|
84
|
+
return null;
|
|
85
|
+
else if (segment.type === 'catchAll') {
|
|
86
|
+
if (index !== segments.length - 1)
|
|
87
|
+
return null;
|
|
88
|
+
parts.push(`[...${segment.value}]`);
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
parts.push(`[${segment.value}]`);
|
|
92
|
+
}
|
|
93
|
+
return '/' + parts.join('/');
|
|
94
|
+
}
|
|
95
|
+
function depth(pattern) {
|
|
96
|
+
return pattern === '/' ? 0 : pattern.split('/').length - 1;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=edgeShells.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edgeShells.js","sourceRoot":"","sources":["../src/edgeShells.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The url patterns of the pages an edge may serve from a cached shell, for\n * `.output/ppr.json` - the marker of the PPR contract, which an edge reads the\n * same way for every framework that implements it.\n *\n * An edge in front of the app asks one question of each request - is this a\n * PPR page? - and answers it from a list it was handed at deploy time. Reading\n * `rsc-static/` for it does not work: a guarded page has a shell on disk too,\n * served from a cache it skips its guard; `_team_` cannot say whether it was a\n * parameter or a folder of that name; and the layout is ours to change.\n *\n * So the list is the build's, by the same rule the shell endpoint applies: a\n * page with a shell, on a route with no middleware - neither a `middleware.ts`\n * above it nor the host's `export const middleware`. The endpoint refuses\n * exactly those, so the list and the origin cannot disagree.\n */\n\nimport type { RouteSegment } from './manifest.js'\n\n/** What an edge is promised, and what it can hold. */\nexport const MAX_SHELLS = 200\nexport const MAX_SHELL_BYTES = 200\n\n/**\n * The marker. `bypass` names this framework's own requests on a page's path\n * that are not a visitor's document: every payload fetch - a navigation, a\n * prefetch, a refresh - carries X-RSC, and an action is a POST, which an edge\n * never answers from a shell anyway.\n */\nexport function pprMarker(build: string, shells: string[]) {\n return { v: 1, build, shells, bypass: { headers: ['X-RSC'] } }\n}\n\nexport interface ShellRoute {\n component: string\n segments: RouteSegment[]\n middleware?: readonly string[]\n hostMiddleware?: readonly string[]\n}\n\n/**\n * The patterns, `/` for the index, `[name]` for one segment and a final\n * `[...name]` for one or more - and what was left out of the list, and why,\n * for the build to say.\n */\nexport function edgeShells(\n results: readonly { type: string; component: string }[],\n routes: readonly ShellRoute[],\n): { shells: string[]; left: string[] } {\n const byComponent = new Map(routes.map((route) => [route.component, route]))\n const shells: string[] = []\n const left: string[] = []\n\n for (const component of new Set(results.filter((r) => r.type === 'shell').map((r) => r.component))) {\n const route = byComponent.get(component)\n\n if (!route) continue\n if (route.middleware?.length || route.hostMiddleware?.length) continue\n\n // Bound from the request's host, which a path pattern cannot say: the\n // same pattern would match every tenant's page on every domain.\n if (route.segments.some((segment) => segment.type === 'host')) continue\n\n const pattern = patternOf(route.segments)\n\n if (pattern === null) continue\n\n if (new TextEncoder().encode(pattern).length > MAX_SHELL_BYTES) {\n left.push(`${pattern.slice(0, 60)}… is over ${MAX_SHELL_BYTES} bytes`)\n continue\n }\n\n if (!shells.includes(pattern)) shells.push(pattern)\n }\n\n // Over the cap, the deepest are the ones not listed: an edge that does not\n // know a page is a PPR page sends it to the origin, which answers it\n // correctly, and the shallow pages are the ones most visited.\n if (shells.length > MAX_SHELLS) {\n shells.sort((a, b) => depth(a) - depth(b) || (a < b ? -1 : 1))\n left.push(`${shells.length - MAX_SHELLS} past the shallowest ${MAX_SHELLS}`)\n shells.length = MAX_SHELLS\n }\n\n shells.sort()\n\n return { shells, left }\n}\n\n/**\n * A route's segments as the edge matches them, or null when it cannot: a\n * catch-all anywhere but last, or a name that is itself bracketed - an\n * optional catch-all, `[[...name]]`, matches no url at all.\n */\nfunction patternOf(segments: readonly RouteSegment[]): string | null {\n const parts: string[] = []\n\n for (const [index, segment] of segments.entries()) {\n // Not a path an edge can match: the contract has no empty, `.` or `..`\n // segment, and a name with a slash would be two.\n if (['', '.', '..'].includes(segment.value) || segment.value.includes('/')) return null\n if (segment.type === 'static') parts.push(segment.value)\n else if (segment.value.includes('[') || segment.value.includes(']')) return null\n else if (segment.type === 'catchAll') {\n if (index !== segments.length - 1) return null\n parts.push(`[...${segment.value}]`)\n } else parts.push(`[${segment.value}]`)\n }\n\n return '/' + parts.join('/')\n}\n\nfunction depth(pattern: string): number {\n return pattern === '/' ? 0 : pattern.split('/').length - 1\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -70,10 +70,6 @@ export declare const HEADER: {
|
|
|
70
70
|
*/
|
|
71
71
|
readonly query: 'X-RSC-Query';
|
|
72
72
|
readonly queryPath: '/_rsc/query';
|
|
73
|
-
/** Build-time shell for a PPR route, for an edge cache to hold. */
|
|
74
|
-
readonly pprShellPath: '/_rsc/ppr-shell';
|
|
75
|
-
/** Finish a shell an edge cache is already serving. */
|
|
76
|
-
readonly pprResumePath: '/_rsc/ppr-resume';
|
|
77
73
|
/**
|
|
78
74
|
* On a refresh a change triggered: which names moved, to which versions
|
|
79
75
|
* (`stock@5,orders@2`). A shared section renders once per such change, and
|
|
@@ -126,3 +122,20 @@ export declare const REVALIDATE = "public, max-age=0, must-revalidate";
|
|
|
126
122
|
* cache may not keep it; the visitor's own browser may, and must ask again.
|
|
127
123
|
*/
|
|
128
124
|
export declare const PER_REQUEST = "private, max-age=0, must-revalidate";
|
|
125
|
+
/**
|
|
126
|
+
* The PPR contract: one marker, one shell endpoint, one resume, for any edge
|
|
127
|
+
* and any framework that implements it. Vendor-neutral names on purpose: an
|
|
128
|
+
* edge speaks the contract, and there is no second way in that is rsc-kit's
|
|
129
|
+
* own. docs/hosts/deployment.mdx has the whole of it.
|
|
130
|
+
*/
|
|
131
|
+
export declare const PPR: {
|
|
132
|
+
/** Reserved: nothing under it is the app's. */
|
|
133
|
+
readonly prefix: '/_ppr/';
|
|
134
|
+
readonly shellPath: '/_ppr/shell';
|
|
135
|
+
readonly resumePath: '/_ppr/resume';
|
|
136
|
+
/** The platform's per-release token. Unset, both endpoints are a 404. */
|
|
137
|
+
readonly tokenEnv: 'PPR_TOKEN';
|
|
138
|
+
readonly token: 'PPR-Token';
|
|
139
|
+
/** The build a shell or holes belong to - the marker's `build`. */
|
|
140
|
+
readonly build: 'PPR-Build';
|
|
141
|
+
};
|
package/dist/headers.js
CHANGED
|
@@ -79,10 +79,6 @@ export const HEADER = {
|
|
|
79
79
|
*/
|
|
80
80
|
query: 'X-RSC-Query',
|
|
81
81
|
queryPath: '/_rsc/query',
|
|
82
|
-
/** Build-time shell for a PPR route, for an edge cache to hold. */
|
|
83
|
-
pprShellPath: '/_rsc/ppr-shell',
|
|
84
|
-
/** Finish a shell an edge cache is already serving. */
|
|
85
|
-
pprResumePath: '/_rsc/ppr-resume',
|
|
86
82
|
/**
|
|
87
83
|
* On a refresh a change triggered: which names moved, to which versions
|
|
88
84
|
* (`stock@5,orders@2`). A shared section renders once per such change, and
|
|
@@ -141,4 +137,21 @@ export const REVALIDATE = 'public, max-age=0, must-revalidate';
|
|
|
141
137
|
* cache may not keep it; the visitor's own browser may, and must ask again.
|
|
142
138
|
*/
|
|
143
139
|
export const PER_REQUEST = 'private, max-age=0, must-revalidate';
|
|
140
|
+
/**
|
|
141
|
+
* The PPR contract: one marker, one shell endpoint, one resume, for any edge
|
|
142
|
+
* and any framework that implements it. Vendor-neutral names on purpose: an
|
|
143
|
+
* edge speaks the contract, and there is no second way in that is rsc-kit's
|
|
144
|
+
* own. docs/hosts/deployment.mdx has the whole of it.
|
|
145
|
+
*/
|
|
146
|
+
export const PPR = {
|
|
147
|
+
/** Reserved: nothing under it is the app's. */
|
|
148
|
+
prefix: '/_ppr/',
|
|
149
|
+
shellPath: '/_ppr/shell',
|
|
150
|
+
resumePath: '/_ppr/resume',
|
|
151
|
+
/** The platform's per-release token. Unset, both endpoints are a 404. */
|
|
152
|
+
tokenEnv: 'PPR_TOKEN',
|
|
153
|
+
token: 'PPR-Token',
|
|
154
|
+
/** The build a shell or holes belong to - the marker's `build`. */
|
|
155
|
+
build: 'PPR-Build',
|
|
156
|
+
};
|
|
144
157
|
//# sourceMappingURL=headers.js.map
|
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;;;;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
|
|
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;;;;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;AAEhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,+CAA+C;IAC/C,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,aAAa;IACxB,UAAU,EAAE,cAAc;IAC1B,yEAAyE;IACzE,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,WAAW;IAClB,mEAAmE;IACnE,KAAK,EAAE,WAAW;CACV,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 /**\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\n/**\n * The PPR contract: one marker, one shell endpoint, one resume, for any edge\n * and any framework that implements it. Vendor-neutral names on purpose: an\n * edge speaks the contract, and there is no second way in that is rsc-kit's\n * own. docs/hosts/deployment.mdx has the whole of it.\n */\nexport const PPR = {\n /** Reserved: nothing under it is the app's. */\n prefix: '/_ppr/',\n shellPath: '/_ppr/shell',\n resumePath: '/_ppr/resume',\n /** The platform's per-release token. Unset, both endpoints are a 404. */\n tokenEnv: 'PPR_TOKEN',\n token: 'PPR-Token',\n /** The build a shell or holes belong to - the marker's `build`. */\n build: 'PPR-Build',\n} as const\n"]}
|