rwsdk 1.0.0-beta.23 → 1.0.0-beta.24

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,14 +1,18 @@
1
1
  import { computeExecutablePath, detectBrowserPlatform, install, Browser as PuppeteerBrowser, resolveBuildId, } from "@puppeteer/browsers";
2
2
  import debug from "debug";
3
3
  import { mkdirp, pathExists } from "fs-extra";
4
- import * as os from "os";
5
4
  import { join } from "path";
6
5
  import puppeteer from "puppeteer-core";
6
+ import { ensureTmpDir } from "./utils.mjs";
7
7
  const log = debug("rwsdk:e2e:browser");
8
8
  /**
9
9
  * Launch a browser instance
10
10
  */
11
11
  export async function launchBrowser(browserPath, headless = true) {
12
+ // Define a consistent cache directory path in system temp folder
13
+ const rwCacheDir = join(await ensureTmpDir(), "redwoodjs-smoke-test-cache");
14
+ await mkdirp(rwCacheDir);
15
+ log("Using cache directory: %s", rwCacheDir);
12
16
  // Get browser path if not provided
13
17
  if (!browserPath) {
14
18
  log("Getting browser executable path");
@@ -41,7 +45,7 @@ export async function getBrowserPath(testOptions) {
41
45
  }
42
46
  log("Detected platform: %s", platform);
43
47
  // Define a consistent cache directory path in system temp folder
44
- const rwCacheDir = join(os.tmpdir(), "redwoodjs-smoke-test-cache");
48
+ const rwCacheDir = join(await ensureTmpDir(), "redwoodjs-smoke-test-cache");
45
49
  await mkdirp(rwCacheDir);
46
50
  log("Using cache directory: %s", rwCacheDir);
47
51
  // Determine browser type based on headless option
@@ -1,4 +1,6 @@
1
1
  export declare const IS_CI: boolean;
2
+ export declare const RWSDK_SKIP_DEV: boolean;
3
+ export declare const RWSDK_SKIP_DEPLOY: boolean;
2
4
  export declare const IS_DEBUG_MODE: boolean;
3
5
  export declare const SETUP_PLAYGROUND_ENV_TIMEOUT: number;
4
6
  export declare const DEPLOYMENT_TIMEOUT: number;
@@ -5,6 +5,9 @@ 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 RWSDK_SKIP_DEV = Boolean(process.env.RWSDK_SKIP_DEV);
9
+ export const RWSDK_SKIP_DEPLOY = process.env.RWSDK_SKIP_DEPLOY === "true" ||
10
+ process.env.RWSDK_SKIP_DEPLOY === "1";
8
11
  export const IS_DEBUG_MODE = process.env.RWSDK_E2E_DEBUG
9
12
  ? process.env.RWSDK_E2E_DEBUG === "true"
10
13
  : !IS_CI;
@@ -73,21 +73,23 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
73
73
  };
74
74
  const pm = getPackageManagerCommand(packageManager);
75
75
  // Use the provided cwd if available
76
- devProcess = $({
76
+ devProcess = $(pm, ["run", "dev"], {
77
77
  all: true,
78
- detached: true, // Re-enable for reliable process cleanup
78
+ // On Windows, detached: true prevents stdio from being captured.
79
+ // On Unix, it's required for reliable cleanup by killing the process group.
80
+ detached: process.platform !== "win32",
79
81
  cleanup: true, // Let execa handle cleanup
80
82
  forceKillAfterTimeout: 2000, // Force kill if graceful shutdown fails
81
83
  cwd: cwd || process.cwd(), // Use provided directory or current directory
82
84
  env, // Pass the updated environment variables
83
85
  stdio: "pipe", // Ensure streams are piped
84
- }) `${pm} run dev`;
86
+ });
85
87
  devProcess.catch((error) => {
86
88
  if (!isErrorExpected) {
87
89
  // Don't re-throw. The error will be handled gracefully by the polling
88
90
  // logic in `waitForUrl`, which will detect that the process has exited.
89
91
  // Re-throwing here would cause an unhandled promise rejection.
90
- log("Dev server process exited unexpectedly:", error.shortMessage);
92
+ log("Dev server process exited unexpectedly: %O", error);
91
93
  }
92
94
  });
93
95
  log("Development server process spawned in directory: %s", cwd || process.cwd());
@@ -97,8 +99,9 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
97
99
  // Listen for all output to get the URL
98
100
  const handleOutput = (data, source) => {
99
101
  const output = data.toString();
102
+ // Raw output for debugging
103
+ process.stdout.write(`[dev:${source}] ` + output);
100
104
  allOutput += output; // Accumulate all output
101
- log("Received output from %s: %s", source, output.replace(/\n/g, "\\n"));
102
105
  if (!url) {
103
106
  // Multiple patterns to catch different package manager outputs
104
107
  const patterns = [
@@ -118,28 +121,17 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
118
121
  ];
119
122
  for (const pattern of patterns) {
120
123
  const match = output.match(pattern);
121
- log("Testing pattern %s against output: %s", pattern.source, output.replace(/\n/g, "\\n"));
122
124
  if (match) {
123
- log("Pattern matched: %s, groups: %o", pattern.source, match);
124
125
  if (match[1] && match[1].startsWith("http")) {
125
126
  url = match[1];
126
- log("Found development server URL with pattern %s: %s", pattern.source, url);
127
127
  break;
128
128
  }
129
129
  else if (match[1] && /^\d+$/.test(match[1])) {
130
130
  url = `http://localhost:${match[1]}`;
131
- log("Found development server URL with port pattern %s: %s", pattern.source, url);
132
131
  break;
133
132
  }
134
133
  }
135
134
  }
136
- // Log potential matches for debugging
137
- if (!url &&
138
- (output.includes("localhost") ||
139
- output.includes("Local") ||
140
- output.includes("server"))) {
141
- log("Potential URL pattern found but not matched: %s", output.trim());
142
- }
143
135
  }
144
136
  };
145
137
  // Listen to all possible output streams
@@ -150,6 +142,15 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
150
142
  // Also try listening to the raw process output
151
143
  if (devProcess.child) {
152
144
  log("Setting up child process stream listeners");
145
+ devProcess.child.on("spawn", () => {
146
+ log("Child process spawned successfully.");
147
+ });
148
+ devProcess.child.on("error", (err) => {
149
+ log("Child process error: %O", err);
150
+ });
151
+ devProcess.child.on("exit", (code, signal) => {
152
+ log("Child process exited with code %s and signal %s", code, signal);
153
+ });
153
154
  devProcess.child.stdout?.on("data", (data) => handleOutput(data, "child.stdout"));
154
155
  devProcess.child.stderr?.on("data", (data) => handleOutput(data, "child.stderr"));
155
156
  }
@@ -1,5 +1,7 @@
1
1
  import tmp from "tmp-promise";
2
2
  import { PackageManager } from "./types.mjs";
3
+ export declare function getFilesRecursively(directory: string): Promise<string[]>;
4
+ export declare function getDirectoryHash(directory: string): Promise<string>;
3
5
  /**
4
6
  * Copy project to a temporary directory with a unique name
5
7
  */
@@ -4,22 +4,77 @@ import { copy, pathExists } from "fs-extra";
4
4
  import ignore from "ignore";
5
5
  import * as fs from "node:fs";
6
6
  import path from "node:path";
7
- import os from "os";
8
7
  import { basename, join, relative, resolve } from "path";
9
8
  import tmp from "tmp-promise";
10
9
  import { $ } from "../../lib/$.mjs";
11
10
  import { ROOT_DIR } from "../constants.mjs";
12
- import { INSTALL_DEPENDENCIES_RETRIES, IS_CI } from "./constants.mjs";
11
+ import { INSTALL_DEPENDENCIES_RETRIES } from "./constants.mjs";
13
12
  import { retry } from "./retry.mjs";
13
+ import { ensureTmpDir } from "./utils.mjs";
14
14
  const log = debug("rwsdk:e2e:environment");
15
- const IS_CACHE_ENABLED = process.env.RWSDK_E2E_CACHE
16
- ? process.env.RWSDK_E2E_CACHE === "1"
17
- : !IS_CI;
15
+ const IS_CACHE_ENABLED = !process.env.RWSDK_E2E_CACHE_DISABLED;
18
16
  if (IS_CACHE_ENABLED) {
19
17
  log("E2E test caching is enabled.");
20
18
  }
19
+ async function getProjectDependencyHash(projectDir) {
20
+ const hash = createHash("md5");
21
+ const dependencyFiles = [
22
+ "package.json",
23
+ "pnpm-lock.yaml",
24
+ "yarn.lock",
25
+ "package-lock.json",
26
+ ];
27
+ for (const file of dependencyFiles) {
28
+ const filePath = path.join(projectDir, file);
29
+ if (await pathExists(filePath)) {
30
+ const data = await fs.promises.readFile(filePath);
31
+ hash.update(path.basename(filePath));
32
+ hash.update(data);
33
+ }
34
+ }
35
+ return hash.digest("hex");
36
+ }
37
+ export async function getFilesRecursively(directory) {
38
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true });
39
+ const files = await Promise.all(entries.map((entry) => {
40
+ const fullPath = path.join(directory, entry.name);
41
+ return entry.isDirectory() ? getFilesRecursively(fullPath) : fullPath;
42
+ }));
43
+ return files.flat();
44
+ }
45
+ export async function getDirectoryHash(directory) {
46
+ const hash = createHash("md5");
47
+ if (!(await pathExists(directory))) {
48
+ return "";
49
+ }
50
+ const files = await getFilesRecursively(directory);
51
+ files.sort();
52
+ for (const file of files) {
53
+ const relativePath = path.relative(directory, file);
54
+ const data = await fs.promises.readFile(file);
55
+ hash.update(relativePath.replace(/\\/g, "/")); // Normalize path separators
56
+ hash.update(data);
57
+ }
58
+ return hash.digest("hex");
59
+ }
21
60
  const getTempDir = async () => {
22
- return tmp.dir({ unsafeCleanup: true });
61
+ const tmpDir = await ensureTmpDir();
62
+ const projectsTempDir = path.join(tmpDir, "e2e-projects");
63
+ await fs.promises.mkdir(projectsTempDir, { recursive: true });
64
+ const tempDir = await tmp.dir({
65
+ unsafeCleanup: true,
66
+ tmpdir: projectsTempDir,
67
+ });
68
+ // context(justinvdm, 2 Nov 2025): On Windows CI, tmp.dir() can return a
69
+ // short path (e.g., RUNNER~1). Vite's internals may later resolve this to a
70
+ // long path (e.g., runneradmin), causing alias resolution to fail due to
71
+ // path mismatch. Using realpathSync ensures we always use the canonical
72
+ // path, avoiding this inconsistency.
73
+ if (process.platform === "win32") {
74
+ tempDir.path = fs.realpathSync.native(tempDir.path);
75
+ }
76
+ await fs.promises.mkdir(tempDir.path, { recursive: true });
77
+ return tempDir;
23
78
  };
