rwsdk 1.0.0-beta.4 → 1.0.0-beta.41

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.
Files changed (139) hide show
  1. package/dist/lib/constants.d.mts +1 -0
  2. package/dist/lib/constants.mjs +7 -4
  3. package/dist/lib/e2e/browser.mjs +6 -2
  4. package/dist/lib/e2e/constants.d.mts +4 -0
  5. package/dist/lib/e2e/constants.mjs +49 -12
  6. package/dist/lib/e2e/dev.mjs +37 -49
  7. package/dist/lib/e2e/environment.d.mts +2 -0
  8. package/dist/lib/e2e/environment.mjs +201 -64
  9. package/dist/lib/e2e/index.d.mts +1 -0
  10. package/dist/lib/e2e/index.mjs +1 -0
  11. package/dist/lib/e2e/poll.d.mts +1 -1
  12. package/dist/lib/e2e/release.d.mts +1 -0
  13. package/dist/lib/e2e/release.mjs +16 -32
  14. package/dist/lib/e2e/tarball.mjs +2 -34
  15. package/dist/lib/e2e/testHarness.d.mts +34 -3
  16. package/dist/lib/e2e/testHarness.mjs +219 -90
  17. package/dist/lib/e2e/utils.d.mts +1 -0
  18. package/dist/lib/e2e/utils.mjs +15 -0
  19. package/dist/runtime/client/client.d.ts +35 -0
  20. package/dist/runtime/client/client.js +35 -0
  21. package/dist/runtime/client/navigation.d.ts +49 -0
  22. package/dist/runtime/client/navigation.js +80 -31
  23. package/dist/runtime/entries/clientSSR.d.ts +1 -0
  24. package/dist/runtime/entries/clientSSR.js +3 -0
  25. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  26. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  27. package/dist/runtime/entries/router.d.ts +1 -0
  28. package/dist/runtime/entries/routerClient.d.ts +1 -0
  29. package/dist/runtime/entries/routerClient.js +1 -0
  30. package/dist/runtime/entries/worker.d.ts +2 -0
  31. package/dist/runtime/entries/worker.js +2 -0
  32. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  33. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  34. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  35. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  36. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  37. package/dist/runtime/lib/db/createDb.js +4 -0
  38. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  39. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  40. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  41. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  42. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  43. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  44. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  45. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  46. package/dist/runtime/lib/links.d.ts +21 -7
  47. package/dist/runtime/lib/links.js +82 -24
  48. package/dist/runtime/lib/links.test.js +20 -0
  49. package/dist/runtime/lib/manifest.d.ts +1 -1
  50. package/dist/runtime/lib/manifest.js +7 -4
  51. package/dist/runtime/lib/realtime/client.js +8 -2
  52. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  53. package/dist/runtime/lib/router.d.ts +153 -36
  54. package/dist/runtime/lib/router.js +169 -20
  55. package/dist/runtime/lib/router.test.js +241 -0
  56. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  57. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  58. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  59. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  60. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  61. package/dist/runtime/lib/types.js +1 -0
  62. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  63. package/dist/runtime/render/renderToStream.d.ts +4 -2
  64. package/dist/runtime/render/renderToStream.js +53 -24
  65. package/dist/runtime/render/renderToString.d.ts +3 -6
  66. package/dist/runtime/requestInfo/types.d.ts +4 -1
  67. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  68. package/dist/runtime/requestInfo/utils.js +44 -0
  69. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  70. package/dist/runtime/requestInfo/worker.js +3 -10
  71. package/dist/runtime/script.d.ts +1 -3
  72. package/dist/runtime/script.js +1 -10
  73. package/dist/runtime/state.d.ts +3 -0
  74. package/dist/runtime/state.js +13 -0
  75. package/dist/runtime/worker.d.ts +3 -1
  76. package/dist/runtime/worker.js +32 -0
  77. package/dist/scripts/debug-sync.mjs +18 -20
  78. package/dist/scripts/worker-run.d.mts +1 -1
  79. package/dist/scripts/worker-run.mjs +59 -113
  80. package/dist/use-synced-state/SyncedStateServer.d.mts +21 -0
  81. package/dist/use-synced-state/SyncedStateServer.mjs +128 -0
  82. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  83. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  84. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  85. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  86. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  87. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  88. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  89. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  90. package/dist/use-synced-state/client-core.d.ts +26 -0
  91. package/dist/use-synced-state/client-core.js +39 -0
  92. package/dist/use-synced-state/client.d.ts +3 -0
  93. package/dist/use-synced-state/client.js +4 -0
  94. package/dist/use-synced-state/constants.d.mts +1 -0
  95. package/dist/use-synced-state/constants.mjs +1 -0
  96. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  97. package/dist/use-synced-state/useSyncedState.js +58 -0
  98. package/dist/use-synced-state/worker.d.mts +13 -0
  99. package/dist/use-synced-state/worker.mjs +69 -0
  100. package/dist/vite/buildApp.mjs +34 -2
  101. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  102. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  103. package/dist/vite/configPlugin.mjs +9 -14
  104. package/dist/vite/constants.d.mts +1 -0
  105. package/dist/vite/constants.mjs +1 -0
  106. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  107. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  108. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  109. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  110. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  111. package/dist/vite/directivesPlugin.mjs +4 -4
  112. package/dist/vite/envResolvers.d.mts +11 -0
  113. package/dist/vite/envResolvers.mjs +20 -0
  114. package/dist/vite/getViteEsbuild.mjs +2 -1
  115. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  116. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  117. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  118. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  119. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  120. package/dist/vite/linkerPlugin.d.mts +2 -1
  121. package/dist/vite/linkerPlugin.mjs +11 -3
  122. package/dist/vite/linkerPlugin.test.mjs +15 -0
  123. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  124. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  125. package/dist/vite/redwoodPlugin.mjs +8 -10
  126. package/dist/vite/runDirectivesScan.mjs +72 -18
  127. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  128. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  129. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  130. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  131. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  132. package/dist/vite/statePlugin.d.mts +4 -0
  133. package/dist/vite/statePlugin.mjs +62 -0
  134. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  135. package/dist/vite/virtualPlugin.mjs +6 -7
  136. package/package.json +27 -10
  137. package/dist/vite/manifestPlugin.d.mts +0 -4
  138. package/dist/vite/manifestPlugin.mjs +0 -63
  139. /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
