rwsdk 1.0.0-beta.3 → 1.0.0-beta.30-test.20251119220440

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 (130) 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 +16 -0
  5. package/dist/lib/e2e/constants.mjs +77 -0
  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 +202 -65
  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 +36 -4
  16. package/dist/lib/e2e/testHarness.mjs +216 -128
  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/router.d.ts +1 -0
  26. package/dist/runtime/entries/routerClient.d.ts +1 -0
  27. package/dist/runtime/entries/routerClient.js +1 -0
  28. package/dist/runtime/entries/worker.d.ts +2 -0
  29. package/dist/runtime/entries/worker.js +2 -0
  30. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  31. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  32. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  33. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  34. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  35. package/dist/runtime/lib/db/createDb.js +4 -0
  36. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  37. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  38. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  39. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  40. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  41. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  42. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  43. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  44. package/dist/runtime/lib/links.d.ts +18 -7
  45. package/dist/runtime/lib/links.js +70 -24
  46. package/dist/runtime/lib/links.test.js +20 -0
  47. package/dist/runtime/lib/manifest.d.ts +1 -1
  48. package/dist/runtime/lib/manifest.js +7 -4
  49. package/dist/runtime/lib/realtime/client.js +8 -2
  50. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  51. package/dist/runtime/lib/router.d.ts +147 -33
  52. package/dist/runtime/lib/router.js +169 -20
  53. package/dist/runtime/lib/router.test.js +241 -0
  54. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  55. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  56. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  57. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  58. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  59. package/dist/runtime/lib/types.js +1 -0
  60. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  61. package/dist/runtime/render/renderToStream.d.ts +4 -2
  62. package/dist/runtime/render/renderToStream.js +53 -24
  63. package/dist/runtime/render/renderToString.d.ts +3 -1
  64. package/dist/runtime/requestInfo/types.d.ts +4 -1
  65. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  66. package/dist/runtime/requestInfo/utils.js +44 -0
  67. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  68. package/dist/runtime/requestInfo/worker.js +3 -10
  69. package/dist/runtime/script.d.ts +1 -3
  70. package/dist/runtime/script.js +1 -10
  71. package/dist/runtime/state.d.ts +3 -0
  72. package/dist/runtime/state.js +13 -0
  73. package/dist/runtime/worker.d.ts +3 -1
  74. package/dist/runtime/worker.js +26 -0
  75. package/dist/scripts/debug-sync.mjs +18 -20
  76. package/dist/scripts/worker-run.d.mts +1 -1
  77. package/dist/scripts/worker-run.mjs +52 -113
  78. package/dist/use-synced-state/SyncStateServer.d.mts +20 -0
  79. package/dist/use-synced-state/SyncStateServer.mjs +124 -0
  80. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  81. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  82. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  83. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  84. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  85. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  86. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  87. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  88. package/dist/use-synced-state/client.d.ts +28 -0
  89. package/dist/use-synced-state/client.js +39 -0
  90. package/dist/use-synced-state/constants.d.mts +1 -0
  91. package/dist/use-synced-state/constants.mjs +1 -0
  92. package/dist/use-synced-state/useSyncState.d.ts +20 -0
  93. package/dist/use-synced-state/useSyncState.js +58 -0
  94. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  95. package/dist/use-synced-state/useSyncedState.js +58 -0
  96. package/dist/use-synced-state/worker.d.mts +14 -0
  97. package/dist/use-synced-state/worker.mjs +73 -0
  98. package/dist/vite/buildApp.mjs +34 -2
  99. package/dist/vite/configPlugin.mjs +8 -14
  100. package/dist/vite/constants.d.mts +1 -0
  101. package/dist/vite/constants.mjs +1 -0
  102. package/dist/vite/createDirectiveLookupPlugin.mjs +4 -0
  103. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  104. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  105. package/dist/vite/directivesPlugin.mjs +4 -0
  106. package/dist/vite/envResolvers.d.mts +11 -0
  107. package/dist/vite/envResolvers.mjs +20 -0
  108. package/dist/vite/getViteEsbuild.mjs +2 -1
  109. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  110. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  111. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  112. package/dist/vite/knownDepsResolverPlugin.mjs +32 -14
  113. package/dist/vite/linkerPlugin.d.mts +2 -1
  114. package/dist/vite/linkerPlugin.mjs +11 -3
  115. package/dist/vite/linkerPlugin.test.mjs +15 -0
  116. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  117. package/dist/vite/moveStaticAssetsPlugin.mjs +14 -4
  118. package/dist/vite/redwoodPlugin.mjs +6 -10
  119. package/dist/vite/runDirectivesScan.mjs +59 -14
  120. package/dist/vite/ssrBridgePlugin.mjs +126 -34
  121. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  122. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  123. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  124. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  125. package/dist/vite/statePlugin.d.mts +4 -0
  126. package/dist/vite/statePlugin.mjs +62 -0
  127. package/package.json +26 -10
  128. package/dist/vite/manifestPlugin.d.mts +0 -4
  129. package/dist/vite/manifestPlugin.mjs +0 -63
  130. /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
