rwsdk 1.0.0-alpha.8 → 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/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
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;
|
|
@@ -1,54 +1,70 @@
|
|
|
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 function hasDirective(code, directive) {
|
|
8
|
-
|
|
9
|
-
const singleQuoteDirective = `'${directive}'`;
|
|
10
|
-
const doubleQuoteDirective = `"${directive}"`;
|
|
11
|
-
if (!code.includes(singleQuoteDirective) &&
|
|
12
|
-
!code.includes(doubleQuoteDirective)) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
// Split into lines and check each one
|
|
16
|
-
const lines = code.split("\n");
|
|
11
|
+
const lines = code.slice(0, 512).split("\n"); // Check first ~512 chars
|
|
17
12
|
let inMultiLineComment = false;
|
|
13
|
+
let foundUseClient = false;
|
|
14
|
+
let foundTargetDirective = false;
|
|
15
|
+
const doubleQuoteDirective = `"${directive}"`;
|
|
16
|
+
const singleQuoteDirective = `'${directive}'`;
|
|
17
|
+
const doubleQuoteUseClient = `"use client"`;
|
|
18
|
+
const singleQuoteUseClient = `'use client'`;
|
|
18
19
|
for (const line of lines) {
|
|
19
20
|
const trimmedLine = line.trim();
|
|
20
|
-
// Skip empty lines
|
|
21
21
|
if (trimmedLine.length === 0) {
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
if (trimmedLine.startsWith("/*")) {
|
|
26
|
-
inMultiLineComment = true;
|
|
27
|
-
// Check if the comment ends on the same line
|
|
24
|
+
if (inMultiLineComment) {
|
|
28
25
|
if (trimmedLine.includes("*/")) {
|
|
29
26
|
inMultiLineComment = false;
|
|
30
27
|
}
|
|
31
28
|
continue;
|
|
32
29
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
inMultiLineComment = false;
|
|
30
|
+
if (trimmedLine.startsWith("/*")) {
|
|
31
|
+
if (!trimmedLine.includes("*/")) {
|
|
32
|
+
inMultiLineComment = true;
|
|
37
33
|
}
|
|
38
34
|
continue;
|
|
39
35
|
}
|
|
40
|
-
// Skip single-line comments
|
|
41
36
|
if (trimmedLine.startsWith("//")) {
|
|
42
37
|
continue;
|
|
43
38
|
}
|
|
44
|
-
|
|
39
|
+
const cleanedLine = trimmedLine.endsWith(";")
|
|
40
|
+
? trimmedLine.slice(0, -1)
|
|
41
|
+
: trimmedLine;
|
|
42
|
+
if (trimmedLine.startsWith(doubleQuoteUseClient) ||
|
|
43
|
+
trimmedLine.startsWith(singleQuoteUseClient)) {
|
|
44
|
+
foundUseClient = true;
|
|
45
|
+
if (directive === "use client") {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
45
49
|
if (trimmedLine.startsWith(doubleQuoteDirective) ||
|
|
46
50
|
trimmedLine.startsWith(singleQuoteDirective)) {
|
|
47
|
-
|
|
51
|
+
foundTargetDirective = true;
|
|
52
|
+
if (directive !== "use server") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
|
-
//
|
|
50
|
-
//
|
|
56
|
+
// Any other string literal is part of a valid directive prologue.
|
|
57
|
+
// We can continue searching.
|
|
58
|
+
if (trimmedLine.startsWith('"') || trimmedLine.startsWith("'")) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// If we encounter any other non-directive, non-comment, non-string-literal
|
|
62
|
+
// line of code, the directive prologue is over. Stop.
|
|
51
63
|
break;
|
|
52
64
|
}
|
|
53
|
-
return false
|
|
65
|
+
// If looking for 'use server' and 'use client' was found, return false (client takes priority)
|
|
66
|
+
if (directive === "use server" && foundUseClient) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return foundTargetDirective;
|
|
54
70
|
}
|