rwsdk 1.0.0-alpha.7 → 1.0.0-alpha.9

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,8 +1,10 @@
1
1
  import { test, beforeAll, afterAll, afterEach, } from "vitest";
2
+ import { basename } from "path";
2
3
  import { setupTarballEnvironment } from "./tarball.mjs";
3
4
  import { runDevServer } from "./dev.mjs";
4
5
  import { runRelease, deleteWorker, deleteD1Database, isRelatedToTest, } from "./release.mjs";
5
6
  import { launchBrowser } from "./browser.mjs";
7
+ const SETUP_PLAYGROUND_ENV_TIMEOUT = 10 * 60 * 1000;
6
8
  // Environment variable flags for skipping tests
7
9
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
8
10
  const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
@@ -107,13 +109,13 @@ export function setupPlaygroundEnvironment(sourceProjectDir) {
107
109
  console.log(`Setting up playground environment from ${projectDir}...`);
108
110
  const tarballEnv = await setupTarballEnvironment({
109
111
  projectDir,
110
- packageManager: "pnpm",
112
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
111
113
  });
112
114
  globalPlaygroundEnv = {
113
115
  projectDir: tarballEnv.targetDir,
114
116
  cleanup: tarballEnv.cleanup,
115
117
  };
116
- });
118
+ }, SETUP_PLAYGROUND_ENV_TIMEOUT);
117
119
  }
