toiljs 0.0.7 → 0.0.9

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 (135) hide show
  1. package/build/backend/.tsbuildinfo +1 -1
  2. package/build/cli/.tsbuildinfo +1 -1
  3. package/build/cli/configure.d.ts +1 -0
  4. package/build/cli/configure.js +85 -20
  5. package/build/cli/create.d.ts +1 -0
  6. package/build/cli/create.js +18 -7
  7. package/build/cli/features.d.ts +2 -0
  8. package/build/cli/features.js +22 -0
  9. package/build/cli/index.js +8 -0
  10. package/build/client/.tsbuildinfo +1 -1
  11. package/build/client/components/Form.d.ts +12 -0
  12. package/build/client/components/Form.js +23 -0
  13. package/build/client/components/Image.d.ts +13 -0
  14. package/build/client/components/Image.js +22 -0
  15. package/build/client/components/Script.d.ts +13 -0
  16. package/build/client/components/Script.js +68 -0
  17. package/build/client/components/Slot.d.ts +6 -0
  18. package/build/client/components/Slot.js +6 -0
  19. package/build/client/dev/error-overlay.d.ts +20 -0
  20. package/build/client/dev/error-overlay.js +123 -0
  21. package/build/client/head/head.d.ts +2 -0
  22. package/build/client/head/head.js +17 -2
  23. package/build/client/head/metadata.d.ts +29 -0
  24. package/build/client/head/metadata.js +38 -0
  25. package/build/client/index.d.ts +15 -3
  26. package/build/client/index.js +8 -2
  27. package/build/client/navigation/navigation.d.ts +3 -0
  28. package/build/client/navigation/navigation.js +42 -1
  29. package/build/client/routing/Router.d.ts +1 -0
  30. package/build/client/routing/Router.js +56 -34
  31. package/build/client/routing/action.d.ts +17 -0
  32. package/build/client/routing/action.js +55 -0
  33. package/build/client/routing/hooks.d.ts +1 -0
  34. package/build/client/routing/hooks.js +6 -7
  35. package/build/client/routing/loader.d.ts +10 -2
  36. package/build/client/routing/loader.js +83 -24
  37. package/build/client/routing/mount.d.ts +1 -1
  38. package/build/client/routing/mount.js +12 -4
  39. package/build/client/routing/slot-context.d.ts +2 -0
  40. package/build/client/routing/slot-context.js +2 -0
  41. package/build/client/types.d.ts +1 -0
  42. package/build/compiler/.tsbuildinfo +1 -1
  43. package/build/compiler/config.d.ts +10 -0
  44. package/build/compiler/config.js +5 -1
  45. package/build/compiler/docs.js +26 -26
  46. package/build/compiler/fonts.d.ts +4 -0
  47. package/build/compiler/fonts.js +64 -0
  48. package/build/compiler/generate.js +67 -32
  49. package/build/compiler/image-report.d.ts +2 -0
  50. package/build/compiler/image-report.js +62 -0
  51. package/build/compiler/plugin.js +1 -1
  52. package/build/compiler/prerender.d.ts +7 -0
  53. package/build/compiler/prerender.js +111 -0
  54. package/build/compiler/routes.d.ts +3 -0
  55. package/build/compiler/routes.js +50 -5
  56. package/build/compiler/seo.d.ts +70 -0
  57. package/build/compiler/seo.js +221 -0
  58. package/build/compiler/vite.js +13 -1
  59. package/build/io/.tsbuildinfo +1 -1
  60. package/build/shared/.tsbuildinfo +1 -1
  61. package/examples/basic/client/404.tsx +1 -1
  62. package/examples/basic/client/components/Header.tsx +38 -0
  63. package/examples/basic/client/components/HoneycombBackground.tsx +86 -18
  64. package/examples/basic/client/global-error.tsx +3 -3
  65. package/examples/basic/client/layout.tsx +2 -33
  66. package/examples/basic/client/public/images/test_image.webp +0 -0
  67. package/examples/basic/client/routes/about.tsx +8 -0
  68. package/examples/basic/client/routes/get-started.tsx +1 -1
  69. package/examples/basic/client/routes/index.tsx +8 -1
  70. package/examples/basic/client/routes/io.tsx +1 -1
  71. package/examples/basic/client/routes/loader-demo/index.tsx +29 -1
  72. package/examples/basic/client/routes/test.tsx +8 -0
  73. package/examples/basic/client/styles/main.css +48 -1
  74. package/package.json +8 -6
  75. package/presets/eslint.js +7 -4
  76. package/presets/tsconfig.json +1 -1
  77. package/src/backend/index.ts +1 -1
  78. package/src/cli/configure.ts +102 -21
  79. package/src/cli/create.ts +25 -9
  80. package/src/cli/features.ts +33 -1
  81. package/src/cli/index.ts +10 -1
  82. package/src/cli/ui.ts +1 -1
  83. package/src/cli/validate.ts +1 -1
  84. package/src/client/components/Form.tsx +65 -0
  85. package/src/client/components/Image.tsx +89 -0
  86. package/src/client/components/Script.tsx +113 -0
  87. package/src/client/components/Slot.tsx +21 -0
  88. package/src/client/dev/error-overlay.tsx +197 -0
  89. package/src/client/head/head.ts +28 -3
  90. package/src/client/head/metadata.ts +92 -0
  91. package/src/client/index.ts +20 -3
  92. package/src/client/navigation/Link.tsx +1 -1
  93. package/src/client/navigation/navigation.ts +74 -4
  94. package/src/client/navigation/prefetch.ts +2 -2
  95. package/src/client/routing/Router.tsx +128 -62
  96. package/src/client/routing/action.ts +122 -0
  97. package/src/client/routing/error-boundary.tsx +1 -1
  98. package/src/client/routing/hooks.ts +17 -23
  99. package/src/client/routing/loader.ts +158 -35
  100. package/src/client/routing/mount.tsx +25 -3
  101. package/src/client/routing/slot-context.ts +7 -0
  102. package/src/client/types.ts +6 -4
  103. package/src/compiler/config.ts +40 -3
  104. package/src/compiler/docs.ts +26 -26
  105. package/src/compiler/fonts.ts +87 -0
  106. package/src/compiler/generate.ts +69 -31
  107. package/src/compiler/image-report.ts +85 -0
  108. package/src/compiler/plugin.ts +2 -2
  109. package/src/compiler/prerender.ts +130 -0
  110. package/src/compiler/routes.ts +62 -7
  111. package/src/compiler/seo.ts +356 -0
  112. package/src/compiler/vite.ts +21 -4
  113. package/src/io/FastSet.ts +1 -1
  114. package/src/io/index.ts +1 -1
  115. package/src/io/types.ts +1 -1
  116. package/src/server/index.ts +1 -1
  117. package/src/server/main.ts +1 -1
  118. package/src/shared/index.ts +1 -1
  119. package/test/dom/Image.test.tsx +46 -0
  120. package/test/dom/Script.test.tsx +45 -0
  121. package/test/dom/action.test.tsx +129 -0
  122. package/test/dom/error-overlay.test.tsx +44 -0
  123. package/test/dom/loader.test.tsx +121 -0
  124. package/test/dom/revalidate.test.tsx +38 -0
  125. package/test/dom/route-head.test.tsx +34 -0
  126. package/test/dom/router-loading.test.tsx +44 -0
  127. package/test/dom/slot.test.tsx +109 -0
  128. package/test/dom/view-transitions.test.tsx +51 -0
  129. package/test/features.test.ts +31 -0
  130. package/test/fonts.test.ts +26 -0
  131. package/test/metadata.test.ts +41 -0
  132. package/test/prerender.test.ts +46 -0
  133. package/test/routes.test.ts +20 -1
  134. package/test/seo.test.ts +142 -0
  135. package/examples/basic/client/template.tsx +0 -7
