@ivogt/rsc-router 0.0.0-experimental.1

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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Client-safe route map builder
3
+ *
4
+ * Provides a fluent API for building route maps with prefixes.
5
+ * Can be imported in client code without pulling in server dependencies.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createRouteMap, registerRouteMap } from "rsc-router/browser";
10
+ *
11
+ * const routeMap = createRouteMap()
12
+ * .add(homeRoutes)
13
+ * .add(blogRoutes, "blog")
14
+ * .add(shopRoutes, "shop");
15
+ *
16
+ * registerRouteMap(routeMap.routes);
17
+ *
18
+ * declare global {
19
+ * namespace RSCRouter {
20
+ * interface RegisteredRoutes extends typeof routeMap.routes {}
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import type { PrefixedRoutes } from "./href.js";
27
+
28
+ /**
29
+ * Route map builder interface
30
+ *
31
+ * Accumulates route types through the builder chain for type-safe href.
32
+ */
33
+ export interface RouteMapBuilder<TRoutes extends Record<string, string> = {}> {
34
+ /**
35
+ * Add routes without prefix
36
+ */
37
+ add<T extends Record<string, string>>(routes: T): RouteMapBuilder<TRoutes & T>;
38
+
39
+ /**
40
+ * Add routes with prefix
41
+ */
42
+ add<T extends Record<string, string>, P extends string>(
43
+ routes: T,
44
+ prefix: P
45
+ ): RouteMapBuilder<TRoutes & PrefixedRoutes<T, P>>;
46
+
47
+ /**
48
+ * The accumulated route map (for typeof extraction in module augmentation)
49
+ */
50
+ readonly routes: TRoutes;
51
+ }
52
+
53
+ /**
54
+ * Add routes to a map with optional prefix
55
+ *
56
+ * @param routeMap - The map to add routes to
57
+ * @param routes - Routes to add
58
+ * @param prefix - Optional prefix for keys and paths
59
+ */
60
+ function addRoutes(
61
+ routeMap: Record<string, string>,
62
+ routes: Record<string, string>,
63
+ prefix: string = ""
64
+ ): void {
65
+ for (const [key, pattern] of Object.entries(routes)) {
66
+ const prefixedKey = prefix ? `${prefix}.${key}` : key;
67
+ const prefixedPattern =
68
+ prefix && pattern !== "/"
69
+ ? `/${prefix}${pattern}`
70
+ : prefix && pattern === "/"
71
+ ? `/${prefix}`
72
+ : pattern;
73
+ routeMap[prefixedKey] = prefixedPattern;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Create a new route map builder
79
+ *
80
+ * @returns A builder for accumulating routes with type-safe prefixes
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const routeMap = createRouteMap()
85
+ * .add(homeRoutes)
86
+ * .add(blogRoutes, "blog");
87
+ *
88
+ * // Types are accumulated through the chain
89
+ * type AppRoutes = typeof routeMap.routes;
90
+ * ```
91
+ */
92
+ export function createRouteMap(): RouteMapBuilder<{}> {
93
+ const routeMap: Record<string, string> = {};
94
+
95
+ const builder: RouteMapBuilder<any> = {
96
+ add(routes: Record<string, string>, prefix?: string) {
97
+ addRoutes(routeMap, routes, prefix);
98
+ return builder;
99
+ },
100
+ get routes() {
101
+ return routeMap;
102
+ },
103
+ };
104
+
105
+ return builder;
106
+ }
107
+
108
+ // Singleton route map instance - populated when routes.ts is imported
109
+ let globalRouteMap: Record<string, string> = {};
110
+
111
+ /**
112
+ * Register the route map globally for href to use at runtime
113
+ *
114
+ * Call this after building your route map to make it available to href.
115
+ *
116
+ * @param map - The route map to register
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const routeMap = createRouteMap()
121
+ * .add(homeRoutes)
122
+ * .add(blogRoutes, "blog");
123
+ *
124
+ * registerRouteMap(routeMap.routes);
125
+ * ```
126
+ */
127
+ export function registerRouteMap(map: Record<string, string>): void {
128
+ globalRouteMap = map;
129
+ }
130
+
131
+ /**
132
+ * Get the globally registered route map
133
+ *
134
+ * Used internally by href to resolve route names to URLs at runtime.
135
+ *
136
+ * @returns The registered route map
137
+ */
138
+ export function getGlobalRouteMap(): Record<string, string> {
139
+ return globalRouteMap;
140
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Type definitions for route system items
3
+ * These are extracted separately to avoid circular dependencies
4
+ * and to prevent bundling server-only code in client bundles
5
+ */
6
+
7
+ /**
8
+ * Branded return types for route helpers
9
+ */
10
+ export declare const LayoutBrand: unique symbol;
11
+ export declare const RouteBrand: unique symbol;
12
+ export declare const ParallelBrand: unique symbol;
13
+ export declare const InterceptBrand: unique symbol;
14
+ export declare const MiddlewareBrand: unique symbol;
15
+ export declare const RevalidateBrand: unique symbol;
16
+ export declare const LoaderBrand: unique symbol;
17
+ export declare const LoadingBrand: unique symbol;
18
+ export declare const ErrorBoundaryBrand: unique symbol;
19
+ export declare const NotFoundBoundaryBrand: unique symbol;
20
+ export declare const WhenBrand: unique symbol;
21
+ export declare const CacheBrand: unique symbol;
22
+
23
+ export type LayoutItem = {
24
+ name: string;
25
+ type: "layout";
26
+ uses?: AllUseItems[];
27
+ [LayoutBrand]: void;
28
+ };
29
+ export type RouteItem = {
30
+ name: string;
31
+ type: "route";
32
+ uses?: AllUseItems[];
33
+ [RouteBrand]: void;
34
+ };
35
+ export type ParallelItem = {
36
+ name: string;
37
+ type: "parallel";
38
+ uses?: ParallelUseItem[];
39
+ [ParallelBrand]: void;
40
+ };
41
+ export type InterceptItem = {
42
+ name: string;
43
+ type: "intercept";
44
+ uses?: InterceptUseItem[];
45
+ [InterceptBrand]: void;
46
+ };
47
+ export type LoaderItem = {
48
+ name: string;
49
+ type: "loader";
50
+ uses?: LoaderUseItem[];
51
+ [LoaderBrand]: void;
52
+ };
53
+ export type MiddlewareItem = {
54
+ name: string;
55
+ type: "middleware";
56
+ uses?: AllUseItems[];
57
+ [MiddlewareBrand]: void;
58
+ };
59
+ export type RevalidateItem = {
60
+ name: string;
61
+ type: "revalidate";
62
+ uses?: AllUseItems[];
63
+ [RevalidateBrand]: void;
64
+ };
65
+ export type LoadingItem = {
66
+ name: string;
67
+ type: "loading";
68
+ [LoadingBrand]: void;
69
+ };
70
+ export type ErrorBoundaryItem = {
71
+ name: string;
72
+ type: "errorBoundary";
73
+ uses?: AllUseItems[];
74
+ [ErrorBoundaryBrand]: void;
75
+ };
76
+ export type NotFoundBoundaryItem = {
77
+ name: string;
78
+ type: "notFoundBoundary";
79
+ uses?: AllUseItems[];
80
+ [NotFoundBoundaryBrand]: void;
81
+ };
82
+ export type WhenItem = {
83
+ name: string;
84
+ type: "when";
85
+ [WhenBrand]: void;
86
+ };
87
+ export type CacheItem = {
88
+ name: string;
89
+ type: "cache";
90
+ uses?: AllUseItems[];
91
+ [CacheBrand]: void;
92
+ };
93
+
94
+ /**
95
+ * Union types for use() callbacks
96
+ */
97
+ export type AllUseItems =
98
+ | LayoutItem
99
+ | RouteItem
100
+ | MiddlewareItem
101
+ | RevalidateItem
102
+ | ParallelItem
103
+ | InterceptItem
104
+ | LoaderItem
105
+ | LoadingItem
106
+ | ErrorBoundaryItem
107
+ | NotFoundBoundaryItem
108
+ | CacheItem;
109
+ export type LayoutUseItem =
110
+ | LayoutItem
111
+ | RouteItem
112
+ | MiddlewareItem
113
+ | RevalidateItem
114
+ | ParallelItem
115
+ | InterceptItem
116
+ | LoaderItem
117
+ | LoadingItem
118
+ | ErrorBoundaryItem
119
+ | NotFoundBoundaryItem
120
+ | CacheItem;
121
+ export type RouteUseItem =
122
+ | LayoutItem
123
+ | ParallelItem
124
+ | InterceptItem
125
+ | MiddlewareItem
126
+ | RevalidateItem
127
+ | LoaderItem
128
+ | LoadingItem
129
+ | ErrorBoundaryItem
130
+ | NotFoundBoundaryItem
131
+ | CacheItem;
132
+ export type ParallelUseItem =
133
+ | RevalidateItem
134
+ | LoaderItem
135
+ | LoadingItem
136
+ | ErrorBoundaryItem
137
+ | NotFoundBoundaryItem;
138
+ export type InterceptUseItem =
139
+ | MiddlewareItem
140
+ | RevalidateItem
141
+ | LoaderItem
142
+ | LoadingItem
143
+ | ErrorBoundaryItem
144
+ | NotFoundBoundaryItem
145
+ | LayoutItem
146
+ | RouteItem
147
+ | WhenItem;
148
+ export type LoaderUseItem = RevalidateItem | CacheItem;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Client-safe route utilities
3
+ *
4
+ * These utilities can be imported in client code without pulling in server dependencies.
5
+ */
6
+
7
+ import type { RouteDefinition, RouteConfig, ResolvedRouteMap, TrailingSlashMode } from "./types.js";
8
+
9
+ /**
10
+ * Check if a value is a RouteConfig object
11
+ */
12
+ function isRouteConfig(value: unknown): value is RouteConfig {
13
+ return (
14
+ typeof value === "object" &&
15
+ value !== null &&
16
+ "path" in value &&
17
+ typeof (value as RouteConfig).path === "string"
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Flatten nested route definitions
23
+ */
24
+ function flattenRoutes(
25
+ routes: RouteDefinition,
26
+ prefix: string,
27
+ trailingSlashConfig: Record<string, TrailingSlashMode>
28
+ ): Record<string, string> {
29
+ const flattened: Record<string, string> = {};
30
+
31
+ for (const [key, value] of Object.entries(routes)) {
32
+ const fullKey = prefix + key;
33
+ if (typeof value === "string") {
34
+ // Direct route pattern - include prefix
35
+ flattened[fullKey] = value;
36
+ } else if (isRouteConfig(value)) {
37
+ // Route config object - extract path and trailing slash config
38
+ flattened[fullKey] = value.path;
39
+ if (value.trailingSlash) {
40
+ trailingSlashConfig[fullKey] = value.trailingSlash;
41
+ }
42
+ } else {
43
+ // Nested routes - flatten recursively
44
+ const nested = flattenRoutes(value, `${fullKey}.`, trailingSlashConfig);
45
+ Object.assign(flattened, nested);
46
+ }
47
+ }
48
+
49
+ return flattened;
50
+ }
51
+
52
+ /**
53
+ * Define routes with type safety (client-safe version)
54
+ *
55
+ * This is a client-safe version of the route function that can be imported
56
+ * in client code without pulling in server dependencies.
57
+ *
58
+ * @param input - Route definition object
59
+ * @returns Flattened route map with full type information
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const shopRoutes = route({
64
+ * index: "/",
65
+ * cart: "/cart",
66
+ * products: {
67
+ * detail: "/product/:slug",
68
+ * category: "/products/:category",
69
+ * },
70
+ * });
71
+ * // Result: { index: "/", cart: "/cart", "products.detail": "/product/:slug", "products.category": "/products/:category" }
72
+ * ```
73
+ */
74
+ export function route<const T extends RouteDefinition>(
75
+ input: T
76
+ ): ResolvedRouteMap<T> {
77
+ const trailingSlash: Record<string, TrailingSlashMode> = {};
78
+ const routes = flattenRoutes(input as RouteDefinition, "", trailingSlash);
79
+
80
+ const result = routes as ResolvedRouteMap<T> & { __trailingSlash?: Record<string, TrailingSlashMode> };
81
+ if (Object.keys(trailingSlash).length > 0) {
82
+ Object.defineProperty(result, "__trailingSlash", {
83
+ value: trailingSlash,
84
+ enumerable: false,
85
+ writable: false,
86
+ });
87
+ }
88
+ return result;
89
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createPipelineState } from "../match-context";
3
+
4
+ describe("match-context", () => {
5
+ describe("createPipelineState()", () => {
6
+ it("should create initial state with default values", () => {
7
+ const state = createPipelineState();
8
+
9
+ expect(state.cacheHit).toBe(false);
10
+ expect(state.segments).toEqual([]);
11
+ expect(state.matchedIds).toEqual([]);
12
+ expect(state.interceptSegments).toEqual([]);
13
+ expect(state.slots).toEqual({});
14
+ });
15
+
16
+ it("should create state without cached data by default", () => {
17
+ const state = createPipelineState();
18
+
19
+ expect(state.cachedSegments).toBeUndefined();
20
+ expect(state.cachedMatchedIds).toBeUndefined();
21
+ expect(state.shouldRevalidate).toBeUndefined();
22
+ });
23
+
24
+ it("should allow mutation of state properties", () => {
25
+ const state = createPipelineState();
26
+
27
+ state.cacheHit = true;
28
+ state.segments.push({
29
+ id: "seg1",
30
+ type: "route",
31
+ component: null,
32
+ params: {},
33
+ });
34
+ state.matchedIds.push("seg1");
35
+ state.slots["@modal"] = { active: true, segments: [] };
36
+
37
+ expect(state.cacheHit).toBe(true);
38
+ expect(state.segments).toHaveLength(1);
39
+ expect(state.matchedIds).toContain("seg1");
40
+ expect(state.slots["@modal"]).toBeDefined();
41
+ });
42
+
43
+ it("should create independent state instances", () => {
44
+ const state1 = createPipelineState();
45
+ const state2 = createPipelineState();
46
+
47
+ state1.cacheHit = true;
48
+ state1.segments.push({
49
+ id: "seg1",
50
+ type: "route",
51
+ component: null,
52
+ params: {},
53
+ });
54
+
55
+ expect(state2.cacheHit).toBe(false);
56
+ expect(state2.segments).toHaveLength(0);
57
+ });
58
+
59
+ it("should support setting cache-related properties", () => {
60
+ const state = createPipelineState();
61
+
62
+ state.cacheHit = true;
63
+ state.shouldRevalidate = true;
64
+ state.cachedSegments = [
65
+ { id: "cached1", type: "route", component: "CachedComponent", params: {} },
66
+ ];
67
+ state.cachedMatchedIds = ["cached1"];
68
+
69
+ expect(state.cacheHit).toBe(true);
70
+ expect(state.shouldRevalidate).toBe(true);
71
+ expect(state.cachedSegments).toHaveLength(1);
72
+ expect(state.cachedMatchedIds).toContain("cached1");
73
+ });
74
+
75
+ it("should support intercept segments", () => {
76
+ const state = createPipelineState();
77
+
78
+ state.interceptSegments.push({
79
+ id: "modal-seg",
80
+ type: "route",
81
+ component: "ModalComponent",
82
+ params: {},
83
+ });
84
+
85
+ expect(state.interceptSegments).toHaveLength(1);
86
+ expect(state.interceptSegments[0].id).toBe("modal-seg");
87
+ });
88
+
89
+ it("should support slots with nested segments", () => {
90
+ const state = createPipelineState();
91
+
92
+ state.slots["@modal"] = {
93
+ active: true,
94
+ segments: [
95
+ { id: "modal1", type: "route", component: "Modal1", params: {} },
96
+ { id: "modal2", type: "route", component: "Modal2", params: {} },
97
+ ],
98
+ };
99
+
100
+ expect(state.slots["@modal"].active).toBe(true);
101
+ expect(state.slots["@modal"].segments).toHaveLength(2);
102
+ });
103
+ });
104
+ });