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.
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.d.ts +1 -0
- package/build/cli/configure.js +85 -20
- package/build/cli/create.d.ts +1 -0
- package/build/cli/create.js +18 -7
- package/build/cli/features.d.ts +2 -0
- package/build/cli/features.js +22 -0
- package/build/cli/index.js +8 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/components/Form.d.ts +12 -0
- package/build/client/components/Form.js +23 -0
- package/build/client/components/Image.d.ts +13 -0
- package/build/client/components/Image.js +22 -0
- package/build/client/components/Script.d.ts +13 -0
- package/build/client/components/Script.js +68 -0
- package/build/client/components/Slot.d.ts +6 -0
- package/build/client/components/Slot.js +6 -0
- package/build/client/dev/error-overlay.d.ts +20 -0
- package/build/client/dev/error-overlay.js +123 -0
- package/build/client/head/head.d.ts +2 -0
- package/build/client/head/head.js +17 -2
- package/build/client/head/metadata.d.ts +29 -0
- package/build/client/head/metadata.js +38 -0
- package/build/client/index.d.ts +15 -3
- package/build/client/index.js +8 -2
- package/build/client/navigation/navigation.d.ts +3 -0
- package/build/client/navigation/navigation.js +42 -1
- package/build/client/routing/Router.d.ts +1 -0
- package/build/client/routing/Router.js +56 -34
- package/build/client/routing/action.d.ts +17 -0
- package/build/client/routing/action.js +55 -0
- package/build/client/routing/hooks.d.ts +1 -0
- package/build/client/routing/hooks.js +6 -7
- package/build/client/routing/loader.d.ts +10 -2
- package/build/client/routing/loader.js +83 -24
- package/build/client/routing/mount.d.ts +1 -1
- package/build/client/routing/mount.js +12 -4
- package/build/client/routing/slot-context.d.ts +2 -0
- package/build/client/routing/slot-context.js +2 -0
- package/build/client/types.d.ts +1 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +10 -0
- package/build/compiler/config.js +5 -1
- package/build/compiler/docs.js +26 -26
- package/build/compiler/fonts.d.ts +4 -0
- package/build/compiler/fonts.js +64 -0
- package/build/compiler/generate.js +67 -32
- package/build/compiler/image-report.d.ts +2 -0
- package/build/compiler/image-report.js +62 -0
- package/build/compiler/plugin.js +1 -1
- package/build/compiler/prerender.d.ts +7 -0
- package/build/compiler/prerender.js +111 -0
- package/build/compiler/routes.d.ts +3 -0
- package/build/compiler/routes.js +50 -5
- package/build/compiler/seo.d.ts +70 -0
- package/build/compiler/seo.js +221 -0
- package/build/compiler/vite.js +13 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/404.tsx +1 -1
- package/examples/basic/client/components/Header.tsx +38 -0
- package/examples/basic/client/components/HoneycombBackground.tsx +86 -18
- package/examples/basic/client/global-error.tsx +3 -3
- package/examples/basic/client/layout.tsx +2 -33
- package/examples/basic/client/public/images/test_image.webp +0 -0
- package/examples/basic/client/routes/about.tsx +8 -0
- package/examples/basic/client/routes/get-started.tsx +1 -1
- package/examples/basic/client/routes/index.tsx +8 -1
- package/examples/basic/client/routes/io.tsx +1 -1
- package/examples/basic/client/routes/loader-demo/index.tsx +29 -1
- package/examples/basic/client/routes/test.tsx +8 -0
- package/examples/basic/client/styles/main.css +48 -1
- package/package.json +8 -6
- package/presets/eslint.js +7 -4
- package/presets/tsconfig.json +1 -1
- package/src/backend/index.ts +1 -1
- package/src/cli/configure.ts +102 -21
- package/src/cli/create.ts +25 -9
- package/src/cli/features.ts +33 -1
- package/src/cli/index.ts +10 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/validate.ts +1 -1
- package/src/client/components/Form.tsx +65 -0
- package/src/client/components/Image.tsx +89 -0
- package/src/client/components/Script.tsx +113 -0
- package/src/client/components/Slot.tsx +21 -0
- package/src/client/dev/error-overlay.tsx +197 -0
- package/src/client/head/head.ts +28 -3
- package/src/client/head/metadata.ts +92 -0
- package/src/client/index.ts +20 -3
- package/src/client/navigation/Link.tsx +1 -1
- package/src/client/navigation/navigation.ts +74 -4
- package/src/client/navigation/prefetch.ts +2 -2
- package/src/client/routing/Router.tsx +128 -62
- package/src/client/routing/action.ts +122 -0
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/hooks.ts +17 -23
- package/src/client/routing/loader.ts +158 -35
- package/src/client/routing/mount.tsx +25 -3
- package/src/client/routing/slot-context.ts +7 -0
- package/src/client/types.ts +6 -4
- package/src/compiler/config.ts +40 -3
- package/src/compiler/docs.ts +26 -26
- package/src/compiler/fonts.ts +87 -0
- package/src/compiler/generate.ts +69 -31
- package/src/compiler/image-report.ts +85 -0
- package/src/compiler/plugin.ts +2 -2
- package/src/compiler/prerender.ts +130 -0
- package/src/compiler/routes.ts +62 -7
- package/src/compiler/seo.ts +356 -0
- package/src/compiler/vite.ts +21 -4
- package/src/io/FastSet.ts +1 -1
- package/src/io/index.ts +1 -1
- package/src/io/types.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/server/main.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/test/dom/Image.test.tsx +46 -0
- package/test/dom/Script.test.tsx +45 -0
- package/test/dom/action.test.tsx +129 -0
- package/test/dom/error-overlay.test.tsx +44 -0
- package/test/dom/loader.test.tsx +121 -0
- package/test/dom/revalidate.test.tsx +38 -0
- package/test/dom/route-head.test.tsx +34 -0
- package/test/dom/router-loading.test.tsx +44 -0
- package/test/dom/slot.test.tsx +109 -0
- package/test/dom/view-transitions.test.tsx +51 -0
- package/test/features.test.ts +31 -0
- package/test/fonts.test.ts +26 -0
- package/test/metadata.test.ts +41 -0
- package/test/prerender.test.ts +46 -0
- package/test/routes.test.ts +20 -1
- package/test/seo.test.ts +142 -0
- package/examples/basic/client/template.tsx +0 -7
package/build/client/index.d.ts
CHANGED
|
@@ -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';
|
package/build/client/index.js
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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<
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 (
|
|
27
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
}
|
package/build/client/types.d.ts
CHANGED
|
@@ -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<{
|