24
79
  function slugify(str) {
25
80
  return str
@@ -43,7 +98,12 @@ const createSdkTarball = async () => {
43
98
  };
44
99
  }
45
100
  // Create a temporary directory to receive the tarball, ensuring a stable path.
46
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rwsdk-tarball-"));
101
+ let tempDir = await fs.promises.mkdtemp(path.join(await ensureTmpDir(), "rwsdk-tarball-"));
102
+ // context(justinvdm, 2 Nov 2025): Normalize the temp dir on Windows
103
+ // to prevent short/long path mismatches.
104
+ if (process.platform === "win32") {
105
+ tempDir = fs.realpathSync.native(tempDir);
106
+ }
47
107
  await $({
48
108
  cwd: ROOT_DIR,
49
109
  stdio: "pipe",
@@ -161,24 +221,25 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
161
221
  log("⚙️ Configuring temp project to not use frozen lockfile...");
162
222
  const npmrcPath = join(targetDir, ".npmrc");
163
223
  await fs.promises.writeFile(npmrcPath, "frozen-lockfile=false\n");
224
+ const tmpDir = await ensureTmpDir();
164
225
  if (packageManager === "yarn") {
165
226
  const yarnrcPath = join(targetDir, ".yarnrc.yml");
166
- const yarnCacheDir = path.join(os.tmpdir(), "yarn-cache");
227
+ const yarnCacheDir = path.join(tmpDir, "yarn-cache");
167
228
  await fs.promises.mkdir(yarnCacheDir, { recursive: true });
168
229
  const yarnConfig = [
169
230
  // todo(justinvdm, 23-09-23): Support yarn pnpm
170
231
  "nodeLinker: node-modules",
171
232
  "enableImmutableInstalls: false",
172
- `cacheFolder: "${yarnCacheDir}"`,
233
+ `cacheFolder: "${yarnCacheDir.replace(/\\/g, "/")}"`,
173
234
  ].join("\n");
174
235
  await fs.promises.writeFile(yarnrcPath, yarnConfig);
175
236
  log("Created .yarnrc.yml to allow lockfile changes for yarn");
176
237
  }
177
238
  if (packageManager === "yarn-classic") {
178
239
  const yarnrcPath = join(targetDir, ".yarnrc");
179
- const yarnCacheDir = path.join(os.tmpdir(), "yarn-classic-cache");
240
+ const yarnCacheDir = path.join(tmpDir, "yarn-classic-cache");
180
241
  await fs.promises.mkdir(yarnCacheDir, { recursive: true });
181
- const yarnConfig = `cache-folder "${yarnCacheDir}"`;
242
+ const yarnConfig = `cache-folder "${yarnCacheDir.replace(/\\/g, "/")}"`;
182
243
  await fs.promises.writeFile(yarnrcPath, yarnConfig);
183
244
  log("Created .yarnrc with cache-folder for yarn-classic");
184
245
  }
@@ -197,43 +258,45 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
197
258
  }
198
259
  }
