@rsc-kit/core 0.30.13 → 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/vite.d.ts CHANGED
@@ -186,6 +186,24 @@ export interface RscKitOptions {
186
186
  * alone before Nitro built the server from the same entry.
187
187
  */
188
188
  devFallback?: string | false;
189
+ /**
190
+ * Paths that belong to the backend, whatever page would also match them.
191
+ *
192
+ * A url nothing here owns is handed to the backend, but a page that matches
193
+ * is this app's: a dynamic root route such as `[team]/[app]` matches every
194
+ * two-segment url, renders, says `notFound()` - and a page that said that is
195
+ * never forwarded, so `/gitlab/connect` was the not-found page and the
196
+ * backend never saw it. Naming the prefixes the backend owns forwards them
197
+ * before any page is matched:
198
+ *
199
+ * rscKit({ backendPaths: ['/auth', '/github', '/gitlab'] })
200
+ *
201
+ * Each is a prefix at a segment boundary - `/auth` is `/auth` and everything
202
+ * under it, never `/authors` - and every method goes, a POST as a GET. A
203
+ * team called `auth` is then unreachable, which is the price of the name.
204
+ * Without a backend where this runs, such a url is this app's not-found page.
205
+ */
206
+ backendPaths?: string[];
189
207
  /**
190
208
  * This package's directory, holding the client runtime the browser entry
191
209
  * imports. Vite stages configs through node_modules/.vite-temp, so
@@ -297,6 +315,12 @@ export interface RscKitOptions {
297
315
  watch?: string[];
298
316
  };
299
317
  }
318
+ /**
319
+ * The prefixes a backend owns, as the matcher reads them: `/auth`, no trailing
320
+ * slash. Refused rather than corrected where a mistake would silently send the
321
+ * whole app, or its own endpoints, to the backend.
322
+ */
323
+ export declare function normaliseBackendPaths(paths: readonly string[] | undefined): string[];
300
324
  /**
301
325
  * The service worker, written into the client output at build time.
302
326
  *
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 ||
@@ -2769,17 +2799,40 @@ function fallbackOrigin(): string {
2769
2799
  const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
2770
2800
  const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
2771
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
+ }
2772
2816
  /** With no backend configured: this app's not-found page, or a test's backend. */
2773
- const NO_FALLBACK_BODY = ` const answer = await devHandler(request)
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)
2774
2821
 
2775
2822
  if (answer) return answer
2776
2823
 
2777
- if (backendForward && !pageSaidNotFound(request)) return await backendForward(request)
2824
+ if (backendForward && (toBackend || !pageSaidNotFound(request))) return await backendForward(request)
2778
2825
 
2779
2826
  return await notFound(request)
2780
2827
  `;
2781
2828
  const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
2782
- const answer = await devHandler(request)
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)
2783
2836
 
2784
2837
  if (answer) return answer
2785
2838
 
@@ -2787,7 +2840,7 @@ const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
2787
2840
  // its guard, or a backend's 404. That is this app's not-found.tsx, not a
2788
2841
  // url for the backend: forwarded, the backend answered with its own 404
2789
2842
  // and the app's never showed.
2790
- if (pageSaidNotFound(request)) return await notFound(request)
2843
+ if (!toBackend && pageSaidNotFound(request)) return await notFound(request)
2791
2844
 
2792
2845
  // Nothing here owns this url. In development the backend usually does — a
2793
2846
  // Blade page, /login, a webhook, an uploaded file under /storage — so the
@@ -5707,7 +5760,7 @@ export async function handleRscPprShell(
5707
5760
  * server rendering live pages it had already frozen. The gate is the mode, not
5708
5761
  * the file.
5709
5762
  */
5710
- ${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}
5711
5764
  let devHandler: ((request: Request) => Promise<Response | null>) | null = null
5712
5765
 
5713
5766
  export default async function handler(request: Request): Promise<Response> {