@@ -4,12 +4,14 @@ export { Link } from './navigation/Link.js';
4
4
  export type { LinkProps } from './navigation/Link.js';
5
5
  export { NavLink, matchActive } from './navigation/NavLink.js';
6
6
  export type { NavLinkProps, NavLinkState } from './navigation/NavLink.js';
7
- export { navigate, back, forward, refresh } from './navigation/navigation.js';
7
+ export { navigate, back, forward, refresh, setViewTransitions } from './navigation/navigation.js';
8
8
  export type { NavigateOptions } from './navigation/navigation.js';
9
9
  export { useParams, useNavigate, useLocation, usePathname, useSearchParams, useRouter, useNavigationPending, } from './routing/hooks.js';
10
10
  export type { RouterInstance } from './routing/hooks.js';
11
- export { useLoaderData } from './routing/loader.js';
12
- export type { LoaderArgs, LoaderFunction } from './routing/loader.js';
11
+ export { useLoaderData, revalidate, invalidateLoaderData } from './routing/loader.js';
12
+ export type { LoaderArgs, LoaderFunction, LoaderData, Revalidate } from './routing/loader.js';
13
+ export { useAction } from './routing/action.js';
14
+ export type { UseActionOptions, ActionState, ActionHandle, RevalidateTarget, } from './routing/action.js';
13
15
  export { prefetch } from './navigation/prefetch.js';