199
260
  async function installDependencies(targetDir, packageManager = "pnpm", projectDir, monorepoRoot) {
261
+ let cacheRoot = null;
262
+ let nodeModulesCachePath = null;
200
263
  if (IS_CACHE_ENABLED) {
201
- // Generate a checksum of the SDK's dist directory to factor into the cache key
202
- const { stdout: sdkDistChecksum } = await $("find . -type f | sort | md5sum", {
203
- shell: true,
204
- cwd: path.join(ROOT_DIR, "dist"),
205
- });
206
- const projectIdentifier = monorepoRoot
207
- ? `${monorepoRoot}-${projectDir}`
208
- : projectDir;
209
- const projectHash = createHash("md5")
210
- .update(`${projectIdentifier}-${sdkDistChecksum}`)
211
- .digest("hex")
212
- .substring(0, 8);
264
+ const dependencyHash = await getProjectDependencyHash(monorepoRoot || projectDir);
213
265
  const cacheDirName = monorepoRoot
214
266
  ? basename(monorepoRoot)
215
267
  : basename(projectDir);
216
- const cacheRoot = path.join(os.tmpdir(), "rwsdk-e2e-cache", `${cacheDirName}-${projectHash}`);
217
- const nodeModulesCachePath = path.join(cacheRoot, "node_modules");
268
+ cacheRoot = path.join(await ensureTmpDir(), "rwsdk-e2e-cache", `${cacheDirName}-${dependencyHash.substring(0, 8)}`);
269
+ nodeModulesCachePath = path.join(cacheRoot, "node_modules");
218
270
  if (await pathExists(nodeModulesCachePath)) {
219
- console.log(`✅ CACHE HIT for source "${projectIdentifier}": Found cached node_modules. Hard-linking from ${nodeModulesCachePath}`);
271
+ console.log(`✅ CACHE HIT for dependencies: Found cached node_modules. Hard-linking from ${nodeModulesCachePath}`);
220
272
  try {
221
- // Use cp -al for a fast, hardlink-based copy
222
- await $("cp", ["-al", nodeModulesCachePath, join(targetDir, "node_modules")], { stdio: "pipe" });
223
- console.log(`✅ Hardlink copy created successfully.`);
273
+ await copy(nodeModulesCachePath, join(targetDir, "node_modules"));
274
+ console.log(`✅ Cache restored successfully.`);
275
+ console.log(`📦 Installing local SDK into cached node_modules...`);
276
+ // We still need to install the packed tarball
277
+ await runInstall(targetDir, packageManager, true);
278
+ return;
224
279
  }
225
280
  catch (e) {
226
- console.warn(`⚠️ Hardlink copy failed, falling back to full copy. Error: ${e.message}`);
227
- // Fallback to a regular copy if hardlinking fails (e.g., cross-device)
228
- await copy(nodeModulesCachePath, join(targetDir, "node_modules"));
229
- console.log(`✅ Full copy created successfully.`);
281
+ console.warn(`⚠️ Cache restore failed. Error: ${e.message}. Proceeding with clean install.`);
230
282
  }
231
- return;
232
283
  }
233
- console.log(`ℹ️ CACHE MISS for source "${projectIdentifier}": No cached node_modules found at ${nodeModulesCachePath}. Proceeding with installation.`);
284
+ else {
285
+ console.log(`ℹ️ CACHE MISS for dependencies: No cached node_modules found at ${nodeModulesCachePath}. Proceeding with clean installation.`);
286
+ }
234
287
  }
235
- console.log(`📦 Installing project dependencies in ${targetDir} using ${packageManager}...`);
236
- try {
288
+ await runInstall(targetDir, packageManager, false);
289
+ if (IS_CACHE_ENABLED && nodeModulesCachePath) {
290
+ console.log(`Caching node_modules to ${nodeModulesCachePath} for future runs...`);
291
+ await fs.promises.mkdir(path.dirname(nodeModulesCachePath), {
292
+ recursive: true,
293
+ });
294
+ await copy(join(targetDir, "node_modules"), nodeModulesCachePath);
295
+ console.log(`✅ node_modules cached successfully.`);
296
+ }
297
+ }
298
+ async function runInstall(targetDir, packageManager, isCacheHit) {
299
+ if (!isCacheHit) {
237
300
  // Clean up any pre-existing node_modules and lockfiles
238
301
  log("Cleaning up pre-existing node_modules and lockfiles...");
239
302
  await Promise.all([
@@ -246,78 +309,54 @@ async function installDependencies(targetDir, packageManager = "pnpm", projectDi
246
309
  fs.promises.rm(join(targetDir, "package-lock.json"), { force: true }),
247
310
  ]);
248
311
  log("Cleanup complete.");
249
- if (packageManager.startsWith("yarn")) {
250
- log(`Enabling corepack...`);
251
- await $("corepack", ["enable"], { cwd: targetDir, stdio: "pipe" });
252
- if (packageManager === "yarn") {
253
- log(`Preparing yarn@stable with corepack...`);
254
- await $("corepack", ["prepare", "yarn@stable", "--activate"], {
255
- cwd: targetDir,
256
- stdio: "pipe",
257
- });
258
- }
259
- else if (packageManager === "yarn-classic") {
260
- log(`Preparing yarn@1.22.19 with corepack...`);
261
- await $("corepack", ["prepare", "yarn@1.x", "--activate"], {
262
- cwd: targetDir,
263
- stdio: "pipe",
264
- });
265
- }
266
- }
267
- const npmCacheDir = path.join(os.tmpdir(), "npm-cache");
268
- await fs.promises.mkdir(npmCacheDir, { recursive: true });
269
- const installCommand = {
270
- pnpm: ["pnpm", "install"],
271
- npm: ["npm", "install", "--cache", npmCacheDir],
272
- yarn: ["yarn", "install"],
273
- "yarn-classic": ["yarn"],
274
- }[packageManager];
275
- // Run install command in the target directory
276
- log(`Running ${installCommand.join(" ")}`);
277
- const [command, ...args] = installCommand;
278
- const result = await $(command, args, {
279
- cwd: targetDir,
280
- stdio: "pipe", // Capture output
281
- env: {
282
- YARN_ENABLE_HARDENED_MODE: "0",
283
- },
284
- });
285
- console.log("✅ Dependencies installed successfully");
286
- // After successful install, populate the cache if enabled
287
- if (IS_CACHE_ENABLED) {
288
- // Re-calculate cache path to be safe
289
- const { stdout: sdkDistChecksum } = await $("find . -type f | sort | md5sum", {
290
- shell: true,
291
- cwd: path.join(ROOT_DIR, "dist"),
292
- });
293
- const projectIdentifier = monorepoRoot
294
- ? `${monorepoRoot}-${projectDir}`
295
- : projectDir;
296
- const projectHash = createHash("md5")
297
- .update(`${projectIdentifier}-${sdkDistChecksum}`)
298
- .digest("hex")
299
- .substring(0, 8);
300
- const cacheDirName = monorepoRoot
301
- ? basename(monorepoRoot)
302
- : basename(projectDir);
303
- const cacheRoot = path.join(os.tmpdir(), "rwsdk-e2e-cache", `${cacheDirName}-${projectHash}`);
304
- const nodeModulesCachePath = path.join(cacheRoot, "node_modules");
305
- console.log(`Caching node_modules to ${nodeModulesCachePath} for future runs...`);
306
- // Ensure parent directory exists
307
- await fs.promises.mkdir(path.dirname(nodeModulesCachePath), {
308
- recursive: true,
312
+ }
313
+ if (packageManager.startsWith("yarn")) {
314
+ log(`Enabling corepack...`);
315
+ await $("corepack", ["enable"], { cwd: targetDir, stdio: "pipe" });
316
+ if (packageManager === "yarn") {
317
+ log(`Preparing yarn@stable with corepack...`);
318
+ await $("corepack", ["prepare", "yarn@stable", "--activate"], {
319
+ cwd: targetDir,
320
+ stdio: "pipe",
309
321
  });
310
- await copy(join(targetDir, "node_modules"), nodeModulesCachePath);
311
- console.log(`✅ node_modules cached successfully.`);
312
322
  }
313
- // Log installation details at debug level
314
- if (result.stdout) {
315
- log(`${packageManager} install output: %s`, result.stdout);
323
+ else if (packageManager === "yarn-classic") {
324
+ log(`Preparing yarn@1.22.19 with corepack...`);
325
+ await $("corepack", ["prepare", "yarn@1.x", "--activate"], {
326
+ cwd: targetDir,
327
+ stdio: "pipe",
328
+ });
316
329
  }
317
330
  }
318
- catch (error) {
319
- log("ERROR: Failed to install dependencies: %O", error);
320
- console.error(`❌ Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`);
321
- throw new Error(`Failed to install project dependencies. Please ensure the project can be installed with ${packageManager}.`);
331
+ const npmCacheDir = path.join(await ensureTmpDir(), "npm-cache");
332
+ await fs.promises.mkdir(npmCacheDir, { recursive: true });
333
+ const installCommand = {
334
+ pnpm: ["pnpm", "install", "--reporter=silent"],
335
+ npm: ["npm", "install", "--cache", npmCacheDir, "--silent"],
336
+ yarn: ["yarn", "install", "--silent"],
337
+ "yarn-classic": ["yarn", "--silent"],
338
+ }[packageManager];
339
+ if (isCacheHit && packageManager === "pnpm") {
340
+ // For pnpm, a targeted `install <tarball>` is much faster
341
+ // We need to find the tarball name first.
342
+ const files = await fs.promises.readdir(targetDir);
343
+ const tarball = files.find((f) => f.startsWith("rwsdk-") && f.endsWith(".tgz"));
344
+ if (tarball) {
345
+ installCommand[1] = `./${tarball}`;
346
+ }
347
+ else {
348
+ log("Could not find SDK tarball for targeted install, falling back to full install.");
349
+ }
322
350
  }
351
+ // Run install command in the target directory
352
+ log(`Running ${installCommand.join(" ")}`);
353
+ const [command, ...args] = installCommand;
354
+ await $(command, args, {
355
+ cwd: targetDir,
356
+ stdio: "pipe",
357
+ env: {
358
+ YARN_ENABLE_HARDENED_MODE: "0",
359
+ },
360
+ });
361
+ console.log("✅ Dependencies installed successfully");
323
362
  }
@@ -44,10 +44,6 @@ export async function $expect(command, expectations, options = {
44
44
  reject: true,
45
45
  }) {
46
46
  return new Promise((resolve, reject) => {
47
- log("$expect starting with command: %s", command);
48
- log("Working directory: %s", options.cwd ?? process.cwd());
49
- log("Expected patterns: %O", expectations.map((e) => e.expect.toString()));
50
- console.log(`Running command: ${command}`);
51
47
  // Spawn the process with pipes for interaction
52
48
  const childProcess = execaCommand(command, {
53
49
  cwd: options.cwd ?? process.cwd(),
@@ -55,7 +51,6 @@ export async function $expect(command, expectations, options = {
55
51
  reject: false, // Never reject so we can handle the error ourselves
56
52
  env: options.env ?? process.env,
57
53
  });
58
- log("Process spawned with PID: %s", childProcess.pid);
59
54
  let stdout = "";
60
55
  let stderr = "";
61
56
  let buffer = "";
@@ -67,7 +62,6 @@ export async function $expect(command, expectations, options = {
67
62
  // Initialize match count for each pattern
68
63
  expectations.forEach(({ expect: expectPattern }) => {
69
64
  matchHistory.set(expectPattern, 0);
70
- log("Initialized pattern match count for: %s", expectPattern.toString());
71
65
  });
72
66
  // Collect stdout
73
67
  childProcess.stdout?.on("data", (data) => {
@@ -85,9 +79,6 @@ export async function $expect(command, expectations, options = {
85
79
  : new RegExp(expectPattern, "m");
86
80
  // Only search in the unmatched portion of the buffer
87
81
  const searchBuffer = buffer.substring(lastMatchIndex);
88
- log("Testing pattern: %s against buffer from position %d (%d chars)", pattern.toString(), lastMatchIndex, searchBuffer.length);
89
- // Enhanced debugging: show actual search buffer content
90
- log("Search buffer content for debugging: %O", searchBuffer);
91
82
  const match = searchBuffer.match(pattern);
92
83
  if (match) {
93
84
  // Found a match
@@ -98,30 +89,21 @@ export async function $expect(command, expectations, options = {
98
89
  const matchStartPosition = lastMatchIndex + match.index;
99
90
  const matchEndPosition = matchStartPosition + match[0].length;
100
91
  lastMatchIndex = matchEndPosition;
101
- log(`Pattern matched: "${patternStr}" (occurrence #${matchCount + 1}) at position ${matchStartPosition}-${matchEndPosition}`);
102
- // Only send a response if one is specified
103
92
  if (send) {
104
- log(`Sending response: "${send.replace(/\r/g, "\\r")}" to stdin`);
105
93
  childProcess.stdin?.write(send);
106
94
  }
107
- else {
108
- log(`Pattern "${patternStr}" matched (verification only)`);
109
- }
110
95
  // Increment the match count for this pattern
111
96
  matchHistory.set(expectPattern, matchCount + 1);
112
- log("Updated match count for %s: %d", patternStr, matchCount + 1);
113
97
  // Move to the next expectation
114
98
  currentExpectationIndex++;
115
99
  // If we've processed all expectations but need to wait for stdin response,
116
100
  // delay closing stdin until the next data event
117
101
  if (currentExpectationIndex >= expectations.length && send) {
118
- log("All patterns matched, closing stdin after last response");
119
102
  childProcess.stdin?.end();
120
103
  }
121
104
  break; // Exit the while loop to process next chunk
122
105
  }
123
106
  else {
124
- log("Pattern not matched. Attempting to diagnose the mismatch:");
125
107
  // Try to find the closest substring that might partially match
126
108
  const patternString = pattern.toString();
127
109
  const patternCore = patternString.substring(1, patternString.lastIndexOf("/") > 0
@@ -132,7 +114,6 @@ export async function $expect(command, expectations, options = {
132
114
  const partialPattern = patternCore.substring(0, i);
133
115
  const partialRegex = new RegExp(partialPattern, "m");
134
116
  const matches = partialRegex.test(searchBuffer);
135
- log(" Partial pattern '%s': %s", partialPattern, matches ? "matched" : "not matched");
136
117
  // Once we find where the matching starts to fail, stop
137
118
  if (!matches)
138
119
  break;
@@ -144,7 +125,6 @@ export async function $expect(command, expectations, options = {
144
125
  // If all expectations have been matched, we can close stdin if not already closed
145
126
  if (currentExpectationIndex >= expectations.length &&
146
127
  childProcess.stdin?.writable) {
147
- log("All patterns matched, ensuring stdin is closed");
148
128
  childProcess.stdin.end();
149
129
  }
150
130
  });
@@ -160,19 +140,10 @@ export async function $expect(command, expectations, options = {
160
140
  // Handle process completion
161
141
  childProcess.on("close", (code) => {
162
142
  log("Process closed with code: %s", code);
163
- // Log the number of matches for each pattern
164
- log("Pattern match summary:");
165
- for (const [pattern, count] of matchHistory.entries()) {
166
- log(` - "${pattern.toString()}": ${count} matches`);
167
- }
168
143
  // Check if any required patterns were not matched
169
144
  const unmatchedPatterns = Array.from(matchHistory.entries())
170
145
  .filter(([_, count]) => count === 0)
171
146
  .map(([pattern, _]) => pattern.toString());
172
- if (unmatchedPatterns.length > 0) {
173
- log("WARNING: Some expected patterns were not matched: %O", unmatchedPatterns);
174
- }
175
- log("$expect completed. Total stdout: %d bytes, stderr: %d bytes", stdout.length, stderr.length);
176
147
  resolve({ stdout, stderr, code });
177
148
  });
178
149
  childProcess.on("error", (err) => {
@@ -4,32 +4,8 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { adjectives, animals, uniqueNamesGenerator, } from "unique-names-generator";
6
6
  import { ROOT_DIR } from "../constants.mjs";
7
- import { copyProjectToTempDir } from "./environment.mjs";
7
+ import { copyProjectToTempDir, } from "./environment.mjs";
8
8
  const log = (message) => console.log(message);
9
- async function verifyPackedContents(targetDir) {
10
- log(" - Verifying installed package contents...");
11
- const packageName = "rwsdk";
12
- const installedDistPath = path.join(targetDir, "node_modules", packageName, "dist");
13
- if (!fs.existsSync(installedDistPath)) {
14
- throw new Error(`dist/ directory not found in installed package at ${installedDistPath}.`);
15
- }
16
- const { stdout: originalDistChecksumOut } = await $("find . -type f | sort | md5sum", {
17
- shell: true,
18
- cwd: path.join(ROOT_DIR, "dist"),
19
- });
20
- const originalDistChecksum = originalDistChecksumOut.split(" ")[0];
21
- const { stdout: installedDistChecksumOut } = await $("find . -type f | sort | md5sum", {
22
- shell: true,
23
- cwd: installedDistPath,
24
- });
25
- const installedDistChecksum = installedDistChecksumOut.split(" ")[0];
26
- log(` - Original dist checksum: ${originalDistChecksum}`);
27
- log(` - Installed dist checksum: ${installedDistChecksum}`);
28
- if (originalDistChecksum !== installedDistChecksum) {
29
- throw new Error("File list in installed dist/ does not match original dist/.");
30
- }
31
- log(" ✅ Installed package contents match the local build.");
32
- }
33
9
  /**
34
10
  * Copies wrangler cache from monorepo to temp directory for deployment tests
35
11
  */
@@ -104,7 +80,6 @@ export async function setupTarballEnvironment({ projectDir, monorepoRoot, packag
104
80
  const resourceUniqueKey = `${uniqueNameSuffix}-${hash}`;
105
81
  try {
106
82
  const { tempDir, targetDir } = await copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager, monorepoRoot);
107
- await verifyPackedContents(targetDir);
108
83
  // Copy wrangler cache to improve deployment performance
109
84
  const sdkRoot = ROOT_DIR;
110
85
  await copyWranglerCache(targetDir, sdkRoot);
@@ -1,9 +1,9 @@
1
1
  import fs from "fs-extra";
2
- import os from "os";
3
2
  import path, { basename, dirname, join as pathJoin } from "path";
4
3
  import puppeteer from "puppeteer-core";
5
4
  import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
5
  import { launchBrowser } from "./browser.mjs";
6
+ import { ensureTmpDir } from "./utils.mjs";
7
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";
8
8
  import { runDevServer } from "./dev.mjs";
9
9
  import { poll, pollValue } from "./poll.mjs";
@@ -34,16 +34,28 @@ function ensureHooksRegistered() {
34
34
  afterAll(async () => {
35
35
  const cleanupPromises = [];
36
36
  for (const instance of devInstances) {
37
- cleanupPromises.push(instance.stopDev());
37
+ cleanupPromises.push(instance.stopDev().catch((error) => {
38
+ // Suppress all cleanup errors - they don't affect test results
39
+ console.warn(`Suppressing error during dev server cleanup: ${error instanceof Error ? error.message : String(error)}`);
40
+ }));
38
41
  }
39
42
  for (const instance of deploymentInstances) {
40
- cleanupPromises.push(instance.cleanup());
43
+ cleanupPromises.push(instance.cleanup().catch((error) => {
44
+ // Suppress all cleanup errors - they don't affect test results
45
+ console.warn(`Suppressing error during deployment cleanup: ${error instanceof Error ? error.message : String(error)}`);
46
+ }));
41
47
  }
42
48
  if (globalDevPlaygroundEnv) {
43
- cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
49
+ cleanupPromises.push(globalDevPlaygroundEnv.cleanup().catch((error) => {
50
+ // Suppress all cleanup errors - they don't affect test results
51
+ console.warn(`Suppressing error during dev environment cleanup: ${error instanceof Error ? error.message : String(error)}`);
52
+ }));
44
53
  }
45
54
  if (globalDeployPlaygroundEnv) {
46
- cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
55
+ cleanupPromises.push(globalDeployPlaygroundEnv.cleanup().catch((error) => {
56
+ // Suppress all cleanup errors - they don't affect test results
57
+ console.warn(`Suppressing error during deploy environment cleanup: ${error instanceof Error ? error.message : String(error)}`);
58
+ }));
47
59
  }
48
60
  await Promise.all(cleanupPromises);
49
61
  devInstances.length = 0;
@@ -128,7 +140,7 @@ export function setupPlaygroundEnvironment(options) {
128
140
  else {
129
141
  globalDevPlaygroundEnv = null;
130
142
  }
131
- if (deploy) {
143
+ if (deploy && !SKIP_DEPLOYMENT_TESTS) {
132
144
  const deployEnv = await setupTarballEnvironment({
133
145
  projectDir,
134
146
  monorepoRoot,
@@ -313,7 +325,7 @@ function createTestRunner(testFn, envType) {
313
325
  let instance;
314
326
  let browser;
315
327
  beforeAll(async () => {
316
- const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
328
+ const tempDir = path.join(await ensureTmpDir(), "rwsdk-e2e-tests");
317
329
  const wsEndpointFile = path.join(tempDir, "wsEndpoint");
318
330
  try {
319
331
  const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
@@ -386,7 +398,7 @@ function createSDKTestRunner() {
386
398
  let page;
387
399
  let browser;
388
400
  beforeAll(async () => {
389
- const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
401
+ const tempDir = path.join(await ensureTmpDir(), "rwsdk-e2e-tests");
390
402
  const wsEndpointFile = path.join(tempDir, "wsEndpoint");
391
403
  try {
392
404
  const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
@@ -0,0 +1 @@
1
+ export declare function ensureTmpDir(): Promise<string>;
@@ -0,0 +1,15 @@
1
+ import { mkdirp } from "fs-extra";
2
+ import os from "node:os";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ export async function ensureTmpDir() {
6
+ let baseTmpDir = os.tmpdir();
7
+ // context(justinvdm, 2 Nov 2025): Normalize the base temp dir on Windows
8
+ // to prevent short/long path mismatches that break Vite's alias resolution.
9
+ if (process.platform === "win32") {
10
+ baseTmpDir = fs.realpathSync.native(baseTmpDir);
11
+ }
12
+ const tmpDir = path.join(baseTmpDir, "rwsdk-e2e");
13
+ await mkdirp(tmpDir);
14
+ return tmpDir;
15
+ }
@@ -75,12 +75,10 @@ export const redwoodPlugin = async (options = {}) => {
75
75
  // context(justinvdm, 31 Mar 2025): We assume that if there is no .wrangler directory,
76
76
  // then this is fresh install, and we run `npm run dev:init` here.
77
77
  if (process.env.RWSDK_WORKER_RUN !== "1" &&
78
- process.env.RWSDK_DEPLOY !== "1" &&
78
+ process.env.NODE_ENV !== "production" &&
79
79
  !(await pathExists(resolve(projectRootDir, ".wrangler"))) &&
80
80
  (await hasPkgScript(projectRootDir, "dev:init"))) {
81
81
  console.log("🚀 Project has no .wrangler directory yet, assuming fresh install: running `npm run dev:init`...");
82
- // @ts-ignore
83
- $.verbose = true;
84
82
  await $ `npm run dev:init`;
85
83
  }
86
84
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.23",
3
+ "version": "1.0.0-beta.24",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {