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.
- package/dist/lib/e2e/testHarness.d.mts +6 -0
- package/dist/lib/e2e/testHarness.mjs +19 -0
- package/dist/llms/rules/middleware.d.ts +1 -1
- package/dist/llms/rules/middleware.js +4 -4
- package/dist/runtime/entries/worker.d.ts +0 -1
- package/dist/runtime/entries/worker.js +0 -1
- package/dist/runtime/lib/auth/session.d.ts +2 -2
- package/dist/runtime/lib/auth/session.js +4 -4
- package/dist/runtime/lib/router.test.js +0 -1
- package/dist/runtime/register/worker.js +8 -1
- package/dist/runtime/requestInfo/types.d.ts +0 -2
- package/dist/runtime/requestInfo/worker.js +1 -9
- package/dist/runtime/worker.js +0 -8
- package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
- package/dist/vite/runDirectivesScan.d.mts +2 -1
- package/dist/vite/runDirectivesScan.mjs +39 -7
- package/package.json +2 -1
- package/dist/runtime/imports/resolveSSRValue.d.ts +0 -1
- package/dist/runtime/imports/resolveSSRValue.js +0 -8
|
@@ -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,
|
|
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,
|
|
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
|
|
|
@@ -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: (
|
|
38
|
+
save: (responseHeaders: Headers, sessionInputData: SessionInputData, { maxAge }?: {
|
|
39
39
|
maxAge?: number | true;
|
|
40
40
|
}) => Promise<void>;
|
|
41
|
-
remove: (request: Request,
|
|
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 (
|
|
92
|
+
const save = async (responseHeaders, sessionInputData, { maxAge } = {}) => {
|
|
93
93
|
const sessionId = await generateSessionId({ secretKey });
|
|
94
94
|
await set(sessionId, sessionInputData);
|
|
95
|
-
|
|
95
|
+
responseHeaders.set("Set-Cookie", createCookie({ name: cookieName, sessionId, maxAge }));
|
|
96
96
|
};
|
|
97
|
-
const remove = async (request,
|
|
97
|
+
const remove = async (request, responseHeaders) => {
|
|
98
98
|
const sessionId = getSessionIdFromCookie(request);
|
|
99
99
|
if (sessionId) {
|
|
100
100
|
await unset(sessionId);
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
responseHeaders.set("Set-Cookie", createCookie({ name: cookieName, sessionId: "", maxAge: 0 }));
|
|
103
103
|
};
|
|
104
104
|
return {
|
|
105
105
|
load,
|
|
@@ -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
|
-
|
|
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,
|
package/dist/runtime/worker.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
};
|