@rsc-kit/core 0.18.1 → 0.20.0

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.
Files changed (77) hide show
  1. package/dist/action.d.ts +16 -2
  2. package/dist/action.js +21 -12
  3. package/dist/action.js.map +1 -1
  4. package/dist/apiPrerender.js +25 -7
  5. package/dist/apiPrerender.js.map +1 -1
  6. package/dist/clientPackages.d.ts +30 -0
  7. package/dist/clientPackages.js +234 -0
  8. package/dist/clientPackages.js.map +1 -0
  9. package/dist/compress.d.ts +12 -0
  10. package/dist/compress.js +134 -0
  11. package/dist/compress.js.map +1 -0
  12. package/dist/compressRuntime.d.ts +8 -0
  13. package/dist/compressRuntime.js +62 -0
  14. package/dist/compressRuntime.js.map +1 -0
  15. package/dist/files.d.ts +20 -0
  16. package/dist/files.js +38 -0
  17. package/dist/files.js.map +1 -1
  18. package/dist/formSubmit.d.ts +1 -0
  19. package/dist/formSubmit.js +14 -0
  20. package/dist/formSubmit.js.map +1 -0
  21. package/dist/host.d.ts +23 -0
  22. package/dist/host.js +175 -31
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +15 -0
  25. package/dist/hostCalls.js +75 -8
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/js/Form.d.ts +21 -4
  28. package/dist/js/Form.js +110 -83
  29. package/dist/js/Form.js.map +1 -1
  30. package/dist/js/Link.d.ts +5 -5
  31. package/dist/js/Link.js.map +1 -1
  32. package/dist/js/RedirectBoundary.js.map +1 -1
  33. package/dist/js/RouteErrorBoundary.d.ts +6 -2
  34. package/dist/js/RouteErrorBoundary.js +10 -0
  35. package/dist/js/RouteErrorBoundary.js.map +1 -1
  36. package/dist/js/SlotBoundary.d.ts +7 -1
  37. package/dist/js/SlotBoundary.js +9 -1
  38. package/dist/js/SlotBoundary.js.map +1 -1
  39. package/dist/js/createViteRscApp.d.ts +1 -0
  40. package/dist/js/createViteRscApp.js +54 -5
  41. package/dist/js/createViteRscApp.js.map +1 -1
  42. package/dist/js/errors.d.ts +3 -1
  43. package/dist/js/errors.js +26 -3
  44. package/dist/js/errors.js.map +1 -1
  45. package/dist/js/formEncoding.d.ts +72 -3
  46. package/dist/js/formEncoding.js +284 -20
  47. package/dist/js/formEncoding.js.map +1 -1
  48. package/dist/js/navigate.d.ts +6 -3
  49. package/dist/js/navigate.js +57 -7
  50. package/dist/js/navigate.js.map +1 -1
  51. package/dist/js/nuqs.js.map +1 -1
  52. package/dist/js/router.d.ts +3 -3
  53. package/dist/js/router.js.map +1 -1
  54. package/dist/js/updateStore.js +8 -2
  55. package/dist/js/updateStore.js.map +1 -1
  56. package/dist/manifest.d.ts +6 -0
  57. package/dist/manifest.js.map +1 -1
  58. package/dist/openapi.d.ts +74 -0
  59. package/dist/openapi.js +172 -0
  60. package/dist/openapi.js.map +1 -0
  61. package/dist/redirect.d.ts +4 -4
  62. package/dist/redirect.js.map +1 -1
  63. package/dist/request.d.ts +56 -2
  64. package/dist/request.js +68 -4
  65. package/dist/request.js.map +1 -1
  66. package/dist/routes.d.ts +17 -6
  67. package/dist/routes.js.map +1 -1
  68. package/dist/testing.d.ts +14 -0
  69. package/dist/testing.js +56 -2
  70. package/dist/testing.js.map +1 -1
  71. package/dist/useSsr.d.ts +11 -0
  72. package/dist/useSsr.js +15 -0
  73. package/dist/useSsr.js.map +1 -1
  74. package/dist/vite.d.ts +69 -1
  75. package/dist/vite.js +481 -51
  76. package/dist/vite.js.map +1 -1
  77. package/package.json +5 -1