14
16
  export type { RouteDef, LayoutLoader, LayoutComponentLoader, NotFoundLoader, RouteErrorProps, Register, RoutePath, Href, } from './types.js';
15
17
  export { matchRoute } from './routing/match.js';
@@ -18,3 +20,13 @@ export { connectChannel, useChannel, resolveChannelUrl } from './channel/channel
18
20
  export type { Channel, ChannelOptions, ChannelHook, ChannelData } from './channel/channel.js';
19
21
  export { useHead, useTitle, Head, mergeHead } from './head/head.js';
20
22
  export type { HeadSpec, MetaTag, LinkTag, ResolvedHead } from './head/head.js';
23
+ export { resolveMetadata } from './head/metadata.js';
24
+ export type { Metadata, GenerateMetadata, GenerateMetadataArgs, OpenGraph } from './head/metadata.js';
25
+ export { Image } from './components/Image.js';
26
+ export type { ImageProps } from './components/Image.js';
27
+ export { Script } from './components/Script.js';
28
+ export type { ScriptProps, ScriptStrategy } from './components/Script.js';
29
+ export { Form } from './components/Form.js';
30
+ export type { FormProps } from './components/Form.js';
31
+ export { Slot } from './components/Slot.js';
32
+ export type { SlotProps } from './components/Slot.js';
@@ -2,10 +2,16 @@ export { mount } from './routing/mount.js';
2
2
  export { Router } from './routing/Router.js';
3
3
  export { Link } from './navigation/Link.js';
4
4
  export { NavLink, matchActive } from './navigation/NavLink.js';
5
- export { navigate, back, forward, refresh } from './navigation/navigation.js';
5
+ export { navigate, back, forward, refresh, setViewTransitions } from './navigation/navigation.js';
6
6
  export { useParams, useNavigate, useLocation, usePathname, useSearchParams, useRouter, useNavigationPending, } from './routing/hooks.js';
7
- export { useLoaderData } from './routing/loader.js';
7
+ export { useLoaderData, revalidate, invalidateLoaderData } from './routing/loader.js';
8
+ export { useAction } from './routing/action.js';
8
9
  export { prefetch } from './navigation/prefetch.js';
9
10
  export { matchRoute } from './routing/match.js';
10
11
  export { connectChannel, useChannel, resolveChannelUrl } from './channel/channel.js';
11
12
  export { useHead, useTitle, Head, mergeHead } from './head/head.js';
13
+ export { resolveMetadata } from './head/metadata.js';
14
+ export { Image } from './components/Image.js';
15
+ export { Script } from './components/Script.js';
16
+ export { Form } from './components/Form.js';
17
+ export { Slot } from './components/Slot.js';
@@ -1,4 +1,7 @@
1
1
  import type { Href } from '../types.js';
2
+ export declare function setViewTransitions(enabled: boolean): void;
3
+ export declare function isSoftNavigation(): boolean;
4
+ export declare function previousPathname(): string;
2
5
  export declare function settleNavigation(): void;
3
6
  export declare function isNavigationPending(): boolean;
4
7
  export declare function navigationEpoch(): number;
@@ -1,16 +1,54 @@
1
+ import { startTransition } from 'react';
2
+ import { flushSync } from 'react-dom';
1
3
  import { enableManualScrollRestoration, planScroll, rememberScroll, } from './scroll.js';
2
4
  const listeners = new Set();
3
5
  let popstateBound = false;
6
+ let viewTransitions = false;
7
+ export function setViewTransitions(enabled) {
8
+ viewTransitions = enabled;
9
+ }
10
+ function shouldViewTransition() {
11
+ if (!viewTransitions || typeof document === 'undefined' || typeof window === 'undefined') {
12
+ return false;
13
+ }
14
+ if (typeof document.startViewTransition !== 'function')
15
+ return false;
16
+ return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
17
+ }
4
18
  let keyCounter = 0;
5
19
  let currentKey = 'initial';
6
20
  function nextKey() {
7
21
  keyCounter += 1;
8
22
  return `t${String(keyCounter)}`;
9
23
  }
