rwsdk 0.2.0-alpha.9 → 0.2.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 (48) hide show
  1. package/dist/runtime/client/client.d.ts +10 -0
  2. package/dist/runtime/{client.js → client/client.js} +13 -10
  3. package/dist/runtime/client/navigation.d.ts +9 -0
  4. package/dist/runtime/client/navigation.js +88 -0
  5. package/dist/runtime/{clientNavigation.test.js → client/navigation.test.js} +1 -1
  6. package/dist/runtime/client/setWebpackRequire.d.ts +1 -0
  7. package/dist/runtime/client/setWebpackRequire.js +2 -0
  8. package/dist/runtime/{client.d.ts → client/types.d.ts} +4 -10
  9. package/dist/runtime/client/types.js +1 -0
  10. package/dist/runtime/entries/client.d.ts +2 -2
  11. package/dist/runtime/entries/client.js +2 -2
  12. package/dist/runtime/imports/client.d.ts +3 -3
  13. package/dist/runtime/imports/client.js +7 -6
  14. package/dist/runtime/imports/ssr.d.ts +3 -3
  15. package/dist/runtime/imports/ssr.js +3 -3
  16. package/dist/runtime/imports/worker.d.ts +3 -3
  17. package/dist/runtime/imports/worker.js +3 -3
  18. package/dist/runtime/lib/manifest.d.ts +11 -2
  19. package/dist/runtime/lib/manifest.js +1 -1
  20. package/dist/runtime/lib/memoizeOnId.d.ts +1 -0
  21. package/dist/runtime/lib/memoizeOnId.js +11 -0
  22. package/dist/runtime/lib/realtime/client.d.ts +1 -1
  23. package/dist/runtime/lib/realtime/client.js +1 -1
  24. package/dist/runtime/lib/router.d.ts +3 -3
  25. package/dist/runtime/lib/router.js +77 -33
  26. package/dist/runtime/register/ssr.d.ts +1 -1
  27. package/dist/runtime/register/ssr.js +2 -2
  28. package/dist/runtime/render/preloads.d.ts +6 -0
  29. package/dist/runtime/render/preloads.js +40 -0
  30. package/dist/runtime/render/renderRscThenableToHtmlStream.js +2 -1
  31. package/dist/runtime/render/stylesheets.js +1 -1
  32. package/dist/runtime/requestInfo/types.d.ts +3 -1
  33. package/dist/runtime/worker.js +1 -1
  34. package/dist/scripts/debug-sync.mjs +159 -33
  35. package/dist/scripts/worker-run.mjs +8 -3
  36. package/dist/vite/hasOwnReactVitePlugin.d.mts +3 -0
  37. package/dist/vite/hasOwnReactVitePlugin.mjs +14 -0
  38. package/dist/vite/miniflareHMRPlugin.mjs +17 -2
  39. package/dist/vite/reactConditionsResolverPlugin.d.mts +3 -4
  40. package/dist/vite/reactConditionsResolverPlugin.mjs +71 -48
  41. package/dist/vite/redwoodPlugin.d.mts +1 -0
  42. package/dist/vite/redwoodPlugin.mjs +9 -3
  43. package/dist/vite/transformJsxScriptTagsPlugin.mjs +106 -91
  44. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +341 -110
  45. package/package.json +22 -4
  46. /package/dist/runtime/{imports → client}/ClientOnly.d.ts +0 -0
  47. /package/dist/runtime/{imports → client}/ClientOnly.js +0 -0
  48. /package/dist/runtime/{clientNavigation.test.d.ts → client/navigation.test.d.ts} +0 -0
