rwsdk 1.0.0-beta.8 → 1.0.0-beta.9-test.20251006190822

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.
@@ -1,3 +1,4 @@
1
+ export * from "../$.mjs";
1
2
  export * from "./browser.mjs";
2
3
  export * from "./dev.mjs";
3
4
  export * from "./environment.mjs";
@@ -1,3 +1,4 @@
1
+ export * from "../$.mjs";
1
2
  export * from "./browser.mjs";
2
3
  export * from "./dev.mjs";
3
4
  export * from "./environment.mjs";
@@ -5,6 +5,7 @@ export type { Browser, Page } from "puppeteer-core";
5
5
  export { DEPLOYMENT_CHECK_TIMEOUT, DEPLOYMENT_MIN_TRIES, DEPLOYMENT_TIMEOUT, DEV_SERVER_MIN_TRIES, DEV_SERVER_TIMEOUT, HYDRATION_TIMEOUT, INSTALL_DEPENDENCIES_RETRIES, PUPPETEER_TIMEOUT, SETUP_PLAYGROUND_ENV_TIMEOUT, SETUP_WAIT_TIMEOUT, TEST_MAX_RETRIES, TEST_MAX_RETRIES_PER_CODE, };
6
6
  interface DevServerInstance {
7
7
  url: string;
8
+ projectDir: string;
8
9
  stopDev: () => Promise<void>;
9
10
  }
10
11
  interface DeploymentInstance {
@@ -43,17 +44,29 @@ export interface SetupPlaygroundEnvironmentOptions {
43
44
  * and installs dependencies using a tarball of the SDK.
44
45
  * This ensures that tests run in a clean, isolated environment.
45
46
  */
46
- export declare function setupPlaygroundEnvironment(options?: string | SetupPlaygroundEnvironmentOptions): void;
47
+ export declare function setupPlaygroundEnvironment(options: SetupPlaygroundEnvironmentOptions | string): void;
47
48
  /**
48
49
  * Creates a dev server instance using the shared playground environment.
49
50
  * Automatically registers cleanup to run after the test.
50
51
  */
51
- export declare function createDevServer(projectDir: string): Promise<DevServerInstance>;
52
+ export declare function createDevServer(): {
53
+ projectDir: string;
54
+ start: () => Promise<DevServerInstance>;
55
+ };
52
56
  /**
53
57
  * Creates a deployment instance using the shared playground environment.
54
58
  * Automatically registers cleanup to run after the test.
55
59
  */
56
- export declare function createDeployment(projectDir: string): Promise<DeploymentInstance>;
60
+ export declare function createDeployment(): {
61
+ projectDir: string;
62
+ start: () => Promise<{
63
+ url: string;
64
+ workerName: string;
65
+ resourceUniqueKey: string;
66
+ projectDir: string;
67
+ cleanup: () => Promise<void>;
68
+ }>;
69
+ };
57
70
  /**
58
71
  * Executes a test function with a retry mechanism for specific error codes.
59
72
  * @param name - The name of the test, used for logging.
@@ -63,13 +76,25 @@ export declare function createDeployment(projectDir: string): Promise<Deployment
63
76
  * called automatically on failure.
64
77
  */
65
78
  export declare function runTestWithRetries(name: string, attemptFn: () => Promise<void>): Promise<void>;
79
+ type SDKRunner = (name: string, testLogic: (context: {
80
+ createDevServer: () => Promise<DevServerInstance>;
81
+ createDeployment: () => Promise<DeploymentInstance>;
82
+ browser: Browser;
83
+ page: Page;
84
+ projectDir: string;
85
+ }) => Promise<void>) => void;
66
86
  declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
67
87
  devServer?: DevServerInstance;
68
88
  deployment?: DeploymentInstance;
69
89
  browser: Browser;
70
90
  page: Page;
71
91
  url: string;
92
+ projectDir: string;
72
93
  }) => Promise<void>) => void;
