rwsdk 1.2.8 → 1.2.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/e2e/browser.d.mts +4 -0
- package/dist/lib/e2e/browser.mjs +58 -0
- package/dist/lib/e2e/constants.d.mts +2 -0
- package/dist/lib/e2e/constants.mjs +6 -0
- package/dist/lib/e2e/dev.mjs +7 -18
- package/dist/lib/e2e/release.d.mts +7 -0
- package/dist/lib/e2e/release.mjs +78 -2
- package/dist/lib/e2e/tarball.mjs +4 -1
- package/dist/lib/e2e/testHarness.d.mts +1 -7
- package/dist/lib/e2e/testHarness.mjs +30 -10
- package/dist/lib/smokeTests/browser.d.mts +1 -1
- package/dist/lib/smokeTests/browser.mjs +36 -30
- package/dist/lib/smokeTests/release.d.mts +8 -1
- package/dist/lib/smokeTests/release.mjs +54 -29
- package/dist/lib/smokeTests/runSmokeTests.mjs +1 -1
- package/dist/runtime/client/navigation.js +42 -61
- package/dist/runtime/client/navigation.test.js +145 -8
- package/dist/runtime/client/scrollRestoration.d.ts +25 -0
- package/dist/runtime/client/scrollRestoration.js +157 -0
- package/dist/runtime/client/scrollRestoration.test.d.ts +1 -0
- package/dist/runtime/client/scrollRestoration.test.js +93 -0
- package/dist/runtime/lib/router.d.ts +1 -0
- package/dist/runtime/lib/router.js +27 -2
- package/dist/runtime/lib/router.test.js +96 -0
- package/dist/scripts/worker-run.mjs +42 -8
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +14 -7
- package/dist/use-synced-state/__tests__/worker.test.mjs +41 -2
- package/dist/use-synced-state/worker.mjs +34 -3
- package/dist/vite/miniflareHMRPlugin.mjs +1 -1
- package/dist/vite/ssrBridgePlugin.d.mts +0 -1
- package/dist/vite/ssrBridgePlugin.mjs +17 -14
- package/dist/vite/ssrVirtualModule.d.mts +3 -0
- package/dist/vite/ssrVirtualModule.mjs +11 -0
- package/dist/vite/transformJsxScriptTagsPlugin.hook.test.d.mts +1 -0
- package/dist/vite/transformJsxScriptTagsPlugin.hook.test.mjs +73 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +5 -0
- package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +4 -3
- package/package.json +2 -3
|
@@ -185,11 +185,28 @@ export function defineRoutes(routes) {
|
|
|
185
185
|
function isExceptHandler(route) {
|
|
186
186
|
return route.type === "except";
|
|
187
187
|
}
|
|
188
|
+
function isPathInExceptScope(pathPattern, requestPath) {
|
|
189
|
+
const normalized = pathPattern.endsWith("/")
|
|
190
|
+
? pathPattern
|
|
191
|
+
: pathPattern + "/";
|
|
192
|
+
const hasParams = normalized.includes(":") || normalized.includes("*");
|
|
193
|
+
if (hasParams) {
|
|
194
|
+
const wildcardPattern = normalized.slice(0, -1) + "/*";
|
|
195
|
+
return (matchPath(wildcardPattern, requestPath) !== null ||
|
|
196
|
+
matchPath(normalized.slice(0, -1), requestPath) !== null);
|
|
197
|
+
}
|
|
198
|
+
return (requestPath === normalized || requestPath.startsWith(normalized));
|
|
199
|
+
}
|
|
188
200
|
async function executeExceptHandlers(error, startIndex) {
|
|
189
201
|
// Search backwards from startIndex to find the most recent except handler
|
|
190
202
|
for (let i = startIndex; i >= 0; i--) {
|
|
191
203
|
const route = compiledRoutes[i];
|
|
192
204
|
if (isExceptHandler(route)) {
|
|
205
|
+
const pattern = route.handler.pathPattern;
|
|
206
|
+
if (pattern &&
|
|
207
|
+
!isPathInExceptScope(pattern, getRequestInfo().path)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
193
210
|
try {
|
|
194
211
|
const result = await route.handler.handler(error, getRequestInfo());
|
|
195
212
|
const handled = await handleMiddlewareResult(result);
|
|
@@ -554,8 +571,16 @@ export function prefix(prefixPath, routes) {
|
|
|
554
571
|
r !== null &&
|
|
555
572
|
"__rwExcept" in r &&
|
|
556
573
|
r.__rwExcept === true) {
|
|
557
|
-
|
|
558
|
-
|
|
574
|
+
const existing = r.pathPattern;
|
|
575
|
+
const combined = existing
|
|
576
|
+
? joinPaths(normalizedPrefix, existing)
|
|
577
|
+
: normalizedPrefix;
|
|
578
|
+
const scoped = {
|
|
579
|
+
__rwExcept: true,
|
|
580
|
+
handler: r.handler,
|
|
581
|
+
pathPattern: combined,
|
|
582
|
+
};
|
|
583
|
+
return scoped;
|
|
559
584
|
}
|
|
560
585
|
if (Array.isArray(r)) {
|
|
561
586
|
// Recursively process nested route arrays
|
|
@@ -1535,5 +1535,101 @@ describe("defineRoutes - Request Handling Behavior", () => {
|
|
|
1535
1535
|
expect(response.status).toBe(500);
|
|
1536
1536
|
expect(await response.text()).toBe(`Caught: ${errorMessage}`);
|
|
1537
1537
|
});
|
|
1538
|
+
describe("prefix scoping", () => {
|
|
1539
|
+
const makePrefixApp = () => {
|
|
1540
|
+
const globalHandler = except(() => new Response("[GLOBAL]", {
|
|
1541
|
+
status: 500,
|
|
1542
|
+
}));
|
|
1543
|
+
const adminHandler = except(() => new Response("[ADMIN]", {
|
|
1544
|
+
status: 500,
|
|
1545
|
+
}));
|
|
1546
|
+
const router = defineRoutes([
|
|
1547
|
+
globalHandler,
|
|
1548
|
+
...prefix("/admin", [
|
|
1549
|
+
adminHandler,
|
|
1550
|
+
route("/dashboard/", () => {
|
|
1551
|
+
throw new Error("admin dashboard error");
|
|
1552
|
+
}),
|
|
1553
|
+
]),
|
|
1554
|
+
route("/", () => {
|
|
1555
|
+
throw new Error("home route error");
|
|
1556
|
+
}),
|
|
1557
|
+
]);
|
|
1558
|
+
return router;
|
|
1559
|
+
};
|
|
1560
|
+
const handle = async (router, path) => {
|
|
1561
|
+
const deps = createMockDependencies();
|
|
1562
|
+
deps.mockRequestInfo.request = new Request(`http://localhost:3000${path}`);
|
|
1563
|
+
return router.handle({
|
|
1564
|
+
request: new Request(`http://localhost:3000${path}`),
|
|
1565
|
+
renderPage: deps.mockRenderPage,
|
|
1566
|
+
getRequestInfo: deps.getRequestInfo,
|
|
1567
|
+
onError: deps.onError,
|
|
1568
|
+
runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
|
|
1569
|
+
rscActionHandler: deps.mockRscActionHandler,
|
|
1570
|
+
});
|
|
1571
|
+
};
|
|
1572
|
+
it("an except inside prefix should NOT catch errors from routes outside the prefix", async () => {
|
|
1573
|
+
const router = makePrefixApp();
|
|
1574
|
+
const response = await handle(router, "/");
|
|
1575
|
+
expect(await response.text()).toBe("[GLOBAL]");
|
|
1576
|
+
});
|
|
1577
|
+
it("an except inside prefix should catch errors from routes inside the prefix", async () => {
|
|
1578
|
+
const router = makePrefixApp();
|
|
1579
|
+
const response = await handle(router, "/admin/dashboard/");
|
|
1580
|
+
expect(await response.text()).toBe("[ADMIN]");
|
|
1581
|
+
});
|
|
1582
|
+
it("an except inside prefix that returns void should bubble to the global handler", async () => {
|
|
1583
|
+
const router = defineRoutes([
|
|
1584
|
+
except(() => new Response("[GLOBAL]", { status: 500 })),
|
|
1585
|
+
...prefix("/admin", [
|
|
1586
|
+
except(() => undefined),
|
|
1587
|
+
route("/dashboard/", () => {
|
|
1588
|
+
throw new Error("boom");
|
|
1589
|
+
}),
|
|
1590
|
+
]),
|
|
1591
|
+
]);
|
|
1592
|
+
const response = await handle(router, "/admin/dashboard/");
|
|
1593
|
+
expect(await response.text()).toBe("[GLOBAL]");
|
|
1594
|
+
});
|
|
1595
|
+
it("nested prefixes compose the scope path", async () => {
|
|
1596
|
+
const router = defineRoutes([
|
|
1597
|
+
except(() => new Response("[GLOBAL]", { status: 500 })),
|
|
1598
|
+
...prefix("/a", [
|
|
1599
|
+
...prefix("/b", [
|
|
1600
|
+
except(() => new Response("[A/B]", { status: 500 })),
|
|
1601
|
+
route("/c/", () => {
|
|
1602
|
+
throw new Error("a/b/c error");
|
|
1603
|
+
}),
|
|
1604
|
+
]),
|
|
1605
|
+
]),
|
|
1606
|
+
route("/d/", () => {
|
|
1607
|
+
throw new Error("d error");
|
|
1608
|
+
}),
|
|
1609
|
+
]);
|
|
1610
|
+
const inside = await handle(router, "/a/b/c/");
|
|
1611
|
+
expect(await inside.text()).toBe("[A/B]");
|
|
1612
|
+
const outside = await handle(router, "/d/");
|
|
1613
|
+
expect(await outside.text()).toBe("[GLOBAL]");
|
|
1614
|
+
});
|
|
1615
|
+
it("parameterized prefixes scope the except handler", async () => {
|
|
1616
|
+
const router = defineRoutes([
|
|
1617
|
+
except(() => new Response("[GLOBAL]", { status: 500 })),
|
|
1618
|
+
...prefix("/users/:id", [
|
|
1619
|
+
except(() => new Response("[USER]", { status: 500 })),
|
|
1620
|
+
route("/profile/", () => {
|
|
1621
|
+
throw new Error("profile error");
|
|
1622
|
+
}),
|
|
1623
|
+
]),
|
|
1624
|
+
route("/about/", () => {
|
|
1625
|
+
throw new Error("about error");
|
|
1626
|
+
}),
|
|
1627
|
+
]);
|
|
1628
|
+
const inside = await handle(router, "/users/42/profile/");
|
|
1629
|
+
expect(await inside.text()).toBe("[USER]");
|
|
1630
|
+
const outside = await handle(router, "/about/");
|
|
1631
|
+
expect(await outside.text()).toBe("[GLOBAL]");
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1538
1634
|
});
|
|
1539
1635
|
});
|
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
import * as vite from "vite";
|
|
7
7
|
import { createLogger } from "vite";
|
|
8
|
+
import { checkServerUp } from "../lib/e2e/browser.mjs";
|
|
8
9
|
const debug = dbg("rwsdk:worker-run");
|
|
9
10
|
const main = async () => {
|
|
10
11
|
process.env.RWSDK_WORKER_RUN = "1";
|
|
@@ -47,14 +48,47 @@ const main = async () => {
|
|
|
47
48
|
});
|
|
48
49
|
await server.listen();
|
|
49
50
|
const fileUrl = pathToFileURL(scriptPath).href;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const readyUrl = await checkServerUp(`http://localhost:${port}`, "/__debug", 30, false);
|
|
52
|
+
const readyOrigin = new URL(readyUrl).origin;
|
|
53
|
+
const readyOriginUrl = new URL(readyOrigin);
|
|
54
|
+
const candidateBaseUrls = Array.from(new Set([
|
|
55
|
+
readyOrigin,
|
|
56
|
+
`${readyOriginUrl.protocol}//127.0.0.1:${readyOriginUrl.port}`,
|
|
57
|
+
`${readyOriginUrl.protocol}//[::1]:${readyOriginUrl.port}`,
|
|
58
|
+
]));
|
|
59
|
+
let response;
|
|
60
|
+
let lastFetchError;
|
|
61
|
+
for (const candidateBaseUrl of candidateBaseUrls) {
|
|
62
|
+
const url = `${candidateBaseUrl}/__worker-run?script=${encodeURIComponent(fileUrl)}`;
|
|
63
|
+
const fetchAttempts = 5;
|
|
64
|
+
debug("Fetching %s", url);
|
|
65
|
+
for (let attempt = 0; attempt < fetchAttempts; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
response = await fetch(url, {
|
|
68
|
+
headers: {
|
|
69
|
+
"x-rwsdk-worker-run-token": token,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
debug("Response from worker: %s", response);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
lastFetchError = error;
|
|
77
|
+
debug("Fetch failed for %s on attempt %d/%d: %O", url, attempt + 1, fetchAttempts, error);
|
|
78
|
+
if (attempt < fetchAttempts - 1) {
|
|
79
|
+
await new Promise((resolve) => {
|
|
80
|
+
setTimeout(() => resolve(), 2000);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (response) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!response) {
|
|
90
|
+
throw lastFetchError ?? new Error("worker-run fetch failed");
|
|
91
|
+
}
|
|
58
92
|
if (!response.ok) {
|
|
59
93
|
const errorText = await response.text();
|
|
60
94
|
console.error(`Error: worker-run script failed with status ${response.status}.`);
|
|
@@ -6,12 +6,15 @@ vi.mock("cloudflare:workers", () => {
|
|
|
6
6
|
});
|
|
7
7
|
import { SyncedStateServer } from "../SyncedStateServer.mjs";
|
|
8
8
|
const createStub = (onInvoke) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const makeStub = () => {
|
|
10
|
+
const fn = Object.assign(async (value) => {
|
|
11
|
+
await onInvoke(value);
|
|
12
|
+
}, {
|
|
13
|
+
dup: () => makeStub(),
|
|
14
|
+
});
|
|
15
|
+
return fn;
|
|
16
|
+
};
|
|
17
|
+
return makeStub();
|
|
15
18
|
};
|
|
16
19
|
describe("SyncedStateServer", () => {
|
|
17
20
|
it("notifies subscribers when state changes", async () => {
|
|
@@ -27,11 +30,15 @@ describe("SyncedStateServer", () => {
|
|
|
27
30
|
});
|
|
28
31
|
it("removes subscriptions on unsubscribe", () => {
|
|
29
32
|
const coordinator = new SyncedStateServer({}, {});
|
|
30
|
-
const
|
|
33
|
+
const received = [];
|
|
34
|
+
const stub = createStub((value) => {
|
|
35
|
+
received.push(value);
|
|
36
|
+
});
|
|
31
37
|
coordinator.subscribe("counter", stub);
|
|
32
38
|
coordinator.unsubscribe("counter", stub);
|
|
33
39
|
coordinator.setState(1, "counter");
|
|
34
40
|
expect(coordinator.getState("counter")).toBe(1);
|
|
41
|
+
expect(received).toEqual([]);
|
|
35
42
|
});
|
|
36
43
|
it("drops failing subscribers", async () => {
|
|
37
44
|
const coordinator = new SyncedStateServer({}, {});
|
|
@@ -9,10 +9,14 @@ vi.mock("capnweb", () => ({
|
|
|
9
9
|
},
|
|
10
10
|
newWorkersRpcResponse: vi.fn(),
|
|
11
11
|
}));
|
|
12
|
-
vi.mock("
|
|
12
|
+
vi.mock("../../runtime/entries/router", () => ({
|
|
13
13
|
route: vi.fn((path, handler) => ({ path, handler })),
|
|
14
14
|
}));
|
|
15
|
-
|
|
15
|
+
vi.mock("../../runtime/requestInfo/worker", () => ({
|
|
16
|
+
runWithRequestInfo: (_requestInfo, fn) => fn(),
|
|
17
|
+
}));
|
|
18
|
+
import { newWorkersRpcResponse } from "capnweb";
|
|
19
|
+
import { syncedStateRoutes, SyncedStateServer } from "../worker.mjs";
|
|
16
20
|
describe("SyncedStateProxy", () => {
|
|
17
21
|
let mockCoordinator;
|
|
18
22
|
beforeEach(() => {
|
|
@@ -20,6 +24,7 @@ describe("SyncedStateProxy", () => {
|
|
|
20
24
|
});
|
|
21
25
|
const mockStub = {};
|
|
22
26
|
afterEach(() => {
|
|
27
|
+
vi.mocked(newWorkersRpcResponse).mockReset();
|
|
23
28
|
SyncedStateServer.registerKeyHandler(async (key, stub) => key);
|
|
24
29
|
});
|
|
25
30
|
it("transforms keys before calling coordinator methods when handler is registered", async () => {
|
|
@@ -58,6 +63,40 @@ describe("SyncedStateProxy", () => {
|
|
|
58
63
|
SyncedStateServer.registerKeyHandler(handler);
|
|
59
64
|
await expect(handler("test", mockStub)).rejects.toThrow("Handler error");
|
|
60
65
|
});
|
|
66
|
+
it("uses the subscribed duplicated RPC client when unsubscribing (#1207)", async () => {
|
|
67
|
+
const coordinator = {
|
|
68
|
+
_setStub: vi.fn(),
|
|
69
|
+
subscribe: vi.fn().mockResolvedValue(undefined),
|
|
70
|
+
unsubscribe: vi.fn().mockResolvedValue(undefined),
|
|
71
|
+
};
|
|
72
|
+
const namespace = {
|
|
73
|
+
idFromName: vi.fn(() => "synced-state-id"),
|
|
74
|
+
get: vi.fn(() => coordinator),
|
|
75
|
+
};
|
|
76
|
+
let proxy;
|
|
77
|
+
vi.mocked(newWorkersRpcResponse).mockImplementation(async (_request, api) => {
|
|
78
|
+
proxy = api;
|
|
79
|
+
return new Response(null, { status: 204 });
|
|
80
|
+
});
|
|
81
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
82
|
+
const [baseRoute] = syncedStateRoutes(() => namespace);
|
|
83
|
+
await baseRoute.handler({
|
|
84
|
+
request: new Request("https://example.com/__synced-state"),
|
|
85
|
+
params: {},
|
|
86
|
+
});
|
|
87
|
+
const duplicatedClient = Object.assign(vi.fn(), {
|
|
88
|
+
dup: vi.fn(() => duplicatedClient),
|
|
89
|
+
});
|
|
90
|
+
const originalClient = Object.assign(vi.fn(), {
|
|
91
|
+
dup: vi.fn(() => duplicatedClient),
|
|
92
|
+
});
|
|
93
|
+
await proxy.subscribe("notifications", originalClient);
|
|
94
|
+
await proxy.unsubscribe("notifications", originalClient);
|
|
95
|
+
const subscribeClient = coordinator.subscribe.mock.calls[0][1];
|
|
96
|
+
const unsubscribeClient = coordinator.unsubscribe.mock.calls[0][1];
|
|
97
|
+
expect(subscribeClient).not.toBe(originalClient);
|
|
98
|
+
expect(unsubscribeClient).toBe(subscribeClient);
|
|
99
|
+
});
|
|
61
100
|
it("handles async operations in handler", async () => {
|
|
62
101
|
const handler = async (key, stub) => {
|
|
63
102
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
@@ -19,7 +19,7 @@ export { SyncedStateServer };
|
|
|
19
19
|
const DEFAULT_SYNC_STATE_NAME = "syncedState";
|
|
20
20
|
let SyncedStateProxyClass = null;
|
|
21
21
|
async function getSyncedStateProxy() {
|
|
22
|
-
var _SyncedStateProxy_instances, _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler, _SyncedStateProxy_requestInfo, _SyncedStateProxy_transformKey, _SyncedStateProxy_callHandler, _a;
|
|
22
|
+
var _SyncedStateProxy_instances, _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler, _SyncedStateProxy_requestInfo, _SyncedStateProxy_subscriptionClients, _SyncedStateProxy_transformKey, _SyncedStateProxy_callHandler, _a;
|
|
23
23
|
const { RpcTarget, newWorkersRpcResponse } = await loadCapnweb();
|
|
24
24
|
if (!SyncedStateProxyClass) {
|
|
25
25
|
SyncedStateProxyClass = (_a = class SyncedStateProxy extends RpcTarget {
|
|
@@ -29,6 +29,9 @@ async function getSyncedStateProxy() {
|
|
|
29
29
|
_SyncedStateProxy_stub.set(this, void 0);
|
|
30
30
|
_SyncedStateProxy_keyHandler.set(this, void 0);
|
|
31
31
|
_SyncedStateProxy_requestInfo.set(this, void 0);
|
|
32
|
+
// Map original RPC callbacks to the duplicated callbacks sent to the DO
|
|
33
|
+
// so unsubscribe uses the same identity that subscribe registered.
|
|
34
|
+
_SyncedStateProxy_subscriptionClients.set(this, new Map());
|
|
32
35
|
__classPrivateFieldSet(this, _SyncedStateProxy_stub, stub, "f");
|
|
33
36
|
__classPrivateFieldSet(this, _SyncedStateProxy_keyHandler, keyHandler, "f");
|
|
34
37
|
__classPrivateFieldSet(this, _SyncedStateProxy_requestInfo, requestInfo, "f");
|
|
@@ -54,7 +57,24 @@ async function getSyncedStateProxy() {
|
|
|
54
57
|
// dup the client if it is a function; otherwise, pass it as is;
|
|
55
58
|
// this is because the client is a WebSocketRpcSession, and we need to pass a new instance of the client to the DO;
|
|
56
59
|
const clientToPass = typeof client.dup === "function" ? client.dup() : client;
|
|
57
|
-
|
|
60
|
+
let clientsForKey = __classPrivateFieldGet(this, _SyncedStateProxy_subscriptionClients, "f").get(transformedKey);
|
|
61
|
+
if (!clientsForKey) {
|
|
62
|
+
clientsForKey = new Map();
|
|
63
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_subscriptionClients, "f").set(transformedKey, clientsForKey);
|
|
64
|
+
}
|
|
65
|
+
clientsForKey.set(client, clientToPass);
|
|
66
|
+
try {
|
|
67
|
+
return await __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").subscribe(transformedKey, clientToPass);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (clientsForKey.get(client) === clientToPass) {
|
|
71
|
+
clientsForKey.delete(client);
|
|
72
|
+
if (clientsForKey.size === 0) {
|
|
73
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_subscriptionClients, "f").delete(transformedKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
async unsubscribe(key, client) {
|
|
60
80
|
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
@@ -65,18 +85,29 @@ async function getSyncedStateProxy() {
|
|
|
65
85
|
if (unsubscribeHandler) {
|
|
66
86
|
__classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_callHandler).call(this, unsubscribeHandler, transformedKey, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
67
87
|
}
|
|
88
|
+
const clientsForKey = __classPrivateFieldGet(this, _SyncedStateProxy_subscriptionClients, "f").get(transformedKey);
|
|
89
|
+
const clientToPass = clientsForKey?.get(client) ?? client;
|
|
68
90
|
try {
|
|
69
|
-
await __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").unsubscribe(transformedKey,
|
|
91
|
+
await __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").unsubscribe(transformedKey, clientToPass);
|
|
70
92
|
}
|
|
71
93
|
catch (error) {
|
|
72
94
|
// Ignore errors during unsubscribe - handler has already been called
|
|
73
95
|
// This prevents RPC stub disposal errors from propagating
|
|
74
96
|
}
|
|
97
|
+
finally {
|
|
98
|
+
if (clientsForKey && clientsForKey.get(client) === clientToPass) {
|
|
99
|
+
clientsForKey.delete(client);
|
|
100
|
+
if (clientsForKey.size === 0) {
|
|
101
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_subscriptionClients, "f").delete(transformedKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
75
105
|
}
|
|
76
106
|
},
|
|
77
107
|
_SyncedStateProxy_stub = new WeakMap(),
|
|
78
108
|
_SyncedStateProxy_keyHandler = new WeakMap(),
|
|
79
109
|
_SyncedStateProxy_requestInfo = new WeakMap(),
|
|
110
|
+
_SyncedStateProxy_subscriptionClients = new WeakMap(),
|
|
80
111
|
_SyncedStateProxy_instances = new WeakSet(),
|
|
81
112
|
_SyncedStateProxy_transformKey =
|
|
82
113
|
/**
|
|
@@ -7,7 +7,7 @@ import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
|
|
|
7
7
|
import { hasDirective as sourceHasDirective } from "./hasDirective.mjs";
|
|
8
8
|
import { invalidateModule } from "./invalidateModule.mjs";
|
|
9
9
|
import { isJsFile } from "./isJsFile.mjs";
|
|
10
|
-
import { VIRTUAL_SSR_PREFIX } from "./
|
|
10
|
+
import { VIRTUAL_SSR_PREFIX } from "./ssrVirtualModule.mjs";
|
|
11
11
|
const log = debug("rwsdk:vite:hmr-plugin");
|
|
12
12
|
let hasErrored = false;
|
|
13
13
|
const hasDirective = async (filepath, directive) => {
|
|
@@ -3,8 +3,8 @@ import MagicString from "magic-string";
|
|
|
3
3
|
import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
|
|
4
4
|
import { externalModulesSet } from "./constants.mjs";
|
|
5
5
|
import { findSsrImportCallSites } from "./findSsrSpecifiers.mjs";
|
|
6
|
+
import { isVirtualSsrModuleId, normalizeVirtualSsrModuleId, VIRTUAL_SSR_PREFIX, } from "./ssrVirtualModule.mjs";
|
|
6
7
|
const log = debug("rwsdk:vite:ssr-bridge-plugin");
|
|
7
|
-
export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
|
|
8
8
|
export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
9
9
|
let devServer;
|
|
10
10
|
let isDev = false;
|
|
@@ -63,10 +63,11 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
63
63
|
process.env.VERBOSE &&
|
|
64
64
|
log("Esbuild onResolve called for path=%s, args=%O", args.path, args);
|
|
65
65
|
if (args.path === "rwsdk/__ssr_bridge" ||
|
|
66
|
-
args.path
|
|
67
|
-
|
|
66
|
+
isVirtualSsrModuleId(args.path)) {
|
|
67
|
+
const path = normalizeVirtualSsrModuleId(args.path) ?? args.path;
|
|
68
|
+
log("Marking as external: %s", path);
|
|
68
69
|
return {
|
|
69
|
-
path
|
|
70
|
+
path,
|
|
70
71
|
external: true,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
@@ -98,14 +99,15 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
98
99
|
// context(justinvdm, 27 May 2025): In dev, we need to dynamically load
|
|
99
100
|
// SSR modules, so we return the virtual id so that the dynamic loading
|
|
100
101
|
// can happen in load()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const virtualSsrId = normalizeVirtualSsrModuleId(id);
|
|
103
|
+
if (virtualSsrId) {
|
|
104
|
+
if (virtualSsrId.endsWith(".css")) {
|
|
105
|
+
const newId = virtualSsrId + ".js";
|
|
104
106
|
log("Virtual CSS module, adding .js suffix. old: %s, new: %s", id, newId);
|
|
105
107
|
return newId;
|
|
106
108
|
}
|
|
107
|
-
log("Returning virtual SSR id for dev: %s",
|
|
108
|
-
return
|
|
109
|
+
log("Returning virtual SSR id for dev: %s", virtualSsrId);
|
|
110
|
+
return virtualSsrId;
|
|
109
111
|
}
|
|
110
112
|
// context(justinvdm, 28 May 2025): The SSR bridge module is a special case -
|
|
111
113
|
// it is the entry point for all SSR modules, so to trigger the
|
|
@@ -119,10 +121,11 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
119
121
|
}
|
|
120
122
|
else {
|
|
121
123
|
// In build mode, the behavior depends on the build pass
|
|
122
|
-
|
|
124
|
+
const virtualSsrId = normalizeVirtualSsrModuleId(id);
|
|
125
|
+
if (virtualSsrId) {
|
|
123
126
|
if (this.environment.name === "worker") {
|
|
124
127
|
log("Virtual SSR module case (build-worker pass): resolving to external");
|
|
125
|
-
return { id, external: true };
|
|
128
|
+
return { id: virtualSsrId, external: true };
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
if (id === "rwsdk/__ssr_bridge" && this.environment.name === "worker") {
|
|
@@ -141,9 +144,9 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
141
144
|
}
|
|
142
145
|
},
|
|
143
146
|
async load(id) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const realId =
|
|
147
|
+
const virtualSsrId = normalizeVirtualSsrModuleId(id);
|
|
148
|
+
if (virtualSsrId && this.environment.name === "worker") {
|
|
149
|
+
const realId = virtualSsrId.slice(VIRTUAL_SSR_PREFIX.length);
|
|
147
150
|
let idForFetch = realId.endsWith(".css.js")
|
|
148
151
|
? realId.slice(0, -3)
|
|
149
152
|
: realId;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const VITE_ID_PREFIX = "/@id/";
|
|
2
|
+
export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
|
|
3
|
+
export function normalizeVirtualSsrModuleId(id) {
|
|
4
|
+
const normalizedId = id.startsWith(VITE_ID_PREFIX)
|
|
5
|
+
? id.slice(VITE_ID_PREFIX.length)
|
|
6
|
+
: id;
|
|
7
|
+
return normalizedId.startsWith(VIRTUAL_SSR_PREFIX) ? normalizedId : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function isVirtualSsrModuleId(id) {
|
|
10
|
+
return normalizeVirtualSsrModuleId(id) !== undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isVirtualSsrModuleId, normalizeVirtualSsrModuleId, VIRTUAL_SSR_PREFIX, } from "./ssrVirtualModule.mjs";
|
|
3
|
+
import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
|
|
4
|
+
describe("isVirtualSsrModuleId", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[`${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`, true],
|
|
7
|
+
[`${VIRTUAL_SSR_PREFIX}rwsdk/__ssr_bridge`, true],
|
|
8
|
+
[`/@id/${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`, true],
|
|
9
|
+
[`/src/${VIRTUAL_SSR_PREFIX}Preview.tsx`, false],
|
|
10
|
+
["/src/Preview.tsx", false],
|
|
11
|
+
])("identifies %s as virtual SSR module id: %s", (id, expected) => {
|
|
12
|
+
expect(isVirtualSsrModuleId(id)).toBe(expected);
|
|
13
|
+
});
|
|
14
|
+
it("normalizes Vite /@id/ virtual SSR module URLs to bare module ids", () => {
|
|
15
|
+
expect(normalizeVirtualSsrModuleId(`/@id/${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`)).toBe(`${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe("transformJsxScriptTagsPlugin transform hook", () => {
|
|
19
|
+
function createPlugin() {
|
|
20
|
+
const clientEntryPoints = new Set();
|
|
21
|
+
const plugin = transformJsxScriptTagsPlugin({
|
|
22
|
+
clientEntryPoints,
|
|
23
|
+
projectRootDir: "/project/root/dir",
|
|
24
|
+
});
|
|
25
|
+
const configResolved = plugin.configResolved;
|
|
26
|
+
if (typeof configResolved === "function") {
|
|
27
|
+
configResolved.call({}, { command: "serve", base: "/" });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
configResolved?.handler.call({}, { command: "serve", base: "/" });
|
|
31
|
+
}
|
|
32
|
+
const transform = typeof plugin.transform === "function"
|
|
33
|
+
? plugin.transform
|
|
34
|
+
: plugin.transform?.handler;
|
|
35
|
+
if (!transform) {
|
|
36
|
+
throw new Error("Expected transform hook to be defined");
|
|
37
|
+
}
|
|
38
|
+
return { clientEntryPoints, transform: transform };
|
|
39
|
+
}
|
|
40
|
+
it.each([
|
|
41
|
+
`${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`,
|
|
42
|
+
`/@id/${VIRTUAL_SSR_PREFIX}/src/Preview.tsx`,
|
|
43
|
+
])("skips virtual SSR bridge module ids in worker: %s", async (id) => {
|
|
44
|
+
// Regression for #1210: virtual SSR bridge modules have .tsx ids but are
|
|
45
|
+
// already transformed by the ssr environment, so the worker transform must
|
|
46
|
+
// not run script-tag discovery on them again.
|
|
47
|
+
const { clientEntryPoints, transform } = createPlugin();
|
|
48
|
+
const ssrBridgeCode = `
|
|
49
|
+
import { jsx } from "react/jsx-runtime";
|
|
50
|
+
const __vite_ssr_import_0__ = await __vite_ssr_import__("react/jsx-runtime", { importedNames: ["jsx"] });
|
|
51
|
+
export function Preview() {
|
|
52
|
+
return jsx("script", { async: true, src: "https://example.com/a.js" });
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const result = await transform.call({ environment: { name: "worker" } }, ssrBridgeCode, id);
|
|
56
|
+
expect(result).toBeNull();
|
|
57
|
+
expect(clientEntryPoints.size).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
it("still transforms non-bridge worker .tsx modules", async () => {
|
|
60
|
+
const { transform } = createPlugin();
|
|
61
|
+
const code = `
|
|
62
|
+
import { jsx } from "react/jsx-runtime";
|
|
63
|
+
export function Preview() {
|
|
64
|
+
return jsx("script", { async: true, src: "https://example.com/a.js" });
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
const result = await transform.call({ environment: { name: "worker" } }, code, "/src/Preview.tsx");
|
|
68
|
+
expect(result).toBeTruthy();
|
|
69
|
+
expect(typeof result === "object" && result !== null && "code" in result
|
|
70
|
+
? result.code
|
|
71
|
+
: "").toContain("nonce: requestInfo.rw.nonce");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -2,6 +2,7 @@ import debug from "debug";
|
|
|
2
2
|
import { Node, Project, SyntaxKind, } from "ts-morph";
|
|
3
3
|
import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
|
|
4
4
|
import { stripBase } from "../lib/stripBase.mjs";
|
|
5
|
+
import { isVirtualSsrModuleId } from "./ssrVirtualModule.mjs";
|
|
5
6
|
const log = debug("rwsdk:vite:transform-jsx-script-tags");
|
|
6
7
|
function transformAssetPath(importPath, projectRootDir, base) {
|
|
7
8
|
if (process.env.VITE_IS_DEV_SERVER === "1") {
|
|
@@ -336,6 +337,10 @@ export const transformJsxScriptTagsPlugin = ({ clientEntryPoints, projectRootDir
|
|
|
336
337
|
return null;
|
|
337
338
|
}
|
|
338
339
|
if (this.environment?.name === "worker" &&
|
|
340
|
+
// context(peterp, 29 May 2026): SSR bridge modules are already
|
|
341
|
+
// transformed by the `ssr` environment; rerunning script-tag discovery
|
|
342
|
+
// in the worker injects duplicate imports into virtual SSR modules.
|
|
343
|
+
!isVirtualSsrModuleId(id) &&
|
|
339
344
|
id.endsWith(".tsx") &&
|
|
340
345
|
hasJsxFunctions(code)) {
|
|
341
346
|
log("Transforming JSX script tags in %s", id);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from "typescript";
|
|
2
2
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import stubEnvVars from "../lib/testUtils/stubEnvVars.mjs";
|
|
4
4
|
import { transformJsxScriptTagsCode } from "./transformJsxScriptTagsPlugin.mjs";
|
|
5
|
-
// Helper function to normalize code formatting for test comparisons
|
|
5
|
+
// Helper function to normalize code formatting for test comparisons.
|
|
6
6
|
function normalizeCode(code) {
|
|
7
|
-
|
|
7
|
+
const source = ts.createSourceFile("test.tsx", code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
8
|
+
return ts.createPrinter({ removeComments: false }).printFile(source).trim();
|
|
8
9
|
}
|
|
9
10
|
stubEnvVars();
|
|
10
11
|
beforeEach(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rwsdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
"magic-string": "~0.30.21",
|
|
183
183
|
"picocolors": "~1.1.1",
|
|
184
184
|
"proper-lockfile": "~4.1.2",
|
|
185
|
+
"playwright-core": "1.60.0",
|
|
185
186
|
"puppeteer-core": "~24.42.0",
|
|
186
187
|
"react-is": "~19.2.6",
|
|
187
188
|
"rsc-html-stream": "~0.0.7",
|
|
@@ -212,11 +213,9 @@
|
|
|
212
213
|
"wrangler": "^4.85.0",
|
|
213
214
|
"capnweb": "~0.5.0",
|
|
214
215
|
"@types/debug": "~4.1.13",
|
|
215
|
-
"@types/js-beautify": "~1.14.3",
|
|
216
216
|
"@types/lodash": "~4.17.24",
|
|
217
217
|
"@types/node": "~25.6.0",
|
|
218
218
|
"@types/proper-lockfile": "~4.1.4",
|
|
219
|
-
"js-beautify": "~1.15.4",
|
|
220
219
|
"semver": "~7.7.4",
|
|
221
220
|
"tsx": "~4.21.0",
|
|
222
221
|
"typescript": "~6.0.3",
|