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.
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +2 -0
- package/dist/lib/e2e/constants.mjs +3 -0
- package/dist/lib/e2e/dev.mjs +17 -16
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +145 -106
- package/dist/lib/e2e/release.mjs +0 -29
- package/dist/lib/e2e/tarball.mjs +1 -26
- package/dist/lib/e2e/testHarness.mjs +20 -8
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/vite/redwoodPlugin.mjs +1 -3
- package/package.json +1 -1
package/dist/lib/e2e/browser.mjs
CHANGED
|
@@ -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(
|
|
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;
|
package/dist/lib/e2e/dev.mjs
CHANGED
|
@@ -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
|
|
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
|
-
})
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
271
|
+
console.log(`✅ CACHE HIT for dependencies: Found cached node_modules. Hard-linking from ${nodeModulesCachePath}`);
|
|
220
272
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
console.log(
|
|
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(`⚠️
|
|
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
|
-
|
|
284
|
+
else {
|
|
285
|
+
console.log(`ℹ️ CACHE MISS for dependencies: No cached node_modules found at ${nodeModulesCachePath}. Proceeding with clean installation.`);
|
|
286
|
+
}
|
|
234
287
|
}
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
}
|
package/dist/lib/e2e/release.mjs
CHANGED
|
@@ -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) => {
|
package/dist/lib/e2e/tarball.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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.
|
|
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 [
|