118
120
  /**
119
121
  * Gets the current playground environment.
@@ -134,7 +136,8 @@ export async function createDevServer() {
134
136
  throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
135
137
  }
136
138
  const env = getPlaygroundEnvironment();
137
- const devResult = await runDevServer("pnpm", env.projectDir);
139
+ const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
140
+ const devResult = await runDevServer(packageManager, env.projectDir);
138
141
  const serverId = `devServer-${Date.now()}-${Math.random()
139
142
  .toString(36)
140
143
  .substring(2, 9)}`;
@@ -161,8 +164,26 @@ export async function createDeployment() {
161
164
  throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
162
165
  }
163
166
  const env = getPlaygroundEnvironment();
164
- const resourceUniqueKey = Math.random().toString(36).substring(2, 15);
167
+ // Extract the unique key from the project directory name instead of generating a new one
168
+ // The directory name format is: {projectName}-e2e-test-{randomId}
169
+ const dirName = basename(env.projectDir);
170
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
171
+ const resourceUniqueKey = match
172
+ ? match[1]
173
+ : Math.random().toString(36).substring(2, 15);
165
174
  const deployResult = await runRelease(env.projectDir, env.projectDir, resourceUniqueKey);
175
+ // Poll the URL to ensure it's live before proceeding
176
+ await poll(async () => {
177
+ try {
178
+ const response = await fetch(deployResult.url);
179
+ // We consider any response (even 4xx or 5xx) as success,
180
+ // as it means the worker is routable.
181
+ return response.status > 0;
182
+ }
183
+ catch (e) {
184
+ return false;
185
+ }
186
+ }, 60000);
166
187
  const deploymentId = `deployment-${Date.now()}-${Math.random()
167
188
  .toString(36)
168
189
  .substring(2, 9)}`;
@@ -189,6 +210,7 @@ export async function createDeployment() {
189
210
  url: deployResult.url,
190
211
  workerName: deployResult.workerName,
191
212
  resourceUniqueKey,
213
+ projectDir: env.projectDir,
192
214
  };
193
215
  }
194
216
  /**
@@ -238,26 +260,91 @@ export async function createBrowser() {
238
260
  };
239
261
  return browser;
240
262
  }
263
+ /**
264
+ * Executes a test function with a retry mechanism for specific error codes.
265
+ * @param name - The name of the test, used for logging.
266
+ * @param attemptFn - A function that executes one attempt of the test.
267
+ * It should set up resources, run the test logic, and
268
+ * return a cleanup function. The cleanup function will be
269
+ * called automatically on failure.
270
+ */
271
+ export async function runTestWithRetries(name, attemptFn) {
272
+ const MAX_RETRIES_PER_CODE = 6;
273
+ const retryCounts = {};
274
+ let attempt = 0;
275
+ while (true) {
276
+ attempt++;
277
+ let cleanup;
278
+ try {
279
+ const res = await attemptFn();
280
+ cleanup = res.cleanup;
281
+ if (attempt > 1) {
282
+ console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
283
+ }
284
+ // On success, we don't run cleanup here. It will be handled by afterEach.
285
+ return; // Success
286
+ }
287
+ catch (e) {
288
+ // On failure, run the cleanup from the failed attempt.
289
+ // The cleanup function is attached to the error object on failure.
290
+ const errorCleanup = e.cleanup;
291
+ if (typeof errorCleanup === "function") {
292
+ await errorCleanup().catch((err) => console.warn(`[runTestWithRetries] Cleanup failed for "${name}" during retry:`, err));
293
+ }
294
+ const errorCode = e?.code;
295
+ if (typeof errorCode === "string" && errorCode) {
296
+ const count = (retryCounts[errorCode] || 0) + 1;
297
+ retryCounts[errorCode] = count;
298
+ if (count <= MAX_RETRIES_PER_CODE) {
299
+ console.log(`[runTestWithRetries] Attempt ${attempt} for "${name}" failed with code ${errorCode}. Retrying (failure ${count}/${MAX_RETRIES_PER_CODE} for this code)...`);
300
+ await new Promise((resolve) => setTimeout(resolve, 1000));
301
+ continue; // Next attempt
302
+ }
303
+ else {
304
+ console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${MAX_RETRIES_PER_CODE} retries for this code.`);
305
+ throw e; // Give up
306
+ }
307
+ }
308
+ else {
309
+ console.error(`[runTestWithRetries] Test "${name}" failed on attempt ${attempt} with a non-retryable error:`, e);
310
+ throw e;
311
+ }
312
+ }
313
+ }
314
+ }
241
315
  /**
242
316
  * High-level test wrapper for dev server tests.
243
317
  * Automatically skips if RWSDK_SKIP_DEV=1
244
318
  */
245
319
  export function testDev(name, testFn) {
246
320
  if (SKIP_DEV_SERVER_TESTS) {
247
- test.skip(name, () => { });
321
+ test.skip(name, testFn);
248
322
  return;
249
323
  }
250
324
  test(name, async () => {
251
- const devServer = await createDevServer();
252
- const browser = await createBrowser();
253
- const page = await browser.newPage();
254
- await testFn({
255
- devServer,
256
- browser,
257
- page,
258
- url: devServer.url,
325
+ await runTestWithRetries(name, async () => {
326
+ const devServer = await createDevServer();
327
+ const browser = await createBrowser();
328
+ const page = await browser.newPage();
329
+ const cleanup = async () => {
330
+ await browser.close();
331
+ await devServer.stopDev();
332
+ };
333
+ try {
334
+ await testFn({
335
+ devServer,
336
+ browser,
337
+ page,
338
+ url: devServer.url,
339
+ });
340
+ return { cleanup };
341
+ }
342
+ catch (error) {
343
+ // Ensure cleanup is available to the retry wrapper even if testFn fails.
344
+ // We re-throw the error to be handled by runTestWithRetries.
345
+ throw Object.assign(error, { cleanup });
346
+ }
259
347
  });
260
- // Automatic cleanup handled by afterEach hooks
261
348
  });
262
349
  }
263
350
  /**
@@ -266,26 +353,70 @@ export function testDev(name, testFn) {
266
353
  testDev.skip = (name, testFn) => {
267
354
  test.skip(name, testFn || (() => { }));
268
355
  };
356
+ testDev.only = (name, testFn) => {
357
+ if (SKIP_DEV_SERVER_TESTS) {
358
+ test.skip(name, () => { });
359
+ return;
360
+ }
361
+ test.only(name, async () => {
362
+ await runTestWithRetries(name, async () => {
363
+ const devServer = await createDevServer();
364
+ const browser = await createBrowser();
365
+ const page = await browser.newPage();
366
+ const cleanup = async () => {
367
+ await browser.close();
368
+ await devServer.stopDev();
369
+ };
370
+ try {
371
+ await testFn({
372
+ devServer,
373
+ browser,
374
+ page,
375
+ url: devServer.url,
376
+ });
377
+ return { cleanup };
378
+ }
379
+ catch (error) {
380
+ // Ensure cleanup is available to the retry wrapper even if testFn fails.
381
+ // We re-throw the error to be handled by runTestWithRetries.
382
+ throw Object.assign(error, { cleanup });
383
+ }
384
+ });
385
+ });
386
+ };
269
387
  /**
270
388
  * High-level test wrapper for deployment tests.
271
389
  * Automatically skips if RWSDK_SKIP_DEPLOY=1
272
390
  */
273
391
  export function testDeploy(name, testFn) {
274
392
  if (SKIP_DEPLOYMENT_TESTS) {
275
- test.skip(name, () => { });
393
+ test.skip(name, testFn);
276
394
  return;
277
395
  }
278
396
  test(name, async () => {
279
- const deployment = await createDeployment();
280
- const browser = await createBrowser();
281
- const page = await browser.newPage();
282
- await testFn({
283
- deployment,
284
- browser,
285
- page,
286
- url: deployment.url,
397
+ await runTestWithRetries(name, async () => {
398
+ const deployment = await createDeployment();
399
+ const browser = await createBrowser();
400
+ const page = await browser.newPage();
401
+ const cleanup = async () => {
402
+ // We don't await this because we want to let it run in the background
403
+ // The afterEach hook for deployments already does this.
404
+ await cleanupDeployment(deployment);
405
+ await browser.close();
406
+ };
407
+ try {
408
+ await testFn({
409
+ deployment,
410
+ browser,
411
+ page,
412
+ url: deployment.url,
413
+ });
414
+ return { cleanup };
415
+ }
416
+ catch (error) {
417
+ throw Object.assign(error, { cleanup });
418
+ }
287
419
  });
288
- // Automatic cleanup handled by afterEach hooks
289
420
  });
290
421
  }
291
422
  /**
@@ -294,88 +425,60 @@ export function testDeploy(name, testFn) {
294
425
  testDeploy.skip = (name, testFn) => {
295
426
  test.skip(name, testFn || (() => { }));
296
427
  };
297
- /**
298
- * Unified test function that runs the same test against both dev server and deployment.
299
- * Automatically skips based on environment variables.
300
- */
301
- export function testDevAndDeploy(name, testFn) {
302
- if (SKIP_DEV_SERVER_TESTS) {
303
- test.skip(`${name} (dev)`, () => { });
304
- }
305
- else {
306
- test(`${name} (dev)`, async () => {
307
- const devServer = await createDevServer();
308
- const browser = await createBrowser();
309
- const page = await browser.newPage();
310
- await testFn({
311
- devServer,
312
- browser,
313
- page,
314
- url: devServer.url,
315
- });
316
- // Automatic cleanup handled by afterEach hooks
317
- });
318
- }
428
+ testDeploy.only = (name, testFn) => {
319
429
  if (SKIP_DEPLOYMENT_TESTS) {
320
- test.skip(`${name} (deployment)`, () => { });
430
+ test.skip(name, () => { });
431
+ return;
321
432
  }
322
- else {
323
- test(`${name} (deployment)`, async () => {
433
+ test.only(name, async () => {
434
+ await runTestWithRetries(name, async () => {
324
435
  const deployment = await createDeployment();
325
436
  const browser = await createBrowser();
326
437
  const page = await browser.newPage();
327
- await testFn({
328
- deployment,
329
- browser,
330
- page,
331
- url: deployment.url,
332
- });
333
- // Automatic cleanup handled by afterEach hooks
438
+ const cleanup = async () => {
439
+ // We don't await this because we want to let it run in the background
440
+ // The afterEach hook for deployments already does this.
441
+ await cleanupDeployment(deployment);
442
+ await browser.close();
443
+ };
444
+ try {
445
+ await testFn({
446
+ deployment,
447
+ browser,
448
+ page,
449
+ url: deployment.url,
450
+ });
451
+ return { cleanup };
452
+ }
453
+ catch (error) {
454
+ throw Object.assign(error, { cleanup });
455
+ }
334
456
  });
335
- }
457
+ });
458
+ };
459
+ /**
460
+ * Unified test function that runs the same test against both dev server and deployment.
461
+ * Automatically skips based on environment variables.
462
+ */
463
+ export function testDevAndDeploy(name, testFn) {
464
+ testDev(`${name} (dev)`, testFn);
465
+ testDeploy(`${name} (deployment)`, testFn);
336
466
  }
337
467
  /**
338
468
  * Skip version of testDevAndDeploy
339
469
  */
340
470
  testDevAndDeploy.skip = (name, testFn) => {
341
- test.skip(`${name} (dev)`, testFn || (() => { }));
342
- test.skip(`${name} (deployment)`, testFn || (() => { }));
471
+ test.skip(name, testFn || (() => { }));
343
472
  };
344
- /**
345
- * Only version of testDevAndDeploy
346
- */
347
473
  testDevAndDeploy.only = (name, testFn) => {
348
- if (!SKIP_DEV_SERVER_TESTS) {
349
- test.only(`${name} (dev)`, async () => {
350
- const devServer = await createDevServer();
351
- const browser = await createBrowser();
352
- const page = await browser.newPage();
353
- await testFn({
354
- devServer,
355
- browser,
356
- page,
357
- url: devServer.url,
358
- });
359
- });
360
- }
361
- if (!SKIP_DEPLOYMENT_TESTS) {
362
- test.only(`${name} (deployment)`, async () => {
363
- const deployment = await createDeployment();
364
- const browser = await createBrowser();
365
- const page = await browser.newPage();
366
- await testFn({
367
- deployment,
368
- browser,
369
- page,
370
- url: deployment.url,
371
- });
372
- });
373
- }
474
+ testDev.only(`${name} (dev)`, testFn);
475
+ testDeploy.only(`${name} (deployment)`, testFn);
374
476
  };
375
477
  /**
376
478
  * Utility function for polling/retrying assertions
377
479
  */
378
- export async function poll(fn, timeout = 5000, interval = 100) {
480
+ export async function poll(fn, timeout = 2 * 60 * 1000, // 2 minutes
481
+ interval = 100) {
379
482
  const startTime = Date.now();
380
483
  while (Date.now() - startTime < timeout) {
381
484
  try {
@@ -16,6 +16,7 @@ export interface SmokeTestOptions {
16
16
  realtime?: boolean;
17
17
  skipHmr?: boolean;
18
18
  skipStyleTests?: boolean;
19
+ tarballPath?: string;
19
20
  }
20
21
  /**
21
22
  * Resources created during a test run that need to be cleaned up
@@ -180,8 +180,14 @@ export function index(handler) {
180
180
  export function prefix(prefixPath, routes) {
181
181
  return routes.map((r) => {
182
182
  if (typeof r === "function") {
183
- // Pass through middleware as-is
184
- return r;
183
+ const middleware = (requestInfo) => {
184
+ const url = new URL(requestInfo.request.url);
185
+ if (url.pathname.startsWith(prefixPath)) {
186
+ return r(requestInfo);
187
+ }
188
+ return;
189
+ };
190
+ return middleware;
185
191
  }
186
192
  if (Array.isArray(r)) {
187
193
  // Recursively process nested route arrays
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import React from "react";
3
- import { matchPath, defineRoutes, route, render, layout } from "./router";
3
+ import { matchPath, defineRoutes, route, render, layout, prefix, } from "./router";
4
4
  describe("matchPath", () => {
5
5
  // Test case 1: Static paths
6
6
  it("should match static paths", () => {
@@ -209,6 +209,90 @@ describe("defineRoutes - Request Handling Behavior", () => {
209
209
  expect(await response.text()).toBe("Rendered: Element");
210
210
  });
211
211
  });
212
+ describe("Prefix Handling", () => {
213
+ it("should only run middleware within the specified prefix", async () => {
214
+ const executionOrder = [];
215
+ const prefixedMiddleware = () => {
216
+ executionOrder.push("prefixedMiddleware");
217
+ };
218
+ const PageComponent = () => {
219
+ executionOrder.push("PageComponent");
220
+ return React.createElement("div", {}, "Page");
221
+ };
222
+ const AdminPageComponent = () => {
223
+ executionOrder.push("AdminPageComponent");
224
+ return React.createElement("div", {}, "Admin Page");
225
+ };
226
+ const router = defineRoutes([
227
+ ...prefix("/admin", [
228
+ prefixedMiddleware,
229
+ route("/", AdminPageComponent),
230
+ ]),
231
+ route("/", PageComponent),
232
+ ]);
233
+ const deps = createMockDependencies();
234
+ // Test 1: Request to a path outside the prefix
235
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/");
236
+ const request1 = new Request("http://localhost:3000/");
237
+ await router.handle({
238
+ request: request1,
239
+ renderPage: deps.mockRenderPage,
240
+ getRequestInfo: deps.getRequestInfo,
241
+ onError: deps.onError,
242
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
243
+ rscActionHandler: deps.mockRscActionHandler,
244
+ });
245
+ expect(executionOrder).toEqual(["PageComponent"]);
246
+ // Reset execution order
247
+ executionOrder.length = 0;
248
+ // Test 2: Request to a path inside the prefix
249
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/admin/");
250
+ const request2 = new Request("http://localhost:3000/admin/");
251
+ await router.handle({
252
+ request: request2,
253
+ renderPage: deps.mockRenderPage,
254
+ getRequestInfo: deps.getRequestInfo,
255
+ onError: deps.onError,
256
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
257
+ rscActionHandler: deps.mockRscActionHandler,
258
+ });
259
+ expect(executionOrder).toEqual([
260
+ "prefixedMiddleware",
261
+ "AdminPageComponent",
262
+ ]);
263
+ });
264
+ it("should short-circuit from a prefixed middleware", async () => {
265
+ const executionOrder = [];
266
+ const prefixedMiddleware = () => {
267
+ executionOrder.push("prefixedMiddleware");
268
+ return new Response("From prefixed middleware");
269
+ };
270
+ const AdminPageComponent = () => {
271
+ executionOrder.push("AdminPageComponent");
272
+ return React.createElement("div", {}, "Admin Page");
273
+ };
274
+ const router = defineRoutes([
275
+ ...prefix("/admin", [
276
+ prefixedMiddleware,
277
+ route("/", AdminPageComponent),
278
+ ]),
279
+ ]);
280
+ const deps = createMockDependencies();
281
+ // Request to a path inside the prefix
282
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/admin/");
283
+ const request = new Request("http://localhost:3000/admin/");
284
+ const response = await router.handle({
285
+ request,
286
+ renderPage: deps.mockRenderPage,
287
+ getRequestInfo: deps.getRequestInfo,
288
+ onError: deps.onError,
289
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
290
+ rscActionHandler: deps.mockRscActionHandler,
291
+ });
292
+ expect(executionOrder).toEqual(["prefixedMiddleware"]);
293
+ expect(await response.text()).toBe("From prefixed middleware");
294
+ });
295
+ });
212
296
  describe("RSC Action Handling", () => {
213
297
  it("should handle RSC actions before the first route definition", async () => {
214
298
  const executionOrder = [];
@@ -6,10 +6,11 @@ import type { ViteBuilder } from "vite";
6
6
  *
7
7
  * @see docs/architecture/productionBuildProcess.md
8
8
  */
9
- export declare function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, }: {
9
+ export declare function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, workerEntryPathname, }: {
10
10
  builder: ViteBuilder;
11
11
  clientEntryPoints: Set<string>;
12
12
  clientFiles: Set<string>;
13
13
  serverFiles: Set<string>;
14
14
  projectRootDir: string;
15
+ workerEntryPathname: string;
15
16
  }): Promise<void>;
@@ -9,13 +9,14 @@ const log = debug("rwsdk:vite:build-app");
9
9
  *
10
10
  * @see docs/architecture/productionBuildProcess.md
11
11
  */
12
- export async function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, }) {
12
+ export async function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, workerEntryPathname, }) {
13
13
  const workerEnv = builder.environments.worker;
14
14
  await runDirectivesScan({
15
15
  rootConfig: builder.config,
16
16
  environments: builder.environments,
17
17
  clientFiles,
18
18
  serverFiles,
19
+ entries: [workerEntryPathname],
19
20
  });
20
21
  console.log("Building worker to discover used client components...");
21
22
  process.env.RWSDK_BUILD_PASS = "worker";
@@ -42,11 +43,14 @@ export async function buildApp({ builder, clientEntryPoints, clientFiles, server
42
43
  // Re-configure the worker environment for the linking pass
43
44
  const workerConfig = workerEnv.config;
44
45
  workerConfig.build.emptyOutDir = false;
46
+ // context(justinvdm, 22 Sep 2025): This is a workaround to satisfy the
47
+ // Cloudflare plugin's expectation of an entry chunk named `index`. The plugin
48
+ // now manages the worker build, so we no longer set rollup options
49
+ // directly. Instead, we re-point the original entry to the intermediate
50
+ // worker bundle from the first pass. This allows the linker pass to re-use
51
+ // the same plugin-driven configuration while bundling the final worker.
45
52
  workerConfig.build.rollupOptions.input = {
46
- worker: resolve(projectRootDir, "dist", "worker", "worker.js"),
47
- };
48
- workerConfig.build.rollupOptions.output = {
49
- entryFileNames: "worker.js",
53
+ index: resolve(projectRootDir, "dist", "worker", "index.js"),
50
54
  };
51
55
  await builder.build(workerEnv);
52
56
  console.log("Build complete!");
@@ -1,13 +1,11 @@
1
1
  import path, { resolve } from "node:path";
2
2
  import enhancedResolve from "enhanced-resolve";
3
- import debug from "debug";
4
3
  import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
5
4
  import { buildApp } from "./buildApp.mjs";
6
5
  import { externalModules } from "./constants.mjs";
7
- const log = debug("rwsdk:vite:config");
8
6
  export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, }) => ({
9
7
  name: "rwsdk:config",
10
- config: async (_) => {
8
+ config: async (_, { command }) => {
11
9
  const mode = process.env.NODE_ENV;
12
10
  const workerConfig = {
13
11
  resolve: {
@@ -49,14 +47,6 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
49
47
  emitAssets: true,
50
48
  emptyOutDir: false,
51
49
  ssr: true,
52
- rollupOptions: {
53
- output: {
54
- inlineDynamicImports: true,
55
- },
56
- input: {
57
- worker: workerEntryPathname,
58
- },
59
- },
60
50
  },
61
51
  };
62
52
  const baseConfig = {
@@ -176,6 +166,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
176
166
  clientEntryPoints,
177
167
  clientFiles,
178
168
  serverFiles,
169
+ workerEntryPathname,
179
170
  });
180
171
  },
181
172
  },
@@ -1,8 +1,9 @@
1
1
  import { Plugin } from "vite";
2
2
  export declare const generateVendorBarrelContent: (files: Set<string>, projectRootDir: string) => string;
3
3
  export declare const generateAppBarrelContent: (files: Set<string>, projectRootDir: string) => string;
4
- export declare const directiveModulesDevPlugin: ({ clientFiles, serverFiles, projectRootDir, }: {
4
+ export declare const directiveModulesDevPlugin: ({ clientFiles, serverFiles, projectRootDir, workerEntryPathname, }: {
5
5
  clientFiles: Set<string>;
6
6
  serverFiles: Set<string>;
7
7
  projectRootDir: string;
8
+ workerEntryPathname: string;
8
9
  }) => Plugin;
@@ -30,7 +30,7 @@ export const generateAppBarrelContent = (files, projectRootDir) => {
30
30
  })
31
31
  .join("\n");
32
32
  };
33
- export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, }) => {
33
+ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, workerEntryPathname, }) => {
34
34
  const { promise: scanPromise, resolve: resolveScanPromise } = Promise.withResolvers();
35
35
  const tempDir = mkdtempSync(path.join(os.tmpdir(), "rwsdk-"));
36
36
  const APP_CLIENT_BARREL_PATH = path.join(tempDir, "app-client-barrel.js");
@@ -47,6 +47,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
47
47
  environments: server.environments,
48
48
  clientFiles,
49
49
  serverFiles,
50
+ entries: [workerEntryPathname],
50
51
  }).then(() => {
51
52
  // context(justinvdm, 11 Sep 2025): For vendor barrels, we write the
52
53
  // files directly to disk after the scan. For app barrels, we use a
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Efficiently checks if a React directive (e.g., "use server", "use client")
3
- * is present in the code. Optimized for performance with a two-step approach:
4
- * 1. Quick string search to see if directive exists anywhere
5
- * 2. Line-by-line check only if the directive might be present
3
+ * is present in the code.
4
+ *
5
+ * This function is optimized for performance by only checking the first few
6
+ * lines of the code, as directives must appear at the very top of a file.
7
+ * It handles comments, whitespace, and any valid directive prologue
8
+ * (e.g., "use strict").
6
9
  */
7
10
  export declare function hasDirective(code: string, directive: string): boolean;