94
+ export declare const testSDK: SDKRunner & {
95
+ only: SDKRunner;
96
+ skip: typeof test.skip;
97
+ };
73
98
  /**
74
99
  * High-level test wrapper for dev server tests.
75
100
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -83,6 +108,7 @@ export declare namespace testDev {
83
108
  browser: Browser;
84
109
  page: Page;
85
110
  url: string;
111
+ projectDir: string;
86
112
  }) => Promise<void>) => void;
87
113
  }
88
114
  /**
@@ -98,6 +124,7 @@ export declare namespace testDeploy {
98
124
  browser: Browser;
99
125
  page: Page;
100
126
  url: string;
127
+ projectDir: string;
101
128
  }) => Promise<void>) => void;
102
129
  }
103
130
  /**
@@ -110,6 +137,7 @@ export declare function testDevAndDeploy(name: string, testFn: (context: {
110
137
  browser: Browser;
111
138
  page: Page;
112
139
  url: string;
140
+ projectDir: string;
113
141
  }) => Promise<void>): void;
114
142
  export declare namespace testDevAndDeploy {
115
143
  var skip: (name: string, testFn?: any) => void;
@@ -20,6 +20,8 @@ let globalDevInstancePromise = null;
20
20
  let globalDeploymentInstancePromise = null;
21
21
  let globalDevInstance = null;
22
22
  let globalDeploymentInstance = null;
23
+ const devInstances = [];
24
+ const deploymentInstances = [];
23
25
  let hooksRegistered = false;
24
26
  /**
25
27
  * Registers global cleanup hooks automatically
@@ -30,11 +32,11 @@ function ensureHooksRegistered() {
30
32
  // Register global afterAll to clean up the playground environment
31
33
  afterAll(async () => {
32
34
  const cleanupPromises = [];
33
- if (globalDevInstance) {
34
- cleanupPromises.push(globalDevInstance.stopDev());
35
+ for (const instance of devInstances) {
36
+ cleanupPromises.push(instance.stopDev());
35
37
  }
36
- if (globalDeploymentInstance) {
37
- cleanupPromises.push(globalDeploymentInstance.cleanup());
38
+ for (const instance of deploymentInstances) {
39
+ cleanupPromises.push(instance.cleanup());
38
40
  }
39
41
  if (globalDevPlaygroundEnv) {
40
42
  cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
@@ -43,8 +45,8 @@ function ensureHooksRegistered() {
43
45
  cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
44
46
  }
45
47
  await Promise.all(cleanupPromises);
46
- globalDevInstance = null;
47
- globalDeploymentInstance = null;
48
+ devInstances.length = 0;
49
+ deploymentInstances.length = 0;
48
50
  globalDevPlaygroundEnv = null;
49
51
  globalDeployPlaygroundEnv = null;
50
52
  });
@@ -82,7 +84,7 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
82
84
  * and installs dependencies using a tarball of the SDK.
83
85
  * This ensures that tests run in a clean, isolated environment.
84
86
  */
