rwsdk 1.0.0-beta.15 → 1.0.0-beta.16
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/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +1 -0
- package/dist/lib/e2e/constants.d.mts +2 -1
- package/dist/lib/e2e/constants.mjs +9 -2
- package/dist/lib/e2e/dev.mjs +0 -1
- package/dist/lib/e2e/environment.mjs +103 -13
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/testHarness.d.mts +5 -2
- package/dist/lib/e2e/testHarness.mjs +13 -9
- package/dist/runtime/requestInfo/worker.js +3 -2
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +68 -0
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +1 -12
- package/dist/vite/redwoodPlugin.mjs +7 -5
- package/dist/vite/ssrBridgePlugin.mjs +104 -34
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +69 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/package.json +5 -1
package/dist/lib/constants.d.mts
CHANGED
|
@@ -7,5 +7,6 @@ export declare const VENDOR_CLIENT_BARREL_PATH: string;
|
|
|
7
7
|
export declare const VENDOR_SERVER_BARREL_PATH: string;
|
|
8
8
|
export declare const VENDOR_CLIENT_BARREL_EXPORT_PATH = "rwsdk/__vendor_client_barrel";
|
|
9
9
|
export declare const VENDOR_SERVER_BARREL_EXPORT_PATH = "rwsdk/__vendor_server_barrel";
|
|
10
|
+
export declare const RW_STATE_EXPORT_PATH = "rwsdk/__state";
|
|
10
11
|
export declare const INTERMEDIATE_SSR_BRIDGE_PATH: string;
|
|
11
12
|
export declare const CLIENT_MANIFEST_RELATIVE_PATH: string;
|
package/dist/lib/constants.mjs
CHANGED
|
@@ -9,5 +9,6 @@ export const VENDOR_CLIENT_BARREL_PATH = path.resolve(INTERMEDIATES_OUTPUT_DIR,
|
|
|
9
9
|
export const VENDOR_SERVER_BARREL_PATH = path.resolve(INTERMEDIATES_OUTPUT_DIR, "rwsdk-vendor-server-barrel.js");
|
|
10
10
|
export const VENDOR_CLIENT_BARREL_EXPORT_PATH = "rwsdk/__vendor_client_barrel";
|
|
11
11
|
export const VENDOR_SERVER_BARREL_EXPORT_PATH = "rwsdk/__vendor_server_barrel";
|
|
12
|
+
export const RW_STATE_EXPORT_PATH = "rwsdk/__state";
|
|
12
13
|
export const INTERMEDIATE_SSR_BRIDGE_PATH = resolve(INTERMEDIATES_OUTPUT_DIR, "ssr", "ssr_bridge.js");
|
|
13
14
|
export const CLIENT_MANIFEST_RELATIVE_PATH = resolve("dist", "client", ".vite", "manifest.json");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const IS_CI: boolean;
|
|
2
|
+
export declare const IS_DEBUG_MODE: boolean;
|
|
2
3
|
export declare const SETUP_PLAYGROUND_ENV_TIMEOUT: number;
|
|
3
4
|
export declare const DEPLOYMENT_TIMEOUT: number;
|
|
4
5
|
export declare const DEPLOYMENT_MIN_TRIES: number;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
export const IS_CI = !!((process.env.CI && !process.env.NOT_CI) ||
|
|
2
|
+
process.env.GITHUB_ACTIONS ||
|
|
3
|
+
process.env.GITLAB_CI ||
|
|
4
|
+
process.env.CIRCLECI ||
|
|
5
|
+
process.env.TRAVIS ||
|
|
6
|
+
process.env.JENKINS_URL ||
|
|
7
|
+
process.env.NETLIFY);
|
|
1
8
|
export const IS_DEBUG_MODE = process.env.RWSDK_E2E_DEBUG
|
|
2
|
-
? process.env.RWSDK_E2E_DEBUG
|
|
3
|
-
: !
|
|
9
|
+
? process.env.RWSDK_E2E_DEBUG === "true"
|
|
10
|
+
: !IS_CI;
|
|
4
11
|
export const SETUP_PLAYGROUND_ENV_TIMEOUT = process.env
|
|
5
12
|
.RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT
|
|
6
13
|
? parseInt(process.env.RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT, 10)
|
package/dist/lib/e2e/dev.mjs
CHANGED
|
@@ -109,7 +109,6 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
|
|
|
109
109
|
// Listen for all output to get the URL
|
|
110
110
|
const handleOutput = (data, source) => {
|
|
111
111
|
const output = data.toString();
|
|
112
|
-
console.log(output);
|
|
113
112
|
allOutput += output; // Accumulate all output
|
|
114
113
|
log("Received output from %s: %s", source, output.replace(/\n/g, "\\n"));
|
|
115
114
|
if (!url) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import debug from "debug";
|
|
2
3
|
import { copy, pathExists } from "fs-extra";
|
|
3
4
|
import ignore from "ignore";
|
|
@@ -8,12 +9,25 @@ import { basename, join, relative, resolve } from "path";
|
|
|
8
9
|
import tmp from "tmp-promise";
|
|
9
10
|
import { $ } from "../../lib/$.mjs";
|
|
10
11
|
import { ROOT_DIR } from "../constants.mjs";
|
|
11
|
-
import { INSTALL_DEPENDENCIES_RETRIES } from "./constants.mjs";
|
|
12
|
+
import { INSTALL_DEPENDENCIES_RETRIES, IS_CI } from "./constants.mjs";
|
|
12
13
|
import { retry } from "./retry.mjs";
|
|
13
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;
|
|
18
|
+
if (IS_CACHE_ENABLED) {
|
|
19
|
+
log("E2E test caching is enabled.");
|
|
20
|
+
}
|
|
14
21
|
const getTempDir = async () => {
|
|
15
22
|
return tmp.dir({ unsafeCleanup: true });
|
|
16
23
|
};
|
|
24
|
+
function slugify(str) {
|
|
25
|
+
return str
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
28
|
+
.replace(/--+/g, "-")
|
|
29
|
+
.replace(/^-|-$/g, "");
|
|
30
|
+
}
|
|
17
31
|
const createSdkTarball = async () => {
|
|
18
32
|
const existingTarballPath = process.env.RWSKD_SMOKE_TEST_TARBALL_PATH;
|
|
19
33
|
if (existingTarballPath) {
|
|
@@ -28,15 +42,27 @@ const createSdkTarball = async () => {
|
|
|
28
42
|
}, // No-op cleanup
|
|
29
43
|
};
|
|
30
44
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
// 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-"));
|
|
47
|
+
await $({
|
|
48
|
+
cwd: ROOT_DIR,
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
}) `npm pack --pack-destination=${tempDir}`;
|
|
51
|
+
// We need to determine the tarball's name, as it's version-dependent.
|
|
52
|
+
// Running `npm pack --dry-run` gives us the filename without creating a file.
|
|
53
|
+
const packDryRun = await $({
|
|
54
|
+
cwd: ROOT_DIR,
|
|
55
|
+
stdio: "pipe",
|
|
56
|
+
}) `npm pack --dry-run`;
|
|
57
|
+
const tarballName = packDryRun.stdout?.trim();
|
|
58
|
+
const tarballPath = path.join(tempDir, tarballName);
|
|
59
|
+
if (!fs.existsSync(tarballPath)) {
|
|
60
|
+
throw new Error(`Tarball was not created in the expected location: ${tarballPath}`);
|
|
61
|
+
}
|
|
62
|
+
log(`📦 Created tarball in stable temp location: ${tarballPath}`);
|
|
35
63
|
const cleanupTarball = async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await fs.promises.rm(tarballPath, { force: true });
|
|
39
|
-
}
|
|
64
|
+
log(`🧹 Cleaning up tarball directory: ${tempDir}`);
|
|
65
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
40
66
|
};
|
|
41
67
|
return { tarballPath, cleanupTarball };
|
|
42
68
|
};
|
|
@@ -59,7 +85,7 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
|
|
|
59
85
|
const sourceDir = monorepoRoot || projectDir;
|
|
60
86
|
// Create unique project directory name
|
|
61
87
|
const originalDirName = basename(sourceDir);
|
|
62
|
-
const workerName = `${originalDirName}-test-${resourceUniqueKey}`;
|
|
88
|
+
const workerName = `${slugify(originalDirName)}-test-${resourceUniqueKey}`;
|
|
63
89
|
const tempCopyRoot = resolve(tempDir.path, workerName);
|
|
64
90
|
// If it's a monorepo, the targetDir for commands is a subdirectory
|
|
65
91
|
const targetDir = monorepoRoot
|
|
@@ -115,7 +141,9 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
|
|
|
115
141
|
const workspaces = rwsdkWs.workspaces;
|
|
116
142
|
if (packageManager === "pnpm") {
|
|
117
143
|
const pnpmWsPath = join(tempCopyRoot, "pnpm-workspace.yaml");
|
|
118
|
-
const pnpmWsConfig = `packages:\n${workspaces
|
|
144
|
+
const pnpmWsConfig = `packages:\n${workspaces
|
|
145
|
+
.map((w) => ` - '${w}'`)
|
|
146
|
+
.join("\n")}\n`;
|
|
119
147
|
await fs.promises.writeFile(pnpmWsPath, pnpmWsConfig);
|
|
120
148
|
log("Created pnpm-workspace.yaml");
|
|
121
149
|
}
|
|
@@ -157,7 +185,7 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
|
|
|
157
185
|
await setTarballDependency(targetDir, tarballFilename);
|
|
158
186
|
// Install dependencies in the target directory
|
|
159
187
|
const installDir = monorepoRoot ? tempCopyRoot : targetDir;
|
|
160
|
-
await retry(() => installDependencies(installDir, packageManager), {
|
|
188
|
+
await retry(() => installDependencies(installDir, packageManager, projectDir, monorepoRoot), {
|
|
161
189
|
retries: INSTALL_DEPENDENCIES_RETRIES,
|
|
162
190
|
delay: 1000,
|
|
163
191
|
});
|
|
@@ -168,7 +196,42 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
|
|
|
168
196
|
await cleanupTarball();
|
|
169
197
|
}
|
|
170
198
|
}
|
|
171
|
-
async function installDependencies(targetDir, packageManager = "pnpm") {
|
|
199
|
+
async function installDependencies(targetDir, packageManager = "pnpm", projectDir, monorepoRoot) {
|
|
200
|
+
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);
|
|
213
|
+
const cacheDirName = monorepoRoot
|
|
214
|
+
? basename(monorepoRoot)
|
|
215
|
+
: basename(projectDir);
|
|
216
|
+
const cacheRoot = path.join(os.tmpdir(), "rwsdk-e2e-cache", `${cacheDirName}-${projectHash}`);
|
|
217
|
+
const nodeModulesCachePath = path.join(cacheRoot, "node_modules");
|
|
218
|
+
if (await pathExists(nodeModulesCachePath)) {
|
|
219
|
+
console.log(`✅ CACHE HIT for source "${projectIdentifier}": Found cached node_modules. Hard-linking from ${nodeModulesCachePath}`);
|
|
220
|
+
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.`);
|
|
224
|
+
}
|
|
225
|
+
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.`);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.log(`ℹ️ CACHE MISS for source "${projectIdentifier}": No cached node_modules found at ${nodeModulesCachePath}. Proceeding with installation.`);
|
|
234
|
+
}
|
|
172
235
|
console.log(`📦 Installing project dependencies in ${targetDir} using ${packageManager}...`);
|
|
173
236
|
try {
|
|
174
237
|
// Clean up any pre-existing node_modules and lockfiles
|
|
@@ -220,6 +283,33 @@ async function installDependencies(targetDir, packageManager = "pnpm") {
|
|
|
220
283
|
},
|
|
221
284
|
});
|
|
222
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,
|
|
309
|
+
});
|
|
310
|
+
await copy(join(targetDir, "node_modules"), nodeModulesCachePath);
|
|
311
|
+
console.log(`✅ node_modules cached successfully.`);
|
|
312
|
+
}
|
|
223
313
|
// Log installation details at debug level
|
|
224
314
|
if (result.stdout) {
|
|
225
315
|
log(`${packageManager} install output: %s`, result.stdout);
|
package/dist/lib/e2e/poll.d.mts
CHANGED
|
@@ -4,5 +4,5 @@ export interface PollOptions {
|
|
|
4
4
|
minTries: number;
|
|
5
5
|
onRetry?: (error: unknown, tries: number) => void;
|
|
6
6
|
}
|
|
7
|
-
export declare function poll(fn: () => Promise<boolean>, options?: Partial<PollOptions>): Promise<void>;
|
|
7
|
+
export declare function poll(fn: () => boolean | Promise<boolean>, options?: Partial<PollOptions>): Promise<void>;
|
|
8
8
|
export declare function pollValue<T>(fn: () => Promise<T>, options?: Partial<PollOptions>): Promise<T>;
|
|
@@ -37,6 +37,11 @@ export interface SetupPlaygroundEnvironmentOptions {
|
|
|
37
37
|
* @default true
|
|
38
38
|
*/
|
|
39
39
|
deploy?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to automatically start the dev server.
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
autoStartDevServer?: boolean;
|
|
40
45
|
}
|
|
41
46
|
/**
|
|
42
47
|
* A Vitest hook that sets up a playground environment for a test file.
|
|
@@ -77,8 +82,6 @@ export declare function createDeployment(): {
|
|
|
77
82
|
*/
|
|
78
83
|
export declare function runTestWithRetries(name: string, attemptFn: () => Promise<void>): Promise<void>;
|
|
79
84
|
type SDKRunner = (name: string, testLogic: (context: {
|
|
80
|
-
createDevServer: () => Promise<DevServerInstance>;
|
|
81
|
-
createDeployment: () => Promise<DeploymentInstance>;
|
|
82
85
|
browser: Browser;
|
|
83
86
|
page: Page;
|
|
84
87
|
projectDir: string;
|
|
@@ -85,7 +85,9 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
|
|
|
85
85
|
* This ensures that tests run in a clean, isolated environment.
|
|
86
86
|
*/
|
|
87
87
|
export function setupPlaygroundEnvironment(options) {
|
|
88
|
-
const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string"
|
|
88
|
+
const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, autoStartDevServer = true, } = typeof options === "string"
|
|
89
|
+
? { sourceProjectDir: options, autoStartDevServer: true }
|
|
90
|
+
: options;
|
|
89
91
|
ensureHooksRegistered();
|
|
90
92
|
beforeAll(async () => {
|
|
91
93
|
let projectDir;
|
|
@@ -111,14 +113,16 @@ export function setupPlaygroundEnvironment(options) {
|
|
|
111
113
|
projectDir: devEnv.targetDir,
|
|
112
114
|
cleanup: devEnv.cleanup,
|
|
113
115
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
if (autoStartDevServer) {
|
|
117
|
+
const devControl = createDevServer();
|
|
118
|
+
globalDevInstancePromise = devControl.start().then((instance) => {
|
|
119
|
+
globalDevInstance = instance;
|
|
120
|
+
return instance;
|
|
121
|
+
});
|
|
122
|
+
// Prevent unhandled promise rejections. The error will be handled inside
|
|
123
|
+
// the test's beforeEach hook where this promise is awaited.
|
|
124
|
+
globalDevInstancePromise.catch(() => { });
|
|
125
|
+
}
|
|
122
126
|
}
|
|
123
127
|
else {
|
|
124
128
|
globalDevPlaygroundEnv = null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
|
|
3
|
-
const
|
|
2
|
+
import { defineRwState } from "rwsdk/__state";
|
|
3
|
+
const requestInfoDeferred = defineRwState("requestInfoDeferred", () => Promise.withResolvers());
|
|
4
|
+
const requestInfoStore = defineRwState("requestInfoStore", () => new AsyncLocalStorage());
|
|
4
5
|
const requestInfoBase = {};
|
|
5
6
|
const REQUEST_INFO_KEYS = ["request", "params", "ctx", "rw", "cf", "response"];
|
|
6
7
|
REQUEST_INFO_KEYS.forEach((key) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const state = {};
|
|
2
|
+
export function defineRwState(key, initializer) {
|
|
3
|
+
if (!(key in state)) {
|
|
4
|
+
state[key] = initializer();
|
|
5
|
+
}
|
|
6
|
+
return state[key];
|
|
7
|
+
}
|
|
8
|
+
export function getRwState(key) {
|
|
9
|
+
return state[key];
|
|
10
|
+
}
|
|
11
|
+
export function setRwState(key, value) {
|
|
12
|
+
state[key] = value;
|
|
13
|
+
}
|
|
@@ -99,28 +99,20 @@ const findUp = async (names, startDir) => {
|
|
|
99
99
|
return undefined;
|
|
100
100
|
};
|
|
101
101
|
const getMonorepoRoot = async (startDir) => {
|
|
102
|
-
|
|
103
|
-
// `pnpm root` is the most reliable way to find the workspace root node_modules
|
|
104
|
-
const { stdout } = await $({
|
|
105
|
-
cwd: startDir,
|
|
106
|
-
}) `pnpm root`;
|
|
107
|
-
// pnpm root returns the node_modules path, so we go up one level
|
|
108
|
-
return path.resolve(stdout, "..");
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
console.warn(`Could not determine pnpm root from ${startDir}. Falling back to file search.`);
|
|
112
|
-
const root = await findUp(["pnpm-workspace.yaml"], startDir);
|
|
113
|
-
if (root) {
|
|
114
|
-
return root;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
console.warn("Could not find pnpm monorepo root. Using parent directory of target as fallback.");
|
|
118
|
-
return path.resolve(startDir, "..");
|
|
102
|
+
return await findUp(["pnpm-workspace.yaml"], startDir);
|
|
119
103
|
};
|
|
120
104
|
const areDependenciesEqual = (deps1, deps2) => {
|
|
121
105
|
// Simple string comparison for this use case is sufficient
|
|
122
106
|
return JSON.stringify(deps1 ?? {}) === JSON.stringify(deps2 ?? {});
|
|
123
107
|
};
|
|
108
|
+
const isPlaygroundExample = async (targetDir, monorepoRoot) => {
|
|
109
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(monorepoRoot, "package.json"), "utf-8"));
|
|
110
|
+
if (pkgJson.name === "rw-sdk-monorepo") {
|
|
111
|
+
const playgroundDir = path.join(monorepoRoot, "playground");
|
|
112
|
+
return targetDir.startsWith(playgroundDir);
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
124
116
|
const performFullSync = async (sdkDir, targetDir, monorepoRoot) => {
|
|
125
117
|
console.log("📦 Performing full sync with tarball...");
|
|
126
118
|
let tarballPath = "";
|
|
@@ -178,8 +170,14 @@ const performSync = async (sdkDir, targetDir) => {
|
|
|
178
170
|
// Clean up vite cache in the target project
|
|
179
171
|
await cleanupViteEntries(targetDir);
|
|
180
172
|
const monorepoRoot = await getMonorepoRoot(targetDir);
|
|
173
|
+
const rootDir = monorepoRoot ?? targetDir;
|
|
181
174
|
const projectName = path.basename(targetDir);
|
|
182
|
-
|
|
175
|
+
if (monorepoRoot && (await isPlaygroundExample(targetDir, monorepoRoot))) {
|
|
176
|
+
console.log("Playground example detected. Skipping file sync; workspace linking will be used.");
|
|
177
|
+
console.log("✅ Done syncing");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const installedSdkPackageJsonPath = path.join(rootDir, "node_modules", `.rwsync_${projectName}`, "node_modules", "rwsdk", "package.json");
|
|
183
181
|
let needsFullSync = false;
|
|
184
182
|
if (!existsSync(installedSdkPackageJsonPath)) {
|
|
185
183
|
console.log("No previous sync found, performing full sync.");
|
|
@@ -195,10 +193,10 @@ const performSync = async (sdkDir, targetDir) => {
|
|
|
195
193
|
}
|
|
196
194
|
}
|
|
197
195
|
if (needsFullSync) {
|
|
198
|
-
await performFullSync(sdkDir, targetDir,
|
|
196
|
+
await performFullSync(sdkDir, targetDir, rootDir);
|
|
199
197
|
}
|
|
200
198
|
else {
|
|
201
|
-
await performFastSync(sdkDir, targetDir,
|
|
199
|
+
await performFastSync(sdkDir, targetDir, rootDir);
|
|
202
200
|
}
|
|
203
201
|
console.log("✅ Done syncing");
|
|
204
202
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import enhancedResolve from "enhanced-resolve";
|
|
2
|
+
export declare const ENV_RESOLVERS: {
|
|
3
|
+
ssr: enhancedResolve.ResolveFunction;
|
|
4
|
+
worker: enhancedResolve.ResolveFunction;
|
|
5
|
+
client: enhancedResolve.ResolveFunction;
|
|
6
|
+
};
|
|
7
|
+
export declare const maybeResolveEnvImport: ({ id, envName, projectRootDir, }: {
|
|
8
|
+
id: string;
|
|
9
|
+
envName: keyof typeof ENV_RESOLVERS;
|
|
10
|
+
projectRootDir: string;
|
|
11
|
+
}) => string | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import enhancedResolve from "enhanced-resolve";
|
|
2
|
+
export const ENV_RESOLVERS = {
|
|
3
|
+
ssr: enhancedResolve.create.sync({
|
|
4
|
+
conditionNames: ["workerd", "worker", "edge", "default"],
|
|
5
|
+
}),
|
|
6
|
+
worker: enhancedResolve.create.sync({
|
|
7
|
+
conditionNames: ["react-server", "workerd", "worker", "edge", "default"],
|
|
8
|
+
}),
|
|
9
|
+
client: enhancedResolve.create.sync({
|
|
10
|
+
conditionNames: ["browser", "default"],
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
export const maybeResolveEnvImport = ({ id, envName, projectRootDir, }) => {
|
|
14
|
+
try {
|
|
15
|
+
return ENV_RESOLVERS[envName](projectRootDir, id) || undefined;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
const log = debug("rws-vite-plugin:hmr-stability");
|
|
3
|
+
let stabilityPromise = null;
|
|
4
|
+
let stabilityResolver = null;
|
|
5
|
+
let debounceTimer = null;
|
|
6
|
+
const DEBOUNCE_MS = 500;
|
|
7
|
+
function startWaitingForStability() {
|
|
8
|
+
if (!stabilityPromise) {
|
|
9
|
+
log("Starting to wait for server stability...");
|
|
10
|
+
stabilityPromise = new Promise((resolve) => {
|
|
11
|
+
stabilityResolver = resolve;
|
|
12
|
+
});
|
|
13
|
+
// Start the timer. If it fires, we're stable.
|
|
14
|
+
debounceTimer = setTimeout(finishWaiting, DEBOUNCE_MS);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function activityDetected() {
|
|
18
|
+
if (stabilityPromise) {
|
|
19
|
+
// If we're waiting for stability, reset the timer.
|
|
20
|
+
log("Activity detected, resetting stability timer.");
|
|
21
|
+
if (debounceTimer)
|
|
22
|
+
clearTimeout(debounceTimer);
|
|
23
|
+
debounceTimer = setTimeout(finishWaiting, DEBOUNCE_MS);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function finishWaiting() {
|
|
27
|
+
if (stabilityResolver) {
|
|
28
|
+
log("Server appears stable. Resolving promise.");
|
|
29
|
+
stabilityResolver();
|
|
30
|
+
}
|
|
31
|
+
stabilityPromise = null;
|
|
32
|
+
stabilityResolver = null;
|
|
33
|
+
debounceTimer = null;
|
|
34
|
+
}
|
|
35
|
+
export function hmrStabilityPlugin() {
|
|
36
|
+
return {
|
|
37
|
+
name: "rws-vite-plugin:hmr-stability",
|
|
38
|
+
// Monitor server activity
|
|
39
|
+
transform() {
|
|
40
|
+
activityDetected();
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
configureServer(server) {
|
|
44
|
+
// Return a function to ensure our middleware is placed after internal middlewares
|
|
45
|
+
return () => {
|
|
46
|
+
server.middlewares.use(async function rwsdkStaleBundleErrorHandler(err, req, res, next) {
|
|
47
|
+
if (err &&
|
|
48
|
+
typeof err.message === "string" &&
|
|
49
|
+
err.message.includes("new version of the pre-bundle")) {
|
|
50
|
+
log("Caught stale pre-bundle error. Waiting for server to stabilize...");
|
|
51
|
+
startWaitingForStability();
|
|
52
|
+
await stabilityPromise;
|
|
53
|
+
log("Server stabilized. Sending full-reload and redirecting.");
|
|
54
|
+
// Signal the client to do a full page reload.
|
|
55
|
+
server.environments.client.hot.send({
|
|
56
|
+
type: "full-reload",
|
|
57
|
+
});
|
|
58
|
+
// No need to wait further here, the stability promise handled it.
|
|
59
|
+
res.writeHead(307, { Location: req.url });
|
|
60
|
+
res.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
next(err);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import enhancedResolve from "enhanced-resolve";
|
|
2
1
|
import { Plugin } from "vite";
|
|
3
2
|
export declare const ENV_PREDEFINED_IMPORTS: {
|
|
4
3
|
worker: string[];
|
|
5
4
|
ssr: string[];
|
|
6
5
|
client: string[];
|
|
7
6
|
};
|
|
8
|
-
export declare const ENV_RESOLVERS: {
|
|
9
|
-
ssr: enhancedResolve.ResolveFunction;
|
|
10
|
-
worker: enhancedResolve.ResolveFunction;
|
|
11
|
-
client: enhancedResolve.ResolveFunction;
|
|
12
|
-
};
|
|
13
7
|
export declare const knownDepsResolverPlugin: ({ projectRootDir, }: {
|
|
14
8
|
projectRootDir: string;
|
|
15
9
|
}) => Plugin[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
|
-
import enhancedResolve from "enhanced-resolve";
|
|
3
2
|
import { ROOT_DIR } from "../lib/constants.mjs";
|
|
4
3
|
import { ensureAliasArray } from "./ensureAliasArray.mjs";
|
|
4
|
+
import { ENV_RESOLVERS } from "./envResolvers.mjs";
|
|
5
5
|
const log = debug("rwsdk:vite:known-deps-resolver-plugin");
|
|
6
6
|
const KNOWN_PREFIXES = [
|
|
7
7
|
"react",
|
|
@@ -36,17 +36,6 @@ export const ENV_PREDEFINED_IMPORTS = {
|
|
|
36
36
|
"react-server-dom-webpack/client.edge",
|
|
37
37
|
],
|
|
38
38
|
};
|
|
39
|
-
export const ENV_RESOLVERS = {
|
|
40
|
-
ssr: enhancedResolve.create.sync({
|
|
41
|
-
conditionNames: ["workerd", "worker", "edge", "default"],
|
|
42
|
-
}),
|
|
43
|
-
worker: enhancedResolve.create.sync({
|
|
44
|
-
conditionNames: ["react-server", "workerd", "worker", "edge", "default"],
|
|
45
|
-
}),
|
|
46
|
-
client: enhancedResolve.create.sync({
|
|
47
|
-
conditionNames: ["browser", "default"],
|
|
48
|
-
}),
|
|
49
|
-
};
|
|
50
39
|
function resolveKnownImport(id, envName, projectRootDir, isPrefixedImport = false) {
|
|
51
40
|
if (!isPrefixedImport) {
|
|
52
41
|
const isKnownImport = KNOWN_PREFIXES.some((prefix) => id === prefix || id.startsWith(`${prefix}/`));
|
|
@@ -23,6 +23,8 @@ import { moveStaticAssetsPlugin } from "./moveStaticAssetsPlugin.mjs";
|
|
|
23
23
|
import { prismaPlugin } from "./prismaPlugin.mjs";
|
|
24
24
|
import { resolveForcedPaths } from "./resolveForcedPaths.mjs";
|
|
25
25
|
import { ssrBridgePlugin } from "./ssrBridgePlugin.mjs";
|
|
26
|
+
import { staleDepRetryPlugin } from "./staleDepRetryPlugin.mjs";
|
|
27
|
+
import { statePlugin } from "./statePlugin.mjs";
|
|
26
28
|
import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
|
|
27
29
|
import { useClientLookupPlugin } from "./useClientLookupPlugin.mjs";
|
|
28
30
|
import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
|
|
@@ -77,13 +79,13 @@ export const redwoodPlugin = async (options = {}) => {
|
|
|
77
79
|
!(await pathExists(resolve(projectRootDir, ".wrangler"))) &&
|
|
78
80
|
(await hasPkgScript(projectRootDir, "dev:init"))) {
|
|
79
81
|
console.log("🚀 Project has no .wrangler directory yet, assuming fresh install: running `npm run dev:init`...");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
84
|
-
}) `npm run dev:init`;
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
$.verbose = true;
|
|
84
|
+
await $ `npm run dev:init`;
|
|
85
85
|
}
|
|
86
86
|
return [
|
|
87
|
+
staleDepRetryPlugin(),
|
|
88
|
+
statePlugin({ projectRootDir }),
|
|
87
89
|
devServerTimingPlugin(),
|
|
88
90
|
devServerConstantPlugin(),
|
|
89
91
|
directiveModulesDevPlugin({
|
|
@@ -10,9 +10,32 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
10
10
|
const ssrBridgePlugin = {
|
|
11
11
|
name: "rwsdk:ssr-bridge",
|
|
12
12
|
enforce: "pre",
|
|
13
|
-
|
|
13
|
+
configureServer(server) {
|
|
14
14
|
devServer = server;
|
|
15
|
+
const ssrHot = server.environments.ssr.hot;
|
|
16
|
+
const originalSsrHotSend = ssrHot.send;
|
|
17
|
+
// Chain the SSR's full reload behaviour to the worker
|
|
18
|
+
ssrHot.send = (...args) => {
|
|
19
|
+
if (typeof args[0] === "object" && args[0].type === "full-reload") {
|
|
20
|
+
for (const envName of ["worker", "ssr"]) {
|
|
21
|
+
const moduleGraph = server.environments[envName].moduleGraph;
|
|
22
|
+
moduleGraph.invalidateAll();
|
|
23
|
+
}
|
|
24
|
+
log("SSR full-reload detected, propagating to worker");
|
|
25
|
+
// context(justinvdm, 21 Oct 2025): By sending the full-reload event
|
|
26
|
+
// to the worker, we ensure that the worker's module runner cache is
|
|
27
|
+
// invalidated, as it would have been if this were a full-reload event
|
|
28
|
+
// from the worker.
|
|
29
|
+
server.environments.worker.hot.send.apply(server.environments.worker.hot, args);
|
|
30
|
+
}
|
|
31
|
+
return originalSsrHotSend.apply(ssrHot, args);
|
|
32
|
+
};
|
|
15
33
|
log("Configured dev server");
|
|
34
|
+
const originalRun = devServer.environments.ssr.depsOptimizer?.run;
|
|
35
|
+
devServer.environments.ssr.depsOptimizer.run = async () => {
|
|
36
|
+
originalRun();
|
|
37
|
+
devServer.environments.worker.depsOptimizer.run();
|
|
38
|
+
};
|
|
16
39
|
},
|
|
17
40
|
config(_, { command, isPreview }) {
|
|
18
41
|
isDev = !isPreview && command === "serve";
|
|
@@ -48,7 +71,7 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
48
71
|
log("Worker environment esbuild configuration complete");
|
|
49
72
|
}
|
|
50
73
|
},
|
|
51
|
-
async resolveId(id) {
|
|
74
|
+
async resolveId(id, importer) {
|
|
52
75
|
// Skip during directive scanning to avoid performance issues
|
|
53
76
|
if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
|
|
54
77
|
return;
|
|
@@ -107,46 +130,93 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
107
130
|
if (id.startsWith(VIRTUAL_SSR_PREFIX) &&
|
|
108
131
|
this.environment.name === "worker") {
|
|
109
132
|
const realId = id.slice(VIRTUAL_SSR_PREFIX.length);
|
|
110
|
-
|
|
133
|
+
let idForFetch = realId.endsWith(".css.js")
|
|
111
134
|
? realId.slice(0, -3)
|
|
112
135
|
: realId;
|
|
113
136
|
log("Virtual SSR module load: id=%s, realId=%s, idForFetch=%s", id, realId, idForFetch);
|
|
114
137
|
if (isDev) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
// from the SSR environment, which is crucial for things like server
|
|
139
|
+
// components.
|
|
140
|
+
try {
|
|
141
|
+
const ssrOptimizer = devServer.environments.ssr.depsOptimizer;
|
|
142
|
+
// context(justinvdm, 20 Oct 2025): This is the fix for the stale
|
|
143
|
+
// dependency issue. The root cause is the "unhashed-to-hashed"
|
|
144
|
+
// transition. Our worker code imports a clean ID
|
|
145
|
+
// (`rwsdk/__ssr_bridge`), but we expect to fetch the hashed,
|
|
146
|
+
// optimized version from the SSR environment. When a re-optimization
|
|
147
|
+
// happens, Vite's `fetchModule` (running in the SSR env) finds a
|
|
148
|
+
// "ghost node" in its module graph for the clean ID and incorrectly
|
|
149
|
+
// re-uses its stale, hashed `id` property.
|
|
150
|
+
//
|
|
151
|
+
// To fix this, we manually resolve the hashed path here, before
|
|
152
|
+
// asking the SSR env to process the module. We look into the SSR
|
|
153
|
+
// optimizer's metadata to find the correct, up-to-date hash and
|
|
154
|
+
// construct the path ourselves. This ensures the SSR env is
|
|
155
|
+
// always working with the correct, versioned ID, bypassing the
|
|
156
|
+
// faulty ghost node lookup.
|
|
157
|
+
if (ssrOptimizer &&
|
|
158
|
+
Object.prototype.hasOwnProperty.call(ssrOptimizer.metadata.optimized, realId)) {
|
|
159
|
+
const depInfo = ssrOptimizer.metadata.optimized[realId];
|
|
160
|
+
idForFetch = ssrOptimizer.getOptimizedDepId(depInfo);
|
|
161
|
+
log("Manually resolved %s to hashed path for fetchModule: %s", realId, idForFetch);
|
|
162
|
+
}
|
|
163
|
+
log("Virtual SSR module load: id=%s, realId=%s, idForFetch=%s", id, realId, idForFetch);
|
|
164
|
+
log("Dev mode: fetching SSR module for realPath=%s", idForFetch);
|
|
165
|
+
// We use `fetchModule` with `cached: false` as a safeguard. Since
|
|
166
|
+
// we're in a `load` hook, we know the worker-side cache for this
|
|
167
|
+
// virtual module is stale. `cached: false` ensures that we also
|
|
168
|
+
// bypass any potentially stale transform result in the SSR
|
|
169
|
+
// environment's cache, guaranteeing we get the freshest possible
|
|
170
|
+
// code.
|
|
171
|
+
const result = await devServer.environments.ssr.fetchModule(idForFetch, undefined, { cached: false });
|
|
172
|
+
if ("code" in result) {
|
|
173
|
+
log("Fetched SSR module code length: %d", result.code?.length || 0);
|
|
174
|
+
const code = result.code;
|
|
175
|
+
if (idForFetch.endsWith(".css") &&
|
|
176
|
+
!idForFetch.endsWith(".module.css")) {
|
|
177
|
+
process.env.VERBOSE &&
|
|
178
|
+
log("Plain CSS file, returning empty module for %s", idForFetch);
|
|
179
|
+
return "export default {};";
|
|
180
|
+
}
|
|
181
|
+
const s = new MagicString(code || "");
|
|
182
|
+
const callsites = findSsrImportCallSites(idForFetch, code || "", log);
|
|
183
|
+
for (const site of callsites) {
|
|
184
|
+
const normalized = site.specifier.startsWith("/@id/")
|
|
185
|
+
? site.specifier.slice("/@id/".length)
|
|
186
|
+
: site.specifier;
|
|
187
|
+
// context(justinvdm, 11 Aug 2025):
|
|
188
|
+
// - We replace __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
189
|
+
// with import() calls so that the module graph can be built
|
|
190
|
+
// correctly (vite looks for imports and import()s to build module
|
|
191
|
+
// graph)
|
|
192
|
+
// - We prepend /@id/$VIRTUAL_SSR_PREFIX to the specifier so that we
|
|
193
|
+
// can stay within the SSR subgraph of the worker module graph
|
|
194
|
+
const replacement = `import("/@id/${VIRTUAL_SSR_PREFIX}${normalized}")`;
|
|
195
|
+
s.overwrite(site.start, site.end, replacement);
|
|
196
|
+
}
|
|
197
|
+
const out = s.toString();
|
|
198
|
+
process.env.VERBOSE &&
|
|
199
|
+
log("Transformed SSR module code for realId=%s: %s", realId, out);
|
|
200
|
+
return {
|
|
201
|
+
code: out,
|
|
202
|
+
map: null, // Sourcemaps are handled by fetchModule's inlining
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// This case can be hit if the module is already cached. We may
|
|
207
|
+
// need to handle this more gracefully, but for now we'll just
|
|
208
|
+
// return an empty module.
|
|
209
|
+
log("SSR module %s was already cached. Returning empty.", idForFetch);
|
|
210
|
+
return "export default {}";
|
|
211
|
+
}
|
|
125
212
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (const site of callsites) {
|
|
130
|
-
const normalized = site.specifier.startsWith("/@id/")
|
|
131
|
-
? site.specifier.slice("/@id/".length)
|
|
132
|
-
: site.specifier;
|
|
133
|
-
// context(justinvdm, 11 Aug 2025):
|
|
134
|
-
// - We replace __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
135
|
-
// with import() calls so that the module graph can be built
|
|
136
|
-
// correctly (vite looks for imports and import()s to build module
|
|
137
|
-
// graph)
|
|
138
|
-
// - We prepend /@id/$VIRTUAL_SSR_PREFIX to the specifier so that we
|
|
139
|
-
// can stay within the SSR subgraph of the worker module graph
|
|
140
|
-
const replacement = `import("/@id/${VIRTUAL_SSR_PREFIX}${normalized}")`;
|
|
141
|
-
s.overwrite(site.start, site.end, replacement);
|
|
213
|
+
catch (e) {
|
|
214
|
+
log("Error fetching SSR module for realPath=%s: %s", id, e);
|
|
215
|
+
throw e;
|
|
142
216
|
}
|
|
143
|
-
const out = s.toString();
|
|
144
|
-
log("Transformed SSR module code length: %d", out.length);
|
|
145
|
-
process.env.VERBOSE &&
|
|
146
|
-
log("Transformed SSR module code for realId=%s: %s", realId, out);
|
|
147
|
-
return out;
|
|
148
217
|
}
|
|
149
218
|
}
|
|
219
|
+
return;
|
|
150
220
|
},
|
|
151
221
|
};
|
|
152
222
|
return ssrBridgePlugin;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
const log = debug("rwsdk:vite:stale-dep-retry-plugin");
|
|
3
|
+
let stabilityPromise = null;
|
|
4
|
+
let stabilityResolver = null;
|
|
5
|
+
let debounceTimer = null;
|
|
6
|
+
const DEBOUNCE_MS = 500;
|
|
7
|
+
function startWaitingForStability() {
|
|
8
|
+
if (!stabilityPromise) {
|
|
9
|
+
log("Starting to wait for server stability...");
|
|
10
|
+
stabilityPromise = new Promise((resolve) => {
|
|
11
|
+
stabilityResolver = resolve;
|
|
12
|
+
});
|
|
13
|
+
// Start the timer. If it fires, we're stable.
|
|
14
|
+
debounceTimer = setTimeout(finishWaiting, DEBOUNCE_MS);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function activityDetected() {
|
|
18
|
+
if (stabilityPromise) {
|
|
19
|
+
// If we're waiting for stability, reset the timer.
|
|
20
|
+
log("Activity detected, resetting stability timer.");
|
|
21
|
+
if (debounceTimer)
|
|
22
|
+
clearTimeout(debounceTimer);
|
|
23
|
+
debounceTimer = setTimeout(finishWaiting, DEBOUNCE_MS);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function finishWaiting() {
|
|
27
|
+
if (stabilityResolver) {
|
|
28
|
+
log("Server appears stable. Resolving promise.");
|
|
29
|
+
stabilityResolver();
|
|
30
|
+
}
|
|
31
|
+
stabilityPromise = null;
|
|
32
|
+
stabilityResolver = null;
|
|
33
|
+
debounceTimer = null;
|
|
34
|
+
}
|
|
35
|
+
export function staleDepRetryPlugin() {
|
|
36
|
+
return {
|
|
37
|
+
name: "rws-vite-plugin:stale-dep-retry",
|
|
38
|
+
apply: "serve",
|
|
39
|
+
// Monitor server activity by tapping into the transform hook. This is a
|
|
40
|
+
// reliable indicator that Vite is busy processing modules.
|
|
41
|
+
transform() {
|
|
42
|
+
activityDetected();
|
|
43
|
+
return null;
|
|
44
|
+
},
|
|
45
|
+
configureServer(server) {
|
|
46
|
+
// Return a function to ensure our middleware is placed after internal middlewares
|
|
47
|
+
return () => {
|
|
48
|
+
server.middlewares.use(async function rwsdkStaleBundleErrorHandler(err, req, res, next) {
|
|
49
|
+
if (err &&
|
|
50
|
+
typeof err.message === "string" &&
|
|
51
|
+
err.message.includes("new version of the pre-bundle")) {
|
|
52
|
+
log("Caught stale pre-bundle error. Waiting for server to stabilize...");
|
|
53
|
+
startWaitingForStability();
|
|
54
|
+
await stabilityPromise;
|
|
55
|
+
log("Server stabilized. Sending full-reload and redirecting.");
|
|
56
|
+
// Signal the client to do a full page reload.
|
|
57
|
+
server.environments.client.hot.send({
|
|
58
|
+
type: "full-reload",
|
|
59
|
+
});
|
|
60
|
+
res.writeHead(307, { Location: req.originalUrl || req.url });
|
|
61
|
+
res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
next(err);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { RW_STATE_EXPORT_PATH } from "../lib/constants.mjs";
|
|
4
|
+
import { maybeResolveEnvImport } from "./envResolvers.mjs";
|
|
5
|
+
const log = debug("rwsdk:vite:state-plugin");
|
|
6
|
+
const VIRTUAL_STATE_PREFIX = "virtual:rwsdk:state:";
|
|
7
|
+
export const statePlugin = ({ projectRootDir, }) => {
|
|
8
|
+
let isDev = false;
|
|
9
|
+
const stateModulePath = maybeResolveEnvImport({
|
|
10
|
+
id: RW_STATE_EXPORT_PATH,
|
|
11
|
+
envName: "worker",
|
|
12
|
+
projectRootDir,
|
|
13
|
+
});
|
|
14
|
+
if (!stateModulePath) {
|
|
15
|
+
throw new Error(`[rwsdk] State module path not found for project root: ${projectRootDir}. This is likely a bug in RedwoodSDK. Please report this issue at https://github.com/redwoodjs/sdk/issues/new`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
name: "rwsdk:state",
|
|
19
|
+
enforce: "pre",
|
|
20
|
+
config(_, { command, isPreview }) {
|
|
21
|
+
isDev = !isPreview && command === "serve";
|
|
22
|
+
},
|
|
23
|
+
configEnvironment(env, config) {
|
|
24
|
+
if (env === "worker") {
|
|
25
|
+
config.optimizeDeps ??= {};
|
|
26
|
+
config.optimizeDeps.esbuildOptions ??= {};
|
|
27
|
+
config.optimizeDeps.esbuildOptions.plugins ??= [];
|
|
28
|
+
config.optimizeDeps.esbuildOptions.plugins.push({
|
|
29
|
+
name: "rwsdk-state-external",
|
|
30
|
+
setup(build) {
|
|
31
|
+
build.onResolve({
|
|
32
|
+
// context(justinvdm, 13 Oct 2025): Vite dep optimizer slugifies the export path
|
|
33
|
+
filter: new RegExp(`^(${RW_STATE_EXPORT_PATH}|${VIRTUAL_STATE_PREFIX}.*)$`),
|
|
34
|
+
}, (args) => {
|
|
35
|
+
log("Marking as external: %s", args.path);
|
|
36
|
+
return {
|
|
37
|
+
path: args.path,
|
|
38
|
+
external: true,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
resolveId(id) {
|
|
46
|
+
if (id === RW_STATE_EXPORT_PATH) {
|
|
47
|
+
if (isDev && this.environment.name === "worker") {
|
|
48
|
+
return `${VIRTUAL_STATE_PREFIX}${id}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return stateModulePath;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async load(id) {
|
|
56
|
+
if (id.startsWith(VIRTUAL_STATE_PREFIX)) {
|
|
57
|
+
log("Loading virtual state module from %s", stateModulePath);
|
|
58
|
+
return await fs.readFile(stateModulePath, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rwsdk",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.16",
|
|
4
4
|
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/runtime/entries/client.d.ts",
|
|
37
37
|
"default": "./dist/runtime/entries/client.js"
|
|
38
38
|
},
|
|
39
|
+
"./__state": {
|
|
40
|
+
"types": "./dist/runtime/state.d.ts",
|
|
41
|
+
"default": "./dist/runtime/state.js"
|
|
42
|
+
},
|
|
39
43
|
"./__ssr": {
|
|
40
44
|
"react-server": "./dist/runtime/entries/no-react-server.js",
|
|
41
45
|
"types": "./dist/runtime/entries/ssr.d.ts",
|