rwsdk 1.0.0-beta.5 → 1.0.0-beta.51

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.
Files changed (157) hide show
  1. package/bin/rw-scripts.mjs +13 -13
  2. package/dist/lib/constants.d.mts +1 -0
  3. package/dist/lib/constants.mjs +7 -4
  4. package/dist/lib/e2e/browser.mjs +6 -2
  5. package/dist/lib/e2e/constants.d.mts +4 -0
  6. package/dist/lib/e2e/constants.mjs +49 -12
  7. package/dist/lib/e2e/dev.mjs +49 -57
  8. package/dist/lib/e2e/environment.d.mts +2 -0
  9. package/dist/lib/e2e/environment.mjs +201 -64
  10. package/dist/lib/e2e/index.d.mts +2 -0
  11. package/dist/lib/e2e/index.mjs +2 -0
  12. package/dist/lib/e2e/poll.d.mts +1 -1
  13. package/dist/lib/e2e/release.d.mts +1 -0
  14. package/dist/lib/e2e/release.mjs +57 -52
  15. package/dist/lib/e2e/tarball.mjs +2 -34
  16. package/dist/lib/e2e/testHarness.d.mts +39 -3
  17. package/dist/lib/e2e/testHarness.mjs +239 -92
  18. package/dist/lib/e2e/utils.d.mts +1 -0
  19. package/dist/lib/e2e/utils.mjs +15 -0
  20. package/dist/lib/normalizeModulePath.mjs +1 -1
  21. package/dist/runtime/client/client.d.ts +64 -2
  22. package/dist/runtime/client/client.js +156 -15
  23. package/dist/runtime/client/navigation.d.ts +45 -0
  24. package/dist/runtime/client/navigation.js +68 -14
  25. package/dist/runtime/client/navigationCache.d.ts +68 -0
  26. package/dist/runtime/client/navigationCache.js +294 -0
  27. package/dist/runtime/client/navigationCache.test.js +469 -0
  28. package/dist/runtime/client/types.d.ts +26 -5
  29. package/dist/runtime/client/types.js +8 -1
  30. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  31. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  32. package/dist/runtime/entries/no-react-server.js +3 -1
  33. package/dist/runtime/entries/react-server-only.js +1 -1
  34. package/dist/runtime/entries/router.d.ts +1 -0
  35. package/dist/runtime/entries/routerClient.d.ts +1 -0
  36. package/dist/runtime/entries/routerClient.js +1 -0
  37. package/dist/runtime/entries/worker.d.ts +4 -0
  38. package/dist/runtime/entries/worker.js +4 -0
  39. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  40. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  41. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  42. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  43. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  44. package/dist/runtime/lib/db/createDb.js +4 -0
  45. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  46. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  47. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  48. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  49. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  50. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  51. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  52. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  53. package/dist/runtime/lib/links.d.ts +21 -7
  54. package/dist/runtime/lib/links.js +84 -26
  55. package/dist/runtime/lib/links.test.d.ts +1 -0
  56. package/dist/runtime/lib/links.test.js +20 -0
  57. package/dist/runtime/lib/manifest.d.ts +1 -1
  58. package/dist/runtime/lib/manifest.js +7 -4
  59. package/dist/runtime/lib/realtime/client.js +28 -6
  60. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  61. package/dist/runtime/lib/router.d.ts +154 -35
  62. package/dist/runtime/lib/router.js +491 -105
  63. package/dist/runtime/lib/router.test.js +611 -1
  64. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  65. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  66. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  67. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  68. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  69. package/dist/runtime/lib/types.js +1 -0
  70. package/dist/runtime/register/client.d.ts +1 -1
  71. package/dist/runtime/register/client.js +10 -3
  72. package/dist/runtime/register/worker.js +13 -4
  73. package/dist/runtime/render/normalizeActionResult.js +8 -1
  74. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  75. package/dist/runtime/render/renderToStream.d.ts +4 -2
  76. package/dist/runtime/render/renderToStream.js +53 -24
  77. package/dist/runtime/render/renderToString.d.ts +3 -6
  78. package/dist/runtime/requestInfo/types.d.ts +5 -1
  79. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  80. package/dist/runtime/requestInfo/utils.js +45 -0
  81. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  82. package/dist/runtime/requestInfo/worker.js +5 -11
  83. package/dist/runtime/script.d.ts +1 -3
  84. package/dist/runtime/script.js +1 -10
  85. package/dist/runtime/server.d.ts +52 -0
  86. package/dist/runtime/server.js +88 -0
  87. package/dist/runtime/state.d.ts +3 -0
  88. package/dist/runtime/state.js +13 -0
  89. package/dist/runtime/worker.d.ts +3 -1
  90. package/dist/runtime/worker.js +45 -2
  91. package/dist/scripts/debug-sync.mjs +18 -20
  92. package/dist/scripts/worker-run.d.mts +1 -1
  93. package/dist/scripts/worker-run.mjs +59 -113
  94. package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
  95. package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
  96. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  97. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
  98. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  99. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  100. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  101. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  102. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  103. package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
  104. package/dist/use-synced-state/client-core.d.ts +29 -0
  105. package/dist/use-synced-state/client-core.js +103 -0
  106. package/dist/use-synced-state/client.d.ts +3 -0
  107. package/dist/use-synced-state/client.js +4 -0
  108. package/dist/use-synced-state/constants.d.mts +1 -0
  109. package/dist/use-synced-state/constants.mjs +1 -0
  110. package/dist/use-synced-state/useSyncedState.d.ts +21 -0
  111. package/dist/use-synced-state/useSyncedState.js +64 -0
  112. package/dist/use-synced-state/worker.d.mts +14 -0
  113. package/dist/use-synced-state/worker.mjs +135 -0
  114. package/dist/vite/buildApp.mjs +34 -2
  115. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  116. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  117. package/dist/vite/configPlugin.mjs +9 -14
  118. package/dist/vite/constants.d.mts +1 -0
  119. package/dist/vite/constants.mjs +1 -0
  120. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  121. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  122. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  123. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  124. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  125. package/dist/vite/directivesPlugin.mjs +4 -4
  126. package/dist/vite/envResolvers.d.mts +11 -0
  127. package/dist/vite/envResolvers.mjs +20 -0
  128. package/dist/vite/getViteEsbuild.mjs +2 -1
  129. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  130. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  131. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  132. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  133. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  134. package/dist/vite/linkerPlugin.d.mts +2 -1
  135. package/dist/vite/linkerPlugin.mjs +11 -3
  136. package/dist/vite/linkerPlugin.test.mjs +15 -0
  137. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  138. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  139. package/dist/vite/redwoodPlugin.mjs +9 -11
  140. package/dist/vite/redwoodPlugin.test.mjs +4 -4
  141. package/dist/vite/runDirectivesScan.mjs +75 -19
  142. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  143. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  144. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  145. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  146. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  147. package/dist/vite/statePlugin.d.mts +4 -0
  148. package/dist/vite/statePlugin.mjs +62 -0
  149. package/dist/vite/transformClientComponents.test.mjs +32 -0
  150. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  151. package/dist/vite/transformServerFunctions.mjs +66 -4
  152. package/dist/vite/transformServerFunctions.test.mjs +35 -0
  153. package/dist/vite/virtualPlugin.mjs +6 -7
  154. package/package.json +45 -20
  155. package/dist/vite/manifestPlugin.d.mts +0 -4
  156. package/dist/vite/manifestPlugin.mjs +0 -63
  157. /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
