rwsdk 0.3.7 → 1.0.0-alpha.0

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.
@@ -3,7 +3,9 @@ export declare const SRC_DIR: string;
3
3
  export declare const DIST_DIR: string;
4
4
  export declare const VITE_DIR: string;
5
5
  export declare const INTERMEDIATES_OUTPUT_DIR: string;
6
- export declare const CLIENT_BARREL_PATH: string;
7
- export declare const SERVER_BARREL_PATH: string;
6
+ export declare const VENDOR_CLIENT_BARREL_PATH: string;
7
+ export declare const VENDOR_SERVER_BARREL_PATH: string;
8
+ export declare const VENDOR_CLIENT_BARREL_EXPORT_PATH = "rwsdk/__vendor_client_barrel";
9
+ export declare const VENDOR_SERVER_BARREL_EXPORT_PATH = "rwsdk/__vendor_server_barrel";
8
10
  export declare const INTERMEDIATE_SSR_BRIDGE_PATH: string;
9
11
  export declare const CLIENT_MANIFEST_RELATIVE_PATH: string;
@@ -1,11 +1,14 @@
1
1
  import { resolve } from "node:path";
2
+ import path from "node:path";
2
3
  const __dirname = new URL(".", import.meta.url).pathname;
3
4
  export const ROOT_DIR = resolve(__dirname, "..", "..");
4
5
  export const SRC_DIR = resolve(ROOT_DIR, "src");
5
6
  export const DIST_DIR = resolve(ROOT_DIR, "dist");
6
7
  export const VITE_DIR = resolve(ROOT_DIR, "src", "vite");
7
8
  export const INTERMEDIATES_OUTPUT_DIR = resolve(DIST_DIR, "__intermediate_builds");
8
- export const CLIENT_BARREL_PATH = resolve(INTERMEDIATES_OUTPUT_DIR, "rwsdk-client-barrel.js");
9
- export const SERVER_BARREL_PATH = resolve(INTERMEDIATES_OUTPUT_DIR, "rwsdk-server-barrel.js");
9
+ export const VENDOR_CLIENT_BARREL_PATH = path.resolve(INTERMEDIATES_OUTPUT_DIR, "rwsdk-vendor-client-barrel.js");
10
+ export const VENDOR_SERVER_BARREL_PATH = path.resolve(INTERMEDIATES_OUTPUT_DIR, "rwsdk-vendor-server-barrel.js");
11
+ export const VENDOR_CLIENT_BARREL_EXPORT_PATH = "rwsdk/__vendor_client_barrel";
12
+ export const VENDOR_SERVER_BARREL_EXPORT_PATH = "rwsdk/__vendor_server_barrel";
10
13
  export const INTERMEDIATE_SSR_BRIDGE_PATH = resolve(INTERMEDIATES_OUTPUT_DIR, "ssr", "ssr_bridge.js");
11
14
  export const CLIENT_MANIFEST_RELATIVE_PATH = resolve("dist", "client", ".vite", "manifest.json");
@@ -82,14 +82,25 @@ export async function runDevServer(cwd) {
82
82
  env.NO_COLOR = "1";
83
83
  env.FORCE_COLOR = "0";
84
84
  }
85
+ // Map package manager names to actual commands
86
+ const getPackageManagerCommand = (pm) => {
87
+ switch (pm) {
88
+ case "yarn-classic":
89
+ return "yarn";
90
+ default:
91
+ return pm;
92
+ }
93
+ };
94
+ const pm = getPackageManagerCommand(state.options.packageManager || "npm");
85
95
  // Use the provided cwd if available
