@octanejs/remix-router 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/package.json +53 -0
  4. package/src/dom.ts +16 -0
  5. package/src/index.ts +267 -0
  6. package/src/internal.ts +37 -0
  7. package/src/lib/Await.tsrx +145 -0
  8. package/src/lib/Await.tsrx.d.ts +6 -0
  9. package/src/lib/DefaultErrorComponent.tsrx +47 -0
  10. package/src/lib/DefaultErrorComponent.tsrx.d.ts +2 -0
  11. package/src/lib/RenderErrorBoundary.tsrx +117 -0
  12. package/src/lib/RenderErrorBoundary.tsrx.d.ts +14 -0
  13. package/src/lib/actions.ts +90 -0
  14. package/src/lib/components/MemoryRouter.tsrx +42 -0
  15. package/src/lib/components/MemoryRouter.tsrx.d.ts +10 -0
  16. package/src/lib/components/Navigate.ts +60 -0
  17. package/src/lib/components/Router.tsrx +88 -0
  18. package/src/lib/components/Router.tsrx.d.ts +13 -0
  19. package/src/lib/components/RouterProvider.tsrx +320 -0
  20. package/src/lib/components/RouterProvider.tsrx.d.ts +21 -0
  21. package/src/lib/components/Routes.tsrx +53 -0
  22. package/src/lib/components/Routes.tsrx.d.ts +7 -0
  23. package/src/lib/components/routes-collector.ts +317 -0
  24. package/src/lib/components/utils.ts +171 -0
  25. package/src/lib/components/with-props.ts +72 -0
  26. package/src/lib/context.ts +129 -0
  27. package/src/lib/dom/Form.tsrx +76 -0
  28. package/src/lib/dom/Form.tsrx.d.ts +22 -0
  29. package/src/lib/dom/Link.tsrx +91 -0
  30. package/src/lib/dom/Link.tsrx.d.ts +22 -0
  31. package/src/lib/dom/NavLink.tsrx +118 -0
  32. package/src/lib/dom/NavLink.tsrx.d.ts +33 -0
  33. package/src/lib/dom/dom.ts +344 -0
  34. package/src/lib/dom/hooks.ts +93 -0
  35. package/src/lib/dom/lib.ts +822 -0
  36. package/src/lib/dom/routers.tsrx +104 -0
  37. package/src/lib/dom/routers.tsrx.d.ts +23 -0
  38. package/src/lib/dom/server.tsrx +350 -0
  39. package/src/lib/dom/server.tsrx.d.ts +25 -0
  40. package/src/lib/errors.ts +84 -0
  41. package/src/lib/framework-stubs.ts +103 -0
  42. package/src/lib/hooks.ts +1797 -0
  43. package/src/lib/href.ts +71 -0
  44. package/src/lib/react-types.ts +10 -0
  45. package/src/lib/router/history.ts +763 -0
  46. package/src/lib/router/instrumentation.ts +496 -0
  47. package/src/lib/router/links.ts +192 -0
  48. package/src/lib/router/router.ts +7165 -0
  49. package/src/lib/router/server-runtime-types.ts +12 -0
  50. package/src/lib/router/url.ts +8 -0
  51. package/src/lib/router/utils.ts +2179 -0
  52. package/src/lib/server-runtime/cookies.ts +240 -0
  53. package/src/lib/server-runtime/crypto.ts +55 -0
  54. package/src/lib/server-runtime/mode.ts +16 -0
  55. package/src/lib/server-runtime/sessions/cookieStorage.ts +54 -0
  56. package/src/lib/server-runtime/sessions/memoryStorage.ts +59 -0
  57. package/src/lib/server-runtime/sessions.ts +291 -0
  58. package/src/lib/server-runtime/warnings.ts +10 -0
  59. package/src/lib/types/future.ts +16 -0
  60. package/src/lib/types/params.ts +8 -0
  61. package/src/lib/types/register.ts +39 -0
  62. package/src/lib/types/route-module.ts +17 -0
  63. package/src/lib/types/utils.ts +37 -0