@@ -0,0 +1,45 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { generateNonce } from "../lib/utils";
3
+ export const DefaultRequestInfoDocument = ({ children }) => (_jsx(_Fragment, { children: children }));
4
+ /**
5
+ * Constructs a generic requestInfo that can be used as defaults.
6
+ * Allows for passing in overrides to initialize with defaults.
7
+ */
8
+ export const constructWithDefaultRequestInfo = (overrides = {}) => {
9
+ const { rw: rwOverrides, ...otherRequestInfoOverrides } = overrides;
10
+ const defaultRequestInfo = {
11
+ request: new Request("http://localhost/"),
12
+ path: "/",
13
+ params: {},
14
+ ctx: {},
15
+ cf: {
16
+ waitUntil: () => { },
17
+ passThroughOnException: () => { },
18
+ props: {},
19
+ },
20
+ response: {
21
+ status: 200,
22
+ headers: new Headers(),
23
+ },
24
+ isAction: false,
25
+ rw: {
26
+ Document: DefaultRequestInfoDocument,
27
+ nonce: generateNonce(),
28
+ rscPayload: true,
29
+ ssr: true,
30
+ databases: new Map(),
31
+ scriptsToBeLoaded: new Set(),
32
+ entryScripts: new Set(),
33
+ inlineScripts: new Set(),
34
+ pageRouteResolved: undefined,
35
+ },
36
+ };
37
+ return {
38
+ ...defaultRequestInfo,
39
+ ...otherRequestInfoOverrides,
40
+ rw: {
41
+ ...defaultRequestInfo.rw,
42
+ ...rwOverrides,
43
+ },
44
+ };
45
+ };
@@ -2,7 +2,6 @@ import { DefaultAppContext, RequestInfo } from "./types";
2
2
  type DefaultRequestInfo = RequestInfo<DefaultAppContext>;
