rwsdk 0.2.0-alpha.1 → 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/clientNavigation.js +19 -65
- 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 +3 -3
- package/dist/runtime/lib/router.js +91 -35
- 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 +25 -6
- 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
|
+
}
|
|
@@ -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) {
|
|
@@ -31,7 +38,6 @@ export function validateClickEvent(event, target) {
|
|
|
31
38
|
return true;
|
|
32
39
|
}
|
|
33
40
|
export function initClientNavigation(opts = {}) {
|
|
34
|
-
// Merge user options with defaults
|
|
35
41
|
const options = {
|
|
36
42
|
onNavigate: async function onNavigate() {
|
|
37
43
|
// @ts-expect-error
|
|
@@ -41,63 +47,7 @@ export function initClientNavigation(opts = {}) {
|
|
|
41
47
|
scrollBehavior: "instant",
|
|
42
48
|
...opts,
|
|
43
49
|
};
|
|
44
|
-
|
|
45
|
-
if ("scrollRestoration" in history) {
|
|
46
|
-
history.scrollRestoration = "manual";
|
|
47
|
-
}
|
|
48
|
-
// Set up scroll behavior management
|
|
49
|
-
let popStateWasCalled = false;
|
|
50
|
-
let savedScrollPosition = null;
|
|
51
|
-
const observer = new MutationObserver(() => {
|
|
52
|
-
if (popStateWasCalled && savedScrollPosition) {
|
|
53
|
-
// Restore scroll position for popstate navigation (always instant)
|
|
54
|
-
window.scrollTo({
|
|
55
|
-
top: savedScrollPosition.y,
|
|
56
|
-
left: savedScrollPosition.x,
|
|
57
|
-
behavior: "instant",
|
|
58
|
-
});
|
|
59
|
-
savedScrollPosition = null;
|
|
60
|
-
}
|
|
61
|
-
else if (options.scrollToTop && !popStateWasCalled) {
|
|
62
|
-
// Scroll to top for anchor click navigation (configurable)
|
|
63
|
-
window.scrollTo({
|
|
64
|
-
top: 0,
|
|
65
|
-
left: 0,
|
|
66
|
-
behavior: options.scrollBehavior,
|
|
67
|
-
});
|
|
68
|
-
// Update the current history entry with the new scroll position (top)
|
|
69
|
-
// This ensures that if we navigate back and then forward again,
|
|
70
|
-
// we return to the top position, not some previous scroll position
|
|
71
|
-
window.history.replaceState({
|
|
72
|
-
...window.history.state,
|
|
73
|
-
scrollX: 0,
|
|
74
|
-
scrollY: 0,
|
|
75
|
-
}, "", window.location.href);
|
|
76
|
-
}
|
|
77
|
-
popStateWasCalled = false;
|
|
78
|
-
});
|
|
79
|
-
const handleScrollPopState = (event) => {
|
|
80
|
-
popStateWasCalled = true;
|
|
81
|
-
// Save the scroll position that the browser would have restored to
|
|
82
|
-
const state = event.state;
|
|
83
|
-
if (state &&
|
|
84
|
-
typeof state === "object" &&
|
|
85
|
-
"scrollX" in state &&
|
|
86
|
-
"scrollY" in state) {
|
|
87
|
-
savedScrollPosition = { x: state.scrollX, y: state.scrollY };
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
// Fallback: try to get scroll position from browser's session history
|
|
91
|
-
// This is a best effort since we can't directly access the browser's stored position
|
|
92
|
-
savedScrollPosition = { x: window.scrollX, y: window.scrollY };
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
const main = document.querySelector("main") || document.body;
|
|
96
|
-
if (main) {
|
|
97
|
-
window.addEventListener("popstate", handleScrollPopState);
|
|
98
|
-
observer.observe(main, { childList: true, subtree: true });
|
|
99
|
-
}
|
|
100
|
-
// Intercept all anchor tag clicks
|
|
50
|
+
history.scrollRestoration = "auto";
|
|
101
51
|
document.addEventListener("click", async function handleClickEvent(event) {
|
|
102
52
|
// Prevent default navigation
|
|
103
53
|
if (!validateClickEvent(event, event.target)) {
|
|
@@ -107,17 +57,21 @@ export function initClientNavigation(opts = {}) {
|
|
|
107
57
|
const el = event.target;
|
|
108
58
|
const a = el.closest("a");
|
|
109
59
|
const href = a?.getAttribute("href");
|
|
110
|
-
|
|
111
|
-
window.history.replaceState({
|
|
112
|
-
path: window.location.pathname,
|
|
113
|
-
scrollX: window.scrollX,
|
|
114
|
-
scrollY: window.scrollY,
|
|
115
|
-
}, "", window.location.href);
|
|
60
|
+
saveScrollPosition(window.scrollX, window.scrollY);
|
|
116
61
|
window.history.pushState({ path: href }, "", window.location.origin + href);
|
|
117
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";
|
|
118
72
|
}, true);
|
|
119
|
-
// Handle browser back/forward buttons
|
|
120
73
|
window.addEventListener("popstate", async function handlePopState() {
|
|
74
|
+
saveScrollPosition(window.scrollX, window.scrollY);
|
|
121
75
|
await options.onNavigate();
|
|
122
76
|
});
|
|
123
77
|
// Return a handleResponse function for use with initClient
|
|
@@ -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");
|
|
@@ -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) =>
|
|
22
|
-
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>;
|
|
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,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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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;
|
|
108
128
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 };
|
|
114
143
|
}
|
|
115
144
|
}
|
|
116
|
-
|
|
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;
|
|
117
173
|
return new Response("Response not returned from route handler", {
|
|
118
174
|
status: 500,
|
|
119
175
|
});
|
|
120
|
-
}
|
|
176
|
+
}
|
|
121
177
|
},
|
|
122
178
|
};
|
|
123
179
|
}
|
|
@@ -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
package/dist/runtime/script.js
CHANGED
package/dist/runtime/worker.js
CHANGED
|
@@ -44,6 +44,10 @@ export const defineApp = (routes) => {
|
|
|
44
44
|
scriptsToBeLoaded: new Set(),
|
|
45
45
|
pageRouteResolved: undefined,
|
|
46
46
|
};
|
|
47
|
+
const userResponseInit = {
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: new Headers(),
|
|
50
|
+
};
|
|
47
51
|
const outerRequestInfo = {
|
|
48
52
|
request,
|
|
49
53
|
headers: userHeaders,
|
|
@@ -51,6 +55,7 @@ export const defineApp = (routes) => {
|
|
|
51
55
|
params: {},
|
|
52
56
|
ctx: {},
|
|
53
57
|
rw,
|
|
58
|
+
response: userResponseInit,
|
|
54
59
|
};
|
|
55
60
|
const createPageElement = (requestInfo, Page) => {
|
|
56
61
|
let pageElement;
|
|
@@ -89,10 +94,12 @@ export const defineApp = (routes) => {
|
|
|
89
94
|
onError,
|
|
90
95
|
});
|
|
91
96
|
if (isRSCRequest) {
|
|
97
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
98
|
+
responseHeaders.set("content-type", "text/x-component; charset=utf-8");
|
|
92
99
|
return new Response(rscPayloadStream, {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
status: userResponseInit.status,
|
|
101
|
+
statusText: userResponseInit.statusText,
|
|
102
|
+
headers: responseHeaders,
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
let injectRSCPayloadStream;
|
|
@@ -112,10 +119,12 @@ export const defineApp = (routes) => {
|
|
|
112
119
|
if (injectRSCPayloadStream) {
|
|
113
120
|
html = html.pipeThrough(injectRSCPayloadStream);
|
|
114
121
|
}
|
|
122
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
123
|
+
responseHeaders.set("content-type", "text/html; charset=utf-8");
|
|
115
124
|
return new Response(html, {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
status: userResponseInit.status,
|
|
126
|
+
statusText: userResponseInit.statusText,
|
|
127
|
+
headers: responseHeaders,
|
|
119
128
|
});
|
|
120
129
|
};
|
|
121
130
|
const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => {
|
|
@@ -135,11 +144,21 @@ export const defineApp = (routes) => {
|
|
|
135
144
|
// context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
|
|
136
145
|
// we need to return a mutable response object.
|
|
137
146
|
const mutableResponse = new Response(response.body, response);
|
|
147
|
+
// Merge user headers from the legacy headers object
|
|
138
148
|
for (const [key, value] of userHeaders.entries()) {
|
|
139
149
|
if (!response.headers.has(key)) {
|
|
140
150
|
mutableResponse.headers.set(key, value);
|
|
141
151
|
}
|
|
142
152
|
}
|
|
153
|
+
// Merge headers from user response init (these take precedence)
|
|
154
|
+
if (userResponseInit.headers) {
|
|
155
|
+
const userResponseHeaders = new Headers(userResponseInit.headers);
|
|
156
|
+
for (const [key, value] of userResponseHeaders.entries()) {
|
|
157
|
+
if (!response.headers.has(key)) {
|
|
158
|
+
mutableResponse.headers.set(key, value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
143
162
|
await rw.pageRouteResolved?.promise;
|
|
144
163
|
return mutableResponse;
|
|
145
164
|
}
|
|
@@ -24,108 +24,12 @@ const getPackageManagerInfo = (targetDir) => {
|
|
|
24
24
|
}
|
|
25
25
|
return pnpmResult;
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
* @summary Workaround for pnpm's local tarball dependency resolution.
|
|
29
|
-
*
|
|
30
|
-
* @description
|
|
31
|
-
* When installing a new version of the SDK from a local tarball (e.g., during
|
|
32
|
-
* development with `rwsync`), pnpm creates a new, uniquely-named directory in
|
|
33
|
-
* the `.pnpm` store (e.g., `rwsdk@file+...`).
|
|
34
|
-
*
|
|
35
|
-
* A challenge arises when other packages list `rwsdk` as a peer dependency.
|
|
36
|
-
* pnpm may not consistently update the symlinks for these peer dependencies
|
|
37
|
-
* to point to the newest `rwsdk` instance. This can result in a state where
|
|
38
|
-
* multiple versions of `rwsdk` coexist in `node_modules`, with some parts of
|
|
39
|
-
* the application using a stale version.
|
|
40
|
-
*
|
|
41
|
-
* This function addresses the issue by:
|
|
42
|
-
* 1. Identifying the most recently installed `rwsdk` instance in the `.pnpm`
|
|
43
|
-
* store after a `pnpm install` run.
|
|
44
|
-
* 2. Forcefully updating the top-level `node_modules/rwsdk` symlink to point
|
|
45
|
-
* to this new instance.
|
|
46
|
-
* 3. Traversing all other `rwsdk`-related directories in the `.pnpm` store
|
|
47
|
-
* and updating their internal `rwsdk` symlinks to also point to the correct
|
|
48
|
-
* new instance.
|
|
49
|
-
*
|
|
50
|
-
* I am sorry for this ugly hack, I am sure there is a better way, and that it is me
|
|
51
|
-
* doing something wrong. The aim is not to go down this rabbit hole right now
|
|
52
|
-
* -- @justinvdm
|
|
53
|
-
*/
|
|
54
|
-
const hackyPnpmSymlinkFix = async (targetDir) => {
|
|
55
|
-
console.log("💣 Performing pnpm symlink fix...");
|
|
56
|
-
const pnpmDir = path.join(targetDir, "node_modules", ".pnpm");
|
|
57
|
-
if (!existsSync(pnpmDir)) {
|
|
58
|
-
console.log(" 🤔 No .pnpm directory found.");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const entries = await fs.readdir(pnpmDir);
|
|
63
|
-
// Find ALL rwsdk directories, not just file-based ones, to handle
|
|
64
|
-
// all kinds of stale peer dependencies.
|
|
65
|
-
const rwsdkDirs = entries.filter((e) => e.startsWith("rwsdk@"));
|
|
66
|
-
console.log(" Found rwsdk directories:", rwsdkDirs);
|
|
67
|
-
if (rwsdkDirs.length === 0) {
|
|
68
|
-
console.log(" 🤔 No rwsdk directories found to hack.");
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
let latestDir = "";
|
|
72
|
-
let latestMtime = new Date(0);
|
|
73
|
-
for (const dir of rwsdkDirs) {
|
|
74
|
-
const fullPath = path.join(pnpmDir, dir);
|
|
75
|
-
const stats = await fs.stat(fullPath);
|
|
76
|
-
if (stats.mtime > latestMtime) {
|
|
77
|
-
latestMtime = stats.mtime;
|
|
78
|
-
latestDir = dir;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
console.log(" Latest rwsdk directory:", latestDir);
|
|
82
|
-
if (!latestDir) {
|
|
83
|
-
console.log(" 🤔 Could not determine the latest rwsdk directory.");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const goldenSourcePath = path.join(pnpmDir, latestDir, "node_modules", "rwsdk");
|
|
87
|
-
if (!existsSync(goldenSourcePath)) {
|
|
88
|
-
console.error(` ❌ Golden source path does not exist: ${goldenSourcePath}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.log(` 🎯 Golden rwsdk path is: ${goldenSourcePath}`);
|
|
92
|
-
// 1. Fix top-level symlink
|
|
93
|
-
const topLevelSymlink = path.join(targetDir, "node_modules", "rwsdk");
|
|
94
|
-
await fs.rm(topLevelSymlink, { recursive: true, force: true });
|
|
95
|
-
await fs.symlink(goldenSourcePath, topLevelSymlink, "dir");
|
|
96
|
-
console.log(` ✅ Symlinked ${topLevelSymlink} -> ${goldenSourcePath}`);
|
|
97
|
-
// 2. Fix peer dependency symlinks
|
|
98
|
-
const allPnpmDirs = await fs.readdir(pnpmDir);
|
|
99
|
-
for (const dir of allPnpmDirs) {
|
|
100
|
-
if (dir === latestDir || !dir.includes("rwsdk"))
|
|
101
|
-
continue;
|
|
102
|
-
const peerSymlink = path.join(pnpmDir, dir, "node_modules", "rwsdk");
|
|
103
|
-
if (existsSync(peerSymlink)) {
|
|
104
|
-
await fs.rm(peerSymlink, { recursive: true, force: true });
|
|
105
|
-
await fs.symlink(goldenSourcePath, peerSymlink, "dir");
|
|
106
|
-
console.log(` ✅ Hijacked symlink in ${dir}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
console.error(" ❌ Failed during hacky pnpm symlink fix:", error);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
const performFullSync = async (sdkDir, targetDir, cacheBust = false) => {
|
|
27
|
+
const performFullSync = async (sdkDir, targetDir) => {
|
|
115
28
|
const sdkPackageJsonPath = path.join(sdkDir, "package.json");
|
|
116
29
|
let originalSdkPackageJson = null;
|
|
117
30
|
let tarballPath = "";
|
|
118
31
|
let tarballName = "";
|
|
119
32
|
try {
|
|
120
|
-
if (cacheBust) {
|
|
121
|
-
console.log("💥 Cache-busting version for full sync...");
|
|
122
|
-
originalSdkPackageJson = await fs.readFile(sdkPackageJsonPath, "utf-8");
|
|
123
|
-
const packageJson = JSON.parse(originalSdkPackageJson);
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
// This is a temporary version used for cache busting
|
|
126
|
-
packageJson.version = `${packageJson.version}-dev.${now}`;
|
|
127
|
-
await fs.writeFile(sdkPackageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
128
|
-
}
|
|
129
33
|
console.log("📦 Packing SDK...");
|
|
130
34
|
const packResult = await $({ cwd: sdkDir }) `npm pack`;
|
|
131
35
|
tarballName = packResult.stdout?.trim() ?? "";
|
|
@@ -145,47 +49,25 @@ const performFullSync = async (sdkDir, targetDir, cacheBust = false) => {
|
|
|
145
49
|
.readFile(lockfilePath, "utf-8")
|
|
146
50
|
.catch(() => null);
|
|
147
51
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
targetPackageJson.pnpm = targetPackageJson.pnpm || {};
|
|
153
|
-
targetPackageJson.pnpm.overrides =
|
|
154
|
-
targetPackageJson.pnpm.overrides || {};
|
|
155
|
-
targetPackageJson.pnpm.overrides.rwsdk = `file:${tarballPath}`;
|
|
156
|
-
await fs.writeFile(packageJsonPath, JSON.stringify(targetPackageJson, null, 2));
|
|
157
|
-
}
|
|
158
|
-
// We use install here, which respects the overrides.
|
|
159
|
-
// We also don't want to fail if the lockfile is out of date.
|
|
160
|
-
await $("pnpm", ["install", "--no-frozen-lockfile"], {
|
|
161
|
-
cwd: targetDir,
|
|
162
|
-
stdio: "inherit",
|
|
163
|
-
});
|
|
164
|
-
if (process.env.RWSDK_PNPM_SYMLINK_FIX) {
|
|
165
|
-
await hackyPnpmSymlinkFix(targetDir);
|
|
166
|
-
}
|
|
52
|
+
const cmd = pm.name;
|
|
53
|
+
const args = [pm.command];
|
|
54
|
+
if (pm.name === "yarn") {
|
|
55
|
+
args.push(`file:${tarballPath}`);
|
|
167
56
|
}
|
|
168
57
|
else {
|
|
169
|
-
|
|
170
|
-
const args = [pm.command];
|
|
171
|
-
if (pm.name === "yarn") {
|
|
172
|
-
args.push(`file:${tarballPath}`);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
args.push(tarballPath);
|
|
176
|
-
}
|
|
177
|
-
await $(cmd, args, {
|
|
178
|
-
cwd: targetDir,
|
|
179
|
-
stdio: "inherit",
|
|
180
|
-
});
|
|
58
|
+
args.push(tarballPath);
|
|
181
59
|
}
|
|
60
|
+
await $(cmd, args, {
|
|
61
|
+
cwd: targetDir,
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
});
|
|
182
64
|
}
|
|
183
65
|
finally {
|
|
184
66
|
if (originalPackageJson) {
|
|
185
67
|
console.log("Restoring package.json...");
|
|
186
68
|
await fs.writeFile(packageJsonPath, originalPackageJson);
|
|
187
69
|
}
|
|
188
|
-
if (originalLockfile
|
|
70
|
+
if (originalLockfile) {
|
|
189
71
|
console.log(`Restoring ${pm.lockFile}...`);
|
|
190
72
|
await fs.writeFile(lockfilePath, originalLockfile);
|
|
191
73
|
}
|
|
@@ -218,13 +100,17 @@ const performFastSync = async (sdkDir, targetDir) => {
|
|
|
218
100
|
// Always copy package.json
|
|
219
101
|
await fs.copyFile(path.join(sdkDir, "package.json"), path.join(targetDir, "node_modules/rwsdk/package.json"));
|
|
220
102
|
};
|
|
103
|
+
const areDependenciesEqual = (deps1, deps2) => {
|
|
104
|
+
// Simple string comparison for this use case is sufficient
|
|
105
|
+
return JSON.stringify(deps1 ?? {}) === JSON.stringify(deps2 ?? {});
|
|
106
|
+
};
|
|
221
107
|
const performSync = async (sdkDir, targetDir) => {
|
|
222
108
|
console.log("🏗️ Rebuilding SDK...");
|
|
223
109
|
await $ `pnpm build`;
|
|
224
110
|
const forceFullSync = Boolean(process.env.RWSDK_FORCE_FULL_SYNC);
|
|
225
111
|
if (forceFullSync) {
|
|
226
112
|
console.log("🏃 Force full sync mode is enabled.");
|
|
227
|
-
await performFullSync(sdkDir, targetDir
|
|
113
|
+
await performFullSync(sdkDir, targetDir);
|
|
228
114
|
console.log("✅ Done syncing");
|
|
229
115
|
return;
|
|
230
116
|
}
|
|
@@ -234,15 +120,17 @@ const performSync = async (sdkDir, targetDir) => {
|
|
|
234
120
|
if (existsSync(installedSdkPackageJsonPath)) {
|
|
235
121
|
const sdkPackageJsonContent = await fs.readFile(sdkPackageJsonPath, "utf-8");
|
|
236
122
|
const installedSdkPackageJsonContent = await fs.readFile(installedSdkPackageJsonPath, "utf-8");
|
|
237
|
-
|
|
123
|
+
const sdkPkg = JSON.parse(sdkPackageJsonContent);
|
|
124
|
+
const installedPkg = JSON.parse(installedSdkPackageJsonContent);
|
|
125
|
+
if (areDependenciesEqual(sdkPkg.dependencies, installedPkg.dependencies) &&
|
|
126
|
+
areDependenciesEqual(sdkPkg.devDependencies, installedPkg.devDependencies) &&
|
|
127
|
+
areDependenciesEqual(sdkPkg.peerDependencies, installedPkg.peerDependencies)) {
|
|
238
128
|
packageJsonChanged = false;
|
|
239
129
|
}
|
|
240
130
|
}
|
|
241
131
|
if (packageJsonChanged) {
|
|
242
132
|
console.log("📦 package.json changed, performing full sync...");
|
|
243
|
-
|
|
244
|
-
// see a new version and the hacky symlink fix runs on a clean slate.
|
|
245
|
-
await performFullSync(sdkDir, targetDir, true);
|
|
133
|
+
await performFullSync(sdkDir, targetDir);
|
|
246
134
|
}
|
|
247
135
|
else {
|
|
248
136
|
await performFastSync(sdkDir, targetDir);
|
|
@@ -261,8 +149,7 @@ export const debugSync = async (opts) => {
|
|
|
261
149
|
return;
|
|
262
150
|
}
|
|
263
151
|
// --- Watch Mode Logic ---
|
|
264
|
-
|
|
265
|
-
const lockfilePath = path.join(sdkDir, ".rwsync.lock");
|
|
152
|
+
const lockfilePath = path.join(targetDir, "node_modules", ".rwsync.lock");
|
|
266
153
|
let release;
|
|
267
154
|
// Ensure the directory for the lockfile exists
|
|
268
155
|
await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
|
|
@@ -273,7 +160,7 @@ export const debugSync = async (opts) => {
|
|
|
273
160
|
}
|
|
274
161
|
catch (e) {
|
|
275
162
|
if (e.code === "ELOCKED") {
|
|
276
|
-
console.error(`❌ Another rwsync process is already
|
|
163
|
+
console.error(`❌ Another rwsync process is already watching ${targetDir}.`);
|
|
277
164
|
console.error(` If this is not correct, please remove the lockfile at ${lockfilePath}`);
|
|
278
165
|
process.exit(1);
|
|
279
166
|
}
|
|
@@ -30,8 +30,7 @@ if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
30
30
|
copyProject: false, // Default to false - don't copy project to artifacts
|
|
31
31
|
realtime: false, // Default to false - don't just test realtime
|
|
32
32
|
skipHmr: false, // Default to false - run HMR tests
|
|
33
|
-
|
|
34
|
-
skipStyleTests: true, // Default to true - skip style tests
|
|
33
|
+
skipStyleTests: false,
|
|
35
34
|
// sync: will be set below
|
|
36
35
|
};
|
|
37
36
|
// Log if we're in CI
|
|
@@ -55,8 +54,8 @@ if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
55
54
|
else if (arg === "--skip-hmr") {
|
|
56
55
|
options.skipHmr = true;
|
|
57
56
|
}
|
|
58
|
-
else if (arg === "--
|
|
59
|
-
options.skipStyleTests =
|
|
57
|
+
else if (arg === "--skip-style-tests") {
|
|
58
|
+
options.skipStyleTests = true;
|
|
60
59
|
}
|
|
61
60
|
else if (arg === "--keep") {
|
|
62
61
|
options.keep = true;
|
|
@@ -94,7 +93,7 @@ Options:
|
|
|
94
93
|
--skip-release Skip testing the release/production deployment
|
|
95
94
|
--skip-client Skip client-side tests, only run server-side checks
|
|
96
95
|
--skip-hmr Skip hot module replacement (HMR) tests
|
|
97
|
-
--
|
|
96
|
+
--skip-style-tests Skip stylesheet-related tests
|
|
98
97
|
--path=PATH Project directory to test
|
|
99
98
|
--artifact-dir=DIR Directory to store test artifacts (default: .artifacts)
|
|
100
99
|
--keep Keep temporary test directory after tests complete
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Finds __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
3
|
-
*
|
|
4
|
-
* @param code The code to search for SSR imports.
|
|
5
|
-
* @param log Optional logger function for debug output.
|
|
6
|
-
* @returns Object with arrays of static and dynamic import specifiers.
|
|
2
|
+
* Finds callsites for __vite_ssr_import__ and __vite_ssr_dynamic_import__ with their ranges.
|
|
3
|
+
* The returned ranges can be used with MagicString to overwrite the entire call expression.
|
|
7
4
|
*/
|
|
8
|
-
export declare function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export declare function findSsrImportCallSites(id: string, code: string, log?: (...args: any[]) => void): Array<{
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
specifier: string;
|
|
9
|
+
kind: "import" | "dynamic_import";
|
|
10
|
+
}>;
|
|
@@ -1,67 +1,69 @@
|
|
|
1
1
|
import { parse as sgParse, Lang as SgLang, Lang } from "@ast-grep/napi";
|
|
2
2
|
import path from "path";
|
|
3
3
|
/**
|
|
4
|
-
* Finds __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
5
|
-
*
|
|
6
|
-
* @param code The code to search for SSR imports.
|
|
7
|
-
* @param log Optional logger function for debug output.
|
|
8
|
-
* @returns Object with arrays of static and dynamic import specifiers.
|
|
4
|
+
* Finds callsites for __vite_ssr_import__ and __vite_ssr_dynamic_import__ with their ranges.
|
|
5
|
+
* The returned ranges can be used with MagicString to overwrite the entire call expression.
|
|
9
6
|
*/
|
|
10
|
-
export function
|
|
7
|
+
export function findSsrImportCallSites(id, code, log) {
|
|
11
8
|
const ext = path.extname(id).toLowerCase();
|
|
12
9
|
const lang = ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : SgLang.TypeScript;
|
|
13
10
|
const logger = process.env.VERBOSE ? (log ?? (() => { })) : () => { };
|
|
14
|
-
const
|
|
15
|
-
const dynamicImports = [];
|
|
11
|
+
const results = [];
|
|
16
12
|
try {
|
|
17
13
|
const root = sgParse(lang, code);
|
|
18
14
|
const patterns = [
|
|
19
15
|
{
|
|
20
16
|
pattern: `__vite_ssr_import__("$SPECIFIER")`,
|
|
21
|
-
|
|
17
|
+
kind: "import",
|
|
22
18
|
},
|
|
23
19
|
{
|
|
24
20
|
pattern: `__vite_ssr_import__('$SPECIFIER')`,
|
|
25
|
-
|
|
21
|
+
kind: "import",
|
|
26
22
|
},
|
|
27
23
|
{
|
|
28
24
|
pattern: `__vite_ssr_dynamic_import__("$SPECIFIER")`,
|
|
29
|
-
|
|
25
|
+
kind: "dynamic_import",
|
|
30
26
|
},
|
|
31
27
|
{
|
|
32
28
|
pattern: `__vite_ssr_dynamic_import__('$SPECIFIER')`,
|
|
33
|
-
|
|
29
|
+
kind: "dynamic_import",
|
|
34
30
|
},
|
|
35
31
|
{
|
|
36
32
|
pattern: `__vite_ssr_import__("$SPECIFIER", $$$REST)`,
|
|
37
|
-
|
|
33
|
+
kind: "import",
|
|
38
34
|
},
|
|
39
35
|
{
|
|
40
36
|
pattern: `__vite_ssr_import__('$SPECIFIER', $$$REST)`,
|
|
41
|
-
|
|
37
|
+
kind: "import",
|
|
42
38
|
},
|
|
43
39
|
{
|
|
44
40
|
pattern: `__vite_ssr_dynamic_import__("$SPECIFIER", $$$REST)`,
|
|
45
|
-
|
|
41
|
+
kind: "dynamic_import",
|
|
46
42
|
},
|
|
47
43
|
{
|
|
48
44
|
pattern: `__vite_ssr_dynamic_import__('$SPECIFIER', $$$REST)`,
|
|
49
|
-
|
|
45
|
+
kind: "dynamic_import",
|
|
50
46
|
},
|
|
51
47
|
];
|
|
52
|
-
for (const { pattern,
|
|
48
|
+
for (const { pattern, kind } of patterns) {
|
|
53
49
|
const matches = root.root().findAll(pattern);
|
|
54
50
|
for (const match of matches) {
|
|
55
51
|
const specifier = match.getMatch("SPECIFIER")?.text();
|
|
56
52
|
if (specifier) {
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
const range = match.range();
|
|
54
|
+
results.push({
|
|
55
|
+
start: range.start.index,
|
|
56
|
+
end: range.end.index,
|
|
57
|
+
specifier,
|
|
58
|
+
kind,
|
|
59
|
+
});
|
|
60
|
+
logger(`Found SSR import callsite: %s [%s] at %d-%d`, specifier, kind, range.start.index, range.end.index);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
catch (err) {
|
|
64
|
-
logger("Error parsing code for SSR
|
|
66
|
+
logger("Error parsing code for SSR import callsites: %O", err);
|
|
65
67
|
}
|
|
66
|
-
return
|
|
68
|
+
return results;
|
|
67
69
|
}
|
|
@@ -4,36 +4,6 @@ import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
|
|
|
4
4
|
const log = debug("rwsdk:vite:manifest-plugin");
|
|
5
5
|
const virtualModuleId = "virtual:rwsdk:manifest.js";
|
|
6
6
|
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
7
|
-
const getCssForModule = (server, moduleId, css) => {
|
|
8
|
-
const stack = [moduleId];
|
|
9
|
-
const visited = new Set();
|
|
10
|
-
while (stack.length > 0) {
|
|
11
|
-
const currentModuleId = stack.pop();
|
|
12
|
-
if (visited.has(currentModuleId)) {
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
visited.add(currentModuleId);
|
|
16
|
-
const moduleNode = server.environments.client.moduleGraph.getModuleById(currentModuleId);
|
|
17
|
-
if (!moduleNode) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
for (const importedModule of moduleNode.importedModules) {
|
|
21
|
-
if (importedModule.url.endsWith(".css")) {
|
|
22
|
-
const absolutePath = importedModule.file;
|
|
23
|
-
css.add({
|
|
24
|
-
url: importedModule.url,
|
|
25
|
-
// The `ssrTransformResult` has the CSS content, because the default
|
|
26
|
-
// transform for CSS is to a string of the CSS content.
|
|
27
|
-
content: importedModule.ssrTransformResult?.code ?? "",
|
|
28
|
-
absolutePath,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (importedModule.id) {
|
|
32
|
-
stack.push(importedModule.id);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
7
|
export const manifestPlugin = ({ manifestPath, }) => {
|
|
38
8
|
let isBuild = false;
|
|
39
9
|
let root;
|
|
@@ -106,46 +76,5 @@ export const manifestPlugin = ({ manifestPath, }) => {
|
|
|
106
76
|
},
|
|
107
77
|
});
|
|
108
78
|
},
|
|
109
|
-
configureServer(server) {
|
|
110
|
-
log("Configuring server middleware for manifest");
|
|
111
|
-
server.middlewares.use("/__rwsdk_manifest", async (req, res, next) => {
|
|
112
|
-
log("Manifest request received: %s", req.url);
|
|
113
|
-
try {
|
|
114
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
115
|
-
const scripts = JSON.parse(url.searchParams.get("scripts") || "[]");
|
|
116
|
-
process.env.VERBOSE && log("Transforming scripts: %o", scripts);
|
|
117
|
-
for (const script of scripts) {
|
|
118
|
-
await server.environments.client.transformRequest(script);
|
|
119
|
-
}
|
|
120
|
-
const manifest = {};
|
|
121
|
-
log("Building manifest from module graph");
|
|
122
|
-
for (const file of server.environments.client.moduleGraph.fileToModulesMap.keys()) {
|
|
123
|
-
const modules = server.environments.client.moduleGraph.getModulesByFile(file);
|
|
124
|
-
if (!modules) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
for (const module of modules) {
|
|
128
|
-
if (module.file) {
|
|
129
|
-
const css = new Set();
|
|
130
|
-
getCssForModule(server, module.id, css);
|
|
131
|
-
manifest[normalizeModulePath(module.file, server.config.root)] =
|
|
132
|
-
{
|
|
133
|
-
file: module.url,
|
|
134
|
-
css: Array.from(css),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
log("Manifest built successfully");
|
|
140
|
-
process.env.VERBOSE && log("Manifest: %o", manifest);
|
|
141
|
-
res.setHeader("Content-Type", "application/json");
|
|
142
|
-
res.end(JSON.stringify(manifest));
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
log("Error building manifest: %o", e);
|
|
146
|
-
next(e);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
79
|
};
|
|
151
80
|
};
|
|
@@ -109,19 +109,19 @@ export const miniflareHMRPlugin = (givenOptions) => [
|
|
|
109
109
|
let clientDirectiveChanged = false;
|
|
110
110
|
let serverDirectiveChanged = false;
|
|
111
111
|
if (!clientFiles.has(ctx.file) && hasClientDirective) {
|
|
112
|
-
clientFiles.add(ctx.file);
|
|
112
|
+
clientFiles.add(normalizeModulePath(ctx.file, givenOptions.rootDir));
|
|
113
113
|
clientDirectiveChanged = true;
|
|
114
114
|
}
|
|
115
115
|
else if (clientFiles.has(ctx.file) && !hasClientDirective) {
|
|
116
|
-
clientFiles.delete(ctx.file);
|
|
116
|
+
clientFiles.delete(normalizeModulePath(ctx.file, givenOptions.rootDir));
|
|
117
117
|
clientDirectiveChanged = true;
|
|
118
118
|
}
|
|
119
119
|
if (!serverFiles.has(ctx.file) && hasServerDirective) {
|
|
120
|
-
serverFiles.add(ctx.file);
|
|
120
|
+
serverFiles.add(normalizeModulePath(ctx.file, givenOptions.rootDir));
|
|
121
121
|
serverDirectiveChanged = true;
|
|
122
122
|
}
|
|
123
123
|
else if (serverFiles.has(ctx.file) && !hasServerDirective) {
|
|
124
|
-
serverFiles.delete(ctx.file);
|
|
124
|
+
serverFiles.delete(normalizeModulePath(ctx.file, givenOptions.rootDir));
|
|
125
125
|
serverDirectiveChanged = true;
|
|
126
126
|
}
|
|
127
127
|
if (clientDirectiveChanged) {
|
|
@@ -129,12 +129,14 @@ export const miniflareHMRPlugin = (givenOptions) => [
|
|
|
129
129
|
invalidateModule(ctx.server, environment, "virtual:use-client-lookup.js");
|
|
130
130
|
});
|
|
131
131
|
invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "/@id/virtual:use-client-lookup.js");
|
|
132
|
+
invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "virtual:use-client-lookup.js");
|
|
132
133
|
}
|
|
133
134
|
if (serverDirectiveChanged) {
|
|
134
135
|
["client", "ssr", environment].forEach((environment) => {
|
|
135
136
|
invalidateModule(ctx.server, environment, "virtual:use-server-lookup.js");
|
|
136
137
|
});
|
|
137
138
|
invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "/@id/virtual:use-server-lookup.js");
|
|
139
|
+
invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "virtual:use-server-lookup.js");
|
|
138
140
|
}
|
|
139
141
|
// todo(justinvdm, 12 Dec 2024): Skip client references
|
|
140
142
|
const modules = Array.from(ctx.server.environments[environment].moduleGraph.getModulesByFile(ctx.file) ?? []);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
2
|
import { SSR_BRIDGE_PATH } from "../lib/constants.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { findSsrImportCallSites } from "./findSsrSpecifiers.mjs";
|
|
4
|
+
import MagicString from "magic-string";
|
|
4
5
|
const log = debug("rwsdk:vite:ssr-bridge-plugin");
|
|
5
6
|
export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
|
|
6
7
|
export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
@@ -108,35 +109,27 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
108
109
|
return "export default {};";
|
|
109
110
|
}
|
|
110
111
|
log("Fetched SSR module code length: %d", code?.length || 0);
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return isDynamic
|
|
132
|
-
? __vite_ssr_dynamic_import__("/@id/${VIRTUAL_SSR_PREFIX}" + id, ...args)
|
|
133
|
-
: __vite_ssr_import__("/@id/${VIRTUAL_SSR_PREFIX}" + id, ...args);
|
|
134
|
-
}
|
|
135
|
-
`;
|
|
136
|
-
log("Transformed SSR module code length: %d", transformedCode.length);
|
|
112
|
+
const s = new MagicString(code || "");
|
|
113
|
+
const callsites = findSsrImportCallSites(idForFetch, code || "", log);
|
|
114
|
+
for (const site of callsites) {
|
|
115
|
+
const normalized = site.specifier.startsWith("/@id/")
|
|
116
|
+
? site.specifier.slice("/@id/".length)
|
|
117
|
+
: site.specifier;
|
|
118
|
+
// context(justinvdm, 11 Aug 2025):
|
|
119
|
+
// - We replace __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
120
|
+
// with import() calls so that the module graph can be built
|
|
121
|
+
// correctly (vite looks for imports and import()s to build module
|
|
122
|
+
// graph)
|
|
123
|
+
// - We prepend /@id/$VIRTUAL_SSR_PREFIX to the specifier so that we
|
|
124
|
+
// can stay within the SSR subgraph of the worker module graph
|
|
125
|
+
const replacement = `import("/@id/${VIRTUAL_SSR_PREFIX}${normalized}")`;
|
|
126
|
+
s.overwrite(site.start, site.end, replacement);
|
|
127
|
+
}
|
|
128
|
+
const out = s.toString();
|
|
129
|
+
log("Transformed SSR module code length: %d", out.length);
|
|
137
130
|
process.env.VERBOSE &&
|
|
138
|
-
log("Transformed SSR module code for realId=%s: %s", realId,
|
|
139
|
-
return
|
|
131
|
+
log("Transformed SSR module code for realId=%s: %s", realId, out);
|
|
132
|
+
return out;
|
|
140
133
|
}
|
|
141
134
|
}
|
|
142
135
|
process.env.VERBOSE && log("No load handling for id=%s", id);
|