@@ -0,0 +1,320 @@
1
+ // RouterProvider + DataRoutes + Outlet — transcribed from react-router@7.18.1
2
+ // lib/components.tsx onto octane. Structure preserved verbatim: useState for
3
+ // router state + layout-effect subscription (early enough for render-driven
4
+ // redirects; subscribe() replays a pre-attach notification), useOptimistic for
5
+ // the optimistic in-flight state, and the three commit paths — plain
6
+ // startTransition, flushSync + startViewTransition, and startTransition INSIDE
7
+ // the deferred startViewTransition callback (which is exactly what octane's
8
+ // transition-at-commit constraint requires — see tanstack-router's
9
+ // src/router.ts:19-53 for the background). jsdom has no startViewTransition,
10
+ // so the VT paths are dormant under tests until Phase E exercises them.
11
+ //
12
+ // Octane substitutions: hooks from 'octane' (useOptimistic exists — upstream's
13
+ // useOptimisticSafe feature-detection collapses to a direct call), memo() for
14
+ // MemoizedDataRoutes, and the RSC branch dropped (RSCRouterContext is
15
+ // hardwired false in this port).
16
+ import {
17
+ memo,
18
+ startTransition,
19
+ useCallback,
20
+ useEffect,
21
+ useLayoutEffect,
22
+ useMemo,
23
+ useOptimistic,
24
+ useRef,
25
+ useState,
26
+ } from 'octane';
27
+ import {
28
+ DataRouterContext,
29
+ DataRouterStateContext,
30
+ FetchersContext,
31
+ ViewTransitionContext,
32
+ } from '../context.ts';
33
+ import { getRoutePattern } from '../router/utils.ts';
34
+ import { useOutlet, useRoutesImpl } from '../hooks.ts';
35
+ import { Router } from './Router.tsrx';
36
+ import { Deferred, getOptimisticRouterState, warnOnce } from './utils.ts';
37
+
38
+ export function Outlet(props) {
39
+ return useOutlet(props.context);
40
+ }
41
+
42
+ export function DataRoutes(props) {
43
+ return useRoutesImpl(props.routes, undefined, {
44
+ manifest: props.manifest,
45
+ state: props.state,
46
+ isStatic: props.isStatic,
47
+ onError: props.onError,
48
+ future: props.future,
49
+ });
50
+ }
51
+
52
+ // Memoize to avoid re-renders when updating `ViewTransitionContext`
53
+ const MemoizedDataRoutes = memo(DataRoutes);
54
+
55
+ export function RouterProvider(props) @{
56
+ const { router, flushSync: reactDomFlushSyncImpl, onError, useTransitions } = props;
57
+
58
+ const [_state, setStateImpl] = useState(router.state);
59
+ const [state, setOptimisticState] = useOptimistic(_state);
60
+ const [pendingState, setPendingState] = useState(undefined);
61
+ const [vtContext, setVtContext] = useState({ isTransitioning: false });
62
+ const [renderDfd, setRenderDfd] = useState(undefined);
63
+ const [transition, setTransition] = useState(undefined);
64
+ const [interruption, setInterruption] = useState(undefined);
65
+ const fetcherData = useRef(new Map());
66
+
67
+ const setState = useCallback((
68
+ newState,
69
+ { deletedFetchers, newErrors, flushSync, viewTransitionOpts },
70
+ ) => {
71
+ // Send router errors through onError
72
+ if (newErrors && onError) {
73
+ Object.values(newErrors).forEach(
74
+ (error) => onError(error, {
75
+ location: newState.location,
76
+ params: newState.matches[0]?.params ?? {},
77
+ pattern: getRoutePattern(newState.matches),
78
+ }),
79
+ );
80
+ }
81
+
82
+ newState.fetchers.forEach((fetcher, key) => {
83
+ if (fetcher.data !== undefined) {
84
+ fetcherData.current.set(key, fetcher.data);
85
+ }
86
+ });
87
+ deletedFetchers.forEach((key) => fetcherData.current.delete(key));
88
+
89
+ warnOnce(
90
+ flushSync === false || reactDomFlushSyncImpl != null,
91
+ 'You provided the `flushSync` option to a router update, ' +
92
+ 'but you are not using the `<RouterProvider>` from `@octanejs/remix-router/dom` ' +
93
+ 'so `flushSync()` is unavailable. Please update your app ' +
94
+ 'to `import { RouterProvider } from "@octanejs/remix-router/dom"` ' +
95
+ 'to use the `flushSync` option.',
96
+ );
97
+
98
+ const isViewTransitionAvailable =
99
+ router.window != null && router.window.document != null &&
100
+ typeof router.window.document.startViewTransition === 'function';
101
+
102
+ warnOnce(
103
+ viewTransitionOpts == null || isViewTransitionAvailable,
104
+ 'You provided the `viewTransition` option to a router update, ' +
105
+ 'but you do not appear to be running in a DOM environment as ' +
106
+ '`window.startViewTransition` is not available.',
107
+ );
108
+
109
+ // If this isn't a view transition or it's not available in this browser,
110
+ // just update and be done with it
111
+ if (!viewTransitionOpts || !isViewTransitionAvailable) {
112
+ if (reactDomFlushSyncImpl && flushSync) {
113
+ reactDomFlushSyncImpl(() => setStateImpl(newState));
114
+ } else if (useTransitions === false) {
115
+ setStateImpl(newState);
116
+ } else {
117
+ startTransition(() => {
118
+ if (useTransitions === true) {
119
+ setOptimisticState((s) => getOptimisticRouterState(s, newState));
120
+ }
121
+ setStateImpl(newState);
122
+ });
123
+ }
124
+ return;
125
+ }
126
+
127
+ // flushSync + startViewTransition
128
+ if (reactDomFlushSyncImpl && flushSync) {
129
+ // Flush through the context to mark DOM elements as transition=ing
130
+ reactDomFlushSyncImpl(() => {
131
+ // Cancel any pending transitions
132
+ if (transition) {
133
+ renderDfd?.resolve();
134
+ transition.skipTransition();
135
+ }
136
+ setVtContext({
137
+ isTransitioning: true,
138
+ flushSync: true,
139
+ currentLocation: viewTransitionOpts.currentLocation,
140
+ nextLocation: viewTransitionOpts.nextLocation,
141
+ });
142
+ });
143
+
144
+ // Update the DOM
145
+ const t = router.window.document.startViewTransition(() => {
146
+ reactDomFlushSyncImpl(() => setStateImpl(newState));
147
+ });
148
+
149
+ // Clean up after the animation completes
150
+ t.finished.finally(() => {
151
+ reactDomFlushSyncImpl(() => {
152
+ setRenderDfd(undefined);
153
+ setTransition(undefined);
154
+ setPendingState(undefined);
155
+ setVtContext({ isTransitioning: false });
156
+ });
157
+ });
158
+
159
+ reactDomFlushSyncImpl(() => setTransition(t));
160
+ return;
161
+ }
162
+
163
+ // startTransition + startViewTransition
164
+ if (transition) {
165
+ // Interrupting an in-progress transition, cancel and let everything flush
166
+ // out, and then kick off a new transition from the interruption state
167
+ renderDfd?.resolve();
168
+ transition.skipTransition();
169
+ setInterruption({
170
+ state: newState,
171
+ currentLocation: viewTransitionOpts.currentLocation,
172
+ nextLocation: viewTransitionOpts.nextLocation,
173
+ });
174
+ } else {
175
+ // Completed navigation update with opted-in view transitions, let 'er rip
176
+ setPendingState(newState);
177
+ setVtContext({
178
+ isTransitioning: true,
179
+ flushSync: false,
180
+ currentLocation: viewTransitionOpts.currentLocation,
181
+ nextLocation: viewTransitionOpts.nextLocation,
182
+ });
183
+ }
184
+ }, [
185
+ router.window,
186
+ reactDomFlushSyncImpl,
187
+ transition,
188
+ renderDfd,
189
+ useTransitions,
190
+ setOptimisticState,
191
+ onError,
192
+ ]);
193
+
194
+ // Need to use a layout effect here so we are subscribed early enough to
195
+ // pick up on any render-driven redirects/navigations (useEffect/<Navigate>).
196
+ // If the router finished initializing (and emitted errors) before this
197
+ // subscriber was attached, `subscribe()` replays that notification so
198
+ // `onError` still fires for initial data load errors.
199
+ useLayoutEffect(() => router.subscribe(setState), [router, setState]);
200
+
201
+ // When we start a view transition, create a Deferred we can use for the
202
+ // eventual "completed" render
203
+ useEffect(() => {
204
+ if (vtContext.isTransitioning && !vtContext.flushSync) {
205
+ setRenderDfd(new Deferred());
206
+ }
207
+ }, [vtContext]);
208
+
209
+ // Once the deferred is created, kick off startViewTransition() to update the
210
+ // DOM and then wait on the Deferred to resolve (indicating the DOM update has
211
+ // happened)
212
+ useEffect(() => {
213
+ if (renderDfd && pendingState && router.window) {
214
+ const newState = pendingState;
215
+ const renderPromise = renderDfd.promise;
216
+ const t = router.window.document.startViewTransition(async () => {
217
+ if (useTransitions === false) {
218
+ setStateImpl(newState);
219
+ } else {
220
+ startTransition(() => {
221
+ if (useTransitions === true) {
222
+ setOptimisticState((s) => getOptimisticRouterState(s, newState));
223
+ }
224
+ setStateImpl(newState);
225
+ });
226
+ }
227
+ await renderPromise;
228
+ });
229
+ t.finished.finally(() => {
230
+ setRenderDfd(undefined);
231
+ setTransition(undefined);
232
+ setPendingState(undefined);
233
+ setVtContext({ isTransitioning: false });
234
+ });
235
+ setTransition(t);
236
+ }
237
+ }, [pendingState, renderDfd, router.window, useTransitions, setOptimisticState]);
238
+
239
+ // When the new location finally renders and is committed to the DOM, this
240
+ // effect will run to resolve the transition
241
+ useEffect(() => {
242
+ if (renderDfd && pendingState && state.location.key === pendingState.location.key) {
243
+ renderDfd.resolve();
244
+ }
245
+ }, [renderDfd, transition, state.location, pendingState]);
246
+
247
+ // If we get interrupted with a new navigation during a transition, we skip
248
+ // the active transition, let it cleanup, then kick it off again here
249
+ useEffect(() => {
250
+ if (!vtContext.isTransitioning && interruption) {
251
+ setPendingState(interruption.state);
252
+ setVtContext({
253
+ isTransitioning: true,
254
+ flushSync: false,
255
+ currentLocation: interruption.currentLocation,
256
+ nextLocation: interruption.nextLocation,
257
+ });
258
+ setInterruption(undefined);
259
+ }
260
+ }, [vtContext.isTransitioning, interruption]);
261
+
262
+ const navigator = useMemo(() => {
263
+ return {
264
+ createHref: router.createHref,
265
+ encodeLocation: router.encodeLocation,
266
+ go: (n) => router.navigate(n),
267
+ push: (to, state2, opts) => router.navigate(to, {
268
+ state: state2,
269
+ preventScrollReset: opts?.preventScrollReset,
270
+ }),
271
+ replace: (to, state2, opts) => router.navigate(to, {
272
+ replace: true,
273
+ state: state2,
274
+ preventScrollReset: opts?.preventScrollReset,
275
+ }),
276
+ };
277
+ }, [router]);
278
+
279
+ const basename = router.basename || '/';
280
+
281
+ const dataRouterContext = useMemo(
282
+ () => ({
283
+ router,
284
+ navigator,
285
+ static: false,
286
+ basename,
287
+ onError,
288
+ }),
289
+ [router, navigator, basename, onError],
290
+ );
291
+
292
+ // The fragment shape mirrors upstream (kept for useId stability parity with
293
+ // the server-rendered StaticRouterProvider in a later phase).
294
+ <>
295
+ <DataRouterContext.Provider value={dataRouterContext}>
296
+ <DataRouterStateContext.Provider value={state}>
297
+ <FetchersContext.Provider value={fetcherData.current}>
298
+ <ViewTransitionContext.Provider value={vtContext}>
299
+ <Router
300
+ basename={basename}
301
+ location={state.location}
302
+ navigationType={state.historyAction}
303
+ navigator={navigator}
304
+ useTransitions={useTransitions}
305
+ >
306
+ <MemoizedDataRoutes
307
+ routes={router.routes}
308
+ manifest={router.manifest}
309
+ future={router.future}
310
+ state={state}
311
+ isStatic={false}
312
+ onError={onError}
313
+ />
314
+ </Router>
315
+ </ViewTransitionContext.Provider>
316
+ </FetchersContext.Provider>
317
+ </DataRouterStateContext.Provider>
318
+ </DataRouterContext.Provider>
319
+ </>
320
+ }
@@ -0,0 +1,21 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { Router as DataRouter } from '../router/router';
3
+ import type { ClientOnErrorFunction } from '../context';
4
+
5
+ export declare const RouterProvider: (props: {
6
+ router: DataRouter;
7
+ flushSync?: (fn: () => unknown) => undefined;
8
+ onError?: ClientOnErrorFunction;
9
+ useTransitions?: boolean;
10
+ }) => unknown;
11
+
12
+ export declare const Outlet: (props: { context?: unknown }) => unknown;
13
+
14
+ export declare const DataRoutes: (props: {
15
+ routes: any[];
16
+ manifest: any;
17
+ future: any;
18
+ state: any;
19
+ isStatic: boolean;
20
+ onError?: ClientOnErrorFunction;
21
+ }) => unknown;
@@ -0,0 +1,53 @@
1
+ // Routes — transcribed from react-router@7.18.1 lib/components.tsx, hybrid
2
+ // per docs/remix-router-port-plan.md §2a:
3
+ // - descriptor children (createRoutesFromElements results, `{expr}` arrays,
4
+ // prop-position elements) go through createRoutesFromChildren verbatim;
5
+ // - the natural `.tsrx` block-children form (`<Routes><Route/></Routes>`
6
+ // compiles children to an opaque render block) goes through the
7
+ // registration collector (see routes-collector.ts): the block renders
8
+ // invisibly under the collector context, each <Route> registers, and the
9
+ // collected config renders through useRoutes on the next pre-paint pass.
10
+ import { isChildrenBlock, useMemo, useRef, useState } from 'octane';
11
+ import { useRoutes } from '../hooks.ts';
12
+ import {
13
+ RoutesCollectorContext,
14
+ createCollector,
15
+ createRoutesFromChildren,
16
+ } from './routes-collector.ts';
17
+
18
+ export function Routes(props) @{
19
+ @if (props.children != null && isChildrenBlock(props.children)) {
20
+ <BlockRoutes children={props.children} location={props.location} />
21
+ } @else {
22
+ <ElementRoutes children={props.children} location={props.location} />
23
+ }
24
+ }
25
+
26
+ function ElementRoutes(props) {
27
+ return useRoutes(createRoutesFromChildren(props.children), props.location);
28
+ }
29
+
30
+ function CollectedRoutes(props) {
31
+ return useRoutes(props.routes, props.location);
32
+ }
33
+
34
+ function BlockRoutes(props) @{
35
+ const [version, setVersion] = useState(0);
36
+ const collectorRef = useRef(null);
37
+ if (collectorRef.current === null) {
38
+ collectorRef.current = createCollector(() => setVersion((v) => v + 1));
39
+ }
40
+ // Registrations land in the children's layout effects (pre-paint); the
41
+ // version bump re-renders with the finalized config, so the first paint
42
+ // already shows the matched route.
43
+ const routes = useMemo(() => collectorRef.current.collect(), [version]);
44
+
45
+ <>
46
+ <RoutesCollectorContext.Provider
47
+ value={collectorRef.current}
48
+ >{props.children}</RoutesCollectorContext.Provider>
49
+ @if (version > 0) {
50
+ <CollectedRoutes routes={routes} location={props.location} />
51
+ }
52
+ </>
53
+ }
@@ -0,0 +1,7 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { Location } from '../router/history';
3
+
4
+ export declare const Routes: (props: {
5
+ children?: unknown;
6
+ location?: Partial<Location> | string;
7
+ }) => unknown;