@ivogt/rsc-router 0.0.0-experimental.11 → 0.0.0-experimental.13

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.
@@ -102,7 +102,7 @@ function exposeActionId() {
102
102
  let hashToFileMap;
103
103
  let rscPluginApi;
104
104
  return {
105
- name: "rsc-router:expose-action-id",
105
+ name: "@ivogt/rsc-router:expose-action-id",
106
106
  // Run after all other plugins (including RSC plugin's transforms)
107
107
  enforce: "post",
108
108
  configResolved(resolvedConfig) {
@@ -116,7 +116,7 @@ function exposeActionId() {
116
116
  }
117
117
  if (!rscPluginApi) {
118
118
  throw new Error(
119
- "[rsc-router] Could not find @vitejs/plugin-rsc. rsc-router requires the Vite RSC plugin.\nThe RSC plugin should be included automatically. If you disabled it with\nrscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
119
+ "[rsc-router] Could not find @vitejs/plugin-rsc. @ivogt/rsc-router requires the Vite RSC plugin.\nThe RSC plugin should be included automatically. If you disabled it with\nrscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
120
120
  );
121
121
  }
122
122
  if (!isBuild) return;
@@ -189,7 +189,7 @@ function hashLoaderId(filePath, exportName) {
189
189
  return `${hash.slice(0, 8)}#${exportName}`;
190
190
  }
191
191
  function hasCreateLoaderImport(code) {
192
- const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
192
+ const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/server)?["']/;
193
193
  return pattern.test(code);
194
194
  }
195
195
  function countCreateLoaderArgs(code, startPos, endPos) {
@@ -265,7 +265,7 @@ function exposeLoaderId() {
265
265
  const loaderRegistry = /* @__PURE__ */ new Map();
266
266
  const pendingLoaderScans = /* @__PURE__ */ new Map();
267
267
  return {
268
- name: "rsc-router:expose-loader-id",
268
+ name: "@ivogt/rsc-router:expose-loader-id",
269
269
  enforce: "post",
270
270
  configResolved(resolvedConfig) {
271
271
  config = resolvedConfig;
@@ -325,7 +325,7 @@ function exposeLoaderId() {
325
325
  load(id) {
326
326
  if (id === RESOLVED_VIRTUAL_LOADER_MANIFEST) {
327
327
  if (!isBuild) {
328
- return `import { setLoaderImports } from "rsc-router/server";
328
+ return `import { setLoaderImports } from "@ivogt/rsc-router/server";
329
329
 
330
330
  // Dev mode: empty map, loaders are resolved dynamically via path parsing
331
331
  setLoaderImports({});
@@ -338,13 +338,13 @@ setLoaderImports({});
338
338
  );
339
339
  }
340
340
  if (lazyImports.length === 0) {
341
- return `import { setLoaderImports } from "rsc-router/server";
341
+ return `import { setLoaderImports } from "@ivogt/rsc-router/server";
342
342
 
343
343
  // No fetchable loaders discovered during build
344
344
  setLoaderImports({});
345
345
  `;
346
346
  }
347
- const code = `import { setLoaderImports } from "rsc-router/server";
347
+ const code = `import { setLoaderImports } from "@ivogt/rsc-router/server";
348
348
 
349
349
  // Lazy import map - loaders are loaded on-demand when first requested
350
350
  setLoaderImports({
@@ -394,7 +394,7 @@ function hashHandleId(filePath, exportName) {
394
394
  return `${hash.slice(0, 8)}#${exportName}`;
395
395
  }
396
396
  function hasCreateHandleImport(code) {
397
- const pattern = /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
397
+ const pattern = /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
398
398
  return pattern.test(code);
399
399
  }
400
400
  function analyzeCreateHandleArgs(code, startPos, endPos) {
@@ -462,7 +462,7 @@ function exposeHandleId() {
462
462
  let config;
463
463
  let isBuild = false;
464
464
  return {
465
- name: "rsc-router:expose-handle-id",
465
+ name: "@ivogt/rsc-router:expose-handle-id",
466
466
  enforce: "post",
467
467
  configResolved(resolvedConfig) {
468
468
  config = resolvedConfig;
@@ -497,7 +497,7 @@ function hashLocationStateKey(filePath, exportName) {
497
497
  return `${hash.slice(0, 8)}#${exportName}`;
498
498
  }
499
499
  function hasCreateLocationStateImport(code) {
500
- const pattern = /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
500
+ const pattern = /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
501
501
  return pattern.test(code);
502
502
  }
503
503
  function transformLocationStateExports(code, filePath, sourceId, isBuild = false) {
@@ -554,7 +554,7 @@ function exposeLocationStateId() {
554
554
  let config;
555
555
  let isBuild = false;
556
556
  return {
557
- name: "rsc-router:expose-location-state-id",
557
+ name: "@ivogt/rsc-router:expose-location-state-id",
558
558
  enforce: "post",
559
559
  configResolved(resolvedConfig) {
560
560
  config = resolvedConfig;
@@ -584,11 +584,11 @@ import {
584
584
  setServerCallback,
585
585
  encodeReply,
586
586
  createTemporaryReferenceSet,
587
- } from "rsc-router/internal/deps/browser";
587
+ } from "@ivogt/rsc-router/internal/deps/browser";
588
588
  import { createElement, StrictMode } from "react";
589
589
  import { hydrateRoot } from "react-dom/client";
590
- import { rscStream } from "rsc-router/internal/deps/html-stream-client";
591
- import { initBrowserApp, RSCRouter } from "rsc-router/browser";
590
+ import { rscStream } from "@ivogt/rsc-router/internal/deps/html-stream-client";
591
+ import { initBrowserApp, RSCRouter } from "@ivogt/rsc-router/browser";
592
592
 
593
593
  async function initializeApp() {
594
594
  const deps = {
@@ -610,10 +610,10 @@ async function initializeApp() {
610
610
  initializeApp().catch(console.error);
611
611
  `.trim();
612
612
  var VIRTUAL_ENTRY_SSR = `
613
- import { createFromReadableStream } from "rsc-router/internal/deps/ssr";
613
+ import { createFromReadableStream } from "@ivogt/rsc-router/internal/deps/ssr";
614
614
  import { renderToReadableStream } from "react-dom/server.edge";
615
- import { injectRSCPayload } from "rsc-router/internal/deps/html-stream-server";
616
- import { createSSRHandler } from "rsc-router/ssr";
615
+ import { injectRSCPayload } from "@ivogt/rsc-router/internal/deps/html-stream-server";
616
+ import { createSSRHandler } from "@ivogt/rsc-router/ssr";
617
617
 
618
618
  export const renderHTML = createSSRHandler({
619
619
  createFromReadableStream,
@@ -632,10 +632,10 @@ import {
632
632
  loadServerAction,
633
633
  decodeAction,
634
634
  decodeFormState,
635
- } from "rsc-router/internal/deps/rsc";
635
+ } from "@ivogt/rsc-router/internal/deps/rsc";
636
636
  import { router } from "${routerPath}";
637
- import { createRSCHandler } from "rsc-router/rsc";
638
- import { VERSION } from "rsc-router:version";
637
+ import { createRSCHandler } from "@ivogt/rsc-router/rsc";
638
+ import { VERSION } from "@ivogt/rsc-router:version";
639
639
 
640
640
  // Import loader manifest to ensure all fetchable loaders are registered at startup
641
641
  // This is critical for serverless/multi-process deployments where the loader module
@@ -662,7 +662,7 @@ var VIRTUAL_IDS = {
662
662
  browser: "virtual:rsc-router/entry.browser.js",
663
663
  ssr: "virtual:rsc-router/entry.ssr.js",
664
664
  rsc: "virtual:rsc-router/entry.rsc.js",
665
- version: "rsc-router:version"
665
+ version: "@ivogt/rsc-router:version"
666
666
  };
667
667
  function getVirtualVersionContent(version) {
668
668
  return `export const VERSION = ${JSON.stringify(version)};`;
@@ -675,7 +675,7 @@ import { resolve } from "node:path";
675
675
  // package.json
676
676
  var package_default = {
677
677
  name: "@ivogt/rsc-router",
678
- version: "0.0.0-experimental.11",
678
+ version: "0.0.0-experimental.13",
679
679
  type: "module",
680
680
  description: "Type-safe RSC router with partial rendering support",
681
681
  author: "Ivo Todorov",
@@ -810,7 +810,7 @@ var package_default = {
810
810
  };
811
811
 
812
812
  // src/vite/package-resolution.ts
813
- var VIRTUAL_PACKAGE_NAME = "rsc-router";
813
+ var VIRTUAL_PACKAGE_NAME = "@ivogt/rsc-router";
814
814
  function getPublishedPackageName() {
815
815
  return package_default.name;
816
816
  }
@@ -870,13 +870,13 @@ function getPackageAliases() {
870
870
 
871
871
  // src/vite/index.ts
872
872
  var versionEsbuildPlugin = {
873
- name: "rsc-router-version",
873
+ name: "@ivogt/rsc-router-version",
874
874
  setup(build) {
875
875
  build.onResolve({ filter: /^rsc-router:version$/ }, (args) => ({
876
876
  path: args.path,
877
- namespace: "rsc-router-virtual"
877
+ namespace: "@ivogt/rsc-router-virtual"
878
878
  }));
879
- build.onLoad({ filter: /.*/, namespace: "rsc-router-virtual" }, () => ({
879
+ build.onLoad({ filter: /.*/, namespace: "@ivogt/rsc-router-virtual" }, () => ({
880
880
  contents: `export const VERSION = "dev";`,
881
881
  loader: "js"
882
882
  }));
@@ -898,7 +898,7 @@ function createVirtualEntriesPlugin(entries, routerPath) {
898
898
  virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
899
899
  }
900
900
  return {
901
- name: "rsc-router:virtual-entries",
901
+ name: "@ivogt/rsc-router:virtual-entries",
902
902
  enforce: "pre",
903
903
  resolveId(id) {
904
904
  if (id in virtualModules) {
@@ -936,7 +936,7 @@ function createVersionPlugin() {
936
936
  let isDev = false;
937
937
  let server = null;
938
938
  return {
939
- name: "rsc-router:version",
939
+ name: "@ivogt/rsc-router:version",
940
940
  enforce: "pre",
941
941
  configResolved(config) {
942
942
  isDev = config.command === "serve";
@@ -984,7 +984,7 @@ function createVersionInjectorPlugin(rscEntryPath) {
984
984
  let projectRoot = "";
985
985
  let resolvedEntryPath = "";
986
986
  return {
987
- name: "rsc-router:version-injector",
987
+ name: "@ivogt/rsc-router:version-injector",
988
988
  enforce: "pre",
989
989
  configResolved(config) {
990
990
  projectRoot = config.root;
@@ -999,7 +999,7 @@ function createVersionInjectorPlugin(rscEntryPath) {
999
999
  if (!code.includes("createRSCHandler")) {
1000
1000
  return null;
1001
1001
  }
1002
- if (code.includes("rsc-router:version")) {
1002
+ if (code.includes("@ivogt/rsc-router:version")) {
1003
1003
  return null;
1004
1004
  }
1005
1005
  const handlerCallMatch = code.match(/createRSCHandler\s*\(\s*\{/);
@@ -1020,7 +1020,7 @@ function createVersionInjectorPlugin(rscEntryPath) {
1020
1020
  if (nextNewline === -1) break;
1021
1021
  insertIndex = nextNewline + 1;
1022
1022
  }
1023
- const versionImport = `import { VERSION } from "rsc-router:version";
1023
+ const versionImport = `import { VERSION } from "@ivogt/rsc-router:version";
1024
1024
  `;
1025
1025
  let newCode = code.slice(0, insertIndex) + versionImport + code.slice(insertIndex);
1026
1026
  newCode = newCode.replace(
@@ -1048,7 +1048,7 @@ async function rscRouter(options) {
1048
1048
  ssr: VIRTUAL_IDS.ssr
1049
1049
  };
1050
1050
  plugins.push({
1051
- name: "rsc-router:cloudflare-integration",
1051
+ name: "@ivogt/rsc-router:cloudflare-integration",
1052
1052
  enforce: "pre",
1053
1053
  config() {
1054
1054
  return {
@@ -1137,7 +1137,7 @@ async function rscRouter(options) {
1137
1137
  rscEntryPath = userEntries.rsc ?? null;
1138
1138
  let hasWarnedDuplicate = false;
1139
1139
  plugins.push({
1140
- name: "rsc-router:rsc-integration",
1140
+ name: "@ivogt/rsc-router:rsc-integration",
1141
1141
  enforce: "pre",
1142
1142
  config() {
1143
1143
  const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
@@ -1231,7 +1231,7 @@ async function rscRouter(options) {
1231
1231
  }
1232
1232
  function createCjsToEsmPlugin() {
1233
1233
  return {
1234
- name: "rsc-router:cjs-to-esm",
1234
+ name: "@ivogt/rsc-router:cjs-to-esm",
1235
1235
  enforce: "pre",
1236
1236
  transform(code, id) {
1237
1237
  const cleanId = id.split("?")[0];
@@ -1250,8 +1250,9 @@ function createCjsToEsmPlugin() {
1250
1250
  if (license) {
1251
1251
  transformed = transformed.slice(license.length);
1252
1252
  }
1253
+ transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
1253
1254
  transformed = transformed.replace(
1254
- /^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
1255
+ /^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
1255
1256
  ""
1256
1257
  );
1257
1258
  transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
@@ -1259,6 +1260,10 @@ function createCjsToEsmPlugin() {
1259
1260
  /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
1260
1261
  'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
1261
1262
  );
1263
+ transformed = transformed.replace(
1264
+ /var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
1265
+ 'import ReactDOM from "react-dom";\nvar '
1266
+ );
1262
1267
  transformed = transformed.replace(
1263
1268
  /exports\.(\w+)\s*=\s*function\s*\(/g,
1264
1269
  "export function $1("
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivogt/rsc-router",
3
- "version": "0.0.0-experimental.11",
3
+ "version": "0.0.0-experimental.13",
4
4
  "type": "module",
5
5
  "description": "Type-safe RSC router with partial rendering support",
6
6
  "author": "Ivo Todorov",
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { isClientComponent, assertClientComponent } from "../component-utils";
3
+
4
+ describe("component-utils", () => {
5
+ describe("isClientComponent", () => {
6
+ it("should return false for regular functions", () => {
7
+ const ServerComponent = () => null;
8
+ expect(isClientComponent(ServerComponent)).toBe(false);
9
+ });
10
+
11
+ it("should return false for non-functions", () => {
12
+ expect(isClientComponent(null)).toBe(false);
13
+ expect(isClientComponent(undefined)).toBe(false);
14
+ expect(isClientComponent("string")).toBe(false);
15
+ expect(isClientComponent(123)).toBe(false);
16
+ expect(isClientComponent({})).toBe(false);
17
+ });
18
+
19
+ it("should return true for functions with client reference marker", () => {
20
+ const ClientComponent = () => null;
21
+ // Simulate what the bundler does for "use client" components
22
+ (ClientComponent as any).$$typeof = Symbol.for("react.client.reference");
23
+ (ClientComponent as any).$$id = "src/components/MyComponent.tsx#default";
24
+
25
+ expect(isClientComponent(ClientComponent)).toBe(true);
26
+ });
27
+
28
+ it("should return false for functions with wrong $$typeof symbol", () => {
29
+ const Component = () => null;
30
+ (Component as any).$$typeof = Symbol.for("react.element");
31
+
32
+ expect(isClientComponent(Component)).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe("assertClientComponent", () => {
37
+ it("should throw for non-function values", () => {
38
+ expect(() => assertClientComponent(null, "document")).toThrow(
39
+ 'document must be a client component function with "use client" directive'
40
+ );
41
+
42
+ expect(() => assertClientComponent({}, "document")).toThrow(
43
+ 'document must be a client component function with "use client" directive'
44
+ );
45
+ });
46
+
47
+ it("should throw for server components (no client marker)", () => {
48
+ const ServerComponent = () => null;
49
+
50
+ expect(() => assertClientComponent(ServerComponent, "document")).toThrow(
51
+ 'document must be a client component with "use client" directive'
52
+ );
53
+ expect(() => assertClientComponent(ServerComponent, "document")).toThrow(
54
+ "cannot be serialized in the RSC payload"
55
+ );
56
+ });
57
+
58
+ it("should not throw for client components", () => {
59
+ const ClientComponent = () => null;
60
+ (ClientComponent as any).$$typeof = Symbol.for("react.client.reference");
61
+ (ClientComponent as any).$$id = "src/document.tsx#default";
62
+
63
+ expect(() =>
64
+ assertClientComponent(ClientComponent, "document")
65
+ ).not.toThrow();
66
+ });
67
+
68
+ it("should include component name in error message", () => {
69
+ const ServerComponent = () => null;
70
+
71
+ expect(() => assertClientComponent(ServerComponent, "myLayout")).toThrow(
72
+ "myLayout must be a client component"
73
+ );
74
+ });
75
+ });
76
+ });
@@ -19,7 +19,7 @@ import type {
19
19
  CacheGetResult,
20
20
  } from "../types.js";
21
21
  import type { RequestContext } from "../../server/request-context.js";
22
- import { VERSION } from "rsc-router:version";
22
+ import { VERSION } from "@ivogt/rsc-router:version";
23
23
 
24
24
  // ============================================================================
25
25
  // Constants
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Component utilities for RSC
3
+ *
4
+ * Helpers for working with React Server Components and client components.
5
+ */
6
+
7
+ import type { ComponentType } from "react";
8
+
9
+ /**
10
+ * Symbol used by React to mark client component references.
11
+ * When a file has "use client" directive, the bundler transforms the exports
12
+ * to include this symbol on $$typeof.
13
+ */
14
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
15
+
16
+ /**
17
+ * Check if a component is a client component (has "use client" directive).
18
+ *
19
+ * Client components are marked by the bundler with:
20
+ * - $$typeof: Symbol.for("react.client.reference")
21
+ * - $$id: string (module identifier)
22
+ *
23
+ * @param component - The component to check
24
+ * @returns true if the component has client reference marker
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { MyComponent } from "./my-component"; // has "use client"
29
+ *
30
+ * if (!isClientComponent(MyComponent)) {
31
+ * throw new Error("MyComponent must be a client component");
32
+ * }
33
+ * ```
34
+ */
35
+ export function isClientComponent(
36
+ component: ComponentType<unknown> | unknown
37
+ ): boolean {
38
+ if (typeof component !== "function") {
39
+ return false;
40
+ }
41
+ const withMeta = component as { $$typeof?: symbol };
42
+ return withMeta.$$typeof === CLIENT_REFERENCE;
43
+ }
44
+
45
+ /**
46
+ * Assert that a component is a client component.
47
+ * Throws a descriptive error if not.
48
+ *
49
+ * @param component - The component to check
50
+ * @param name - Name to use in error message (e.g., "document")
51
+ * @throws Error if the component is not a client component
52
+ */
53
+ export function assertClientComponent(
54
+ component: ComponentType<unknown> | unknown,
55
+ name: string
56
+ ): asserts component is ComponentType<unknown> {
57
+ if (typeof component !== "function") {
58
+ throw new Error(
59
+ `${name} must be a client component function with "use client" directive. ` +
60
+ `Make sure to pass the component itself, not a JSX element: ` +
61
+ `${name}: My${capitalize(name)} (correct) vs ${name}: <My${capitalize(name)} /> (incorrect)`
62
+ );
63
+ }
64
+
65
+ if (!isClientComponent(component)) {
66
+ throw new Error(
67
+ `${name} must be a client component with "use client" directive at the top of the file. ` +
68
+ `Server components cannot be used as the ${name} because their function reference ` +
69
+ `cannot be serialized in the RSC payload. Add "use client" to your ${name} file.`
70
+ );
71
+ }
72
+ }
73
+
74
+ function capitalize(str: string): string {
75
+ return str.charAt(0).toUpperCase() + str.slice(1);
76
+ }
package/src/router.ts CHANGED
@@ -2,6 +2,7 @@ import type { ComponentType } from "react";
2
2
  import { type ReactNode } from "react";
3
3
  import { CacheScope, createCacheScope } from "./cache/cache-scope.js";
4
4
  import type { SegmentCacheStore } from "./cache/types.js";
5
+ import { assertClientComponent } from "./component-utils.js";
5
6
  import { DefaultDocument } from "./components/DefaultDocument.js";
6
7
  import { DefaultErrorFallback } from "./default-error-boundary.js";
7
8
  import {
@@ -511,16 +512,9 @@ export function createRSCRouter<TEnv = any>(
511
512
  invokeOnError(onError, error, phase, context, "Router");
512
513
  }
513
514
 
514
- // Validate document is a function (component)
515
- // Note: We cannot validate "use client" at runtime since it's a bundler directive.
516
- // If a server component is passed, React will throw during rendering with a
517
- // "Functions cannot be passed to Client Components" error.
518
- if (documentOption !== undefined && typeof documentOption !== "function") {
519
- throw new Error(
520
- `document must be a client component function with "use client" directive. ` +
521
- `Make sure to pass the component itself, not a JSX element: ` +
522
- `document: MyDocument (correct) vs document: <MyDocument /> (incorrect)`
523
- );
515
+ // Validate document is a client component
516
+ if (documentOption !== undefined) {
517
+ assertClientComponent(documentOption, "document");
524
518
  }
525
519
 
526
520
  // Use default document if none provided (keeps internal name as rootLayout)
@@ -32,7 +32,7 @@ import type {
32
32
  } from "./types.js";
33
33
  import { hasBodyContent, createResponseWithMergedHeaders } from "./helpers.js";
34
34
  import { generateNonce } from "./nonce.js";
35
- import { VERSION } from "rsc-router:version";
35
+ import { VERSION } from "@ivogt/rsc-router:version";
36
36
  import type { ErrorPhase } from "../types.js";
37
37
  import { invokeOnError } from "../router/error-handling.js";
38
38
 
package/src/server.ts CHANGED
@@ -82,6 +82,12 @@ export {
82
82
  sanitizeError,
83
83
  } from "./errors.js";
84
84
 
85
+ // Component utilities
86
+ export {
87
+ isClientComponent,
88
+ assertClientComponent,
89
+ } from "./component-utils.js";
90
+
85
91
  // Types (re-exported for convenience)
86
92
  export type {
87
93
  RouterEnv,
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
4
4
  * Mock function to test createLoader detection patterns
5
5
  */
6
6
  function hasCreateLoaderImport(code: string): boolean {
7
- const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
7
+ const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/server)?["']/;
8
8
  return pattern.test(code);
9
9
  }
10
10
 
@@ -24,22 +24,22 @@ function extractLoaderExports(code: string): string[] {
24
24
  describe("exposeLoaderId plugin", () => {
25
25
  describe("hasCreateLoaderImport", () => {
26
26
  it("should detect direct import from rsc-router", () => {
27
- const code = `import { createLoader } from "rsc-router";`;
27
+ const code = `import { createLoader } from "@ivogt/rsc-router";`;
28
28
  expect(hasCreateLoaderImport(code)).toBe(true);
29
29
  });
30
30
 
31
31
  it("should detect import from rsc-router/server", () => {
32
- const code = `import { createLoader } from "rsc-router/server";`;
32
+ const code = `import { createLoader } from "@ivogt/rsc-router/server";`;
33
33
  expect(hasCreateLoaderImport(code)).toBe(true);
34
34
  });
35
35
 
36
36
  it("should detect createLoader with other imports", () => {
37
- const code = `import { map, createLoader, route } from "rsc-router";`;
37
+ const code = `import { map, createLoader, route } from "@ivogt/rsc-router";`;
38
38
  expect(hasCreateLoaderImport(code)).toBe(true);
39
39
  });
40
40
 
41
41
  it("should NOT detect aliased import", () => {
42
- const code = `import { createLoader as cl } from "rsc-router";`;
42
+ const code = `import { createLoader as cl } from "@ivogt/rsc-router";`;
43
43
  // Our simple pattern doesn't support aliasing - this is intentional
44
44
  expect(hasCreateLoaderImport(code)).toBe(true); // Still matches the word
45
45
  });
@@ -50,12 +50,12 @@ describe("exposeLoaderId plugin", () => {
50
50
  });
51
51
 
52
52
  it("should NOT detect default import", () => {
53
- const code = `import createLoader from "rsc-router";`;
53
+ const code = `import createLoader from "@ivogt/rsc-router";`;
54
54
  expect(hasCreateLoaderImport(code)).toBe(false);
55
55
  });
56
56
 
57
57
  it("should NOT detect namespace import", () => {
58
- const code = `import * as router from "rsc-router";`;
58
+ const code = `import * as router from "@ivogt/rsc-router";`;
59
59
  expect(hasCreateLoaderImport(code)).toBe(false);
60
60
  });
61
61
  });
@@ -229,7 +229,7 @@ export function exposeActionId(): Plugin {
229
229
  let rscPluginApi: RscPluginApi | undefined;
230
230
 
231
231
  return {
232
- name: "rsc-router:expose-action-id",
232
+ name: "@ivogt/rsc-router:expose-action-id",
233
233
  // Run after all other plugins (including RSC plugin's transforms)
234
234
  enforce: "post",
235
235
 
@@ -251,7 +251,7 @@ export function exposeActionId(): Plugin {
251
251
  if (!rscPluginApi) {
252
252
  throw new Error(
253
253
  "[rsc-router] Could not find @vitejs/plugin-rsc. " +
254
- "rsc-router requires the Vite RSC plugin.\n" +
254
+ "@ivogt/rsc-router requires the Vite RSC plugin.\n" +
255
255
  "The RSC plugin should be included automatically. If you disabled it with\n" +
256
256
  "rscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
257
257
  );
@@ -25,9 +25,9 @@ function hashHandleId(filePath: string, exportName: string): string {
25
25
  * Check if file imports createHandle from rsc-router
26
26
  */
27
27
  function hasCreateHandleImport(code: string): boolean {
28
- // Match: import { createHandle } from "rsc-router" or "rsc-router/..."
28
+ // Match: import { createHandle } from "@ivogt/rsc-router" or "@ivogt/rsc-router/..."
29
29
  const pattern =
30
- /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
30
+ /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
31
31
  return pattern.test(code);
32
32
  }
33
33
 
@@ -167,7 +167,7 @@ function transformHandleExports(
167
167
  * The name is auto-generated from file path + export name.
168
168
  *
169
169
  * Requirements:
170
- * - Must use direct import: import { createHandle } from "rsc-router"
170
+ * - Must use direct import: import { createHandle } from "@ivogt/rsc-router"
171
171
  * - Must use named export: export const MyHandle = createHandle(...)
172
172
  */
173
173
  export function exposeHandleId(): Plugin {
@@ -175,7 +175,7 @@ export function exposeHandleId(): Plugin {
175
175
  let isBuild = false;
176
176
 
177
177
  return {
178
- name: "rsc-router:expose-handle-id",
178
+ name: "@ivogt/rsc-router:expose-handle-id",
179
179
  enforce: "post",
180
180
 
181
181
  configResolved(resolvedConfig) {
@@ -25,10 +25,10 @@ function hashLoaderId(filePath: string, exportName: string): string {
25
25
  * Check if file imports createLoader from rsc-router
26
26
  */
27
27
  function hasCreateLoaderImport(code: string): boolean {
28
- // Match: import { createLoader } from "rsc-router" or "rsc-router/server"
28
+ // Match: import { createLoader } from "@ivogt/rsc-router" or "@ivogt/rsc-router/server"
29
29
  // Must be exact - no aliasing support
30
30
  const pattern =
31
- /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
31
+ /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/server)?["']/;
32
32
  return pattern.test(code);
33
33
  }
34
34
 
@@ -176,7 +176,7 @@ let manifestGenerated = false;
176
176
  * The manifest can be imported by the RSC handler to get all loaders.
177
177
  *
178
178
  * Requirements:
179
- * - Must use direct import: import { createLoader } from "rsc-router"
179
+ * - Must use direct import: import { createLoader } from "@ivogt/rsc-router"
180
180
  * - No aliasing support (import { createLoader as cl } won't work)
181
181
  * - Must use named export: export const MyLoader = createLoader(...)
182
182
  */
@@ -194,7 +194,7 @@ export function exposeLoaderId(): Plugin {
194
194
  const pendingLoaderScans = new Map<string, Promise<void>>();
195
195
 
196
196
  return {
197
- name: "rsc-router:expose-loader-id",
197
+ name: "@ivogt/rsc-router:expose-loader-id",
198
198
  enforce: "post",
199
199
 
200
200
  configResolved(resolvedConfig) {
@@ -276,7 +276,7 @@ export function exposeLoaderId(): Plugin {
276
276
  if (!isBuild) {
277
277
  // Dev mode: empty map - use fallback path parsing in loader registry
278
278
  // IDs in dev mode are "filePath#exportName" format for easier debugging
279
- return `import { setLoaderImports } from "rsc-router/server";
279
+ return `import { setLoaderImports } from "@ivogt/rsc-router/server";
280
280
 
281
281
  // Dev mode: empty map, loaders are resolved dynamically via path parsing
282
282
  setLoaderImports({});
@@ -297,14 +297,14 @@ setLoaderImports({});
297
297
 
298
298
  // If no loaders discovered, set empty map
299
299
  if (lazyImports.length === 0) {
300
- return `import { setLoaderImports } from "rsc-router/server";
300
+ return `import { setLoaderImports } from "@ivogt/rsc-router/server";
301
301
 
302
302
  // No fetchable loaders discovered during build
303
303
  setLoaderImports({});
304
304
  `;
305
305
  }
306
306
 
307
- const code = `import { setLoaderImports } from "rsc-router/server";
307
+ const code = `import { setLoaderImports } from "@ivogt/rsc-router/server";
308
308
 
309
309
  // Lazy import map - loaders are loaded on-demand when first requested
310
310
  setLoaderImports({
@@ -25,9 +25,9 @@ function hashLocationStateKey(filePath: string, exportName: string): string {
25
25
  * Check if file imports createLocationState from rsc-router
26
26
  */
27
27
  function hasCreateLocationStateImport(code: string): boolean {
28
- // Match: import { createLocationState } from "rsc-router" or "rsc-router/client"
28
+ // Match: import { createLocationState } from "@ivogt/rsc-router" or "@ivogt/rsc-router/client"
29
29
  const pattern =
30
- /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
30
+ /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
31
31
  return pattern.test(code);
32
32
  }
33
33
 
@@ -135,7 +135,7 @@ function transformLocationStateExports(
135
135
  * The key is auto-generated from file path + export name.
136
136
  *
137
137
  * Requirements:
138
- * - Must use direct import: import { createLocationState } from "rsc-router"
138
+ * - Must use direct import: import { createLocationState } from "@ivogt/rsc-router"
139
139
  * - Must use named export: export const MyState = createLocationState(...)
140
140
  */
141
141
  export function exposeLocationStateId(): Plugin {
@@ -143,7 +143,7 @@ export function exposeLocationStateId(): Plugin {
143
143
  let isBuild = false;
144
144
 
145
145
  return {
146
- name: "rsc-router:expose-location-state-id",
146
+ name: "@ivogt/rsc-router:expose-location-state-id",
147
147
  enforce: "post",
148
148
 
149
149
  configResolved(resolvedConfig) {
package/src/vite/index.ts CHANGED
@@ -33,13 +33,13 @@ export { exposeLocationStateId } from "./expose-location-state-id.ts";
33
33
  * before Vite's plugin system can handle virtual modules.
34
34
  */
35
35
  const versionEsbuildPlugin = {
36
- name: "rsc-router-version",
36
+ name: "@ivogt/rsc-router-version",
37
37
  setup(build: any) {
38
38
  build.onResolve({ filter: /^rsc-router:version$/ }, (args: any) => ({
39
39
  path: args.path,
40
- namespace: "rsc-router-virtual",
40
+ namespace: "@ivogt/rsc-router-virtual",
41
41
  }));
42
- build.onLoad({ filter: /.*/, namespace: "rsc-router-virtual" }, () => ({
42
+ build.onLoad({ filter: /.*/, namespace: "@ivogt/rsc-router-virtual" }, () => ({
43
43
  contents: `export const VERSION = "dev";`,
44
44
  loader: "js",
45
45
  }));
@@ -184,7 +184,7 @@ function createVirtualEntriesPlugin(
184
184
  }
185
185
 
186
186
  return {
187
- name: "rsc-router:virtual-entries",
187
+ name: "@ivogt/rsc-router:virtual-entries",
188
188
  enforce: "pre",
189
189
 
190
190
  resolveId(id) {
@@ -254,7 +254,7 @@ function createVersionPlugin(): Plugin {
254
254
  let server: any = null;
255
255
 
256
256
  return {
257
- name: "rsc-router:version",
257
+ name: "@ivogt/rsc-router:version",
258
258
  enforce: "pre",
259
259
 
260
260
  configResolved(config) {
@@ -323,7 +323,7 @@ function createVersionInjectorPlugin(rscEntryPath: string): Plugin {
323
323
  let resolvedEntryPath = "";
324
324
 
325
325
  return {
326
- name: "rsc-router:version-injector",
326
+ name: "@ivogt/rsc-router:version-injector",
327
327
  enforce: "pre",
328
328
 
329
329
  configResolved(config) {
@@ -346,7 +346,7 @@ function createVersionInjectorPlugin(rscEntryPath: string): Plugin {
346
346
  }
347
347
 
348
348
  // Check if VERSION is already imported
349
- if (code.includes("rsc-router:version")) {
349
+ if (code.includes("@ivogt/rsc-router:version")) {
350
350
  return null;
351
351
  }
352
352
 
@@ -382,7 +382,7 @@ function createVersionInjectorPlugin(rscEntryPath: string): Plugin {
382
382
  }
383
383
 
384
384
  // Insert VERSION import
385
- const versionImport = `import { VERSION } from "rsc-router:version";\n`;
385
+ const versionImport = `import { VERSION } from "@ivogt/rsc-router:version";\n`;
386
386
  let newCode = code.slice(0, insertIndex) + versionImport + code.slice(insertIndex);
387
387
 
388
388
  // Add version: VERSION to createRSCHandler call
@@ -454,7 +454,7 @@ export async function rscRouter(
454
454
  };
455
455
 
456
456
  plugins.push({
457
- name: "rsc-router:cloudflare-integration",
457
+ name: "@ivogt/rsc-router:cloudflare-integration",
458
458
  enforce: "pre",
459
459
  config() {
460
460
  // Configure environments for cloudflare deployment
@@ -561,7 +561,7 @@ export async function rscRouter(
561
561
  let hasWarnedDuplicate = false;
562
562
 
563
563
  plugins.push({
564
- name: "rsc-router:rsc-integration",
564
+ name: "@ivogt/rsc-router:rsc-integration",
565
565
  enforce: "pre",
566
566
 
567
567
  config() {
@@ -691,7 +691,7 @@ export async function rscRouter(
691
691
  */
692
692
  function createCjsToEsmPlugin(): Plugin {
693
693
  return {
694
- name: "rsc-router:cjs-to-esm",
694
+ name: "@ivogt/rsc-router:cjs-to-esm",
695
695
  enforce: "pre",
696
696
  transform(code, id) {
697
697
  const cleanId = id.split("?")[0];
@@ -727,21 +727,30 @@ function createCjsToEsmPlugin(): Plugin {
727
727
  transformed = transformed.slice(license.length);
728
728
  }
729
729
 
730
- // Remove "use strict" and the conditional IIFE wrapper
730
+ // Remove "use strict" (both dev and prod have this)
731
+ transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
732
+
733
+ // Remove the conditional IIFE wrapper (development only)
731
734
  transformed = transformed.replace(
732
- /^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
735
+ /^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
733
736
  ""
734
737
  );
735
738
 
736
- // Remove the closing of the conditional IIFE at the end
739
+ // Remove the closing of the conditional IIFE at the end (development only)
737
740
  transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
738
741
 
739
- // Replace require('react') and require('react-dom') with imports
742
+ // Replace require('react') and require('react-dom') with imports (development)
740
743
  transformed = transformed.replace(
741
744
  /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
742
745
  'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
743
746
  );
744
747
 
748
+ // Replace require('react-dom') only (production - doesn't import React)
749
+ transformed = transformed.replace(
750
+ /var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
751
+ 'import ReactDOM from "react-dom";\nvar '
752
+ );
753
+
745
754
  // Transform exports.xyz = function() to export function xyz()
746
755
  transformed = transformed.replace(
747
756
  /exports\.(\w+)\s*=\s*function\s*\(/g,
@@ -12,7 +12,7 @@ import packageJson from "../../package.json" with { type: "json" };
12
12
  /**
13
13
  * The canonical name used in virtual entries (without scope)
14
14
  */
15
- const VIRTUAL_PACKAGE_NAME = "rsc-router";
15
+ const VIRTUAL_PACKAGE_NAME = "@ivogt/rsc-router";
16
16
 
17
17
  /**
18
18
  * Get the published package name (e.g., "@ivogt/rsc-router")
@@ -25,12 +25,12 @@ export function getPublishedPackageName(): string {
25
25
  * Check if the package is installed from npm (scoped) vs workspace (unscoped)
26
26
  *
27
27
  * In workspace development:
28
- * - Package is installed as "rsc-router" via pnpm workspace alias
28
+ * - Package is installed as "@ivogt/rsc-router" via pnpm workspace alias
29
29
  * - The scoped name (@ivogt/rsc-router) doesn't exist in node_modules
30
30
  *
31
31
  * When installed from npm:
32
32
  * - Package is installed as "@ivogt/rsc-router"
33
- * - We need aliases to map "rsc-router/*" to "@ivogt/rsc-router/*"
33
+ * - We need aliases to map "@ivogt/rsc-router/*" to "@ivogt/rsc-router/*"
34
34
  */
35
35
  export function isInstalledFromNpm(): boolean {
36
36
  const packageName = getPublishedPackageName();
@@ -3,7 +3,7 @@
3
3
  * This module is provided by the Vite plugin at build/dev time.
4
4
  */
5
5
 
6
- declare module "rsc-router:version" {
6
+ declare module "@ivogt/rsc-router:version" {
7
7
  /**
8
8
  * Auto-generated version string for cache invalidation.
9
9
  * Changes on server restart (dev) or build (prod).
@@ -10,11 +10,11 @@ import {
10
10
  setServerCallback,
11
11
  encodeReply,
12
12
  createTemporaryReferenceSet,
13
- } from "rsc-router/internal/deps/browser";
13
+ } from "@ivogt/rsc-router/internal/deps/browser";
14
14
  import { createElement, StrictMode } from "react";
15
15
  import { hydrateRoot } from "react-dom/client";
16
- import { rscStream } from "rsc-router/internal/deps/html-stream-client";
17
- import { initBrowserApp, RSCRouter } from "rsc-router/browser";
16
+ import { rscStream } from "@ivogt/rsc-router/internal/deps/html-stream-client";
17
+ import { initBrowserApp, RSCRouter } from "@ivogt/rsc-router/browser";
18
18
 
19
19
  async function initializeApp() {
20
20
  const deps = {
@@ -37,10 +37,10 @@ initializeApp().catch(console.error);
37
37
  `.trim();
38
38
 
39
39
  export const VIRTUAL_ENTRY_SSR: string = `
40
- import { createFromReadableStream } from "rsc-router/internal/deps/ssr";
40
+ import { createFromReadableStream } from "@ivogt/rsc-router/internal/deps/ssr";
41
41
  import { renderToReadableStream } from "react-dom/server.edge";
42
- import { injectRSCPayload } from "rsc-router/internal/deps/html-stream-server";
43
- import { createSSRHandler } from "rsc-router/ssr";
42
+ import { injectRSCPayload } from "@ivogt/rsc-router/internal/deps/html-stream-server";
43
+ import { createSSRHandler } from "@ivogt/rsc-router/ssr";
44
44
 
45
45
  export const renderHTML = createSSRHandler({
46
46
  createFromReadableStream,
@@ -63,10 +63,10 @@ import {
63
63
  loadServerAction,
64
64
  decodeAction,
65
65
  decodeFormState,
66
- } from "rsc-router/internal/deps/rsc";
66
+ } from "@ivogt/rsc-router/internal/deps/rsc";
67
67
  import { router } from "${routerPath}";
68
- import { createRSCHandler } from "rsc-router/rsc";
69
- import { VERSION } from "rsc-router:version";
68
+ import { createRSCHandler } from "@ivogt/rsc-router/rsc";
69
+ import { VERSION } from "@ivogt/rsc-router:version";
70
70
 
71
71
  // Import loader manifest to ensure all fetchable loaders are registered at startup
72
72
  // This is critical for serverless/multi-process deployments where the loader module
@@ -97,7 +97,7 @@ export const VIRTUAL_IDS = {
97
97
  browser: "virtual:rsc-router/entry.browser.js",
98
98
  ssr: "virtual:rsc-router/entry.ssr.js",
99
99
  rsc: "virtual:rsc-router/entry.rsc.js",
100
- version: "rsc-router:version",
100
+ version: "@ivogt/rsc-router:version",
101
101
  } as const;
102
102
 
103
103
  /**