@@ -82,6 +82,12 @@ export interface ManifestIntercept {
82
82
  export interface ManifestApiRoute {
83
83
  /** The module name, as the engine's registry keys it. */
84
84
  name: string;
85
+ /**
86
+ * The file that answers, relative to the source directory - `app/api/x/route.ts`,
87
+ * or `app/sitemap.ts` for a route the build synthesised from a metadata file
88
+ * - so a line about the route can say where to look.
89
+ */
90
+ source?: string;
85
91
  segments: RouteSegment[];
86
92
  /** Which methods the file exports, so a 405 can name the rest. */
87
93
  methods: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n /**\n * The file that answers, relative to the source directory - `app/api/x/route.ts`,\n * or `app/sitemap.ts` for a route the build synthesised from a metadata file\n * - so a line about the route can say where to look.\n */\n source?: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
@@ -0,0 +1,74 @@
1
+ /** What the generated entry hands over for each route.ts. */
2
+ export interface OpenApiRoute {
3
+ /** The route's pattern in this package's spelling: `/api/orders/[id]`. */
4
+ pattern: string;
5
+ methods: string[];
6
+ /** The middleware.ts files above it: a guarded route gets a security requirement. */
7
+ guarded: boolean;
8
+ /** The route module: its params, searchParams, body and openapi exports, if any. */
9
+ module: Record<string, unknown>;
10
+ }
11
+ export interface OpenApiInfo {
12
+ title?: string;
13
+ version?: string;
14
+ description?: string;
15
+ }
16
+ /**
17
+ * What the document says that no route can: where the API is served, and
18
+ * how a caller authenticates. Spread onto the document as written, so
19
+ * anything OpenAPI allows at the top level is allowed here.
20
+ */
21
+ export interface OpenApiDocumentOptions {
22
+ /**
23
+ * Which routes the document describes. `'all'` (the default) is every
24
+ * route.ts that did not opt out; `'declared'` is only the ones that
25
+ * export `openapi`, for an app whose routes are mostly webhooks and
26
+ * callbacks with a handful of endpoints meant for a caller to read about.
27
+ */
28
+ include?: "all" | "declared";
29
+ info?: OpenApiInfo;
30
+ servers?: {
31
+ url: string;
32
+ description?: string;
33
+ }[];
34
+ security?: Record<string, string[]>[];
35
+ components?: Record<string, unknown>;
36
+ tags?: {
37
+ name: string;
38
+ description?: string;
39
+ }[];
40
+ }
41
+ /**
42
+ * What a route.ts may say about itself, beside its handler:
43
+ *
44
+ * export const openapi = {
45
+ * summary: 'Chat completions',
46
+ * tags: ['Chat'],
47
+ * responses: { 200: { description: 'The completion', content: { … } } },
48
+ * }
49
+ *
50
+ * Merged onto every operation the file exports, or per method when keyed
51
+ * by one: `{ POST: { summary: … } }`. Anything OpenAPI allows on an operation.
52
+ * `false` leaves the route out of the document altogether; `{ DELETE: false }`
53
+ * leaves one method out. HEAD and OPTIONS are never documented: the engine
54
+ * answers them for every route.
55
+ */
56
+ export type OpenApiOperationExtras = Record<string, unknown>;
57
+ /** `/api/orders/[id]/route` → `/api/orders/{id}`, and the names it binds. */
58
+ export declare function openApiPath(pattern: string): {
59
+ path: string;
60
+ params: string[];
61
+ };
62
+ /**
63
+ * The document.
64
+ *
65
+ * Path parameters come from the pattern, typed by the route's `params` schema
66
+ * where it has one and as strings otherwise; query parameters from the
67
+ * `searchParams` schema's properties, each optional unless the schema
68
+ * requires it; a request body from the `body` schema, as JSON. A guarded
69
+ * route names the session as its security requirement, which is what a
70
+ * middleware.ts above it checks.
71
+ */
72
+ export declare function buildOpenApi(routes: OpenApiRoute[], options?: OpenApiDocumentOptions): Record<string, unknown>;
73
+ /** The document, as a route answers it: JSON, cacheable, built once. */
74
+ export declare function openApiResponse(routes: OpenApiRoute[], options?: OpenApiDocumentOptions): Response;
@@ -0,0 +1,172 @@
1
+ // An OpenAPI document from the route tree, and a page that reads it.
2
+ //
3
+ // Every route.ts already says what an operation needs: the methods it
4
+ // exports, and the params, searchParams and body schemas beside them. A
5
+ // Standard Schema describes itself as JSON Schema (Zod 4 and ArkType do;
6
+ // Valibot needs its converter and contributes nothing here), so the document
7
+ // is derived rather than written - the way Elysia derives its from TypeBox.
8
+ // Nothing is annotated twice.
9
+ //
10
+ // The document is built from the modules the generated entry imported, and
11
+ // stored by the build like any other api route that reads nothing per
12
+ // request. The page that reads it is Scalar's own package, mounted as a
13
+ // route: `export const GET = ApiReference({ url: '/openapi.json' })`.
14
+ const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15
+ /** The extras a route declared for one method: the shared ones, then that method's. */
16
+ function extrasFor(module, method) {
17
+ const declared = module.openapi;
18
+ if (declared === null || typeof declared !== "object")
19
+ return {};
20
+ const record = declared;
21
+ const shared = {};
22
+ const own = (record[method] ?? {});
23
+ for (const [key, value] of Object.entries(record)) {
24
+ if (!METHODS.has(key))
25
+ shared[key] = value;
26
+ }
27
+ return { ...shared, ...own };
28
+ }
29
+ /** A Standard Schema's JSON Schema, or null when it cannot describe itself. */
30
+ function jsonSchemaOf(schema) {
31
+ if (schema === null || typeof schema !== "object")
32
+ return null;
33
+ try {
34
+ const produce = schema["~standard"]?.jsonSchema?.input;
35
+ if (typeof produce !== "function")
36
+ return null;
37
+ // A leaf JSON Schema cannot say - a Date, a custom check - documents as
38
+ // `{}` rather than costing the route its whole body schema.
39
+ const json = produce({ target: "draft-2020-12", libraryOptions: { unrepresentable: "any" } });
40
+ // The dialect marker belongs on the document, not on every schema in it.
41
+ delete json.$schema;
42
+ return json;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** `/api/orders/[id]/route` → `/api/orders/{id}`, and the names it binds. */
49
+ export function openApiPath(pattern) {
50
+ const params = [];
51
+ const path = pattern.replace(/\[(?:\.\.\.)?(\w+)\]/g, (_, name) => {
52
+ params.push(name);
53
+ return `{${name}}`;
54
+ });
55
+ return { path, params };
56
+ }
57
+ const HAS_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);
58
+ /**
59
+ * The document.
60
+ *
61
+ * Path parameters come from the pattern, typed by the route's `params` schema
62
+ * where it has one and as strings otherwise; query parameters from the
63
+ * `searchParams` schema's properties, each optional unless the schema
64
+ * requires it; a request body from the `body` schema, as JSON. A guarded
65
+ * route names the session as its security requirement, which is what a
66
+ * middleware.ts above it checks.
67
+ */
68
+ export function buildOpenApi(routes, options = {}) {
69
+ const { info = {}, include = "all", ...rest } = options;
70
+ const paths = {};
71
+ let anyGuarded = false;
72
+ for (const route of routes) {
73
+ // `export const openapi = false`: a route that is not part of the API -
74
+ // the page that renders this document, a webhook for one caller. With
75
+ // include: 'declared', a route with no `openapi` export is the same.
76
+ if (route.module.openapi === false)
77
+ continue;
78
+ if (include === "declared" && route.module.openapi === undefined)
79
+ continue;
80
+ const { path, params } = openApiPath(route.pattern);
81
+ const paramsSchema = jsonSchemaOf(route.module.params);
82
+ const searchSchema = jsonSchemaOf(route.module.searchParams);
83
+ const bodySchema = jsonSchemaOf(route.module.body);
84
+ const pathParameters = params.map((name) => ({
85
+ name,
86
+ in: "path",
87
+ required: true,
88
+ schema: paramsSchema?.properties?.[name] ?? { type: "string" },
89
+ }));
90
+ const queryParameters = Object.entries(searchSchema?.properties ?? {}).map(([name, schema]) => ({
91
+ name,
92
+ in: "query",
93
+ required: searchSchema?.required?.includes(name) ?? false,
94
+ schema,
95
+ }));
96
+ const operations = {};
97
+ for (const method of route.methods) {
98
+ // HEAD and OPTIONS are answered for every route by the engine, and a
99
+ // file that exports one - a CORS preflight - is not documenting an
100
+ // operation. `openapi: { OPTIONS: false }` drops any other method.
101
+ if (method === "HEAD" || method === "OPTIONS")
102
+ continue;
103
+ if ((route.module.openapi?.[method]) === false)
104
+ continue;
105
+ const operation = {
106
+ operationId: `${method.toLowerCase()}${path
107
+ .replace(/\{(\w+)\}/g, "By$1")
108
+ .split("/")
109
+ .filter(Boolean)
110
+ .map((part) => part[0].toUpperCase() + part.slice(1))
111
+ .join("")}`,
112
+ parameters: [...pathParameters, ...queryParameters],
113
+ responses: { "200": { description: "OK" } },
114
+ };
115
+ if (bodySchema && HAS_BODY.has(method)) {
116
+ operation.requestBody = {
117
+ required: true,
118
+ content: { "application/json": { schema: bodySchema } },
119
+ };
120
+ operation.responses["422"] = {
121
+ description: "The input was refused; validationErrors names each field.",
122
+ };
123
+ }
124
+ if (route.guarded) {
125
+ anyGuarded = true;
126
+ operation.security = [{ session: [] }];
127
+ operation.responses["401"] = { description: "Not signed in." };
128
+ operation.responses["403"] = { description: "Signed in, and still not allowed." };
129
+ }
130
+ // What the route said about itself wins over what was derived, field
131
+ // by field; its responses merge onto the derived ones.
132
+ const extras = extrasFor(route.module, method);
133
+ const responses = { ...operation.responses, ...(extras.responses ?? {}) };
134
+ operations[method.toLowerCase()] = { ...operation, ...extras, responses };
135
+ }
136
+ paths[path] = { ...(paths[path] ?? {}), ...operations };
137
+ }
138
+ const document = {
139
+ openapi: "3.1.0",
140
+ info: {
141
+ title: info.title ?? "API",
142
+ version: info.version ?? "0.0.0",
143
+ ...(info.description ? { description: info.description } : {}),
144
+ },
145
+ ...rest,
146
+ paths,
147
+ };
148
+ if (anyGuarded) {
149
+ const components = (rest.components ?? {});
150
+ const schemes = (components.securitySchemes ?? {});
151
+ document.components = {
152
+ ...components,
153
+ securitySchemes: {
154
+ session: {
155
+ type: "apiKey",
156
+ in: "cookie",
157
+ name: "session",
158
+ description: "The visitor's session, checked by the middleware.ts above the route.",
159
+ },
160
+ ...schemes,
161
+ },
162
+ };
163
+ }
164
+ return document;
165
+ }
166
+ /** The document, as a route answers it: JSON, cacheable, built once. */
167
+ export function openApiResponse(routes, options = {}) {
168
+ return Response.json(buildOpenApi(routes, options), {
169
+ headers: { "Cache-Control": "public, max-age=300" },
170
+ });
171
+ }
172
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["// An OpenAPI document from the route tree, and a page that reads it.\n//\n// Every route.ts already says what an operation needs: the methods it\n// exports, and the params, searchParams and body schemas beside them. A\n// Standard Schema describes itself as JSON Schema (Zod 4 and ArkType do;\n// Valibot needs its converter and contributes nothing here), so the document\n// is derived rather than written - the way Elysia derives its from TypeBox.\n// Nothing is annotated twice.\n//\n// The document is built from the modules the generated entry imported, and\n// stored by the build like any other api route that reads nothing per\n// request. The page that reads it is Scalar's own package, mounted as a\n// route: `export const GET = ApiReference({ url: '/openapi.json' })`.\n\ntype JsonSchema = Record<string, unknown> & {\n type?: string | string[];\n properties?: Record<string, JsonSchema>;\n required?: string[];\n};\n\ntype WithJsonSchema = {\n \"~standard\"?: {\n jsonSchema?: {\n input?: (options: { target: string; libraryOptions?: Record<string, unknown> }) => unknown;\n };\n };\n};\n\n/** What the generated entry hands over for each route.ts. */\nexport interface OpenApiRoute {\n /** The route's pattern in this package's spelling: `/api/orders/[id]`. */\n pattern: string;\n methods: string[];\n /** The middleware.ts files above it: a guarded route gets a security requirement. */\n guarded: boolean;\n /** The route module: its params, searchParams, body and openapi exports, if any. */\n module: Record<string, unknown>;\n}\n\nconst METHODS = new Set([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/** The extras a route declared for one method: the shared ones, then that method's. */\nfunction extrasFor(module: Record<string, unknown>, method: string): OpenApiOperationExtras {\n const declared = module.openapi;\n\n if (declared === null || typeof declared !== \"object\") return {};\n\n const record = declared as Record<string, unknown>;\n const shared: Record<string, unknown> = {};\n const own = (record[method] ?? {}) as Record<string, unknown>;\n\n for (const [key, value] of Object.entries(record)) {\n if (!METHODS.has(key)) shared[key] = value;\n }\n\n return { ...shared, ...own };\n}\n\nexport interface OpenApiInfo {\n title?: string;\n version?: string;\n description?: string;\n}\n\n/**\n * What the document says that no route can: where the API is served, and\n * how a caller authenticates. Spread onto the document as written, so\n * anything OpenAPI allows at the top level is allowed here.\n */\nexport interface OpenApiDocumentOptions {\n /**\n * Which routes the document describes. `'all'` (the default) is every\n * route.ts that did not opt out; `'declared'` is only the ones that\n * export `openapi`, for an app whose routes are mostly webhooks and\n * callbacks with a handful of endpoints meant for a caller to read about.\n */\n include?: \"all\" | \"declared\";\n info?: OpenApiInfo;\n servers?: { url: string; description?: string }[];\n security?: Record<string, string[]>[];\n components?: Record<string, unknown>;\n tags?: { name: string; description?: string }[];\n}\n\n/**\n * What a route.ts may say about itself, beside its handler:\n *\n * export const openapi = {\n * summary: 'Chat completions',\n * tags: ['Chat'],\n * responses: { 200: { description: 'The completion', content: { … } } },\n * }\n *\n * Merged onto every operation the file exports, or per method when keyed\n * by one: `{ POST: { summary: … } }`. Anything OpenAPI allows on an operation.\n * `false` leaves the route out of the document altogether; `{ DELETE: false }`\n * leaves one method out. HEAD and OPTIONS are never documented: the engine\n * answers them for every route.\n */\nexport type OpenApiOperationExtras = Record<string, unknown>;\n\n/** A Standard Schema's JSON Schema, or null when it cannot describe itself. */\nfunction jsonSchemaOf(schema: unknown): JsonSchema | null {\n if (schema === null || typeof schema !== \"object\") return null;\n\n try {\n const produce = (schema as WithJsonSchema)[\"~standard\"]?.jsonSchema?.input;\n\n if (typeof produce !== \"function\") return null;\n\n // A leaf JSON Schema cannot say - a Date, a custom check - documents as\n // `{}` rather than costing the route its whole body schema.\n const json = produce({ target: \"draft-2020-12\", libraryOptions: { unrepresentable: \"any\" } }) as JsonSchema;\n\n // The dialect marker belongs on the document, not on every schema in it.\n delete json.$schema;\n\n return json;\n } catch {\n return null;\n }\n}\n\n/** `/api/orders/[id]/route` → `/api/orders/{id}`, and the names it binds. */\nexport function openApiPath(pattern: string): { path: string; params: string[] } {\n const params: string[] = [];\n const path = pattern.replace(/\\[(?:\\.\\.\\.)?(\\w+)\\]/g, (_, name: string) => {\n params.push(name);\n\n return `{${name}}`;\n });\n\n return { path, params };\n}\n\nconst HAS_BODY = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * The document.\n *\n * Path parameters come from the pattern, typed by the route's `params` schema\n * where it has one and as strings otherwise; query parameters from the\n * `searchParams` schema's properties, each optional unless the schema\n * requires it; a request body from the `body` schema, as JSON. A guarded\n * route names the session as its security requirement, which is what a\n * middleware.ts above it checks.\n */\nexport function buildOpenApi(routes: OpenApiRoute[], options: OpenApiDocumentOptions = {}): Record<string, unknown> {\n const { info = {}, include = \"all\", ...rest } = options;\n const paths: Record<string, Record<string, unknown>> = {};\n let anyGuarded = false;\n\n for (const route of routes) {\n // `export const openapi = false`: a route that is not part of the API -\n // the page that renders this document, a webhook for one caller. With\n // include: 'declared', a route with no `openapi` export is the same.\n if (route.module.openapi === false) continue;\n if (include === \"declared\" && route.module.openapi === undefined) continue;\n\n const { path, params } = openApiPath(route.pattern);\n const paramsSchema = jsonSchemaOf(route.module.params);\n const searchSchema = jsonSchemaOf(route.module.searchParams);\n const bodySchema = jsonSchemaOf(route.module.body);\n\n const pathParameters = params.map((name) => ({\n name,\n in: \"path\",\n required: true,\n schema: paramsSchema?.properties?.[name] ?? { type: \"string\" },\n }));\n\n const queryParameters = Object.entries(searchSchema?.properties ?? {}).map(([name, schema]) => ({\n name,\n in: \"query\",\n required: searchSchema?.required?.includes(name) ?? false,\n schema,\n }));\n\n const operations: Record<string, unknown> = {};\n\n for (const method of route.methods) {\n // HEAD and OPTIONS are answered for every route by the engine, and a\n // file that exports one - a CORS preflight - is not documenting an\n // operation. `openapi: { OPTIONS: false }` drops any other method.\n if (method === \"HEAD\" || method === \"OPTIONS\") continue;\n if (((route.module.openapi as Record<string, unknown> | undefined)?.[method]) === false) continue;\n\n const operation: Record<string, unknown> = {\n operationId: `${method.toLowerCase()}${path\n .replace(/\\{(\\w+)\\}/g, \"By$1\")\n .split(\"/\")\n .filter(Boolean)\n .map((part) => part[0].toUpperCase() + part.slice(1))\n .join(\"\")}`,\n parameters: [...pathParameters, ...queryParameters],\n responses: { \"200\": { description: \"OK\" } },\n };\n\n if (bodySchema && HAS_BODY.has(method)) {\n operation.requestBody = {\n required: true,\n content: { \"application/json\": { schema: bodySchema } },\n };\n (operation.responses as Record<string, unknown>)[\"422\"] = {\n description: \"The input was refused; validationErrors names each field.\",\n };\n }\n\n if (route.guarded) {\n anyGuarded = true;\n operation.security = [{ session: [] }];\n (operation.responses as Record<string, unknown>)[\"401\"] = { description: \"Not signed in.\" };\n (operation.responses as Record<string, unknown>)[\"403\"] = { description: \"Signed in, and still not allowed.\" };\n }\n\n // What the route said about itself wins over what was derived, field\n // by field; its responses merge onto the derived ones.\n const extras = extrasFor(route.module, method);\n const responses = { ...(operation.responses as object), ...((extras.responses as object) ?? {}) };\n\n operations[method.toLowerCase()] = { ...operation, ...extras, responses };\n }\n\n paths[path] = { ...(paths[path] ?? {}), ...operations };\n }\n\n const document: Record<string, unknown> = {\n openapi: \"3.1.0\",\n info: {\n title: info.title ?? \"API\",\n version: info.version ?? \"0.0.0\",\n ...(info.description ? { description: info.description } : {}),\n },\n ...rest,\n paths,\n };\n\n if (anyGuarded) {\n const components = (rest.components ?? {}) as Record<string, unknown>;\n const schemes = (components.securitySchemes ?? {}) as Record<string, unknown>;\n\n document.components = {\n ...components,\n securitySchemes: {\n session: {\n type: \"apiKey\",\n in: \"cookie\",\n name: \"session\",\n description: \"The visitor's session, checked by the middleware.ts above the route.\",\n },\n ...schemes,\n },\n };\n }\n\n return document;\n}\n\n/** The document, as a route answers it: JSON, cacheable, built once. */\nexport function openApiResponse(routes: OpenApiRoute[], options: OpenApiDocumentOptions = {}): Response {\n return Response.json(buildOpenApi(routes, options), {\n headers: { \"Cache-Control\": \"public, max-age=300\" },\n });\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { Href, SearchFor } from './routes.js';
1
+ import type { Route, SearchFor } from './routes.js';
2
2
  import type { Redirection } from './redirectDigest.js';
3
3
  export { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js';
4
4
  export type { Redirection } from './redirectDigest.js';
@@ -10,7 +10,7 @@ export type { Redirection } from './redirectDigest.js';
10
10
  * swallowing this turns a redirect into a blank region.
11
11
  *
12
12
  * `location` is typed to the routes the build found, so a redirect to a page
13
- * that no longer exists stops compiling. Cast with `as Href` when the
13
+ * that no longer exists stops compiling. Cast with `as Route` when the
14
14
  * destination is computed — remembering where someone was going and sending
15
15
  * them back to it is the usual case.
16
16
  *
@@ -26,7 +26,7 @@ export type { Redirection } from './redirectDigest.js';
26
26
  * same check `Link` puts on its `search` prop - so a key the page never
27
27
  * reads, or a number written as text, does not compile.
28
28
  */
29
- export type RedirectOptions<H extends Href> = ({} extends SearchFor<H> ? {
29
+ export type RedirectOptions<H extends Route> = ({} extends SearchFor<H> ? {
30
30
  search?: SearchFor<H>;
31
31
  } : {
32
32
  search: SearchFor<H>;
@@ -34,7 +34,7 @@ export type RedirectOptions<H extends Href> = ({} extends SearchFor<H> ? {
34
34
  /** 307 unless said otherwise; 308 for a permanent one. */
35
35
  status?: number;
36
36
  };
37
- export declare function redirect<H extends Href>(location: H, ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]): never;
37
+ export declare function redirect<H extends Route>(location: H, ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]): never;
38
38
  /** The redirect asked for during the current render, if any. */
39
39
  export declare function currentRedirect(): Redirection | null;
40
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":"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","sourcesContent":["// Redirecting from inside a render.\n//\n// import { redirect } from '@rsc-kit/core/redirect'\n//\n// export default async function ProductPage({ slug }) {\n// const product = await findProduct(slug)\n// if (!product) redirect('/products')\n// ...\n// }\n//\n// The hard part is not throwing — it is that a redirect decided during a\n// render may be decided after the response has already begun. Headers flush\n// early on purpose (see the stream-start invariant), so by the time a\n// component deep in a Suspense boundary changes its mind, the 200 is gone.\n//\n// So there are two windows, and which one a redirect lands in is decided by\n// where it was thrown rather than by anything the caller does:\n//\n// Before the shell resolves — nothing has been written. The host answers\n// with a real 3xx, or for a navigation with X-RSC-Redirect, and the browser\n// never sees the page. This is the window every guard lands in, because a\n// guard runs above the boundaries.\n//\n// After the shell resolves — the shell is already on the wire. It holds\n// layouts and Suspense fallbacks, so no data from the redirecting subtree\n// has been shown. The location travels to the client instead, which\n// performs it as an ordinary SPA navigation.\n//\n// Neither window costs a buffered byte. The first exists because the host\n// already awaits the shell before writing anything; the second because React\n// already carries an error digest to the client.\n\nimport { assertSafeRedirect } from './safeUrl.js'\nimport { withSearch } from './routes.js'\nimport type { Href, SearchFor } from './routes.js'\nimport { resolveScope } from './revalidate.js'\nimport { RedirectSignal } from './redirectDigest.js'\nimport type { Redirection } from './redirectDigest.js'\n\nexport { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js'\nexport type { Redirection } from './redirectDigest.js'\n\n/** Per-render state, for the same reason revalidation has it: two can be in flight. */\ninterface Slot {\n redirect: Redirection | null\n /**\n * Whether the render asked for the not-found page.\n *\n * Shares this scope rather than opening a second one, because it is the same\n * question asked once per render — \"did this page decide to answer as\n * something other than itself\" — and every render path is already wrapped in\n * this one. notFound.ts sets it through the scope symbol without importing\n * this module; see the note there.\n */\n notFound?: boolean\n}\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\n/**\n * One scope, however many copies of this module exist.\n *\n * The app's components are bundled into the server bundle and the host is\n * not, so this module is loaded twice. Two scopes would mean the component\n * records in one and the host reads the other: the redirect is simply never\n * honoured, and nothing on either side reports a problem.\n */\nconst SCOPE = Symbol.for('@rsc-kit/core.redirect-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\n/**\n * Leave this page for another one.\n *\n * Never returns: it throws, which stops the component that called it. Do not\n * wrap a call in `try`/`catch` without rethrowing what you do not recognise —\n * swallowing this turns a redirect into a blank region.\n *\n * `location` is typed to the routes the build found, so a redirect to a page\n * that no longer exists stops compiling. Cast with `as Href` when the\n * destination is computed — remembering where someone was going and sending\n * them back to it is the usual case.\n *\n * Where it is called decides how it is delivered, and the difference matters\n * for anything that must not be seen. A call above every Suspense boundary is\n * answered with a status code before a byte is written. A call inside one is\n * answered after the shell — which holds no data from inside the boundary,\n * but does hold whatever the layouts above it rendered.\n */\n/**\n * What a redirect may carry beside its destination. `search` is typed to the\n * destination page's own `searchParams` schema when it exports one - the\n * same check `Link` puts on its `search` prop - so a key the page never\n * reads, or a number written as text, does not compile.\n */\nexport type RedirectOptions<H extends Href> = ({} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> }) & {\n /** 307 unless said otherwise; 308 for a permanent one. */\n status?: number\n}\n\nexport function redirect<H extends Href>(\n location: H,\n ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]\n): never {\n // The second argument was a status alone once; it still is, and an object\n // carries the query string beside it.\n const options = typeof rest[0] === 'number' ? { status: rest[0] } : (rest[0] ?? {})\n const status = options.status ?? 307\n const search = (options as { search?: object }).search\n\n return redirectTo(search ? (withSearch(location, search) as Href) : location, status)\n}\n\nfunction redirectTo(location: Href, status: number): never {\n // Refused here, at the one place every delivery path leads back to. The\n // destination reaches location.href on the client and an inline script in a\n // document, and a javascript: url runs in all of them.\n assertSafeRedirect(location)\n\n // A redirect is a 3xx. Anything else produces a response carrying Location\n // that no browser acts on — the page renders, the redirect silently does not\n // happen, and nothing says why.\n if (status < 300 || status > 399) {\n throw new Error(\n 'Not a redirect status: ' + status + '. A redirect is 3xx; 307 preserves the method.',\n )\n }\n\n const store = scope()?.getStore()\n\n // First wins. A layout that redirects and a page that also redirects should\n // land where the outer one said, not wherever finished last.\n if (store && !store.redirect) {\n store.redirect = { location, status }\n }\n\n throw new RedirectSignal(location, status)\n}\n\n/** The redirect asked for during the current render, if any. */\nexport function currentRedirect(): Redirection | null {\n return scope()?.getStore()?.redirect ?? null\n}\n\n/**\n * Whether the current render called notFound().\n *\n * Read rather than caught, for the reason the redirect above is: React\n * re-raises a component's error as its own, so the thrown signal does not\n * arrive intact at the host in production.\n */\nexport function currentNotFound(): boolean {\n return scope()?.getStore()?.notFound === true\n}\n\n/**\n * Run a render with somewhere for a redirect to be recorded, and report one.\n *\n * `taken()` is read twice by a streaming host: once when the shell resolves,\n * to answer with a status code, and again when the stream ends, for a\n * redirect that arrived too late for one.\n */\nexport async function withRedirect<T>(\n run: (taken: () => Redirection | null) => Promise<T>,\n): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const slot: Slot = { redirect: null, notFound: false }\n\n return await scope()!.run(slot, () => run(() => slot.redirect))\n}\n"]}
1
+ {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":"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","sourcesContent":["// Redirecting from inside a render.\n//\n// import { redirect } from '@rsc-kit/core/redirect'\n//\n// export default async function ProductPage({ slug }) {\n// const product = await findProduct(slug)\n// if (!product) redirect('/products')\n// ...\n// }\n//\n// The hard part is not throwing — it is that a redirect decided during a\n// render may be decided after the response has already begun. Headers flush\n// early on purpose (see the stream-start invariant), so by the time a\n// component deep in a Suspense boundary changes its mind, the 200 is gone.\n//\n// So there are two windows, and which one a redirect lands in is decided by\n// where it was thrown rather than by anything the caller does:\n//\n// Before the shell resolves — nothing has been written. The host answers\n// with a real 3xx, or for a navigation with X-RSC-Redirect, and the browser\n// never sees the page. This is the window every guard lands in, because a\n// guard runs above the boundaries.\n//\n// After the shell resolves — the shell is already on the wire. It holds\n// layouts and Suspense fallbacks, so no data from the redirecting subtree\n// has been shown. The location travels to the client instead, which\n// performs it as an ordinary SPA navigation.\n//\n// Neither window costs a buffered byte. The first exists because the host\n// already awaits the shell before writing anything; the second because React\n// already carries an error digest to the client.\n\nimport { assertSafeRedirect } from './safeUrl.js'\nimport { withSearch } from './routes.js'\nimport type { Route, SearchFor } from './routes.js'\nimport { resolveScope } from './revalidate.js'\nimport { RedirectSignal } from './redirectDigest.js'\nimport type { Redirection } from './redirectDigest.js'\n\nexport { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js'\nexport type { Redirection } from './redirectDigest.js'\n\n/** Per-render state, for the same reason revalidation has it: two can be in flight. */\ninterface Slot {\n redirect: Redirection | null\n /**\n * Whether the render asked for the not-found page.\n *\n * Shares this scope rather than opening a second one, because it is the same\n * question asked once per render — \"did this page decide to answer as\n * something other than itself\" — and every render path is already wrapped in\n * this one. notFound.ts sets it through the scope symbol without importing\n * this module; see the note there.\n */\n notFound?: boolean\n}\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\n/**\n * One scope, however many copies of this module exist.\n *\n * The app's components are bundled into the server bundle and the host is\n * not, so this module is loaded twice. Two scopes would mean the component\n * records in one and the host reads the other: the redirect is simply never\n * honoured, and nothing on either side reports a problem.\n */\nconst SCOPE = Symbol.for('@rsc-kit/core.redirect-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\n/**\n * Leave this page for another one.\n *\n * Never returns: it throws, which stops the component that called it. Do not\n * wrap a call in `try`/`catch` without rethrowing what you do not recognise —\n * swallowing this turns a redirect into a blank region.\n *\n * `location` is typed to the routes the build found, so a redirect to a page\n * that no longer exists stops compiling. Cast with `as Route` when the\n * destination is computed — remembering where someone was going and sending\n * them back to it is the usual case.\n *\n * Where it is called decides how it is delivered, and the difference matters\n * for anything that must not be seen. A call above every Suspense boundary is\n * answered with a status code before a byte is written. A call inside one is\n * answered after the shell — which holds no data from inside the boundary,\n * but does hold whatever the layouts above it rendered.\n */\n/**\n * What a redirect may carry beside its destination. `search` is typed to the\n * destination page's own `searchParams` schema when it exports one - the\n * same check `Link` puts on its `search` prop - so a key the page never\n * reads, or a number written as text, does not compile.\n */\nexport type RedirectOptions<H extends Route> = ({} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> }) & {\n /** 307 unless said otherwise; 308 for a permanent one. */\n status?: number\n}\n\nexport function redirect<H extends Route>(\n location: H,\n ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]\n): never {\n // The second argument was a status alone once; it still is, and an object\n // carries the query string beside it.\n const options = typeof rest[0] === 'number' ? { status: rest[0] } : (rest[0] ?? {})\n const status = options.status ?? 307\n const search = (options as { search?: object }).search\n\n return redirectTo(search ? (withSearch(location, search) as Route) : location, status)\n}\n\nfunction redirectTo(location: Route, status: number): never {\n // Refused here, at the one place every delivery path leads back to. The\n // destination reaches location.href on the client and an inline script in a\n // document, and a javascript: url runs in all of them.\n assertSafeRedirect(location)\n\n // A redirect is a 3xx. Anything else produces a response carrying Location\n // that no browser acts on — the page renders, the redirect silently does not\n // happen, and nothing says why.\n if (status < 300 || status > 399) {\n throw new Error(\n 'Not a redirect status: ' + status + '. A redirect is 3xx; 307 preserves the method.',\n )\n }\n\n const store = scope()?.getStore()\n\n // First wins. A layout that redirects and a page that also redirects should\n // land where the outer one said, not wherever finished last.\n if (store && !store.redirect) {\n store.redirect = { location, status }\n }\n\n throw new RedirectSignal(location, status)\n}\n\n/** The redirect asked for during the current render, if any. */\nexport function currentRedirect(): Redirection | null {\n return scope()?.getStore()?.redirect ?? null\n}\n\n/**\n * Whether the current render called notFound().\n *\n * Read rather than caught, for the reason the redirect above is: React\n * re-raises a component's error as its own, so the thrown signal does not\n * arrive intact at the host in production.\n */\nexport function currentNotFound(): boolean {\n return scope()?.getStore()?.notFound === true\n}\n\n/**\n * Run a render with somewhere for a redirect to be recorded, and report one.\n *\n * `taken()` is read twice by a streaming host: once when the shell resolves,\n * to answer with a status code, and again when the stream ends, for a\n * redirect that arrived too late for one.\n */\nexport async function withRedirect<T>(\n run: (taken: () => Redirection | null) => Promise<T>,\n): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const slot: Slot = { redirect: null, notFound: false }\n\n return await scope()!.run(slot, () => run(() => slot.redirect))\n}\n"]}
package/dist/request.d.ts CHANGED
@@ -21,10 +21,25 @@ export interface CookieOptions {
21
21
  sameSite?: "strict" | "lax" | "none";
22
22
  partitioned?: boolean;
23
23
  }
24
+ /** One cookie as the request carried it: the shape Next's `cookies().get()` returns. */
25
+ export interface RequestCookie {
26
+ name: string;
27
+ value: string;
28
+ }
29
+ /**
30
+ * The cookie jar, in the shape Next's `cookies()` has.
31
+ *
32
+ * `get()` returns `{ name, value }` rather than the string, because the guide
33
+ * says "the same names from `@rsc-kit/core/request`" and a name that is the
34
+ * same with a different return shape is the worst of both: ported code
35
+ * reading `?.value` off a string got `undefined`, silently. `getAll()` is a
36
+ * list for the same reason, and because a list composes where a record does
37
+ * not.
38
+ */
24
39
  export interface Cookies {
25
- get(name: string): string | undefined;
40
+ get(name: string): RequestCookie | undefined;
26
41
  has(name: string): boolean;
27
- getAll(): Record<string, string>;
42
+ getAll(): RequestCookie[];
28
43
  /**
29
44
  * Write one on the response.
30
45
  *
@@ -32,8 +47,12 @@ export interface Cookies {
32
47
  * been sent. A render has already flushed its headers by the time a
33
48
  * component runs — that is what makes the first paint fast — so this throws
34
49
  * there rather than appearing to work.
50
+ *
51
+ * Either call shape Next takes: `set(name, value, options)` or
52
+ * `set({ name, value, ...options })`.
35
53
  */
36
54
  set(name: string, value: string, options?: CookieOptions): void;
55
+ set(cookie: RequestCookie & CookieOptions): void;
37
56
  /** Write one that expires immediately. Same rule about where. */
38
57
  delete(name: string, options?: CookieOptions): void;
39
58
  }
@@ -114,6 +133,19 @@ export declare function withRequest<T>(from: RequestLike, run: () => Promise<T>)
114
133
  * call that never answers: the component suspends, its Suspense fallback goes
115
134
  * into the shell, and the probe's budget decides the rest.
116
135
  */
136
+ /**
137
+ * The build's probe hands a route a Request that records what is read out of
138
+ * it, and a read of `url` marks the route as depending on the caller — the
139
+ * Next way to read a query is `new URL(request.url).searchParams`, which the
140
+ * probe cannot otherwise see. The engine itself reads the url to resolve the
141
+ * awaited `searchParams`, and that read is accounted for by `searchParams`,
142
+ * not by `url`; this is the door it goes through. A probe answers the real
143
+ * Request to this key; anything else answers nothing, and the request is its
144
+ * own.
145
+ */
146
+ export declare const UNPROBED: unique symbol;
147
+ /** The request's url, read by the engine rather than the route. */
148
+ export declare function urlOf(request: Request): string;
117
149
  /**
118
150
  * Record a read without suspending on it.
119
151
  *
@@ -142,6 +174,28 @@ export declare function requestWasRead(): boolean;
142
174
  export declare function requestReadBy(): string[];
143
175
  /** A read React caught at a boundary during SSR: `useSearchParams() in Query`. */
144
176
  export declare function noteFallback(text: string): void;
177
+ /**
178
+ * Run something once the answer is on its way, without making it wait.
179
+ *
180
+ * Logging, an audit row, an email, a cache warm: work the visitor should not
181
+ * pay for, and that must still finish. Next's `after()`, and needed for the
182
+ * same reason on every host: on a long-lived process a detached promise
183
+ * happens to run to completion, but a Worker tears the isolate down when the
184
+ * response ends unless the work is registered with the platform's
185
+ * `waitUntil` - so a fire-and-forget promise there dies silently, some of
186
+ * the time. The host hands these to `waitUntil` where one exists and runs
187
+ * them detached where a process will keep them.
188
+ *
189
+ * From a component, a middleware, a server action or an api route. A
190
+ * rejection is reported and never reaches the response, which has already
191
+ * gone. Outside a request - a build - the work runs at once.
192
+ */
193
+ export declare function after(work: () => unknown): void;
194
+ /**
195
+ * @internal For the host: the work `after()` collected for this request, as
196
+ * one promise that never rejects. Taken once; a second call has nothing.
197
+ */
198
+ export declare function takeAfterWork(): Promise<void> | null;
145
199
  /** The reads caught at a boundary during this render, with their components. */
146
200
  export declare function requestFallbacks(): string[];
147
201
  /**
package/dist/request.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // import { headers, cookies } from '@rsc-kit/core/request'
4
4
  //
5
5
  // export default async function middleware() {
6
- // const locale = cookies().get('locale') ?? negotiate(headers().get('accept-language'))
6
+ // const locale = cookies().get('locale')?.value ?? negotiate(headers().get('accept-language'))
7
7
  // if (!locale) redirect('/en')
8
8
  // }
9
9
  //
@@ -310,10 +310,15 @@ export async function cookies() {
310
310
  writable("cookies().set()").headers.append("Set-Cookie", serializeCookie({ name, value, options }));
311
311
  };
312
312
  return {
313
- get: (name) => parsed[name],
313
+ get: (name) => (name in parsed ? { name, value: parsed[name] } : undefined),
314
314
  has: (name) => name in parsed,
315
- getAll: () => ({ ...parsed }),
316
- set: write,
315
+ getAll: () => Object.entries(parsed).map(([name, value]) => ({ name, value })),
316
+ set: (nameOrCookie, value, options) => {
317
+ if (typeof nameOrCookie === "string")
318
+ return write(nameOrCookie, value ?? "", options);
319
+ const { name, value: v, ...rest } = nameOrCookie;
320
+ return write(name, v, rest);
321
+ },
317
322
  // Expired rather than removed: a browser drops a cookie when it is told
318
323
  // one has already passed, and there is no other way to say it.
319
324
  delete: (name, options = {}) => write(name, "", { ...options, maxAge: 0 }),
@@ -424,6 +429,7 @@ export async function withRequest(from, run) {
424
429
  awaiters: new Map(),
425
430
  inHelper: null,
426
431
  readVia: new Map(),
432
+ after: [],
427
433
  };
428
434
  return await scope().run(store, run);
429
435
  }
@@ -434,6 +440,21 @@ export async function withRequest(from, run) {
434
440
  * call that never answers: the component suspends, its Suspense fallback goes
435
441
  * into the shell, and the probe's budget decides the rest.
436
442
  */
443
+ /**
444
+ * The build's probe hands a route a Request that records what is read out of
445
+ * it, and a read of `url` marks the route as depending on the caller — the
446
+ * Next way to read a query is `new URL(request.url).searchParams`, which the
447
+ * probe cannot otherwise see. The engine itself reads the url to resolve the
448
+ * awaited `searchParams`, and that read is accounted for by `searchParams`,
449
+ * not by `url`; this is the door it goes through. A probe answers the real
450
+ * Request to this key; anything else answers nothing, and the request is its
451
+ * own.
452
+ */
453
+ export const UNPROBED = Symbol.for("rsc-kit.unprobed");
454
+ /** The request's url, read by the engine rather than the route. */
455
+ export function urlOf(request) {
456
+ return (request[UNPROBED] ?? request).url;
457
+ }
437
458
  /**
438
459
  * Record a read without suspending on it.
439
460
  *
@@ -484,6 +505,49 @@ export function noteFallback(text) {
484
505
  if (!store.fallbacks.includes(text))
485
506
  store.fallbacks.push(text);
486
507
  }
508
+ /**
509
+ * Run something once the answer is on its way, without making it wait.
510
+ *
511
+ * Logging, an audit row, an email, a cache warm: work the visitor should not
512
+ * pay for, and that must still finish. Next's `after()`, and needed for the
513
+ * same reason on every host: on a long-lived process a detached promise
514
+ * happens to run to completion, but a Worker tears the isolate down when the
515
+ * response ends unless the work is registered with the platform's
516
+ * `waitUntil` - so a fire-and-forget promise there dies silently, some of
517
+ * the time. The host hands these to `waitUntil` where one exists and runs
518
+ * them detached where a process will keep them.
519
+ *
520
+ * From a component, a middleware, a server action or an api route. A
521
+ * rejection is reported and never reaches the response, which has already
522
+ * gone. Outside a request - a build - the work runs at once.
523
+ */
524
+ export function after(work) {
525
+ const store = scope()?.getStore();
526
+ if (!store || !store.request) {
527
+ void Promise.resolve().then(work).catch(reportAfter);
528
+ return;
529
+ }
530
+ store.after.push(work);
531
+ }
532
+ function reportAfter(error) {
533
+ console.error("[rsc-kit] after() work failed:", error);
534
+ }
535
+ /**
536
+ * @internal For the host: the work `after()` collected for this request, as
537
+ * one promise that never rejects. Taken once; a second call has nothing.
538
+ */
539
+ export function takeAfterWork() {
540
+ const store = scope()?.getStore();
541
+ if (!store || store.after.length === 0)
542
+ return null;
543
+ const work = store.after.splice(0);
544
+ return Promise.allSettled(work.map((run) => Promise.resolve().then(run))).then((results) => {
545
+ for (const result of results) {
546
+ if (result.status === "rejected")
547
+ reportAfter(result.reason);
548
+ }
549
+ });
550
+ }
487
551
  /** The reads caught at a boundary during this render, with their components. */
488
552
  export function requestFallbacks() {
489
553
  return scope()?.getStore()?.fallbacks ?? [];