rwsdk 1.2.8 → 1.2.10

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.
Files changed (38) hide show
  1. package/dist/lib/e2e/browser.d.mts +4 -0
  2. package/dist/lib/e2e/browser.mjs +58 -0
  3. package/dist/lib/e2e/constants.d.mts +2 -0
  4. package/dist/lib/e2e/constants.mjs +6 -0
  5. package/dist/lib/e2e/dev.mjs +7 -18
  6. package/dist/lib/e2e/release.d.mts +7 -0
  7. package/dist/lib/e2e/release.mjs +78 -2
  8. package/dist/lib/e2e/tarball.mjs +4 -1
  9. package/dist/lib/e2e/testHarness.d.mts +1 -7
  10. package/dist/lib/e2e/testHarness.mjs +30 -10
  11. package/dist/lib/smokeTests/browser.d.mts +1 -1
  12. package/dist/lib/smokeTests/browser.mjs +36 -30
  13. package/dist/lib/smokeTests/release.d.mts +8 -1
  14. package/dist/lib/smokeTests/release.mjs +54 -29
  15. package/dist/lib/smokeTests/runSmokeTests.mjs +1 -1
  16. package/dist/runtime/client/navigation.js +42 -61
  17. package/dist/runtime/client/navigation.test.js +145 -8
  18. package/dist/runtime/client/scrollRestoration.d.ts +25 -0
  19. package/dist/runtime/client/scrollRestoration.js +157 -0
  20. package/dist/runtime/client/scrollRestoration.test.d.ts +1 -0
  21. package/dist/runtime/client/scrollRestoration.test.js +93 -0
  22. package/dist/runtime/lib/router.d.ts +1 -0
  23. package/dist/runtime/lib/router.js +27 -2
  24. package/dist/runtime/lib/router.test.js +96 -0
  25. package/dist/scripts/worker-run.mjs +42 -8
  26. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +14 -7
  27. package/dist/use-synced-state/__tests__/worker.test.mjs +41 -2
  28. package/dist/use-synced-state/worker.mjs +34 -3
  29. package/dist/vite/miniflareHMRPlugin.mjs +1 -1
  30. package/dist/vite/ssrBridgePlugin.d.mts +0 -1
  31. package/dist/vite/ssrBridgePlugin.mjs +17 -14
  32. package/dist/vite/ssrVirtualModule.d.mts +3 -0
  33. package/dist/vite/ssrVirtualModule.mjs +11 -0
  34. package/dist/vite/transformJsxScriptTagsPlugin.hook.test.d.mts +1 -0
  35. package/dist/vite/transformJsxScriptTagsPlugin.hook.test.mjs +73 -0
  36. package/dist/vite/transformJsxScriptTagsPlugin.mjs +5 -0
  37. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +4 -3
  38. package/package.json +2 -3
@@ -4,6 +4,10 @@ import { SmokeTestOptions } from "./types.mjs";
4
4
  * Launch a browser instance
5
5
  */
6
6
  export declare function launchBrowser(browserPath?: string, headless?: boolean): Promise<Browser>;
7
+ /**
8
+ * Check if a server is up, trying localhost and loopback host variants.
9
+ */
10
+ export declare function checkServerUp(baseUrl: string, customPath?: string, retries?: number, includeRoot?: boolean): Promise<string>;
7
11
  /**
8
12
  * Get the browser executable path
9
13
  */
