round-core 0.0.1
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/README.md +354 -0
- package/Round.png +0 -0
- package/bun.lock +414 -0
- package/cli.js +2 -0
- package/index.html +19 -0
- package/index.js +2 -0
- package/package.json +40 -0
- package/src/cli.js +599 -0
- package/src/compiler/index.js +2 -0
- package/src/compiler/transformer.js +395 -0
- package/src/compiler/vite-plugin.js +461 -0
- package/src/index.js +45 -0
- package/src/runtime/context.js +62 -0
- package/src/runtime/dom.js +345 -0
- package/src/runtime/error-boundary.js +48 -0
- package/src/runtime/error-reporter.js +13 -0
- package/src/runtime/error-store.js +85 -0
- package/src/runtime/errors.js +152 -0
- package/src/runtime/lifecycle.js +142 -0
- package/src/runtime/markdown.js +72 -0
- package/src/runtime/router.js +371 -0
- package/src/runtime/signals.js +510 -0
- package/src/runtime/store.js +208 -0
- package/src/runtime/suspense.js +106 -0
- package/vite.config.js +10 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { reportErrorSafe } from './error-reporter.js';
|
|
2
|
+
|
|
3
|
+
const componentStack = [];
|
|
4
|
+
|
|
5
|
+
export function getCurrentComponent() {
|
|
6
|
+
return componentStack[componentStack.length - 1];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function runInContext(componentInstance, fn) {
|
|
10
|
+
componentStack.push(componentInstance);
|
|
11
|
+
try {
|
|
12
|
+
return fn();
|
|
13
|
+
} finally {
|
|
14
|
+
componentStack.pop();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createComponentInstance() {
|
|
19
|
+
return {
|
|
20
|
+
mountHooks: [],
|
|
21
|
+
unmountHooks: [],
|
|
22
|
+
updateHooks: [],
|
|
23
|
+
nodes: [],
|
|
24
|
+
isMounted: false,
|
|
25
|
+
mountTimerId: null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function onMount(fn) {
|
|
30
|
+
const component = getCurrentComponent();
|
|
31
|
+
if (component) {
|
|
32
|
+
component.mountHooks.push(fn);
|
|
33
|
+
} else {
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
try {
|
|
36
|
+
fn();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
reportErrorSafe(e, { phase: 'onMount' });
|
|
39
|
+
}
|
|
40
|
+
}, 0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function onUnmount(fn) {
|
|
45
|
+
const component = getCurrentComponent();
|
|
46
|
+
if (component) {
|
|
47
|
+
component.unmountHooks.push(fn);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const onCleanup = onUnmount;
|
|
52
|
+
|
|
53
|
+
export function onUpdate(fn) {
|
|
54
|
+
const component = getCurrentComponent();
|
|
55
|
+
if (component) {
|
|
56
|
+
component.updateHooks.push(fn);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function mountComponent(component) {
|
|
61
|
+
if (component.isMounted) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const root = component?.nodes?.[0];
|
|
65
|
+
if (root && root instanceof Node && root.isConnected === false) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
component.isMounted = true;
|
|
72
|
+
component.mountHooks.forEach(hook => {
|
|
73
|
+
try {
|
|
74
|
+
const cleanup = hook();
|
|
75
|
+
if (typeof cleanup === 'function') {
|
|
76
|
+
component.unmountHooks.push(cleanup);
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
reportErrorSafe(e, { phase: 'mount', component: component.name ?? null });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function unmountComponent(component) {
|
|
85
|
+
if (!component.isMounted) return;
|
|
86
|
+
|
|
87
|
+
if (component.mountTimerId != null) {
|
|
88
|
+
try {
|
|
89
|
+
clearTimeout(component.mountTimerId);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
component.mountTimerId = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
component.isMounted = false;
|
|
96
|
+
component.unmountHooks.forEach(hook => {
|
|
97
|
+
try {
|
|
98
|
+
hook();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
reportErrorSafe(e, { phase: 'unmount', component: component.name ?? null });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function triggerUpdate(component) {
|
|
106
|
+
if (!component.isMounted) return;
|
|
107
|
+
component.updateHooks.forEach(hook => {
|
|
108
|
+
try {
|
|
109
|
+
hook();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
reportErrorSafe(e, { phase: 'update', component: component.name ?? null });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const observer = (typeof MutationObserver !== 'undefined')
|
|
117
|
+
? new MutationObserver((mutations) => {
|
|
118
|
+
mutations.forEach(mutation => {
|
|
119
|
+
if (mutation.removedNodes.length > 0) {
|
|
120
|
+
mutation.removedNodes.forEach(node => {
|
|
121
|
+
if (node._componentInstance) {
|
|
122
|
+
unmountComponent(node._componentInstance);
|
|
123
|
+
}
|
|
124
|
+
cleanupNodeRecursively(node);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
})
|
|
129
|
+
: null;
|
|
130
|
+
|
|
131
|
+
function cleanupNodeRecursively(node) {
|
|
132
|
+
if (node._componentInstance) {
|
|
133
|
+
unmountComponent(node._componentInstance);
|
|
134
|
+
}
|
|
135
|
+
node.childNodes.forEach(cleanupNodeRecursively);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function initLifecycleRoot(rootNode) {
|
|
139
|
+
if (!rootNode) return;
|
|
140
|
+
if (!observer) return;
|
|
141
|
+
observer.observe(rootNode, { childList: true, subtree: true });
|
|
142
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { signal } from './signals.js';
|
|
2
|
+
import { onMount } from './lifecycle.js';
|
|
3
|
+
import { createElement } from './dom.js';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import { reportErrorSafe } from './error-reporter.js';
|
|
6
|
+
|
|
7
|
+
const mdLoaders = (typeof import.meta !== 'undefined' && typeof import.meta.glob === 'function')
|
|
8
|
+
? import.meta.glob('/src/**/*.md', { query: '?raw', import: 'default' })
|
|
9
|
+
: {};
|
|
10
|
+
|
|
11
|
+
export function Markdown(props = {}) {
|
|
12
|
+
const html = signal('');
|
|
13
|
+
|
|
14
|
+
const parse = (md) => {
|
|
15
|
+
try {
|
|
16
|
+
return marked.parse(md ?? '');
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (typeof props.content === 'string') {
|
|
23
|
+
html(parse(props.content));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onMount(async () => {
|
|
27
|
+
if (typeof props.src !== 'string') return;
|
|
28
|
+
|
|
29
|
+
const base = typeof props.base === 'string' ? props.base : '/src';
|
|
30
|
+
const resolved = props.src.startsWith('./') ? (base + props.src.slice(1)) : props.src;
|
|
31
|
+
|
|
32
|
+
const loader = mdLoaders[resolved];
|
|
33
|
+
if (typeof loader === 'function') {
|
|
34
|
+
try {
|
|
35
|
+
const text = await loader();
|
|
36
|
+
html(parse(text ?? ''));
|
|
37
|
+
return;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
reportErrorSafe(e instanceof Error ? e : new Error(`Failed to load markdown: ${resolved}`), { phase: 'markdown.load', component: 'Markdown' });
|
|
40
|
+
html('');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const r = await fetch(resolved);
|
|
47
|
+
if (!r.ok) {
|
|
48
|
+
reportErrorSafe(new Error(`Markdown not found: ${resolved} (HTTP ${r.status})`), { phase: 'markdown.fetch', component: 'Markdown' });
|
|
49
|
+
html('');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const text = await r.text();
|
|
53
|
+
|
|
54
|
+
const looksLikeHtml = /^\s*<!doctype\s+html\b|^\s*<html\b/i.test(text);
|
|
55
|
+
if (looksLikeHtml) {
|
|
56
|
+
reportErrorSafe(new Error(`Markdown not found (served HTML fallback): ${resolved}`), { phase: 'markdown.fetch', component: 'Markdown' });
|
|
57
|
+
html('');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
html(parse(text));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
reportErrorSafe(e instanceof Error ? e : new Error(`Failed to fetch markdown: ${resolved}`), { phase: 'markdown.fetch', component: 'Markdown' });
|
|
63
|
+
html('');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const className = props.className ?? props.theme ?? '';
|
|
68
|
+
return createElement('div', {
|
|
69
|
+
className,
|
|
70
|
+
dangerouslySetInnerHTML: () => ({ __html: html() })
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { signal, effect } from './signals.js';
|
|
2
|
+
import { createElement } from './dom.js';
|
|
3
|
+
|
|
4
|
+
const hasWindow = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
5
|
+
|
|
6
|
+
const ROUTING_TRAILING_SLASH = (typeof __ROUND_ROUTING_TRAILING_SLASH__ !== 'undefined')
|
|
7
|
+
? Boolean(__ROUND_ROUTING_TRAILING_SLASH__)
|
|
8
|
+
: true;
|
|
9
|
+
|
|
10
|
+
const currentPath = signal(hasWindow ? window.location.pathname : '/');
|
|
11
|
+
let listenerInitialized = false;
|
|
12
|
+
|
|
13
|
+
let lastPathEvaluated = null;
|
|
14
|
+
let hasMatchForPath = false;
|
|
15
|
+
|
|
16
|
+
const pathHasMatch = signal(false);
|
|
17
|
+
const pathEvalReady = signal(true);
|
|
18
|
+
|
|
19
|
+
let defaultNotFoundComponent = null;
|
|
20
|
+
let autoNotFoundMounted = false;
|
|
21
|
+
let userProvidedNotFound = false;
|
|
22
|
+
|
|
23
|
+
function ensureListener() {
|
|
24
|
+
if (!hasWindow || listenerInitialized) return;
|
|
25
|
+
listenerInitialized = true;
|
|
26
|
+
|
|
27
|
+
mountAutoNotFound();
|
|
28
|
+
|
|
29
|
+
window.addEventListener('popstate', () => {
|
|
30
|
+
currentPath(window.location.pathname);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getPathname() {
|
|
35
|
+
return normalizePathname(currentPath());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function usePathname() {
|
|
39
|
+
return () => normalizePathname(currentPath());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getLocation() {
|
|
43
|
+
if (!hasWindow) {
|
|
44
|
+
return { pathname: normalizePathname('/'), search: '', hash: '' };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
pathname: normalizePathname(window.location.pathname),
|
|
48
|
+
search: window.location.search ?? '',
|
|
49
|
+
hash: window.location.hash ?? ''
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useLocation() {
|
|
54
|
+
return () => {
|
|
55
|
+
const pathname = normalizePathname(currentPath());
|
|
56
|
+
if (!hasWindow) return { pathname, search: '', hash: '' };
|
|
57
|
+
return { pathname, search: window.location.search ?? '', hash: window.location.hash ?? '' };
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getRouteReady() {
|
|
62
|
+
const pathname = normalizePathname(currentPath());
|
|
63
|
+
return Boolean(pathEvalReady()) && lastPathEvaluated === pathname;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useRouteReady() {
|
|
67
|
+
return () => {
|
|
68
|
+
const pathname = normalizePathname(currentPath());
|
|
69
|
+
return Boolean(pathEvalReady()) && lastPathEvaluated === pathname;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getIsNotFound() {
|
|
74
|
+
const pathname = normalizePathname(currentPath());
|
|
75
|
+
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
76
|
+
return !Boolean(pathHasMatch());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useIsNotFound() {
|
|
80
|
+
return () => {
|
|
81
|
+
const pathname = normalizePathname(currentPath());
|
|
82
|
+
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
83
|
+
return !Boolean(pathHasMatch());
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mountAutoNotFound() {
|
|
88
|
+
if (!hasWindow || autoNotFoundMounted) return;
|
|
89
|
+
autoNotFoundMounted = true;
|
|
90
|
+
|
|
91
|
+
const host = document.getElementById('app') ?? document.body;
|
|
92
|
+
const root = document.createElement('div');
|
|
93
|
+
root.setAttribute('data-round-auto-notfound', '1');
|
|
94
|
+
host.appendChild(root);
|
|
95
|
+
|
|
96
|
+
const view = createElement('span', { style: { display: 'contents' } }, () => {
|
|
97
|
+
if (userProvidedNotFound) return null;
|
|
98
|
+
|
|
99
|
+
const pathname = normalizePathname(currentPath());
|
|
100
|
+
const ready = pathEvalReady();
|
|
101
|
+
const hasMatch = pathHasMatch();
|
|
102
|
+
|
|
103
|
+
if (!ready) return null;
|
|
104
|
+
if (lastPathEvaluated !== pathname) return null;
|
|
105
|
+
if (hasMatch) return null;
|
|
106
|
+
|
|
107
|
+
const Comp = defaultNotFoundComponent;
|
|
108
|
+
if (typeof Comp === 'function') {
|
|
109
|
+
return createElement(Comp, { pathname });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return createElement('div', { style: { padding: '16px' } },
|
|
113
|
+
createElement('h1', null, '404'),
|
|
114
|
+
createElement('p', null, 'Page not found: ', pathname)
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
root.appendChild(view);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function navigate(to, options = {}) {
|
|
122
|
+
if (!hasWindow) return;
|
|
123
|
+
ensureListener();
|
|
124
|
+
|
|
125
|
+
const normalizedTo = normalizeTo(to);
|
|
126
|
+
const replace = Boolean(options.replace);
|
|
127
|
+
if (replace) window.history.replaceState({}, '', normalizedTo);
|
|
128
|
+
else window.history.pushState({}, '', normalizedTo);
|
|
129
|
+
|
|
130
|
+
currentPath(window.location.pathname);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyHead({ title, meta, links, icon, favicon }) {
|
|
134
|
+
if (!hasWindow) return;
|
|
135
|
+
|
|
136
|
+
if (typeof title === 'string') {
|
|
137
|
+
document.title = title;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
document.querySelectorAll('[data-round-head="1"]').forEach((n) => n.remove());
|
|
141
|
+
|
|
142
|
+
const iconHref = icon ?? favicon;
|
|
143
|
+
if (typeof iconHref === 'string' && iconHref.length) {
|
|
144
|
+
const el = document.createElement('link');
|
|
145
|
+
el.setAttribute('data-round-head', '1');
|
|
146
|
+
el.setAttribute('rel', 'icon');
|
|
147
|
+
el.setAttribute('href', iconHref);
|
|
148
|
+
document.head.appendChild(el);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Array.isArray(links)) {
|
|
152
|
+
links.forEach((l) => {
|
|
153
|
+
if (!l || typeof l !== 'object') return;
|
|
154
|
+
const el = document.createElement('link');
|
|
155
|
+
el.setAttribute('data-round-head', '1');
|
|
156
|
+
Object.entries(l).forEach(([k, v]) => {
|
|
157
|
+
if (v === null || v === undefined) return;
|
|
158
|
+
el.setAttribute(k, String(v));
|
|
159
|
+
});
|
|
160
|
+
document.head.appendChild(el);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(meta)) {
|
|
165
|
+
meta.forEach((entry) => {
|
|
166
|
+
if (!entry) return;
|
|
167
|
+
const el = document.createElement('meta');
|
|
168
|
+
el.setAttribute('data-round-head', '1');
|
|
169
|
+
|
|
170
|
+
if (Array.isArray(entry) && entry.length >= 2) {
|
|
171
|
+
const [name, content] = entry;
|
|
172
|
+
if (typeof name === 'string') el.setAttribute('name', name);
|
|
173
|
+
el.setAttribute('content', String(content ?? ''));
|
|
174
|
+
} else if (typeof entry === 'object') {
|
|
175
|
+
Object.entries(entry).forEach(([k, v]) => {
|
|
176
|
+
if (v === null || v === undefined) return;
|
|
177
|
+
el.setAttribute(k, String(v));
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
document.head.appendChild(el);
|
|
184
|
+
});
|
|
185
|
+
} else if (meta && typeof meta === 'object') {
|
|
186
|
+
Object.entries(meta).forEach(([name, content]) => {
|
|
187
|
+
if (typeof name !== 'string') return;
|
|
188
|
+
const el = document.createElement('meta');
|
|
189
|
+
el.setAttribute('data-round-head', '1');
|
|
190
|
+
el.setAttribute('name', name);
|
|
191
|
+
el.setAttribute('content', String(content ?? ''));
|
|
192
|
+
document.head.appendChild(el);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function startHead(_head) {
|
|
198
|
+
return _head;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function splitUrl(url) {
|
|
202
|
+
const str = String(url ?? '');
|
|
203
|
+
const hashIdx = str.indexOf('#');
|
|
204
|
+
const queryIdx = str.indexOf('?');
|
|
205
|
+
const cutIdx = (hashIdx === -1)
|
|
206
|
+
? queryIdx
|
|
207
|
+
: (queryIdx === -1 ? hashIdx : Math.min(hashIdx, queryIdx));
|
|
208
|
+
|
|
209
|
+
if (cutIdx === -1) return { path: str, suffix: '' };
|
|
210
|
+
return { path: str.slice(0, cutIdx), suffix: str.slice(cutIdx) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizePathname(p) {
|
|
214
|
+
let pathname = String(p ?? '/');
|
|
215
|
+
if (!pathname.startsWith('/')) pathname = '/' + pathname;
|
|
216
|
+
if (pathname.length > 1) {
|
|
217
|
+
if (ROUTING_TRAILING_SLASH) {
|
|
218
|
+
if (!pathname.endsWith('/')) pathname += '/';
|
|
219
|
+
} else {
|
|
220
|
+
if (pathname.endsWith('/')) pathname = pathname.slice(0, -1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return pathname;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeTo(to) {
|
|
227
|
+
const { path, suffix } = splitUrl(to);
|
|
228
|
+
if (!path.startsWith('/')) return String(to ?? '');
|
|
229
|
+
return normalizePathname(path) + suffix;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function matchRoute(route, pathname) {
|
|
233
|
+
const r = normalizePathname(route);
|
|
234
|
+
const p = normalizePathname(pathname);
|
|
235
|
+
return r === p;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function beginPathEvaluation(pathname) {
|
|
239
|
+
if (pathname !== lastPathEvaluated) {
|
|
240
|
+
lastPathEvaluated = pathname;
|
|
241
|
+
hasMatchForPath = false;
|
|
242
|
+
pathHasMatch(false);
|
|
243
|
+
|
|
244
|
+
pathEvalReady(false);
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
if (lastPathEvaluated !== pathname) return;
|
|
247
|
+
pathEvalReady(true);
|
|
248
|
+
}, 0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function setNotFound(Component) {
|
|
253
|
+
defaultNotFoundComponent = Component;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function Route(props = {}) {
|
|
257
|
+
ensureListener();
|
|
258
|
+
|
|
259
|
+
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
260
|
+
const pathname = normalizePathname(currentPath());
|
|
261
|
+
beginPathEvaluation(pathname);
|
|
262
|
+
const route = props.route ?? '/';
|
|
263
|
+
if (!matchRoute(route, pathname)) return null;
|
|
264
|
+
|
|
265
|
+
hasMatchForPath = true;
|
|
266
|
+
pathHasMatch(true);
|
|
267
|
+
const mergedHead = (props.head && typeof props.head === 'object') ? props.head : {};
|
|
268
|
+
const meta = props.description
|
|
269
|
+
? ([{ name: 'description', content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []))
|
|
270
|
+
: (mergedHead.meta ?? props.meta);
|
|
271
|
+
const links = mergedHead.links ?? props.links;
|
|
272
|
+
const title = mergedHead.title ?? props.title;
|
|
273
|
+
const icon = mergedHead.icon ?? props.icon;
|
|
274
|
+
const favicon = mergedHead.favicon ?? props.favicon;
|
|
275
|
+
|
|
276
|
+
applyHead({ title, meta, links, icon, favicon });
|
|
277
|
+
return props.children;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function Page(props = {}) {
|
|
282
|
+
ensureListener();
|
|
283
|
+
|
|
284
|
+
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
285
|
+
const pathname = normalizePathname(currentPath());
|
|
286
|
+
beginPathEvaluation(pathname);
|
|
287
|
+
const route = props.route ?? '/';
|
|
288
|
+
if (!matchRoute(route, pathname)) return null;
|
|
289
|
+
|
|
290
|
+
hasMatchForPath = true;
|
|
291
|
+
pathHasMatch(true);
|
|
292
|
+
const mergedHead = (props.head && typeof props.head === 'object') ? props.head : {};
|
|
293
|
+
const meta = props.description
|
|
294
|
+
? ([{ name: 'description', content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []))
|
|
295
|
+
: (mergedHead.meta ?? props.meta);
|
|
296
|
+
const links = mergedHead.links ?? props.links;
|
|
297
|
+
const title = mergedHead.title ?? props.title;
|
|
298
|
+
const icon = mergedHead.icon ?? props.icon;
|
|
299
|
+
const favicon = mergedHead.favicon ?? props.favicon;
|
|
300
|
+
|
|
301
|
+
applyHead({ title, meta, links, icon, favicon });
|
|
302
|
+
return props.children;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function NotFound(props = {}) {
|
|
307
|
+
ensureListener();
|
|
308
|
+
|
|
309
|
+
userProvidedNotFound = true;
|
|
310
|
+
|
|
311
|
+
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
312
|
+
const pathname = normalizePathname(currentPath());
|
|
313
|
+
beginPathEvaluation(pathname);
|
|
314
|
+
|
|
315
|
+
const ready = pathEvalReady();
|
|
316
|
+
const hasMatch = pathHasMatch();
|
|
317
|
+
if (!ready) return null;
|
|
318
|
+
if (lastPathEvaluated !== pathname) return null;
|
|
319
|
+
|
|
320
|
+
if (hasMatch) return null;
|
|
321
|
+
|
|
322
|
+
const Comp = props.component ?? defaultNotFoundComponent;
|
|
323
|
+
if (typeof Comp === 'function') {
|
|
324
|
+
return createElement(Comp, { pathname });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (props.children !== undefined) return props.children;
|
|
328
|
+
|
|
329
|
+
return createElement('div', { style: { padding: '16px' } },
|
|
330
|
+
createElement('h1', null, '404'),
|
|
331
|
+
createElement('p', null, 'Page not found: ', pathname)
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function Link(props = {}) {
|
|
337
|
+
ensureListener();
|
|
338
|
+
|
|
339
|
+
const rawHref = props.href ?? props.to ?? '#';
|
|
340
|
+
const href = spaNormalizeHref(rawHref);
|
|
341
|
+
|
|
342
|
+
const spa = props.spa !== undefined ? Boolean(props.spa) : true;
|
|
343
|
+
const reload = Boolean(props.reload);
|
|
344
|
+
|
|
345
|
+
const onClick = (e) => {
|
|
346
|
+
if (typeof props.onClick === 'function') props.onClick(e);
|
|
347
|
+
if (e.defaultPrevented) return;
|
|
348
|
+
|
|
349
|
+
// Classic navigation: allow the browser to reload.
|
|
350
|
+
if (!spa || reload) return;
|
|
351
|
+
|
|
352
|
+
if (e.button !== 0) return;
|
|
353
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
354
|
+
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
navigate(href);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const { children, to, ...rest } = props;
|
|
360
|
+
const normalizedChildren = Array.isArray(children)
|
|
361
|
+
? children
|
|
362
|
+
: (children === undefined || children === null ? [] : [children]);
|
|
363
|
+
|
|
364
|
+
return createElement('a', { ...rest, href, onClick }, ...normalizedChildren);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function spaNormalizeHref(href) {
|
|
368
|
+
const str = String(href ?? '#');
|
|
369
|
+
if (!str.startsWith('/')) return str;
|
|
370
|
+
return normalizeTo(str);
|
|
371
|
+
}
|