rwsdk 1.0.0-beta.4 → 1.0.0-beta.40
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/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +7 -4
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +4 -0
- package/dist/lib/e2e/constants.mjs +49 -12
- package/dist/lib/e2e/dev.mjs +37 -49
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +201 -64
- package/dist/lib/e2e/index.d.mts +1 -0
- package/dist/lib/e2e/index.mjs +1 -0
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +16 -32
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +34 -3
- package/dist/lib/e2e/testHarness.mjs +219 -90
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/runtime/client/client.d.ts +35 -0
- package/dist/runtime/client/client.js +35 -0
- package/dist/runtime/client/navigation.d.ts +49 -0
- package/dist/runtime/client/navigation.js +80 -31
- package/dist/runtime/entries/clientSSR.d.ts +1 -0
- package/dist/runtime/entries/clientSSR.js +3 -0
- package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
- package/dist/runtime/entries/router.d.ts +1 -0
- package/dist/runtime/entries/routerClient.d.ts +1 -0
- package/dist/runtime/entries/routerClient.js +1 -0
- package/dist/runtime/entries/worker.d.ts +2 -0
- package/dist/runtime/entries/worker.js +2 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
- package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/createDb.d.ts +1 -2
- package/dist/runtime/lib/db/createDb.js +4 -0
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +21 -7
- package/dist/runtime/lib/links.js +82 -24
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/manifest.d.ts +1 -1
- package/dist/runtime/lib/manifest.js +7 -4
- package/dist/runtime/lib/realtime/client.js +8 -2
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +153 -36
- package/dist/runtime/lib/router.js +169 -20
- package/dist/runtime/lib/router.test.js +241 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
- package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
- package/dist/runtime/lib/types.js +1 -0
- package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.d.ts +4 -2
- package/dist/runtime/render/renderToStream.js +53 -24
- package/dist/runtime/render/renderToString.d.ts +3 -6
- package/dist/runtime/requestInfo/types.d.ts +4 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +44 -0
- package/dist/runtime/requestInfo/worker.d.ts +0 -1
- package/dist/runtime/requestInfo/worker.js +3 -10
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +32 -0
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/scripts/worker-run.d.mts +1 -1
- package/dist/scripts/worker-run.mjs +59 -113
- package/dist/use-synced-state/SyncedStateServer.d.mts +21 -0
- package/dist/use-synced-state/SyncedStateServer.mjs +128 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
- package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
- package/dist/use-synced-state/client-core.d.ts +26 -0
- package/dist/use-synced-state/client-core.js +39 -0
- package/dist/use-synced-state/client.d.ts +3 -0
- package/dist/use-synced-state/client.js +4 -0
- package/dist/use-synced-state/constants.d.mts +1 -0
- package/dist/use-synced-state/constants.mjs +1 -0
- package/dist/use-synced-state/useSyncedState.d.ts +20 -0
- package/dist/use-synced-state/useSyncedState.js +58 -0
- package/dist/use-synced-state/worker.d.mts +13 -0
- package/dist/use-synced-state/worker.mjs +69 -0
- package/dist/vite/buildApp.mjs +34 -2
- package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
- package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
- package/dist/vite/configPlugin.mjs +9 -14
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
- package/dist/vite/devServerTimingPlugin.mjs +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
- package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
- package/dist/vite/directivesPlugin.mjs +4 -4
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/getViteEsbuild.mjs +2 -1
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +73 -0
- package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
- package/dist/vite/linkerPlugin.d.mts +2 -1
- package/dist/vite/linkerPlugin.mjs +11 -3
- package/dist/vite/linkerPlugin.test.mjs +15 -0
- package/dist/vite/miniflareHMRPlugin.mjs +6 -38
- package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
- package/dist/vite/redwoodPlugin.mjs +8 -10
- package/dist/vite/runDirectivesScan.mjs +72 -18
- package/dist/vite/ssrBridgePlugin.mjs +132 -40
- package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
- package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +74 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
- package/dist/vite/virtualPlugin.mjs +6 -7
- package/package.json +27 -10
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setSyncedStateClientForTesting, } from "../client";
|
|
3
|
+
import { createSyncedStateHook, } from "../useSyncedState";
|
|
4
|
+
const createStateHarness = () => {
|
|
5
|
+
let currentState;
|
|
6
|
+
const cleanups = [];
|
|
7
|
+
const useStateImpl = ((initialValue) => {
|
|
8
|
+
const resolved = typeof initialValue === "function"
|
|
9
|
+
? initialValue()
|
|
10
|
+
: initialValue;
|
|
11
|
+
currentState = resolved;
|
|
12
|
+
const setState = (next) => {
|
|
13
|
+
currentState =
|
|
14
|
+
typeof next === "function"
|
|
15
|
+
? next(currentState)
|
|
16
|
+
: next;
|
|
17
|
+
};
|
|
18
|
+
return [currentState, setState];
|
|
19
|
+
});
|
|
20
|
+
const useEffectImpl = (callback) => {
|
|
21
|
+
const cleanup = callback();
|
|
22
|
+
if (typeof cleanup === "function") {
|
|
23
|
+
cleanups.push(cleanup);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const useRefImpl = ((value) => ({
|
|
27
|
+
current: value,
|
|
28
|
+
}));
|
|
29
|
+
const useCallbackImpl = (fn) => fn;
|
|
30
|
+
const deps = {
|
|
31
|
+
useState: useStateImpl,
|
|
32
|
+
useEffect: useEffectImpl,
|
|
33
|
+
useRef: useRefImpl,
|
|
34
|
+
useCallback: useCallbackImpl,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
deps,
|
|
38
|
+
getState: () => currentState,
|
|
39
|
+
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
describe("createSyncedStateHook", () => {
|
|
43
|
+
const subscribeHandlers = new Map();
|
|
44
|
+
const client = {
|
|
45
|
+
async getState() {
|
|
46
|
+
return 5;
|
|
47
|
+
},
|
|
48
|
+
async setState(_value, _key) { },
|
|
49
|
+
async subscribe(key, handler) {
|
|
50
|
+
subscribeHandlers.set(key, handler);
|
|
51
|
+
},
|
|
52
|
+
async unsubscribe(key) {
|
|
53
|
+
subscribeHandlers.delete(key);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const resetClient = () => {
|
|
57
|
+
client.getState = async () => 5;
|
|
58
|
+
client.setState = async (_value, _key) => { };
|
|
59
|
+
client.subscribe = async (key, handler) => {
|
|
60
|
+
subscribeHandlers.set(key, handler);
|
|
61
|
+
};
|
|
62
|
+
client.unsubscribe = async (key) => {
|
|
63
|
+
subscribeHandlers.delete(key);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetClient();
|
|
68
|
+
setSyncedStateClientForTesting(client);
|
|
69
|
+
subscribeHandlers.clear();
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
setSyncedStateClientForTesting(null);
|
|
73
|
+
});
|
|
74
|
+
it("loads remote state and updates local value", async () => {
|
|
75
|
+
const harness = createStateHarness();
|
|
76
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
77
|
+
const [value] = useSyncedState(0, "counter");
|
|
78
|
+
expect(value).toBe(0);
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
expect(harness.getState()).toBe(5);
|
|
81
|
+
});
|
|
82
|
+
it("sends updates through the client and applies optimistic value", async () => {
|
|
83
|
+
const harness = createStateHarness();
|
|
84
|
+
const setCalls = [];
|
|
85
|
+
client.setState = async (value, key) => {
|
|
86
|
+
setCalls.push({ key, value });
|
|
87
|
+
};
|
|
88
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
89
|
+
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
|
+
setSyncValue(9);
|
|
91
|
+
expect(harness.getState()).toBe(9);
|
|
92
|
+
expect(setCalls).toEqual([{ key: "counter", value: 9 }]);
|
|
93
|
+
});
|
|
94
|
+
it("applies remote updates from the subscription handler", async () => {
|
|
95
|
+
const harness = createStateHarness();
|
|
96
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
97
|
+
useSyncedState(0, "counter");
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
const handler = subscribeHandlers.get("counter");
|
|
100
|
+
handler?.(7);
|
|
101
|
+
expect(harness.getState()).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
it("unsubscribes during cleanup", () => {
|
|
104
|
+
const harness = createStateHarness();
|
|
105
|
+
const unsubscribed = [];
|
|
106
|
+
client.unsubscribe = async (key) => {
|
|
107
|
+
unsubscribed.push({ key });
|
|
108
|
+
subscribeHandlers.delete(key);
|
|
109
|
+
};
|
|
110
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
111
|
+
useSyncedState(0, "counter");
|
|
112
|
+
harness.runCleanups();
|
|
113
|
+
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setSyncedStateClientForTesting, } from "../client";
|
|
3
|
+
import { createSyncedStateHook, } from "../useSyncedState";
|
|
4
|
+
const createStateHarness = () => {
|
|
5
|
+
let currentState;
|
|
6
|
+
const cleanups = [];
|
|
7
|
+
const useStateImpl = ((initialValue) => {
|
|
8
|
+
const resolved = typeof initialValue === "function"
|
|
9
|
+
? initialValue()
|
|
10
|
+
: initialValue;
|
|
11
|
+
currentState = resolved;
|
|
12
|
+
const setState = (next) => {
|
|
13
|
+
currentState =
|
|
14
|
+
typeof next === "function"
|
|
15
|
+
? next(currentState)
|
|
16
|
+
: next;
|
|
17
|
+
};
|
|
18
|
+
return [currentState, setState];
|
|
19
|
+
});
|
|
20
|
+
const useEffectImpl = (callback) => {
|
|
21
|
+
const cleanup = callback();
|
|
22
|
+
if (typeof cleanup === "function") {
|
|
23
|
+
cleanups.push(cleanup);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const useRefImpl = ((value) => ({
|
|
27
|
+
current: value,
|
|
28
|
+
}));
|
|
29
|
+
const useCallbackImpl = (fn) => fn;
|
|
30
|
+
const deps = {
|
|
31
|
+
useState: useStateImpl,
|
|
32
|
+
useEffect: useEffectImpl,
|
|
33
|
+
useRef: useRefImpl,
|
|
34
|
+
useCallback: useCallbackImpl,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
deps,
|
|
38
|
+
getState: () => currentState,
|
|
39
|
+
runCleanups: () => cleanups.forEach((fn) => fn()),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
describe("createSyncedStateHook", () => {
|
|
43
|
+
const subscribeHandlers = new Map();
|
|
44
|
+
const client = {
|
|
45
|
+
async getState() {
|
|
46
|
+
return 5;
|
|
47
|
+
},
|
|
48
|
+
async setState(_value, _key) { },
|
|
49
|
+
async subscribe(key, handler) {
|
|
50
|
+
subscribeHandlers.set(key, handler);
|
|
51
|
+
},
|
|
52
|
+
async unsubscribe(key) {
|
|
53
|
+
subscribeHandlers.delete(key);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const resetClient = () => {
|
|
57
|
+
client.getState = async () => 5;
|
|
58
|
+
client.setState = async (_value, _key) => { };
|
|
59
|
+
client.subscribe = async (key, handler) => {
|
|
60
|
+
subscribeHandlers.set(key, handler);
|
|
61
|
+
};
|
|
62
|
+
client.unsubscribe = async (key) => {
|
|
63
|
+
subscribeHandlers.delete(key);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetClient();
|
|
68
|
+
setSyncedStateClientForTesting(client);
|
|
69
|
+
subscribeHandlers.clear();
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
setSyncedStateClientForTesting(null);
|
|
73
|
+
});
|
|
74
|
+
it("loads remote state and updates local value", async () => {
|
|
75
|
+
const harness = createStateHarness();
|
|
76
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
77
|
+
const [value] = useSyncedState(0, "counter");
|
|
78
|
+
expect(value).toBe(0);
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
expect(harness.getState()).toBe(5);
|
|
81
|
+
});
|
|
82
|
+
it("sends updates through the client and applies optimistic value", async () => {
|
|
83
|
+
const harness = createStateHarness();
|
|
84
|
+
const setCalls = [];
|
|
85
|
+
client.setState = async (value, key) => {
|
|
86
|
+
setCalls.push({ key, value });
|
|
87
|
+
};
|
|
88
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
89
|
+
const [, setSyncValue] = useSyncedState(0, "counter");
|
|
90
|
+
setSyncValue(9);
|
|
91
|
+
expect(harness.getState()).toBe(9);
|
|
92
|
+
expect(setCalls).toEqual([{ key: "counter", value: 9 }]);
|
|
93
|
+
});
|
|
94
|
+
it("applies remote updates from the subscription handler", async () => {
|
|
95
|
+
const harness = createStateHarness();
|
|
96
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
97
|
+
useSyncedState(0, "counter");
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
const handler = subscribeHandlers.get("counter");
|
|
100
|
+
handler?.(7);
|
|
101
|
+
expect(harness.getState()).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
it("unsubscribes during cleanup", () => {
|
|
104
|
+
const harness = createStateHarness();
|
|
105
|
+
const unsubscribed = [];
|
|
106
|
+
client.unsubscribe = async (key) => {
|
|
107
|
+
unsubscribed.push({ key });
|
|
108
|
+
subscribeHandlers.delete(key);
|
|
109
|
+
};
|
|
110
|
+
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
|
|
111
|
+
useSyncedState(0, "counter");
|
|
112
|
+
harness.runCleanups();
|
|
113
|
+
expect(unsubscribed).toEqual([{ key: "counter" }]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
vi.mock("cloudflare:workers", () => {
|
|
3
|
+
class DurableObject {
|
|
4
|
+
}
|
|
5
|
+
return { DurableObject, env: {} };
|
|
6
|
+
});
|
|
7
|
+
vi.mock("capnweb", () => ({
|
|
8
|
+
RpcTarget: class RpcTarget {
|
|
9
|
+
},
|
|
10
|
+
newWorkersRpcResponse: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("../runtime/entries/router", () => ({
|
|
13
|
+
route: vi.fn((path, handler) => ({ path, handler })),
|
|
14
|
+
}));
|
|
15
|
+
import { SyncedStateServer } from "../SyncedStateServer.mjs";
|
|
16
|
+
describe("SyncedStateProxy", () => {
|
|
17
|
+
let mockCoordinator;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockCoordinator = new SyncedStateServer({}, {});
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
23
|
+
});
|
|
24
|
+
it("transforms keys before calling coordinator methods when handler is registered", async () => {
|
|
25
|
+
const handler = async (key) => `transformed:${key}`;
|
|
26
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
27
|
+
const transformedKey = await handler("counter");
|
|
28
|
+
expect(transformedKey).toBe("transformed:counter");
|
|
29
|
+
mockCoordinator.setState(5, transformedKey);
|
|
30
|
+
const value = mockCoordinator.getState(transformedKey);
|
|
31
|
+
expect(value).toBe(5);
|
|
32
|
+
});
|
|
33
|
+
it("does not transform keys when no handler is registered", () => {
|
|
34
|
+
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
35
|
+
const handler = SyncedStateServer.getKeyHandler();
|
|
36
|
+
expect(handler).not.toBeNull();
|
|
37
|
+
});
|
|
38
|
+
it("passes through original key when handler returns it unchanged", async () => {
|
|
39
|
+
const handler = async (key) => key;
|
|
40
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
41
|
+
const result = await handler("counter");
|
|
42
|
+
expect(result).toBe("counter");
|
|
43
|
+
});
|
|
44
|
+
it("handler can scope keys per user", async () => {
|
|
45
|
+
const handler = async (key) => {
|
|
46
|
+
const userId = "user123";
|
|
47
|
+
return `user:${userId}:${key}`;
|
|
48
|
+
};
|
|
49
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
50
|
+
const result = await handler("settings");
|
|
51
|
+
expect(result).toBe("user:user123:settings");
|
|
52
|
+
});
|
|
53
|
+
it("allows errors from handler to propagate", async () => {
|
|
54
|
+
const handler = async (_key) => {
|
|
55
|
+
throw new Error("Handler error");
|
|
56
|
+
};
|
|
57
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
58
|
+
await expect(handler("test")).rejects.toThrow("Handler error");
|
|
59
|
+
});
|
|
60
|
+
it("handles async operations in handler", async () => {
|
|
61
|
+
const handler = async (key) => {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
63
|
+
return `async:${key}`;
|
|
64
|
+
};
|
|
65
|
+
SyncedStateServer.registerKeyHandler(handler);
|
|
66
|
+
const result = await handler("data");
|
|
67
|
+
expect(result).toBe("async:data");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SyncedStateClient = {
|
|
2
|
+
getState(key: string): Promise<unknown>;
|
|
3
|
+
setState(value: unknown, key: string): Promise<void>;
|
|
4
|
+
subscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
5
|
+
unsubscribe(key: string, handler: (value: unknown) => void): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
9
|
+
* @param endpoint Endpoint to connect to.
|
|
10
|
+
* @returns RPC client instance.
|
|
11
|
+
*/
|
|
12
|
+
export declare const getSyncedStateClient: (endpoint?: string) => SyncedStateClient;
|
|
13
|
+
/**
|
|
14
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
15
|
+
* @param options Optional endpoint override.
|
|
16
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const initSyncedStateClient: (options?: {
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
}) => SyncedStateClient | null;
|
|
21
|
+
/**
|
|
22
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
23
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
24
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
25
|
+
*/
|
|
26
|
+
export declare const setSyncedStateClientForTesting: (client: SyncedStateClient | null, endpoint?: string) => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
3
|
+
let cachedClient = null;
|
|
4
|
+
let cachedEndpoint = DEFAULT_SYNCED_STATE_PATH;
|
|
5
|
+
/**
|
|
6
|
+
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
7
|
+
* @param endpoint Endpoint to connect to.
|
|
8
|
+
* @returns RPC client instance.
|
|
9
|
+
*/
|
|
10
|
+
export const getSyncedStateClient = (endpoint = cachedEndpoint) => {
|
|
11
|
+
if (cachedClient && endpoint === cachedEndpoint) {
|
|
12
|
+
return cachedClient;
|
|
13
|
+
}
|
|
14
|
+
cachedEndpoint = endpoint;
|
|
15
|
+
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
16
|
+
return cachedClient;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
20
|
+
* @param options Optional endpoint override.
|
|
21
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
22
|
+
*/
|
|
23
|
+
export const initSyncedStateClient = (options = {}) => {
|
|
24
|
+
cachedEndpoint = options.endpoint ?? DEFAULT_SYNCED_STATE_PATH;
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
cachedClient = newWebSocketRpcSession(cachedEndpoint);
|
|
29
|
+
return cachedClient;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
33
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
34
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
35
|
+
*/
|
|
36
|
+
export const setSyncedStateClientForTesting = (client, endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
37
|
+
cachedClient = client;
|
|
38
|
+
cachedEndpoint = endpoint;
|
|
39
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Re-export everything from client-core to maintain the public API
|
|
2
|
+
export { getSyncedStateClient, initSyncedStateClient, setSyncedStateClientForTesting, } from "./client-core.js";
|
|
3
|
+
// Re-export useSyncedState (no circular dependency since useSyncedState imports from client-core, not client)
|
|
4
|
+
export { useSyncedState } from "./useSyncedState.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_SYNCED_STATE_PATH = "/__synced-state";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_SYNCED_STATE_PATH = "/__synced-state";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type HookDeps = {
|
|
3
|
+
useState: typeof React.useState;
|
|
4
|
+
useEffect: typeof React.useEffect;
|
|
5
|
+
useRef: typeof React.useRef;
|
|
6
|
+
useCallback: typeof React.useCallback;
|
|
7
|
+
};
|
|
8
|
+
type Setter<T> = (value: T | ((previous: T) => T)) => void;
|
|
9
|
+
export type CreateSyncedStateHookOptions = {
|
|
10
|
+
url?: string;
|
|
11
|
+
hooks?: HookDeps;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
15
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
16
|
+
* @returns Hook that syncs state through the sync state service.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createSyncedStateHook: (options?: CreateSyncedStateHookOptions) => <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
19
|
+
export declare const useSyncedState: <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { getSyncedStateClient } from "./client-core.js";
|
|
3
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
4
|
+
const defaultDeps = {
|
|
5
|
+
useState: React.useState,
|
|
6
|
+
useEffect: React.useEffect,
|
|
7
|
+
useRef: React.useRef,
|
|
8
|
+
useCallback: React.useCallback,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
12
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
13
|
+
* @returns Hook that syncs state through the sync state service.
|
|
14
|
+
*/
|
|
15
|
+
export const createSyncedStateHook = (options = {}) => {
|
|
16
|
+
const resolvedUrl = options.url ?? DEFAULT_SYNCED_STATE_PATH;
|
|
17
|
+
const deps = options.hooks ?? defaultDeps;
|
|
18
|
+
const { useState, useEffect, useRef, useCallback } = deps;
|
|
19
|
+
return function useSyncedState(initialValue, key) {
|
|
20
|
+
if (typeof window === "undefined" && !options.hooks) {
|
|
21
|
+
return [initialValue, () => { }];
|
|
22
|
+
}
|
|
23
|
+
const client = getSyncedStateClient(resolvedUrl);
|
|
24
|
+
const [value, setValue] = useState(initialValue);
|
|
25
|
+
const valueRef = useRef(value);
|
|
26
|
+
valueRef.current = value;
|
|
27
|
+
const setSyncValue = useCallback((nextValue) => {
|
|
28
|
+
const resolved = typeof nextValue === "function"
|
|
29
|
+
? nextValue(valueRef.current)
|
|
30
|
+
: nextValue;
|
|
31
|
+
setValue(resolved);
|
|
32
|
+
valueRef.current = resolved;
|
|
33
|
+
void client.setState(resolved, key);
|
|
34
|
+
}, [client, key, setValue, valueRef]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let isActive = true;
|
|
37
|
+
const handleUpdate = (next) => {
|
|
38
|
+
if (isActive) {
|
|
39
|
+
setValue(next);
|
|
40
|
+
valueRef.current = next;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
void client.getState(key).then((existing) => {
|
|
44
|
+
if (existing !== undefined && isActive) {
|
|
45
|
+
setValue(existing);
|
|
46
|
+
valueRef.current = existing;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
void client.subscribe(key, handleUpdate);
|
|
50
|
+
return () => {
|
|
51
|
+
isActive = false;
|
|
52
|
+
void client.unsubscribe(key, handleUpdate);
|
|
53
|
+
};
|
|
54
|
+
}, [client, key, setValue, valueRef]);
|
|
55
|
+
return [value, setSyncValue];
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const useSyncedState = createSyncedStateHook();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SyncedStateServer } from "./SyncedStateServer.mjs";
|
|
2
|
+
export { SyncedStateServer };
|
|
3
|
+
export type SyncedStateRouteOptions = {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
durableObjectName?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
9
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
10
|
+
* @param options Optional overrides for base path and object name.
|
|
11
|
+
* @returns Router entries for the sync state API.
|
|
12
|
+
*/
|
|
13
|
+
export declare const syncedStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncedStateServer>, options?: SyncedStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`, import("../runtime/worker.js").RequestInfo<any, import("../runtime/worker.js").DefaultAppContext>>[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler;
|
|
13
|
+
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
|
+
import { env } from "cloudflare:workers";
|
|
15
|
+
import { route } from "../runtime/entries/router";
|
|
16
|
+
import { SyncedStateServer, } from "./SyncedStateServer.mjs";
|
|
17
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
18
|
+
export { SyncedStateServer };
|
|
19
|
+
const DEFAULT_SYNC_STATE_NAME = "syncedState";
|
|
20
|
+
class SyncedStateProxy extends RpcTarget {
|
|
21
|
+
constructor(stub, keyHandler) {
|
|
22
|
+
super();
|
|
23
|
+
_SyncedStateProxy_stub.set(this, void 0);
|
|
24
|
+
_SyncedStateProxy_keyHandler.set(this, void 0);
|
|
25
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_stub, stub, "f");
|
|
26
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_keyHandler, keyHandler, "f");
|
|
27
|
+
}
|
|
28
|
+
async getState(key) {
|
|
29
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key) : key;
|
|
30
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").getState(transformedKey);
|
|
31
|
+
}
|
|
32
|
+
async setState(value, key) {
|
|
33
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key) : key;
|
|
34
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").setState(value, transformedKey);
|
|
35
|
+
}
|
|
36
|
+
async subscribe(key, client) {
|
|
37
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key) : key;
|
|
38
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").subscribe(transformedKey, client);
|
|
39
|
+
}
|
|
40
|
+
async unsubscribe(key, client) {
|
|
41
|
+
const transformedKey = __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f") ? await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key) : key;
|
|
42
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").unsubscribe(transformedKey, client);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_SyncedStateProxy_stub = new WeakMap(), _SyncedStateProxy_keyHandler = new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
48
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
49
|
+
* @param options Optional overrides for base path and object name.
|
|
50
|
+
* @returns Router entries for the sync state API.
|
|
51
|
+
*/
|
|
52
|
+
export const syncedStateRoutes = (getNamespace, options = {}) => {
|
|
53
|
+
const basePath = options.basePath ?? DEFAULT_SYNCED_STATE_PATH;
|
|
54
|
+
const durableObjectName = options.durableObjectName ?? DEFAULT_SYNC_STATE_NAME;
|
|
55
|
+
const forwardRequest = async (request) => {
|
|
56
|
+
const keyHandler = SyncedStateServer.getKeyHandler();
|
|
57
|
+
if (!keyHandler) {
|
|
58
|
+
const namespace = getNamespace(env);
|
|
59
|
+
const id = namespace.idFromName(durableObjectName);
|
|
60
|
+
return namespace.get(id).fetch(request);
|
|
61
|
+
}
|
|
62
|
+
const namespace = getNamespace(env);
|
|
63
|
+
const id = namespace.idFromName(durableObjectName);
|
|
64
|
+
const coordinator = namespace.get(id);
|
|
65
|
+
const proxy = new SyncedStateProxy(coordinator, keyHandler);
|
|
66
|
+
return newWorkersRpcResponse(request, proxy);
|
|
67
|
+
};
|
|
68
|
+
return [route(basePath, ({ request }) => forwardRequest(request))];
|
|
69
|
+
};
|
package/dist/vite/buildApp.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
|
|
3
6
|
import { runDirectivesScan } from "./runDirectivesScan.mjs";
|
|
4
7
|
const log = debug("rwsdk:vite:build-app");
|
|
5
8
|
/**
|
|
@@ -10,7 +13,36 @@ const log = debug("rwsdk:vite:build-app");
|
|
|
10
13
|
* @see docs/architecture/productionBuildProcess.md
|
|
11
14
|
*/
|
|
12
15
|
export async function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, workerEntryPathname, }) {
|
|
16
|
+
await rm(resolve(projectRootDir, "dist"), { recursive: true, force: true });
|
|
13
17
|
const workerEnv = builder.environments.worker;
|
|
18
|
+
// Run a pre-scan build pass to allow plugins to set up and generate code
|
|
19
|
+
// before scanning.
|
|
20
|
+
console.log("Running plugin setup pass...");
|
|
21
|
+
process.env.RWSDK_BUILD_PASS = "plugin-setup";
|
|
22
|
+
const tempEntryPath = resolve(INTERMEDIATES_OUTPUT_DIR, "temp-entry.js");
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(dirname(tempEntryPath))) {
|
|
25
|
+
await mkdir(dirname(tempEntryPath), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
await writeFile(tempEntryPath, "");
|
|
28
|
+
const originalWorkerBuildConfig = workerEnv.config.build;
|
|
29
|
+
workerEnv.config.build = {
|
|
30
|
+
...originalWorkerBuildConfig,
|
|
31
|
+
write: false,
|
|
32
|
+
rollupOptions: {
|
|
33
|
+
...originalWorkerBuildConfig?.rollupOptions,
|
|
34
|
+
input: {
|
|
35
|
+
index: tempEntryPath,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
await builder.build(workerEnv);
|
|
40
|
+
// Restore the original config
|
|
41
|
+
workerEnv.config.build = originalWorkerBuildConfig;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(tempEntryPath, { force: true });
|
|
45
|
+
}
|
|
14
46
|
await runDirectivesScan({
|
|
15
47
|
rootConfig: builder.config,
|
|
16
48
|
environments: builder.environments,
|
|
@@ -18,7 +50,7 @@ export async function buildApp({ builder, clientEntryPoints, clientFiles, server
|
|
|
18
50
|
serverFiles,
|
|
19
51
|
entries: [workerEntryPathname],
|
|
20
52
|
});
|
|
21
|
-
console.log("Building worker
|
|
53
|
+
console.log("Building worker...");
|
|
22
54
|
process.env.RWSDK_BUILD_PASS = "worker";
|
|
23
55
|
await builder.build(workerEnv);
|
|
24
56
|
log("Used client files after worker build & filtering: %O", Array.from(clientFiles));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Plugin that performs initialization workarounds that must run before
|
|
4
|
+
* the Cloudflare plugin's `configureServer` hook executes.
|
|
5
|
+
*
|
|
6
|
+
* Cloudflare plugin v1.15.0 executes the worker entry file during
|
|
7
|
+
* `configureServer` to detect exports, which triggers SSR code evaluation
|
|
8
|
+
* before normal server initialization completes. This plugin ensures
|
|
9
|
+
* required systems are initialized beforehand.
|
|
10
|
+
*/
|
|
11
|
+
export declare const cloudflarePreInitPlugin: () => Plugin;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin that performs initialization workarounds that must run before
|
|
3
|
+
* the Cloudflare plugin's `configureServer` hook executes.
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare plugin v1.15.0 executes the worker entry file during
|
|
6
|
+
* `configureServer` to detect exports, which triggers SSR code evaluation
|
|
7
|
+
* before normal server initialization completes. This plugin ensures
|
|
8
|
+
* required systems are initialized beforehand.
|
|
9
|
+
*/
|
|
10
|
+
export const cloudflarePreInitPlugin = () => {
|
|
11
|
+
return {
|
|
12
|
+
name: "rwsdk:cloudflare-pre-init",
|
|
13
|
+
// context(justinvdm, 20 Jan 2025): This plugin must run before the
|
|
14
|
+
// Cloudflare plugin's `configureServer` hook. The Cloudflare plugin
|
|
15
|
+
// executes the worker entry file during `configureServer` to detect
|
|
16
|
+
// exports, and blocks the rest of the configureServer plugins until the
|
|
17
|
+
// request is complete. We must initialize required systems (SSR dependency
|
|
18
|
+
// optimizer + our own plugins to them, as well as CSS plugin's moduleCache)
|
|
19
|
+
// before this happens to prevent errors.
|
|
20
|
+
enforce: "pre",
|
|
21
|
+
async configureServer(server) {
|
|
22
|
+
// context(justinvdm, 20 Jan 2025): Initialize SSR dependency optimizer before
|
|
23
|
+
// Cloudflare plugin triggers SSR code evaluation. This ensures dependencies
|
|
24
|
+
// in `optimizeDeps.include` (like `react-dom/server.edge`) are correctly
|
|
25
|
+
// registered before they are discovered lazily.
|
|
26
|
+
if (server.environments.ssr?.depsOptimizer) {
|
|
27
|
+
await server.environments.ssr.depsOptimizer.init();
|
|
28
|
+
}
|
|
29
|
+
// context(justinvdm, 20 Jan 2025): Initialize CSS plugin's shared moduleCache
|
|
30
|
+
// before CSS modules are processed. The Cloudflare plugin's export detection
|
|
31
|
+
// can trigger CSS module processing in the SSR environment during `configureServer`,
|
|
32
|
+
// which happens before `initServer` runs. Vite's CSS plugin uses a shared
|
|
33
|
+
// `moduleCache` that is initialized in the client environment's `buildStart` hook.
|
|
34
|
+
// By calling `buildStart` here (which is idempotent), we ensure the CSS plugin's
|
|
35
|
+
// cache is initialized before CSS modules are processed, preventing "Cannot read
|
|
36
|
+
// properties of undefined (reading 'set')" errors.
|
|
37
|
+
await server.environments.client.pluginContainer.buildStart();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|