@@ -2,7 +2,9 @@ import { computeExecutablePath, detectBrowserPlatform, install, Browser as Puppe
2
2
  import debug from "debug";
3
3
  import { mkdirp, pathExists } from "fs-extra";
4
4
  import { join } from "path";
5
+ import { chromium as playwrightChromium } from "playwright-core";
5
6
  import puppeteer from "puppeteer-core";
7
+ import { $ } from "../$.mjs";
6
8
  import { ensureTmpDir } from "./utils.mjs";
7
9
  const log = debug("rwsdk:e2e:browser");
8
10
  /**
@@ -26,6 +28,52 @@ export async function launchBrowser(browserPath, headless = true) {
26
28
  args: ["--no-sandbox", "--disable-setuid-sandbox"],
27
29
  });
28
30
  }
31
+ /**
32
+ * Check if a server is up, trying localhost and loopback host variants.
33
+ */
34
+ export async function checkServerUp(baseUrl, customPath = "/", retries = 30, includeRoot = true) {
35
+ const pathsToCheck = includeRoot ? ["/"] : [];
36
+ if (customPath !== "/" && customPath !== "") {
37
+ pathsToCheck.push(customPath);
38
+ }
39
+ if (pathsToCheck.length === 0) {
40
+ pathsToCheck.push("/");
41
+ }
42
+ for (const path of pathsToCheck) {
43
+ const normalizedPath = path.startsWith("/") ? path : "/" + path;
44
+ const baseUrlObject = new URL(baseUrl);
45
+ const candidateBaseUrls = Array.from(new Set([
46
+ baseUrl,
47
+ `${baseUrlObject.protocol}//127.0.0.1:${baseUrlObject.port}`,
48
+ `${baseUrlObject.protocol}//[::1]:${baseUrlObject.port}`,
49
+ ]));
50
+ const candidateUrls = candidateBaseUrls.map((candidateBaseUrl) => candidateBaseUrl + normalizedPath);
51
+ log("Checking if server is up at %s (max retries: %d)", candidateUrls[0], retries);
52
+ for (let i = 0; i < retries; i++) {
53
+ for (const candidateUrl of candidateUrls) {
54
+ try {
55
+ log("Attempt %d/%d to check server at %s", i + 1, retries, candidateUrl);
56
+ console.log(`Checking if server is up at ${candidateUrl} (attempt ${i + 1}/${retries})...`);
57
+ await $ `curl --max-time 1 -s -o /dev/null -w "%{http_code}" ${candidateUrl}`;
58
+ log("Server is up at %s", candidateUrl);
59
+ console.log(`āœ… Server is up at ${candidateUrl}`);
60
+ return candidateUrl;
61
+ }
62
+ catch {
63
+ // Try the next host variant.
64
+ }
65
+ }
66
+ if (i === retries - 1) {
67
+ log("ERROR: Server at %s did not become available after %d attempts", candidateUrls[0], retries);
68
+ throw new Error(`Server at ${candidateUrls[0]} did not become available after ${retries} attempts`);
69
+ }
70
+ log("Server not up yet, retrying in 2 seconds");
71
+ console.log(`Server not up yet, retrying in 2 seconds...`);
72
+ await new Promise((resolve) => setTimeout(() => resolve(), 2000));
73
+ }
74
+ }
75
+ return "";
76
+ }
29
77
  /**
30
78
  * Get the browser executable path
31
79
  */
@@ -36,6 +84,16 @@ export async function getBrowserPath(testOptions) {
36
84
  console.log(`Using Chrome from environment variable: ${process.env.CHROME_PATH}`);
37
85
  return process.env.CHROME_PATH;
38
86
  }
87
+ try {
88
+ const playwrightPath = playwrightChromium.executablePath();
89
+ if (await pathExists(playwrightPath)) {
90
+ console.log(`Found Playwright Chrome at: ${playwrightPath}`);
91
+ return playwrightPath;
92
+ }
93
+ }
94
+ catch (error) {
95
+ log("Playwright browser path unavailable: %O", error);
96
+ }
39
97
  // Detect platform
40
98
  log("Detecting platform");
41
99
  const platform = detectBrowserPlatform();
@@ -1,4 +1,5 @@
1
1
  export declare const IS_CI: boolean;
2
+ export declare const IS_PULL_REQUEST: boolean;
2
3
  export declare const RWSDK_SKIP_DEV: boolean;
3
4
  export declare const RWSDK_SKIP_DEPLOY: boolean;
4
5
  export declare const IS_DEBUG_MODE: boolean;
@@ -6,6 +7,7 @@ export declare const SETUP_PLAYGROUND_ENV_TIMEOUT: number;
6
7
  export declare const DEPLOYMENT_TIMEOUT: number;
7
8
  export declare const DEPLOYMENT_MIN_TRIES: number;
8
9
  export declare const DEPLOYMENT_CHECK_TIMEOUT: number;
10
+ export declare const PREVIEW_SERVER_TIMEOUT: number;
9
11
  export declare const PUPPETEER_TIMEOUT: number;
10
12
  export declare const HYDRATION_TIMEOUT: number;
11
13
  export declare const DEV_SERVER_TIMEOUT: number;
@@ -5,6 +5,7 @@ export const IS_CI = !!((process.env.CI && !process.env.NOT_CI) ||
5
5
  process.env.TRAVIS ||
6
6
  process.env.JENKINS_URL ||
7
7
  process.env.NETLIFY);
8
+ export const IS_PULL_REQUEST = process.env.GITHUB_EVENT_NAME === "pull_request";
8
9
  export const RWSDK_SKIP_DEV = Boolean(process.env.RWSDK_SKIP_DEV);
9
10
  export const RWSDK_SKIP_DEPLOY = process.env.RWSDK_SKIP_DEPLOY === "true" ||
10
11
  process.env.RWSDK_SKIP_DEPLOY === "1";
@@ -33,6 +34,11 @@ export const DEPLOYMENT_CHECK_TIMEOUT = process.env
33
34
  : IS_DEBUG_MODE
34
35
  ? 30 * 1000
35
36
  : 5 * 60 * 1000;
37
+ export const PREVIEW_SERVER_TIMEOUT = process.env.RWSDK_PREVIEW_SERVER_TIMEOUT
38
+ ? parseInt(process.env.RWSDK_PREVIEW_SERVER_TIMEOUT, 10)
39
+ : IS_DEBUG_MODE
40
+ ? 60 * 1000
41
+ : 10 * 60 * 1000;
36
42
  export const PUPPETEER_TIMEOUT = process.env.RWSDK_PUPPETEER_TIMEOUT
37
43
  ? parseInt(process.env.RWSDK_PUPPETEER_TIMEOUT, 10)
38
44
  : IS_DEBUG_MODE
@@ -1,7 +1,7 @@
1
1
  import debug from "debug";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
3
  import { $, $sh } from "../../lib/$.mjs";
4
- import { poll } from "./poll.mjs";
4
+ import { checkServerUp } from "./browser.mjs";
5
5
  import { IS_DEBUG_MODE } from "./constants.mjs";
6
6
  const DEV_SERVER_CHECK_TIMEOUT = process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT
7
7
  ? parseInt(process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT, 10)
@@ -205,23 +205,12 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
205
205
  // Wait for the URL
206
206
  const serverUrl = await waitForUrl();
207
207
  console.log(`āœ… Development server started at ${serverUrl}`);
208
- // Poll the URL to ensure it's live before proceeding
209
- await poll(async () => {
210
- try {
211
- const response = await fetch(serverUrl, {
212
- signal: AbortSignal.timeout(1000),
213
- });
214
- // We consider any response (even 4xx or 5xx) as success,
215
- // as it means the worker is routable.
216
- return response.status > 0;
217
- }
218
- catch (e) {
219
- return false;
220
- }
221
- }, {
222
- timeout: DEV_SERVER_CHECK_TIMEOUT,
223
- });
224
- return { url: serverUrl, stopDev };
208
+ // context(justinvdm, 2026-05-13): Probe the debug endpoint instead of
209
+ // the app root because the root response can take several seconds to wake
210
+ // up even after Vite prints its local URL.
211
+ const readinessRetries = Math.max(1, Math.ceil(DEV_SERVER_CHECK_TIMEOUT / 2000));
212
+ const reachableDebugUrl = await checkServerUp(serverUrl, "/__debug", readinessRetries, false);
213
+ return { url: new URL(reachableDebugUrl).origin, stopDev };
225
214
  }