10
- function notify() {
24
+ function runListeners() {
11
25
  for (const listener of listeners)
12
26
  listener();
13
27
  }
28
+ function notify() {
29
+ if (shouldViewTransition()) {
30
+ document.startViewTransition?.(() => {
31
+ flushSync(runListeners);
32
+ });
33
+ }
34
+ else {
35
+ startTransition(runListeners);
36
+ }
37
+ }
38
+ let softNav = false;
39
+ let currentPath = typeof window === 'undefined' ? '/' : window.location.pathname;
40
+ let previousPath = currentPath;
41
+ function recordTransition(soft) {
42
+ previousPath = currentPath;
43
+ currentPath = typeof window === 'undefined' ? '/' : window.location.pathname;
44
+ softNav = soft;
45
+ }
46
+ export function isSoftNavigation() {
47
+ return softNav;
48
+ }
49
+ export function previousPathname() {
50
+ return previousPath;
51
+ }
14
52
  let startedTick = 0;
15
53
  let committedTick = 0;
16
54
  const pendingListeners = new Set();
@@ -68,6 +106,7 @@ export function navigate(href, options) {
68
106
  currentKey = nextKey();
69
107
  window.history.pushState({ __toilKey: currentKey }, '', href);
70
108
  }
109
+ recordTransition(true);
71
110
  planScroll({ hash, toTop: options?.scroll !== false });
72
111
  notify();
73
112
  }
@@ -78,6 +117,7 @@ export function forward() {
78
117
  window.history.forward();
79
118
  }
80
119
  export function refresh() {
120
+ beginNavigation();
81
121
  notify();
82
122
  }