85
- export function setupPlaygroundEnvironment(options = {}) {
87
+ export function setupPlaygroundEnvironment(options) {
86
88
  const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
87
89
  ensureHooksRegistered();
88
90
  beforeAll(async () => {
@@ -109,7 +111,8 @@ export function setupPlaygroundEnvironment(options = {}) {
109
111
  projectDir: devEnv.targetDir,
110
112
  cleanup: devEnv.cleanup,
111
113
  };
112
- globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
114
+ const devControl = createDevServer();
115
+ globalDevInstancePromise = devControl.start().then((instance) => {
113
116
  globalDevInstance = instance;
114
117
  return instance;
115
118
  });
@@ -118,7 +121,7 @@ export function setupPlaygroundEnvironment(options = {}) {
118
121
  globalDevInstancePromise.catch(() => { });
119
122
  }
120
123
  else {
121
- globalDevInstancePromise = Promise.resolve(null);
124
+ globalDevPlaygroundEnv = null;
122
125
  }
123
126
  if (deploy) {
124
127
  const deployEnv = await setupTarballEnvironment({
@@ -130,7 +133,10 @@ export function setupPlaygroundEnvironment(options = {}) {
130
133
  projectDir: deployEnv.targetDir,
131
134
  cleanup: deployEnv.cleanup,
132
135
  };
133
- globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
136
+ const deployControl = createDeployment();
137
+ globalDeploymentInstancePromise = deployControl
138
+ .start()
139
+ .then((instance) => {
134
140
  globalDeploymentInstance = instance;
135
141
  return instance;
136
142
  });
@@ -138,7 +144,7 @@ export function setupPlaygroundEnvironment(options = {}) {
138
144
  globalDeploymentInstancePromise.catch(() => { });
139
145
  }
140
146
  else {
141
- globalDeploymentInstancePromise = Promise.resolve(null);
147
+ globalDeployPlaygroundEnv = null;
142
148
  }
143
149
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
144
150
  }
@@ -146,82 +152,106 @@ export function setupPlaygroundEnvironment(options = {}) {
146
152
  * Creates a dev server instance using the shared playground environment.
147
153
  * Automatically registers cleanup to run after the test.
148
154
  */
149
- export async function createDevServer(projectDir) {
150
- if (SKIP_DEV_SERVER_TESTS) {
151
- throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
155
+ export function createDevServer() {
156
+ ensureHooksRegistered();
157
+ if (!globalDevPlaygroundEnv) {
158
+ throw new Error("Dev playground environment not initialized. Enable `dev: true` in setupPlaygroundEnvironment.");
152
159
  }
160
+ const { projectDir } = globalDevPlaygroundEnv;
153
161
  const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
154
- const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
155
- timeout: DEV_SERVER_TIMEOUT,
156
- minTries: DEV_SERVER_MIN_TRIES,
157
- onRetry: (error, tries) => {
158
- console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
159
- },
160
- });
162
+ let instance = null;
161
163
  return {
162
- url: devResult.url,
163
- stopDev: devResult.stopDev,
164
+ projectDir,
165
+ start: async () => {
166
+ if (instance)
167
+ return instance;
168
+ if (SKIP_DEV_SERVER_TESTS) {
169
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
170
+ }
171
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
172
+ timeout: DEV_SERVER_TIMEOUT,
173
+ minTries: DEV_SERVER_MIN_TRIES,
174
+ onRetry: (error, tries) => {
175
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
176
+ },
177
+ });
178
+ instance = {
179
+ url: devResult.url,
180
+ projectDir,
181
+ stopDev: devResult.stopDev,
182
+ };
183
+ devInstances.push(instance);
184
+ return instance;
185
+ },
164
186
  };
165
187
  }
166
188
  /**
167
189
  * Creates a deployment instance using the shared playground environment.
168
190
  * Automatically registers cleanup to run after the test.
169
191
  */
170
- export async function createDeployment(projectDir) {
171
- if (SKIP_DEPLOYMENT_TESTS) {
172
- throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
192
+ export function createDeployment() {
193
+ ensureHooksRegistered();
194
+ if (!globalDeployPlaygroundEnv) {
195
+ throw new Error("Deploy playground environment not initialized. Enable `deploy: true` in setupPlaygroundEnvironment.");
173
196
  }
174
- return await pollValue(async () => {
175
- // Extract the unique key from the project directory name instead of generating a new one
176
- // The directory name format is: {projectName}-e2e-test-{randomId}
177
- const dirName = basename(projectDir);
178
- const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
179
- const resourceUniqueKey = match
180
- ? match[1]
181
- : Math.random().toString(36).substring(2, 15);
182
- const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
183
- // Poll the URL to ensure it's live before proceeding
184
- await poll(async () => {
185
- try {
186
- const response = await fetch(deployResult.url);
187
- // We consider any response (even 4xx or 5xx) as success,
188
- // as it means the worker is routable.
189
- return response.status > 0;
190
- }
191
- catch (e) {
192
- return false;
197
+ const { projectDir } = globalDeployPlaygroundEnv;
198
+ let instance = null;
199
+ return {
200
+ projectDir,
201
+ start: async () => {
202
+ if (instance)
203
+ return instance;
204
+ if (SKIP_DEPLOYMENT_TESTS) {
205
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
193
206
  }
194
- }, {
195
- timeout: DEPLOYMENT_CHECK_TIMEOUT,
196
- });
197
- const cleanup = async () => {
198
- // Run deployment cleanup in background without blocking
199
- const performCleanup = async () => {
200
- if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
201
- await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
202
- }
203
- await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
204
- };
205
- // Start cleanup in background and return immediately
206
- performCleanup().catch((error) => {
207
- console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
207
+ const newInstance = await pollValue(async () => {
208
+ const dirName = basename(projectDir);
209
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
210
+ const resourceUniqueKey = match
211
+ ? match[1]
212
+ : Math.random().toString(36).substring(2, 15);
213
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
214
+ await poll(async () => {
215
+ try {
216
+ const response = await fetch(deployResult.url);
217
+ return response.status > 0;
218
+ }
219
+ catch (e) {
220
+ return false;
221
+ }
222
+ }, {
223
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
224
+ });
225
+ const cleanup = async () => {
226
+ const performCleanup = async () => {
227
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
228
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
229
+ }
230
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
231
+ };
232
+ performCleanup().catch((error) => {
233
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
234
+ });
235
+ return Promise.resolve();
236
+ };
237
+ return {
238
+ url: deployResult.url,
239
+ workerName: deployResult.workerName,
240
+ resourceUniqueKey,
241
+ projectDir: projectDir,
242
+ cleanup,
243
+ };
244
+ }, {
245
+ timeout: DEPLOYMENT_TIMEOUT,
246
+ minTries: DEPLOYMENT_MIN_TRIES,
247
+ onRetry: (error, tries) => {
248
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
249
+ },
208
250
  });
209
- return Promise.resolve();
210
- };
211
- return {
212
- url: deployResult.url,
213
- workerName: deployResult.workerName,
214
- resourceUniqueKey,
215
- projectDir: projectDir,
216
- cleanup,
217
- };
218
- }, {
219
- timeout: DEPLOYMENT_TIMEOUT,
220
- minTries: DEPLOYMENT_MIN_TRIES,
221
- onRetry: (error, tries) => {
222
- console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
251
+ deploymentInstances.push(newInstance);
252
+ return newInstance;
223
253
  },
224
- });
254
+ };
225
255
  }
226
256
  /**
227
257
  * Executes a test function with a retry mechanism for specific error codes.
@@ -270,7 +300,7 @@ function createTestRunner(testFn, envType) {
270
300
  return (name, testLogic) => {
271
301
  if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
272
302
  (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
273
- test.skip(name, () => { });
303
+ test.skip(`${name} (${envType})`, () => { });
274
304
  return;
275
305
  }
276
306
  describe.concurrent(name, () => {
@@ -332,12 +362,86 @@ function createTestRunner(testFn, envType) {
332
362
  browser: browser,
333
363
  page: page,
334
364
  url: instance.url,
365
+ projectDir: instance
366
+ .projectDir,
335
367
  });
336
368
  });
337
369
  });
338
370
  });
339
371
  };
340
372
  }
373
+ /**
374
+ * Creates a low-level test runner that provides utilities for creating
375
+ * tests that need to perform setup actions before the server starts.
376
+ */
377
+ function createSDKTestRunner() {
378
+ const internalRunner = (testFn) => {
379
+ return (name, testLogic) => {
380
+ describe.concurrent(name, () => {
381
+ let page;
382
+ let browser;
383
+ beforeAll(async () => {
384
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
385
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
386
+ try {
387
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
388
+ browser = await puppeteer.connect({
389
+ browserWSEndpoint: wsEndpoint,
390
+ });
391
+ }
392
+ catch (error) {
393
+ console.warn("Failed to connect to existing browser instance. " +
394
+ "This might happen if you are running a single test file. " +
395
+ "Launching a new browser instance instead.");
396
+ browser = await launchBrowser();
397
+ }
398
+ }, SETUP_WAIT_TIMEOUT);
399
+ afterAll(async () => {
400
+ if (browser) {
401
+ await browser.disconnect();
402
+ }
403
+ });
404
+ beforeEach(async () => {
405
+ if (!globalDevPlaygroundEnv && !globalDeployPlaygroundEnv) {
406
+ throw new Error("Test environment not initialized. Call setupPlaygroundEnvironment() in your test file.");
407
+ }
408
+ page = await browser.newPage();
409
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
410
+ }, SETUP_WAIT_TIMEOUT);
411
+ afterEach(async () => {
412
+ if (page) {
413
+ try {
414
+ await page.close();
415
+ }
416
+ catch (error) {
417
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
418
+ }
419
+ }
420
+ });
421
+ testFn(">", async () => {
422
+ if (!browser) {
423
+ throw new Error("Test environment not ready.");
424
+ }
425
+ await runTestWithRetries(name, async () => {
426
+ await testLogic({
427
+ browser: browser,
428
+ page: page,
429
+ projectDir: globalDevPlaygroundEnv?.projectDir ||
430
+ globalDeployPlaygroundEnv?.projectDir ||
431
+ "",
432
+ });
433
+ });
434
+ });
435
+ });
436
+ };
437
+ };
438
+ const main = internalRunner(test);
439
+ return Object.assign(main, {
440
+ only: internalRunner(test.only),
441
+ skip: test.skip,
442
+ });
443
+ }
444
+ export const testSDK = createSDKTestRunner();
341
445
  /**
342
446
  * High-level test wrapper for dev server tests.
343
447
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -1,3 +1,2 @@
1
1
  import { Kysely } from "kysely";
2
- import { type SqliteDurableObject } from "./index.js";
3
- export declare function createDb<T>(durableObjectBinding: DurableObjectNamespace<SqliteDurableObject>, name?: string): Kysely<T>;
2
+ export declare function createDb<DatabaseType>(durableObjectBinding: DurableObjectNamespace<any>, name?: string): Kysely<DatabaseType>;
@@ -5,6 +5,10 @@ export function createDb(durableObjectBinding, name = "main") {
5
5
  dialect: new DOWorkerDialect({
6
6
  kyselyExecuteQuery: (...args) => {
7
7
  const durableObjectId = durableObjectBinding.idFromName(name);
8
+ // context(justinvdm, 2 Oct 2025): First prize would be a type parameter
9
+ // for the durable object and then use it for `durableObjectBinding`'s
10
+ // type, rather than casting like this. However, that would prevent
11
+ // users from being able to do createDb<InferredDbType> then though.
8
12
  const stub = durableObjectBinding.get(durableObjectId);
9
13
  stub.initialize();
10
14
  return stub.kyselyExecuteQuery(...args);
@@ -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 });
@@ -23,6 +23,29 @@ export const defineApp = (routes) => {
23
23
  url.pathname = url.pathname.slice("/assets/".length);
24
24
  return env.ASSETS.fetch(new Request(url.toString(), request));
25
25
  }
26
+ else if (import.meta.env.VITE_IS_DEV_SERVER &&
27
+ new URL(request.url).pathname === "/__worker-run") {
28
+ const url = new URL(request.url);
29
+ const scriptPath = url.searchParams.get("script");
30
+ if (!scriptPath) {
31
+ return new Response("Missing 'script' query parameter", {
32
+ status: 400,
33
+ });
34
+ }
35
+ try {
36
+ const scriptModule = await import(/* @vite-ignore */ scriptPath);
37
+ if (scriptModule.default) {
38
+ await scriptModule.default(request, env, cf);
39
+ }
40
+ return new Response("Script executed successfully");
41
+ }
42
+ catch (e) {
43
+ console.error(`Error executing script: ${scriptPath}\n\n${e.stack}`);
44
+ return new Response(`Error executing script: ${e.message}`, {
45
+ status: 500,
46
+ });
47
+ }
48
+ }
26
49
  else if (import.meta.env.VITE_IS_DEV_SERVER &&
27
50
  request.url.includes("/__vite_preamble__")) {
28
51
  return new Response('import RefreshRuntime from "/@react-refresh"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;', {
@@ -1 +1 @@
1
- export declare const runWorkerScript: (relativeScriptPath: string) => Promise<never>;
1
+ export {};
@@ -1,131 +1,68 @@
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 dbg from "debug";
2
+ import getPort from "get-port";
3
+ import path from "path";
4
+ import * as vite from "vite";
5
+ import { createLogger } from "vite";
6
+ const debug = dbg("rwsdk:worker-run");
7
+ const main = async () => {
8
+ process.env.RWSDK_WORKER_RUN = "1";
9
+ const relativeScriptPath = process.argv[2];
13
10
  if (!relativeScriptPath) {
14
11
  console.error("Error: Script path is required");
15
12
  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");
13
+ console.log(" rwsdk worker-run <script-path>");
19
14
  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");
15
+ console.log(" rwsdk worker-run src/scripts/seed.ts\n");
22
16
  process.exit(1);
23
17
  }
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
- }
18
+ const scriptPath = path.resolve(process.cwd(), relativeScriptPath);
19
+ const port = await getPort();
20
+ let server;
21
+ const cleanup = async () => {
22
+ if (server) {
23
+ await server.close();
65
24
  }
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,
25
+ process.exit();
79
26
  };
27
+ process.on("SIGINT", cleanup);
28
+ process.on("SIGTERM", cleanup);
80
29
  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
- ],
30
+ server = await vite.createServer({
31
+ logLevel: "silent",
32
+ build: {
33
+ outDir: ".rwsdk",
34
+ },
35
+ customLogger: createLogger("info", {
36
+ prefix: "[rwsdk]",
37
+ allowClearScreen: true,
38
+ }),
100
39
  server: {
101
- port: 0,
40
+ port,
41
+ host: "localhost",
102
42
  },
103
43
  });
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");
44
+ await server.listen();
45
+ const url = `http://localhost:${port}/__worker-run?script=${scriptPath}`;
46
+ debug("Fetching %s", url);
47
+ const response = await fetch(url);
48
+ debug("Response from worker: %s", response);
49
+ if (!response.ok) {
50
+ const errorText = await response.text();
51
+ console.error(`Error: worker-run script failed with status ${response.status}.`);
52
+ if (errorText) {
53
+ console.error("Response:", errorText);
111
54
  }
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");
55
+ process.exit(1);
120
56
  }
57
+ const responseText = await response.text();
58
+ debug("Response from worker: %s", responseText);
59
+ }
60
+ catch (e) {
61
+ console.error("rwsdk: Error running script:\n\n%s", e.message);
62
+ process.exit(1);
121
63
  }
122
64
  finally {
123
- debug("Closing inspector servers...");
124
- debug("Temporary files cleaned up");
65
+ await cleanup();
125
66
  }
126
- // todo(justinvdm, 01 Apr 2025): Investigate what handles are remaining open
127
- process.exit(0);
128
67
  };
129
- if (import.meta.url === new URL(process.argv[1], import.meta.url).href) {
130
- runWorkerScript(process.argv[2]);
131
- }
68
+ main();
@@ -38,7 +38,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
38
38
  return {
39
39
  name: "rwsdk:directive-modules-dev",
40
40
  configureServer(server) {
41
- if (!process.env.VITE_IS_DEV_SERVER || process.env.RWSDK_WORKER_RUN) {
41
+ if (!process.env.VITE_IS_DEV_SERVER) {
42
42
  resolveScanPromise();
43
43
  return;
44
44
  }
@@ -1,6 +1,7 @@
1
1
  // @ts-ignore
2
2
  import { compile } from "@mdx-js/mdx";
3
3
  import debug from "debug";
4
+ import { glob } from "glob";
4
5
  import fsp from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
@@ -17,6 +18,40 @@ const isObject = (value) => Object.prototype.toString.call(value) === "[object O
17
18
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts
18
19
  const externalRE = /^(https?:)?\/\//;
19
20
  const isExternalUrl = (url) => externalRE.test(url);
21
+ async function findDirectiveRoots({ root, readFileWithCache, directiveCheckCache, }) {
22
+ const srcDir = path.resolve(root, "src");
23
+ console.log("########", srcDir);
24
+ const files = await glob("**/*.{ts,tsx,js,jsx,mjs,mts,cjs,cts,mdx}", {
25
+ cwd: srcDir,
26
+ absolute: true,
27
+ });
28
+ const directiveFiles = new Set();
29
+ for (const file of files) {
30
+ if (directiveCheckCache.has(file)) {
31
+ if (directiveCheckCache.get(file)) {
32
+ directiveFiles.add(file);
33
+ }
34
+ continue;
35
+ }
36
+ try {
37
+ const content = await readFileWithCache(file);
38
+ const hasClient = hasDirective(content, "use client");
39
+ const hasServer = hasDirective(content, "use server");
40
+ const hasAnyDirective = hasClient || hasServer;
41
+ directiveCheckCache.set(file, hasAnyDirective);
42
+ if (hasAnyDirective) {
43
+ directiveFiles.add(file);
44
+ }
45
+ }
46
+ catch (e) {
47
+ log("Could not read file during pre-scan, skipping:", file);
48
+ // Cache the failure to avoid re-reading a problematic file
49
+ directiveCheckCache.set(file, false);
50
+ }
51
+ }
52
+ log("Pre-scan found directive files:", Array.from(directiveFiles));
53
+ return directiveFiles;
54
+ }
20
55
  export async function resolveModuleWithEnvironment({ path, importer, importerEnv, clientResolver, workerResolver, }) {
21
56
  const resolver = importerEnv === "client" ? clientResolver : workerResolver;
22
57
  return new Promise((resolvePromise) => {
@@ -52,10 +87,21 @@ export function classifyModule({ contents, inheritedEnv, }) {
52
87
  return { moduleEnv, isClient, isServer };
53
88
  }
54
89
  export const runDirectivesScan = async ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }) => {
90
+ console.log("######## entries", initialEntries);
55
91
  deferredLog("\n… (rwsdk) Scanning for 'use client' and 'use server' directives...");
56
92
  // Set environment variable to indicate scanning is in progress
57
93
  process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE = "true";
58
94
  try {
95
+ const fileContentCache = new Map();
96
+ const directiveCheckCache = new Map();
97
+ const readFileWithCache = async (path) => {
98
+ if (fileContentCache.has(path)) {
99
+ return fileContentCache.get(path);
100
+ }
101
+ const contents = await fsp.readFile(path, "utf-8");
102
+ fileContentCache.set(path, contents);
103
+ return contents;
104
+ };
59
105
  const esbuild = await getViteEsbuild(rootConfig.root);
60
106
  const input = initialEntries ?? environments.worker.config.build.rollupOptions?.input;
61
107
  let entries;
@@ -78,19 +124,19 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
78
124
  // Filter out virtual modules since they can't be scanned by esbuild
79
125
  const realEntries = entries.filter((entry) => !entry.includes("virtual:"));
80
126
  const absoluteEntries = realEntries.map((entry) => path.resolve(rootConfig.root, entry));
81
- log("Starting directives scan for worker environment with entries:", absoluteEntries);
127
+ const applicationDirectiveFiles = await findDirectiveRoots({
128
+ root: rootConfig.root,
129
+ readFileWithCache,
130
+ directiveCheckCache,
131
+ });
132
+ const combinedEntries = new Set([
133
+ ...absoluteEntries,
134
+ ...applicationDirectiveFiles,
135
+ ]);
136
+ log("Starting directives scan with combined entries:", Array.from(combinedEntries));
82
137
  const workerResolver = createViteAwareResolver(rootConfig, environments.worker);
83
138
  const clientResolver = createViteAwareResolver(rootConfig, environments.client);
84
139
  const moduleEnvironments = new Map();
85
- const fileContentCache = new Map();
86
- const readFileWithCache = async (path) => {
87
- if (fileContentCache.has(path)) {
88
- return fileContentCache.get(path);
89
- }
90
- const contents = await fsp.readFile(path, "utf-8");
91
- fileContentCache.set(path, contents);
92
- return contents;
93
- };
94
140
  const esbuildScanPlugin = {
95
141
  name: "rwsdk:esbuild-scan-plugin",
96
142
  setup(build) {
@@ -232,7 +278,7 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
232
278
  },
233
279
  };
234
280
  await esbuild.build({
235
- entryPoints: absoluteEntries,
281
+ entryPoints: Array.from(combinedEntries),
236
282
  bundle: true,
237
283
  write: false,
238
284
  outdir: path.join(INTERMEDIATES_OUTPUT_DIR, "directive-scan"),
@@ -254,7 +300,8 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
254
300
  }
255
301
  };
256
302
  const deferredLog = (message) => {
303
+ const doLog = process.env.RWSDK_WORKER_RUN ? log : console.log;
257
304
  setTimeout(() => {
258
- console.log(message);
305
+ doLog(message);
259
306
  }, 500);
260
307
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9-test.20251006190822",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -145,11 +145,13 @@
145
145
  "@vitejs/plugin-react": "~5.0.0",
146
146
  "chokidar": "~4.0.0",
147
147
  "debug": "~4.4.0",
148
+ "decompress": "~4.2.1",
148
149
  "enhanced-resolve": "~5.18.1",
149
150
  "eventsource-parser": "~3.0.0",
150
151
  "execa": "~9.6.0",
151
152
  "find-up": "~8.0.0",
152
153
  "fs-extra": "~11.3.0",
154
+ "get-port": "^7.1.0",
153
155
  "glob": "~11.0.1",
154
156
  "ignore": "~7.0.4",
155
157
  "jsonc-parser": "~3.3.1",
@@ -167,7 +169,7 @@
167
169
  "unique-names-generator": "~4.7.1",
168
170
  "vibe-rules": "~0.3.0",
169
171
  "vite-tsconfig-paths": "~5.1.4",
170
- "decompress": "~4.2.1"
172
+ "@types/glob": "^8.1.0"
171
173
  },
172
174
  "peerDependencies": {
173
175
  "@cloudflare/vite-plugin": "^1.12.4",
@@ -189,7 +191,7 @@
189
191
  "semver": "~7.7.1",
190
192
  "tsx": "~4.20.0",
191
193
  "typescript": "~5.9.0",
192
- "vite": "7.1.6",
194
+ "vite": "~7.1.9",
193
195
  "vitest": "~3.2.0"
194
196
  }
195
197
  }