rwsdk 1.0.0-alpha.11 → 1.0.0-alpha.13

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.
@@ -124,3 +124,9 @@ export declare namespace testDevAndDeploy {
124
124
  * This should be used before any user interaction is simulated.
125
125
  */
126
126
  export declare function waitForHydration(page: Page): Promise<void>;
127
+ export declare function trackPageErrors(page: Page): {
128
+ get: () => {
129
+ consoleErrors: string[];
130
+ failedRequests: string[];
131
+ };
132
+ };
@@ -416,3 +416,22 @@ export async function waitForHydration(page) {
416
416
  // This is a pragmatic approach to ensure React has mounted.
417
417
  await new Promise((resolve) => setTimeout(resolve, HYDRATION_TIMEOUT));
418
418
  }
419
+ export function trackPageErrors(page) {
420
+ const consoleErrors = [];
421
+ const failedRequests = [];
422
+ page.on("requestfailed", (request) => {
423
+ failedRequests.push(`${request.url()} | ${request.failure()?.errorText}`);
424
+ });
425
+ page.on("console", (msg) => {
426
+ if (msg.type() === "error") {
427
+ consoleErrors.push(msg.text());
428
+ }
429
+ });
430
+ return {
431
+ get: () => ({
432
+ // context(justinvdm, 25 Sep 2025): Filter out irrelevant 404s (e.g. favicon)
433
+ consoleErrors: consoleErrors.filter((e) => !e.includes("404")),
434
+ failedRequests,
435
+ }),
436
+ };
437
+ }
@@ -1 +1 @@
1
- export declare const middleware = "\n\n# RedwoodSDK: Middleware\n\nYou're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK middleware** that adhere to the following best practices:\n\n## Guidelines\n\n1. Create focused, single-responsibility middleware functions\n2. Organize middleware in dedicated files (e.g., `middleware.ts`, `middleware.tsx`)\n3. Use typed parameters and return values\n4. Include clear error handling and logging\n5. Follow the principle of least privilege\n6. Implement proper security headers and CORS policies\n7. Optimize for performance with caching strategies\n\n## What is Middleware?\n\nMiddleware functions in RedwoodSDK are functions that run on every request before your route handlers. They can:\n\n- Add security headers\n- Handle CORS\n- Implement caching strategies\n- Add request/response logging\n- Transform request/response data\n- Implement rate limiting\n- Add performance monitoring\n- Handle error boundaries\n- Setup sessions\n- Authenticate users\n\n## Example Templates\n\n### Basic Middleware Structure\n\n```tsx\nexport default defineApp([\n setCommonHeaders(),\n async ({ ctx, request, headers }) => {\n await setupDb(env);\n setupSessionStore(env);\n try {\n // Grab the session's data.\n ctx.session = await sessions.load(request);\n } catch (error) {\n if (error instanceof ErrorResponse && error.code === 401) {\n await sessions.remove(request, headers);\n headers.set(\"Location\", \"/user/login\");\n\n return new Response(null, {\n status: 302,\n headers,\n });\n }\n\n throw error;\n }\n\n // Populate the ctx with the user's data\n if (ctx.session?.userId) {\n ctx.user = await db.user.findUnique({\n where: {\n id: ctx.session.userId,\n },\n });\n }\n },\n // Route handlers\n]);\n```\n";
1
+ export declare const middleware = "\n\n# RedwoodSDK: Middleware\n\nYou're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK middleware** that adhere to the following best practices:\n\n## Guidelines\n\n1. Create focused, single-responsibility middleware functions\n2. Organize middleware in dedicated files (e.g., `middleware.ts`, `middleware.tsx`)\n3. Use typed parameters and return values\n4. Include clear error handling and logging\n5. Follow the principle of least privilege\n6. Implement proper security headers and CORS policies\n7. Optimize for performance with caching strategies\n\n## What is Middleware?\n\nMiddleware functions in RedwoodSDK are functions that run on every request before your route handlers. They can:\n\n- Add security headers\n- Handle CORS\n- Implement caching strategies\n- Add request/response logging\n- Transform request/response data\n- Implement rate limiting\n- Add performance monitoring\n- Handle error boundaries\n- Setup sessions\n- Authenticate users\n\n## Example Templates\n\n### Basic Middleware Structure\n\n```tsx\nexport default defineApp([\n setCommonHeaders(),\n async ({ ctx, request, response }) => {\n await setupDb(env);\n setupSessionStore(env);\n try {\n // Grab the session's data.\n ctx.session = await sessions.load(request);\n } catch (error) {\n if (error instanceof ErrorResponse && error.code === 401) {\n await sessions.remove(request, response.headers);\n response.headers.set(\"Location\", \"/user/login\");\n\n return new Response(null, {\n status: 302,\n headers: response.headers,\n });\n }\n\n throw error;\n }\n\n // Populate the ctx with the user's data\n if (ctx.session?.userId) {\n ctx.user = await db.user.findUnique({\n where: {\n id: ctx.session.userId,\n },\n });\n }\n },\n // Route handlers\n]);\n```\n";
@@ -36,7 +36,7 @@ Middleware functions in RedwoodSDK are functions that run on every request befor
36
36
  \`\`\`tsx
37
37
  export default defineApp([
38
38
  setCommonHeaders(),
39
- async ({ ctx, request, headers }) => {
39
+ async ({ ctx, request, response }) => {
40
40
  await setupDb(env);
41
41
  setupSessionStore(env);
42
42
  try {
@@ -44,12 +44,12 @@ export default defineApp([
44
44
  ctx.session = await sessions.load(request);
45
45
  } catch (error) {
46
46
  if (error instanceof ErrorResponse && error.code === 401) {
47
- await sessions.remove(request, headers);
48
- headers.set("Location", "/user/login");
47
+ await sessions.remove(request, response.headers);
48
+ response.headers.set("Location", "/user/login");
49
49
 
50
50
  return new Response(null, {
51
51
  status: 302,
52
- headers,
52
+ headers: response.headers,
53
53
  });
54
54
  }
55
55
 
@@ -8,4 +8,3 @@ export * from "../requestInfo/types";
8
8
  export * from "../requestInfo/worker";
9
9
  export * from "../render/renderToString";
10
10
  export * from "../render/renderToStream";
11
- export * from "../imports/resolveSSRValue";
@@ -8,4 +8,3 @@ export * from "../requestInfo/types";
8
8
  export * from "../requestInfo/worker";
9
9
  export * from "../render/renderToString";
10
10
  export * from "../render/renderToStream";
11
- export * from "../imports/resolveSSRValue";
@@ -35,10 +35,10 @@ export declare const defineSessionStore: <Session, SessionInputData>({ cookieNam
35
35
  unset: (sessionId: string) => Promise<void>;
36
36
  }) => {
37
37
  load: (request: Request) => Promise<Session | null>;
38
- save: (headers: Headers, sessionInputData: SessionInputData, { maxAge }?: {
38
+ save: (responseHeaders: Headers, sessionInputData: SessionInputData, { maxAge }?: {
39
39
  maxAge?: number | true;
40
40
  }) => Promise<void>;
41
- remove: (request: Request, headers: Headers) => Promise<void>;
41
+ remove: (request: Request, responseHeaders: Headers) => Promise<void>;
42
42
  };
43
43
  type SessionStoreFromDurableObject<SessionDurableObject> = SessionDurableObject extends DurableObjectMethods<infer Session, infer SessionInputData> ? SessionStore<Session, SessionInputData> : never;
44
44
  export declare const defineDurableSession: <SessionDurableObject extends DurableObjectMethods<any, any>>({ cookieName, createCookie, secretKey, sessionDurableObject, }: {
@@ -89,17 +89,17 @@ export const defineSessionStore = ({ cookieName = "session_id", createCookie = c
89
89
  throw new ErrorResponse(401, "Invalid session id");
90
90
  }
91
91
  };
92
- const save = async (headers, sessionInputData, { maxAge } = {}) => {
92
+ const save = async (responseHeaders, sessionInputData, { maxAge } = {}) => {
93
93
  const sessionId = await generateSessionId({ secretKey });
94
94
  await set(sessionId, sessionInputData);
95
- headers.set("Set-Cookie", createCookie({ name: cookieName, sessionId, maxAge }));
95
+ responseHeaders.set("Set-Cookie", createCookie({ name: cookieName, sessionId, maxAge }));
96
96
  };
97
- const remove = async (request, headers) => {
97
+ const remove = async (request, responseHeaders) => {
98
98
  const sessionId = getSessionIdFromCookie(request);
99
99
  if (sessionId) {
100
100
  await unset(sessionId);
101
101
  }
102
- headers.set("Set-Cookie", createCookie({ name: cookieName, sessionId: "", maxAge: 0 }));
102
+ responseHeaders.set("Set-Cookie", createCookie({ name: cookieName, sessionId: "", maxAge: 0 }));
103
103
  };
104
104
  return {
105
105
  load,
@@ -64,7 +64,6 @@ describe("defineRoutes - Request Handling Behavior", () => {
64
64
  request: new Request("http://localhost:3000/"),
65
65
  params: {},
66
66
  ctx: {},
67
- headers: new Headers(),
68
67
  rw: {
69
68
  nonce: "test-nonce",
70
69
  Document: () => React.createElement("html"),
@@ -9,6 +9,7 @@ export function registerServerReference(action, id, name) {
9
9
  // Note: We no longer need to register in a Map since we use virtual lookup
10
10
  return baseRegisterServerReference(action, id, name);
11
11
  }
12
+ const isComponent = (target) => isValidElementType(target) && target?.toString().includes("jsx");
12
13
  export function registerClientReference(ssrModule, id, exportName) {
13
14
  const target = ssrModule[exportName] ?? {};
14
15
  if (isValidElementType(target)) {
@@ -31,7 +32,13 @@ export function registerClientReference(ssrModule, id, exportName) {
31
32
  }
32
33
  finalDescriptors.$$async = { value: true };
33
34
  finalDescriptors.$$isClientReference = { value: true };
34
- return Object.defineProperties(() => null, finalDescriptors);
35
+ // context(justinvdm, 25 Sep 2025): We create a wrapper function to avoid
36
+ // getting the SSR component's property descriptors - otherwise
37
+ // this will take precedence over the client reference descriptors
38
+ const fn = typeof target === "function"
39
+ ? (...args) => target(...args)
40
+ : () => null;
41
+ return Object.defineProperties(fn, finalDescriptors);
35
42
  }
36
43
  // For non-components, return the target object directly for use in SSR.
37
44
  return target;
@@ -5,8 +5,6 @@ export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
5
5
  request: Request;
6
6
  params: Params;
7
7
  ctx: AppContext;
8
- /** @deprecated: Use `response.headers` instead */
9
- headers: Headers;
10
8
  rw: RwContext;
11
9
  cf: ExecutionContext;
12
10
  response: ResponseInit & {
@@ -2,15 +2,7 @@ import { AsyncLocalStorage } from "async_hooks";
2
2
  const requestInfoDeferred = Promise.withResolvers();
3
3
  const requestInfoStore = new AsyncLocalStorage();
4
4
  const requestInfoBase = {};
5
- const REQUEST_INFO_KEYS = [
6
- "request",
7
- "params",
8
- "ctx",
9
- "headers",
10
- "rw",
11
- "cf",
12
- "response",
13
- ];
5
+ const REQUEST_INFO_KEYS = ["request", "params", "ctx", "rw", "cf", "response"];
14
6
  REQUEST_INFO_KEYS.forEach((key) => {
15
7
  Object.defineProperty(requestInfoBase, key, {
16
8
  enumerable: true,
@@ -36,7 +36,6 @@ export const defineApp = (routes) => {
36
36
  const isRSCRequest = url.searchParams.has("__rsc") ||
37
37
  request.headers.get("accept")?.includes("text/x-component");
38
38
  const isAction = url.searchParams.has("__rsc_action_id");
39
- const userHeaders = new Headers();
40
39
  const rw = {
41
40
  Document: DefaultDocument,
42
41
  nonce: generateNonce(),
@@ -52,7 +51,6 @@ export const defineApp = (routes) => {
52
51
  };
53
52
  const outerRequestInfo = {
54
53
  request,
55
- headers: userHeaders,
56
54
  cf,
57
55
  params: {},
58
56
  ctx: {},
@@ -144,12 +142,6 @@ export const defineApp = (routes) => {
144
142
  // context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
145
143
  // we need to return a mutable response object.
146
144
  const mutableResponse = new Response(response.body, response);
147
- // Merge user headers from the legacy headers object
148
- for (const [key, value] of userHeaders.entries()) {
149
- if (!response.headers.has(key)) {
150
- mutableResponse.headers.set(key, value);
151
- }
152
- }
153
145
  // Merge headers from user response init (these take precedence)
154
146
  if (userResponseInit.headers) {
155
147
  const userResponseHeaders = new Headers(userResponseInit.headers);
@@ -84,7 +84,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
84
84
  env.optimizeDeps.include ??= [];
85
85
  const entries = (env.optimizeDeps.entries = castArray(env.optimizeDeps.entries ?? []));
86
86
  env.optimizeDeps.include.push(VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH);
87
- if (envName === "client") {
87
+ if (envName === "client" || envName === "ssr") {
88
88
  entries.push(APP_CLIENT_BARREL_PATH);
89
89
  }
90
90
  else if (envName === "worker") {
@@ -17,11 +17,12 @@ export declare function classifyModule({ contents, inheritedEnv, }: {
17
17
  isClient: boolean;
18
18
  isServer: boolean;
19
19
  };
20
+ export type EsbuildLoader = "js" | "jsx" | "ts" | "tsx" | "default";
20
21
  export declare const runDirectivesScan: ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }: {
21
22
  rootConfig: ResolvedConfig;
22
23
  environments: Record<string, Environment>;
23
24
  clientFiles: Set<string>;
24
25
  serverFiles: Set<string>;
25
- entries: string[];
26
+ entries?: string[];
26
27
  }) => Promise<void>;
27
28
  export {};
@@ -7,6 +7,7 @@ import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
7
7
  import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
8
8
  import { externalModules } from "./constants.mjs";
9
9
  import { createViteAwareResolver } from "./createViteAwareResolver.mjs";
10
+ import { compile } from "@mdx-js/mdx";
10
11
  const log = debug("rwsdk:vite:run-directives-scan");
11
12
  // Copied from Vite's source code.
12
13
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/shared/utils.ts
@@ -50,7 +51,7 @@ export function classifyModule({ contents, inheritedEnv, }) {
50
51
  return { moduleEnv, isClient, isServer };
51
52
  }
52
53
  export const runDirectivesScan = async ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }) => {
53
- console.log("\n🔍 Scanning for 'use client' and 'use server' directives...");
54
+ deferredLog("\n (rwsdk) Scanning for 'use client' and 'use server' directives...");
54
55
  // Set environment variable to indicate scanning is in progress
55
56
  process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE = "true";
56
57
  try {
@@ -94,7 +95,7 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
94
95
  setup(build) {
95
96
  // Match Vite's behavior by externalizing assets and special queries.
96
97
  // This prevents esbuild from trying to bundle them, which would fail.
97
- const scriptFilter = /\.(c|m)?[jt]sx?$/;
98
+ const scriptFilter = /\.(c|m)?[jt]sx?$|\.mdx$/;
98
99
  const specialQueryFilter = /[?&](?:url|raw|worker|sharedworker|inline)\b/;
99
100
  // This regex is used to identify if a path has any file extension.
100
101
  const hasExtensionRegex = /\.[^/]+$/;
@@ -162,7 +163,7 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
162
163
  log("Marking as external:", args.path, "resolved to:", resolvedPath);
163
164
  return { external: true };
164
165
  });
165
- build.onLoad({ filter: /\.(m|c)?[jt]sx?$/ }, async (args) => {
166
+ build.onLoad({ filter: /\.(m|c)?[jt]sx?$|\.mdx$/ }, async (args) => {
166
167
  log("onLoad called for:", args.path);
167
168
  if (!args.path.startsWith("/") ||
168
169
  args.path.includes("virtual:") ||
@@ -175,10 +176,10 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
175
176
  return null;
176
177
  }
177
178
  try {
178
- const contents = await readFileWithCache(args.path);
179
+ const originalContents = await readFileWithCache(args.path);
179
180
  const inheritedEnv = args.pluginData?.inheritedEnv || "worker";
180
181
  const { moduleEnv, isClient, isServer } = classifyModule({
181
- contents,
182
+ contents: originalContents,
182
183
  inheritedEnv,
183
184
  });
184
185
  // Store the definitive environment for this module, so it can be used when it becomes an importer.
@@ -194,7 +195,33 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
194
195
  log("Discovered 'use server' in:", realPath);
195
196
  serverFiles.add(normalizeModulePath(realPath, rootConfig.root));
196
197
  }
197
- return { contents, loader: "default" };
198
+ let code;
199
+ let loader;
200
+ if (args.path.endsWith(".mdx")) {
201
+ const result = await compile(originalContents, {
202
+ jsx: true,
203
+ jsxImportSource: "react",
204
+ });
205
+ code = String(result.value);
206
+ loader = "tsx";
207
+ }
208
+ else if (/\.(m|c)?tsx$/.test(args.path)) {
209
+ code = originalContents;
210
+ loader = "tsx";
211
+ }
212
+ else if (/\.(m|c)?ts$/.test(args.path)) {
213
+ code = originalContents;
214
+ loader = "ts";
215
+ }
216
+ else if (/\.(m|c)?jsx$/.test(args.path)) {
217
+ code = originalContents;
218
+ loader = "jsx";
219
+ }
220
+ else {
221
+ code = originalContents;
222
+ loader = "js";
223
+ }
224
+ return { contents: code, loader };
198
225
  }
199
226
  catch (e) {
200
227
  log("Could not read file during scan, skipping:", args.path, e);
@@ -220,8 +247,13 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
220
247
  finally {
221
248
  // Always clear the scanning flag when done
222
249
  delete process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE;
223
- console.log(" Scan complete.");
250
+ deferredLog(" (rwsdk) Done scanning for 'use client' and 'use server' directives.");
224
251
  process.env.VERBOSE &&
225
252
  log("Client/server files after scanning: client=%O, server=%O", Array.from(clientFiles), Array.from(serverFiles));
226
253
  }
227
254
  };
255
+ const deferredLog = (message) => {
256
+ setTimeout(() => {
257
+ console.log(message);
258
+ }, 500);
259
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-alpha.11",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -134,6 +134,7 @@
134
134
  "dependencies": {
135
135
  "@ast-grep/napi": "^0.38.5",
136
136
  "@cloudflare/workers-types": "^4.20250407.0",
137
+ "@mdx-js/mdx": "^3.1.1",
137
138
  "@puppeteer/browsers": "^2.8.0",
138
139
  "@types/fs-extra": "^11.0.4",
139
140
  "@types/react": "^19.1.2",
@@ -1 +0,0 @@
1
- export declare const resolveSSRValue: <Value>(clientReference: Value) => Promise<Value>;
@@ -1,8 +0,0 @@
1
- import { ssrGetModuleExport } from "rwsdk/__ssr_bridge";
2
- export const resolveSSRValue = (clientReference) => {
3
- const id = clientReference.__rwsdk_clientReferenceId;
4
- if (!id) {
5
- throw new Error("RWSDK: Client reference is not a client reference");
6
- }
7
- return ssrGetModuleExport(id);
8
- };