@rsc-kit/core 0.30.12 → 0.30.14
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/js/Form.d.ts +20 -5
- package/dist/js/Form.js +4 -1
- package/dist/js/Form.js.map +1 -1
- package/dist/vite.d.ts +24 -0
- package/dist/vite.js +134 -17
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/vite.js
CHANGED
|
@@ -157,6 +157,7 @@ let typesDir;
|
|
|
157
157
|
let hotFile;
|
|
158
158
|
let hostCallOptions;
|
|
159
159
|
let packageDir;
|
|
160
|
+
let backendPaths = [];
|
|
160
161
|
let hostGlobal;
|
|
161
162
|
let interceptManifestFile;
|
|
162
163
|
let packageAlias;
|
|
@@ -348,6 +349,34 @@ function aliasEntries() {
|
|
|
348
349
|
});
|
|
349
350
|
return entries;
|
|
350
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The prefixes a backend owns, as the matcher reads them: `/auth`, no trailing
|
|
354
|
+
* slash. Refused rather than corrected where a mistake would silently send the
|
|
355
|
+
* whole app, or its own endpoints, to the backend.
|
|
356
|
+
*/
|
|
357
|
+
export function normaliseBackendPaths(paths) {
|
|
358
|
+
const out = [];
|
|
359
|
+
for (const raw of paths ?? []) {
|
|
360
|
+
const given = typeof raw === "string" ? raw.trim() : "";
|
|
361
|
+
if (!given.startsWith("/")) {
|
|
362
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is not a path. Each starts with a slash, as in '/auth'.`);
|
|
363
|
+
}
|
|
364
|
+
// After the slash check, so "/" is the root and not "not a path".
|
|
365
|
+
const path = given.replace(/\/+$/, "");
|
|
366
|
+
if (path === "") {
|
|
367
|
+
throw new Error("[rsc-kit] backendPaths: '/' is every url, and nothing would be left for this app. Name the prefixes the backend owns.");
|
|
368
|
+
}
|
|
369
|
+
if (path === "/_rsc" || path.startsWith("/_rsc/")) {
|
|
370
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is this package's own endpoint, which the renderer answers.`);
|
|
371
|
+
}
|
|
372
|
+
if (/[?#*]/.test(path)) {
|
|
373
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment or wildcard.`);
|
|
374
|
+
}
|
|
375
|
+
if (!out.includes(path))
|
|
376
|
+
out.push(path);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
351
380
|
function resolvePaths(options) {
|
|
352
381
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
353
382
|
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
|
|
@@ -385,6 +414,7 @@ function resolvePaths(options) {
|
|
|
385
414
|
hotFile = options.hotFile || process.env.RSC_HOT_FILE || "";
|
|
386
415
|
hostCallOptions = options.hostCall;
|
|
387
416
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
417
|
+
backendPaths = normaliseBackendPaths(options.backendPaths);
|
|
388
418
|
hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL || "rpc";
|
|
389
419
|
interceptManifestFile = resolve(options.interceptManifestFile ||
|
|
390
420
|
process.env.RSC_INTERCEPT_MANIFEST ||
|
|
@@ -2155,13 +2185,64 @@ function tsParams(sig) {
|
|
|
2155
2185
|
params.push("...rest: Array<" + tsOf(sig.rest) + ">");
|
|
2156
2186
|
return params.join(", ");
|
|
2157
2187
|
}
|
|
2158
|
-
/**
|
|
2159
|
-
function
|
|
2160
|
-
const
|
|
2161
|
-
const ref = first?.$ref;
|
|
2188
|
+
/** A parameter's schema with its reference followed, for decoding a form into it. */
|
|
2189
|
+
function resolveParam(param) {
|
|
2190
|
+
const ref = param?.$ref;
|
|
2162
2191
|
if (typeof ref === "string" && ref.startsWith("#/defs/"))
|
|
2163
2192
|
return hostDefs[ref.slice(7)] ?? null;
|
|
2164
|
-
return
|
|
2193
|
+
return param ?? null;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* The parameter a form lands on: the last one that is a struct.
|
|
2197
|
+
*
|
|
2198
|
+
* A form is one object, and a backend action may take others before it - the
|
|
2199
|
+
* team an action is scoped to, bound with `stub.bind(null, team)` - so it is
|
|
2200
|
+
* the struct at the end, not the first parameter, that the fields belong to.
|
|
2201
|
+
* -1 when no parameter is one, and then a form is not what the action takes.
|
|
2202
|
+
*/
|
|
2203
|
+
function formParam(sig) {
|
|
2204
|
+
const params = sig?.params ?? [];
|
|
2205
|
+
for (let i = params.length - 1; i >= 0; i--) {
|
|
2206
|
+
const schema = resolveParam(params[i]);
|
|
2207
|
+
if (schema && (schema.type === "object" || schema.properties))
|
|
2208
|
+
return i;
|
|
2209
|
+
}
|
|
2210
|
+
return -1;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Every name a struct can be read or refused under, as a TypeScript type: its
|
|
2214
|
+
* fields, a nested object's `address.city`, and a list's `items.${number}`.
|
|
2215
|
+
* The dotted paths a validation reports, so `error()` is closed to them.
|
|
2216
|
+
*/
|
|
2217
|
+
function fieldNames(schema, prefix = "", depth = 3) {
|
|
2218
|
+
const properties = (schema?.properties ?? {});
|
|
2219
|
+
const out = [];
|
|
2220
|
+
for (const [key, raw] of Object.entries(properties)) {
|
|
2221
|
+
const prop = resolveParam(raw);
|
|
2222
|
+
const path = prefix + key;
|
|
2223
|
+
out.push(path);
|
|
2224
|
+
if (depth === 0 || !prop)
|
|
2225
|
+
continue;
|
|
2226
|
+
if (prop.type === "array") {
|
|
2227
|
+
out.push(path + ".${number}");
|
|
2228
|
+
const item = resolveParam(prop.items);
|
|
2229
|
+
if (item?.properties)
|
|
2230
|
+
out.push(...fieldNames(item, path + ".${number}.", depth - 1));
|
|
2231
|
+
}
|
|
2232
|
+
else if (prop.properties) {
|
|
2233
|
+
out.push(...fieldNames(prop, path + ".", depth - 1));
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return out;
|
|
2237
|
+
}
|
|
2238
|
+
/** What a form posted to this action is: FormFields naming the struct's fields, or a plain FormData. */
|
|
2239
|
+
function formType(sig) {
|
|
2240
|
+
const index = formParam(sig);
|
|
2241
|
+
const names = index < 0 ? [] : fieldNames(resolveParam(sig.params[index]));
|
|
2242
|
+
if (names.length === 0)
|
|
2243
|
+
return "FormData";
|
|
2244
|
+
const literal = (name) => (name.includes("${") ? "`" + name + "`" : JSON.stringify(name));
|
|
2245
|
+
return "FormFields<" + [...new Set(names)].map(literal).join(" | ") + ">";
|
|
2165
2246
|
}
|
|
2166
2247
|
/**
|
|
2167
2248
|
* A form posted to a backend action. React hands the action a FormData; on
|
|
@@ -2170,10 +2251,16 @@ function firstParamSchema(sig) {
|
|
|
2170
2251
|
* fields become one object, typed by the first parameter where the backend
|
|
2171
2252
|
* described it: numbers and booleans coerced, a repeated name a list.
|
|
2172
2253
|
*/
|
|
2173
|
-
const FORM_ARGS = `function formArgs(args: unknown[],
|
|
2174
|
-
|
|
2254
|
+
const FORM_ARGS = `function formArgs(args: unknown[], schemas: (Record<string, any> | null)[]): unknown[] {
|
|
2255
|
+
// The form is the last argument, and it fills the parameter it lands on: the
|
|
2256
|
+
// first when the action takes nothing before it, the one after the team when
|
|
2257
|
+
// that was bound with stub.bind(null, team).
|
|
2258
|
+
const at = args.length - 1
|
|
2175
2259
|
|
|
2176
|
-
|
|
2260
|
+
if (at < 0 || !(args[at] instanceof FormData)) return args
|
|
2261
|
+
|
|
2262
|
+
const form = args[at] as FormData
|
|
2263
|
+
const schema = schemas[at] ?? null
|
|
2177
2264
|
const props: Record<string, any> = schema?.properties ?? {}
|
|
2178
2265
|
const coerce = (type: unknown, v: string): unknown =>
|
|
2179
2266
|
type === 'integer' || type === 'number' ? (v === '' ? null : Number(v))
|
|
@@ -2198,7 +2285,7 @@ const FORM_ARGS = `function formArgs(args: unknown[], schema: Record<string, any
|
|
|
2198
2285
|
if (!(key in out) && prop?.type === 'boolean') out[key] = false
|
|
2199
2286
|
}
|
|
2200
2287
|
|
|
2201
|
-
return [out]
|
|
2288
|
+
return [...args.slice(0, at), out]
|
|
2202
2289
|
}
|
|
2203
2290
|
`;
|
|
2204
2291
|
/** The "use server" module exposing each host function as a plain async call. */
|
|
@@ -2209,7 +2296,8 @@ function renderHostActions() {
|
|
|
2209
2296
|
"",
|
|
2210
2297
|
];
|
|
2211
2298
|
// Type-only, so the module stays what "use server" allows: functions.
|
|
2212
|
-
lines.push('import type { Redirected } from "@rsc-kit/core/errors";'
|
|
2299
|
+
lines.push('import type { Redirected } from "@rsc-kit/core/errors";');
|
|
2300
|
+
lines.push('import type { FormFields } from "@rsc-kit/core/form";', "");
|
|
2213
2301
|
lines.push(FORM_ARGS);
|
|
2214
2302
|
for (const [name, target] of Object.entries(hostActions)) {
|
|
2215
2303
|
const sig = hostTypes[target];
|
|
@@ -2222,7 +2310,13 @@ function renderHostActions() {
|
|
|
2222
2310
|
// it as data.
|
|
2223
2311
|
const result = "Promise<" + (sig.result ? tsOf(sig.result) : "void") + " | Redirected>";
|
|
2224
2312
|
lines.push("export async function " + name + "(" + tsParams(sig) + "): " + result + ";");
|
|
2225
|
-
|
|
2313
|
+
// The form overload is last, which is the one `stub.bind(null, team)`
|
|
2314
|
+
// reads: bound, it is `(form: FormFields<…>) => …`, so a form's action
|
|
2315
|
+
// keeps the struct's field names. Leading parameters are the ones the
|
|
2316
|
+
// form does not fill; any after the struct are left to the backend.
|
|
2317
|
+
const at = formParam(sig);
|
|
2318
|
+
const leading = at > 0 ? sig.params.slice(0, at).map((p, i) => "arg" + (i + 1) + ": " + tsOf(p)) : [];
|
|
2319
|
+
lines.push("export async function " + name + "(" + [...leading, "form: " + formType(sig)].join(", ") + "): " + result + ";");
|
|
2226
2320
|
}
|
|
2227
2321
|
lines.push("export async function " + name + "(...args: unknown[]) {");
|
|
2228
2322
|
lines.push(" return await (globalThis as any)." +
|
|
@@ -2230,7 +2324,7 @@ function renderHostActions() {
|
|
|
2230
2324
|
"(" +
|
|
2231
2325
|
JSON.stringify(target) +
|
|
2232
2326
|
", ...formArgs(args, " +
|
|
2233
|
-
JSON.stringify(
|
|
2327
|
+
JSON.stringify((sig?.params ?? []).map(resolveParam)) +
|
|
2234
2328
|
"));");
|
|
2235
2329
|
lines.push("}");
|
|
2236
2330
|
lines.push("");
|
|
@@ -2705,17 +2799,40 @@ function fallbackOrigin(): string {
|
|
|
2705
2799
|
const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
|
|
2706
2800
|
const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
|
|
2707
2801
|
`;
|
|
2802
|
+
/**
|
|
2803
|
+
* The urls the app said belong to the backend, tested before any page is
|
|
2804
|
+
* matched. Emitted in both variants: a test's stand-in backend is reached by
|
|
2805
|
+
* the same prefixes as the real one.
|
|
2806
|
+
*/
|
|
2807
|
+
function backendPathConsts() {
|
|
2808
|
+
return `const BACKEND_PATHS: string[] = ${JSON.stringify(backendPaths)}
|
|
2809
|
+
|
|
2810
|
+
// A prefix at a segment boundary: '/auth' is '/auth' and what is under it, not '/authors'.
|
|
2811
|
+
function onBackendPath(pathname: string): boolean {
|
|
2812
|
+
return BACKEND_PATHS.some((prefix) => pathname === prefix || pathname.startsWith(prefix + '/'))
|
|
2813
|
+
}
|
|
2814
|
+
`;
|
|
2815
|
+
}
|
|
2708
2816
|
/** With no backend configured: this app's not-found page, or a test's backend. */
|
|
2709
|
-
const NO_FALLBACK_BODY = `
|
|
2817
|
+
const NO_FALLBACK_BODY = ` // The app named this prefix as the backend's: no page is asked about it, so one
|
|
2818
|
+
// that matches (a dynamic root route) cannot answer for it.
|
|
2819
|
+
const toBackend = onBackendPath(new URL(request.url).pathname)
|
|
2820
|
+
const answer = toBackend ? null : await devHandler(request)
|
|
2710
2821
|
|
|
2711
2822
|
if (answer) return answer
|
|
2712
2823
|
|
|
2713
|
-
if (backendForward && !pageSaidNotFound(request)) return await backendForward(request)
|
|
2824
|
+
if (backendForward && (toBackend || !pageSaidNotFound(request))) return await backendForward(request)
|
|
2714
2825
|
|
|
2715
2826
|
return await notFound(request)
|
|
2716
2827
|
`;
|
|
2717
2828
|
const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
|
|
2718
|
-
|
|
2829
|
+
|
|
2830
|
+
// The app named this prefix as the backend's (rscKit({ backendPaths })): no
|
|
2831
|
+
// page is asked about it. A dynamic root route such as [team]/[app] matches
|
|
2832
|
+
// /gitlab/connect, renders, says notFound() - and a page that said that is not
|
|
2833
|
+
// forwarded, so the backend's own route was the not-found page.
|
|
2834
|
+
const toBackend = onBackendPath(new URL(request.url).pathname)
|
|
2835
|
+
const answer = toBackend ? null : await devHandler(request)
|
|
2719
2836
|
|
|
2720
2837
|
if (answer) return answer
|
|
2721
2838
|
|
|
@@ -2723,7 +2840,7 @@ const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
|
|
|
2723
2840
|
// its guard, or a backend's 404. That is this app's not-found.tsx, not a
|
|
2724
2841
|
// url for the backend: forwarded, the backend answered with its own 404
|
|
2725
2842
|
// and the app's never showed.
|
|
2726
|
-
if (pageSaidNotFound(request)) return await notFound(request)
|
|
2843
|
+
if (!toBackend && pageSaidNotFound(request)) return await notFound(request)
|
|
2727
2844
|
|
|
2728
2845
|
// Nothing here owns this url. In development the backend usually does — a
|
|
2729
2846
|
// Blade page, /login, a webhook, an uploaded file under /storage — so the
|
|
@@ -5643,7 +5760,7 @@ export async function handleRscPprShell(
|
|
|
5643
5760
|
* server rendering live pages it had already frozen. The gate is the mode, not
|
|
5644
5761
|
* the file.
|
|
5645
5762
|
*/
|
|
5646
|
-
${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin))}${NITRO_HOST_CALLS}
|
|
5763
|
+
${backendPathConsts()}${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin))}${NITRO_HOST_CALLS}
|
|
5647
5764
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
5648
5765
|
|
|
5649
5766
|
export default async function handler(request: Request): Promise<Response> {
|