@@ -0,0 +1,10 @@
1
+ import "./setWebpackRequire";
2
+ export { ClientOnly } from "./ClientOnly.js";
3
+ export { default as React } from "react";
4
+ import type { Transport, HydrationOptions } from "./types";
5
+ export declare const fetchTransport: Transport;
6
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
7
+ transport?: Transport;
8
+ hydrateRootOptions?: HydrationOptions;
9
+ handleResponse?: (response: Response) => boolean;
10
+ }) => Promise<void>;
@@ -1,11 +1,18 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { clientWebpackRequire } from "./imports/client";
3
- // NOTE: `react-server-dom-webpack` uses this global to load modules,
4
- // so we need to define it here before importing "react-server-dom-webpack."
5
- globalThis.__webpack_require__ = clientWebpackRequire;
2
+ // note(justinvdm, 14 Aug 2025): Rendering related imports and logic go here.
3
+ // See client.tsx for the actual client entrypoint.
4
+ // context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this global
5
+ // to load modules, so we need to define it here before importing
6
+ // "react-server-dom-webpack."
7
+ import "./setWebpackRequire";
8
+ import React from "react";
9
+ import { hydrateRoot } from "react-dom/client";
10
+ import { createFromReadableStream, createFromFetch, encodeReply, } from "react-server-dom-webpack/client.browser";
11
+ import { rscStream } from "rsc-html-stream/client";
12
+ export { ClientOnly } from "./ClientOnly.js";
13
+ export { default as React } from "react";
6
14
  export const fetchTransport = (transportContext) => {
7
15
  const fetchCallServer = async (id, args) => {
8
- const { createFromFetch, encodeReply } = await import("react-server-dom-webpack/client.browser");
9
16
  const url = new URL(window.location.href);
10
17
  url.searchParams.set("__rsc", "");
11
18
  if (id != null) {
@@ -41,8 +48,6 @@ export const fetchTransport = (transportContext) => {
41
48
  return fetchCallServer;
42
49
  };
43
50
  export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
44
- const React = await import("react");
45
- const { hydrateRoot } = await import("react-dom/client");
46
51
  const transportContext = {
47
52
  setRscPayload: () => { },
48
53
  handleResponse,
@@ -50,7 +55,7 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
50
55
  let transportCallServer = transport(transportContext);
51
56
  const callServer = (id, args) => transportCallServer(id, args);
52
57
  const upgradeToRealtime = async ({ key } = {}) => {
53
- const { realtimeTransport } = await import("./lib/realtime/client");
58
+ const { realtimeTransport } = await import("../lib/realtime/client");
54
59
  const createRealtimeTransport = realtimeTransport({ key });
55
60
  transportCallServer = createRealtimeTransport(transportContext);
56
61
  };
@@ -67,8 +72,6 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
67
72
  // context(justinvdm, 18 Jun 2025): We inject the RSC payload
68
73
  // unless render(Document, [...], { rscPayload: false }) was used.
69
74
  if (globalThis.__FLIGHT_DATA) {
70
- const { createFromReadableStream } = await import("react-server-dom-webpack/client.browser");
71
- const { rscStream } = await import("rsc-html-stream/client");
72
75
  rscPayload = createFromReadableStream(rscStream, {
73
76
  callServer,
74
77
  });
@@ -0,0 +1,9 @@
1
+ export interface ClientNavigationOptions {
2
+ onNavigate?: () => void;
3
+ scrollToTop?: boolean;
4
+ scrollBehavior?: "auto" | "smooth" | "instant";
5
+ }
6
+ export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
7
+ export declare function initClientNavigation(opts?: ClientNavigationOptions): {
8
+ handleResponse: (response: Response) => boolean;
9
+ };
@@ -0,0 +1,88 @@
1
+ function saveScrollPosition(x, y) {
2
+ window.history.replaceState({
3
+ ...window.history.state,
4
+ scrollX: x,
5
+ scrollY: y,
6
+ }, "", window.location.href);
7
+ }
8
+ export function validateClickEvent(event, target) {
9
+ // should this only work for left click?
10
+ if (event.button !== 0) {
11
+ return false;
12
+ }
13
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
14
+ return false;
15
+ }
16
+ const link = target.closest("a");
17
+ if (!link) {
18
+ return false;
19
+ }
20
+ const href = link.getAttribute("href");
21
+ if (!href) {
22
+ return false;
23
+ }
24
+ if (href.includes("#")) {
25
+ return false;
26
+ }
27
+ // Skip if target="_blank" or similar
28
+ if (link.target && link.target !== "_self") {
29
+ return false;
30
+ }
31
+ if (href.startsWith("http")) {
32
+ return false;
33
+ }
34
+ // Skip if download attribute
35
+ if (link.hasAttribute("download")) {
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ export function initClientNavigation(opts = {}) {
41
+ const options = {
42
+ onNavigate: async function onNavigate() {
43
+ // @ts-expect-error
44
+ await globalThis.__rsc_callServer();
45
+ },
46
+ scrollToTop: true,
47
+ scrollBehavior: "instant",
48
+ ...opts,
49
+ };
50
+ history.scrollRestoration = "auto";
51
+ document.addEventListener("click", async function handleClickEvent(event) {
52
+ // Prevent default navigation
53
+ if (!validateClickEvent(event, event.target)) {
54
+ return;
55
+ }
56
+ event.preventDefault();
57
+ const el = event.target;
58
+ const a = el.closest("a");
59
+ const href = a?.getAttribute("href");
60
+ saveScrollPosition(window.scrollX, window.scrollY);
61
+ window.history.pushState({ path: href }, "", window.location.origin + href);
62
+ await options.onNavigate();
63
+ if (options.scrollToTop && history.scrollRestoration === "auto") {
64
+ window.scrollTo({
65
+ top: 0,
66
+ left: 0,
67
+ behavior: options.scrollBehavior,
68
+ });
69
+ saveScrollPosition(0, 0);
70
+ }
71
+ history.scrollRestoration = "auto";
72
+ }, true);
73
+ window.addEventListener("popstate", async function handlePopState() {
74
+ saveScrollPosition(window.scrollX, window.scrollY);
75
+ await options.onNavigate();
76
+ });
77
+ // Return a handleResponse function for use with initClient
78
+ return {
79
+ handleResponse: function handleResponse(response) {
80
+ if (!response.ok) {
81
+ // Redirect to the current page (window.location) to show the error
82
+ window.location.href = window.location.href;
83
+ return false;
84
+ }
85
+ return true;
86
+ },
87
+ };
88
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { validateClickEvent } from "./clientNavigation";
2
+ import { validateClickEvent } from "./navigation";
3
3
  describe("clientNavigation", () => {
4
4
  let mockEvent = {
5
5
  button: 0, // right click
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { clientWebpackRequire } from "../imports/client";
2
+ globalThis.__webpack_require__ = clientWebpackRequire;
@@ -1,19 +1,13 @@
1
- import { type CallServerCallback } from "react-server-dom-webpack/client.browser";
2
- import { type HydrationOptions } from "react-dom/client";
1
+ import type { CallServerCallback } from "react-server-dom-webpack/client.browser";
2
+ export type { CallServerCallback } from "react-server-dom-webpack/client.browser";
3
+ export type { HydrationOptions } from "react-dom/client";
3
4
  export type ActionResponse<Result> = {
4
5
  node: React.ReactNode;
5
6
  actionResult: Result;
6
7
  };
7
- type TransportContext = {
8
+ export type TransportContext = {
8
9
  setRscPayload: <Result>(v: Promise<ActionResponse<Result>>) => void;
9
10
  handleResponse?: (response: Response) => boolean;
10
11
  };
11
12
  export type Transport = (context: TransportContext) => CallServerCallback;
12
13
  export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[]) => Promise<Result>;
13
- export declare const fetchTransport: Transport;
14
- export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
15
- transport?: Transport;
16
- hydrateRootOptions?: HydrationOptions;
17
- handleResponse?: (response: Response) => boolean;
18
- }) => Promise<void>;
19
- export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  import "./types/client";
2
- export * from "../client";
2
+ export * from "../client/client";
3
3
  export * from "../register/client";
4
4
  export * from "../lib/streams/consumeEventStream";
5
- export * from "../clientNavigation";
5
+ export * from "../client/navigation";
@@ -1,5 +1,5 @@
1
1
  import "./types/client";
2
- export * from "../client";
2
+ export * from "../client/client";
3
3
  export * from "../register/client";
4
4
  export * from "../lib/streams/consumeEventStream";
5
- export * from "../clientNavigation";
5
+ export * from "../client/navigation";
@@ -1,4 +1,4 @@
1
- export declare const loadModule: ((id: string) => Promise<any>) & import("lodash").MemoizedFunction;
2
- export declare const clientWebpackRequire: ((id: string) => Promise<{
1
+ export declare const loadModule: (id: string) => Promise<any>;
2
+ export declare const clientWebpackRequire: (id: string) => Promise<{
3
3
  [x: string]: any;
4
- }>) & import("lodash").MemoizedFunction;
4
+ }>;
@@ -1,11 +1,13 @@
1
1
  import React from "react";
2
- import memoize from "lodash/memoize";
3
- export const loadModule = memoize(async (id) => {
2
+ import { ClientOnly } from "../client/client";
3
+ import { memoizeOnId } from "../lib/memoizeOnId";
4
+ // @ts-ignore
5
+ import { useClientLookup } from "virtual:use-client-lookup.js";
6
+ export const loadModule = memoizeOnId(async (id) => {
4
7
  if (import.meta.env.VITE_IS_DEV_SERVER) {
5
8
  return await import(/* @vite-ignore */ id);
6
9
  }
7
10
  else {
8
- const { useClientLookup } = await import("virtual:use-client-lookup.js");
9
11
  const moduleFn = useClientLookup[id];
10
12
  if (!moduleFn) {
11
13
  throw new Error(`(client) No module found for '${id}' in module lookup for "use client" directive`);
@@ -14,7 +16,7 @@ export const loadModule = memoize(async (id) => {
14
16
  }
15
17
  });
16
18
  // context(justinvdm, 2 Dec 2024): re memoize(): React relies on the same promise instance being returned for the same id
17
- export const clientWebpackRequire = memoize(async (id) => {
19
+ export const clientWebpackRequire = memoizeOnId(async (id) => {
18
20
  const [file, name] = id.split("#");
19
21
  const promisedModule = loadModule(file);
20
22
  const promisedComponent = promisedModule.then((module) => module[name]);
@@ -23,11 +25,10 @@ export const clientWebpackRequire = memoize(async (id) => {
23
25
  const awaitedComponent = await promisedComponent;
24
26
  return { [id]: awaitedComponent };
25
27
  }
26
- const { ClientOnly } = await import("./ClientOnly");
27
28
  const promisedDefault = promisedComponent.then((Component) => ({
28
29
  default: Component,
29
30
  }));
30
31
  const Lazy = React.lazy(() => promisedDefault);
31
- const Wrapped = () => React.createElement(ClientOnly, null, React.createElement(Lazy));
32
+ const Wrapped = (props) => React.createElement(ClientOnly, null, React.createElement(Lazy, props));
32
33
  return { [id]: Wrapped };
33
34
  });
@@ -1,5 +1,5 @@
1
- export declare const ssrLoadModule: ((id: string) => Promise<any>) & import("lodash").MemoizedFunction;
1
+ export declare const ssrLoadModule: (id: string) => Promise<any>;
2
2
  export declare const ssrGetModuleExport: (id: string) => Promise<any>;
3
- export declare const ssrWebpackRequire: ((id: string) => Promise<{
3
+ export declare const ssrWebpackRequire: (id: string) => Promise<{
4
4
  [x: string]: any;
5
- }>) & import("lodash").MemoizedFunction;
5
+ }>;
@@ -1,5 +1,5 @@
1
- import memoize from "lodash/memoize";
2
- export const ssrLoadModule = memoize(async (id) => {
1
+ import { memoizeOnId } from "../lib/memoizeOnId";
2
+ export const ssrLoadModule = memoizeOnId(async (id) => {
3
3
  const { useClientLookup } = await import("virtual:use-client-lookup.js");
4
4
  const moduleFn = useClientLookup[id];
5
5
  if (!moduleFn) {
@@ -13,7 +13,7 @@ export const ssrGetModuleExport = async (id) => {
13
13
  return module[name];
14
14
  };
15
15
  // context(justinvdm, 2 Dec 2024): re memoize(): React relies on the same promise instance being returned for the same id
16
- export const ssrWebpackRequire = memoize(async (id) => {
16
+ export const ssrWebpackRequire = memoizeOnId(async (id) => {
17
17
  const [file, name] = id.split("#");
18
18
  const module = await ssrLoadModule(file);
19
19
  return { [id]: module[name] };
@@ -1,5 +1,5 @@
1
- export declare const loadServerModule: ((id: string) => Promise<any>) & import("lodash").MemoizedFunction;
1
+ export declare const loadServerModule: (id: string) => Promise<any>;
2
2
  export declare const getServerModuleExport: (id: string) => Promise<any>;
3
- export declare const ssrWebpackRequire: ((id: string) => Promise<{
3
+ export declare const ssrWebpackRequire: (id: string) => Promise<{
4
4
  [x: string]: any;
5
- }>) & import("lodash").MemoizedFunction;
5
+ }>;
@@ -1,7 +1,7 @@
1
- import memoize from "lodash/memoize";
2
1
  import { requestInfo } from "../requestInfo/worker";
3
2
  import { ssrWebpackRequire as baseSsrWebpackRequire } from "rwsdk/__ssr_bridge";
4
- export const loadServerModule = memoize(async (id) => {
3
+ import { memoizeOnId } from "../lib/memoizeOnId";
4
+ export const loadServerModule = memoizeOnId(async (id) => {
5
5
  const { useServerLookup } = await import("virtual:use-server-lookup.js");
6
6
  const moduleFn = useServerLookup[id];
7
7
  if (!moduleFn) {
@@ -14,7 +14,7 @@ export const getServerModuleExport = async (id) => {
14
14
  const module = await loadServerModule(file);
15
15
  return module[name];
16
16
  };
17
- export const ssrWebpackRequire = memoize(async (id) => {
17
+ export const ssrWebpackRequire = memoizeOnId(async (id) => {
18
18
  if (!requestInfo.rw.ssr) {
19
19
  return { [id]: () => null };
20
20
  }
@@ -1,2 +1,11 @@
1
- import { type RequestInfo } from "../requestInfo/types";
2
- export declare const getManifest: (requestInfo: RequestInfo) => Promise<Record<string, any>>;
1
+ export type Manifest = Record<string, ManifestChunk>;
2
+ export interface ManifestChunk {
3
+ file: string;
4
+ src?: string;
5
+ isEntry?: boolean;
6
+ isDynamicEntry?: boolean;
7
+ imports?: string[];
8
+ css?: string[];
9
+ assets?: string[];
10
+ }
11
+ export declare const getManifest: () => Promise<Manifest>;
@@ -1,5 +1,5 @@
1
1
  let manifest;
2
- export const getManifest = async (requestInfo) => {
2
+ export const getManifest = async () => {
3
3
  if (manifest) {
4
4
  return manifest;
5
5
  }
@@ -0,0 +1 @@
1
+ export declare const memoizeOnId: <Result>(fn: (id: string) => Result) => (id: string) => Result;
@@ -0,0 +1,11 @@
1
+ export const memoizeOnId = (fn) => {
2
+ const hasOwnProperty = Object.prototype.hasOwnProperty;
3
+ const results = {};
4
+ const memoizedFn = (id) => {
5
+ if (hasOwnProperty.call(results, id)) {
6
+ return results[id];
7
+ }
8
+ return (results[id] = fn(id));
9
+ };
10
+ return memoizedFn;
11
+ };
@@ -1,4 +1,4 @@
1
- import { type Transport } from "../../client";
1
+ import { type Transport } from "../../client/types";
2
2
  export declare const initRealtimeClient: ({ key, handleResponse, }?: {
3
3
  key?: string;
4
4
  handleResponse?: (response: Response) => boolean;
@@ -1,4 +1,4 @@
1
- import { initClient } from "../../client";
1
+ import { initClient } from "../../client/client";
2
2
  import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
3
3
  import { MESSAGE_TYPE } from "./shared";
4
4
  import { packMessage, unpackMessage, } from "./protocol";
@@ -18,10 +18,10 @@ export type RwContext = {
18
18
  scriptsToBeLoaded: Set<string>;
19
19
  pageRouteResolved: PromiseWithResolvers<void> | undefined;
20
20
  };
21
- export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response> | void | Promise<void> | Promise<Response | void>;
22
- type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response>;
21
+ export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
22
+ type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<Response>;
23
23
  type MaybePromise<T> = T | Promise<T>;
24
- type RouteComponent<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response>;
24
+ type RouteComponent<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
25
25
  type RouteHandler<T extends RequestInfo = RequestInfo> = RouteFunction<T> | RouteComponent<T> | [...RouteMiddleware<T>[], RouteFunction<T> | RouteComponent<T>];
26
26
  export type Route<T extends RequestInfo = RequestInfo> = RouteMiddleware<T> | RouteDefinition<T> | Array<Route<T>>;
27
27
  export type RouteDefinition<T extends RequestInfo = RequestInfo> = {
@@ -71,53 +71,97 @@ export function defineRoutes(routes) {
71
71
  if (path !== "/" && !path.endsWith("/")) {
72
72
  path = path + "/";
73
73
  }
74
- // Find matching route
75
- let match = null;
74
+ // Flow below; helpers are declared after the main flow for readability
75
+ // 1) Global middlewares: run any middleware functions encountered (Response short-circuits, element renders)
76
+ // 2) Route matching: skip non-matching route definitions; stop at first match
77
+ // 3) Route-specific middlewares: run middlewares attached to the matched route
78
+ // 4) Final component: render the matched route's final component (layouts apply only here)
76
79
  for (const route of flattenedRoutes) {
80
+ // 1) Global middlewares (encountered before a match)
77
81
  if (typeof route === "function") {
78
- const r = await route(getRequestInfo());
79
- if (r instanceof Response) {
80
- return r;
82
+ const result = await route(getRequestInfo());
83
+ const handled = await handleMiddlewareResult(result);
84
+ if (handled) {
85
+ return handled;
81
86
  }
82
87
  continue;
83
88
  }
89
+ // 2) Route matching (skip if not matched)
84
90
  const params = matchPath(route.path, path);
85
- if (params) {
86
- match = { params, handler: route.handler, layouts: route.layouts };
87
- break;
91
+ if (!params) {
92
+ continue;
88
93
  }
94
+ // Found a match: 3) route middlewares, then 4) final component, then stop
95
+ return await runWithRequestInfoOverrides({ params }, async () => {
96
+ const { routeMiddlewares, componentHandler } = parseHandlers(route.handler);
97
+ // 3) Route-specific middlewares
98
+ const mwHandled = await handleRouteMiddlewares(routeMiddlewares);
99
+ if (mwHandled) {
100
+ return mwHandled;
101
+ }
102
+ // 4) Final component (always last item)
103
+ return await handleRouteComponent(componentHandler, route.layouts || []);
104
+ });
89
105
  }
90
- if (!match) {
91
- // todo(peterp, 2025-01-28): Allow the user to define their own "not found" route.
92
- return new Response("Not Found", { status: 404 });
93
- }
94
- let { params, handler, layouts } = match;
95
- return runWithRequestInfoOverrides({ params }, async () => {
106
+ // No route matched and no middleware handled the request
107
+ // todo(peterp, 2025-01-28): Allow the user to define their own "not found" route.
108
+ return new Response("Not Found", { status: 404 });
109
+ // --- Helpers ---
110
+ function parseHandlers(handler) {
96
111
  const handlers = Array.isArray(handler) ? handler : [handler];
97
- for (const h of handlers) {
98
- if (isRouteComponent(h)) {
99
- const requestInfo = getRequestInfo();
100
- const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(h), layouts || [], requestInfo);
101
- if (!isClientReference(h)) {
102
- // context(justinvdm, 31 Jul 2025): We now know we're dealing with a page route,
103
- // so we create a deferred so that we can signal when we're done determining whether
104
- // we're returning a response or a react element
105
- requestInfo.rw.pageRouteResolved = Promise.withResolvers();
106
- }
107
- return await renderPage(requestInfo, WrappedComponent, onError);
108
- }
109
- else {
110
- const r = await h(getRequestInfo());
111
- if (r instanceof Response) {
112
- return r;
113
- }
112
+ const routeMiddlewares = handlers.slice(0, Math.max(handlers.length - 1, 0));
113
+ const componentHandler = handlers[handlers.length - 1];
114
+ return {
115
+ routeMiddlewares: routeMiddlewares,
116
+ componentHandler,
117
+ };
118
+ }
119
+ function renderElement(element) {
120
+ const requestInfo = getRequestInfo();
121
+ const Element = () => element;
122
+ return renderPage(requestInfo, Element, onError);
123
+ }
124
+ async function handleMiddlewareResult(result) {
125
+ if (result instanceof Response) {
126
+ return result;
127
+ }
128
+ if (result && React.isValidElement(result)) {
129
+ return await renderElement(result);
130
+ }
131
+ return undefined;
132
+ }
133
+ // Note: We no longer have separate global pass or match-only pass;
134
+ // the outer single pass above handles both behaviors correctly.
135
+ async function handleRouteMiddlewares(mws) {
136
+ for (const mw of mws) {
137
+ const result = await mw(getRequestInfo());
138
+ const handled = await handleMiddlewareResult(result);
139
+ if (handled)
140
+ return handled;
141
+ }
142
+ return undefined;
143
+ }
144
+ async function handleRouteComponent(component, layouts) {
145
+ if (isRouteComponent(component)) {
146
+ const requestInfo = getRequestInfo();
147
+ const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(component), layouts, requestInfo);
148
+ if (!isClientReference(component)) {
149
+ // context(justinvdm, 31 Jul 2025): We now know we're dealing with a page route,
150
+ // so we create a deferred so that we can signal when we're done determining whether
151
+ // we're returning a response or a react element
152
+ requestInfo.rw.pageRouteResolved = Promise.withResolvers();
114
153
  }
154
+ return await renderPage(requestInfo, WrappedComponent, onError);
115
155
  }
116
- // Add fallback return
156
+ // If the last handler is not a component, handle as middleware result (no layouts)
157
+ const tailResult = await component(getRequestInfo());
158
+ const handledTail = await handleMiddlewareResult(tailResult);
159
+ if (handledTail)
160
+ return handledTail;
117
161
  return new Response("Response not returned from route handler", {
118
162
  status: 500,
119
163
  });
120
- });
164
+ }
121
165
  },
122
166
  };
123
167
  }
@@ -1,3 +1,3 @@
1
- export declare const loadServerModule: ((id: string) => Promise<any>) & import("lodash").MemoizedFunction;
1
+ export declare const loadServerModule: (id: string) => Promise<any>;
2
2
  export declare const getServerModuleExport: (id: string) => Promise<any>;
3
3
  export declare const createServerReference: (id: string, name: string) => any;
@@ -1,6 +1,6 @@
1
- import memoize from "lodash/memoize";
2
1
  import { createServerReference as baseCreateServerReference } from "react-server-dom-webpack/client.edge";
3
- export const loadServerModule = memoize(async (id) => {
2
+ import { memoizeOnId } from "../lib/memoizeOnId";
3
+ export const loadServerModule = memoizeOnId(async (id) => {
4
4
  const { useServerLookup } = await import("virtual:use-server-lookup.js");
5
5
  const moduleFn = useServerLookup[id];
6
6
  if (!moduleFn) {
@@ -0,0 +1,6 @@
1
+ import type { RequestInfo } from "../requestInfo/types.js";
2
+ import type { Manifest, ManifestChunk } from "../lib/manifest.js";
3
+ export declare function findScriptForModule(id: string, manifest: Manifest): ManifestChunk | undefined;
4
+ export declare const Preloads: ({ requestInfo }: {
5
+ requestInfo: RequestInfo;
6
+ }) => import("react/jsx-runtime.js").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { use } from "react";
3
+ import { getManifest } from "../lib/manifest.js";
4
+ export function findScriptForModule(id, manifest) {
5
+ const visited = new Set();
6
+ function find(id) {
7
+ if (visited.has(id)) {
8
+ return;
9
+ }
10
+ visited.add(id);
11
+ const manifestEntry = manifest[id];
12
+ if (!manifestEntry) {
13
+ return;
14
+ }
15
+ if (manifestEntry.isEntry || manifestEntry.isDynamicEntry) {
16
+ return manifestEntry;
17
+ }
18
+ if (manifestEntry.imports) {
19
+ for (const dep of manifestEntry.imports) {
20
+ const entry = find(dep);
21
+ if (entry) {
22
+ return entry;
23
+ }
24
+ }
25
+ }
26
+ return;
27
+ }
28
+ return find(id);
29
+ }
30
+ export const Preloads = ({ requestInfo }) => {
31
+ const manifest = use(getManifest());
32
+ const allScripts = new Set();
33
+ for (const scriptId of requestInfo.rw.scriptsToBeLoaded) {
34
+ const script = findScriptForModule(scriptId, manifest);
35
+ if (script) {
36
+ allScripts.add(script.file);
37
+ }
38
+ }
39
+ return (_jsx(_Fragment, { children: Array.from(allScripts).map((href) => (_jsx("link", { rel: "modulepreload", href: href }, href))) }));
40
+ };
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { use } from "react";
3
3
  import { renderToReadableStream } from "react-dom/server.edge";
4
+ import { Preloads } from "./preloads.js";
4
5
  import { Stylesheets } from "./stylesheets.js";
5
6
  export const renderRscThenableToHtmlStream = async ({ thenable, Document, requestInfo, shouldSSR, onError, }) => {
6
7
  const Component = () => {
7
8
  const RscApp = () => {
8
9
  const node = use(thenable).node;
9
- return (_jsxs(_Fragment, { children: [_jsx(Stylesheets, { requestInfo: requestInfo }), _jsx("div", { id: "hydrate-root", children: node })] }));
10
+ return (_jsxs(_Fragment, { children: [_jsx(Stylesheets, { requestInfo: requestInfo }), _jsx(Preloads, { requestInfo: requestInfo }), _jsx("div", { id: "hydrate-root", children: node })] }));
10
11
  };
11
12
  // todo(justinvdm, 18 Jun 2025): We can build on this later to allow users
12
13
  // surface context. e.g:
@@ -23,7 +23,7 @@ const findCssForModule = (scriptId, manifest) => {
23
23
  return Array.from(css);
24
24
  };
25
25
  export const Stylesheets = ({ requestInfo }) => {
26
- const manifest = use(getManifest(requestInfo));
26
+ const manifest = use(getManifest());
27
27
  const allStylesheets = new Set();
28
28
  for (const scriptId of requestInfo.rw.scriptsToBeLoaded) {
29
29
  const css = findCssForModule(scriptId, manifest);
@@ -9,5 +9,7 @@ export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
9
9
  headers: Headers;
10
10
  rw: RwContext;
11
11
  cf: ExecutionContext;
12
- response: ResponseInit;
12
+ response: ResponseInit & {
13
+ headers: Headers;
14
+ };
13
15
  }