rwsdk 1.0.0-beta.14 → 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/lib/router.d.ts +15 -2
- package/dist/runtime/lib/router.js +67 -1
- package/dist/runtime/lib/router.test.js +239 -0
- 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
|
@@ -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",
|