83
123
  function handlePopState(event) {
@@ -85,6 +125,7 @@ function handlePopState(event) {
85
125
  rememberScroll(currentKey);
86
126
  const state = event.state;
87
127
  currentKey = state?.__toilKey ?? 'initial';
128
+ recordTransition(true);
88
129
  planScroll({ restoreKey: currentKey, hash: window.location.hash, toTop: false });
89
130
  notify();
90
131
  }
@@ -5,4 +5,5 @@ export declare function Router(props: {
5
5
  layout?: LayoutLoader;
6
6
  notFound?: NotFoundLoader;
7
7
  globalError?: ErrorComponentLoader;
8
+ slots?: Record<string, RouteDef[]>;
8
9
  }): ReactNode;
@@ -3,60 +3,82 @@ import { createElement, Suspense, useLayoutEffect } from 'react';
3
3
  import { ErrorBoundary } from './error-boundary.js';
4
4
  import { useLocation } from './hooks.js';
5
5
  import { errorComponent, loadingComponent, nestedLayout, resolveLayout, resolveNotFound, } from './lazy.js';
6
- import { LoaderDataContext, readRouteData } from './loader.js';
6
+ import { loaderKey, LoaderDataContext, readRouteData } from './loader.js';
7
7
  import { matchRoute } from './match.js';
8
+ import { useRouteHead } from '../head/head.js';
8
9
  import { ParamsContext } from './params-context.js';
9
- import { navigationEpoch, settleNavigation } from '../navigation/navigation.js';
10
+ import { SlotContext } from './slot-context.js';
11
+ import { isSoftNavigation, navigationEpoch, previousPathname, settleNavigation, } from '../navigation/navigation.js';
10
12
  import { applyScroll } from '../navigation/scroll.js';
11
13
  function RoutePage(props) {
12
- const { Component, data } = readRouteData(props.route, props.params, props.dataKey);
14
+ const { Component, data, head } = readRouteData(props.route, props.params, props.dataKey, props.epoch);
15
+ useRouteHead(head);
13
16
  return _jsx(LoaderDataContext.Provider, { value: data, children: createElement(Component) });
14
17
  }
18
+ function renderMatched(matched, params, pathname, epoch, keyPrefix) {
19
+ const search = typeof window === 'undefined' ? '' : window.location.search;
20
+ const dataKey = keyPrefix + loaderKey(pathname, search);
21
+ const fallback = matched.loading
22
+ ? createElement(Suspense, { fallback: null }, createElement(loadingComponent(matched.loading)))
23
+ : null;
24
+ let content = (_jsx(Suspense, { fallback: fallback, children: _jsx(RoutePage, { route: matched, params: params, dataKey: dataKey, epoch: epoch }) }, matched.loading ? `${dataKey}:${String(epoch)}` : undefined));
25
+ const templates = matched.templates ?? [];
26
+ for (let i = templates.length - 1; i >= 0; i--) {
27
+ const Template = nestedLayout(templates[i]);
28
+ content = (_jsx(Suspense, { fallback: null, children: _jsx(Template, { children: content }) }, `${keyPrefix}${pathname}:${String(i)}`));
29
+ }
30
+ const chain = matched.layouts ?? [];
31
+ for (let i = chain.length - 1; i >= 0; i--) {
32
+ const NestedLayout = nestedLayout(chain[i]);
33
+ content = (_jsx(Suspense, { fallback: null, children: _jsx(NestedLayout, { children: content }) }));
34
+ }
35
+ if (matched.errorComponent) {
36
+ content = _jsx(ErrorBoundary, { fallback: errorComponent(matched.errorComponent), children: content });
37
+ }
38
+ return content;
39
+ }
40
+ function match(routes, pathname, allowIntercept = true) {
41
+ for (const route of routes) {
42
+ if (route.intercept && !allowIntercept)
43
+ continue;
44
+ const params = matchRoute(route.pattern, pathname);
45
+ if (params)
46
+ return { route, params };
47
+ }
48
+ return null;
49
+ }
15
50
  export function Router(props) {
16
- const { routes, layout = null, notFound = null, globalError = null } = props;
51
+ const { routes, layout = null, notFound = null, globalError = null, slots = {} } = props;
17
52
  const pathname = useLocation();
18
53
  useLayoutEffect(() => {
19
54
  applyScroll();
20
55
  settleNavigation();
21
56
  });
22
- let matched;
23
- let params = {};
24
- for (const route of routes) {
25
- const result = matchRoute(route.pattern, pathname);
26
- if (result) {
27
- matched = route;
28
- params = result;
29
- break;
30
- }
57
+ const epoch = navigationEpoch();
58
+ const soft = isSoftNavigation();
59
+ const slotElements = {};
60
+ let intercepting = false;
61
+ for (const [name, defs] of Object.entries(slots)) {
62
+ const slotMatch = match(defs, pathname, soft);
63
+ if (!slotMatch)
64
+ continue;
65
+ if (slotMatch.route.intercept)
66
+ intercepting = true;
67
+ slotElements[name] = (_jsx(ParamsContext.Provider, { value: slotMatch.params, children: renderMatched(slotMatch.route, slotMatch.params, pathname, epoch, `@${name} `) }));
31
68
  }
69
+ const mainPath = intercepting ? previousPathname() : pathname;
70
+ const matched = match(routes, mainPath);
71
+ const params = matched?.params ?? {};
32
72
  let content;
33
73
  if (matched) {
34
- const fallback = matched.loading
35
- ? createElement(Suspense, { fallback: null }, createElement(loadingComponent(matched.loading)))
36
- : null;
37
- const search = typeof window === 'undefined' ? '' : window.location.search;
38
- const dataKey = `${String(navigationEpoch())}:${pathname}${search}`;
39
- content = (_jsx(Suspense, { fallback: fallback, children: _jsx(RoutePage, { route: matched, params: params, dataKey: dataKey }) }));
40
- const templates = matched.templates ?? [];
41
- for (let i = templates.length - 1; i >= 0; i--) {
42
- const Template = nestedLayout(templates[i]);
43
- content = (_jsx(Suspense, { fallback: null, children: _jsx(Template, { children: content }) }, `${pathname}:${String(i)}`));
44
- }
45
- const chain = matched.layouts ?? [];
46
- for (let i = chain.length - 1; i >= 0; i--) {
47
- const NestedLayout = nestedLayout(chain[i]);
48
- content = (_jsx(Suspense, { fallback: null, children: _jsx(NestedLayout, { children: content }) }));
49
- }
50
- if (matched.errorComponent) {
51
- content = (_jsx(ErrorBoundary, { fallback: errorComponent(matched.errorComponent), children: content }));
52
- }
74
+ content = renderMatched(matched.route, matched.params, mainPath, epoch, '');
53
75
  }
54
76
  else if (notFound) {
55
77
  const NotFound = resolveNotFound(notFound);
56
78
  content = (_jsx(Suspense, { fallback: null, children: _jsx(NotFound, {}) }));
57
79
  }
58
80
  else {
59
- content = _jsx("div", { style: { padding: 24, fontFamily: 'system-ui' }, children: "404 \u2014 Not found" });
81
+ content = _jsx("div", { style: { padding: 24, fontFamily: 'system-ui' }, children: "404, Not found" });
60
82
  }
61
83
  if (layout) {
62
84
  const Layout = resolveLayout(layout);
@@ -65,5 +87,5 @@ export function Router(props) {
65
87
  if (globalError) {
66
88
  content = _jsx(ErrorBoundary, { fallback: errorComponent(globalError), children: content });
67
89
  }
68
- return _jsx(ParamsContext.Provider, { value: params, children: content });
90
+ return (_jsx(ParamsContext.Provider, { value: params, children: _jsx(SlotContext.Provider, { value: slotElements, children: content }) }));
69
91
  }
@@ -0,0 +1,17 @@
1
+ import type { Href } from '../types.js';
2
+ export type RevalidateTarget = boolean | Href | readonly Href[];
3
+ export interface UseActionOptions<TData> {
4
+ readonly revalidate?: RevalidateTarget;
5
+ readonly onSuccess?: (data: TData) => void;
6
+ readonly onError?: (error: unknown) => void;
7
+ }
8
+ export interface ActionState<TData> {
9
+ readonly pending: boolean;
10
+ readonly error: unknown;
11
+ readonly data: TData | undefined;
12
+ }
13
+ export interface ActionHandle<TInput, TData> extends ActionState<TData> {
14
+ run: (input: TInput) => Promise<TData | undefined>;
15
+ reset: () => void;
16
+ }
17
+ export declare function useAction<TInput = void, TData = unknown>(fn: (input: TInput) => TData | Promise<TData>, options?: UseActionOptions<TData>): ActionHandle<TInput, TData>;
@@ -0,0 +1,55 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { invalidateLoaderData } from './loader.js';
3
+ import { refresh } from '../navigation/navigation.js';
4
+ function applyRevalidate(target) {
5
+ if (target === false)
6
+ return;
7
+ if (target === undefined || target === true) {
8
+ invalidateLoaderData();
9
+ }
10
+ else {
11
+ const hrefs = typeof target === 'string' ? [target] : target;
12
+ for (const href of hrefs)
13
+ invalidateLoaderData(href);
14
+ }
15
+ refresh();
16
+ }
17
+ export function useAction(fn, options = {}) {
18
+ const [state, setState] = useState({
19
+ pending: false,
20
+ error: undefined,
21
+ data: undefined,
22
+ });
23
+ const latest = useRef({ fn, options });
24
+ latest.current = { fn, options };
25
+ const runId = useRef(0);
26
+ const mounted = useRef(true);
27
+ useEffect(() => () => {
28
+ mounted.current = false;
29
+ }, []);
30
+ const run = useCallback(async (input) => {
31
+ const id = ++runId.current;
32
+ setState((s) => ({ ...s, pending: true, error: undefined }));
33
+ try {
34
+ const data = await latest.current.fn(input);
35
+ if (mounted.current && id === runId.current) {
36
+ setState({ pending: false, error: undefined, data });
37
+ }
38
+ applyRevalidate(latest.current.options.revalidate);
39
+ latest.current.options.onSuccess?.(data);
40
+ return data;
41
+ }
42
+ catch (error) {
43
+ if (mounted.current && id === runId.current) {
44
+ setState({ pending: false, error, data: undefined });
45
+ }
46
+ latest.current.options.onError?.(error);
47
+ return undefined;
48
+ }
49
+ }, []);
50
+ const reset = useCallback(() => {
51
+ runId.current += 1;
52
+ setState({ pending: false, error: undefined, data: undefined });
53
+ }, []);
54
+ return { ...state, run, reset };
55
+ }
@@ -7,6 +7,7 @@ export interface RouterInstance {
7
7
  back(): void;
8
8
  forward(): void;
9
9
  refresh(): void;
10
+ revalidate(href?: Href): void;
10
11
  prefetch(href: Href): void;
11
12
  }
12
13
  export declare function useParams<T extends RouteParams = RouteParams>(): T;
@@ -1,6 +1,6 @@
1
- import { startTransition, useContext, useEffect, useMemo, useReducer, useSyncExternalStore, } from 'react';
1
+ import { useContext, useEffect, useMemo, useReducer, useSyncExternalStore } from 'react';
2
2
  import { back, forward, isNavigationPending, navigate, refresh, subscribeLocation, subscribePending, } from '../navigation/navigation.js';
3
- import { clearLoaderData } from './loader.js';
3
+ import { clearLoaderData, revalidate as revalidateData } from './loader.js';
4
4
  import { ParamsContext } from './params-context.js';
5
5
  import { prefetch } from '../navigation/prefetch.js';
6
6
  const ROUTER = {
@@ -16,6 +16,9 @@ const ROUTER = {
16
16
  clearLoaderData();
17
17
  refresh();
18
18
  },
19
+ revalidate: (href) => {
20
+ revalidateData(href);
21
+ },
19
22
  prefetch,
20
23
  };
21
24
  export function useParams() {
@@ -29,11 +32,7 @@ export function useRouter() {
29
32
  }
30
33
  function useLocationSubscription() {
31
34
  const [, forceUpdate] = useReducer((n) => n + 1, 0);
32
- useEffect(() => subscribeLocation(() => {
33
- startTransition(() => {
34
- forceUpdate();
35
- });
36
- }), []);
35
+ useEffect(() => subscribeLocation(forceUpdate), []);
37
36
  }
38
37
  export function useLocation() {
39
38
  useLocationSubscription();
@@ -1,4 +1,5 @@
1
1
  import { type ComponentType } from 'react';
2
+ import type { HeadSpec } from '../head/head.js';
2
3
  import type { RouteDef } from '../types.js';
3
4
  import type { RouteParams } from './match.js';
4
5
  export interface LoaderArgs {
@@ -6,12 +7,19 @@ export interface LoaderArgs {
6
7
  readonly searchParams: URLSearchParams;
7
8
  }
8
9
  export type LoaderFunction<T = unknown> = (args: LoaderArgs) => T | Promise<T>;
10
+ export type Revalidate = number | false;
11
+ export type LoaderData<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T;
9
12
  interface RouteData {
10
13
  Component: ComponentType;
11
14
  data: unknown;
15
+ head?: HeadSpec;
12
16
  }
13
- export declare function readRouteData(route: RouteDef, params: RouteParams, key: string): RouteData;
17
+ export declare function loaderKey(pathname: string, search: string): string;
18
+ export declare function readRouteData(route: RouteDef, params: RouteParams, key: string, epoch: number): RouteData;
14
19
  export declare function clearLoaderData(): void;
20
+ export declare function invalidateLoaderData(href?: string): void;
21
+ export declare function revalidate(href?: string): void;
15
22
  export declare const LoaderDataContext: import("react").Context<unknown>;
16
- export declare function useLoaderData<T = unknown>(): T;
23
+ export declare function useLoaderData<L extends LoaderFunction>(loader: L): Awaited<ReturnType<L>>;
24
+ export declare function useLoaderData<T = unknown>(): LoaderData<T>;
17
25
  export {};
@@ -1,51 +1,110 @@
1
1
  import { createContext, useContext } from 'react';
2
- import { navigationEpoch } from '../navigation/navigation.js';
2
+ import { resolveMetadata } from '../head/metadata.js';
3
+ import { refresh as rerender } from '../navigation/navigation.js';
3
4
  const cache = new Map();
4
- const MAX_ENTRIES = 16;
5
+ const MAX_ENTRIES = 32;
6
+ export function loaderKey(pathname, search) {
7
+ return `${pathname}${search}`;
8
+ }
5
9
  async function loadRoute(route, params) {
6
10
  const mod = await route.load();
7
11
  const searchParams = new URLSearchParams(typeof window === 'undefined' ? '' : window.location.search);
8
12
  const data = mod.loader ? await mod.loader({ params, searchParams }) : undefined;
9
- return { Component: mod.default, data };
10
- }
11
- function prune() {
12
- const current = `${String(navigationEpoch())}:`;
13
- for (const key of cache.keys()) {
14
- if (!key.startsWith(current))
15
- cache.delete(key);
13
+ let head;
14
+ if (mod.generateMetadata) {
15
+ head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
16
16
  }
17
+ else if (mod.metadata) {
18
+ head = resolveMetadata(mod.metadata);
19
+ }
20
+ return {
21
+ data: { Component: mod.default, data, head },
22
+ revalidate: mod.revalidate ?? 0,
23
+ hasLoader: mod.loader != null,
24
+ };
25
+ }
26
+ function isStale(entry, epoch) {
27
+ if (entry.status === 'error')
28
+ return true;
29
+ if (!entry.hasLoader)
30
+ return false;
31
+ if (entry.revalidate === false)
32
+ return false;
33
+ if (entry.revalidate === 0)
34
+ return entry.epoch !== epoch;
35
+ return Date.now() - entry.loadedAt >= entry.revalidate * 1000;
36
+ }
37
+ function startFetch(route, params, key, epoch) {
38
+ const created = {
39
+ status: 'pending',
40
+ promise: Promise.resolve(),
41
+ loadedAt: 0,
42
+ revalidate: 0,
43
+ epoch,
44
+ hasLoader: false,
45
+ };
46
+ created.promise = loadRoute(route, params).then((result) => {
47
+ created.value = result.data;
48
+ created.revalidate = result.revalidate;
49
+ created.hasLoader = result.hasLoader;
50
+ created.loadedAt = Date.now();
51
+ created.status = 'done';
52
+ }, (error) => {
53
+ created.error = error;
54
+ created.loadedAt = Date.now();
55
+ created.status = 'error';
56
+ });
57
+ cache.set(key, created);
17
58
  while (cache.size > MAX_ENTRIES) {
18
59
  const oldest = cache.keys().next().value;
19
- if (oldest === undefined)
60
+ if (oldest === undefined || oldest === key)
20
61
  break;
21
62
  cache.delete(oldest);
22
63
  }
64
+ return created;
23
65
  }
24
- export function readRouteData(route, params, key) {
66
+ export function readRouteData(route, params, key, epoch) {
25
67
  let entry = cache.get(key);
26
- if (!entry) {
27
- const created = { status: 'pending', promise: Promise.resolve() };
28
- created.promise = loadRoute(route, params).then((value) => {
29
- created.value = value;
30
- created.status = 'done';
31
- }, (error) => {
32
- created.error = error;
33
- created.status = 'error';
34
- });
35
- cache.set(key, created);
36
- prune();
37
- entry = created;
68
+ if (entry && entry.status !== 'pending' && isStale(entry, epoch)) {
69
+ entry = undefined;
38
70
  }
71
+ entry ??= startFetch(route, params, key, epoch);
39
72
  if (entry.status === 'pending')
40
73
  throw entry.promise;
41
74
  if (entry.status === 'error')
42
75
  throw entry.error;
76
+ if (!entry.value)
77
+ throw entry.promise;
43
78
  return entry.value;
44
79
  }
45
80
  export function clearLoaderData() {
46
81
  cache.clear();
47
82
  }
83
+ function keyForHref(href) {
84
+ if (typeof window === 'undefined')
85
+ return undefined;
86
+ try {
87
+ const url = new URL(href, window.location.href);
88
+ return loaderKey(url.pathname, url.search);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
94
+ export function invalidateLoaderData(href) {
95
+ if (href === undefined) {
96
+ cache.clear();
97
+ return;
98
+ }
99
+ const key = keyForHref(href);
100
+ if (key !== undefined)
101
+ cache.delete(key);
102
+ }
103
+ export function revalidate(href) {
104
+ invalidateLoaderData(href);
105
+ rerender();
106
+ }
48
107
  export const LoaderDataContext = createContext(undefined);
49
- export function useLoaderData() {
108
+ export function useLoaderData(_loader) {
50
109
  return useContext(LoaderDataContext);
51
110
  }
@@ -1,2 +1,2 @@
1
1
  import type { ErrorComponentLoader, LayoutLoader, NotFoundLoader, RouteDef } from '../types.js';
2
- export declare function mount(routes: RouteDef[], layout?: LayoutLoader, notFound?: NotFoundLoader, globalError?: ErrorComponentLoader): void;
2
+ export declare function mount(routes: RouteDef[], layout?: LayoutLoader, notFound?: NotFoundLoader, globalError?: ErrorComponentLoader, slots?: Record<string, RouteDef[]>): void;
@@ -1,13 +1,21 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { DevErrorBoundary, DevErrorOverlay, initDevErrorOverlay, isDevMode, } from '../dev/error-overlay.js';
3
4
  import { initNavigation } from '../navigation/navigation.js';
4
5
  import { startPrefetcher } from '../navigation/prefetch.js';
5
6
  import { Router } from './Router.js';
6
- export function mount(routes, layout = null, notFound = null, globalError = null) {
7
+ export function mount(routes, layout = null, notFound = null, globalError = null, slots = {}) {
7
8
  const el = document.getElementById('root');
8
9
  if (!el)
9
10
  throw new Error('toil: #root element not found');
10
11
  initNavigation();
11
- createRoot(el).render(_jsx(Router, { routes: routes, layout: layout, notFound: notFound, globalError: globalError }));
12
- startPrefetcher(routes);
12
+ const app = (_jsx(Router, { routes: routes, layout: layout, notFound: notFound, globalError: globalError, slots: slots }));
13
+ if (isDevMode()) {
14
+ initDevErrorOverlay();
15
+ createRoot(el).render(_jsxs(_Fragment, { children: [_jsx(DevErrorBoundary, { children: app }), _jsx(DevErrorOverlay, {})] }));
16
+ }
17
+ else {
18
+ createRoot(el).render(app);
19
+ }
20
+ startPrefetcher([...routes, ...Object.values(slots).flat()]);
13
21
  }
@@ -0,0 +1,2 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const SlotContext: import("react").Context<Record<string, ReactNode>>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from 'react';
2
+ export const SlotContext = createContext({});
@@ -30,6 +30,7 @@ export interface RouteDef {
30
30
  readonly errorComponent?: () => Promise<{
31
31
  default: ComponentType<RouteErrorProps>;
32
32
  }>;
33
+ readonly intercept?: boolean;
33
34
  }
34
35
  export type LayoutLoader = LayoutComponentLoader | null;
35
36
  export type NotFoundLoader = (() => Promise<{