@ivogt/rsc-router 0.0.0-experimental.12 → 0.0.0-experimental.14

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.12",
678
+ version: "0.0.0-experimental.14",
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivogt/rsc-router",
3
- "version": "0.0.0-experimental.12",
3
+ "version": "0.0.0-experimental.14",
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/href.ts CHANGED
@@ -8,19 +8,22 @@ export type SanitizePrefix<T extends string> = T extends `/${infer P}` ? P : T;
8
8
 
9
9
  /**
10
10
  * Helper type to merge multiple route definitions into a single accumulated type.
11
- * Use this to define your app's complete route map for type-safe router.href().
11
+ * Note: When using createRSCRouter, types accumulate automatically through the
12
+ * builder chain, so this type is typically not needed.
12
13
  *
13
14
  * @example
14
15
  * ```typescript
15
- * import { homeRoutes, blogRoutes, shopRoutes } from "./routes";
16
- *
16
+ * // Manual type merging (rarely needed):
17
17
  * type AppRoutes = MergeRoutes<[
18
18
  * typeof homeRoutes,
19
- * PrefixedRoutes<typeof blogRoutes, "blog">,
20
- * PrefixedRoutes<typeof shopRoutes, "shop">,
19
+ * PrefixRoutePatterns<typeof blogRoutes, "/blog">,
21
20
  * ]>;
22
21
  *
23
- * export const router = createRSCRouter<AppEnv>() as RSCRouter<AppEnv, AppRoutes>;
22
+ * // Preferred: Let router accumulate types automatically
23
+ * const router = createRSCRouter<AppEnv>()
24
+ * .routes(homeRoutes).map(...)
25
+ * .routes("/blog", blogRoutes).map(...);
26
+ * type AppRoutes = typeof router.routeMap;
24
27
  * ```
25
28
  */
