toiljs 0.0.8 → 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.js +5 -5
- package/build/cli/create.js +4 -4
- package/build/client/.tsbuildinfo +1 -1
- 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 +5 -1
- package/build/client/index.js +3 -1
- 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 +55 -33
- package/build/client/routing/hooks.js +2 -6
- package/build/client/routing/loader.d.ts +2 -0
- package/build/client/routing/loader.js +9 -1
- 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 +8 -0
- package/build/compiler/config.js +4 -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 +65 -32
- 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 +5 -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/global-error.tsx +1 -1
- package/examples/basic/client/routes/about.tsx +8 -0
- package/examples/basic/client/routes/get-started.tsx +1 -1
- package/examples/basic/client/routes/io.tsx +1 -1
- package/examples/basic/client/routes/loader-demo/index.tsx +7 -2
- package/package.json +1 -1
- package/presets/eslint.js +7 -4
- package/presets/tsconfig.json +1 -1
- package/src/backend/index.ts +1 -1
- package/src/cli/configure.ts +7 -7
- package/src/cli/create.ts +7 -7
- package/src/cli/features.ts +2 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/validate.ts +1 -1
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +2 -2
- package/src/client/components/Script.tsx +3 -3
- 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 +5 -1
- 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 +121 -67
- package/src/client/routing/action.ts +4 -4
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/hooks.ts +6 -25
- package/src/client/routing/loader.ts +20 -8
- 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 +31 -3
- package/src/compiler/docs.ts +26 -26
- package/src/compiler/fonts.ts +87 -0
- package/src/compiler/generate.ts +66 -31
- package/src/compiler/image-report.ts +1 -1
- 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 +9 -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/error-overlay.test.tsx +44 -0
- package/test/dom/revalidate.test.tsx +38 -0
- package/test/dom/route-head.test.tsx +34 -0
- package/test/dom/slot.test.tsx +109 -0
- package/test/dom/view-transitions.test.tsx +51 -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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
export declare function isDevMode(): boolean;
|
|
3
|
+
export declare function initDevErrorOverlay(): void;
|
|
4
|
+
interface BoundaryProps {
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
interface BoundaryState {
|
|
8
|
+
readonly crashed: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class DevErrorBoundary extends Component<BoundaryProps, BoundaryState> {
|
|
11
|
+
state: BoundaryState;
|
|
12
|
+
private unsubscribe;
|
|
13
|
+
static getDerivedStateFromError(): BoundaryState;
|
|
14
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
15
|
+
componentDidMount(): void;
|
|
16
|
+
componentWillUnmount(): void;
|
|
17
|
+
render(): ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare function DevErrorOverlay(): ReactNode;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Component, useSyncExternalStore, } from 'react';
|
|
3
|
+
let current = null;
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
function emit() {
|
|
6
|
+
for (const listener of listeners)
|
|
7
|
+
listener();
|
|
8
|
+
}
|
|
9
|
+
function setDevError(next) {
|
|
10
|
+
current = next;
|
|
11
|
+
emit();
|
|
12
|
+
}
|
|
13
|
+
function subscribe(listener) {
|
|
14
|
+
listeners.add(listener);
|
|
15
|
+
return () => {
|
|
16
|
+
listeners.delete(listener);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function isDevMode() {
|
|
20
|
+
try {
|
|
21
|
+
return Boolean(import.meta.env?.DEV);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
let windowBound = false;
|
|
28
|
+
export function initDevErrorOverlay() {
|
|
29
|
+
if (windowBound || typeof window === 'undefined')
|
|
30
|
+
return;
|
|
31
|
+
windowBound = true;
|
|
32
|
+
window.addEventListener('error', (event) => {
|
|
33
|
+
if (event.error instanceof Error)
|
|
34
|
+
setDevError({ error: event.error, source: 'window' });
|
|
35
|
+
});
|
|
36
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
37
|
+
const reason = event.reason;
|
|
38
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
39
|
+
setDevError({ error, source: 'unhandledrejection' });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export class DevErrorBoundary extends Component {
|
|
43
|
+
state = { crashed: false };
|
|
44
|
+
unsubscribe;
|
|
45
|
+
static getDerivedStateFromError() {
|
|
46
|
+
return { crashed: true };
|
|
47
|
+
}
|
|
48
|
+
componentDidCatch(error, info) {
|
|
49
|
+
setDevError({ error, componentStack: info.componentStack ?? undefined, source: 'render' });
|
|
50
|
+
}
|
|
51
|
+
componentDidMount() {
|
|
52
|
+
this.unsubscribe = subscribe(() => {
|
|
53
|
+
if (current === null && this.state.crashed)
|
|
54
|
+
this.setState({ crashed: false });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
componentWillUnmount() {
|
|
58
|
+
this.unsubscribe?.();
|
|
59
|
+
}
|
|
60
|
+
render() {
|
|
61
|
+
return this.state.crashed ? null : this.props.children;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const overlayStyle = {
|
|
65
|
+
position: 'fixed',
|
|
66
|
+
inset: 0,
|
|
67
|
+
zIndex: 2147483647,
|
|
68
|
+
background: 'rgba(8, 8, 12, 0.88)',
|
|
69
|
+
color: '#f5f6fa',
|
|
70
|
+
font: '13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
71
|
+
padding: '2rem',
|
|
72
|
+
overflow: 'auto',
|
|
73
|
+
display: 'flex',
|
|
74
|
+
justifyContent: 'center',
|
|
75
|
+
alignItems: 'flex-start',
|
|
76
|
+
};
|
|
77
|
+
const panelStyle = {
|
|
78
|
+
maxWidth: 900,
|
|
79
|
+
width: '100%',
|
|
80
|
+
background: '#15151c',
|
|
81
|
+
border: '1px solid #ef4444',
|
|
82
|
+
borderRadius: 10,
|
|
83
|
+
padding: '1.25rem 1.5rem',
|
|
84
|
+
boxShadow: '0 12px 48px rgba(0,0,0,0.5)',
|
|
85
|
+
};
|
|
86
|
+
const titleStyle = {
|
|
87
|
+
margin: 0,
|
|
88
|
+
color: '#ff6b6b',
|
|
89
|
+
fontSize: '1rem',
|
|
90
|
+
fontWeight: 700,
|
|
91
|
+
wordBreak: 'break-word',
|
|
92
|
+
};
|
|
93
|
+
const preStyle = {
|
|
94
|
+
whiteSpace: 'pre-wrap',
|
|
95
|
+
wordBreak: 'break-word',
|
|
96
|
+
margin: '0.75rem 0 0',
|
|
97
|
+
color: '#c8cee0',
|
|
98
|
+
};
|
|
99
|
+
const buttonStyle = {
|
|
100
|
+
font: 'inherit',
|
|
101
|
+
color: '#f5f6fa',
|
|
102
|
+
background: '#2a2a36',
|
|
103
|
+
border: '1px solid #3a3a48',
|
|
104
|
+
borderRadius: 6,
|
|
105
|
+
padding: '0.4em 1em',
|
|
106
|
+
cursor: 'pointer',
|
|
107
|
+
marginRight: '0.5rem',
|
|
108
|
+
};
|
|
109
|
+
const SOURCE_LABEL = {
|
|
110
|
+
render: 'Render error',
|
|
111
|
+
window: 'Uncaught error',
|
|
112
|
+
unhandledrejection: 'Unhandled promise rejection',
|
|
113
|
+
};
|
|
114
|
+
export function DevErrorOverlay() {
|
|
115
|
+
const devError = useSyncExternalStore(subscribe, () => current, () => null);
|
|
116
|
+
if (!devError)
|
|
117
|
+
return null;
|
|
118
|
+
return (_jsx("div", { style: overlayStyle, role: "alert", children: _jsxs("div", { style: panelStyle, children: [_jsxs("p", { style: titleStyle, children: [SOURCE_LABEL[devError.source], ", ", devError.error.name, ": ", devError.error.message] }), devError.error.stack !== undefined && _jsx("pre", { style: preStyle, children: devError.error.stack }), devError.componentStack !== undefined && (_jsx("pre", { style: { ...preStyle, color: '#8b9ab4' }, children: devError.componentStack })), _jsxs("div", { style: { marginTop: '1.25rem' }, children: [_jsx("button", { type: "button", style: buttonStyle, onClick: () => {
|
|
119
|
+
setDevError(null);
|
|
120
|
+
}, children: "Dismiss" }), _jsx("button", { type: "button", style: buttonStyle, onClick: () => {
|
|
121
|
+
window.location.reload();
|
|
122
|
+
}, children: "Reload" })] })] }) }));
|
|
123
|
+
}
|
|
@@ -24,3 +24,5 @@ export declare function mergeHead(specs: readonly HeadSpec[]): ResolvedHead;
|
|
|
24
24
|
export declare function useHead(spec: HeadSpec): void;
|
|
25
25
|
export declare function useTitle(title: string): void;
|
|
26
26
|
export declare function Head(props: HeadSpec): null;
|
|
27
|
+
export declare function setRouteHead(spec: HeadSpec | null): void;
|
|
28
|
+
export declare function useRouteHead(spec: HeadSpec | undefined): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
2
|
function metaKey(m) {
|
|
3
3
|
if (m.name !== undefined)
|
|
4
4
|
return `name:${m.name}`;
|
|
@@ -30,6 +30,7 @@ const entries = new Map();
|
|
|
30
30
|
let order = [];
|
|
31
31
|
let seq = 0;
|
|
32
32
|
let baseTitle = null;
|
|
33
|
+
let routeHead = null;
|
|
33
34
|
function setAttrs(el, attrs) {
|
|
34
35
|
el.setAttribute('data-toil-head', '');
|
|
35
36
|
for (const [key, value] of Object.entries(attrs)) {
|
|
@@ -42,7 +43,8 @@ function apply() {
|
|
|
42
43
|
return;
|
|
43
44
|
if (baseTitle === null)
|
|
44
45
|
baseTitle = document.title;
|
|
45
|
-
const
|
|
46
|
+
const specs = [routeHead, ...order.map((id) => entries.get(id))];
|
|
47
|
+
const resolved = mergeHead(specs.filter((s) => !!s));
|
|
46
48
|
document.title = resolved.title ?? baseTitle;
|
|
47
49
|
for (const stale of document.head.querySelectorAll('[data-toil-head]'))
|
|
48
50
|
stale.remove();
|
|
@@ -85,3 +87,16 @@ export function Head(props) {
|
|
|
85
87
|
useHead(props);
|
|
86
88
|
return null;
|
|
87
89
|
}
|
|
90
|
+
export function setRouteHead(spec) {
|
|
91
|
+
routeHead = spec;
|
|
92
|
+
apply();
|
|
93
|
+
}
|
|
94
|
+
export function useRouteHead(spec) {
|
|
95
|
+
const json = spec ? JSON.stringify(spec) : '';
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
setRouteHead(json ? JSON.parse(json) : null);
|
|
98
|
+
return () => {
|
|
99
|
+
setRouteHead(null);
|
|
100
|
+
};
|
|
101
|
+
}, [json]);
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HeadSpec, LinkTag, MetaTag } from './head.js';
|
|
2
|
+
import type { RouteParams } from '../routing/match.js';
|
|
3
|
+
export interface OpenGraph {
|
|
4
|
+
readonly title?: string;
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
readonly type?: string;
|
|
7
|
+
readonly url?: string;
|
|
8
|
+
readonly image?: string;
|
|
9
|
+
readonly siteName?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Metadata {
|
|
12
|
+
readonly title?: string;
|
|
13
|
+
readonly titleTemplate?: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly keywords?: string | readonly string[];
|
|
16
|
+
readonly canonical?: string;
|
|
17
|
+
readonly robots?: string;
|
|
18
|
+
readonly themeColor?: string;
|
|
19
|
+
readonly openGraph?: OpenGraph;
|
|
20
|
+
readonly meta?: readonly MetaTag[];
|
|
21
|
+
readonly link?: readonly LinkTag[];
|
|
22
|
+
}
|
|
23
|
+
export interface GenerateMetadataArgs<T = unknown> {
|
|
24
|
+
readonly params: RouteParams;
|
|
25
|
+
readonly searchParams: URLSearchParams;
|
|
26
|
+
readonly data: T;
|
|
27
|
+
}
|
|
28
|
+
export type GenerateMetadata<T = unknown> = (args: GenerateMetadataArgs<T>) => Metadata | Promise<Metadata>;
|
|
29
|
+
export declare function resolveMetadata(metadata: Metadata): HeadSpec;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function resolveMetadata(metadata) {
|
|
2
|
+
const meta = [];
|
|
3
|
+
if (metadata.description !== undefined) {
|
|
4
|
+
meta.push({ name: 'description', content: metadata.description });
|
|
5
|
+
}
|
|
6
|
+
if (metadata.keywords !== undefined) {
|
|
7
|
+
const content = typeof metadata.keywords === 'string' ? metadata.keywords : metadata.keywords.join(', ');
|
|
8
|
+
meta.push({ name: 'keywords', content });
|
|
9
|
+
}
|
|
10
|
+
if (metadata.robots !== undefined)
|
|
11
|
+
meta.push({ name: 'robots', content: metadata.robots });
|
|
12
|
+
if (metadata.themeColor !== undefined) {
|
|
13
|
+
meta.push({ name: 'theme-color', content: metadata.themeColor });
|
|
14
|
+
}
|
|
15
|
+
const og = metadata.openGraph;
|
|
16
|
+
if (og) {
|
|
17
|
+
const pairs = [
|
|
18
|
+
['og:title', og.title],
|
|
19
|
+
['og:description', og.description],
|
|
20
|
+
['og:type', og.type],
|
|
21
|
+
['og:url', og.url],
|
|
22
|
+
['og:image', og.image],
|
|
23
|
+
['og:site_name', og.siteName],
|
|
24
|
+
];
|
|
25
|
+
for (const [property, content] of pairs) {
|
|
26
|
+
if (content !== undefined)
|
|
27
|
+
meta.push({ property, content });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (metadata.meta)
|
|
31
|
+
meta.push(...metadata.meta);
|
|
32
|
+
const link = [];
|
|
33
|
+
if (metadata.canonical !== undefined)
|
|
34
|
+
link.push({ rel: 'canonical', href: metadata.canonical });
|
|
35
|
+
if (metadata.link)
|
|
36
|
+
link.push(...metadata.link);
|
|
37
|
+
return { title: metadata.title, titleTemplate: metadata.titleTemplate, meta, link };
|
|
38
|
+
}
|
package/build/client/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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';
|
|
@@ -20,9 +20,13 @@ export { connectChannel, useChannel, resolveChannelUrl } from './channel/channel
|
|
|
20
20
|
export type { Channel, ChannelOptions, ChannelHook, ChannelData } from './channel/channel.js';
|
|
21
21
|
export { useHead, useTitle, Head, mergeHead } from './head/head.js';
|
|
22
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';
|
|
23
25
|
export { Image } from './components/Image.js';
|
|
24
26
|
export type { ImageProps } from './components/Image.js';
|
|
25
27
|
export { Script } from './components/Script.js';
|
|
26
28
|
export type { ScriptProps, ScriptStrategy } from './components/Script.js';
|
|
27
29
|
export { Form } from './components/Form.js';
|
|
28
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,7 +2,7 @@ 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
7
|
export { useLoaderData, revalidate, invalidateLoaderData } from './routing/loader.js';
|
|
8
8
|
export { useAction } from './routing/action.js';
|
|
@@ -10,6 +10,8 @@ export { prefetch } from './navigation/prefetch.js';
|
|
|
10
10
|
export { matchRoute } from './routing/match.js';
|
|
11
11
|
export { connectChannel, useChannel, resolveChannelUrl } from './channel/channel.js';
|
|
12
12
|
export { useHead, useTitle, Head, mergeHead } from './head/head.js';
|
|
13
|
+
export { resolveMetadata } from './head/metadata.js';
|
|
13
14
|
export { Image } from './components/Image.js';
|
|
14
15
|
export { Script } from './components/Script.js';
|
|
15
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
|
}
|
|
@@ -5,58 +5,80 @@ import { useLocation } from './hooks.js';
|
|
|
5
5
|
import { errorComponent, loadingComponent, nestedLayout, resolveLayout, resolveNotFound, } from './lazy.js';
|
|
6
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, props.epoch);
|
|
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 = loaderKey(pathname, search);
|
|
39
|
-
content = (_jsx(Suspense, { fallback: fallback, children: _jsx(RoutePage, { route: matched, params: params, dataKey: dataKey, epoch: navigationEpoch() }) }, matched.loading ? dataKey : undefined));
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
import { clearLoaderData, revalidate as revalidateData } from './loader.js';
|
|
4
4
|
import { ParamsContext } from './params-context.js';
|
|
@@ -32,11 +32,7 @@ export function useRouter() {
|
|
|
32
32
|
}
|
|
33
33
|
function useLocationSubscription() {
|
|
34
34
|
const [, forceUpdate] = useReducer((n) => n + 1, 0);
|
|
35
|
-
useEffect(() => subscribeLocation(
|
|
36
|
-
startTransition(() => {
|
|
37
|
-
forceUpdate();
|
|
38
|
-
});
|
|
39
|
-
}), []);
|
|
35
|
+
useEffect(() => subscribeLocation(forceUpdate), []);
|
|
40
36
|
}
|
|
41
37
|
export function useLocation() {
|
|
42
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 {
|
|
@@ -11,6 +12,7 @@ export type LoaderData<T> = T extends (...args: never[]) => infer R ? Awaited<R>
|
|
|
11
12
|
interface RouteData {
|
|
12
13
|
Component: ComponentType;
|
|
13
14
|
data: unknown;
|
|
15
|
+
head?: HeadSpec;
|
|
14
16
|
}
|
|
15
17
|
export declare function loaderKey(pathname: string, search: string): string;
|
|
16
18
|
export declare function readRouteData(route: RouteDef, params: RouteParams, key: string, epoch: number): RouteData;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
+
import { resolveMetadata } from '../head/metadata.js';
|
|
2
3
|
import { refresh as rerender } from '../navigation/navigation.js';
|
|
3
4
|
const cache = new Map();
|
|
4
5
|
const MAX_ENTRIES = 32;
|
|
@@ -9,8 +10,15 @@ async function loadRoute(route, params) {
|
|
|
9
10
|
const mod = await route.load();
|
|
10
11
|
const searchParams = new URLSearchParams(typeof window === 'undefined' ? '' : window.location.search);
|
|
11
12
|
const data = mod.loader ? await mod.loader({ params, searchParams }) : undefined;
|
|
13
|
+
let head;
|
|
14
|
+
if (mod.generateMetadata) {
|
|
15
|
+
head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
|
|
16
|
+
}
|
|
17
|
+
else if (mod.metadata) {
|
|
18
|
+
head = resolveMetadata(mod.metadata);
|
|
19
|
+
}
|
|
12
20
|
return {
|
|
13
|
-
data: { Component: mod.default, data },
|
|
21
|
+
data: { Component: mod.default, data, head },
|
|
14
22
|
revalidate: mod.revalidate ?? 0,
|
|
15
23
|
hasLoader: mod.loader != null,
|
|
16
24
|
};
|
|
@@ -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<{
|