rwsdk 1.0.0-beta.5 → 1.0.0-beta.50
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/bin/rw-scripts.mjs +13 -13
- 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 +49 -57
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +201 -64
- package/dist/lib/e2e/index.d.mts +2 -0
- package/dist/lib/e2e/index.mjs +2 -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 +57 -52
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +39 -3
- package/dist/lib/e2e/testHarness.mjs +239 -92
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/lib/normalizeModulePath.mjs +1 -1
- package/dist/runtime/client/client.d.ts +64 -2
- package/dist/runtime/client/client.js +156 -15
- package/dist/runtime/client/navigation.d.ts +45 -0
- package/dist/runtime/client/navigation.js +68 -14
- package/dist/runtime/client/navigationCache.d.ts +68 -0
- package/dist/runtime/client/navigationCache.js +294 -0
- package/dist/runtime/client/navigationCache.test.js +469 -0
- package/dist/runtime/client/types.d.ts +26 -5
- package/dist/runtime/client/types.js +8 -1
- 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/no-react-server.js +3 -1
- package/dist/runtime/entries/react-server-only.js +1 -1
- 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 +4 -0
- package/dist/runtime/entries/worker.js +4 -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 +84 -26
- package/dist/runtime/lib/links.test.d.ts +1 -0
- 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 +28 -6
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +154 -35
- package/dist/runtime/lib/router.js +491 -105
- package/dist/runtime/lib/router.test.js +611 -1
- 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/register/client.d.ts +1 -1
- package/dist/runtime/register/client.js +10 -3
- package/dist/runtime/register/worker.js +13 -4
- package/dist/runtime/render/normalizeActionResult.js +8 -1
- 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 +5 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +45 -0
- package/dist/runtime/requestInfo/worker.d.ts +0 -1
- package/dist/runtime/requestInfo/worker.js +5 -11
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/server.d.ts +52 -0
- package/dist/runtime/server.js +88 -0
- 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 +45 -2
- 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 +36 -0
- package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -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 +70 -0
- package/dist/use-synced-state/client-core.d.ts +29 -0
- package/dist/use-synced-state/client-core.js +103 -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 +21 -0
- package/dist/use-synced-state/useSyncedState.js +64 -0
- package/dist/use-synced-state/worker.d.mts +14 -0
- package/dist/use-synced-state/worker.mjs +135 -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 +9 -11
- package/dist/vite/redwoodPlugin.test.mjs +4 -4
- package/dist/vite/runDirectivesScan.mjs +75 -19
- 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/transformClientComponents.test.mjs +32 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
- package/dist/vite/transformServerFunctions.mjs +66 -4
- package/dist/vite/transformServerFunctions.test.mjs +35 -0
- package/dist/vite/virtualPlugin.mjs +6 -7
- package/package.json +41 -19
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
10
|
+
* @param endpoint Endpoint to connect to.
|
|
11
|
+
* @returns RPC client instance.
|
|
12
|
+
*/
|
|
13
|
+
export declare const getSyncedStateClient: (endpoint?: string) => SyncedStateClient;
|
|
14
|
+
/**
|
|
15
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
16
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
17
|
+
* @param options Optional endpoint override.
|
|
18
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const initSyncedStateClient: (options?: {
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
}) => SyncedStateClient | null;
|
|
23
|
+
/**
|
|
24
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
25
|
+
* Also clears the subscription registry for test isolation.
|
|
26
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
27
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
28
|
+
*/
|
|
29
|
+
export declare const setSyncedStateClientForTesting: (client: SyncedStateClient | null, endpoint?: string) => void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
3
|
+
// Map of endpoint URLs to their respective clients
|
|
4
|
+
const clientCache = new Map();
|
|
5
|
+
const activeSubscriptions = new Set();
|
|
6
|
+
// Set up beforeunload handler to unsubscribe all active subscriptions
|
|
7
|
+
if (typeof window !== "undefined") {
|
|
8
|
+
const handleBeforeUnload = () => {
|
|
9
|
+
if (activeSubscriptions.size === 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Unsubscribe all active subscriptions
|
|
13
|
+
// Use a synchronous approach where possible, but don't block page unload
|
|
14
|
+
const subscriptions = Array.from(activeSubscriptions);
|
|
15
|
+
activeSubscriptions.clear();
|
|
16
|
+
// Fire-and-forget unsubscribe calls - we can't await during beforeunload
|
|
17
|
+
for (const { key, handler, client } of subscriptions) {
|
|
18
|
+
void client.unsubscribe(key, handler).catch(() => {
|
|
19
|
+
// Ignore errors during page unload - the connection will be closed anyway
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
27
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
28
|
+
* @param endpoint Endpoint to connect to.
|
|
29
|
+
* @returns RPC client instance.
|
|
30
|
+
*/
|
|
31
|
+
export const getSyncedStateClient = (endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
32
|
+
// Return existing client if already cached for this endpoint
|
|
33
|
+
const existingClient = clientCache.get(endpoint);
|
|
34
|
+
if (existingClient) {
|
|
35
|
+
return existingClient;
|
|
36
|
+
}
|
|
37
|
+
const baseClient = newWebSocketRpcSession(endpoint);
|
|
38
|
+
// Wrap the client using a Proxy to track subscriptions
|
|
39
|
+
// The RPC client uses dynamic property access, so we can't use .bind()
|
|
40
|
+
const wrappedClient = new Proxy(baseClient, {
|
|
41
|
+
get(target, prop) {
|
|
42
|
+
if (prop === "subscribe") {
|
|
43
|
+
return async (key, handler) => {
|
|
44
|
+
const subscription = {
|
|
45
|
+
key,
|
|
46
|
+
handler,
|
|
47
|
+
client: wrappedClient,
|
|
48
|
+
};
|
|
49
|
+
activeSubscriptions.add(subscription);
|
|
50
|
+
return target[prop](key, handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (prop === "unsubscribe") {
|
|
54
|
+
return async (key, handler) => {
|
|
55
|
+
// Find and remove the subscription
|
|
56
|
+
for (const sub of activeSubscriptions) {
|
|
57
|
+
if (sub.key === key &&
|
|
58
|
+
sub.handler === handler &&
|
|
59
|
+
sub.client === wrappedClient) {
|
|
60
|
+
activeSubscriptions.delete(sub);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return target[prop](key, handler);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Pass through all other properties/methods
|
|
68
|
+
return target[prop];
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Cache the client for this endpoint
|
|
72
|
+
clientCache.set(endpoint, wrappedClient);
|
|
73
|
+
return wrappedClient;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
77
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
78
|
+
* @param options Optional endpoint override.
|
|
79
|
+
* @returns Cached client instance or `null` when running without `window`.
|
|
80
|
+
*/
|
|
81
|
+
export const initSyncedStateClient = (options = {}) => {
|
|
82
|
+
const endpoint = options.endpoint ?? DEFAULT_SYNCED_STATE_PATH;
|
|
83
|
+
if (typeof window === "undefined") {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Use getSyncedStateClient which now handles caching via Map
|
|
87
|
+
return getSyncedStateClient(endpoint);
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Injects a client instance for tests and updates the cached endpoint.
|
|
91
|
+
* Also clears the subscription registry for test isolation.
|
|
92
|
+
* @param client Stub client instance or `null` to clear the cache.
|
|
93
|
+
* @param endpoint Endpoint associated with the injected client.
|
|
94
|
+
*/
|
|
95
|
+
export const setSyncedStateClientForTesting = (client, endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
96
|
+
if (client) {
|
|
97
|
+
clientCache.set(endpoint, client);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
clientCache.delete(endpoint);
|
|
101
|
+
}
|
|
102
|
+
activeSubscriptions.clear();
|
|
103
|
+
};
|
|
@@ -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,21 @@
|
|
|
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
|
+
roomId?: string;
|
|
12
|
+
hooks?: HookDeps;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
|
|
16
|
+
* @param options Optional overrides for endpoint and React primitives.
|
|
17
|
+
* @returns Hook that syncs state through the sync state service.
|
|
18
|
+
*/
|
|
19
|
+
export declare const createSyncedStateHook: (options?: CreateSyncedStateHookOptions) => <T>(initialValue: T, key: string, roomId?: string | undefined) => [T, Setter<T>];
|
|
20
|
+
export declare const useSyncedState: <T>(initialValue: T, key: string, roomId?: string | undefined) => [T, Setter<T>];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
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 basePath = 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, roomId = options.roomId) {
|
|
20
|
+
if (typeof window === "undefined" && !options.hooks) {
|
|
21
|
+
return [initialValue, () => { }];
|
|
22
|
+
}
|
|
23
|
+
const resolvedUrl = roomId ? `${basePath}/${roomId}` : basePath;
|
|
24
|
+
const client = getSyncedStateClient(resolvedUrl);
|
|
25
|
+
const [value, setValue] = useState(initialValue);
|
|
26
|
+
const valueRef = useRef(value);
|
|
27
|
+
valueRef.current = value;
|
|
28
|
+
const setSyncValue = useCallback((nextValue) => {
|
|
29
|
+
const resolved = typeof nextValue === "function"
|
|
30
|
+
? nextValue(valueRef.current)
|
|
31
|
+
: nextValue;
|
|
32
|
+
setValue(resolved);
|
|
33
|
+
valueRef.current = resolved;
|
|
34
|
+
void client.setState(resolved, key);
|
|
35
|
+
}, [client, key, setValue, valueRef]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let isActive = true;
|
|
38
|
+
const handleUpdate = (next) => {
|
|
39
|
+
if (isActive) {
|
|
40
|
+
setValue(next);
|
|
41
|
+
valueRef.current = next;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
void client.getState(key).then((existing) => {
|
|
45
|
+
if (existing !== undefined && isActive) {
|
|
46
|
+
setValue(existing);
|
|
47
|
+
valueRef.current = existing;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
void client.subscribe(key, handleUpdate);
|
|
51
|
+
return () => {
|
|
52
|
+
isActive = false;
|
|
53
|
+
// Call unsubscribe when component unmounts
|
|
54
|
+
// Page reloads are handled by the beforeunload event listener in client-core.ts
|
|
55
|
+
void client.unsubscribe(key, handleUpdate).catch((error) => {
|
|
56
|
+
// Log but don't throw - cleanup should not prevent unmounting
|
|
57
|
+
console.error("[useSyncedState] Error during unsubscribe:", error);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}, [client, key, setValue, valueRef]);
|
|
61
|
+
return [value, setSyncValue];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export const useSyncedState = createSyncedStateHook();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RequestInfo } from "../runtime/requestInfo/types";
|
|
2
|
+
import { SyncedStateServer } from "./SyncedStateServer.mjs";
|
|
3
|
+
export { SyncedStateServer };
|
|
4
|
+
export type SyncedStateRouteOptions = {
|
|
5
|
+
basePath?: string;
|
|
6
|
+
durableObjectName?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
10
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
11
|
+
* @param options Optional overrides for base path and object name.
|
|
12
|
+
* @returns Router entries for the sync state API.
|
|
13
|
+
*/
|
|
14
|
+
export declare const syncedStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncedStateServer>, options?: SyncedStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`, RequestInfo<any, import("../runtime/requestInfo/types").DefaultAppContext>>[];
|
|
@@ -0,0 +1,135 @@
|
|
|
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_instances, _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler, _SyncedStateProxy_requestInfo, _SyncedStateProxy_transformKey, _SyncedStateProxy_callHandler;
|
|
13
|
+
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
|
+
import { env } from "cloudflare:workers";
|
|
15
|
+
import { route } from "../runtime/entries/router";
|
|
16
|
+
import { runWithRequestInfo } from "../runtime/requestInfo/worker";
|
|
17
|
+
import { SyncedStateServer, } from "./SyncedStateServer.mjs";
|
|
18
|
+
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
19
|
+
export { SyncedStateServer };
|
|
20
|
+
const DEFAULT_SYNC_STATE_NAME = "syncedState";
|
|
21
|
+
class SyncedStateProxy extends RpcTarget {
|
|
22
|
+
constructor(stub, keyHandler, requestInfo) {
|
|
23
|
+
super();
|
|
24
|
+
_SyncedStateProxy_instances.add(this);
|
|
25
|
+
_SyncedStateProxy_stub.set(this, void 0);
|
|
26
|
+
_SyncedStateProxy_keyHandler.set(this, void 0);
|
|
27
|
+
_SyncedStateProxy_requestInfo.set(this, void 0);
|
|
28
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_stub, stub, "f");
|
|
29
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_keyHandler, keyHandler, "f");
|
|
30
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_requestInfo, requestInfo, "f");
|
|
31
|
+
// Set stub in DO instance so handlers can access it
|
|
32
|
+
if (stub && typeof stub._setStub === "function") {
|
|
33
|
+
void stub._setStub(stub);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async getState(key) {
|
|
37
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
38
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").getState(transformedKey);
|
|
39
|
+
}
|
|
40
|
+
async setState(value, key) {
|
|
41
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
42
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").setState(value, transformedKey);
|
|
43
|
+
}
|
|
44
|
+
async subscribe(key, client) {
|
|
45
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
46
|
+
const subscribeHandler = SyncedStateServer.getSubscribeHandler();
|
|
47
|
+
if (subscribeHandler) {
|
|
48
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_callHandler).call(this, subscribeHandler, transformedKey, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
49
|
+
}
|
|
50
|
+
// dup the client if it is a function; otherwise, pass it as is;
|
|
51
|
+
// this is because the client is a WebSocketRpcSession, and we need to pass a new instance of the client to the DO;
|
|
52
|
+
const clientToPass = typeof client.dup === "function" ? client.dup() : client;
|
|
53
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").subscribe(transformedKey, clientToPass);
|
|
54
|
+
}
|
|
55
|
+
async unsubscribe(key, client) {
|
|
56
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
57
|
+
// Call unsubscribe handler before unsubscribe, similar to subscribe handler
|
|
58
|
+
// This ensures the handler is called even if the unsubscribe doesn't find a match
|
|
59
|
+
// or if the RPC call fails
|
|
60
|
+
const unsubscribeHandler = SyncedStateServer.getUnsubscribeHandler();
|
|
61
|
+
if (unsubscribeHandler) {
|
|
62
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_callHandler).call(this, unsubscribeHandler, transformedKey, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").unsubscribe(transformedKey, client);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Ignore errors during unsubscribe - handler has already been called
|
|
69
|
+
// This prevents RPC stub disposal errors from propagating
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
_SyncedStateProxy_stub = new WeakMap(), _SyncedStateProxy_keyHandler = new WeakMap(), _SyncedStateProxy_requestInfo = new WeakMap(), _SyncedStateProxy_instances = new WeakSet(), _SyncedStateProxy_transformKey =
|
|
74
|
+
/**
|
|
75
|
+
* Transforms a key using the keyHandler, preserving async context so requestInfo.ctx is available.
|
|
76
|
+
*/
|
|
77
|
+
async function _SyncedStateProxy_transformKey(key) {
|
|
78
|
+
if (!__classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f")) {
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
if (__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f")) {
|
|
82
|
+
// Preserve async context when calling keyHandler so requestInfo.ctx is available
|
|
83
|
+
return await runWithRequestInfo(__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f"), async () => await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f")(key, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f")));
|
|
84
|
+
}
|
|
85
|
+
return await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
86
|
+
}, _SyncedStateProxy_callHandler = function _SyncedStateProxy_callHandler(handler, key, stub) {
|
|
87
|
+
if (__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f")) {
|
|
88
|
+
// Preserve async context when calling handler so requestInfo.ctx is available
|
|
89
|
+
runWithRequestInfo(__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f"), () => {
|
|
90
|
+
handler(key, stub);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
handler(key, stub);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
99
|
+
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
100
|
+
* @param options Optional overrides for base path and object name.
|
|
101
|
+
* @returns Router entries for the sync state API.
|
|
102
|
+
*/
|
|
103
|
+
export const syncedStateRoutes = (getNamespace, options = {}) => {
|
|
104
|
+
const basePath = options.basePath ?? DEFAULT_SYNCED_STATE_PATH;
|
|
105
|
+
const durableObjectName = options.durableObjectName ?? DEFAULT_SYNC_STATE_NAME;
|
|
106
|
+
const forwardRequest = async (request, requestInfo) => {
|
|
107
|
+
const namespace = getNamespace(env);
|
|
108
|
+
// Register the namespace and DO name so handlers can access it
|
|
109
|
+
SyncedStateServer.registerNamespace(namespace, durableObjectName);
|
|
110
|
+
const keyHandler = SyncedStateServer.getKeyHandler();
|
|
111
|
+
const roomHandler = SyncedStateServer.getRoomHandler();
|
|
112
|
+
// Get the room ID from the URL parameter, or undefined if not present
|
|
113
|
+
const idParam = requestInfo.params?.id;
|
|
114
|
+
// Resolve the room name using the roomHandler if present, otherwise use the param or default
|
|
115
|
+
let resolvedRoomName;
|
|
116
|
+
if (roomHandler) {
|
|
117
|
+
resolvedRoomName = await runWithRequestInfo(requestInfo, async () => await roomHandler(idParam, requestInfo));
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
resolvedRoomName = idParam ?? durableObjectName;
|
|
121
|
+
}
|
|
122
|
+
if (!keyHandler) {
|
|
123
|
+
const id = namespace.idFromName(resolvedRoomName);
|
|
124
|
+
return namespace.get(id).fetch(request);
|
|
125
|
+
}
|
|
126
|
+
const id = namespace.idFromName(resolvedRoomName);
|
|
127
|
+
const coordinator = namespace.get(id);
|
|
128
|
+
const proxy = new SyncedStateProxy(coordinator, keyHandler, requestInfo);
|
|
129
|
+
return newWorkersRpcResponse(request, proxy);
|
|
130
|
+
};
|
|
131
|
+
return [
|
|
132
|
+
route(basePath, (requestInfo) => forwardRequest(requestInfo.request, requestInfo)),
|
|
133
|
+
route(basePath + "/:id", (requestInfo) => forwardRequest(requestInfo.request, requestInfo)),
|
|
134
|
+
];
|
|
135
|
+
};
|
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
|
+
};
|
|
@@ -3,6 +3,7 @@ import path, { resolve } from "node:path";
|
|
|
3
3
|
import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
|
|
4
4
|
import { buildApp } from "./buildApp.mjs";
|
|
5
5
|
import { externalModules } from "./constants.mjs";
|
|
6
|
+
import { ssrBridgeWrapPlugin } from "./ssrBridgeWrapPlugin.mjs";
|
|
6
7
|
export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, }) => ({
|
|
7
8
|
name: "rwsdk:config",
|
|
8
9
|
config: async (_, { command }) => {
|
|
@@ -55,6 +56,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
55
56
|
build: {
|
|
56
57
|
outDir: resolve(projectRootDir, "dist", "worker"),
|
|
57
58
|
emitAssets: true,
|
|
59
|
+
ssrManifest: true,
|
|
58
60
|
emptyOutDir: false,
|
|
59
61
|
ssr: true,
|
|
60
62
|
},
|
|
@@ -93,6 +95,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
93
95
|
"rwsdk/constants",
|
|
94
96
|
"rwsdk/debug",
|
|
95
97
|
"rwsdk/realtime/client",
|
|
98
|
+
"rwsdk/router",
|
|
96
99
|
"rwsdk/turnstile",
|
|
97
100
|
],
|
|
98
101
|
entries: [],
|
|
@@ -161,22 +164,14 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
161
164
|
// causes a redeclaration error. To solve this, we wrap the SSR
|
|
162
165
|
// bundle in an exporting IIFE. This creates a scope boundary,
|
|
163
166
|
// preventing symbol collisions while producing a valid,
|
|
164
|
-
// tree-shakeable ES module.
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
+
// tree-shakeable ES module.
|
|
168
|
+
//
|
|
169
|
+
// context(justinvdm, 19 Nov 2025): We use a custom plugin
|
|
170
|
+
// (ssrBridgeWrapPlugin) to intelligently inject the IIFE *after*
|
|
171
|
+
// any top-level external imports, ensuring they remain valid.
|
|
167
172
|
inlineDynamicImports: true,
|
|
168
|
-
banner: `export const { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream } = (function() {`,
|
|
169
|
-
footer: `return { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream };\n})();`,
|
|
170
173
|
},
|
|
171
|
-
plugins: [
|
|
172
|
-
{
|
|
173
|
-
name: "rwsdk:ssr-bridge-exports",
|
|
174
|
-
renderChunk(code) {
|
|
175
|
-
// Remove the original export statement as it's now handled by the banner/footer
|
|
176
|
-
return code.replace(/export\s*{[^}]+};?/, "");
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
],
|
|
174
|
+
plugins: [ssrBridgeWrapPlugin()],
|
|
180
175
|
},
|
|
181
176
|
},
|
|
182
177
|
},
|
package/dist/vite/constants.mjs
CHANGED
|
@@ -42,6 +42,10 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
|
|
|
42
42
|
log("Development mode: %s", isDev);
|
|
43
43
|
},
|
|
44
44
|
configureServer(server) {
|
|
45
|
+
// context(justinvdm, 19 Nov 2025): This hook simply saves a reference
|
|
46
|
+
// to the dev server instance for use in other hooks. Unlike plugins that
|
|
47
|
+
// must run before the Cloudflare plugin to prevent startup deadlocks,
|
|
48
|
+
// its execution order is not critical, so `enforce: 'pre'` is not needed.
|
|
45
49
|
devServer = server;
|
|
46
50
|
},
|
|
47
51
|
configEnvironment(env, viteConfig) {
|
|
@@ -97,9 +101,12 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
|
|
|
97
101
|
log("Skipping optimizeDeps and aliasing for environment: %s", env);
|
|
98
102
|
}
|
|
99
103
|
},
|
|
100
|
-
resolveId(source) {
|
|
101
|
-
// Skip during directive scanning to avoid performance issues
|
|
102
|
-
|
|
104
|
+
resolveId(source, _importer, options) {
|
|
105
|
+
// Skip during our directive scanning to avoid performance issues
|
|
106
|
+
// context(justinvdm, 20 Jan 2025): We check options.custom?.rwsdk?.directiveScan to distinguish
|
|
107
|
+
// between our directive scan (which should skip) and external calls like Cloudflare's early
|
|
108
|
+
// dispatch (which should be handled normally).
|
|
109
|
+
if (options?.custom?.rwsdk?.directiveScan === true) {
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
112
|
if (source !== `${config.virtualModuleName}.js`) {
|
|
@@ -140,10 +147,6 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
|
|
|
140
147
|
return source;
|
|
141
148
|
},
|
|
142
149
|
async load(id) {
|
|
143
|
-
// Skip during directive scanning to avoid performance issues
|
|
144
|
-
if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
150
|
if (id === config.virtualModuleName + ".js") {
|
|
148
151
|
log("Loading %s module with %d files", config.virtualModuleName, files.size);
|
|
149
152
|
const environment = this.environment?.name || "client";
|
|
@@ -6,6 +6,10 @@ export const devServerTimingPlugin = () => {
|
|
|
6
6
|
return {
|
|
7
7
|
name: "rwsdk:dev-server-timing",
|
|
8
8
|
configureServer(server) {
|
|
9
|
+
// context(justinvdm, 19 Nov 2025): This hook adds a middleware for
|
|
10
|
+
// logging the time to first response. Unlike other plugins that must
|
|
11
|
+
// run before the Cloudflare plugin to prevent startup deadlocks, its
|
|
12
|
+
// execution order is not critical, so `enforce: 'pre'` is not needed.
|
|
9
13
|
server.middlewares.use((_req, res, next) => {
|
|
10
14
|
if (!hasLoggedFirstResponse) {
|
|
11
15
|
res.on("finish", () => {
|