26
29
  export type MergeRoutes<T extends Record<string, string>[]> = T extends [
@@ -111,9 +114,10 @@ export type HrefFunction<TRoutes extends Record<string, string>> = {
111
114
  *
112
115
  * @example
113
116
  * ```typescript
114
- * const href = createHref(mergedRouteMap);
115
- * href("shop.cart"); // "/shop/cart"
116
- * href("shop.products.detail", { slug: "my-product" }); // "/shop/product/my-product"
117
+ * // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" }
118
+ * const href = createHref(routeMap);
119
+ * href("cart"); // "/shop/cart"
120
+ * href("detail", { slug: "my-product" }); // "/shop/product/my-product"
117
121
  * ```
118
122
  */
119
123
  export function createHref<TRoutes extends Record<string, string>>(
@@ -1248,6 +1248,30 @@ export function map<const T extends RouteDefinition, TEnv = DefaultEnv>(
1248
1248
  };
1249
1249
  }
1250
1250
 
1251
+ /**
1252
+ * Create RouteHelpers for inline route definitions
1253
+ * Used internally by router.map() for inline handler syntax
1254
+ */
1255
+ export function createRouteHelpers<
1256
+ T extends RouteDefinition,
1257
+ TEnv,
1258
+ >(): RouteHelpers<T, TEnv> {
1259
+ return {
1260
+ route: createRouteHelper<T, TEnv>(),
1261
+ layout: createLayoutHelper<TEnv>(),
1262
+ parallel: createParallelHelper<TEnv>(),
1263
+ intercept: createInterceptHelper<T, TEnv>(),
1264
+ middleware: createMiddlewareHelper<TEnv>(),
1265
+ revalidate: createRevalidateHelper<TEnv>(),
1266
+ loader: createLoaderHelper<TEnv>(),
1267
+ loading: createLoadingHelper(),
1268
+ errorBoundary: createErrorBoundaryHelper<TEnv>(),
1269
+ notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
1270
+ when: createWhenHelper(),
1271
+ cache: createCacheHelper(),
1272
+ };
1273
+ }
1274
+
1251
1275
  /**
1252
1276
  * Create a loader definition
1253
1277
  *
@@ -23,7 +23,7 @@
23
23
  * ```
24
24
  */
25
25
 
26
- import type { PrefixedRoutes } from "./href.js";
26
+ import type { PrefixRoutePatterns } from "./href.js";
27
27
 
28
28
  /**
29
29
  * Route map builder interface
@@ -37,12 +37,14 @@ export interface RouteMapBuilder<TRoutes extends Record<string, string> = {}> {
37
37
  add<T extends Record<string, string>>(routes: T): RouteMapBuilder<TRoutes & T>;
38
38
 
39
39
  /**
40
- * Add routes with prefix
40
+ * Add routes with prefix (only URL patterns are prefixed, keys stay unchanged)
41
+ * @param routes - Route definitions to add
42
+ * @param prefix - URL prefix WITHOUT leading slash (e.g., "blog" not "/blog")
41
43
  */
42
44
  add<T extends Record<string, string>, P extends string>(
43
45
  routes: T,
44
46
  prefix: P
45
- ): RouteMapBuilder<TRoutes & PrefixedRoutes<T, P>>;
47
+ ): RouteMapBuilder<TRoutes & PrefixRoutePatterns<T, `/${P}`>>;
46
48
 
47
49
  /**
48
50
  * The accumulated route map (for typeof extraction in module augmentation)
@@ -52,25 +54,29 @@ export interface RouteMapBuilder<TRoutes extends Record<string, string> = {}> {
52
54
 
53
55
  /**
54
56
  * Add routes to a map with optional prefix
57
+ * Keys stay unchanged for composability - only URL patterns get prefixed.
55
58
  *
56
59
  * @param routeMap - The map to add routes to
57
60
  * @param routes - Routes to add
58
- * @param prefix - Optional prefix for keys and paths
61
+ * @param prefix - Optional prefix for URL paths WITHOUT leading slash (keys stay unchanged)
59
62
  */
60
63
  function addRoutes(
61
64
  routeMap: Record<string, string>,
62
65
  routes: Record<string, string>,
63
66
  prefix: string = ""
64
67
  ): void {
68
+ // Normalize prefix: remove leading slash if accidentally provided
69
+ const normalizedPrefix = prefix.startsWith("/") ? prefix.slice(1) : prefix;
70
+
65
71
  for (const [key, pattern] of Object.entries(routes)) {
66
- const prefixedKey = prefix ? `${prefix}.${key}` : key;
67
72
  const prefixedPattern =
68
- prefix && pattern !== "/"
69
- ? `/${prefix}${pattern}`
70
- : prefix && pattern === "/"
71
- ? `/${prefix}`
73
+ normalizedPrefix && pattern !== "/"
74
+ ? `/${normalizedPrefix}${pattern}`
75
+ : normalizedPrefix && pattern === "/"
76
+ ? `/${normalizedPrefix}`
72
77
  : pattern;
73
- routeMap[prefixedKey] = prefixedPattern;
78
+ // Use original key - enables reusable route modules
79
+ routeMap[key] = prefixedPattern;
74
80
  }
75
81
  }
76
82
 
@@ -5,7 +5,9 @@
5
5
  */
6
6
 
7
7
  import { invariant, RouteNotFoundError } from "../errors";
8
+ import { createRouteHelpers } from "../route-definition";
8
9
  import { getContext, type EntryData, type MetricsStore } from "../server/context";
10
+ import MapRootLayout from "../server/root-layout";
9
11
  import type { RouteEntry } from "../types";
10
12
 
11
13
  /**
@@ -63,19 +65,39 @@ export async function loadManifest(
63
65
  Store.namespace || namespaceWithMount,
64
66
  Store.parent,
65
67
  async () => {
66
- const load = await entry.handler();
67
- if (
68
- load &&
69
- load !== null &&
70
- typeof load === "object" &&
71
- "default" in load
72
- ) {
73
- return load.default();
74
- }
75
- if (typeof load === "function") {
76
- return load();
68
+ // Create helpers - inline handlers use them, lazy handlers ignore them
69
+ const helpers = createRouteHelpers();
70
+
71
+ // Call handler with helpers - works for both inline and lazy
72
+ const result = entry.handler(helpers);
73
+
74
+ // Handle based on return type
75
+ if (result instanceof Promise) {
76
+ // Lazy: () => import(...) - returns Promise
77
+ const load = await result;
78
+ if (
79
+ load &&
80
+ load !== null &&
81
+ typeof load === "object" &&
82
+ "default" in load
83
+ ) {
84
+ // Promise<{ default: () => Array }> - e.g., dynamic import
85
+ // Pass helpers - functions that need them will use them,
86
+ // functions from route-definition's map() will ignore them
87
+ return load.default(helpers);
88
+ }
89
+ if (typeof load === "function") {
90
+ // Promise<() => Array>
91
+ return load(helpers);
92
+ }
93
+ // Promise<Array> - direct array from async handler
94
+ return load;
77
95
  }
78
- return load;
96
+
97
+ // Inline: ({ route }) => [...] - returns Array directly
98
+ // Wrap with layout (like map() from route-definition does)
99
+ // Flatten nested arrays from layout/route definitions
100
+ return [helpers.layout(MapRootLayout, () => result)].flat(3);
79
101
  }
80
102
  );
81
103