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.
- package/dist/lib/e2e/browser.mjs +18 -1
- package/dist/lib/e2e/environment.d.mts +1 -1
- package/dist/lib/e2e/environment.mjs +126 -104
- package/dist/lib/e2e/release.d.mts +1 -1
- package/dist/lib/e2e/release.mjs +52 -30
- package/dist/lib/e2e/tarball.d.mts +0 -1
- package/dist/lib/e2e/tarball.mjs +27 -117
- package/dist/lib/e2e/testHarness.d.mts +26 -1
- package/dist/lib/e2e/testHarness.mjs +192 -89
- package/dist/lib/e2e/types.d.mts +1 -0
- package/dist/runtime/lib/router.js +8 -2
- package/dist/runtime/lib/router.test.js +85 -1
- package/dist/vite/buildApp.d.mts +2 -1
- package/dist/vite/buildApp.mjs +9 -5
- package/dist/vite/configPlugin.mjs +2 -11
- package/dist/vite/directiveModulesDevPlugin.d.mts +2 -1
- package/dist/vite/directiveModulesDevPlugin.mjs +2 -1
- package/dist/vite/hasDirective.d.mts +6 -3
- package/dist/vite/hasDirective.mjs +43 -27
- package/dist/vite/hasDirective.test.mjs +72 -74
- package/dist/vite/redwoodPlugin.mjs +1 -0
- package/dist/vite/runDirectivesScan.d.mts +2 -1
- package/dist/vite/runDirectivesScan.mjs +8 -4
- package/package.json +7 -7
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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(
|
|
430
|
+
test.skip(name, () => { });
|
|
431
|
+
return;
|
|
321
432
|
}
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
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(
|
|
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
|
-
|
|
349
|
-
|
|
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 =
|
|
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 {
|
package/dist/lib/e2e/types.d.mts
CHANGED
|
@@ -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
|
-
|
|
184
|
-
|
|
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 = [];
|
package/dist/vite/buildApp.d.mts
CHANGED
|
@@ -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>;
|
package/dist/vite/buildApp.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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;
|