@@ -1,5 +1,8 @@
1
1
  import debug from "debug";
2
- import { resolve } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
3
6
  import { runDirectivesScan } from "./runDirectivesScan.mjs";
4
7
  const log = debug("rwsdk:vite:build-app");
5
8
  /**
@@ -10,7 +13,36 @@ const log = debug("rwsdk:vite:build-app");
10
13
  * @see docs/architecture/productionBuildProcess.md
11
14
  */
12
15
  export async function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, workerEntryPathname, }) {
16
+ await rm(resolve(projectRootDir, "dist"), { recursive: true, force: true });
13
17
  const workerEnv = builder.environments.worker;
18
+ // Run a pre-scan build pass to allow plugins to set up and generate code
19
+ // before scanning.
20
+ console.log("Running plugin setup pass...");
21
+ process.env.RWSDK_BUILD_PASS = "plugin-setup";
22
+ const tempEntryPath = resolve(INTERMEDIATES_OUTPUT_DIR, "temp-entry.js");
23
+ try {
24
+ if (!existsSync(dirname(tempEntryPath))) {
25
+ await mkdir(dirname(tempEntryPath), { recursive: true });
26
+ }
27
+ await writeFile(tempEntryPath, "");
28
+ const originalWorkerBuildConfig = workerEnv.config.build;
29
+ workerEnv.config.build = {
30
+ ...originalWorkerBuildConfig,
31
+ write: false,
32
+ rollupOptions: {
33
+ ...originalWorkerBuildConfig?.rollupOptions,
34
+ input: {
35
+ index: tempEntryPath,
36
+ },
37
+ },
38
+ };
39
+ await builder.build(workerEnv);
40
+ // Restore the original config
41
+ workerEnv.config.build = originalWorkerBuildConfig;
42
+ }
43
+ finally {
44
+ await rm(tempEntryPath, { force: true });
45
+ }
14
46
  await runDirectivesScan({
15
47
  rootConfig: builder.config,
16
48
  environments: builder.environments,
@@ -18,7 +50,7 @@ export async function buildApp({ builder, clientEntryPoints, clientFiles, server
18
50
  serverFiles,
19
51
  entries: [workerEntryPathname],
20
52
  });
21
- console.log("Building worker to discover used client components...");
53
+ console.log("Building worker...");
22
54
  process.env.RWSDK_BUILD_PASS = "worker";
23
55
  await builder.build(workerEnv);
24
56
  log("Used client files after worker build & filtering: %O", Array.from(clientFiles));
@@ -3,6 +3,7 @@ import path, { resolve } from "node:path";
3
3
  import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
4
4
  import { buildApp } from "./buildApp.mjs";
5
5
  import { externalModules } from "./constants.mjs";
6
+ import { ssrBridgeWrapPlugin } from "./ssrBridgeWrapPlugin.mjs";
6
7
  export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, }) => ({
7
8
  name: "rwsdk:config",
8
9
  config: async (_, { command }) => {
@@ -93,6 +94,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
93
94
  "rwsdk/constants",
94
95
  "rwsdk/debug",
95
96
  "rwsdk/realtime/client",
97
+ "rwsdk/router",
96
98
  "rwsdk/turnstile",
97
99
  ],
98
100
  entries: [],
@@ -161,22 +163,14 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
161
163
  // causes a redeclaration error. To solve this, we wrap the SSR
162
164
  // bundle in an exporting IIFE. This creates a scope boundary,
163
165
  // preventing symbol collisions while producing a valid,
164
- // tree-shakeable ES module. The inline plugin below removes the
165
- // original `export` statement from the bundle to prevent syntax
166
- // errors.
166
+ // tree-shakeable ES module.
167
+ //
168
+ // context(justinvdm, 19 Nov 2025): We use a custom plugin
169
+ // (ssrBridgeWrapPlugin) to intelligently inject the IIFE *after*
170
+ // any top-level external imports, ensuring they remain valid.
167
171
  inlineDynamicImports: true,
168
- banner: `export const { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream } = (function() {`,
169
- footer: `return { renderHtmlStream, ssrLoadModule, ssrWebpackRequire, ssrGetModuleExport, createThenableFromReadableStream };\n})();`,
170
172
  },
171
- plugins: [
172
- {
173
- name: "rwsdk:ssr-bridge-exports",
174
- renderChunk(code) {
175
- // Remove the original export statement as it's now handled by the banner/footer
176
- return code.replace(/export\s*{[^}]+};?/, "");
177
- },
178
- },
179
- ],
173
+ plugins: [ssrBridgeWrapPlugin()],
180
174
  },