226
215
  catch (error) {
227
216
  // Make sure to try to stop the server on error
@@ -36,6 +36,13 @@ export declare function runRelease(cwd: string, projectDir: string, resourceUniq
36
36
  url: string;
37
37
  workerName: string;
38
38
  }>;
39
+ /**
40
+ * Run a local production preview server (build + preview) and return the URL.
41
+ */
42
+ export declare function runPreviewServer(packageManager?: string, cwd?: string): Promise<{
43
+ url: string;
44
+ stopPreview: () => Promise<void>;
45
+ }>;
39
46
  /**
40
47
  * Check if a resource name includes a specific resource unique key
41
48
  * This is used to identify resources created during our tests
@@ -5,10 +5,11 @@ import { pathExists } from "fs-extra";
5
5
  import * as fs from "fs/promises";
6
6
  import { parse as parseJsonc } from "jsonc-parser";
7
7
  import { setTimeout } from "node:timers/promises";
8
- import { basename, dirname, join, resolve } from "path";
8
+ import { basename, dirname, join, relative, resolve } from "path";
9
9
  import { $ } from "../../lib/$.mjs";
10
+ import { checkServerUp } from "./browser.mjs";
10
11
  import { extractLastJson, parseJson } from "../../lib/jsonUtils.mjs";
11
- import { IS_DEBUG_MODE } from "./constants.mjs";
12
+ import { IS_DEBUG_MODE, PREVIEW_SERVER_TIMEOUT } from "./constants.mjs";
12
13
  const log = debug("rwsdk:e2e:release");
13
14
  /**
14
15
  * Find wrangler cache by searching up the directory tree for node_modules/.cache/wrangler
@@ -400,6 +401,81 @@ export async function runRelease(cwd, projectDir, resourceUniqueKey) {
400
401
  throw error;
401
402
  }
402
403
  }
404
+ /**
405
+ * Run a local production preview server (build + preview) and return the URL.
406
+ */
407
+ export async function runPreviewServer(packageManager = "pnpm", cwd) {
408
+ console.log("šŸš€ Building for production preview...");
409
+ const pm = packageManager === "yarn-classic" ? "yarn" : packageManager;
410
+ await $(pm, ["run", "build"], {
411
+ cwd: cwd || process.cwd(),
412
+ stdio: "pipe",
413
+ env: { ...process.env, NODE_ENV: "production" },
414
+ });
415
+ console.log("āœ… Build complete. Starting preview server...");
416
+ let previewProcess = null;
417
+ let isErrorExpected = false;
418
+ const stopPreview = async () => {
419
+ isErrorExpected = true;
420
+ if (!previewProcess || !previewProcess.pid) {
421
+ return;
422
+ }
423
+ console.log("Stopping preview server...");
424
+ if (process.platform !== "win32") {
425
+ try {
426
+ process.kill(-previewProcess.pid, "SIGKILL");
427
+ }
428
+ catch { }
429
+ }
430
+ await previewProcess.catch(() => { });
431
+ console.log("Preview server stopped");
432
+ };
433
+ previewProcess = $(pm, ["run", "preview", "--", "--port", "4173", "--strictPort"], {
434
+ all: true,
435
+ detached: process.platform !== "win32",
436
+ cleanup: true,
437
+ forceKillAfterTimeout: 2000,
438
+ cwd: cwd || process.cwd(),
439
+ env: { ...process.env, NODE_ENV: "production" },
440
+ stdio: "pipe",
441
+ });
442
+ previewProcess.catch((error) => {
443
+ if (!isErrorExpected) {
444
+ log("Preview server process exited unexpectedly: %O", error);
445
+ }
446
+ });
447
+ const ensurePreviewDeployConfig = async () => {
448
+ const deployConfigPath = resolve(cwd || process.cwd(), ".wrangler/deploy/config.json");
449
+ if (await pathExists(deployConfigPath)) {
450
+ return;
451
+ }
452
+ let workerConfigPath = null;
453
+ for (const candidate of ["wrangler.jsonc", "wrangler.json"]) {
454
+ const resolvedCandidate = resolve(cwd || process.cwd(), candidate);
455
+ if (await pathExists(resolvedCandidate)) {
456
+ workerConfigPath = resolvedCandidate;
457
+ break;
458
+ }
459
+ }
460
+ if (!workerConfigPath) {
461
+ throw new Error(`Unable to create preview deploy config because no wrangler.jsonc or wrangler.json was found in ${cwd || process.cwd()}`);
462
+ }
463
+ await fs.mkdir(dirname(deployConfigPath), { recursive: true });
464
+ const deployConfig = {
465
+ configPath: relative(dirname(deployConfigPath), workerConfigPath),
466
+ auxiliaryWorkers: [],
467
+ };
468
+ await fs.writeFile(deployConfigPath, JSON.stringify(deployConfig, null, 2));
469
+ };
470
+ await ensurePreviewDeployConfig();
471
+ // context(justinvdm, 2026-05-13): Give the CI preview path the same
472
+ // readiness budget as the dev server so local agent-ci runs can absorb build
473
+ // and startup latency without falling back to Cloudflare.
474
+ const reachableDebugUrl = await checkServerUp("http://localhost:4173", "/__debug", Math.max(1, Math.ceil(PREVIEW_SERVER_TIMEOUT / 2000)), false);
475
+ const serverUrl = new URL(reachableDebugUrl).origin;
476
+ console.log(`āœ… Preview server started at ${serverUrl}`);
477
+ return { url: serverUrl, stopPreview };
478
+ }
403
479
  /**
404
480
  * Check if a resource name includes a specific resource unique key
405
481
  * This is used to identify resources created during our tests
@@ -3,6 +3,7 @@ import { $ } from "execa";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { ROOT_DIR } from "../constants.mjs";
6
+ import { IS_CI } from "./constants.mjs";
6
7
  import { copyProjectToTempDir } from "./environment.mjs";
7
8
  const log = (message) => console.log(message);
8
9
  /**
@@ -51,7 +52,9 @@ async function copyWranglerCache(targetDir, sdkRoot) {
51
52
  log(` āœ… Wrangler cache copied successfully`);
52
53
  }
53
54
  else {
54
- log(` āš ļø No wrangler cache found in monorepo, deployment tests may require authentication`);
55
+ log(IS_CI
56
+ ? ` ā„¹ļø No wrangler cache found in monorepo; CI preview tests do not require it`
57
+ : ` āš ļø No wrangler cache found in monorepo, deployment tests may require authentication`);
55
58
  }
56
59
  }
57
60
  catch (error) {
@@ -66,13 +66,7 @@ export declare function createDevServer(): {
66
66
  */
67
67
  export declare function createDeployment(): {
68
68
  projectDir: string;
69
- start: () => Promise<{
70
- url: string;
71
- workerName: string;
72
- resourceUniqueKey: string;
73
- projectDir: string;
74
- cleanup: () => Promise<void>;
75
- }>;
69
+ start: () => Promise<DeploymentInstance>;
76
70
  };
77
71
  /**
78
72
  * Executes a test function with a retry mechanism for specific error codes.
@@ -4,10 +4,10 @@ import puppeteer from "puppeteer-core";
4
4
  import { fileURLToPath } from "url";
5
5
  import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
6
  import { launchBrowser } from "./browser.mjs";
7
- import { 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, } from "./constants.mjs";
7
+ import { DEPLOYMENT_CHECK_TIMEOUT, DEPLOYMENT_MIN_TRIES, DEPLOYMENT_TIMEOUT, DEV_SERVER_MIN_TRIES, DEV_SERVER_TIMEOUT, HYDRATION_TIMEOUT, INSTALL_DEPENDENCIES_RETRIES, IS_PULL_REQUEST, PUPPETEER_TIMEOUT, SETUP_PLAYGROUND_ENV_TIMEOUT, SETUP_WAIT_TIMEOUT, TEST_MAX_RETRIES, TEST_MAX_RETRIES_PER_CODE, } from "./constants.mjs";
8
8
  import { runDevServer } from "./dev.mjs";
9
9
  import { poll, pollValue } from "./poll.mjs";
10
- import { deleteD1Database, deleteWorker, isRelatedToTest, runRelease, } from "./release.mjs";
10
+ import { deleteD1Database, deleteWorker, isRelatedToTest, runPreviewServer, runRelease, } from "./release.mjs";
11
11
  import { setupTarballEnvironment } from "./tarball.mjs";
12
12
  import { ensureTmpDir } from "./utils.mjs";
13
13
  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, };
@@ -221,15 +221,34 @@ export function createDeployment() {
221
221
  if (SKIP_DEPLOYMENT_TESTS) {
222
222
  throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
223
223
  }
224
+ const dirName = basename(projectDir);
225
+ // Match formats: {projectName}-t-{hash}, {projectName}-test-{hash}, or {projectName}-e2e-test-{hash}
226
+ const match = dirName.match(/-t-([a-f0-9]+)$/) ||
227
+ dirName.match(/-test-([a-f0-9]+)$/) ||
228
+ dirName.match(/-e2e-test-([a-f0-9]+)$/);
229
+ const resourceUniqueKey = match
230
+ ? match[1]
231
+ : Math.random().toString(36).substring(2, 15);
232
+ if (IS_PULL_REQUEST) {
233
+ console.log("PR mode detected — using local preview instead of deploy");
234
+ const previewResult = await runPreviewServer(process.env.PACKAGE_MANAGER ||
235
+ "pnpm", projectDir);
236
+ instance = {
237
+ url: previewResult.url,
238
+ workerName: `preview-${resourceUniqueKey}`,
239
+ resourceUniqueKey,
240
+ projectDir,
241
+ cleanup: async () => {
242
+ await previewResult.stopPreview().catch((error) => {
243
+ console.warn(`Warning: Background preview cleanup failed: ${error.message}`);
244
+ });
245
+ return Promise.resolve();
246
+ },
247
+ };
248
+ deploymentInstances.push(instance);
249
+ return instance;
250
+ }
224
251
  const newInstance = await pollValue(async () => {
225
- const dirName = basename(projectDir);
226
- // Match formats: {projectName}-t-{hash}, {projectName}-test-{hash}, or {projectName}-e2e-test-{hash}
227
- const match = dirName.match(/-t-([a-f0-9]+)$/) ||
228
- dirName.match(/-test-([a-f0-9]+)$/) ||
229
- dirName.match(/-e2e-test-([a-f0-9]+)$/);
230
- const resourceUniqueKey = match
231
- ? match[1]
232
- : Math.random().toString(36).substring(2, 15);
233
252
  const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
234
253
  // A fresh *.workers.dev subdomain can return 200 with Cloudflare's
235
254
  // "There is nothing here yet" placeholder before the worker code
@@ -273,6 +292,7 @@ export function createDeployment() {
273
292
  console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
274
293
  },
275
294
  });
295
+ instance = newInstance;
276
296
  deploymentInstances.push(newInstance);
277
297
  return newInstance;
278
298
  },
@@ -37,7 +37,7 @@ bail?: boolean): Promise<void>;
37
37
  /**
38
38
  * DRY: checkServerUp now checks both root and custom path if needed
39
39
  */
40
- export declare function checkServerUp(baseUrl: string, customPath?: string, retries?: number, bail?: boolean): Promise<boolean>;
40
+ export declare function checkServerUp(baseUrl: string, customPath?: string, retries?: number, bail?: boolean): Promise<string>;
41
41
  /**
42
42
  * HMR test for server component
43
43
  * Updates the server component and verifies that HMR applies the changes
@@ -749,42 +749,48 @@ export async function checkServerUp(baseUrl, customPath = "/", retries = RETRIES
749
749
  pathsToCheck.push(customPath);
750
750
  }
751
751
  for (const path of pathsToCheck) {
752
- const url = baseUrl + (path.startsWith("/") ? path : "/" + path);
753
- log("Checking if server is up at %s (max retries: %d)", url, retries);
754
- let up = false;
752
+ const normalizedPath = path.startsWith("/") ? path : "/" + path;
753
+ const baseUrlObject = new URL(baseUrl);
754
+ const candidateBaseUrls = Array.from(new Set([
755
+ baseUrl,
756
+ `${baseUrlObject.protocol}//127.0.0.1:${baseUrlObject.port}`,
757
+ `${baseUrlObject.protocol}//[::1]:${baseUrlObject.port}`,
758
+ ]));
759
+ const candidateUrls = candidateBaseUrls.map((candidateBaseUrl) => candidateBaseUrl + normalizedPath);
760
+ log("Checking if server is up at %s (max retries: %d)", candidateUrls[0], retries);
755
761
  for (let i = 0; i < retries; i++) {
756
- try {
757
- log("Attempt %d/%d to check server at %s", i + 1, retries, url);
758
- console.log(`Checking if server is up at ${url} (attempt ${i + 1}/${retries})...`);
759
- await $ `curl -s -o /dev/null -w "%{http_code}" ${url}`;
760
- log("Server is up at %s", url);
761
- console.log(`āœ… Server is up at ${url}`);
762
- up = true;
763
- break;
762
+ for (const candidateUrl of candidateUrls) {
763
+ try {
764
+ log("Attempt %d/%d to check server at %s", i + 1, retries, candidateUrl);
765
+ console.log(`Checking if server is up at ${candidateUrl} (attempt ${i + 1}/${retries})...`);
766
+ await $ `curl --max-time 1 -s -o /dev/null -w "%{http_code}" ${candidateUrl}`;
767
+ log("Server is up at %s", candidateUrl);
768
+ console.log(`āœ… Server is up at ${candidateUrl}`);
769
+ return candidateUrl;
770
+ }
771
+ catch {
772
+ // Try the next host variant.
773
+ }
764
774
  }
765
- catch (error) {
766
- if (i === retries - 1) {
767
- log("ERROR: Server at %s did not become available after %d attempts", url, retries);
768
- const errorMessage = `Server at ${url} did not become available after ${retries} attempts`;
769
- if (bail) {
770
- // If bail is true, call fail() which will exit the process
771
- await fail(new Error(errorMessage), 1, `Server Availability Check: ${url}`);
772
- return false; // This will never be reached due to fail() exiting
773
- }
774
- else {
775
- // Otherwise throw an error that can be caught by the caller
776
- throw new Error(errorMessage);
777
- }
775
+ if (i === retries - 1) {
776
+ log("ERROR: Server at %s did not become available after %d attempts", candidateUrls[0], retries);
777
+ const errorMessage = `Server at ${candidateUrls[0]} did not become available after ${retries} attempts`;
778
+ if (bail) {
779
+ // If bail is true, call fail() which will exit the process
780
+ await fail(new Error(errorMessage), 1, `Server Availability Check: ${candidateUrls[0]}`);
781
+ return ""; // This will never be reached due to fail() exiting
782
+ }
783
+ else {
784
+ // Otherwise throw an error that can be caught by the caller
785
+ throw new Error(errorMessage);
778
786
  }
779
- log("Server not up yet, retrying in 2 seconds");
780
- console.log(`Server not up yet, retrying in 2 seconds...`);
781
- await new Promise((resolve) => setTimeout(() => resolve(), 2000));
782
787
  }
788
+ log("Server not up yet, retrying in 2 seconds");
789
+ console.log(`Server not up yet, retrying in 2 seconds...`);
790
+ await new Promise((resolve) => setTimeout(() => resolve(), 2000));
783
791
  }
784
- if (!up)
785
- return false;
786
792
  }
787
- return true;
793
+ return "";
788
794
  }
789
795
  /**
790
796
  * Perform only the realtime upgrade and tests without doing initial checks
@@ -8,7 +8,14 @@ export declare function runRelease(cwd: string, projectDir: string, resourceUniq
8
8
  url: string;
9
9
  workerName: string;
10
10
  }>;
11
+ /**
12
+ * Run the local preview server (build + preview) and return the URL
13
+ */
14
+ export declare function runPreviewServer(packageManager?: string, cwd?: string): Promise<{
15
+ url: string;
16
+ stopPreview: () => Promise<void>;
17
+ }>;
11
18
  /**
12
19
  * Runs tests against the production deployment
13
20
  */
14
- export declare function runReleaseTest(artifactDir: string, resources: TestResources, browserPath?: string, headless?: boolean, bail?: boolean, skipClient?: boolean, projectDir?: string, realtime?: boolean, skipHmr?: boolean, skipStyleTests?: boolean): Promise<void>;
21
+ export declare function runReleaseTest(artifactDir: string, resources: TestResources, browserPath?: string, headless?: boolean, bail?: boolean, skipClient?: boolean, projectDir?: string, realtime?: boolean, skipHmr?: boolean, skipStyleTests?: boolean, ci?: boolean): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
- import { $expect, deleteD1Database, deleteWorker, isRelatedToTest, listD1Databases, runRelease as runE2ERelease, } from "../../lib/e2e/release.mjs";
2
+ import { $expect, deleteD1Database, deleteWorker, isRelatedToTest, listD1Databases, runPreviewServer as runE2EPreviewServer, runRelease as runE2ERelease, } from "../../lib/e2e/release.mjs";
3
3
  import { checkServerUp, checkUrl } from "./browser.mjs";
4
4
  import { log } from "./constants.mjs";
5
+ import { state } from "./state.mjs";
5
6
  export { $expect, deleteD1Database, deleteWorker, isRelatedToTest, listD1Databases, };
6
7
  /**
7
8
  * Run the release command to deploy to Cloudflare
@@ -9,6 +10,12 @@ export { $expect, deleteD1Database, deleteWorker, isRelatedToTest, listD1Databas
9
10
  export async function runRelease(cwd, projectDir, resourceUniqueKey) {
10
11
  return runE2ERelease(cwd, projectDir, resourceUniqueKey);
11
12
  }
13
+ /**
14
+ * Run the local preview server (build + preview) and return the URL
15
+ */
16
+ export async function runPreviewServer(packageManager = "pnpm", cwd) {
17
+ return runE2EPreviewServer(packageManager, cwd);
18
+ }
12
19
  async function waitForDeploymentContent(baseUrl, { timeoutMs = 60_000, intervalMs = 2_000, } = {}) {
13
20
  const marker = "__RWSDK_CONTEXT";
14
21
  const deadline = Date.now() + timeoutMs;
@@ -41,46 +48,64 @@ async function waitForDeploymentContent(baseUrl, { timeoutMs = 60_000, intervalM
41
48
  /**
42
49
  * Runs tests against the production deployment
43
50
  */
44
- export async function runReleaseTest(artifactDir, resources, browserPath, headless = true, bail = false, skipClient = false, projectDir, realtime = false, skipHmr = false, skipStyleTests = false) {
51
+ export async function runReleaseTest(artifactDir, resources, browserPath, headless = true, bail = false, skipClient = false, projectDir, realtime = false, skipHmr = false, skipStyleTests = false, ci = false) {
45
52
  log("Starting release test");
46
53
  console.log("\nšŸš€ Testing production deployment");
54
+ let url;
55
+ let stopPreview;
47
56
  try {
48
- log("Running release process");
49
- const { url, workerName } = await runRelease(resources.targetDir || "", projectDir || "", resources.resourceUniqueKey);
50
- // Wait a moment before checking server availability
51
- log("Waiting 1s before checking server...");
52
- await setTimeout(1000);
53
- // DRY: check both root and custom path
54
- await checkServerUp(url, "/");
55
- // A fresh *.workers.dev subdomain can return 200 with Cloudflare's
56
- // "There is nothing here yet" placeholder before the worker code is
57
- // globally propagated. Poll the URL until the response body contains
58
- // an rwsdk-rendered marker so we don't run the browser tests against
59
- // the placeholder.
60
- await waitForDeploymentContent(url);
57
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
58
+ log("PR mode detected — using local preview instead of deploy");
59
+ const previewResult = await runPreviewServer(state.options.packageManager, resources.targetDir || "");
60
+ url = previewResult.url;
61
+ stopPreview = previewResult.stopPreview;
62
+ }
63
+ else {
64
+ log("Running release process");
65
+ const { url: deployUrl, workerName } = await runRelease(resources.targetDir || "", projectDir || "", resources.resourceUniqueKey);
66
+ url = deployUrl;
67
+ // Wait a moment before checking server availability
68
+ log("Waiting 1s before checking server...");
69
+ await setTimeout(1000);
70
+ // DRY: check both root and custom path
71
+ await checkServerUp(url, "/");
72
+ // A fresh *.workers.dev subdomain can return 200 with Cloudflare's
73
+ // "There is nothing here yet" placeholder before the worker code is
74
+ // globally propagated. Poll the URL until the response body contains
75
+ // an rwsdk-rendered marker so we don't run the browser tests against
76
+ // the placeholder.
77
+ await waitForDeploymentContent(url);
78
+ // Store the worker name if we didn't set it earlier
79
+ if (resources && !resources.workerName) {
80
+ log("Storing worker name: %s", workerName);
81
+ resources.workerName = workerName;
82
+ }
83
+ // Mark that we created this worker during the test
84
+ if (resources) {
85
+ log("Marking worker %s as created during this test", workerName);
86
+ resources.workerCreatedDuringTest = true;
87
+ // Update the global state
88
+ if (resources.workerCreatedDuringTest !== undefined) {
89
+ resources.workerCreatedDuringTest = true;
90
+ }
91
+ }
92
+ }
61
93
  // Now run the tests with the custom path
62
94
  const testUrl = new URL("/__smoke_test", url).toString();
63
95
  await checkUrl(testUrl, artifactDir, browserPath, headless, bail, skipClient, "Production", realtime, resources.targetDir, // Add target directory parameter
64
96
  true, // Always skip HMR in production
65
97
  skipStyleTests);
66
98
  log("Release test completed successfully");
67
- // Store the worker name if we didn't set it earlier
68
- if (resources && !resources.workerName) {
69
- log("Storing worker name: %s", workerName);
70
- resources.workerName = workerName;
71
- }
72
- // Mark that we created this worker during the test
73
- if (resources) {
74
- log("Marking worker %s as created during this test", workerName);
75
- resources.workerCreatedDuringTest = true;
76
- // Update the global state
77
- if (resources.workerCreatedDuringTest !== undefined) {
78
- resources.workerCreatedDuringTest = true;
79
- }
80
- }
81
99
  }
82
100
  catch (error) {
83
101
  log("Error during release testing: %O", error);
84
102
  throw error;
85
103
  }
104
+ finally {
105
+ if (stopPreview) {
106
+ await stopPreview().catch((e) => {
107
+ log("Error stopping preview server: %O", e);
108
+ });
109
+ }
110
+ }
86
111
  }
@@ -95,7 +95,7 @@ export async function runSmokeTests(options = {}) {
95
95
  // Update status when release command runs
96
96
  try {
97
97
  console.log("\nšŸš€ Running release command smoke test");
98
- await runReleaseTest(options.artifactDir, resources, browserPath, options.headless !== false, options.bail, options.skipClient, options.projectDir, options.realtime, options.skipHmr, options.skipStyleTests);
98
+ await runReleaseTest(options.artifactDir, resources, browserPath, options.headless !== false, options.bail, options.skipClient, options.projectDir, options.realtime, options.skipHmr, options.skipStyleTests, options.ci);
99
99
  // Update release command status to PASSED
100
100
  updateTestStatus("production", "releaseCommand", "PASSED");
101
101
  // Mark that release tests have run successfully