86
96
  devProcess = $({
87
- stdio: ["inherit", "pipe", "pipe"], // Pipe stderr again to check both streams
88
- detached: true,
97
+ all: true,
98
+ detached: false, // Keep attached so we can access streams
89
99
  cleanup: false, // Don't auto-kill on exit
90
100
  cwd: cwd || process.cwd(), // Use provided directory or current directory
91
101
  env, // Pass the updated environment variables
92
- }) `npm run dev`;
102
+ stdio: 'pipe', // Ensure streams are piped
103
+ }) `${pm} run dev`;
93
104
  devProcess.catch((error) => {
94
105
  if (!isErrorExpected) {
95
106
  // Use fail() directly here to properly handle errors from the dev process
@@ -99,44 +110,67 @@ export async function runDevServer(cwd) {
99
110
  log("Development server process spawned in directory: %s", cwd || process.cwd());
100
111
  // Store chunks to parse the URL
101
112
  let url = "";
102
- // Listen for stdout to get the URL
103
- devProcess.stdout?.on("data", (data) => {
113
+ let allOutput = "";
114
+ // Listen for all output to get the URL
115
+ const handleOutput = (data, source) => {
104
116
  const output = data.toString();
105
117
  console.log(output);
106
- // Try to extract the URL from the server output with a more flexible regex
107
- // Allow for variable amounts of whitespace between "Local:" and the URL
108
- // And handle ANSI color codes by using a more robust pattern
109
- const localMatch = output.match(/Local:.*?(http:\/\/localhost:\d+)/);
110
- if (localMatch && localMatch[1] && !url) {
111
- url = localMatch[1];
112
- log("Found development server URL: %s", url);
113
- }
114
- else if (output.includes("Local:") &&
115
- output.includes("http://localhost:")) {
116
- // Log near-match for debugging
117
- log("Found potential URL pattern but regex didn't match. Content: %s", output);
118
- // Try an alternative, more general pattern that's more resilient to ANSI codes
119
- const altMatch = output.match(/localhost:(\d+)/i);
120
- if (altMatch && altMatch[1] && !url) {
121
- url = `http://localhost:${altMatch[1]}`;
122
- log("Found development server URL with alternative pattern: %s", url);
118
+ allOutput += output; // Accumulate all output
119
+ log("Received output from %s: %s", source, output.replace(/\n/g, "\\n"));
120
+ if (!url) {
121
+ // Multiple patterns to catch different package manager outputs
122
+ const patterns = [
123
+ // Standard Vite output: "Local: http://localhost:5173/"
124
+ /Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
125
+ // Alternative Vite output: "➜ Local: http://localhost:5173/"
126
+ /[➜→]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
127
+ // Unicode-safe arrow pattern
128
+ /[\u27A1\u2192\u279C]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
129
+ // Direct URL pattern: "http://localhost:5173"
130
+ /(https?:\/\/localhost:\d+)/i,
131
+ // Port-only pattern: "localhost:5173"
132
+ /localhost:(\d+)/i,
133
+ // Server ready messages
134
+ /server.*ready.*localhost:(\d+)/i,
135
+ /dev server.*localhost:(\d+)/i,
136
+ ];
137
+ for (const pattern of patterns) {
138
+ const match = output.match(pattern);
139
+ log("Testing pattern %s against output: %s", pattern.source, output.replace(/\n/g, "\\n"));
140
+ if (match) {
141
+ log("Pattern matched: %s, groups: %o", pattern.source, match);
142
+ if (match[1] && match[1].startsWith("http")) {
143
+ url = match[1];
144
+ log("Found development server URL with pattern %s: %s", pattern.source, url);
145
+ break;
146
+ }
147
+ else if (match[1] && /^\d+$/.test(match[1])) {
148
+ url = `http://localhost:${match[1]}`;
149
+ log("Found development server URL with port pattern %s: %s", pattern.source, url);
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ // Log potential matches for debugging
155
+ if (!url &&
156
+ (output.includes("localhost") ||
157
+ output.includes("Local") ||
158
+ output.includes("server"))) {
159
+ log("Potential URL pattern found but not matched: %s", output.trim());
123
160
  }
124
161
  }
125
- });
126
- // Also listen for stderr to check for URL patterns there as well
127
- devProcess.stderr?.on("data", (data) => {
128
- const output = data.toString();
129
- console.error(output); // Output error messages to console
130
- // Check if we already found a URL
131
- if (url)
132
- return;
133
- // Check for localhost URLs in stderr using the same resilient patterns as stdout
134
- const urlMatch = output.match(/localhost:(\d+)/i);
135
- if (urlMatch && urlMatch[1]) {
136
- url = `http://localhost:${urlMatch[1]}`;
137
- log("Found development server URL in stderr: %s", url);
138
- }
139
- });
162
+ };
163
+ // Listen to all possible output streams
164
+ log("Setting up stream listeners. Available streams: all=%s, stdout=%s, stderr=%s", !!devProcess.all, !!devProcess.stdout, !!devProcess.stderr);
165
+ devProcess.all?.on("data", (data) => handleOutput(data, "all"));
166
+ devProcess.stdout?.on("data", (data) => handleOutput(data, "stdout"));
167
+ devProcess.stderr?.on("data", (data) => handleOutput(data, "stderr"));
168
+ // Also try listening to the raw process output
169
+ if (devProcess.child) {
170
+ log("Setting up child process stream listeners");
171
+ devProcess.child.stdout?.on("data", (data) => handleOutput(data, "child.stdout"));
172
+ devProcess.child.stderr?.on("data", (data) => handleOutput(data, "child.stderr"));
173
+ }
140
174
  // Wait for URL with timeout
141
175
  const waitForUrl = async () => {
142
176
  const start = Date.now();
@@ -145,14 +179,40 @@ export async function runDevServer(cwd) {
145
179
  if (url) {
146
180
  return url;
147
181
  }
182
+ // Fallback: check accumulated output if stream listeners aren't working
183
+ if (!url && allOutput) {
184
+ log("Checking accumulated output for URL patterns: %s", allOutput.replace(/\n/g, "\\n"));
185
+ const patterns = [
186
+ /Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
187
+ /[➜→]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
188
+ /[\u27A1\u2192\u279C]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
189
+ /(https?:\/\/localhost:\d+)/i,
190
+ /localhost:(\d+)/i,
191
+ ];
192
+ for (const pattern of patterns) {
193
+ const match = allOutput.match(pattern);
194
+ if (match) {
195
+ if (match[1] && match[1].startsWith("http")) {
196
+ url = match[1];
197
+ log("Found URL in accumulated output with pattern %s: %s", pattern.source, url);
198
+ return url;
199
+ }
200
+ else if (match[1] && /^\d+$/.test(match[1])) {
201
+ url = `http://localhost:${match[1]}`;
202
+ log("Found URL in accumulated output with port pattern %s: %s", pattern.source, url);
203
+ return url;
204
+ }
205
+ }
206
+ }
207
+ }
148
208
  // Check if the process is still running
149
209
  if (devProcess.exitCode !== null) {
150
- log("ERROR: Development server process exited with code %d", devProcess.exitCode);
210
+ log("ERROR: Development server process exited with code %d. Final output: %s", devProcess.exitCode, allOutput);
151
211
  throw new Error(`Development server process exited with code ${devProcess.exitCode}`);
152
212
  }
153
213
  await setTimeout(500); // Check every 500ms
154
214
  }
155
- log("ERROR: Timed out waiting for dev server URL");
215
+ log("ERROR: Timed out waiting for dev server URL. Final accumulated output: %s", allOutput);
156
216
  throw new Error("Timed out waiting for dev server URL");
157
217
  };
158
218
  // Wait for the URL
@@ -1,5 +1,5 @@
1
1
  import tmp from "tmp-promise";
2
- import { SmokeTestOptions, TestResources } from "./types.mjs";
2
+ import { SmokeTestOptions, TestResources, PackageManager } from "./types.mjs";
3
3
  /**
4
4
  * Sets up the test environment, preparing any resources needed for testing
5
5
  */
@@ -7,7 +7,7 @@ export declare function setupTestEnvironment(options?: SmokeTestOptions): Promis
7
7
  /**
8
8
  * Copy project to a temporary directory with a unique name
9
9
  */
10
- export declare function copyProjectToTempDir(projectDir: string, sync: boolean | undefined, resourceUniqueKey: string): Promise<{
10
+ export declare function copyProjectToTempDir(projectDir: string, sync: boolean | undefined, resourceUniqueKey: string, packageManager?: PackageManager): Promise<{
11
11
  tempDir: tmp.DirectoryResult;
12
12
  targetDir: string;
13
13
  workerName: string;
@@ -44,7 +44,8 @@ export async function setupTestEnvironment(options = {}) {
44
44
  if (options.projectDir) {
45
45
  log("Project directory specified: %s", options.projectDir);
46
46
  const { tempDir, targetDir, workerName } = await copyProjectToTempDir(options.projectDir, options.sync !== false, // default to true if undefined
47
- resourceUniqueKey);
47
+ resourceUniqueKey, // Pass in the existing resourceUniqueKey
48
+ options.packageManager);
48
49
  // Store cleanup function
49
50
  resources.tempDirCleanup = tempDir.cleanup;
50
51
  resources.workerName = workerName;
@@ -69,7 +70,7 @@ export async function setupTestEnvironment(options = {}) {
69
70
  /**
70
71
  * Copy project to a temporary directory with a unique name
71
72
  */
72
- export async function copyProjectToTempDir(projectDir, sync = true, resourceUniqueKey) {
73
+ export async function copyProjectToTempDir(projectDir, sync = true, resourceUniqueKey, packageManager) {
73
74
  log("Creating temporary directory for project");
74
75
  // Create a temporary directory
75
76
  const tempDir = await tmp.dir({ unsafeCleanup: true });
@@ -115,8 +116,14 @@ export async function copyProjectToTempDir(projectDir, sync = true, resourceUniq
115
116
  },
116
117
  });
117
118
  log("Project copy completed successfully");
119
+ // For yarn, create .yarnrc.yml to disable PnP and use node_modules
120
+ if (packageManager === "yarn" || packageManager === "yarn-classic") {
121
+ const yarnrcPath = join(targetDir, ".yarnrc.yml");
122
+ await fs.writeFile(yarnrcPath, "nodeLinker: node-modules\n");
123
+ log("Created .yarnrc.yml to disable PnP for yarn");
124
+ }
118
125
  // Install dependencies in the target directory
119
- await installDependencies(targetDir);
126
+ await installDependencies(targetDir, packageManager);
120
127
  // Sync SDK to the temp dir if requested
121
128
  if (sync) {
122
129
  console.log(`🔄 Syncing SDK to ${targetDir} after installing dependencies...`);
@@ -127,24 +134,31 @@ export async function copyProjectToTempDir(projectDir, sync = true, resourceUniq
127
134
  /**
128
135
  * Install project dependencies using pnpm
129
136
  */
130
- async function installDependencies(targetDir) {
131
- console.log(`📦 Installing project dependencies in ${targetDir}...`);
137
+ async function installDependencies(targetDir, packageManager = "pnpm") {
138
+ console.log(`📦 Installing project dependencies in ${targetDir} using ${packageManager}...`);
132
139
  try {
133
- // Run pnpm install in the target directory
134
- log("Running pnpm install");
135
- const result = await $({
140
+ const installCommand = {
141
+ pnpm: ["pnpm", "install"],
142
+ npm: ["npm", "install"],
143
+ yarn: ["yarn", "install", "--immutable"],
144
+ "yarn-classic": ["yarn", "install", "--immutable"],
145
+ }[packageManager];
146
+ // Run install command in the target directory
147
+ log(`Running ${installCommand.join(" ")}`);
148
+ const [command, ...args] = installCommand;
149
+ const result = await $(command, args, {
136
150
  cwd: targetDir,
137
151
  stdio: "pipe", // Capture output
138
- }) `pnpm install`;
152
+ });
139
153
  console.log("✅ Dependencies installed successfully");
140
154
  // Log installation details at debug level
141
155
  if (result.stdout) {
142
- log("pnpm install output: %s", result.stdout);
156
+ log(`${packageManager} install output: %s`, result.stdout);
143
157
  }
144
158
  }
145
159
  catch (error) {
146
160
  log("ERROR: Failed to install dependencies: %O", error);
147
161
  console.error(`❌ Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`);
148
- throw new Error(`Failed to install project dependencies. Please ensure the project can be installed with pnpm.`);
162
+ throw new Error(`Failed to install project dependencies. Please ensure the project can be installed with ${packageManager}.`);
149
163
  }
150
164
  }
@@ -1,4 +1,5 @@
1
1
  import { WriteStream } from "fs";
2
+ export type PackageManager = "pnpm" | "npm" | "yarn" | "yarn-classic";
2
3
  export interface SmokeTestResult {
3
4
  status: string;
4
5
  verificationPassed: boolean;
@@ -24,6 +25,7 @@ export interface SmokeTestOptions {
24
25
  realtime?: boolean;
25
26
  skipHmr?: boolean;
26
27
  skipStyleTests?: boolean;
28
+ packageManager?: PackageManager;
27
29
  }
28
30
  export interface TestResources {
29
31
  tempDirCleanup?: () => Promise<void>;
@@ -17,6 +17,7 @@ export type RwContext = {
17
17
  databases: Map<string, Kysely<any>>;
18
18
  scriptsToBeLoaded: Set<string>;
19
19
  pageRouteResolved: PromiseWithResolvers<void> | undefined;
20
+ actionResult?: unknown;
20
21
  };
21
22
  export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
22
23
  type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<Response>;
@@ -32,12 +33,13 @@ export type RouteDefinition<T extends RequestInfo = RequestInfo> = {
32
33
  export declare function matchPath<T extends RequestInfo = RequestInfo>(routePath: string, requestPath: string): T["params"] | null;
33
34
  export declare function defineRoutes<T extends RequestInfo = RequestInfo>(routes: Route<T>[]): {
34
35
  routes: Route<T>[];
35
- handle: ({ request, renderPage, getRequestInfo, onError, runWithRequestInfoOverrides, }: {
36
+ handle: ({ request, renderPage, getRequestInfo, onError, runWithRequestInfoOverrides, rscActionHandler, }: {
36
37
  request: Request;
37
38
  renderPage: (requestInfo: T, Page: React.FC, onError: (error: unknown) => void) => Promise<Response>;
38
39
  getRequestInfo: () => T;
39
40
  onError: (error: unknown) => void;
40
41
  runWithRequestInfoOverrides: <Result>(overrides: Partial<T>, fn: () => Promise<Result>) => Promise<Result>;
42
+ rscActionHandler: (request: Request) => Promise<unknown>;
41
43
  }) => Response | Promise<Response>;
42
44
  };
43
45
  export declare function route<T extends RequestInfo = RequestInfo>(path: string, handler: RouteHandler<T>): RouteDefinition<T>;
@@ -64,49 +64,15 @@ export function defineRoutes(routes) {
64
64
  const flattenedRoutes = flattenRoutes(routes);
65
65
  return {
66
66
  routes: flattenedRoutes,
67
- async handle({ request, renderPage, getRequestInfo, onError, runWithRequestInfoOverrides, }) {
67
+ async handle({ request, renderPage, getRequestInfo, onError, runWithRequestInfoOverrides, rscActionHandler, }) {
68
68
  const url = new URL(request.url);
69
69
  let path = url.pathname;
70
70
  // Must end with a trailing slash.
71
71
  if (path !== "/" && !path.endsWith("/")) {
72
72
  path = path + "/";
73
73
  }
74
- // Flow below; helpers are declared after the main flow for readability
75
- // 1) Global middlewares: run any middleware functions encountered (Response short-circuits, element renders)
76
- // 2) Route matching: skip non-matching route definitions; stop at first match
77
- // 3) Route-specific middlewares: run middlewares attached to the matched route
78
- // 4) Final component: render the matched route's final component (layouts apply only here)
79
- for (const route of flattenedRoutes) {
80
- // 1) Global middlewares (encountered before a match)
81
- if (typeof route === "function") {
82
- const result = await route(getRequestInfo());
83
- const handled = await handleMiddlewareResult(result);
84
- if (handled) {
85
- return handled;
86
- }
87
- continue;
88
- }
89
- // 2) Route matching (skip if not matched)
90
- const params = matchPath(route.path, path);
91
- if (!params) {
92
- continue;
93
- }
94
- // Found a match: 3) route middlewares, then 4) final component, then stop
95
- return await runWithRequestInfoOverrides({ params }, async () => {
96
- const { routeMiddlewares, componentHandler } = parseHandlers(route.handler);
97
- // 3) Route-specific middlewares
98
- const mwHandled = await handleRouteMiddlewares(routeMiddlewares);
99
- if (mwHandled) {
100
- return mwHandled;
101
- }
102
- // 4) Final component (always last item)
103
- return await handleRouteComponent(componentHandler, route.layouts || []);
104
- });
105
- }
106
- // No route matched and no middleware handled the request
107
- // todo(peterp, 2025-01-28): Allow the user to define their own "not found" route.
108
- return new Response("Not Found", { status: 404 });
109
74
  // --- Helpers ---
75
+ // (Hoisted for readability)
110
76
  function parseHandlers(handler) {
111
77
  const handlers = Array.isArray(handler) ? handler : [handler];
112
78
  const routeMiddlewares = handlers.slice(0, Math.max(handlers.length - 1, 0));
@@ -130,38 +96,60 @@ export function defineRoutes(routes) {
130
96
  }
131
97
  return undefined;
132
98
  }
133
- // Note: We no longer have separate global pass or match-only pass;
134
- // the outer single pass above handles both behaviors correctly.
135
- async function handleRouteMiddlewares(mws) {
136
- for (const mw of mws) {
137
- const result = await mw(getRequestInfo());
138
- const handled = await handleMiddlewareResult(result);
139
- if (handled)
140
- return handled;
99
+ // --- Main flow ---
100
+ const globalMiddlewares = flattenedRoutes.filter((route) => typeof route === "function");
101
+ const routeDefinitions = flattenedRoutes.filter((route) => typeof route !== "function");
102
+ // 1. Run global middlewares
103
+ for (const middleware of globalMiddlewares) {
104
+ const result = await middleware(getRequestInfo());
105
+ const handled = await handleMiddlewareResult(result);
106
+ if (handled) {
107
+ return handled;
141
108
  }
142
- return undefined;
143
109
  }
144
- async function handleRouteComponent(component, layouts) {
145
- if (isRouteComponent(component)) {
146
- const requestInfo = getRequestInfo();
147
- const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(component), layouts, requestInfo);
148
- if (!isClientReference(component)) {
149
- // context(justinvdm, 31 Jul 2025): We now know we're dealing with a page route,
150
- // so we create a deferred so that we can signal when we're done determining whether
151
- // we're returning a response or a react element
152
- requestInfo.rw.pageRouteResolved = Promise.withResolvers();
153
- }
154
- return await renderPage(requestInfo, WrappedComponent, onError);
110
+ // 2. Handle RSC actions
111
+ if (url.searchParams.has("__rsc_action_id")) {
112
+ getRequestInfo().rw.actionResult = await rscActionHandler(request);
113
+ }
114
+ // 3. Match and handle routes
115
+ for (const route of routeDefinitions) {
116
+ const params = matchPath(route.path, path);
117
+ if (!params) {
118
+ continue;
155
119
  }
156
- // If the last handler is not a component, handle as middleware result (no layouts)
157
- const tailResult = await component(getRequestInfo());
158
- const handledTail = await handleMiddlewareResult(tailResult);
159
- if (handledTail)
160
- return handledTail;
161
- return new Response("Response not returned from route handler", {
162
- status: 500,
120
+ // Found a match: run route-specific middlewares, then the final component
121
+ return await runWithRequestInfoOverrides({ params }, async () => {
122
+ const { routeMiddlewares, componentHandler } = parseHandlers(route.handler);
123
+ // 3a. Route-specific middlewares
124
+ for (const mw of routeMiddlewares) {
125
+ const result = await mw(getRequestInfo());
126
+ const handled = await handleMiddlewareResult(result);
127
+ if (handled) {
128
+ return handled;
129
+ }
130
+ }
131
+ // 3b. Final component/handler
132
+ if (isRouteComponent(componentHandler)) {
133
+ const requestInfo = getRequestInfo();
134
+ const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(componentHandler), route.layouts || [], requestInfo);
135
+ if (!isClientReference(componentHandler)) {
136
+ requestInfo.rw.pageRouteResolved = Promise.withResolvers();
137
+ }
138
+ return await renderPage(requestInfo, WrappedComponent, onError);
139
+ }
140
+ // Handle non-component final handler (e.g., returns new Response)
141
+ const tailResult = await componentHandler(getRequestInfo());
142
+ const handledTail = await handleMiddlewareResult(tailResult);
143
+ if (handledTail) {
144
+ return handledTail;
145
+ }
146
+ return new Response("Response not returned from route handler", {
147
+ status: 500,
148
+ });
163
149
  });
164
150
  }
151
+ // No route matched
152
+ return new Response("Not Found", { status: 404 });
165
153
  },
166
154
  };
167
155
  }
@@ -12,4 +12,5 @@ export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
12
12
  response: ResponseInit & {
13
13
  headers: Headers;
14
14
  };
15
+ isAction: boolean;
15
16
  }
@@ -33,6 +33,7 @@ export const defineApp = (routes) => {
33
33
  const url = new URL(request.url);
34
34
  const isRSCRequest = url.searchParams.has("__rsc") ||
35
35
  request.headers.get("accept")?.includes("text/x-component");
36
+ const isAction = url.searchParams.has("__rsc_action_id");
36
37
  const userHeaders = new Headers();
37
38
  const rw = {
38
39
  Document: DefaultDocument,
@@ -55,6 +56,7 @@ export const defineApp = (routes) => {
55
56
  ctx: {},
56
57
  rw,
57
58
  response: userResponseInit,
59
+ isAction,
58
60
  };
59
61
  const createPageElement = (requestInfo, Page) => {
60
62
  let pageElement;
@@ -77,11 +79,7 @@ export const defineApp = (routes) => {
77
79
  status: 500,
78
80
  });
79
81
  }
80
- let actionResult = undefined;
81
- const isRSCActionHandler = url.searchParams.has("__rsc_action_id");
82
- if (isRSCActionHandler) {
83
- actionResult = await rscActionHandler(request);
84
- }
82
+ const actionResult = requestInfo.rw.actionResult;
85
83
  const pageElement = createPageElement(requestInfo, Page);
86
84
  const { rscPayload: shouldInjectRSCPayload } = rw;
87
85
  let rscPayloadStream = renderToRscStream({
@@ -131,6 +129,7 @@ export const defineApp = (routes) => {
131
129
  getRequestInfo: getRequestInfo,
132
130
  runWithRequestInfoOverrides,
133
131
  onError: reject,
132
+ rscActionHandler,
134
133
  }));
135
134
  }
136
135
  catch (e) {
@@ -111,7 +111,9 @@ const performFullSync = async (sdkDir, targetDir) => {
111
111
  const cmd = pm.name;
112
112
  const args = [pm.command];
113
113
  if (pm.name === "yarn") {
114
- args.push(`file:${tarballPath}`);
114
+ // For modern yarn, disable PnP to avoid resolution issues with local tarballs
115
+ process.env.YARN_NODE_LINKER = "node-modules";
116
+ args.push(`rwsdk@file:${tarballPath}`);
115
117
  }
116
118
  else {
117
119
  args.push(tarballPath);
@@ -30,6 +30,7 @@ if (fileURLToPath(import.meta.url) === process.argv[1]) {
30
30
  realtime: false, // Default to false - don't just test realtime
31
31
  skipHmr: false, // Default to false - run HMR tests
32
32
  skipStyleTests: false,
33
+ packageManager: "pnpm", // Default to pnpm
33
34
  // sync: will be set below
34
35
  };
35
36
  // Log if we're in CI
@@ -94,6 +95,7 @@ Options:
94
95
  --skip-style-tests Skip stylesheet-related tests
95
96
  --path=PATH Project directory to test
96
97
  --artifact-dir=DIR Directory to store test artifacts (default: .artifacts)
98
+ --package-manager=MGR Package manager to use (pnpm, npm, yarn, yarn-classic; default: pnpm)
97
99
  --keep Keep temporary test directory after tests complete
98
100
  --no-headless Run browser tests with GUI (not headless)
99
101
  --sync Force syncing SDK code to test project
@@ -112,6 +114,13 @@ Options:
112
114
  else if (arg.startsWith("--artifact-dir=")) {
113
115
  options.artifactDir = arg.substring(15);
114
116
  }
117
+ else if (arg.startsWith("--package-manager=")) {
118
+ const pm = arg.substring(18);
119
+ if (!["pnpm", "npm", "yarn", "yarn-classic"].includes(pm)) {
120
+ throw new Error(`Invalid package manager: ${pm}`);
121
+ }
122
+ options.packageManager = pm;
123
+ }
115
124
  else {
116
125
  // Throw error for unknown options instead of just warning
117
126
  log("Unknown option: %s", arg);
@@ -1,6 +1,7 @@
1
1
  import MagicString from "magic-string";
2
2
  import path from "path";
3
3
  import debug from "debug";
4
+ import { VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, } from "../lib/constants.mjs";
4
5
  export const createDirectiveLookupPlugin = async ({ projectRootDir, files, config, }) => {
5
6
  const debugNamespace = `rwsdk:vite:${config.pluginName}`;
6
7
  const log = debug(debugNamespace);
@@ -126,8 +127,8 @@ export const ${config.exportName} = {
126
127
  .map((file) => {
127
128
  if (file.includes("node_modules") && isDev) {
128
129
  const barrelPath = config.kind === "client"
129
- ? "rwsdk/__client_barrel"
130
- : "rwsdk/__server_barrel";
130
+ ? VENDOR_CLIENT_BARREL_EXPORT_PATH
131
+ : VENDOR_SERVER_BARREL_EXPORT_PATH;
131
132
  return `
132
133
  "${file}": () => import("${barrelPath}").then(m => m.default["${file}"]),
133
134
  `;
@@ -1,6 +1,4 @@
1
1
  import { Plugin } from "vite";
2
- export declare const VIRTUAL_CLIENT_BARREL_ID = "virtual:rwsdk:client-module-barrel";
3
- export declare const VIRTUAL_SERVER_BARREL_ID = "virtual:rwsdk:server-module-barrel";
4
2
  export declare const directiveModulesDevPlugin: ({ clientFiles, serverFiles, projectRootDir, }: {
5
3
  clientFiles: Set<string>;
6
4
  serverFiles: Set<string>;
@@ -1,13 +1,10 @@
1
1
  import path from "path";
2
- import { writeFileSync, mkdirSync } from "node:fs";
2
+ import os from "os";
3
+ import { writeFileSync, mkdirSync, mkdtempSync } from "node:fs";
3
4
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
4
- import { CLIENT_BARREL_PATH, SERVER_BARREL_PATH } from "../lib/constants.mjs";
5
+ import { VENDOR_CLIENT_BARREL_PATH, VENDOR_SERVER_BARREL_PATH, VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, } from "../lib/constants.mjs";
5
6
  import { runDirectivesScan } from "./runDirectivesScan.mjs";
6
- export const VIRTUAL_CLIENT_BARREL_ID = "virtual:rwsdk:client-module-barrel";
7
- export const VIRTUAL_SERVER_BARREL_ID = "virtual:rwsdk:server-module-barrel";
8
- const CLIENT_BARREL_EXPORT_PATH = "rwsdk/__client_barrel";
9
- const SERVER_BARREL_EXPORT_PATH = "rwsdk/__server_barrel";
10
- const generateBarrelContent = (files, projectRootDir) => {
7
+ const generateVendorBarrelContent = (files, projectRootDir) => {
11
8
  const imports = [...files]
12
9
  .filter((file) => file.includes("node_modules"))
13
10
  .map((file, i) => `import * as M${i} from '${normalizeModulePath(file, projectRootDir, {
@@ -22,36 +19,50 @@ const generateBarrelContent = (files, projectRootDir) => {
22
19
  "\n};";
23
20
  return `${imports}\n\n${exports}`;
24
21
  };
22
+ const generateAppBarrelContent = (files, projectRootDir, barrelFilePath) => {
23
+ return [...files]
24
+ .filter((file) => !file.includes("node_modules"))
25
+ .map((file) => {
26
+ const resolvedPath = normalizeModulePath(file, projectRootDir, {
27
+ absolute: true,
28
+ });
29
+ return `import "${resolvedPath}";`;
30
+ })
31
+ .join("\n");
32
+ };
25
33
  export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, }) => {
26
- let scanPromise = null;
34
+ const { promise: scanPromise, resolve: resolveScanPromise } = Promise.withResolvers();
35
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "rwsdk-"));
36
+ const APP_CLIENT_BARREL_PATH = path.join(tempDir, "app-client-barrel.js");
37
+ const APP_SERVER_BARREL_PATH = path.join(tempDir, "app-server-barrel.js");
27
38
  return {
28
39
  name: "rwsdk:directive-modules-dev",
29
40
  configureServer(server) {
30
41
  if (!process.env.VITE_IS_DEV_SERVER || process.env.RWSDK_WORKER_RUN) {
42
+ resolveScanPromise();
31
43
  return;
32
44
  }
33
- // Start the directive scan as soon as the server is configured.
34
- // We don't await it here, allowing Vite to continue its startup.
35
- scanPromise = runDirectivesScan({
45
+ runDirectivesScan({
36
46
  rootConfig: server.config,
37
47
  environments: server.environments,
38
48
  clientFiles,
39
49
  serverFiles,
40
50
  }).then(() => {
41
- // After the scan is complete, write the barrel files.
42
- const clientBarrelContent = generateBarrelContent(clientFiles, projectRootDir);
43
- writeFileSync(CLIENT_BARREL_PATH, clientBarrelContent);
44
- const serverBarrelContent = generateBarrelContent(serverFiles, projectRootDir);
45
- writeFileSync(SERVER_BARREL_PATH, serverBarrelContent);
51
+ // context(justinvdm, 11 Sep 2025): For vendor barrels, we write the
52
+ // files directly to disk after the scan. For app barrels, we use a
53
+ // more complex esbuild plugin to provide content in-memory. This is
54
+ // because app barrels require special handling to prevent Vite from
55
+ // marking application code as `external: true`. Vendor barrels do not
56
+ // have this requirement and a simpler, direct-write approach is more
57
+ // stable.
58
+ const vendorClientBarrelContent = generateVendorBarrelContent(clientFiles, projectRootDir);
59
+ writeFileSync(VENDOR_CLIENT_BARREL_PATH, vendorClientBarrelContent);
60
+ const vendorServerBarrelContent = generateVendorBarrelContent(serverFiles, projectRootDir);
61
+ writeFileSync(VENDOR_SERVER_BARREL_PATH, vendorServerBarrelContent);
62
+ resolveScanPromise();
46
63
  });
47
- // context(justinvdm, 4 Sep 2025): Add middleware to block incoming
48
- // requests until the scan is complete. This gives us a single hook for
49
- // preventing app code being processed by vite until the scan is complete.
50
- // This improves perceived startup time by not blocking Vite's optimizer.
51
64
  server.middlewares.use(async (_req, _res, next) => {
52
- if (scanPromise) {
53
- await scanPromise;
54
- }
65
+ await scanPromise;
55
66
  next();
56
67
  });
57
68
  },
@@ -59,25 +70,70 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
59
70
  if (config.command !== "serve") {
60
71
  return;
61
72
  }
62
- // Create dummy files to give esbuild a real path to resolve.
63
- mkdirSync(path.dirname(CLIENT_BARREL_PATH), { recursive: true });
64
- writeFileSync(CLIENT_BARREL_PATH, "");
65
- mkdirSync(path.dirname(SERVER_BARREL_PATH), { recursive: true });
66
- writeFileSync(SERVER_BARREL_PATH, "");
73
+ mkdirSync(path.dirname(APP_CLIENT_BARREL_PATH), { recursive: true });
74
+ writeFileSync(APP_CLIENT_BARREL_PATH, "");
75
+ mkdirSync(path.dirname(APP_SERVER_BARREL_PATH), { recursive: true });
76
+ writeFileSync(APP_SERVER_BARREL_PATH, "");
77
+ mkdirSync(path.dirname(VENDOR_CLIENT_BARREL_PATH), { recursive: true });
78
+ writeFileSync(VENDOR_CLIENT_BARREL_PATH, "");
79
+ mkdirSync(path.dirname(VENDOR_SERVER_BARREL_PATH), { recursive: true });
80
+ writeFileSync(VENDOR_SERVER_BARREL_PATH, "");
67
81
  for (const [envName, env] of Object.entries(config.environments || {})) {
68
82
  env.optimizeDeps ??= {};
69
83
  env.optimizeDeps.include ??= [];
70
- env.optimizeDeps.include.push(CLIENT_BARREL_EXPORT_PATH, SERVER_BARREL_EXPORT_PATH);
84
+ const entries = (env.optimizeDeps.entries = castArray(env.optimizeDeps.entries ?? []));
85
+ env.optimizeDeps.include.push(VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH);
86
+ if (envName === "client") {
87
+ entries.push(APP_CLIENT_BARREL_PATH);
88
+ }
89
+ else if (envName === "worker") {
90
+ entries.push(APP_SERVER_BARREL_PATH);
91
+ }
71
92
  env.optimizeDeps.esbuildOptions ??= {};
72
93
  env.optimizeDeps.esbuildOptions.plugins ??= [];
73
- env.optimizeDeps.esbuildOptions.plugins.push({
74
- name: "rwsdk:block-optimizer-for-scan",
94
+ env.optimizeDeps.esbuildOptions.plugins.unshift({
95
+ name: "rwsdk:app-barrel-blocker",
75
96
  setup(build) {
76
- build.onStart(async () => {
77
- // context(justinvdm, 4 Sep 2025): We await the scan promise
78
- // here because we want to block the optimizer until the scan is
79
- // complete.
97
+ const appBarrelPaths = [
98
+ APP_CLIENT_BARREL_PATH,
99
+ APP_SERVER_BARREL_PATH,
100
+ ];
101
+ const appBarrelFilter = new RegExp(`(${appBarrelPaths
102
+ .map((p) => p.replace(/\\/g, "\\\\"))
103
+ .join("|")})$`);
104
+ build.onResolve({ filter: /.*/ }, async (args) => {
105
+ // Block all resolutions until the scan is complete.
80
106
  await scanPromise;
107
+ // Handle app barrel files
108
+ if (appBarrelFilter.test(args.path)) {
109
+ return {
110
+ path: args.path,
111
+ namespace: "rwsdk-app-barrel-ns",
112
+ };
113
+ }
114
+ // context(justinvdm, 11 Sep 2025): Prevent Vite from
115
+ // externalizing our application files. If we don't, paths
116
+ // imported in our application barrel files will be marked as
117
+ // external, and thus not scanned for dependencies.
118
+ if (args.path.startsWith("/") &&
119
+ (args.path.includes("/src/") ||
120
+ args.path.includes("/generated/")) &&
121
+ !args.path.includes("node_modules")) {
122
+ // By returning a result, we claim the module and prevent vite:dep-scan
123
+ // from marking it as external.
124
+ return {
125
+ path: args.path,
126
+ };
127
+ }
128
+ });
129
+ build.onLoad({ filter: /.*/, namespace: "rwsdk-app-barrel-ns" }, (args) => {
130
+ const isServerBarrel = args.path.includes("app-server-barrel");
131
+ const files = isServerBarrel ? serverFiles : clientFiles;
132
+ const content = generateAppBarrelContent(files, projectRootDir, args.path);
133
+ return {
134
+ contents: content,
135
+ loader: "js",
136
+ };
81
137
  });
82
138
  },
83
139
  });
@@ -85,3 +141,6 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
85
141
  },
86
142
  };
87
143
  };
144
+ const castArray = (value) => {
145
+ return Array.isArray(value) ? value : [value];
146
+ };
@@ -146,7 +146,6 @@ export const miniflareHMRPlugin = (givenOptions) => [
146
146
  invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "/@id/virtual:use-server-lookup.js");
147
147
  invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX + "virtual:use-server-lookup.js");
148
148
  }
149
- // todo(justinvdm, 12 Dec 2024): Skip client references
150
149
  const modules = Array.from(ctx.server.environments[environment].moduleGraph.getModulesByFile(ctx.file) ?? []);
151
150
  const isWorkerUpdate = Boolean(modules);
152
151
  // The worker needs an update, but this is the client environment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.3.7",
3
+ "version": "1.0.0-alpha.0",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,11 +45,11 @@
45
45
  "types": "./dist/runtime/ssrBridge.d.ts",
46
46
  "default": "./dist/runtime/ssrBridge.js"
47
47
  },
48
- "./__client_barrel": {
49
- "default": "./dist/__intermediate_builds/rwsdk-client-barrel.js"
48
+ "./__vendor_client_barrel": {
49
+ "default": "./dist/__intermediate_builds/rwsdk-vendor-client-barrel.js"
50
50
  },
51
- "./__server_barrel": {
52
- "default": "./dist/__intermediate_builds/rwsdk-server-barrel.js"
51
+ "./__vendor_server_barrel": {
52
+ "default": "./dist/__intermediate_builds/rwsdk-vendor-server-barrel.js"
53
53
  },
54
54
  "./router": {
55
55
  "types": "./dist/runtime/entries/router.d.ts",