3
3
  export declare const requestInfo: DefaultRequestInfo;
4
4
  export declare function getRequestInfo(): RequestInfo;
5
- export declare function waitForRequestInfo(): Promise<DefaultRequestInfo>;
6
5
  export declare function runWithRequestInfo<Result>(nextRequestInfo: DefaultRequestInfo, fn: () => Result): Result;
7
6
  export declare function runWithRequestInfoOverrides<Result>(overrides: Partial<DefaultRequestInfo>, fn: () => Result): Result;
8
7
  export {};
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "async_hooks";
2
- const requestInfoDeferred = Promise.withResolvers();
3
- const requestInfoStore = new AsyncLocalStorage();
2
+ import { defineRwState } from "rwsdk/__state";
3
+ const requestInfoStore = defineRwState("requestInfoStore", () => new AsyncLocalStorage());
4
4
  const requestInfoBase = {};
5
5
  const REQUEST_INFO_KEYS = ["request", "params", "ctx", "rw", "cf", "response"];
6
6
  REQUEST_INFO_KEYS.forEach((key) => {
@@ -17,19 +17,13 @@ export const requestInfo = Object.freeze(requestInfoBase);
17
17
  export function getRequestInfo() {
18
18
  const store = requestInfoStore.getStore();
19
19
  if (!store) {
20
- throw new Error("Request context not found");
20
+ throw new Error("RedwoodSDK: Request context not found. getRequestInfo() can only be called within the request lifecycle (e.g., in a route handler, middleware, or server action).\n\n" +
21
+ "For detailed troubleshooting steps, see: https://docs.rwsdk.com/guides/troubleshooting#request-context-errors");
21
22
  }
22
23
  return store;
23
24
  }
24
- export function waitForRequestInfo() {
25
- return requestInfoDeferred.promise;
26
- }
27
25
  export function runWithRequestInfo(nextRequestInfo, fn) {
28
- const runWithRequestInfoFn = () => {
29
- requestInfoDeferred.resolve(nextRequestInfo);
30
- return fn();
31
- };
32
- return requestInfoStore.run(nextRequestInfo, runWithRequestInfoFn);
26
+ return requestInfoStore.run(nextRequestInfo, fn);
33
27
  }
34
28
  export function runWithRequestInfoOverrides(overrides, fn) {
35
29
  const requestInfo = requestInfoStore.getStore();
@@ -1,5 +1,3 @@
1
1
  export declare const defineScript: (fn: ({ env }: {
2
2
  env: Env;
3
- }) => Promise<unknown>) => {
4
- fetch: (request: Request, env: Env, cf: ExecutionContext) => Promise<Response>;
5
- };
3
+ }) => Promise<unknown>) => () => Promise<unknown>;
@@ -1,11 +1,2 @@
1
1
  import { env } from "cloudflare:workers";
