rwsdk 0.2.0-alpha.0 → 0.2.0-alpha.10

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.
@@ -8,7 +8,7 @@ import puppeteer from "puppeteer-core";
8
8
  import { takeScreenshot } from "./artifacts.mjs";
9
9
  import { RETRIES } from "./constants.mjs";
10
10
  import { $ } from "../$.mjs";
11
- import { fail } from "./utils.mjs";
11
+ import { fail, withRetries } from "./utils.mjs";
12
12
  import { reportSmokeTestResult } from "./reporting.mjs";
13
13
  import { updateTestStatus } from "./state.mjs";
14
14
  import * as fs from "fs/promises";
@@ -404,7 +404,7 @@ export async function checkUrlSmoke(page, url, isRealtime, bail = false, skipCli
404
404
  ? "realtimeClientModuleStyles"
405
405
  : "initialClientModuleStyles";
406
406
  try {
407
- await checkUrlStyles(page, "red");
407
+ await withRetries(() => checkUrlStyles(page, "red"), "URL styles check");
408
408
  updateTestStatus(env, urlStylesKey, "PASSED");
409
409
  log(`${phase} URL styles check passed`);
410
410
  }
@@ -420,7 +420,7 @@ export async function checkUrlSmoke(page, url, isRealtime, bail = false, skipCli
420
420
  }
421
421
  }
422
422
  try {
423
- await checkClientModuleStyles(page, "blue");
423
+ await withRetries(() => checkClientModuleStyles(page, "blue"), "Client module styles check");
424
424
  updateTestStatus(env, clientModuleStylesKey, "PASSED");
425
425
  log(`${phase} client module styles check passed`);
426
426
  }