181
175
  },
182
176
  },
@@ -1,2 +1,3 @@
1
1
  export declare const cloudflareBuiltInModules: string[];
2
2
  export declare const externalModules: string[];
3
+ export declare const externalModulesSet: Set<string>;
@@ -10,3 +10,4 @@ export const externalModules = [
10
10
  ...builtinModules,
11
11
  ...builtinModules.map((m) => `node:${m}`),
12
12
  ];
13
+ export const externalModulesSet = new Set(externalModules);
@@ -42,6 +42,10 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
42
42
  log("Development mode: %s", isDev);
43
43
  },
44
44
  configureServer(server) {
45
+ // context(justinvdm, 19 Nov 2025): This hook simply saves a reference
46
+ // to the dev server instance for use in other hooks. Unlike plugins that
47
+ // must run before the Cloudflare plugin to prevent startup deadlocks,
48
+ // its execution order is not critical, so `enforce: 'pre'` is not needed.
45
49
  devServer = server;
46
50
  },
47
51
  configEnvironment(env, viteConfig) {
@@ -6,6 +6,10 @@ export const devServerTimingPlugin = () => {
6
6
  return {
7
7
  name: "rwsdk:dev-server-timing",
8
8
  configureServer(server) {
9
+ // context(justinvdm, 19 Nov 2025): This hook adds a middleware for
10
+ // logging the time to first response. Unlike other plugins that must
11
+ // run before the Cloudflare plugin to prevent startup deadlocks, its
12
+ // execution order is not critical, so `enforce: 'pre'` is not needed.
9
13
  server.middlewares.use((_req, res, next) => {
10
14
  if (!hasLoggedFirstResponse) {
11
15
  res.on("finish", () => {
@@ -37,8 +37,16 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
37
37
  const APP_SERVER_BARREL_PATH = path.join(tempDir, "app-server-barrel.js");
38
38
  return {
39
39
  name: "rwsdk:directive-modules-dev",
40
+ enforce: "pre",
40
41
  configureServer(server) {
41
- if (!process.env.VITE_IS_DEV_SERVER || process.env.RWSDK_WORKER_RUN) {
42
+ // context(justinvdm, 19 Nov 2025): We must run this hook before the
43
+ // Cloudflare plugin's `configureServer` hook. The Cloudflare plugin makes
44
+ // a request back to the dev server to determine worker exports, which
45
+ // triggers Vite's dependency optimizer. Our esbuild plugin for the
46
+ // optimizer blocks on `scanPromise`. By running this first with `enforce: 'pre'`,
47
+ // we ensure our scan is kicked off before the Cloudflare plugin can trigger
48
+ // the optimizer, preventing a deadlock.
49
+ if (!process.env.VITE_IS_DEV_SERVER) {
42
50
  resolveScanPromise();
43
51
  return;
44
52
  }
@@ -35,6 +35,10 @@ export const directivesPlugin = ({ projectRootDir, clientFiles, serverFiles, })
35
35
  isBuild = config.command === "build";
36
36
  },
37
37
  configureServer(server) {
38
+ // context(justinvdm, 19 Nov 2025): This hook adds a middleware to track
39
+ // when the first server response has finished. Unlike plugins that must
40
+ // run before the Cloudflare plugin to prevent startup deadlocks, its
41
+ // execution order is not critical, so `enforce: 'pre'` is not needed.
38
42
  devServer = server;
39
43
  devServer.middlewares.use((_req, res, next) => {
40
44
  // context(justinvdm, 15 Jun 2025): We want to watch for new client and server modules
@@ -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
+ };
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
3
4
  const require = createRequire(import.meta.url);
4
5
  export async function getViteEsbuild(projectRootDir) {
5
6
  const vitePath = require.resolve("vite/package.json", {
@@ -7,6 +8,6 @@ export async function getViteEsbuild(projectRootDir) {
7
8
  });
8
9
  const viteDir = path.dirname(vitePath);
9
10
  const esbuildPath = require.resolve("esbuild", { paths: [viteDir] });
10
- const esbuildModule = await import(esbuildPath);
11
+ const esbuildModule = await import(pathToFileURL(esbuildPath).href);
11
12
  return esbuildModule.default || esbuildModule;
12
13
  }
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "vite";
2
+ export declare function hmrStabilityPlugin(): Plugin;
@@ -0,0 +1,73 @@
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
+ // context(justinvdm, 19 Nov 2025): This hook adds an error handling
45
+ // middleware for stale dependency errors. It runs in a returned function,
46
+ // which intentionally places it late in the middleware stack. Unlike
47
+ // other plugins that must run before the Cloudflare plugin to prevent
48
+ // startup deadlocks, its timing is not critical, so `enforce: 'pre'`
49
+ // is not needed.
50
+ return () => {
51
+ server.middlewares.use(async function rwsdkStaleBundleErrorHandler(err, req, res, next) {
52
+ if (err &&
53
+ typeof err.message === "string" &&
54
+ err.message.includes("new version of the pre-bundle")) {
55
+ log("Caught stale pre-bundle error. Waiting for server to stabilize...");
56
+ startWaitingForStability();
57
+ await stabilityPromise;
58
+ log("Server stabilized. Sending full-reload and redirecting.");
59
+ // Signal the client to do a full page reload.
60
+ server.environments.client.hot.send({
61
+ type: "full-reload",
62
+ });
63
+ // No need to wait further here, the stability promise handled it.
64
+ res.writeHead(307, { Location: req.url });
65
+ res.end();
66
+ return;
67
+ }
68
+ next(err);
69
+ });
70
+ };
71
+ },
72
+ };
73
+ }
@@ -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}/`));
@@ -106,15 +95,32 @@ export const knownDepsResolverPlugin = ({ projectRootDir, }) => {
106
95
  if (!mappings) {
107
96
  return null;
108
97
  }
98
+ // Create reverse mapping from slugified names to original imports
99
+ // Vite converts "react-dom/server.edge" -> "react-dom_server__edge"
100
+ // Pattern: / becomes _, . becomes __
101
+ const slugifiedToOriginal = new Map();
102
+ for (const [original, resolved] of mappings) {
103
+ const slugified = original.replace(/\//g, "_").replace(/\./g, "__");
104
+ slugifiedToOriginal.set(slugified, original);
105
+ }
109
106
  return {
110
107
  name: `rwsdk:known-dependencies-resolver-esbuild-${envName}`,
111
108
  setup(build) {
112
109
  build.onResolve({ filter: /.*/ }, (args) => {
113
110
  let resolved = mappings.get(args.path);
111
+ // If not found, check if it's a slugified version
112
+ if (!resolved) {
113
+ const originalImport = slugifiedToOriginal.get(args.path);
114
+ if (originalImport) {
115
+ resolved = mappings.get(originalImport);
116
+ }
117
+ }
114
118
  if (!resolved) {
115
119
  resolved = resolveKnownImport(args.path, envName, projectRootDir);
116
120
  }
117
- if (resolved && args.importer !== "") {
121
+ // Resolve for both entry points (importer === '') and regular imports
122
+ // Entry points come from optimizeDeps.include and are critical to intercept
123
+ if (resolved) {
118
124
  if (args.path === "react-server-dom-webpack/client.edge") {
119
125
  return;
120
126
  }
@@ -129,11 +135,23 @@ export const knownDepsResolverPlugin = ({ projectRootDir, }) => {
129
135
  return [
130
136
  {
131
137
  name: "rwsdk:known-dependencies-resolver:config",
132
- enforce: "post",
138
+ enforce: "pre",
133
139
  config(config, { command }) {
134
140
  isBuild = command === "build";
135
141
  log("Configuring plugin for command=%s", command);
136
142
  },
143
+ async configureServer(server) {
144
+ // context(justinvdm, 19 Nov 2025): This hook must run before the
145
+ // Cloudflare plugin's `configureServer` hook, so we use `enforce: 'pre'`.
146
+ // The Cloudflare plugin's hook executes the worker entry file to discover
147
+ // its exports. This can trigger the evaluation of SSR code. We must initialize
148
+ // the SSR dependency optimizer *before* that happens to ensure that any
149
+ // dependencies in `optimizeDeps.include` (like `react-dom/server.edge`)
150
+ // are correctly registered before they are discovered lazily.
151
+ if (server.environments.ssr?.depsOptimizer) {
152
+ await server.environments.ssr.depsOptimizer.init();
153
+ }
154
+ },
137
155
  configResolved(config) {
138
156
  log("Setting up resolve aliases and optimizeDeps for each environment");
139
157
  // Set up aliases and optimizeDeps for each environment
@@ -1,8 +1,9 @@
1
1
  import type { Plugin } from "vite";
2
- export declare function linkWorkerBundle({ code, manifestContent, projectRootDir, }: {
2
+ export declare function linkWorkerBundle({ code, manifestContent, projectRootDir, base, }: {
3
3
  code: string;
4
4
  manifestContent: string;
5
5
  projectRootDir: string;
6
+ base?: string;
6
7
  }): {
7
8
  code: string;
8
9
  map: null;
@@ -4,19 +4,22 @@ import path from "node:path";
4
4
  import { CLIENT_MANIFEST_RELATIVE_PATH } from "../lib/constants.mjs";
5
5
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
6
6
  const log = debug("rwsdk:vite:linker-plugin");
7
- export function linkWorkerBundle({ code, manifestContent, projectRootDir, }) {
7
+ export function linkWorkerBundle({ code, manifestContent, projectRootDir, base, }) {
8
8
  let newCode = code;
9
9
  const manifest = JSON.parse(manifestContent);
10
10
  // 1. Replace the manifest placeholder with the actual manifest content.
11
11
  log("Injecting manifest into worker bundle");
12
- newCode = newCode.replace('"__RWSDK_MANIFEST_PLACEHOLDER__"', manifestContent);
12
+ newCode = newCode.replace(/['"]__RWSDK_MANIFEST_PLACEHOLDER__['"]/, manifestContent);
13
13
  // 2. Replace asset placeholders with their final hashed paths.
14
14
  log("Replacing asset placeholders in final worker bundle");
15
15
  for (const [key, value] of Object.entries(manifest)) {
16
16
  const normalizedKey = normalizeModulePath(key, projectRootDir, {
17
17
  isViteStyle: false,
18
18
  });
19
- newCode = newCode.replaceAll(`rwsdk_asset:${normalizedKey}`, `/${value.file}`);
19
+ // If base is provided, prepend it with the final hashed path.
20
+ // Base is assumed to have a trailing "/".
21
+ const assetPath = (base ? base : "/") + value.file;
22
+ newCode = newCode.replaceAll(`rwsdk_asset:${normalizedKey}`, assetPath);
20
23
  }
21
24
  // 3. Deprefix any remaining placeholders that were not in the manifest.
22
25
  // This handles public assets that don't go through the bundler.
@@ -28,8 +31,12 @@ export function linkWorkerBundle({ code, manifestContent, projectRootDir, }) {
28
31
  };
29
32
  }
30
33
  export const linkerPlugin = ({ projectRootDir, }) => {
34
+ let config;
31
35
  return {
32
36
  name: "rwsdk:linker",
37
+ configResolved(resolvedConfig) {
38
+ config = resolvedConfig;
39
+ },
33
40
  async renderChunk(code) {
34
41
  if (this.environment.name !== "worker" ||
35
42
  process.env.RWSDK_BUILD_PASS !== "linker") {
@@ -41,6 +48,7 @@ export const linkerPlugin = ({ projectRootDir, }) => {
41
48
  code,
42
49
  manifestContent,
43
50
  projectRootDir,
51
+ base: config.base,
44
52
  });
45
53
  log("Final worker chunk rendered");
46
54
  return result;
@@ -29,6 +29,21 @@ describe("linkWorkerBundle", () => {
29
29
  expect(result.code).toContain(`const stylesheet = "/assets/styles.123.css";`);
30
30
  expect(result.code).toContain(`const logo = "/assets/logo.abc.svg";`);
31
31
  });
32
+ it("should replace asset placeholder with a base + hashed paths from the manifest if base is provided", () => {
33
+ const code = `
34
+ const stylesheet = "rwsdk_asset:/src/styles.css";
35
+ const logo = "rwsdk_asset:/src/logo.svg";
36
+ `;
37
+ const base = "/base/";
38
+ const result = linkWorkerBundle({
39
+ code,
40
+ manifestContent,
41
+ projectRootDir,
42
+ base,
43
+ });
44
+ expect(result.code).toContain(`const stylesheet = "/base/assets/styles.123.css";`);
45
+ expect(result.code).toContain(`const logo = "/base/assets/logo.abc.svg";`);
46
+ });
32
47
  it("should deprefix remaining asset placeholders not in the manifest", () => {
33
48
  const code = `const publicImg = "rwsdk_asset:/images/photo.jpg";`;
34
49
  const result = linkWorkerBundle({
@@ -33,29 +33,15 @@ export const hasEntryAsAncestor = ({ module, entryFile, seen = new Set(), }) =>
33
33
  }
34
34
  return false;
35
35
  };
36
- const isInUseClientGraph = ({ file, clientFiles, server, }) => {
37
- const id = normalizeModulePath(file, server.config.root);
38
- if (clientFiles.has(id)) {
39
- return true;
40
- }
41
- const modules = server.environments.client.moduleGraph.getModulesByFile(file);
42
- if (!modules) {
43
- return false;
44
- }
45
- for (const m of modules) {
46
- for (const importer of m.importers) {
47
- if (importer.file &&
48
- isInUseClientGraph({ file: importer.file, clientFiles, server })) {
49
- return true;
50
- }
51
- }
52
- }
53
- return false;
54
- };
55
36
  export const miniflareHMRPlugin = (givenOptions) => [
56
37
  {
57
38
  name: "rwsdk:miniflare-hmr",
58
39
  configureServer(server) {
40
+ // context(justinvdm, 19 Nov 2025): This hook sets up an error handler
41
+ // middleware. It runs in a returned function, which intentionally
42
+ // places it late in the middleware stack. Unlike plugins that must
43
+ // run before the Cloudflare plugin to prevent startup deadlocks, its
44
+ // timing is not critical, so `enforce: 'pre'` is not needed.
59
45
  return () => {
60
46
  server.middlewares.use(function rwsdkDevServerErrorHandler(err, _req, _res, next) {
61
47
  if (err) {
@@ -93,15 +79,6 @@ export const miniflareHMRPlugin = (givenOptions) => [
93
79
  }
94
80
  if (this.environment.name === "ssr") {
95
81
  log("SSR update, invalidating recursively", ctx.file);
96
- const isUseClientUpdate = isInUseClientGraph({
97
- file: ctx.file,
98
- clientFiles,
99
- server: ctx.server,
100
- });
101
- if (!isUseClientUpdate) {
102
- log("hmr: not a use client update, short circuiting");
103
- return [];
104
- }
105
82
  invalidateModule(ctx.server, "ssr", ctx.file);
106
83
  invalidateModule(ctx.server, environment, VIRTUAL_SSR_PREFIX +
107
84
  normalizeModulePath(ctx.file, givenOptions.rootDir));
@@ -166,21 +143,12 @@ export const miniflareHMRPlugin = (givenOptions) => [
166
143
  }
167
144
  }
168
145
  }
169
- const isUseClientUpdate = isInUseClientGraph({
170
- file: ctx.file,
171
- clientFiles,
172
- server: ctx.server,
173
- });
174
- if (!isUseClientUpdate && !ctx.file.endsWith(".css")) {
175
- log("hmr: not a use client update and not css, short circuiting");
176
- return [];
177
- }
178
- log("hmr: returning client modules for hmr");
179
146
  return ctx.modules;
180
147
  }
181
148
  // The worker needs an update, and the hot check is for the worker environment
182
149
  // => Notify for custom RSC-based HMR update, then short circuit HMR
183
150
  if (isWorkerUpdate && this.environment.name === environment) {
151
+ invalidateModule(ctx.server, environment, ctx.file);
184
152
  const shortName = getShortName(ctx.file, ctx.server.config.root);
185
153
  this.environment.logger.info(`${colors.green(`worker update`)} ${colors.dim(shortName)}`, {
186
154
  clear: true,
@@ -1,13 +1,23 @@
1
- import { $sh } from "../lib/$.mjs";
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { glob } from "glob";
2
4
  export const moveStaticAssetsPlugin = ({ rootDir, }) => ({
3
5
  name: "rwsdk:move-static-assets",
4
6
  apply: "build",
5
7
  async closeBundle() {
6
8
  if (this.environment.name === "worker" &&
7
9
  process.env.RWSDK_BUILD_PASS === "linker") {
8
- await $sh({
9
- cwd: rootDir,
10
- }) `mv dist/worker/assets/*.css dist/client/assets/ || true`;
10
+ const sourceDir = path.join(rootDir, "dist", "worker", "assets");
11
+ const destDir = path.join(rootDir, "dist", "client", "assets");
12
+ const cssFiles = await glob("*.css", { cwd: sourceDir });
13
+ if (cssFiles.length > 0) {
14
+ await fs.ensureDir(destDir);
15
+ for (const file of cssFiles) {
16
+ const sourceFile = path.join(sourceDir, file);
17
+ const destFile = path.join(destDir, file);
18
+ await fs.move(sourceFile, destFile, { overwrite: true });
19
+ }
20
+ }
11
21
  }
12
22
  },
13
23
  });
@@ -18,12 +18,13 @@ import { directivesPlugin } from "./directivesPlugin.mjs";
18
18
  import { injectVitePreamble } from "./injectVitePreamblePlugin.mjs";
19
19
  import { knownDepsResolverPlugin } from "./knownDepsResolverPlugin.mjs";
20
20
  import { linkerPlugin } from "./linkerPlugin.mjs";
21
- import { manifestPlugin } from "./manifestPlugin.mjs";
22
21
  import { miniflareHMRPlugin } from "./miniflareHMRPlugin.mjs";
23
22
  import { moveStaticAssetsPlugin } from "./moveStaticAssetsPlugin.mjs";
24
23
  import { prismaPlugin } from "./prismaPlugin.mjs";
25
24
  import { resolveForcedPaths } from "./resolveForcedPaths.mjs";
26
25
  import { ssrBridgePlugin } from "./ssrBridgePlugin.mjs";
26
+ import { staleDepRetryPlugin } from "./staleDepRetryPlugin.mjs";
27
+ import { statePlugin } from "./statePlugin.mjs";
27
28
  import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
28
29
  import { useClientLookupPlugin } from "./useClientLookupPlugin.mjs";
29
30
  import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
@@ -74,17 +75,15 @@ export const redwoodPlugin = async (options = {}) => {
74
75
  // context(justinvdm, 31 Mar 2025): We assume that if there is no .wrangler directory,
75
76
  // then this is fresh install, and we run `npm run dev:init` here.
76
77
  if (process.env.RWSDK_WORKER_RUN !== "1" &&
77
- process.env.RWSDK_DEPLOY !== "1" &&
78
+ process.env.NODE_ENV !== "production" &&
78
79
  !(await pathExists(resolve(projectRootDir, ".wrangler"))) &&
79
80
  (await hasPkgScript(projectRootDir, "dev:init"))) {
80
81
  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`;
82
+ await $ `npm run dev:init`;
86
83
  }
87
84
  return [
85
+ staleDepRetryPlugin(),
86
+ statePlugin({ projectRootDir }),
88
87
  devServerTimingPlugin(),
89
88
  devServerConstantPlugin(),
90
89
  directiveModulesDevPlugin({
@@ -144,9 +143,6 @@ export const redwoodPlugin = async (options = {}) => {
144
143
  clientEntryPoints,
145
144
  projectRootDir,
146
145
  }),
147
- manifestPlugin({
148
- projectRootDir,
149
- }),
150
146
  moveStaticAssetsPlugin({ rootDir: projectRootDir }),
151
147
  prismaPlugin({ projectRootDir }),
152
148
  linkerPlugin({ projectRootDir }),