2
- import { defineApp } from "./worker";
3
- export const defineScript = (fn) => {
4
- const app = defineApp([
5
- async () => {
6
- await fn({ env: env });
7
- return new Response("Done!");
8
- },
9
- ]);
10
- return app;
11
- };
2
+ export const defineScript = (fn) => () => fn({ env: env });
@@ -0,0 +1,52 @@
1
+ type Interruptor<TArgs extends any[] = any[], TResult = any> = (context: {
2
+ request: Request;
3
+ ctx: Record<string, any>;
4
+ args: TArgs;
5
+ }) => Promise<Response | void | TResult> | Response | void | TResult;
6
+ type ServerFunction<TArgs extends any[] = any[], TResult = any> = (...args: TArgs) => Promise<TResult>;
7
+ type ServerFunctionOptions = {
8
+ method?: "GET" | "POST";
9
+ };
10
+ type WrappedServerFunction<TArgs extends any[] = any[], TResult = any> = {
11
+ (...args: TArgs): Promise<TResult>;
12
+ method?: "GET" | "POST";
13
+ };
14
+ /**
15
+ * Wrap a function to be used as a server query.
16
+ *
17
+ * - **Method**: Defaults to `GET`. can be changed via `options`.
18
+ * - **Behavior**: When called from the client, it returns data-only and does **not** rehydrate or re-render the React page.
19
+ * - **Location**: Must be defined in a file with `"use server"`. We recommend `queries.ts` colocated with components.
20
+ * - **Middleware**: You can pass an array of functions as the first argument to act as interruptors (e.g. for auth).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // getters.ts
25
+ * "use server"
26
+ *
27
+ * export const getUser = serverQuery(async (id: string) => {
28
+ * return db.user.findUnique({ where: { id } })
29
+ * })
30
+ * ```
31
+ */
32
+ export declare function serverQuery<TArgs extends any[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
33
+ /**
34
+ * Wrap a function to be used as a server action.
35
+ *
36
+ * - **Method**: Defaults to `POST`. can be changed via `options`.
37
+ * - **Behavior**: When called from the client, it **will** rehydrate and re-render the React page with the new server state.
38
+ * - **Location**: Must be defined in a file with `"use server"`. We recommend `actions.ts` colocated with components.
39
+ * - **Middleware**: You can pass an array of functions as the first argument to act as interruptors (e.g. for auth).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * // actions.ts
44
+ * "use server"
45
+ *
46
+ * export const updateUser = serverAction(async (id: string, data: any) => {
47
+ * return db.user.update({ where: { id }, data })
48
+ * })
49
+ * ```
50
+ */
51
+ export declare function serverAction<TArgs extends any[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
52
+ export {};
@@ -0,0 +1,88 @@
1
+ import { requestInfo } from "./requestInfo/worker";
2
+ function createServerFunction(fns, mainFn, options) {
3
+ const wrapped = async (...args) => {
4
+ const { request, ctx } = requestInfo;
5
+ // Execute interruptors
6
+ for (const fn of fns) {
7
+ const result = await fn({ request, ctx, args });
8
+ if (result instanceof Response) {
9
+ // We can't easily return a Response from a server action function
10
+ // because the return type is expected to be TResult.
11
+ // However, if the interruptor returns a Response, it usually means "stop and return this HTTP response".
12
+ // In the RSC context, throwing a Response is a common pattern to short-circuit.
13
+ throw result;
14
+ }
15
+ }
16
+ return mainFn(...args);
17
+ };
18
+ wrapped.method = options?.method ?? "POST"; // Default to POST if not specified, though user said serverQuery defaults to GET?
19
+ // User said: "export const getProject = serverQuery(...) // Defaults to GET"
20
+ // So serverQuery defaults to GET, serverAction defaults to POST?
21
+ return wrapped;
22
+ }
23
+ /**
24
+ * Wrap a function to be used as a server query.
25
+ *
26
+ * - **Method**: Defaults to `GET`. can be changed via `options`.
27
+ * - **Behavior**: When called from the client, it returns data-only and does **not** rehydrate or re-render the React page.
28
+ * - **Location**: Must be defined in a file with `"use server"`. We recommend `queries.ts` colocated with components.
29
+ * - **Middleware**: You can pass an array of functions as the first argument to act as interruptors (e.g. for auth).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // getters.ts
34
+ * "use server"
35
+ *
36
+ * export const getUser = serverQuery(async (id: string) => {
37
+ * return db.user.findUnique({ where: { id } })
38
+ * })
39
+ * ```
40
+ */
41
+ export function serverQuery(fnsOrFn, options) {
42
+ let fns = [];
43
+ let mainFn;
44
+ if (Array.isArray(fnsOrFn)) {
45
+ fns = fnsOrFn.slice(0, -1);
46
+ mainFn = fnsOrFn[fnsOrFn.length - 1];
47
+ }
48
+ else {
49
+ mainFn = fnsOrFn;
50
+ }
51
+ const method = options?.method ?? "GET"; // Default to GET for query
52
+ const wrapped = createServerFunction(fns, mainFn, { ...options, method });
53
+ wrapped.method = method;
54
+ return wrapped;
55
+ }
56
+ /**
57
+ * Wrap a function to be used as a server action.
58
+ *
59
+ * - **Method**: Defaults to `POST`. can be changed via `options`.
60
+ * - **Behavior**: When called from the client, it **will** rehydrate and re-render the React page with the new server state.
61
+ * - **Location**: Must be defined in a file with `"use server"`. We recommend `actions.ts` colocated with components.
62
+ * - **Middleware**: You can pass an array of functions as the first argument to act as interruptors (e.g. for auth).
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // actions.ts
67
+ * "use server"
68
+ *
69
+ * export const updateUser = serverAction(async (id: string, data: any) => {
70
+ * return db.user.update({ where: { id }, data })
71
+ * })
72
+ * ```
73
+ */
74
+ export function serverAction(fnsOrFn, options) {
75
+ let fns = [];
76
+ let mainFn;
77
+ if (Array.isArray(fnsOrFn)) {
78
+ fns = fnsOrFn.slice(0, -1);
79
+ mainFn = fnsOrFn[fnsOrFn.length - 1];
80
+ }
81
+ else {
82
+ mainFn = fnsOrFn;
83
+ }
84
+ const method = options?.method ?? "POST"; // Default to POST for action
85
+ const wrapped = createServerFunction(fns, mainFn, { ...options, method });
86
+ wrapped.method = method;
87
+ return wrapped;
88
+ }
@@ -0,0 +1,3 @@
1
+ export declare function defineRwState<T>(key: string, initializer: () => T): T;
2
+ export declare function getRwState<T>(key: string): T | undefined;
3
+ export declare function setRwState<T>(key: string, value: T): void;
@@ -0,0 +1,13 @@
1
+ const state = {};
2
+ export function defineRwState(key, initializer) {
3
+ if (!(key in state)) {
4
+ state[key] = initializer();
5
+ }
6
+ return state[key];
7
+ }
8
+ export function getRwState(key) {
9
+ return state[key];
10
+ }
11
+ export function setRwState(key, value) {
12
+ state[key] = value;
13
+ }
@@ -8,9 +8,11 @@ declare global {
8
8
  DB: D1Database;
9
9
  };
10
10
  }
11
- export declare const defineApp: <T extends RequestInfo = RequestInfo<any, DefaultAppContext>>(routes: Route<T>[]) => {
11
+ export type AppDefinition<Routes extends readonly Route<any>[], T extends RequestInfo> = {
12
12
  fetch: (request: Request, env: Env, cf: ExecutionContext) => Promise<Response>;
13
+ __rwRoutes: Routes;
13
14
  };
15
+ export declare const defineApp: <T extends RequestInfo = RequestInfo<any, DefaultAppContext>, Routes extends readonly Route<T>[] = readonly Route<T>[]>(routes: Routes) => AppDefinition<Routes, T>;
14
16
  export declare const DefaultDocument: React.FC<{
15
17
  children: React.ReactNode;
16
18
  }>;
@@ -11,10 +11,11 @@ import { defineRoutes } from "./lib/router";
11
11
  import { generateNonce } from "./lib/utils";
12
12
  export * from "./requestInfo/types";
13
13
  export const defineApp = (routes) => {
14
+ const router = defineRoutes(routes);
14
15
  return {
16
+ __rwRoutes: routes,
15
17
  fetch: async (request, env, cf) => {
16
18
  globalThis.__webpack_require__ = ssrWebpackRequire;
17
- const router = defineRoutes(routes);
18
19
  // context(justinvdm, 5 Feb 2025): Serve assets requests using the assets service binding
19
20
  // todo(justinvdm, 5 Feb 2025): Find a way to avoid this so asset requests are served directly
20
21
  // rather than first needing to go through the worker
@@ -23,6 +24,35 @@ export const defineApp = (routes) => {
23
24
  url.pathname = url.pathname.slice("/assets/".length);
24
25
  return env.ASSETS.fetch(new Request(url.toString(), request));
25
26
  }
27
+ else if (import.meta.env.VITE_IS_DEV_SERVER &&
28
+ new URL(request.url).pathname === "/__worker-run") {
29
+ const expectedToken = import.meta.env
30
+ .VITE_RWSDK_WORKER_RUN_TOKEN;
31
+ const requestToken = request.headers.get("x-rwsdk-worker-run-token");
32
+ if (!expectedToken || expectedToken !== requestToken) {
33
+ return new Response("Forbidden", { status: 403 });
34
+ }
35
+ const url = new URL(request.url);
36
+ const scriptPath = url.searchParams.get("script");
37
+ if (!scriptPath) {
38
+ return new Response("Missing 'script' query parameter", {
39
+ status: 400,
40
+ });
41
+ }
42
+ try {
43
+ const scriptModule = await import(/* @vite-ignore */ scriptPath);
44
+ if (scriptModule.default) {
45
+ await scriptModule.default(request, env, cf);
46
+ }
47
+ return new Response("Script executed successfully");
48
+ }
49
+ catch (e) {
50
+ console.error(`Error executing script: ${scriptPath}\n\n${e.stack}`);
51
+ return new Response(`Error executing script: ${e.message}`, {
52
+ status: 500,
53
+ });
54
+ }
55
+ }
26
56
  else if (import.meta.env.VITE_IS_DEV_SERVER &&
27
57
  request.url.includes("/__vite_preamble__")) {
28
58
  return new Response('import RefreshRuntime from "/@react-refresh"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;', {
@@ -33,6 +63,10 @@ export const defineApp = (routes) => {
33
63
  }
34
64
  try {
35
65
  const url = new URL(request.url);
66
+ let path = url.pathname;
67
+ if (path !== "/" && !path.endsWith("/")) {
68
+ path = path + "/";
69
+ }
36
70
  const isRSCRequest = url.searchParams.has("__rsc") ||
37
71
  request.headers.get("accept")?.includes("text/x-component");
38
72
  const isAction = url.searchParams.has("__rsc_action_id");
@@ -43,6 +77,8 @@ export const defineApp = (routes) => {
43
77
  ssr: true,
44
78
  databases: new Map(),
45
79
  scriptsToBeLoaded: new Set(),
80
+ entryScripts: new Set(),
81
+ inlineScripts: new Set(),
46
82
  pageRouteResolved: undefined,
47
83
  };
48
84
  const userResponseInit = {
@@ -51,6 +87,7 @@ export const defineApp = (routes) => {
51
87
  };
52
88
  const outerRequestInfo = {
53
89
  request,
90
+ path,
54
91
  cf,
55
92
  params: {},
56
93
  ctx: {},
@@ -80,7 +117,13 @@ export const defineApp = (routes) => {
80
117
  });
81
118
  }
82
119
  const actionResult = normalizeActionResult(requestInfo.rw.actionResult);
83
- const pageElement = createPageElement(requestInfo, Page);
120
+ const isDataOnly = request.headers.get("x-rsc-data-only") === "true";
121
+ const pageElement = isDataOnly && actionResult !== undefined
122
+ ? null
123
+ : createPageElement(requestInfo, Page);
124
+ if (pageElement === null) {
125
+ rw.pageRouteResolved?.resolve();
126
+ }
84
127
  const { rscPayload: shouldInjectRSCPayload } = rw;
85
128
  let rscPayloadStream = renderToRscStream({
86
129
  input: {
@@ -99,28 +99,20 @@ const findUp = async (names, startDir) => {
99
99
  return undefined;
100
100
  };
101
101
  const getMonorepoRoot = async (startDir) => {
102
- try {
103
- // `pnpm root` is the most reliable way to find the workspace root node_modules
104
- const { stdout } = await $({
105
- cwd: startDir,
106
- }) `pnpm root`;
107
- // pnpm root returns the node_modules path, so we go up one level
108
- return path.resolve(stdout, "..");
109
- }
110
- catch (e) {
111
- console.warn(`Could not determine pnpm root from ${startDir}. Falling back to file search.`);
112
- const root = await findUp(["pnpm-workspace.yaml"], startDir);
113
- if (root) {
114
- return root;
115
- }
116
- }
117
- console.warn("Could not find pnpm monorepo root. Using parent directory of target as fallback.");
118
- return path.resolve(startDir, "..");
102
+ return await findUp(["pnpm-workspace.yaml"], startDir);
119
103
  };
120
104
  const areDependenciesEqual = (deps1, deps2) => {
121
105
  // Simple string comparison for this use case is sufficient
122
106
  return JSON.stringify(deps1 ?? {}) === JSON.stringify(deps2 ?? {});
123
107
  };
108
+ const isPlaygroundExample = async (targetDir, monorepoRoot) => {
109
+ const pkgJson = JSON.parse(await fs.readFile(path.join(monorepoRoot, "package.json"), "utf-8"));
110
+ if (pkgJson.name === "rw-sdk-monorepo") {
111
+ const playgroundDir = path.join(monorepoRoot, "playground");
112
+ return targetDir.startsWith(playgroundDir);
113
+ }
114
+ return false;
115
+ };
124
116
  const performFullSync = async (sdkDir, targetDir, monorepoRoot) => {
125
117
  console.log("📦 Performing full sync with tarball...");
126
118
  let tarballPath = "";
@@ -178,8 +170,14 @@ const performSync = async (sdkDir, targetDir) => {
178
170
  // Clean up vite cache in the target project
179
171
  await cleanupViteEntries(targetDir);
180
172
  const monorepoRoot = await getMonorepoRoot(targetDir);
173
+ const rootDir = monorepoRoot ?? targetDir;
181
174
  const projectName = path.basename(targetDir);
182
- const installedSdkPackageJsonPath = path.join(monorepoRoot, "node_modules", `.rwsync_${projectName}`, "node_modules", "rwsdk", "package.json");
175
+ if (monorepoRoot && (await isPlaygroundExample(targetDir, monorepoRoot))) {
176
+ console.log("Playground example detected. Skipping file sync; workspace linking will be used.");
177
+ console.log("✅ Done syncing");
178
+ return;
179
+ }
180
+ const installedSdkPackageJsonPath = path.join(rootDir, "node_modules", `.rwsync_${projectName}`, "node_modules", "rwsdk", "package.json");
183
181
  let needsFullSync = false;
184
182
  if (!existsSync(installedSdkPackageJsonPath)) {
185
183
  console.log("No previous sync found, performing full sync.");
@@ -195,10 +193,10 @@ const performSync = async (sdkDir, targetDir) => {
195
193
  }
196
194
  }
197
195
  if (needsFullSync) {
198
- await performFullSync(sdkDir, targetDir, monorepoRoot);
196
+ await performFullSync(sdkDir, targetDir, rootDir);
199
197
  }
200
198
  else {
201
- await performFastSync(sdkDir, targetDir, monorepoRoot);
199
+ await performFastSync(sdkDir, targetDir, rootDir);
202
200
  }
203
201
  console.log("✅ Done syncing");
204
202
  };
@@ -1 +1 @@
1
- export declare const runWorkerScript: (relativeScriptPath: string) => Promise<never>;
1
+ export {};
@@ -1,131 +1,77 @@
1
- import { Lang, parse } from "@ast-grep/napi";
2
- import baseDebug from "debug";
3
- import enhancedResolve from "enhanced-resolve";
4
- import { readFile, writeFile } from "fs/promises";
5
- import path, { resolve } from "path";
6
- import tmp from "tmp-promise";
7
- import { createServer as createViteServer } from "vite";
8
- import { unstable_readConfig } from "wrangler";
9
- import { findWranglerConfig } from "../lib/findWranglerConfig.mjs";
10
- import { redwood } from "../vite/index.mjs";
11
- const debug = baseDebug("rwsdk:worker-run");
12
- export const runWorkerScript = async (relativeScriptPath) => {
1
+ import crypto from "crypto";
2
+ import dbg from "debug";
3
+ import getPort from "get-port";
4
+ import path from "path";
5
+ import { pathToFileURL } from "url";
6
+ import * as vite from "vite";
7
+ import { createLogger } from "vite";
8
+ const debug = dbg("rwsdk:worker-run");
9
+ const main = async () => {
10
+ process.env.RWSDK_WORKER_RUN = "1";
11
+ const token = crypto.randomBytes(32).toString("hex");
12
+ process.env.VITE_RWSDK_WORKER_RUN_TOKEN = token;
13
+ const relativeScriptPath = process.argv[2];
13
14
  if (!relativeScriptPath) {
14
15
  console.error("Error: Script path is required");
15
16
  console.log("\nUsage:");
16
- console.log(" npm run worker:run <script-path>");
17
- console.log("\nOptions:");
18
- console.log(" RWSDK_WRANGLER_CONFIG Environment variable for config path");
17
+ console.log(" rwsdk worker-run <script-path>");
19
18
  console.log("\nExamples:");
20
- console.log(" npm run worker:run src/scripts/seed.ts");
21
- console.log(" RWSDK_WRANGLER_CONFIG=custom.toml npm run worker:run src/scripts/seed.ts\n");
19
+ console.log(" rwsdk worker-run src/scripts/seed.ts\n");
22
20
  process.exit(1);
23
21
  }
24
- const scriptPath = resolve(process.cwd(), relativeScriptPath);
25
- debug("Running worker script: %s", scriptPath);
26
- const workerConfigPath = process.env.RWSDK_WRANGLER_CONFIG
27
- ? resolve(process.cwd(), process.env.RWSDK_WRANGLER_CONFIG)
28
- : await findWranglerConfig(process.cwd());
29
- debug("Using wrangler config: %s", workerConfigPath);
30
- const workerConfig = unstable_readConfig({
31
- config: workerConfigPath,
32
- env: "dev",
33
- });
34
- const durableObjectsToExport = workerConfig.durable_objects?.bindings
35
- .filter((binding) => !binding.script_name)
36
- .map((binding) => binding.class_name) ?? [];
37
- const workerEntryRelativePath = workerConfig.main;
38
- const workerEntryPath = workerEntryRelativePath ?? path.join(process.cwd(), "src/worker.tsx");
39
- const durableObjectExports = [];
40
- if (durableObjectsToExport.length > 0) {
41
- const resolver = enhancedResolve.create.sync({
42
- extensions: [".mts", ".ts", ".tsx", ".mjs", ".js", ".jsx", ".json"],
43
- });
44
- const workerEntryContents = await readFile(workerEntryPath, "utf-8");
45
- const workerEntryAst = parse(Lang.Tsx, workerEntryContents);
46
- const exportDeclarations = [
47
- ...workerEntryAst.root().findAll('export { $$$EXPORTS } from "$MODULE"'),
48
- ...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
49
- ...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
50
- ];
51
- for (const exportDeclaration of exportDeclarations) {
52
- const moduleMatch = exportDeclaration.getMatch("MODULE");
53
- const exportsMatch = exportDeclaration.getMultipleMatches("EXPORTS");
54
- if (!moduleMatch || exportsMatch.length === 0) {
55
- continue;
56
- }
57
- const modulePath = moduleMatch.text();
58
- const specifiers = exportsMatch.map((m) => m.text().trim());
59
- for (const specifier of specifiers) {
60
- if (durableObjectsToExport.includes(specifier)) {
61
- const resolvedPath = resolver(path.dirname(workerEntryPath), modulePath);
62
- durableObjectExports.push(`export { ${specifier} } from "${resolvedPath}";`);
63
- }
64
- }
22
+ const scriptPath = path.resolve(process.cwd(), relativeScriptPath);
23
+ const port = await getPort();
24
+ let server;
25
+ const cleanup = async () => {
26
+ if (server) {
27
+ await server.close();
65
28
  }
66
- }
67
- const tmpDir = await tmp.dir({
68
- prefix: "rw-worker-run-",
69
- unsafeCleanup: true,
70
- });
71
- const relativeTmpWorkerEntryPath = "worker.tsx";
72
- const tmpWorkerPath = path.join(tmpDir.path, "wrangler.json");
73
- const tmpWorkerEntryPath = path.join(tmpDir.path, relativeTmpWorkerEntryPath);
74
- const scriptWorkerConfig = {
75
- ...workerConfig,
76
- configPath: tmpWorkerPath,
77
- userConfigPath: tmpWorkerPath,
78
- main: relativeTmpWorkerEntryPath,
29
+ process.exit();
79
30
  };
31
+ process.on("SIGINT", cleanup);
32
+ process.on("SIGTERM", cleanup);
80
33
  try {
81
- await writeFile(tmpWorkerPath, JSON.stringify(scriptWorkerConfig, null, 2));
82
- await writeFile(tmpWorkerEntryPath, `
83
- ${durableObjectExports.join("\n")}
84
- export { default } from "${scriptPath}";
85
- `);
86
- debug("Worker config written to: %s", tmpWorkerPath);
87
- debug("Worker entry written to: %s", tmpWorkerEntryPath);
88
- process.env.RWSDK_WORKER_RUN = "1";
89
- const server = await createViteServer({
90
- configFile: false,
91
- plugins: [
92
- redwood({
93
- configPath: tmpWorkerPath,
94
- includeCloudflarePlugin: true,
95
- entry: {
96
- worker: tmpWorkerEntryPath,
97
- },
98
- }),
99
- ],
34
+ server = await vite.createServer({
35
+ logLevel: "silent",
36
+ build: {
37
+ outDir: ".rwsdk",
38
+ },
39
+ customLogger: createLogger("info", {
40
+ prefix: "[rwsdk]",
41
+ allowClearScreen: true,
42
+ }),
100
43
  server: {
101
- port: 0,
44
+ port,
45
+ host: "localhost",
102
46
  },
103
47
  });
104
- debug("Vite server created");
105
- try {
106
- await server.listen();
107
- const address = server.httpServer?.address();
108
- debug("Server listening on address: %o", address);
109
- if (!address || typeof address === "string") {
110
- throw new Error("Dev server address is invalid");
48
+ await server.listen();
49
+ const fileUrl = pathToFileURL(scriptPath).href;
50
+ const url = `http://localhost:${port}/__worker-run?script=${encodeURIComponent(fileUrl)}`;
51
+ debug("Fetching %s", url);
52
+ const response = await fetch(url, {
53
+ headers: {
54
+ "x-rwsdk-worker-run-token": token,
55
+ },
56
+ });
57
+ debug("Response from worker: %s", response);
58
+ if (!response.ok) {
59
+ const errorText = await response.text();
60
+ console.error(`Error: worker-run script failed with status ${response.status}.`);
61
+ if (errorText) {
62
+ console.error("Response:", errorText);
111
63
  }
112
- debug("Fetching worker...");
113
- await fetch(`http://localhost:${address.port}/`);
114
- debug("Worker fetched successfully");
115
- }
116
- finally {
117
- debug("Closing server...");
118
- server.close();
119
- debug("Server closed");
64
+ process.exit(1);
120
65
  }
66
+ const responseText = await response.text();
67
+ debug("Response from worker: %s", responseText);
68
+ }
69
+ catch (e) {
70
+ console.error("rwsdk: Error running script:\n\n%s", e.message);
71
+ process.exit(1);
121
72
  }
122
73
  finally {
123
- debug("Closing inspector servers...");
124
- debug("Temporary files cleaned up");
74
+ await cleanup();
125
75
  }
126
- // todo(justinvdm, 01 Apr 2025): Investigate what handles are remaining open
127
- process.exit(0);
128
76
  };
129
- if (import.meta.url === new URL(process.argv[1], import.meta.url).href) {
130
- runWorkerScript(process.argv[2]);
131
- }
77
+ main();