@@ -507,7 +507,14 @@ export async function checkUrlSmoke(page, url, isRealtime, bail = false, skipCli
507
507
  await testClientComponentHmr(page, targetDir, phase, environment, bail);
508
508
  // Test style HMR if style tests aren't skipped
509
509
  if (!skipStyleTests) {
510
- await testStyleHMR(page, targetDir);
510
+ await withRetries(() => testStyleHMR(page, targetDir), "Style HMR test", async () => {
511
+ // This logic runs before each retry of testStyleHMR
512
+ const urlStylePath = join(targetDir, "src", "app", "smokeTestUrlStyles.css");
513
+ const clientStylePath = join(targetDir, "src", "app", "components", "smokeTestClientStyles.module.css");
514
+ // Restore original styles before re-running HMR test
515
+ await fs.writeFile(urlStylePath, urlStylesTemplate);
516
+ await fs.writeFile(clientStylePath, clientStylesTemplate);
517
+ });
511
518
  }
512
519
  else {
513
520
  log("Skipping style HMR test as requested");
@@ -1182,9 +1189,9 @@ async function testStyleHMR(page, targetDir) {
1182
1189
  // Allow time for HMR to kick in
1183
1190
  await new Promise((resolve) => setTimeout(resolve, 5000));
1184
1191
  // Check URL-based stylesheet HMR
1185
- await checkUrlStyles(page, "green");
1192
+ await withRetries(() => checkUrlStyles(page, "green"), "URL styles HMR check");
1186
1193
  // Check client-module stylesheet HMR
1187
- await checkClientModuleStyles(page, "green");
1194
+ await withRetries(() => checkClientModuleStyles(page, "green"), "Client module styles HMR check");
1188
1195
  // Restore original styles
1189
1196
  await fs.writeFile(urlStylePath, urlStylesTemplate);
1190
1197
  await fs.writeFile(clientStylePath, clientStylesTemplate);
@@ -13,3 +13,12 @@ export declare function teardown(): Promise<void>;
13
13
  * Formats the path suffix from a custom path
14
14
  */
15
15
  export declare function formatPathSuffix(customPath?: string): string;
16
+ /**
17
+ * Wraps an async function with retry logic.
18
+ * @param fn The async function to execute.
19
+ * @param description A description of the operation for logging.
20
+ * @param beforeRetry A function to run before each retry attempt.
21
+ * @param maxRetries The maximum number of retries.
22
+ * @param delay The delay between retries in milliseconds.
23
+ */
24
+ export declare function withRetries<T>(fn: () => Promise<T>, description: string, beforeRetry?: () => Promise<void>, maxRetries?: number, delay?: number): Promise<T>;
@@ -145,3 +145,32 @@ export function formatPathSuffix(customPath) {
145
145
  log("Formatted path suffix: %s", suffix);
146
146
  return suffix;
147
147
  }
148
+ /**
149
+ * Wraps an async function with retry logic.
150
+ * @param fn The async function to execute.
151
+ * @param description A description of the operation for logging.
152
+ * @param beforeRetry A function to run before each retry attempt.
153
+ * @param maxRetries The maximum number of retries.
154
+ * @param delay The delay between retries in milliseconds.
155
+ */
156
+ export async function withRetries(fn, description, beforeRetry, maxRetries = 5, delay = 2000) {
157
+ for (let i = 0; i < maxRetries; i++) {
158
+ try {
159
+ if (i > 0 && beforeRetry) {
160
+ log(`Running beforeRetry hook for "${description}"`);
161
+ await beforeRetry();
162
+ }
163
+ return await fn();
164
+ }
165
+ catch (error) {
166
+ log(`Attempt ${i + 1} of ${maxRetries} failed for "${description}": ${error instanceof Error ? error.message : String(error)}`);
167
+ if (i === maxRetries - 1) {
168
+ log(`All ${maxRetries} retries failed for "${description}".`);
169
+ throw error;
170
+ }
171
+ log(`Retrying in ${delay}ms...`);
172
+ await setTimeout(delay);
173
+ }
174
+ }
175
+ throw new Error("Retry loop failed unexpectedly.");
176
+ }
@@ -77,7 +77,9 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
77
77
  const [streamData, setStreamData] = React.useState(rscPayload);
78
78
  const [_isPending, startTransition] = React.useTransition();
79
79
  transportContext.setRscPayload = (v) => startTransition(() => setStreamData(v));
80
- return _jsx(_Fragment, { children: React.use(streamData).node });
80
+ return (_jsx(_Fragment, { children: streamData
81
+ ? React.use(streamData).node
82
+ : null }));
81
83
  }
82
84
  hydrateRoot(rootEl, _jsx(Content, {}), {
83
85
  onUncaughtError: (error, { componentStack }) => {
@@ -1,3 +1,10 @@
1
+ function saveScrollPosition(x, y) {
2
+ window.history.replaceState({
3
+ ...window.history.state,
4
+ scrollX: x,
5
+ scrollY: y,
6
+ }, "", window.location.href);
7
+ }
1
8
  export function validateClickEvent(event, target) {
2
9
  // should this only work for left click?
3
10
  if (event.button !== 0) {
@@ -14,6 +21,9 @@ export function validateClickEvent(event, target) {
14
21
  if (!href) {
15
22
  return false;
16
23
  }
24
+ if (href.includes("#")) {
25
+ return false;
26
+ }
17
27
  // Skip if target="_blank" or similar
18
28
  if (link.target && link.target !== "_self") {
19
29
  return false;
@@ -28,70 +38,16 @@ export function validateClickEvent(event, target) {
28
38
  return true;
29
39
  }
30
40
  export function initClientNavigation(opts = {}) {
31
- // Merge user options with defaults
32
41
  const options = {
33
42
  onNavigate: async function onNavigate() {
34
43
  // @ts-expect-error
35
44
  await globalThis.__rsc_callServer();
36
45
  },
37
46
  scrollToTop: true,
38
- scrollBehavior: 'instant',
47
+ scrollBehavior: "instant",
39
48
  ...opts,
40
49
  };
41
- // Prevent browser's automatic scroll restoration for popstate
42
- if ('scrollRestoration' in history) {
43
- history.scrollRestoration = 'manual';
44
- }
45
- // Set up scroll behavior management
46
- let popStateWasCalled = false;
47
- let savedScrollPosition = null;
48
- const observer = new MutationObserver(() => {
49
- if (popStateWasCalled && savedScrollPosition) {
50
- // Restore scroll position for popstate navigation (always instant)
51
- window.scrollTo({
52
- top: savedScrollPosition.y,
53
- left: savedScrollPosition.x,
54
- behavior: 'instant',
55
- });
56
- savedScrollPosition = null;
57
- }
58
- else if (options.scrollToTop && !popStateWasCalled) {
59
- // Scroll to top for anchor click navigation (configurable)
60
- window.scrollTo({
61
- top: 0,
62
- left: 0,
63
- behavior: options.scrollBehavior,
64
- });
65
- // Update the current history entry with the new scroll position (top)
66
- // This ensures that if we navigate back and then forward again,
67
- // we return to the top position, not some previous scroll position
68
- window.history.replaceState({
69
- ...window.history.state,
70
- scrollX: 0,
71
- scrollY: 0
72
- }, "", window.location.href);
73
- }
74
- popStateWasCalled = false;
75
- });
76
- const handleScrollPopState = (event) => {
77
- popStateWasCalled = true;
78
- // Save the scroll position that the browser would have restored to
79
- const state = event.state;
80
- if (state && typeof state === 'object' && 'scrollX' in state && 'scrollY' in state) {
81
- savedScrollPosition = { x: state.scrollX, y: state.scrollY };
82
- }
83
- else {
84
- // Fallback: try to get scroll position from browser's session history
85
- // This is a best effort since we can't directly access the browser's stored position
86
- savedScrollPosition = { x: window.scrollX, y: window.scrollY };
87
- }
88
- };
89
- const main = document.querySelector("main") || document.body;
90
- if (main) {
91
- window.addEventListener("popstate", handleScrollPopState);
92
- observer.observe(main, { childList: true, subtree: true });
93
- }
94
- // Intercept all anchor tag clicks
50
+ history.scrollRestoration = "auto";
95
51
  document.addEventListener("click", async function handleClickEvent(event) {
96
52
  // Prevent default navigation
97
53
  if (!validateClickEvent(event, event.target)) {
@@ -101,17 +57,21 @@ export function initClientNavigation(opts = {}) {
101
57
  const el = event.target;
102
58
  const a = el.closest("a");
103
59
  const href = a?.getAttribute("href");
104
- // Save current scroll position before navigating
105
- window.history.replaceState({
106
- path: window.location.pathname,
107
- scrollX: window.scrollX,
108
- scrollY: window.scrollY
109
- }, "", window.location.href);
60
+ saveScrollPosition(window.scrollX, window.scrollY);
110
61
  window.history.pushState({ path: href }, "", window.location.origin + href);
111
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";
112
72
  }, true);
113
- // Handle browser back/forward buttons
114
73
  window.addEventListener("popstate", async function handlePopState() {
74
+ saveScrollPosition(window.scrollX, window.scrollY);
115
75
  await options.onNavigate();
116
76
  });
117
77
  // Return a handleResponse function for use with initClient
@@ -35,11 +35,10 @@ describe("clientNavigation", () => {
35
35
  closest: () => ({ getAttribute: () => undefined }),
36
36
  })).toBe(false);
37
37
  });
38
- it("should not have a target attribute", () => {
38
+ it("should not include an #hash", () => {
39
39
  expect(validateClickEvent(mockEvent, {
40
40
  closest: () => ({
41
- target: "_blank",
42
- getAttribute: () => "/test",
41
+ getAttribute: () => "/test#hash",
43
42
  hasAttribute: () => false,
44
43
  }),
45
44
  })).toBe(false);
@@ -1,23 +1,21 @@
1
- import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, Driver, DatabaseConnection } from "kysely";
1
+ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, Driver, DatabaseConnection, QueryResult } from "kysely";
2
+ type DOWorkerDialectConfig = {
3
+ kyselyExecuteQuery: (compiledQuery: {
4
+ sql: string;
5
+ parameters: readonly unknown[];
6
+ }) => Promise<QueryResult<any>>;
7
+ };
2
8
  export declare class DOWorkerDialect {
3
- config: {
4
- stub: any;
5
- };
6
- constructor(config: {
7
- stub: any;
8
- });
9
+ config: DOWorkerDialectConfig;
10
+ constructor(config: DOWorkerDialectConfig);
9
11
  createAdapter(): SqliteAdapter;
10
12
  createDriver(): DOWorkerDriver;
11
13
  createQueryCompiler(): SqliteQueryCompiler;
12
14
  createIntrospector(db: any): SqliteIntrospector;
13
15
  }
14
16
  declare class DOWorkerDriver implements Driver {
15
- config: {
16
- stub: any;
17
- };
18
- constructor(config: {
19
- stub: any;
20
- });
17
+ config: DOWorkerDialectConfig;
18
+ constructor(config: DOWorkerDialectConfig);
21
19
  init(): Promise<void>;
22
20
  acquireConnection(): Promise<DatabaseConnection>;
23
21
  beginTransaction(conn: any): Promise<any>;
@@ -24,7 +24,7 @@ class DOWorkerDriver {
24
24
  }
25
25
  async init() { }
26
26
  async acquireConnection() {
27
- return new DOWorkerConnection(this.config.stub.kyselyExecuteQuery);
27
+ return new DOWorkerConnection(this.config.kyselyExecuteQuery);
28
28
  }
29
29
  async beginTransaction(conn) {
30
30
  return await conn.beginTransaction();
@@ -1,2 +1,3 @@
1
1
  import { Kysely } from "kysely";
2
- export declare function createDb<T>(durableObjectBinding: any, name?: string): Kysely<T>;
2
+ import { type SqliteDurableObject } from "./index.js";
3
+ export declare function createDb<T>(durableObjectBinding: DurableObjectNamespace<SqliteDurableObject>, name?: string): Kysely<T>;
@@ -1,40 +1,14 @@
1
1
  import { Kysely } from "kysely";
2
- import { requestInfo, waitForRequestInfo } from "../../requestInfo/worker.js";
3
2
  import { DOWorkerDialect } from "./DOWorkerDialect.js";
4
- const createDurableObjectDb = (durableObjectBinding, name = "main") => {
5
- const durableObjectId = durableObjectBinding.idFromName(name);
6
- const stub = durableObjectBinding.get(durableObjectId);
7
- stub.initialize();
8
- return new Kysely({
9
- dialect: new DOWorkerDialect({ stub }),
10
- });
11
- };
12
3
  export function createDb(durableObjectBinding, name = "main") {
13
- const cacheKey = `${durableObjectBinding}_${name}`;
14
- const doCreateDb = () => {
15
- if (!requestInfo.rw) {
16
- throw new Error(`
17
- rwsdk: A database created using createDb() was accessed before requestInfo was available.
18
-
19
- Please make sure database access is happening in a request handler or action handler.
20
- `);
21
- }
22
- let db = requestInfo.rw.databases.get(cacheKey);
23
- if (!db) {
24
- db = createDurableObjectDb(durableObjectBinding, name);
25
- requestInfo.rw.databases.set(cacheKey, db);
26
- }
27
- return db;
28
- };
29
- waitForRequestInfo().then(() => doCreateDb());
30
- return new Proxy({}, {
31
- get(target, prop, receiver) {
32
- const db = doCreateDb();
33
- const value = db[prop];
34
- if (typeof value === "function") {
35
- return value.bind(db);
36
- }
37
- return value;
38
- },
4
+ return new Kysely({
5
+ dialect: new DOWorkerDialect({
6
+ kyselyExecuteQuery: (...args) => {
7
+ const durableObjectId = durableObjectBinding.idFromName(name);
8
+ const stub = durableObjectBinding.get(durableObjectId);
9
+ stub.initialize();
10
+ return stub.kyselyExecuteQuery(...args);
11
+ },
12
+ }),
39
13
  });
40
14
  }
@@ -2,3 +2,4 @@ export * from "./migrations.js";
2
2
  export * from "./SqliteDurableObject.js";
3
3
  export * from "./createDb.js";
4
4
  export type * from "./typeInference/database.js";
5
+ export { sql } from "kysely";
@@ -1,3 +1,4 @@
1
1
  export * from "./migrations.js";
2
2
  export * from "./SqliteDurableObject.js";
3
3
  export * from "./createDb.js";
4
+ export { sql } from "kysely";
@@ -4,10 +4,7 @@ export const getManifest = async (requestInfo) => {
4
4
  return manifest;
5
5
  }
6
6
  if (import.meta.env.VITE_IS_DEV_SERVER) {
7
- const url = new URL(requestInfo.request.url);
8
- url.searchParams.set("scripts", JSON.stringify(Array.from(requestInfo.rw.scriptsToBeLoaded)));
9
- url.pathname = "/__rwsdk_manifest";
10
- manifest = await fetch(url.toString()).then((res) => res.json());
7
+ manifest = {};
11
8
  }
12
9
  else {
13
10
  const { default: prodManifest } = await import("virtual:rwsdk:manifest.js");
@@ -16,11 +16,12 @@ export type RwContext = {
16
16
  layouts?: React.FC<LayoutProps<any>>[];
17
17
  databases: Map<string, Kysely<any>>;
18
18
  scriptsToBeLoaded: Set<string>;
19
+ pageRouteResolved: PromiseWithResolvers<void> | undefined;
19
20
  };
20
- export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response> | void | Promise<void> | Promise<Response | void>;
21
- 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>;
22
23
  type MaybePromise<T> = T | Promise<T>;
23
- 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>;
24
25
  type RouteHandler<T extends RequestInfo = RequestInfo> = RouteFunction<T> | RouteComponent<T> | [...RouteMiddleware<T>[], RouteFunction<T> | RouteComponent<T>];
25
26
  export type Route<T extends RequestInfo = RequestInfo> = RouteMiddleware<T> | RouteDefinition<T> | Array<Route<T>>;
26
27
  export type RouteDefinition<T extends RequestInfo = RequestInfo> = {
@@ -42,6 +43,7 @@ export declare function defineRoutes<T extends RequestInfo = RequestInfo>(routes
42
43
  export declare function route<T extends RequestInfo = RequestInfo>(path: string, handler: RouteHandler<T>): RouteDefinition<T>;
43
44
  export declare function index<T extends RequestInfo = RequestInfo>(handler: RouteHandler<T>): RouteDefinition<T>;
44
45
  export declare function prefix<T extends RequestInfo = RequestInfo>(prefixPath: string, routes: Route<T>[]): Route<T>[];
46
+ export declare const wrapHandlerToThrowResponses: <T extends RequestInfo = RequestInfo>(handler: RouteFunction<T> | RouteComponent<T>) => RouteHandler<T>;
45
47
  export declare function layout<T extends RequestInfo = RequestInfo>(LayoutComponent: React.FC<LayoutProps<T>>, routes: Route<T>[]): Route<T>[];
46
48
  export declare function render<T extends RequestInfo = RequestInfo>(Document: React.FC<DocumentProps<T>>, routes: Route<T>[],
47
49
  /**
@@ -53,5 +55,5 @@ options?: {
53
55
  rscPayload?: boolean;
54
56
  ssr?: boolean;
55
57
  }): Route<T>[];
56
- export declare const isClientReference: (Component: React.FC<any>) => boolean;
58
+ export declare const isClientReference: (value: any) => boolean;
57
59
  export {};
@@ -71,47 +71,109 @@ export function defineRoutes(routes) {
71
71
  if (path !== "/" && !path.endsWith("/")) {
72
72
  path = path + "/";
73
73
  }
74
- // Find matching route
75
- let match = null;
76
- for (const route of flattenedRoutes) {
77
- if (typeof route === "function") {
78
- const r = await route(getRequestInfo());
79
- if (r instanceof Response) {
80
- return r;
81
- }
82
- continue;
83
- }
84
- const params = matchPath(route.path, path);
85
- if (params) {
86
- match = { params, handler: route.handler, layouts: route.layouts };
87
- break;
88
- }
74
+ // Flow below; helpers are declared after the main flow for readability
75
+ // 1) Global middlewares
76
+ // ----------------------
77
+ const globalResult = await handleGlobalMiddlewares();
78
+ if (globalResult) {
79
+ return globalResult;
89
80
  }
81
+ // 2) Match route
82
+ // ----------------------
83
+ const match = matchRoute();
90
84
  if (!match) {
91
85
  // todo(peterp, 2025-01-28): Allow the user to define their own "not found" route.
92
86
  return new Response("Not Found", { status: 404 });
93
87
  }
94
- let { params, handler, layouts } = match;
95
- return runWithRequestInfoOverrides({ params }, async () => {
88
+ return await runWithRequestInfoOverrides({ params: match.params }, async () => {
89
+ const { routeMiddlewares, componentHandler } = parseHandlers(match.handler);
90
+ // 3) Route-specific middlewares
91
+ // -----------------------------
92
+ const mwHandled = await handleRouteMiddlewares(routeMiddlewares);
93
+ if (mwHandled) {
94
+ return mwHandled;
95
+ }
96
+ // 4) Final component (always last item)
97
+ // -------------------------------------
98
+ return await handleRouteComponent(componentHandler, match.layouts || []);
99
+ });
100
+ // --- Helpers ---
101
+ function parseHandlers(handler) {
96
102
  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(h, layouts || [], requestInfo);
101
- return await renderPage(requestInfo, WrappedComponent, onError);
103
+ const routeMiddlewares = handlers.slice(0, Math.max(handlers.length - 1, 0));
104
+ const componentHandler = handlers[handlers.length - 1];
105
+ return {
106
+ routeMiddlewares: routeMiddlewares,
107
+ componentHandler,
108
+ };
109
+ }
110
+ function renderElement(element) {
111
+ const requestInfo = getRequestInfo();
112
+ const Element = () => element;
113
+ return renderPage(requestInfo, Element, onError);
114
+ }
115
+ async function handleMiddlewareResult(result) {
116
+ if (result instanceof Response) {
117
+ return result;
118
+ }
119
+ if (result && React.isValidElement(result)) {
120
+ return await renderElement(result);
121
+ }
122
+ return undefined;
123
+ }
124
+ async function handleGlobalMiddlewares() {
125
+ for (const route of flattenedRoutes) {
126
+ if (typeof route !== "function") {
127
+ continue;
102
128
  }
103
- else {
104
- const r = await h(getRequestInfo());
105
- if (r instanceof Response) {
106
- return r;
107
- }
129
+ const result = await route(getRequestInfo());
130
+ const handled = await handleMiddlewareResult(result);
131
+ if (handled)
132
+ return handled;
133
+ }
134
+ return undefined;
135
+ }
136
+ function matchRoute() {
137
+ for (const route of flattenedRoutes) {
138
+ if (typeof route === "function")
139
+ continue;
140
+ const params = matchPath(route.path, path);
141
+ if (params) {
142
+ return { params, handler: route.handler, layouts: route.layouts };
108
143
  }
109
144
  }
110
- // Add fallback return
145
+ return null;
146
+ }
147
+ async function handleRouteMiddlewares(mws) {
148
+ for (const mw of mws) {
149
+ const result = await mw(getRequestInfo());
150
+ const handled = await handleMiddlewareResult(result);
151
+ if (handled)
152
+ return handled;
153
+ }
154
+ return undefined;
155
+ }
156
+ async function handleRouteComponent(component, layouts) {
157
+ if (isRouteComponent(component)) {
158
+ const requestInfo = getRequestInfo();
159
+ const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(component), layouts, requestInfo);
160
+ if (!isClientReference(component)) {
161
+ // context(justinvdm, 31 Jul 2025): We now know we're dealing with a page route,
162
+ // so we create a deferred so that we can signal when we're done determining whether
163
+ // we're returning a response or a react element
164
+ requestInfo.rw.pageRouteResolved = Promise.withResolvers();
165
+ }
166
+ return await renderPage(requestInfo, WrappedComponent, onError);
167
+ }
168
+ // If the last handler is not a component, handle as middleware result (no layouts)
169
+ const tailResult = await component(getRequestInfo());
170
+ const handledTail = await handleMiddlewareResult(tailResult);
171
+ if (handledTail)
172
+ return handledTail;
111
173
  return new Response("Response not returned from route handler", {
112
174
  status: 500,
113
175
  });
114
- });
176
+ }
115
177
  },
116
178
  };
117
179
  }
@@ -164,6 +226,28 @@ function wrapWithLayouts(Component, layouts = [], requestInfo) {
164
226
  return Wrapped;
165
227
  }, Component);
166
228
  }
229
+ // context(justinvdm, 31 Jul 2025): We need to wrap the handler's that might
230
+ // return react elements, so that it throws the response to bubble it up and
231
+ // break out of react rendering context This way, we're able to return a
232
+ // response from the handler while still staying within react rendering context
233
+ export const wrapHandlerToThrowResponses = (handler) => {
234
+ if (isClientReference(handler) ||
235
+ !isRouteComponent(handler) ||
236
+ Object.prototype.hasOwnProperty.call(handler, "__rwsdk_route_component")) {
237
+ return handler;
238
+ }
239
+ const ComponentWrappedToThrowResponses = async (requestInfo) => {
240
+ const result = await handler(requestInfo);
241
+ if (result instanceof Response) {
242
+ requestInfo.rw.pageRouteResolved?.reject(result);
243
+ throw result;
244
+ }
245
+ requestInfo.rw.pageRouteResolved?.resolve();
246
+ return result;
247
+ };
248
+ ComponentWrappedToThrowResponses.__rwsdk_route_component = true;
249
+ return ComponentWrappedToThrowResponses;
250
+ };
167
251
  export function layout(LayoutComponent, routes) {
168
252
  // Attach layouts directly to route definitions
169
253
  return routes.map((route) => {
@@ -202,9 +286,10 @@ options = {}) {
202
286
  return [documentMiddleware, ...routes];
203
287
  }
204
288
  function isRouteComponent(handler) {
205
- return ((isValidElementType(handler) && handler.toString().includes("jsx")) ||
289
+ return (Object.prototype.hasOwnProperty.call(handler, "__rwsdk_route_component") ||
290
+ (isValidElementType(handler) && handler.toString().includes("jsx")) ||
206
291
  isClientReference(handler));
207
292
  }
208
- export const isClientReference = (Component) => {
209
- return Object.prototype.hasOwnProperty.call(Component, "$$isClientReference");
293
+ export const isClientReference = (value) => {
294
+ return Object.prototype.hasOwnProperty.call(value, "$$isClientReference");
210
295
  };
@@ -1,9 +1,4 @@
1
1
  import { type RequestInfo } from "../requestInfo/types.js";
2
- export type CssEntry = {
3
- url: string;
4
- content: string;
5
- absolutePath: string;
6
- };
7
2
  export declare const Stylesheets: ({ requestInfo }: {
8
3
  requestInfo: RequestInfo;
9
4
  }) => import("react/jsx-runtime.js").JSX.Element;
@@ -31,15 +31,5 @@ export const Stylesheets = ({ requestInfo }) => {
31
31
  allStylesheets.add(entry);
32
32
  }
33
33
  }
34
- return (_jsx(_Fragment, { children: Array.from(allStylesheets).map((entry) => {
35
- if (typeof entry === "string") {
36
- return (_jsx("link", { rel: "stylesheet", href: entry, precedence: "first" }, entry));
37
- }
38
- if (import.meta.env.VITE_IS_DEV_SERVER) {
39
- return (_jsx("style", { "data-vite-dev-id": entry.absolutePath, dangerouslySetInnerHTML: { __html: entry.content } }, entry.url));
40
- }
41
- else {
42
- return (_jsx("link", { rel: "stylesheet", href: entry.url, precedence: "first" }, entry.url));
43
- }
44
- }) }));
34
+ return (_jsx(_Fragment, { children: Array.from(allStylesheets).map((href) => (_jsx("link", { rel: "stylesheet", href: href, precedence: "first" }, href))) }));
45
35
  };
@@ -5,7 +5,9 @@ export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
5
5
  request: Request;
6
6
  params: Params;
7
7
  ctx: AppContext;
8
+ /** @deprecated: Use `response.headers` instead */
8
9
  headers: Headers;
9
10
  rw: RwContext;
10
11
  cf: ExecutionContext;
12
+ response: ResponseInit;
11
13
  }
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "async_hooks";
2
2
  const requestInfoDeferred = Promise.withResolvers();
3
3
  const requestInfoStore = new AsyncLocalStorage();
4
4
  const requestInfoBase = {};
5
- const REQUEST_INFO_KEYS = ["request", "params", "ctx", "headers", "rw", "cf"];
5
+ const REQUEST_INFO_KEYS = ["request", "params", "ctx", "headers", "rw", "cf", "response"];
6
6
  REQUEST_INFO_KEYS.forEach((key) => {
7
7
  Object.defineProperty(requestInfoBase, key, {
8
8
  enumerable: true,
@@ -1,5 +1,5 @@
1
1
  export declare const defineScript: (fn: ({ env }: {
2
- env: Cloudflare.Env;
2
+ env: Env;
3
3
  }) => Promise<unknown>) => {
4
4
  fetch: (request: Request, env: Env, cf: ExecutionContext) => Promise<Response>;
5
5
  };