@@ -10,6 +10,7 @@ import { pathExists } from "fs-extra";
10
10
  import { $ } from "../lib/$.mjs";
11
11
  import { findWranglerConfig } from "../lib/findWranglerConfig.mjs";
12
12
  import { hasPkgScript } from "../lib/hasPkgScript.mjs";
13
+ import { cloudflarePreInitPlugin } from "./cloudflarePreInitPlugin.mjs";
13
14
  import { configPlugin } from "./configPlugin.mjs";
14
15
  import { devServerTimingPlugin } from "./devServerTimingPlugin.mjs";
15
16
  import { directiveModulesDevPlugin } from "./directiveModulesDevPlugin.mjs";
@@ -18,12 +19,13 @@ import { directivesPlugin } from "./directivesPlugin.mjs";
18
19
  import { injectVitePreamble } from "./injectVitePreamblePlugin.mjs";
19
20
  import { knownDepsResolverPlugin } from "./knownDepsResolverPlugin.mjs";
20
21
  import { linkerPlugin } from "./linkerPlugin.mjs";
21
- import { manifestPlugin } from "./manifestPlugin.mjs";
22
22
  import { miniflareHMRPlugin } from "./miniflareHMRPlugin.mjs";
23
23
  import { moveStaticAssetsPlugin } from "./moveStaticAssetsPlugin.mjs";
24
24
  import { prismaPlugin } from "./prismaPlugin.mjs";
25
25
  import { resolveForcedPaths } from "./resolveForcedPaths.mjs";
26
26
  import { ssrBridgePlugin } from "./ssrBridgePlugin.mjs";
27
+ import { staleDepRetryPlugin } from "./staleDepRetryPlugin.mjs";
28
+ import { statePlugin } from "./statePlugin.mjs";
27
29
  import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
28
30
  import { useClientLookupPlugin } from "./useClientLookupPlugin.mjs";
29
31
  import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
