@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,47 @@
1
+ // DefaultErrorComponent — port of react-router@7.18.1 lib/hooks.tsx's default
2
+ // error UI. Markup-identical (differential fixtures still avoid it — the dev
3
+ // stack trace can never byte-match across runtimes).
4
+ import { useContext } from 'octane';
5
+ import { RouteErrorContext, ENABLE_DEV_WARNINGS } from './context.ts';
6
+ import { isRouteErrorResponse } from './router/utils.ts';
7
+
8
+ export function DefaultErrorComponent() @{
9
+ const error = useContext(RouteErrorContext);
10
+ const message =
11
+ isRouteErrorResponse(error)
12
+ ? error.status + ' ' + error.statusText
13
+ : error instanceof Error
14
+ ? error.message
15
+ : JSON.stringify(error);
16
+ const stack =
17
+ error instanceof Error ? error.stack : null;
18
+ const lightgrey = 'rgba(200,200,200, 0.5)';
19
+
20
+ if (ENABLE_DEV_WARNINGS) {
21
+ console.error('Error handled by React Router default ErrorBoundary:', error);
22
+ }
23
+
24
+ <>
25
+ <h2>Unexpected Application Error!</h2>
26
+ <h3 style={{ fontStyle: 'italic' }}>
27
+ {message as string}
28
+ </h3>
29
+ @if (stack) {
30
+ <pre style={{ padding: '0.5rem', backgroundColor: lightgrey }}>
31
+ {stack as string}
32
+ </pre>
33
+ }
34
+ @if (ENABLE_DEV_WARNINGS) {
35
+ <>
36
+ <p>💿 Hey developer 👋</p>
37
+ <p>
38
+ {'You can provide a way better UX than this when your app throws errors by providing your own '}
39
+ <code style={{ padding: '2px 4px', backgroundColor: lightgrey }}>ErrorBoundary</code>
40
+ {' or '}
41
+ <code style={{ padding: '2px 4px', backgroundColor: lightgrey }}>errorElement</code>
42
+ {' prop on your route.'}
43
+ </p>
44
+ </>
45
+ }
46
+ </>
47
+ }
@@ -0,0 +1,2 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ export declare const DefaultErrorComponent: (props?: {}) => unknown;
@@ -0,0 +1,117 @@
1
+ // RenderErrorBoundary — port of the class boundary in react-router@7.18.1
2
+ // lib/hooks.tsx onto octane's @try/@catch (octane has no class components).
3
+ // RSC digest branches are dropped (RSCRouterContext is hardwired false here).
4
+ //
5
+ // Upstream semantics reproduced:
6
+ // - getDerivedStateFromProps: the retained error resets when the location
7
+ // changes or a revalidation returns to "idle"; otherwise a NEW props.error
8
+ // surfaces while a stale one is retained even if app state cleared it.
9
+ // - getDerivedStateFromError: render errors thrown below become the error.
10
+ // - componentDidCatch → onError (or the upstream console.error) once per
11
+ // distinct caught error, DURING the catch render — so a rethrow escapes to
12
+ // the parent boundary.
13
+ // - Render: error !== undefined → RouteContext(routeContext) →
14
+ // RouteErrorContext(error) → component; else children.
15
+ //
16
+ // Octane mapping: DATA errors (props.error, from router state.errors) never
17
+ // throw — the error branch renders directly. RENDER errors land in @catch;
18
+ // the catch view applies the same precedence (props.error wins) and leaves
19
+ // the catch state via reset() one commit after the location/revalidation
20
+ // derivation says the error cleared (documented divergence: upstream clears
21
+ // in a render-phase derivation; same observable outcome).
22
+ import { useRef, useLayoutEffect } from 'octane';
23
+ import { RouteContext, RouteErrorContext } from './context.ts';
24
+
25
+ export function RenderErrorBoundary(props) @{
26
+ // getDerivedStateFromProps, run during render (pure derivation).
27
+ const state = useRef({
28
+ location: props.location,
29
+ revalidation: props.revalidation,
30
+ error: props.error,
31
+ });
32
+ {
33
+ const s = state.current;
34
+ if (
35
+ s.location !== props.location || s.revalidation !== 'idle' && props.revalidation === 'idle'
36
+ ) {
37
+ state.current = {
38
+ error: props.error,
39
+ location: props.location,
40
+ revalidation: props.revalidation,
41
+ };
42
+ } else {
43
+ state.current = {
44
+ error: props.error !== undefined ? props.error : s.error,
45
+ location: s.location,
46
+ revalidation: props.revalidation || s.revalidation,
47
+ };
48
+ }
49
+ }
50
+
51
+ @try {
52
+ @if (state.current.error !== undefined) {
53
+ <RouteContext.Provider value={props.routeContext}>
54
+ <RouteErrorContext.Provider
55
+ value={state.current.error}
56
+ >{props.component}</RouteErrorContext.Provider>
57
+ </RouteContext.Provider>
58
+ } @else {
59
+ <>
60
+ {props.children}
61
+ </>
62
+ }
63
+ } @catch (caught, reset) {
64
+ <CaughtErrorView
65
+ caught={caught}
66
+ reset={reset}
67
+ error={props.error}
68
+ location={props.location}
69
+ revalidation={props.revalidation}
70
+ routeContext={props.routeContext}
71
+ component={props.component}
72
+ onError={props.onError}
73
+ />
74
+ }
75
+ }
76
+
77
+ function CaughtErrorView(props) @{
78
+ // componentDidCatch parity — once per distinct caught error, during render
79
+ // so a rethrowing onError escapes to the parent boundary.
80
+ const reported = useRef(null);
81
+ if (reported.current !== props.caught) {
82
+ reported.current = props.caught;
83
+ if (props.onError) {
84
+ props.onError(props.caught, { componentStack: '' });
85
+ } else {
86
+ console.error('React Router caught the following error during render', props.caught);
87
+ }
88
+ }
89
+
90
+ // Error precedence per upstream derivation: a defined props.error (data
91
+ // error for this location) wins over the caught render error.
92
+ const error =
93
+ props.error !== undefined ? props.error : props.caught;
94
+
95
+ // Leave the catch state when the location changes or a revalidation
96
+ // completes — the octane analogue of upstream's render-phase reset.
97
+ // `location` stays pinned at the catch baseline (upstream's state.location
98
+ // only moves on reset), but `revalidation` must TRACK every render the way
99
+ // upstream's derived state does (`props.revalidation || state.revalidation`)
100
+ // — a render error is usually caught while revalidation is already "idle",
101
+ // and the recovery signal is the SUBSEQUENT loading → idle transition, not
102
+ // a transition relative to the frozen catch-time value.
103
+ const baseline = useRef({ location: props.location, revalidation: props.revalidation });
104
+ useLayoutEffect(() => {
105
+ const b = baseline.current;
106
+ const shouldReset =
107
+ b.location !== props.location || b.revalidation !== 'idle' && props.revalidation === 'idle';
108
+ b.revalidation = props.revalidation || b.revalidation;
109
+ if (shouldReset) {
110
+ props.reset();
111
+ }
112
+ }, [props.location, props.revalidation]);
113
+
114
+ <RouteContext.Provider value={props.routeContext}>
115
+ <RouteErrorContext.Provider value={error}>{props.component}</RouteErrorContext.Provider>
116
+ </RouteContext.Provider>
117
+ }
@@ -0,0 +1,14 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { Location } from './router/history';
3
+ import type { RevalidationState } from './router/router';
4
+ import type { RouteContextObject } from './context';
5
+
6
+ export declare const RenderErrorBoundary: (props: {
7
+ location: Location;
8
+ revalidation: RevalidationState;
9
+ error: any;
10
+ component: unknown;
11
+ routeContext: RouteContextObject;
12
+ onError?: (error: unknown, errorInfo?: unknown) => void;
13
+ children?: unknown;
14
+ }) => unknown;
@@ -0,0 +1,90 @@
1
+ // Vendored from react-router@7.18.1 packages/react-router/lib/actions.ts — unmodified.
2
+ // Re-vendor with `node scripts/vendor-remix-router.mjs`; never hand-edit.
3
+ export function throwIfPotentialCSRFAttack(
4
+ request: Request,
5
+ allowedActionOrigins: string[] | undefined,
6
+ ) {
7
+ let originHeader = request.headers.get('origin');
8
+ let originDomain: string | null = null;
9
+
10
+ try {
11
+ originDomain =
12
+ typeof originHeader === 'string' && originHeader !== 'null'
13
+ ? new URL(originHeader).host
14
+ : originHeader;
15
+ } catch {
16
+ throw new Error(`\`origin\` header is not a valid URL. Aborting the action.`);
17
+ }
18
+ let host = new URL(request.url).host;
19
+
20
+ if (originDomain && originDomain !== host) {
21
+ if (!isAllowedOrigin(originDomain, allowedActionOrigins)) {
22
+ // This seems to be an CSRF attack. We should not proceed with the action.
23
+ throw new Error(
24
+ 'The `request.url` host does not match `origin` header from a forwarded ' +
25
+ 'action request. Aborting the action.',
26
+ );
27
+ }
28
+ }
29
+ }
30
+
31
+ // Implementation of micromatch by Next.js https://github.com/vercel/next.js/blob/ea927b583d24f42e538001bf13370e38c91d17bf/packages/next/src/server/app-render/csrf-protection.ts#L6
32
+ function matchWildcardDomain(domain: string, pattern: string) {
33
+ const domainParts = domain.split('.');
34
+ const patternParts = pattern.split('.');
35
+
36
+ if (patternParts.length < 1) {
37
+ // pattern is empty and therefore invalid to match against
38
+ return false;
39
+ }
40
+
41
+ if (domainParts.length < patternParts.length) {
42
+ // domain has too few segments and thus cannot match
43
+ return false;
44
+ }
45
+
46
+ while (patternParts.length) {
47
+ const patternPart = patternParts.pop();
48
+ const domainPart = domainParts.pop();
49
+
50
+ switch (patternPart) {
51
+ case '': {
52
+ // invalid pattern. pattern segments must be non empty
53
+ return false;
54
+ }
55
+ case '*': {
56
+ // wildcard matches anything so we continue if the domain part is non-empty
57
+ if (domainPart) {
58
+ continue;
59
+ } else {
60
+ return false;
61
+ }
62
+ }
63
+ case '**': {
64
+ // if this is not the last item in the pattern the pattern is invalid
65
+ if (patternParts.length > 0) {
66
+ return false;
67
+ }
68
+ // recursive wildcard matches anything so we terminate here if the domain part is non empty
69
+ return domainPart !== undefined;
70
+ }
71
+ case undefined:
72
+ default: {
73
+ if (domainPart !== patternPart) {
74
+ return false;
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ // We exhausted the pattern. If we also exhausted the domain we have a match
81
+ return domainParts.length === 0;
82
+ }
83
+
84
+ function isAllowedOrigin(originDomain: string, allowedActionOrigins: string[] | undefined = []) {
85
+ return allowedActionOrigins.some(
86
+ (allowedOrigin) =>
87
+ allowedOrigin &&
88
+ (allowedOrigin === originDomain || matchWildcardDomain(originDomain, allowedOrigin)),
89
+ );
90
+ }
@@ -0,0 +1,42 @@
1
+ // MemoryRouter — transcribed from react-router@7.18.1 lib/components.tsx.
2
+ // A declarative Router over an in-memory history stack.
3
+ import { startTransition, useCallback, useLayoutEffect, useRef, useState } from 'octane';
4
+ import { createMemoryHistory } from '../router/history.ts';
5
+ import { Router } from './Router.tsrx';
6
+
7
+ export function MemoryRouter(props) @{
8
+ const { basename, children, initialEntries, initialIndex, useTransitions } = props;
9
+
10
+ const historyRef = useRef(null);
11
+ if (historyRef.current == null) {
12
+ historyRef.current = createMemoryHistory({
13
+ initialEntries,
14
+ initialIndex,
15
+ v5Compat: true,
16
+ });
17
+ }
18
+
19
+ const history = historyRef.current;
20
+ const [state, setStateImpl] = useState({
21
+ action: history.action,
22
+ location: history.location,
23
+ });
24
+ const setState = useCallback((newState) => {
25
+ if (useTransitions === false) {
26
+ setStateImpl(newState);
27
+ } else {
28
+ startTransition(() => setStateImpl(newState));
29
+ }
30
+ }, [useTransitions]);
31
+
32
+ useLayoutEffect(() => history.listen(setState), [history, setState]);
33
+
34
+ <Router
35
+ basename={basename}
36
+ location={state.location}
37
+ navigationType={state.action}
38
+ navigator={history}
39
+ useTransitions={useTransitions}
40
+ children={children}
41
+ />
42
+ }
@@ -0,0 +1,10 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { InitialEntry } from '../router/history';
3
+
4
+ export declare const MemoryRouter: (props: {
5
+ basename?: string;
6
+ children?: unknown;
7
+ initialEntries?: InitialEntry[];
8
+ initialIndex?: number;
9
+ useTransitions?: boolean;
10
+ }) => unknown;
@@ -0,0 +1,60 @@
1
+ // Navigate — transcribed from react-router@7.18.1 lib/components.tsx.
2
+ // Renders nothing; navigates from an effect on mount/update. Plain .ts (no
3
+ // JSX needed); the hooks it composes handle a slotless caller via bare-tag
4
+ // sub-slots.
5
+ import { useContext, useEffect } from 'octane';
6
+ import { NavigationContext, RouteContext } from '../context';
7
+ import { invariant, warning } from '../router/history';
8
+ import type { To } from '../router/history';
9
+ import type { RelativeRoutingType } from '../router/router';
10
+ import { getResolveToMatches, resolveTo } from '../router/utils';
11
+ import { useInRouterContext, useLocation, useNavigate } from '../hooks';
12
+
13
+ export interface NavigateProps {
14
+ to: To;
15
+ replace?: boolean;
16
+ state?: any;
17
+ relative?: RelativeRoutingType;
18
+ }
19
+
20
+ /**
21
+ * A component-based version of `useNavigate` to use in a render-prop-less
22
+ * context — navigates to the given `to` value whenever it renders.
23
+ */
24
+ export function Navigate(props: NavigateProps): null {
25
+ const { to, replace, state, relative } = props;
26
+ invariant(
27
+ useInRouterContext(),
28
+ `<Navigate> may be used only in the context of a <Router> component.`,
29
+ );
30
+
31
+ const { static: isStatic } = useContext(NavigationContext);
32
+
33
+ warning(
34
+ !isStatic,
35
+ `<Navigate> must not be used on the initial render in a <StaticRouter>. ` +
36
+ `This is a no-op, but you should modify your code so the <Navigate> is ` +
37
+ `only ever rendered in response to some user interaction or state change.`,
38
+ );
39
+
40
+ const { matches } = useContext(RouteContext);
41
+ const { pathname: locationPathname } = useLocation();
42
+ const navigate = useNavigate() as (to: To, opts?: any) => void;
43
+
44
+ // Resolve the path outside of the effect so repeat effect runs navigate to
45
+ // the same place (upstream: StrictMode double-invoke safety).
46
+ const path = resolveTo(to, getResolveToMatches(matches), locationPathname, relative === 'path');
47
+ const jsonPath = JSON.stringify(path);
48
+
49
+ // Plain-.ts component: hand-passed stable slot (state is keyed per
50
+ // component-instance scope).
51
+ useEffect(
52
+ () => {
53
+ navigate(JSON.parse(jsonPath), { replace, state, relative });
54
+ },
55
+ [navigate, jsonPath, relative, replace, state],
56
+ Symbol.for('rr:navigate:eff') as any,
57
+ );
58
+
59
+ return null;
60
+ }
@@ -0,0 +1,88 @@
1
+ // Router — transcribed from react-router@7.18.1 lib/components.tsx. The
2
+ // low-level context provider every router variant renders through.
3
+ import { useMemo } from 'octane';
4
+ import { LocationContext, NavigationContext } from '../context.ts';
5
+ import { Action as NavigationType, invariant, parsePath, warning } from '../router/history.ts';
6
+ import { stripBasename } from '../router/utils.ts';
7
+ import { useInRouterContext } from '../hooks.ts';
8
+
9
+ export function Router(props) @{
10
+ const {
11
+ basename: basenameProp = '/',
12
+ children = null,
13
+ location: locationPropIn,
14
+ navigationType = NavigationType.Pop,
15
+ navigator,
16
+ static: staticProp = false,
17
+ useTransitions,
18
+ } = props;
19
+
20
+ invariant(
21
+ !useInRouterContext(),
22
+ `You cannot render a <Router> inside another <Router>.` +
23
+ ` You should never have more than one in your app.`,
24
+ );
25
+
26
+ // Preserve trailing slashes on basename, so we can let the user control
27
+ // the enforcement of trailing slashes throughout the app
28
+ const basename = basenameProp.replace(/^\/*/, '/');
29
+ const navigationContext = useMemo(
30
+ () => ({
31
+ basename,
32
+ navigator,
33
+ static: staticProp,
34
+ useTransitions,
35
+ future: {},
36
+ }),
37
+ [basename, navigator, staticProp, useTransitions],
38
+ );
39
+
40
+ let locationProp = locationPropIn;
41
+ if (typeof locationProp === 'string') {
42
+ locationProp = parsePath(locationProp);
43
+ }
44
+
45
+ const {
46
+ pathname = '/',
47
+ search = '',
48
+ hash = '',
49
+ state = null,
50
+ key = 'default',
51
+ mask,
52
+ } = locationProp;
53
+
54
+ const locationContext = useMemo(() => {
55
+ const trailingPathname = stripBasename(pathname, basename);
56
+
57
+ if (trailingPathname == null) {
58
+ return null;
59
+ }
60
+
61
+ return {
62
+ location: {
63
+ pathname: trailingPathname,
64
+ search,
65
+ hash,
66
+ state,
67
+ key,
68
+ mask,
69
+ },
70
+ navigationType,
71
+ };
72
+ }, [basename, pathname, search, hash, state, key, navigationType, mask]);
73
+
74
+ warning(
75
+ locationContext != null,
76
+ `<Router basename="${basename}"> is not able to match the URL ` +
77
+ `"${pathname}${search}${hash}" because it does not start with the ` +
78
+ `basename, so the <Router> won't render anything.`,
79
+ );
80
+
81
+ @if (locationContext == null) {
82
+ <></>
83
+ } @else {
84
+ <NavigationContext.Provider value={navigationContext}>
85
+ <LocationContext.Provider value={locationContext}>{children}</LocationContext.Provider>
86
+ </NavigationContext.Provider>
87
+ }
88
+ }
@@ -0,0 +1,13 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { Location } from '../router/history';
3
+ import type { Navigator } from '../context';
4
+
5
+ export declare const Router: (props: {
6
+ basename?: string;
7
+ children?: unknown;
8
+ location: Partial<Location> | string;
9
+ navigationType?: any;
10
+ navigator: Navigator;
11
+ static?: boolean;
12
+ useTransitions?: boolean;
13
+ }) => unknown;