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.
- package/dist/lib/smokeTests/browser.mjs +13 -6
- package/dist/lib/smokeTests/utils.d.mts +9 -0
- package/dist/lib/smokeTests/utils.mjs +29 -0
- package/dist/runtime/client.js +3 -1
- package/dist/runtime/clientNavigation.js +23 -63
- package/dist/runtime/clientNavigation.test.js +2 -3
- package/dist/runtime/lib/db/DOWorkerDialect.d.ts +11 -13
- package/dist/runtime/lib/db/DOWorkerDialect.js +1 -1
- package/dist/runtime/lib/db/createDb.d.ts +2 -1
- package/dist/runtime/lib/db/createDb.js +9 -35
- package/dist/runtime/lib/db/index.d.ts +1 -0
- package/dist/runtime/lib/db/index.js +1 -0
- package/dist/runtime/lib/manifest.js +1 -4
- package/dist/runtime/lib/router.d.ts +6 -4
- package/dist/runtime/lib/router.js +117 -32
- package/dist/runtime/render/stylesheets.d.ts +0 -5
- package/dist/runtime/render/stylesheets.js +1 -11
- package/dist/runtime/requestInfo/types.d.ts +2 -0
- package/dist/runtime/requestInfo/worker.js +1 -1
- package/dist/runtime/script.d.ts +1 -1
- package/dist/runtime/script.js +1 -1
- package/dist/runtime/worker.js +28 -16
- package/dist/scripts/debug-sync.mjs +24 -137
- package/dist/scripts/smoke-test.mjs +4 -5
- package/dist/vite/findSsrSpecifiers.d.mts +8 -9
- package/dist/vite/findSsrSpecifiers.mjs +23 -21
- package/dist/vite/manifestPlugin.mjs +0 -71
- package/dist/vite/miniflareHMRPlugin.mjs +6 -4
- package/dist/vite/ssrBridgePlugin.mjs +22 -29
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/runtime/client.js
CHANGED
|
@@ -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:
|
|
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:
|
|
47
|
+
scrollBehavior: "instant",
|
|
39
48
|
...opts,
|
|
40
49
|
};
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
38
|
+
it("should not include an #hash", () => {
|
|
39
39
|
expect(validateClickEvent(mockEvent, {
|
|
40
40
|
closest: () => ({
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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) =>
|
|
21
|
-
type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) =>
|
|
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: (
|
|
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
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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 ((
|
|
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 = (
|
|
209
|
-
return Object.prototype.hasOwnProperty.call(
|
|
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((
|
|
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,
|
package/dist/runtime/script.d.ts
CHANGED