@@ -74,17 +76,15 @@ export const redwoodPlugin = async (options = {}) => {
74
76
  // context(justinvdm, 31 Mar 2025): We assume that if there is no .wrangler directory,
75
77
  // then this is fresh install, and we run `npm run dev:init` here.
76
78
  if (process.env.RWSDK_WORKER_RUN !== "1" &&
77
- process.env.RWSDK_DEPLOY !== "1" &&
79
+ process.env.NODE_ENV !== "production" &&
78
80
  !(await pathExists(resolve(projectRootDir, ".wrangler"))) &&
79
81
  (await hasPkgScript(projectRootDir, "dev:init"))) {
80
82
  console.log("🚀 Project has no .wrangler directory yet, assuming fresh install: running `npm run dev:init`...");
81
- await $({
82
- // context(justinvdm, 01 Apr 2025): We want to avoid interactive migration y/n prompt, so we ignore stdin
83
- // as a signal to operate in no-tty mode
84
- stdio: ["ignore", "inherit", "inherit"],
85
- }) `npm run dev:init`;
83
+ await $ `npm run dev:init`;
86
84
  }
87
85
  return [
86
+ staleDepRetryPlugin(),
87
+ statePlugin({ projectRootDir }),
88
88
  devServerTimingPlugin(),
89
89
  devServerConstantPlugin(),
90
90
  directiveModulesDevPlugin({
@@ -107,6 +107,7 @@ export const redwoodPlugin = async (options = {}) => {
107
107
  projectRootDir,
108
108
  }),
109
109
  knownDepsResolverPlugin({ projectRootDir }),
110
+ cloudflarePreInitPlugin(),
110
111
  tsconfigPaths({ root: projectRootDir }),
111
112
  shouldIncludeCloudflarePlugin
112
113
  ? cloudflare({
@@ -144,9 +145,6 @@ export const redwoodPlugin = async (options = {}) => {
144
145
  clientEntryPoints,
145
146
  projectRootDir,
146
147
  }),
147
- manifestPlugin({
148
- projectRootDir,
149
- }),
150
148
  moveStaticAssetsPlugin({ rootDir: projectRootDir }),
151
149
  prismaPlugin({ projectRootDir }),
152
150
  linkerPlugin({ projectRootDir }),
@@ -1,6 +1,7 @@
1
1
  // @ts-ignore
2
2
  import { compile } from "@mdx-js/mdx";
3
3
  import debug from "debug";
4
+ import { glob } from "glob";
4
5
  import fsp from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
@@ -17,6 +18,39 @@ const isObject = (value) => Object.prototype.toString.call(value) === "[object O
17
18
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts
18
19
  const externalRE = /^(https?:)?\/\//;
19
20
  const isExternalUrl = (url) => externalRE.test(url);
21
+ async function findDirectiveRoots({ root, readFileWithCache, directiveCheckCache, }) {
22
+ const srcDir = path.resolve(root, "src");
23
+ const files = await glob("**/*.{ts,tsx,js,jsx,mjs,mts,cjs,cts,mdx}", {
24
+ cwd: srcDir,
25
+ absolute: true,
26
+ });
27
+ const directiveFiles = new Set();
28
+ for (const file of files) {
29
+ if (directiveCheckCache.has(file)) {
30
+ if (directiveCheckCache.get(file)) {
31
+ directiveFiles.add(file);
32
+ }
33
+ continue;
34
+ }
35
+ try {
36
+ const content = await readFileWithCache(file);
37
+ const hasClient = hasDirective(content, "use client");
38
+ const hasServer = hasDirective(content, "use server");
39
+ const hasAnyDirective = hasClient || hasServer;
40
+ directiveCheckCache.set(file, hasAnyDirective);
41
+ if (hasAnyDirective) {
42
+ directiveFiles.add(file);
43
+ }
44
+ }
45
+ catch (e) {
46
+ log("Could not read file during pre-scan, skipping:", file);
47
+ // Cache the failure to avoid re-reading a problematic file
48
+ directiveCheckCache.set(file, false);
49
+ }
50
+ }
51
+ log("Pre-scan found directive files:", Array.from(directiveFiles));
52
+ return directiveFiles;
53
+ }
20
54
  export async function resolveModuleWithEnvironment({ path, importer, importerEnv, clientResolver, workerResolver, }) {
21
55
  const resolver = importerEnv === "client" ? clientResolver : workerResolver;
22
56
  return new Promise((resolvePromise) => {
@@ -53,9 +87,17 @@ export function classifyModule({ contents, inheritedEnv, }) {
53
87
  }
54
88
  export const runDirectivesScan = async ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }) => {
55
89
  deferredLog("\n… (rwsdk) Scanning for 'use client' and 'use server' directives...");
56
- // Set environment variable to indicate scanning is in progress
57
- process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE = "true";
58
90
  try {
91
+ const fileContentCache = new Map();
92
+ const directiveCheckCache = new Map();
93
+ const readFileWithCache = async (path) => {
94
+ if (fileContentCache.has(path)) {
95
+ return fileContentCache.get(path);
96
+ }
97
+ const contents = await fsp.readFile(path, "utf-8");
98
+ fileContentCache.set(path, contents);
99
+ return contents;
100
+ };
59
101
  const esbuild = await getViteEsbuild(rootConfig.root);
60
102
  const input = initialEntries ?? environments.worker.config.build.rollupOptions?.input;
61
103
  let entries;
@@ -78,19 +120,19 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
78
120
  // Filter out virtual modules since they can't be scanned by esbuild
79
121
  const realEntries = entries.filter((entry) => !entry.includes("virtual:"));
80
122
  const absoluteEntries = realEntries.map((entry) => path.resolve(rootConfig.root, entry));
81
- log("Starting directives scan for worker environment with entries:", absoluteEntries);
123
+ const applicationDirectiveFiles = await findDirectiveRoots({
124
+ root: rootConfig.root,
125
+ readFileWithCache,
126
+ directiveCheckCache,
127
+ });
128
+ const combinedEntries = new Set([
129
+ ...absoluteEntries,
130
+ ...applicationDirectiveFiles,
131
+ ]);
132
+ log("Starting directives scan with combined entries:", Array.from(combinedEntries));
82
133
  const workerResolver = createViteAwareResolver(rootConfig, environments.worker);
83
134
  const clientResolver = createViteAwareResolver(rootConfig, environments.client);
84
135
  const moduleEnvironments = new Map();
85
- const fileContentCache = new Map();
86
- const readFileWithCache = async (path) => {
87
- if (fileContentCache.has(path)) {
88
- return fileContentCache.get(path);
89
- }
90
- const contents = await fsp.readFile(path, "utf-8");
91
- fileContentCache.set(path, contents);
92
- return contents;
93
- };
94
136
  const esbuildScanPlugin = {
95
137
  name: "rwsdk:esbuild-scan-plugin",
96
138
  setup(build) {
@@ -153,6 +195,19 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
153
195
  log("Resolution result:", resolved);
154
196
  const resolvedPath = resolved?.id;
155
197
  if (resolvedPath && path.isAbsolute(resolvedPath)) {
198
+ try {
199
+ const stats = await fsp.stat(resolvedPath);
200
+ if (stats.isDirectory()) {
201
+ log("Resolved path is a directory, marking as external to avoid scan error:", resolvedPath);
202
+ return { external: true };
203
+ }
204
+ }
205
+ catch (e) {
206
+ // This can happen for virtual modules or special paths that don't
207
+ // exist on the filesystem. We can safely externalize them.
208
+ log("Could not stat resolved path, marking as external:", resolvedPath);
209
+ return { external: true };
210
+ }
156
211
  // Normalize the path for esbuild compatibility
157
212
  const normalizedPath = normalizeModulePath(resolvedPath, rootConfig.root, { absolute: true });
158
213
  log("Normalized path:", normalizedPath);
@@ -166,11 +221,11 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
166
221
  });
167
222
  build.onLoad({ filter: /\.(m|c)?[jt]sx?$|\.mdx$/ }, async (args) => {
168
223
  log("onLoad called for:", args.path);
169
- if (!args.path.startsWith("/") ||
224
+ if (!path.isAbsolute(args.path) ||
170
225
  args.path.includes("virtual:") ||
171
226
  isExternalUrl(args.path)) {
172
227
  log("Skipping file due to filter:", args.path, {
173
- startsWithSlash: args.path.startsWith("/"),
228
+ isAbsolute: path.isAbsolute(args.path),
174
229
  hasVirtual: args.path.includes("virtual:"),
175
230
  isExternal: isExternalUrl(args.path),
176
231
  });
@@ -232,7 +287,7 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
232
287
  },
233
288
  };
234
289
  await esbuild.build({
235
- entryPoints: absoluteEntries,
290
+ entryPoints: Array.from(combinedEntries),
236
291
  bundle: true,
237
292
  write: false,
238
293
  outdir: path.join(INTERMEDIATES_OUTPUT_DIR, "directive-scan"),
@@ -246,15 +301,14 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
246
301
  throw new Error(`RWSDK directive scan failed:\n${e.stack}`);
247
302
  }
248
303
  finally {
249
- // Always clear the scanning flag when done
250
- delete process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE;
251
304
  deferredLog("✔ (rwsdk) Done scanning for 'use client' and 'use server' directives.");
252
305
  process.env.VERBOSE &&
253
306
  log("Client/server files after scanning: client=%O, server=%O", Array.from(clientFiles), Array.from(serverFiles));
254
307
  }
255
308
  };
256
309
  const deferredLog = (message) => {
310
+ const doLog = process.env.RWSDK_WORKER_RUN ? log : console.log;
257
311
  setTimeout(() => {
258
- console.log(message);
312
+ doLog(message);
259
313
  }, 500);
260
314
  };
@@ -1,6 +1,7 @@
1
1
  import debug from "debug";
2
2
  import MagicString from "magic-string";
3
3
  import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
4
+ import { externalModulesSet } from "./constants.mjs";
4
5
  import { findSsrImportCallSites } from "./findSsrSpecifiers.mjs";
5
6
  const log = debug("rwsdk:vite:ssr-bridge-plugin");
6
7
  export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
@@ -10,9 +11,36 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
10
11
  const ssrBridgePlugin = {
11
12
  name: "rwsdk:ssr-bridge",
12
13
  enforce: "pre",
13
- async configureServer(server) {
14
+ configureServer(server) {
15
+ // context(justinvdm, 19 Nov 2025): This plugin patches the dev server's
16
+ // HMR and optimizer behavior to coordinate the `ssr` and `worker`
17
+ // environments. It runs with `enforce: 'pre'` to ensure these patches
18
+ // are in place before other plugins start interacting with the server.
14
19
  devServer = server;
20
+ const ssrHot = server.environments.ssr.hot;
21
+ const originalSsrHotSend = ssrHot.send;
22
+ // Chain the SSR's full reload behaviour to the worker
23
+ ssrHot.send = (...args) => {
24
+ if (typeof args[0] === "object" && args[0].type === "full-reload") {
25
+ for (const envName of ["worker", "ssr"]) {
26
+ const moduleGraph = server.environments[envName].moduleGraph;
27
+ moduleGraph.invalidateAll();
28
+ }
29
+ log("SSR full-reload detected, propagating to worker");
30
+ // context(justinvdm, 21 Oct 2025): By sending the full-reload event
31
+ // to the worker, we ensure that the worker's module runner cache is
32
+ // invalidated, as it would have been if this were a full-reload event
33
+ // from the worker.
34
+ server.environments.worker.hot.send.apply(server.environments.worker.hot, args);
35
+ }
36
+ return originalSsrHotSend.apply(ssrHot, args);
37
+ };
15
38
  log("Configured dev server");
39
+ const originalRun = devServer.environments.ssr.depsOptimizer?.run;
40
+ devServer.environments.ssr.depsOptimizer.run = async () => {
41
+ originalRun();
42
+ devServer.environments.worker.depsOptimizer.run();
43
+ };
16
44
  },
17
45
  config(_, { command, isPreview }) {
18
46
  isDev = !isPreview && command === "serve";
@@ -48,11 +76,24 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
48
76
  log("Worker environment esbuild configuration complete");
49
77
  }
50
78
  },
51
- async resolveId(id) {
52
- // Skip during directive scanning to avoid performance issues
53
- if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
79
+ async resolveId(id, importer, options) {
80
+ // Skip during our directive scanning to avoid performance issues
81
+ // context(justinvdm, 20 Jan 2025): We check options.custom?.rwsdk?.directiveScan to distinguish
82
+ // between our directive scan (which should skip) and external calls like Cloudflare's early
83
+ // dispatch (which should be handled normally). This prevents race conditions where external
84
+ // calls happen during directive scanning.
85
+ if (options?.custom?.rwsdk?.directiveScan === true) {
54
86
  return;
55
87
  }
88
+ // context(justinvdm, 19 Nov 2025):
89
+ // Ensure platform-specific modules are always treated as external in the
90
+ // SSR environment. This is critical for builds, where we produce a
91
+ // standalone SSR bundle. Without this, Vite might try to bundle these
92
+ // virtual modules or fail to resolve them.
93
+ if (this.environment.name === "ssr" && externalModulesSet.has(id)) {
94
+ log("SSR environment: marking %s as external", id);
95
+ return { id, external: true };
96
+ }
56
97
  if (isDev) {
57
98
  // context(justinvdm, 27 May 2025): In dev, we need to dynamically load
58
99
  // SSR modules, so we return the virtual id so that the dynamic loading
@@ -100,53 +141,104 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
100
141
  }
101
142
  },
102
143
  async load(id) {
103
- // Skip during directive scanning to avoid performance issues
104
- if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
105
- return;
106
- }
107
144
  if (id.startsWith(VIRTUAL_SSR_PREFIX) &&
108
145
  this.environment.name === "worker") {
109
146
  const realId = id.slice(VIRTUAL_SSR_PREFIX.length);
110
- const idForFetch = realId.endsWith(".css.js")
147
+ let idForFetch = realId.endsWith(".css.js")
111
148
  ? realId.slice(0, -3)
112
149
  : realId;
113
150
  log("Virtual SSR module load: id=%s, realId=%s, idForFetch=%s", id, realId, idForFetch);
114
151
  if (isDev) {
115
- log("Dev mode: fetching SSR module for realPath=%s", idForFetch);
116
- const result = await devServer?.environments.ssr.fetchModule(idForFetch);
117
- process.env.VERBOSE &&
118
- log("Fetch module result: id=%s, result=%O", idForFetch, result);
119
- const code = "code" in result ? result.code : undefined;
120
- if (idForFetch.endsWith(".css") &&
121
- !idForFetch.endsWith(".module.css")) {
122
- process.env.VERBOSE &&
123
- log("Plain CSS file, returning empty module for %s", idForFetch);
124
- return "export default {};";
152
+ // from the SSR environment, which is crucial for things like server
153
+ // components.
154
+ try {
155
+ const ssrOptimizer = devServer.environments.ssr.depsOptimizer;
156
+ // context(justinvdm, 20 Oct 2025): This is the fix for the stale
157
+ // dependency issue. The root cause is the "unhashed-to-hashed"
158
+ // transition. Our worker code imports a clean ID
159
+ // (`rwsdk/__ssr_bridge`), but we expect to fetch the hashed,
160
+ // optimized version from the SSR environment. When a re-optimization
161
+ // happens, Vite's `fetchModule` (running in the SSR env) finds a
162
+ // "ghost node" in its module graph for the clean ID and incorrectly
163
+ // re-uses its stale, hashed `id` property.
164
+ //
165
+ // To fix this, we manually resolve the hashed path here, before
166
+ // asking the SSR env to process the module. We look into the SSR
167
+ // optimizer's metadata to find the correct, up-to-date hash and
168
+ // construct the path ourselves. This ensures the SSR env is
169
+ // always working with the correct, versioned ID, bypassing the
170
+ // faulty ghost node lookup.
171
+ if (ssrOptimizer &&
172
+ Object.prototype.hasOwnProperty.call(ssrOptimizer.metadata.optimized, realId)) {
173
+ const depInfo = ssrOptimizer.metadata.optimized[realId];
174
+ idForFetch = ssrOptimizer.getOptimizedDepId(depInfo);
175
+ log("Manually resolved %s to hashed path for fetchModule: %s", realId, idForFetch);
176
+ }
177
+ log("Virtual SSR module load: id=%s, realId=%s, idForFetch=%s", id, realId, idForFetch);
178
+ log("Dev mode: fetching SSR module for realPath=%s", idForFetch);
179
+ // We use `fetchModule` with `cached: false` as a safeguard. Since
180
+ // we're in a `load` hook, we know the worker-side cache for this
181
+ // virtual module is stale. `cached: false` ensures that we also
182
+ // bypass any potentially stale transform result in the SSR
183
+ // environment's cache, guaranteeing we get the freshest possible
184
+ // code.
185
+ const result = await devServer.environments.ssr.fetchModule(idForFetch, undefined, { cached: false });
186
+ if ("code" in result) {
187
+ log("Fetched SSR module code length: %d", result.code?.length || 0);
188
+ const code = result.code;
189
+ if (idForFetch.endsWith(".css") &&
190
+ !idForFetch.endsWith(".module.css")) {
191
+ process.env.VERBOSE &&
192
+ log("Plain CSS file, returning empty module for %s", idForFetch);
193
+ return "export default {};";
194
+ }
195
+ const s = new MagicString(code || "");
196
+ const callsites = findSsrImportCallSites(idForFetch, code || "", log);
197
+ for (const site of callsites) {
198
+ const normalized = site.specifier.startsWith("/@id/")
199
+ ? site.specifier.slice("/@id/".length)
200
+ : site.specifier;
201
+ // If the import is for a known external module, we must leave it
202
+ // as a bare specifier. Rewriting it with any prefix (`/@id/` or
203
+ // our virtual one) will break Vite's default externalization.
204
+ if (externalModulesSet.has(normalized)) {
205
+ const replacement = `import("${normalized}")`;
206
+ s.overwrite(site.start, site.end, replacement);
207
+ continue;
208
+ }
209
+ // context(justinvdm, 11 Aug 2025):
210
+ // - We replace __vite_ssr_import__ and __vite_ssr_dynamic_import__
211
+ // with import() calls so that the module graph can be built
212
+ // correctly (vite looks for imports and import()s to build module
213
+ // graph)
214
+ // - We prepend /@id/$VIRTUAL_SSR_PREFIX to the specifier so that we
215
+ // can stay within the SSR subgraph of the worker module graph
216
+ const replacement = `import("/@id/${VIRTUAL_SSR_PREFIX}${normalized}")`;
217
+ s.overwrite(site.start, site.end, replacement);
218
+ }
219
+ const out = s.toString();
220
+ process.env.VERBOSE &&
221
+ log("Transformed SSR module code for realId=%s: %s", realId, out);
222
+ return {
223
+ code: out,
224
+ map: null, // Sourcemaps are handled by fetchModule's inlining
225
+ };
226
+ }
227
+ else {
228
+ // This case can be hit if the module is already cached. We may
229
+ // need to handle this more gracefully, but for now we'll just
230
+ // return an empty module.
231
+ log("SSR module %s was already cached. Returning empty.", idForFetch);
232
+ return "export default {}";
233
+ }
125
234
  }
126
- log("Fetched SSR module code length: %d", code?.length || 0);
127
- const s = new MagicString(code || "");
128
- const callsites = findSsrImportCallSites(idForFetch, code || "", log);
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);
235
+ catch (e) {
236
+ log("Error fetching SSR module for realPath=%s: %s", id, e);
237
+ throw e;
142
238
  }
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
239
  }
149
240
  }
241
+ return;
150
242
  },
151
243
  };
152
244
  return ssrBridgePlugin;
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "vite";
2
+ export declare const ssrBridgeWrapPlugin: () => Plugin;
@@ -0,0 +1,85 @@
1
+ import { Lang, parse as sgParse } from "@ast-grep/napi";
2
+ import debug from "debug";
3
+ import MagicString from "magic-string";
4
+ const log = debug("rwsdk:vite:ssr-bridge-wrap");
5
+ export const ssrBridgeWrapPlugin = () => {
6
+ return {
7
+ name: "rwsdk:ssr-bridge-wrap",
8
+ apply: "build",
9
+ renderChunk(code, chunk) {
10
+ try {
11
+ if (!chunk.fileName.endsWith("ssr_bridge.js")) {
12
+ return null;
13
+ }
14
+ const s = new MagicString(code);
15
+ // Use AST parsing to find actual import statements (not in comments)
16
+ const root = sgParse(Lang.JavaScript, code);
17
+ // Find all import statements using AST patterns
18
+ const importPatterns = [
19
+ 'import { $$$ } from "$MODULE"',
20
+ "import { $$$ } from '$MODULE'",
21
+ 'import $DEFAULT from "$MODULE"',
22
+ "import $DEFAULT from '$MODULE'",
23
+ 'import * as $NS from "$MODULE"',
24
+ "import * as $NS from '$MODULE'",
25
+ 'import "$MODULE"',
26
+ "import '$MODULE'",
27
+ ];
28
+ let lastImportEnd = -1;
29
+ for (const pattern of importPatterns) {
30
+ const matches = root.root().findAll(pattern);
31
+ for (const match of matches) {
32
+ const range = match.range();
33
+ if (range.end.index > lastImportEnd) {
34
+ lastImportEnd = range.end.index;
35
+ }
36
+ }
37
+ }
38
+ // Find the export statement using AST
39
+ const exportPatterns = [
40
+ "export { $$$ }",
41
+ 'export { $$$ } from "$MODULE"',
42
+ "export { $$$ } from '$MODULE'",
43
+ ];
44
+ let exportStart = -1;
45
+ let exportEnd = -1;
46
+ for (const pattern of exportPatterns) {
47
+ const matches = root.root().findAll(pattern);
48
+ for (const match of matches) {
49
+ const range = match.range();
50
+ // Check if this export contains our target symbols
51
+ const text = match.text();
52
+ if (text.includes("renderHtmlStream") &&
53
+ text.includes("ssrLoadModule") &&
54
+ text.includes("ssrWebpackRequire")) {
55
+ exportStart = range.start.index;
56
+ exportEnd = range.end.index;
57
+ break;
58
+ }
59
+ }
60
+ if (exportStart !== -1)
61
+ break;
62
+ }
63
+ const banner = `export const { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream } = (function() {`;
64
+ const footer = `return { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream };\n})();`;
65
+ // Insert banner after the last import (or at the beginning if no imports)
66
+ const insertIndex = lastImportEnd === -1 ? 0 : lastImportEnd;
67
+ s.appendLeft(insertIndex, banner + "\n");
68
+ // Append footer at the end
69
+ s.append(footer);
70
+ // Remove the original export statement if found
71
+ if (exportStart !== -1 && exportEnd !== -1) {
72
+ s.remove(exportStart, exportEnd);
73
+ }
74
+ return {
75
+ code: s.toString(),
76
+ map: s.generateMap(),
77
+ };
78
+ }
79
+ catch (e) {
80
+ console.error("Error in ssrBridgeWrapPlugin:", e);
81
+ throw e;
82
+ }
83
+ },
84
+ };
85
+ };
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "vite";
2
+ export declare function staleDepRetryPlugin(): Plugin;
@@ -0,0 +1,74 @@
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
+ // context(justinvdm, 19 Nov 2025): This hook adds an error handling
47
+ // middleware for stale dependency errors. It runs in a returned function,
48
+ // which intentionally places it late in the middleware stack. Unlike
49
+ // other plugins that must run before the Cloudflare plugin to prevent
50
+ // startup deadlocks, its timing is not critical, so `enforce: 'pre'`
51
+ // is not needed.
52
+ return () => {
53
+ server.middlewares.use(async function rwsdkStaleBundleErrorHandler(err, req, res, next) {
54
+ if (err &&
55
+ typeof err.message === "string" &&
56
+ err.message.includes("new version of the pre-bundle")) {
57
+ log("Caught stale pre-bundle error. Waiting for server to stabilize...");
58
+ startWaitingForStability();
59
+ await stabilityPromise;
60
+ log("Server stabilized. Sending full-reload and redirecting.");
61
+ // Signal the client to do a full page reload.
62
+ server.environments.client.hot.send({
63
+ type: "full-reload",
64
+ });
65
+ res.writeHead(307, { Location: req.originalUrl || req.url });
66
+ res.end();
67
+ return;
68
+ }
69
+ next(err);
70
+ });
71
+ };
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "vite";
2
+ export declare const statePlugin: ({ projectRootDir, }: {
3
+ projectRootDir: string;
4
+ }) => Plugin;