rwsdk 1.0.0-alpha.1 → 1.0.0-alpha.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.
- package/dist/lib/e2e/browser.d.mts +10 -0
- package/dist/lib/e2e/browser.mjs +124 -0
- package/dist/lib/e2e/dev.d.mts +8 -0
- package/dist/lib/e2e/dev.mjs +232 -0
- package/dist/lib/e2e/environment.d.mts +14 -0
- package/dist/lib/e2e/environment.mjs +223 -0
- package/dist/lib/e2e/index.d.mts +7 -0
- package/dist/lib/e2e/index.mjs +7 -0
- package/dist/lib/e2e/release.d.mts +56 -0
- package/dist/lib/e2e/release.mjs +559 -0
- package/dist/lib/e2e/tarball.d.mts +13 -0
- package/dist/lib/e2e/tarball.mjs +99 -0
- package/dist/lib/e2e/testHarness.d.mts +123 -0
- package/dist/lib/e2e/testHarness.mjs +507 -0
- package/dist/lib/e2e/types.d.mts +32 -0
- package/dist/lib/getShortName.mjs +6 -1
- package/dist/lib/getShortName.test.d.mts +1 -0
- package/dist/lib/getShortName.test.mjs +25 -0
- package/dist/lib/hasPkgScript.d.mts +4 -1
- package/dist/lib/hasPkgScript.mjs +9 -6
- package/dist/lib/hasPkgScript.test.d.mts +1 -0
- package/dist/lib/hasPkgScript.test.mjs +33 -0
- package/dist/lib/jsonUtils.mjs +3 -0
- package/dist/lib/jsonUtils.test.d.mts +1 -0
- package/dist/lib/jsonUtils.test.mjs +90 -0
- package/dist/lib/normalizeModulePath.d.mts +5 -0
- package/dist/lib/normalizeModulePath.mjs +1 -1
- package/dist/lib/normalizeModulePath.test.d.mts +1 -0
- package/dist/lib/{normalizeModulePath.test.js → normalizeModulePath.test.mjs} +20 -1
- package/dist/lib/smokeTests/browser.mjs +3 -94
- package/dist/lib/smokeTests/development.mjs +2 -223
- package/dist/lib/smokeTests/environment.d.mts +4 -11
- package/dist/lib/smokeTests/environment.mjs +10 -158
- package/dist/lib/smokeTests/release.d.mts +2 -49
- package/dist/lib/smokeTests/release.mjs +3 -503
- package/dist/runtime/lib/injectHtmlAtMarker.d.ts +11 -0
- package/dist/runtime/lib/injectHtmlAtMarker.js +90 -0
- package/dist/runtime/lib/memoizeOnId.test.d.ts +1 -0
- package/dist/runtime/lib/memoizeOnId.test.js +49 -0
- package/dist/runtime/lib/realtime/protocol.test.d.ts +1 -0
- package/dist/runtime/lib/realtime/protocol.test.js +107 -0
- package/dist/runtime/lib/realtime/shared.test.d.ts +1 -0
- package/dist/runtime/lib/realtime/shared.test.js +18 -0
- package/dist/runtime/lib/realtime/validateUpgradeRequest.test.d.ts +1 -0
- package/dist/runtime/lib/realtime/validateUpgradeRequest.test.js +66 -0
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.js +40 -22
- package/dist/runtime/lib/router.test.js +591 -2
- package/dist/runtime/lib/rwContext.d.ts +22 -0
- package/dist/runtime/lib/rwContext.js +1 -0
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.d.ts +2 -1
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.js +6 -6
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.d.ts +1 -0
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.js +49 -0
- package/dist/runtime/register/worker.d.ts +1 -1
- package/dist/runtime/register/worker.js +26 -21
- package/dist/runtime/render/assembleDocument.d.ts +6 -0
- package/dist/runtime/render/assembleDocument.js +22 -0
- package/dist/runtime/render/createThenableFromReadableStream.d.ts +1 -0
- package/dist/runtime/render/createThenableFromReadableStream.js +9 -0
- package/dist/runtime/render/normalizeActionResult.d.ts +1 -0
- package/dist/runtime/render/normalizeActionResult.js +43 -0
- package/dist/runtime/render/preloads.d.ts +2 -2
- package/dist/runtime/render/preloads.js +2 -3
- package/dist/runtime/render/{renderRscThenableToHtmlStream.d.ts → renderDocumentHtmlStream.d.ts} +3 -3
- package/dist/runtime/render/renderDocumentHtmlStream.js +39 -0
- package/dist/runtime/render/renderHtmlStream.d.ts +7 -0
- package/dist/runtime/render/renderHtmlStream.js +31 -0
- package/dist/runtime/render/renderToRscStream.d.ts +2 -3
- package/dist/runtime/render/renderToRscStream.js +2 -41
- package/dist/runtime/render/renderToStream.d.ts +2 -1
- package/dist/runtime/render/renderToStream.js +15 -8
- package/dist/runtime/render/stylesheets.d.ts +2 -2
- package/dist/runtime/render/stylesheets.js +2 -3
- package/dist/runtime/ssrBridge.d.ts +2 -1
- package/dist/runtime/ssrBridge.js +2 -1
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +11 -6
- package/dist/scripts/debug-sync.mjs +102 -133
- package/dist/vite/buildApp.d.mts +2 -1
- package/dist/vite/buildApp.mjs +9 -5
- package/dist/vite/checkIsUsingPrisma.d.mts +4 -0
- package/dist/vite/checkIsUsingPrisma.mjs +2 -2
- package/dist/vite/checkIsUsingPrisma.test.d.mts +1 -0
- package/dist/vite/checkIsUsingPrisma.test.mjs +30 -0
- package/dist/vite/configPlugin.mjs +35 -14
- package/dist/vite/createDirectiveLookupPlugin.d.mts +9 -0
- package/dist/vite/createDirectiveLookupPlugin.mjs +33 -29
- package/dist/vite/createDirectiveLookupPlugin.test.d.mts +1 -0
- package/dist/vite/createDirectiveLookupPlugin.test.mjs +40 -0
- package/dist/vite/directiveModulesDevPlugin.d.mts +4 -1
- package/dist/vite/directiveModulesDevPlugin.mjs +5 -4
- package/dist/vite/directiveModulesDevPlugin.test.d.mts +1 -0
- package/dist/vite/directiveModulesDevPlugin.test.mjs +59 -0
- package/dist/vite/directivesPlugin.d.mts +1 -0
- package/dist/vite/directivesPlugin.mjs +1 -1
- package/dist/vite/directivesPlugin.test.d.mts +1 -0
- package/dist/vite/directivesPlugin.test.mjs +24 -0
- package/dist/vite/ensureAliasArray.test.d.mts +1 -0
- package/dist/vite/ensureAliasArray.test.mjs +71 -0
- package/dist/vite/findSpecifiers.mjs +2 -1
- package/dist/vite/findSpecifiers.test.d.mts +1 -0
- package/dist/vite/findSpecifiers.test.mjs +202 -0
- package/dist/vite/findSsrSpecifiers.test.d.mts +1 -0
- package/dist/vite/findSsrSpecifiers.test.mjs +99 -0
- package/dist/vite/hasDirective.d.mts +6 -3
- package/dist/vite/hasDirective.mjs +43 -27
- package/dist/vite/hasDirective.test.d.mts +1 -0
- package/dist/vite/hasDirective.test.mjs +107 -0
- package/dist/vite/isJsFile.test.d.mts +1 -0
- package/dist/vite/isJsFile.test.mjs +38 -0
- package/dist/vite/{reactConditionsResolverPlugin.d.mts → knownDepsResolverPlugin.d.mts} +2 -2
- package/dist/vite/{reactConditionsResolverPlugin.mjs → knownDepsResolverPlugin.mjs} +28 -23
- package/dist/vite/linkerPlugin.d.mts +8 -0
- package/dist/vite/linkerPlugin.mjs +30 -22
- package/dist/vite/linkerPlugin.test.d.mts +1 -0
- package/dist/vite/linkerPlugin.test.mjs +41 -0
- package/dist/vite/miniflareHMRPlugin.d.mts +5 -0
- package/dist/vite/miniflareHMRPlugin.mjs +2 -2
- package/dist/vite/miniflareHMRPlugin.test.d.mts +1 -0
- package/dist/vite/miniflareHMRPlugin.test.mjs +42 -0
- package/dist/vite/redwoodPlugin.d.mts +7 -0
- package/dist/vite/redwoodPlugin.mjs +10 -5
- package/dist/vite/redwoodPlugin.test.d.mts +1 -0
- package/dist/vite/redwoodPlugin.test.mjs +34 -0
- package/dist/vite/runDirectivesScan.d.mts +21 -1
- package/dist/vite/runDirectivesScan.mjs +67 -52
- package/dist/vite/runDirectivesScan.test.d.mts +1 -0
- package/dist/vite/runDirectivesScan.test.mjs +73 -0
- package/dist/vite/ssrBridgePlugin.mjs +8 -1
- package/dist/vite/transformClientComponents.mjs +4 -3
- package/dist/vite/transformClientComponents.test.mjs +116 -58
- package/package.json +9 -5
- package/dist/runtime/render/renderRscThenableToHtmlStream.js +0 -54
- package/dist/runtime/render/transformRscToHtmlStream.d.ts +0 -8
- package/dist/runtime/render/transformRscToHtmlStream.js +0 -19
- /package/dist/lib/{normalizeModulePath.test.d.ts → e2e/types.mjs} +0 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { test, beforeAll, afterAll, afterEach, } from "vitest";
|
|
2
|
+
import { basename, join as pathJoin, dirname } from "path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { setupTarballEnvironment } from "./tarball.mjs";
|
|
5
|
+
import { runDevServer } from "./dev.mjs";
|
|
6
|
+
import { runRelease, deleteWorker, deleteD1Database, isRelatedToTest, } from "./release.mjs";
|
|
7
|
+
import { launchBrowser } from "./browser.mjs";
|
|
8
|
+
const SETUP_PLAYGROUND_ENV_TIMEOUT = 10 * 60 * 1000;
|
|
9
|
+
const PUPPETEER_TIMEOUT = process.env.RWSDK_PUPPETEER_TIMEOUT
|
|
10
|
+
? parseInt(process.env.RWSDK_PUPPETEER_TIMEOUT, 10)
|
|
11
|
+
: 60 * 1000; // 60 seconds
|
|
12
|
+
// Environment variable flags for skipping tests
|
|
13
|
+
const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
|
|
14
|
+
const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
|
|
15
|
+
// Global test environment state
|
|
16
|
+
let globalPlaygroundEnv = null;
|
|
17
|
+
const cleanupTasks = [];
|
|
18
|
+
let hooksRegistered = false;
|
|
19
|
+
/**
|
|
20
|
+
* Registers global cleanup hooks automatically
|
|
21
|
+
*/
|
|
22
|
+
function ensureHooksRegistered() {
|
|
23
|
+
if (hooksRegistered)
|
|
24
|
+
return;
|
|
25
|
+
// Register global afterEach to clean up resources created during tests
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
const tasksToCleanup = [...cleanupTasks];
|
|
28
|
+
cleanupTasks.length = 0; // Clear the array
|
|
29
|
+
for (const task of tasksToCleanup) {
|
|
30
|
+
try {
|
|
31
|
+
await task.cleanup();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.warn(`Failed to cleanup ${task.type} ${task.id}:`, error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// Register global afterAll to clean up the playground environment
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
if (globalPlaygroundEnv) {
|
|
41
|
+
try {
|
|
42
|
+
await globalPlaygroundEnv.cleanup();
|
|
43
|
+
globalPlaygroundEnv = null;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.warn("Failed to cleanup playground environment:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
hooksRegistered = true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Registers a cleanup task to be executed automatically
|
|
54
|
+
*/
|
|
55
|
+
function registerCleanupTask(task) {
|
|
56
|
+
ensureHooksRegistered();
|
|
57
|
+
cleanupTasks.push(task);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Removes a cleanup task from the registry (when manually cleaned up)
|
|
61
|
+
*/
|
|
62
|
+
function unregisterCleanupTask(id) {
|
|
63
|
+
const index = cleanupTasks.findIndex((task) => task.id === id);
|
|
64
|
+
if (index !== -1) {
|
|
65
|
+
cleanupTasks.splice(index, 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the project directory for the current test by looking at the call stack
|
|
70
|
+
*/
|
|
71
|
+
function getProjectDirectory() {
|
|
72
|
+
// For now, let's hardcode this to '../playground/hello-world' since we only have one project
|
|
73
|
+
// TODO: Make this more dynamic when we have multiple playground projects
|
|
74
|
+
return "../playground/hello-world";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Derive the playground directory from import.meta.url by finding the nearest package.json
|
|
78
|
+
*/
|
|
79
|
+
function getPlaygroundDirFromImportMeta(importMetaUrl) {
|
|
80
|
+
const url = new URL(importMetaUrl);
|
|
81
|
+
const testFilePath = url.pathname;
|
|
82
|
+
let currentDir = dirname(testFilePath);
|
|
83
|
+
// Walk up the tree from the test file's directory
|
|
84
|
+
while (currentDir !== "/") {
|
|
85
|
+
// Check if a package.json exists in the current directory
|
|
86
|
+
if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
|
|
87
|
+
return currentDir;
|
|
88
|
+
}
|
|
89
|
+
currentDir = dirname(currentDir);
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}. ` +
|
|
92
|
+
`Failed to find a package.json in any parent directory.`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Sets up a playground environment for the entire test suite.
|
|
96
|
+
* Automatically registers beforeAll and afterAll hooks.
|
|
97
|
+
*
|
|
98
|
+
* @param sourceProjectDir - Explicit path to playground directory, or import.meta.url to auto-detect
|
|
99
|
+
*/
|
|
100
|
+
export function setupPlaygroundEnvironment(sourceProjectDir) {
|
|
101
|
+
ensureHooksRegistered();
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
let projectDir;
|
|
104
|
+
if (!sourceProjectDir) {
|
|
105
|
+
projectDir = getProjectDirectory();
|
|
106
|
+
}
|
|
107
|
+
else if (sourceProjectDir.startsWith("file://")) {
|
|
108
|
+
// This is import.meta.url, derive the playground directory
|
|
109
|
+
projectDir = getPlaygroundDirFromImportMeta(sourceProjectDir);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// This is an explicit path
|
|
113
|
+
projectDir = sourceProjectDir;
|
|
114
|
+
}
|
|
115
|
+
console.log(`Setting up playground environment from ${projectDir}...`);
|
|
116
|
+
const tarballEnv = await setupTarballEnvironment({
|
|
117
|
+
projectDir,
|
|
118
|
+
packageManager: process.env.PACKAGE_MANAGER || "pnpm",
|
|
119
|
+
});
|
|
120
|
+
globalPlaygroundEnv = {
|
|
121
|
+
projectDir: tarballEnv.targetDir,
|
|
122
|
+
cleanup: tarballEnv.cleanup,
|
|
123
|
+
};
|
|
124
|
+
}, SETUP_PLAYGROUND_ENV_TIMEOUT);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gets the current playground environment.
|
|
128
|
+
* Throws if no environment has been set up.
|
|
129
|
+
*/
|
|
130
|
+
export function getPlaygroundEnvironment() {
|
|
131
|
+
if (!globalPlaygroundEnv) {
|
|
132
|
+
throw new Error("No playground environment set up. Call setupPlaygroundEnvironment() in beforeAll()");
|
|
133
|
+
}
|
|
134
|
+
return globalPlaygroundEnv;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Creates a dev server instance using the shared playground environment.
|
|
138
|
+
* Automatically registers cleanup to run after the test.
|
|
139
|
+
*/
|
|
140
|
+
export async function createDevServer() {
|
|
141
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
142
|
+
throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
|
|
143
|
+
}
|
|
144
|
+
const env = getPlaygroundEnvironment();
|
|
145
|
+
const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
|
|
146
|
+
const devResult = await runDevServer(packageManager, env.projectDir);
|
|
147
|
+
const serverId = `devServer-${Date.now()}-${Math.random()
|
|
148
|
+
.toString(36)
|
|
149
|
+
.substring(2, 9)}`;
|
|
150
|
+
// Register automatic cleanup
|
|
151
|
+
registerCleanupTask({
|
|
152
|
+
id: serverId,
|
|
153
|
+
type: "devServer",
|
|
154
|
+
cleanup: devResult.stopDev,
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
url: devResult.url,
|
|
158
|
+
stopDev: async () => {
|
|
159
|
+
await devResult.stopDev();
|
|
160
|
+
unregisterCleanupTask(serverId); // Remove from auto-cleanup since manually cleaned
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Creates a deployment instance using the shared playground environment.
|
|
166
|
+
* Automatically registers cleanup to run after the test.
|
|
167
|
+
*/
|
|
168
|
+
export async function createDeployment() {
|
|
169
|
+
if (SKIP_DEPLOYMENT_TESTS) {
|
|
170
|
+
throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
|
|
171
|
+
}
|
|
172
|
+
const env = getPlaygroundEnvironment();
|
|
173
|
+
// Extract the unique key from the project directory name instead of generating a new one
|
|
174
|
+
// The directory name format is: {projectName}-e2e-test-{randomId}
|
|
175
|
+
const dirName = basename(env.projectDir);
|
|
176
|
+
const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
|
|
177
|
+
const resourceUniqueKey = match
|
|
178
|
+
? match[1]
|
|
179
|
+
: Math.random().toString(36).substring(2, 15);
|
|
180
|
+
const deployResult = await runRelease(env.projectDir, env.projectDir, resourceUniqueKey);
|
|
181
|
+
// Poll the URL to ensure it's live before proceeding
|
|
182
|
+
await poll(async () => {
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(deployResult.url);
|
|
185
|
+
// We consider any response (even 4xx or 5xx) as success,
|
|
186
|
+
// as it means the worker is routable.
|
|
187
|
+
return response.status > 0;
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}, 60000);
|
|
193
|
+
const deploymentId = `deployment-${Date.now()}-${Math.random()
|
|
194
|
+
.toString(36)
|
|
195
|
+
.substring(2, 9)}`;
|
|
196
|
+
// Register automatic cleanup (non-blocking for deployments)
|
|
197
|
+
registerCleanupTask({
|
|
198
|
+
id: deploymentId,
|
|
199
|
+
type: "deployment",
|
|
200
|
+
cleanup: async () => {
|
|
201
|
+
// Run deployment cleanup in background without blocking
|
|
202
|
+
const performCleanup = async () => {
|
|
203
|
+
if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
|
|
204
|
+
await deleteWorker(deployResult.workerName, env.projectDir, resourceUniqueKey);
|
|
205
|
+
}
|
|
206
|
+
await deleteD1Database(resourceUniqueKey, env.projectDir, resourceUniqueKey);
|
|
207
|
+
};
|
|
208
|
+
// Start cleanup in background and return immediately
|
|
209
|
+
performCleanup().catch((error) => {
|
|
210
|
+
console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
|
|
211
|
+
});
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
url: deployResult.url,
|
|
217
|
+
workerName: deployResult.workerName,
|
|
218
|
+
resourceUniqueKey,
|
|
219
|
+
projectDir: env.projectDir,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Manually cleans up a deployment instance (deletes worker and D1 database).
|
|
224
|
+
* This is optional since cleanup happens automatically after each test.
|
|
225
|
+
*/
|
|
226
|
+
export async function cleanupDeployment(deployment) {
|
|
227
|
+
console.log(`🧹 Cleaning up deployment: ${deployment.workerName} (${deployment.resourceUniqueKey})`);
|
|
228
|
+
const env = getPlaygroundEnvironment();
|
|
229
|
+
if (isRelatedToTest(deployment.workerName, deployment.resourceUniqueKey)) {
|
|
230
|
+
await deleteWorker(deployment.workerName, env.projectDir, deployment.resourceUniqueKey);
|
|
231
|
+
}
|
|
232
|
+
await deleteD1Database(deployment.resourceUniqueKey, env.projectDir, deployment.resourceUniqueKey);
|
|
233
|
+
// Remove from auto-cleanup registry since manually cleaned
|
|
234
|
+
const deploymentId = cleanupTasks.find((task) => task.type === "deployment" &&
|
|
235
|
+
task.id.includes(deployment.resourceUniqueKey))?.id;
|
|
236
|
+
if (deploymentId) {
|
|
237
|
+
unregisterCleanupTask(deploymentId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Creates a browser instance for testing.
|
|
242
|
+
* Automatically registers cleanup to run after the test.
|
|
243
|
+
*/
|
|
244
|
+
export async function createBrowser() {
|
|
245
|
+
// Check if we should run in headed mode for debugging
|
|
246
|
+
const headless = process.env.RWSDK_HEADLESS !== "false";
|
|
247
|
+
const browser = await launchBrowser(undefined, headless);
|
|
248
|
+
const browserId = `browser-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
249
|
+
// Register automatic cleanup
|
|
250
|
+
registerCleanupTask({
|
|
251
|
+
id: browserId,
|
|
252
|
+
type: "browser",
|
|
253
|
+
cleanup: async () => {
|
|
254
|
+
try {
|
|
255
|
+
await browser.close();
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
// Browser might already be closed, ignore the error
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
// Wrap the close method to handle cleanup registration
|
|
263
|
+
const originalClose = browser.close.bind(browser);
|
|
264
|
+
browser.close = async () => {
|
|
265
|
+
await originalClose();
|
|
266
|
+
unregisterCleanupTask(browserId); // Remove from auto-cleanup since manually closed
|
|
267
|
+
};
|
|
268
|
+
return browser;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Executes a test function with a retry mechanism for specific error codes.
|
|
272
|
+
* @param name - The name of the test, used for logging.
|
|
273
|
+
* @param attemptFn - A function that executes one attempt of the test.
|
|
274
|
+
* It should set up resources, run the test logic, and
|
|
275
|
+
* return a cleanup function. The cleanup function will be
|
|
276
|
+
* called automatically on failure.
|
|
277
|
+
*/
|
|
278
|
+
export async function runTestWithRetries(name, attemptFn) {
|
|
279
|
+
const MAX_RETRIES_PER_CODE = 6;
|
|
280
|
+
const retryCounts = {};
|
|
281
|
+
let attempt = 0;
|
|
282
|
+
while (true) {
|
|
283
|
+
attempt++;
|
|
284
|
+
let cleanup;
|
|
285
|
+
try {
|
|
286
|
+
const res = await attemptFn();
|
|
287
|
+
cleanup = res.cleanup;
|
|
288
|
+
if (attempt > 1) {
|
|
289
|
+
console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
|
|
290
|
+
}
|
|
291
|
+
// On success, we don't run cleanup here. It will be handled by afterEach.
|
|
292
|
+
return; // Success
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
// On failure, run the cleanup from the failed attempt.
|
|
296
|
+
// The cleanup function is attached to the error object on failure.
|
|
297
|
+
const errorCleanup = e.cleanup;
|
|
298
|
+
if (typeof errorCleanup === "function") {
|
|
299
|
+
await errorCleanup().catch((err) => console.warn(`[runTestWithRetries] Cleanup failed for "${name}" during retry:`, err));
|
|
300
|
+
}
|
|
301
|
+
const errorCode = e?.code;
|
|
302
|
+
if (typeof errorCode === "string" && errorCode) {
|
|
303
|
+
const count = (retryCounts[errorCode] || 0) + 1;
|
|
304
|
+
retryCounts[errorCode] = count;
|
|
305
|
+
if (count <= MAX_RETRIES_PER_CODE) {
|
|
306
|
+
console.log(`[runTestWithRetries] Attempt ${attempt} for "${name}" failed with code ${errorCode}. Retrying (failure ${count}/${MAX_RETRIES_PER_CODE} for this code)...`);
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
308
|
+
continue; // Next attempt
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${MAX_RETRIES_PER_CODE} retries for this code.`);
|
|
312
|
+
throw e; // Give up
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.error(`[runTestWithRetries] Test "${name}" failed on attempt ${attempt} with a non-retryable error:`, e);
|
|
317
|
+
throw e;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* High-level test wrapper for dev server tests.
|
|
324
|
+
* Automatically skips if RWSDK_SKIP_DEV=1
|
|
325
|
+
*/
|
|
326
|
+
export function testDev(name, testFn) {
|
|
327
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
328
|
+
test.skip(name, testFn);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
test(name, async () => {
|
|
332
|
+
await runTestWithRetries(name, async () => {
|
|
333
|
+
const devServer = await createDevServer();
|
|
334
|
+
const browser = await createBrowser();
|
|
335
|
+
const page = await browser.newPage();
|
|
336
|
+
page.setDefaultTimeout(PUPPETEER_TIMEOUT);
|
|
337
|
+
const cleanup = async () => {
|
|
338
|
+
await browser.close();
|
|
339
|
+
await devServer.stopDev();
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
await testFn({
|
|
343
|
+
devServer,
|
|
344
|
+
browser,
|
|
345
|
+
page,
|
|
346
|
+
url: devServer.url,
|
|
347
|
+
});
|
|
348
|
+
return { cleanup };
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
// Ensure cleanup is available to the retry wrapper even if testFn fails.
|
|
352
|
+
// We re-throw the error to be handled by runTestWithRetries.
|
|
353
|
+
throw Object.assign(error, { cleanup });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Skip version of testDev
|
|
360
|
+
*/
|
|
361
|
+
testDev.skip = (name, testFn) => {
|
|
362
|
+
test.skip(name, testFn || (() => { }));
|
|
363
|
+
};
|
|
364
|
+
testDev.only = (name, testFn) => {
|
|
365
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
366
|
+
test.skip(name, () => { });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
test.only(name, async () => {
|
|
370
|
+
await runTestWithRetries(name, async () => {
|
|
371
|
+
const devServer = await createDevServer();
|
|
372
|
+
const browser = await createBrowser();
|
|
373
|
+
const page = await browser.newPage();
|
|
374
|
+
page.setDefaultTimeout(PUPPETEER_TIMEOUT);
|
|
375
|
+
const cleanup = async () => {
|
|
376
|
+
await browser.close();
|
|
377
|
+
await devServer.stopDev();
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
await testFn({
|
|
381
|
+
devServer,
|
|
382
|
+
browser,
|
|
383
|
+
page,
|
|
384
|
+
url: devServer.url,
|
|
385
|
+
});
|
|
386
|
+
return { cleanup };
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
// Ensure cleanup is available to the retry wrapper even if testFn fails.
|
|
390
|
+
// We re-throw the error to be handled by runTestWithRetries.
|
|
391
|
+
throw Object.assign(error, { cleanup });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* High-level test wrapper for deployment tests.
|
|
398
|
+
* Automatically skips if RWSDK_SKIP_DEPLOY=1
|
|
399
|
+
*/
|
|
400
|
+
export function testDeploy(name, testFn) {
|
|
401
|
+
if (SKIP_DEPLOYMENT_TESTS) {
|
|
402
|
+
test.skip(name, testFn);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
test(name, async () => {
|
|
406
|
+
await runTestWithRetries(name, async () => {
|
|
407
|
+
const deployment = await createDeployment();
|
|
408
|
+
const browser = await createBrowser();
|
|
409
|
+
const page = await browser.newPage();
|
|
410
|
+
page.setDefaultTimeout(PUPPETEER_TIMEOUT);
|
|
411
|
+
const cleanup = async () => {
|
|
412
|
+
// We don't await this because we want to let it run in the background
|
|
413
|
+
// The afterEach hook for deployments already does this.
|
|
414
|
+
await cleanupDeployment(deployment);
|
|
415
|
+
await browser.close();
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
await testFn({
|
|
419
|
+
deployment,
|
|
420
|
+
browser,
|
|
421
|
+
page,
|
|
422
|
+
url: deployment.url,
|
|
423
|
+
});
|
|
424
|
+
return { cleanup };
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
throw Object.assign(error, { cleanup });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Skip version of testDeploy
|
|
434
|
+
*/
|
|
435
|
+
testDeploy.skip = (name, testFn) => {
|
|
436
|
+
test.skip(name, testFn || (() => { }));
|
|
437
|
+
};
|
|
438
|
+
testDeploy.only = (name, testFn) => {
|
|
439
|
+
if (SKIP_DEPLOYMENT_TESTS) {
|
|
440
|
+
test.skip(name, () => { });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
test.only(name, async () => {
|
|
444
|
+
await runTestWithRetries(name, async () => {
|
|
445
|
+
const deployment = await createDeployment();
|
|
446
|
+
const browser = await createBrowser();
|
|
447
|
+
const page = await browser.newPage();
|
|
448
|
+
page.setDefaultTimeout(PUPPETEER_TIMEOUT);
|
|
449
|
+
const cleanup = async () => {
|
|
450
|
+
// We don't await this because we want to let it run in the background
|
|
451
|
+
// The afterEach hook for deployments already does this.
|
|
452
|
+
await cleanupDeployment(deployment);
|
|
453
|
+
await browser.close();
|
|
454
|
+
};
|
|
455
|
+
try {
|
|
456
|
+
await testFn({
|
|
457
|
+
deployment,
|
|
458
|
+
browser,
|
|
459
|
+
page,
|
|
460
|
+
url: deployment.url,
|
|
461
|
+
});
|
|
462
|
+
return { cleanup };
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
throw Object.assign(error, { cleanup });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* Unified test function that runs the same test against both dev server and deployment.
|
|
472
|
+
* Automatically skips based on environment variables.
|
|
473
|
+
*/
|
|
474
|
+
export function testDevAndDeploy(name, testFn) {
|
|
475
|
+
testDev(`${name} (dev)`, testFn);
|
|
476
|
+
testDeploy(`${name} (deployment)`, testFn);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Skip version of testDevAndDeploy
|
|
480
|
+
*/
|
|
481
|
+
testDevAndDeploy.skip = (name, testFn) => {
|
|
482
|
+
test.skip(name, testFn || (() => { }));
|
|
483
|
+
};
|
|
484
|
+
testDevAndDeploy.only = (name, testFn) => {
|
|
485
|
+
testDev.only(`${name} (dev)`, testFn);
|
|
486
|
+
testDeploy.only(`${name} (deployment)`, testFn);
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Utility function for polling/retrying assertions
|
|
490
|
+
*/
|
|
491
|
+
export async function poll(fn, timeout = 2 * 60 * 1000, // 2 minutes
|
|
492
|
+
interval = 100) {
|
|
493
|
+
const startTime = Date.now();
|
|
494
|
+
while (Date.now() - startTime < timeout) {
|
|
495
|
+
try {
|
|
496
|
+
const result = await fn();
|
|
497
|
+
if (result) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
// Continue polling on errors
|
|
503
|
+
}
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
505
|
+
}
|
|
506
|
+
throw new Error(`Polling timed out after ${timeout}ms`);
|
|
507
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "yarn-classic";
|
|
2
|
+
/**
|
|
3
|
+
* Options for smoke tests
|
|
4
|
+
*/
|
|
5
|
+
export interface SmokeTestOptions {
|
|
6
|
+
projectDir?: string;
|
|
7
|
+
artifactDir?: string;
|
|
8
|
+
headless?: boolean;
|
|
9
|
+
keep?: boolean;
|
|
10
|
+
sync?: boolean;
|
|
11
|
+
bail?: boolean;
|
|
12
|
+
skipDev?: boolean;
|
|
13
|
+
skipRelease?: boolean;
|
|
14
|
+
skipClient?: boolean;
|
|
15
|
+
packageManager?: PackageManager;
|
|
16
|
+
realtime?: boolean;
|
|
17
|
+
skipHmr?: boolean;
|
|
18
|
+
skipStyleTests?: boolean;
|
|
19
|
+
tarballPath?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resources created during a test run that need to be cleaned up
|
|
23
|
+
*/
|
|
24
|
+
export interface TestResources {
|
|
25
|
+
tempDirCleanup?: () => Promise<void>;
|
|
26
|
+
workerName?: string;
|
|
27
|
+
targetDir?: string;
|
|
28
|
+
originalCwd: string;
|
|
29
|
+
workerCreatedDuringTest: boolean;
|
|
30
|
+
stopDev?: () => Promise<void>;
|
|
31
|
+
resourceUniqueKey: string;
|
|
32
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { relative } from "node:path";
|
|
2
|
-
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const getShortName = (file, root) => file === root
|
|
4
|
+
? ""
|
|
5
|
+
: file.startsWith(root + path.sep)
|
|
6
|
+
? relative(root, file)
|
|
7
|
+
: file;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getShortName } from "./getShortName.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
describe("getShortName", () => {
|
|
5
|
+
it("should return the relative path if the file is inside the root", () => {
|
|
6
|
+
const root = path.join("/Users", "test", "project");
|
|
7
|
+
const file = path.join(root, "src", "index.ts");
|
|
8
|
+
expect(getShortName(file, root)).toBe(path.join("src", "index.ts"));
|
|
9
|
+
});
|
|
10
|
+
it("should return the original path if the file is outside the root", () => {
|
|
11
|
+
const root = path.join("/Users", "test", "project");
|
|
12
|
+
const file = path.join("/Users", "test", "another", "project", "src", "index.ts");
|
|
13
|
+
expect(getShortName(file, root)).toBe(file);
|
|
14
|
+
});
|
|
15
|
+
it("should return an empty string if the paths are identical", () => {
|
|
16
|
+
const root = path.join("/Users", "test", "project");
|
|
17
|
+
const file = path.join("/Users", "test", "project");
|
|
18
|
+
expect(getShortName(file, root)).toBe("");
|
|
19
|
+
});
|
|
20
|
+
it("should handle paths that are substrings of each other correctly", () => {
|
|
21
|
+
const root = path.join("/Users", "test", "project");
|
|
22
|
+
const file = path.join("/Users", "test", "project-longer", "src", "index.ts");
|
|
23
|
+
expect(getShortName(file, root)).toBe(file);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFile as fsReadFile } from "fs/promises";
|
|
2
|
+
export declare let _pkgCache: Record<string, any> | undefined;
|
|
3
|
+
export declare const hasPkgScript: (projectRootDir: string, script: string, readFile?: typeof fsReadFile) => Promise<any>;
|
|
4
|
+
export declare const _resetPkgCache: () => void;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
1
|
+
import { readFile as fsReadFile } from "fs/promises";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
-
let
|
|
4
|
-
export const hasPkgScript = async (projectRootDir, script) => {
|
|
5
|
-
if (!
|
|
6
|
-
|
|
3
|
+
export let _pkgCache;
|
|
4
|
+
export const hasPkgScript = async (projectRootDir, script, readFile = fsReadFile) => {
|
|
5
|
+
if (!_pkgCache) {
|
|
6
|
+
_pkgCache = JSON.parse((await readFile(resolve(projectRootDir, "package.json"))).toString());
|
|
7
7
|
}
|
|
8
|
-
return
|
|
8
|
+
return _pkgCache?.scripts?.[script];
|
|
9
|
+
};
|
|
10
|
+
export const _resetPkgCache = () => {
|
|
11
|
+
_pkgCache = undefined;
|
|
9
12
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { hasPkgScript, _resetPkgCache } from "./hasPkgScript.mjs";
|
|
3
|
+
// Manually reset the cache before each test
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
_resetPkgCache();
|
|
6
|
+
});
|
|
7
|
+
describe("hasPkgScript", () => {
|
|
8
|
+
it("should return the script if it exists", async () => {
|
|
9
|
+
const fakeReadFile = async () => JSON.stringify({ scripts: { "dev:init": "command" } });
|
|
10
|
+
const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
|
|
11
|
+
expect(result).toBe("command");
|
|
12
|
+
});
|
|
13
|
+
it("should return undefined if the script does not exist", async () => {
|
|
14
|
+
const fakeReadFile = async () => JSON.stringify({ scripts: { test: "command" } });
|
|
15
|
+
const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
|
|
16
|
+
expect(result).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
it("should return undefined if scripts block does not exist", async () => {
|
|
19
|
+
const fakeReadFile = async () => JSON.stringify({});
|
|
20
|
+
const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
|
|
21
|
+
expect(result).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
it("should cache the package.json read", async () => {
|
|
24
|
+
let readCount = 0;
|
|
25
|
+
const fakeReadFile = async () => {
|
|
26
|
+
readCount++;
|
|
27
|
+
return JSON.stringify({ scripts: { "dev:init": "command" } });
|
|
28
|
+
};
|
|
29
|
+
await hasPkgScript("/test", "dev:init", fakeReadFile);
|
|
30
|
+
await hasPkgScript("/test", "dev:init", fakeReadFile);
|
|
31
|
+
expect(readCount).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/dist/lib/jsonUtils.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|