@ivogt/rsc-router 0.0.0-experimental.2 → 0.0.0-experimental.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/vite/index.ts +168 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivogt/rsc-router",
3
- "version": "0.0.0-experimental.2",
3
+ "version": "0.0.0-experimental.5",
4
4
  "type": "module",
5
5
  "description": "Type-safe RSC router with partial rendering support",
6
6
  "author": "Ivo Todorov",
package/src/vite/index.ts CHANGED
@@ -22,23 +22,95 @@ export { exposeLocationStateId } from "./expose-location-state-id.ts";
22
22
  // Virtual module type declarations in ./version.d.ts
23
23
 
24
24
  /**
25
- * Plugin to transform CJS react-server-dom vendor file to ESM.
25
+ * Modules that must be excluded from Vite's dependency optimization.
26
+ *
27
+ * When rsc-router is installed from npm, Vite's dep optimizer bundles these modules
28
+ * into separate chunks. However, @vitejs/plugin-rsc creates virtual proxy modules
29
+ * for client components that import from the original source paths.
30
+ *
31
+ * This creates two different module instances:
32
+ * 1. Bundled version in /node_modules/.vite/deps/
33
+ * 2. Original source via plugin-rsc proxy
34
+ *
35
+ * When both instances create React Contexts (e.g., OutletContext), React sees them
36
+ * as different contexts, causing useContext to return undefined even when a Provider
37
+ * exists in the tree.
38
+ *
39
+ * By excluding these modules, we ensure a single module instance is used everywhere.
40
+ *
41
+ * We include both the scoped package name (@ivogt/rsc-router) and the aliased paths
42
+ * (rsc-router) because Vite's optimizer runs before alias resolution.
43
+ */
44
+ /**
45
+ * esbuild plugin to provide rsc-router:version virtual module during optimization.
46
+ * This is needed because esbuild runs during Vite's dependency optimization phase,
47
+ * before Vite's plugin system can handle virtual modules.
48
+ */
49
+ const versionEsbuildPlugin = {
50
+ name: "rsc-router-version",
51
+ setup(build: any) {
52
+ build.onResolve({ filter: /^rsc-router:version$/ }, (args: any) => ({
53
+ path: args.path,
54
+ namespace: "rsc-router-virtual",
55
+ }));
56
+ build.onLoad({ filter: /.*/, namespace: "rsc-router-virtual" }, () => ({
57
+ contents: `export const VERSION = "dev";`,
58
+ loader: "js",
59
+ }));
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Shared esbuild options for dependency optimization.
65
+ * Includes the version stub plugin for all environments.
66
+ */
67
+ const sharedEsbuildOptions = {
68
+ plugins: [versionEsbuildPlugin],
69
+ };
70
+
71
+ const RSC_ROUTER_EXCLUDE_DEPS = [
72
+ // Scoped package paths
73
+ "@ivogt/rsc-router",
74
+ "@ivogt/rsc-router/browser",
75
+ "@ivogt/rsc-router/client",
76
+ "@ivogt/rsc-router/server",
77
+ "@ivogt/rsc-router/rsc",
78
+ "@ivogt/rsc-router/ssr",
79
+ "@ivogt/rsc-router/internal/deps/browser",
80
+ "@ivogt/rsc-router/internal/deps/html-stream-client",
81
+ "@ivogt/rsc-router/internal/deps/ssr",
82
+ "@ivogt/rsc-router/internal/deps/rsc",
83
+ // Aliased paths (before alias resolution)
84
+ "rsc-router/browser",
85
+ "rsc-router/client",
86
+ "rsc-router/server",
87
+ "rsc-router/rsc",
88
+ "rsc-router/ssr",
89
+ "rsc-router/internal/deps/browser",
90
+ "rsc-router/internal/deps/html-stream-client",
91
+ "rsc-router/internal/deps/ssr",
92
+ "rsc-router/internal/deps/rsc",
93
+ ];
94
+
95
+ /**
96
+ * Plugin to transform CJS react-server-dom vendor files to ESM.
26
97
  * The @vitejs/plugin-rsc package ships client.browser.js as CommonJS
27
- * which doesn't work in the browser. This transforms it to ESM re-exports.
98
+ * which doesn't work in the browser. This transforms both:
99
+ * 1. The entry point (client.browser.js) to re-export from the CJS file
100
+ * 2. The actual CJS file content to ESM syntax
28
101
  */
29
102
  function createCjsToEsmPlugin(): Plugin {
30
103
  return {
31
104
  name: "rsc-router:cjs-to-esm",
32
105
  enforce: "pre",
33
106
  transform(code, id) {
34
- // Transform the client.browser.js file from CJS to ESM
35
- // Match any path containing vendor/react-server-dom/client.browser.js
107
+ const cleanId = id.split("?")[0];
108
+
109
+ // Transform the client.browser.js entry point to re-export from CJS
36
110
  if (
37
- id.includes("vendor/react-server-dom/client.browser.js") ||
38
- id.includes("vendor\\react-server-dom\\client.browser.js")
111
+ cleanId.includes("vendor/react-server-dom/client.browser.js") ||
112
+ cleanId.includes("vendor\\react-server-dom\\client.browser.js")
39
113
  ) {
40
- // The original file uses: module.exports = require('./cjs/...')
41
- // Transform to ESM re-export from the development or production CJS file
42
114
  const isProd = process.env.NODE_ENV === "production";
43
115
  const cjsFile = isProd
44
116
  ? "./cjs/react-server-dom-webpack-client.browser.production.js"
@@ -49,6 +121,60 @@ function createCjsToEsmPlugin(): Plugin {
49
121
  map: null,
50
122
  };
51
123
  }
124
+
125
+ // Transform the actual CJS files to ESM
126
+ if (
127
+ (cleanId.includes("vendor/react-server-dom/cjs/") ||
128
+ cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
129
+ cleanId.includes("client.browser")
130
+ ) {
131
+ let transformed = code;
132
+
133
+ // Extract the license comment to preserve it
134
+ const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
135
+ const license = licenseMatch ? licenseMatch[0] : "";
136
+ if (license) {
137
+ transformed = transformed.slice(license.length);
138
+ }
139
+
140
+ // Remove "use strict" and the conditional IIFE wrapper
141
+ // Pattern: "use strict"; "production" !== process.env.NODE_ENV && (function() { ... })();
142
+ transformed = transformed.replace(
143
+ /^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
144
+ ""
145
+ );
146
+
147
+ // Remove the closing of the conditional IIFE at the end: })();
148
+ transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
149
+
150
+ // Replace require('react') and require('react-dom') with imports
151
+ // The pattern spans multiple lines with whitespace
152
+ transformed = transformed.replace(
153
+ /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
154
+ 'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
155
+ );
156
+
157
+ // Transform exports.xyz = function() to export function xyz()
158
+ transformed = transformed.replace(
159
+ /exports\.(\w+)\s*=\s*function\s*\(/g,
160
+ "export function $1("
161
+ );
162
+
163
+ // Transform exports.xyz = value to export const xyz = value
164
+ transformed = transformed.replace(
165
+ /exports\.(\w+)\s*=/g,
166
+ "export const $1 ="
167
+ );
168
+
169
+ // Reconstruct with license at the top
170
+ transformed = license + "\n" + transformed;
171
+
172
+ return {
173
+ code: transformed,
174
+ map: null,
175
+ };
176
+ }
177
+
52
178
  return null;
53
179
  },
54
180
  };
@@ -455,6 +581,12 @@ export async function rscRouter(
455
581
  config() {
456
582
  // Configure environments for cloudflare deployment
457
583
  return {
584
+ // Exclude rsc-router modules from optimization to prevent module duplication
585
+ // This ensures the same Context instance is used by both browser entry and RSC proxy modules
586
+ optimizeDeps: {
587
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
588
+ esbuildOptions: sharedEsbuildOptions,
589
+ },
458
590
  resolve: {
459
591
  alias: {
460
592
  // Map rsc-router/* to @ivogt/rsc-router/* for virtual entries
@@ -486,8 +618,11 @@ export async function rscRouter(
486
618
  },
487
619
  },
488
620
  // Pre-bundle rsc-html-stream to prevent discovery during first request
621
+ // Exclude rsc-router modules to ensure same Context instance
489
622
  optimizeDeps: {
490
623
  include: ["rsc-html-stream/client"],
624
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
625
+ esbuildOptions: sharedEsbuildOptions,
491
626
  },
492
627
  },
493
628
  ssr: {
@@ -500,6 +635,7 @@ export async function rscRouter(
500
635
  dedupe: ["react", "react-dom"],
501
636
  },
502
637
  // Pre-bundle SSR entry and React for proper module linking with childEnvironments
638
+ // Exclude rsc-router modules to ensure same Context instance
503
639
  optimizeDeps: {
504
640
  entries: [finalEntries.ssr],
505
641
  include: [
@@ -508,6 +644,14 @@ export async function rscRouter(
508
644
  "react/jsx-runtime",
509
645
  "rsc-html-stream/server",
510
646
  ],
647
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
648
+ esbuildOptions: sharedEsbuildOptions,
649
+ },
650
+ },
651
+ rsc: {
652
+ // RSC environment also needs esbuild options for version virtual module
653
+ optimizeDeps: {
654
+ esbuildOptions: sharedEsbuildOptions,
511
655
  },
512
656
  },
513
657
  },
@@ -569,6 +713,12 @@ export async function rscRouter(
569
713
  const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
570
714
 
571
715
  return {
716
+ // Exclude rsc-router modules from optimization to prevent module duplication
717
+ // This ensures the same Context instance is used by both browser entry and RSC proxy modules
718
+ optimizeDeps: {
719
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
720
+ esbuildOptions: sharedEsbuildOptions,
721
+ },
572
722
  resolve: {
573
723
  alias: {
574
724
  // Map rsc-router/* to @ivogt/rsc-router/* for virtual entries
@@ -599,12 +749,15 @@ export async function rscRouter(
599
749
  },
600
750
  },
601
751
  },
602
- ...(useVirtualClient && {
603
- optimizeDeps: {
752
+ // Always exclude rsc-router modules, conditionally add virtual entry
753
+ optimizeDeps: {
754
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
755
+ esbuildOptions: sharedEsbuildOptions,
756
+ ...(useVirtualClient && {
604
757
  // Tell Vite to scan the virtual entry for dependencies
605
758
  entries: [VIRTUAL_IDS.browser],
606
- },
607
- }),
759
+ }),
760
+ },
608
761
  },
609
762
  ...(useVirtualSSR && {
610
763
  ssr: {
@@ -612,6 +765,8 @@ export async function rscRouter(
612
765
  entries: [VIRTUAL_IDS.ssr],
613
766
  // Pre-bundle React for SSR to ensure single instance
614
767
  include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
768
+ exclude: RSC_ROUTER_EXCLUDE_DEPS,
769
+ esbuildOptions: sharedEsbuildOptions,
615
770
  },
616
771
  },
617
772
  }),
@@ -621,6 +776,7 @@ export async function rscRouter(
621
776
  entries: [VIRTUAL_IDS.rsc],
622
777
  // Pre-bundle React for RSC to ensure single instance
623
778
  include: ["react", "react/jsx-runtime"],
779
+ esbuildOptions: sharedEsbuildOptions,
624
780
  },
625
781
  },
626
782
  }),