vorzelajs 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 +188 -0
- package/bin/vorzelajs.mjs +2 -0
- package/dist/analytics.d.ts +132 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +690 -0
- package/dist/cli/build.d.ts +2 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +22 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +93 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +29 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +43 -0
- package/dist/cookie.d.ts +33 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +198 -0
- package/dist/debug/error-stack.d.ts +14 -0
- package/dist/debug/error-stack.d.ts.map +1 -0
- package/dist/debug/error-stack.js +52 -0
- package/dist/internal/document.d.ts +12 -0
- package/dist/internal/document.d.ts.map +1 -0
- package/dist/internal/document.jsx +56 -0
- package/dist/internal/entry-client.d.ts +2 -0
- package/dist/internal/entry-client.d.ts.map +1 -0
- package/dist/internal/entry-client.jsx +8 -0
- package/dist/internal/entry-server.d.ts +14 -0
- package/dist/internal/entry-server.d.ts.map +1 -0
- package/dist/internal/entry-server.jsx +71 -0
- package/dist/runtime/create-route.d.ts +8 -0
- package/dist/runtime/create-route.d.ts.map +1 -0
- package/dist/runtime/create-route.js +18 -0
- package/dist/runtime/head.d.ts +10 -0
- package/dist/runtime/head.d.ts.map +1 -0
- package/dist/runtime/head.js +111 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.jsx +4 -0
- package/dist/runtime/navigation.d.ts +36 -0
- package/dist/runtime/navigation.d.ts.map +1 -0
- package/dist/runtime/navigation.js +70 -0
- package/dist/runtime/path.d.ts +11 -0
- package/dist/runtime/path.d.ts.map +1 -0
- package/dist/runtime/path.js +80 -0
- package/dist/runtime/resolve.d.ts +11 -0
- package/dist/runtime/resolve.d.ts.map +1 -0
- package/dist/runtime/resolve.js +449 -0
- package/dist/runtime/runtime.d.ts +40 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.jsx +779 -0
- package/dist/runtime/search.d.ts +23 -0
- package/dist/runtime/search.d.ts.map +1 -0
- package/dist/runtime/search.js +178 -0
- package/dist/runtime/server.d.ts +10 -0
- package/dist/runtime/server.d.ts.map +1 -0
- package/dist/runtime/server.js +5 -0
- package/dist/runtime/types.d.ts +248 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +1 -0
- package/dist/seo.d.ts +16 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +69 -0
- package/dist/server/index.d.ts +53 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +268 -0
- package/dist/session.d.ts +23 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +58 -0
- package/dist/vite/index.d.ts +13 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +174 -0
- package/dist/vite/routes-plugin.d.ts +4 -0
- package/dist/vite/routes-plugin.d.ts.map +1 -0
- package/dist/vite/routes-plugin.js +345 -0
- package/dist/vite/server-only.d.ts +3 -0
- package/dist/vite/server-only.d.ts.map +1 -0
- package/dist/vite/server-only.js +342 -0
- package/package.json +76 -0
- package/templates/bare/README.md +22 -0
- package/templates/bare/src/components/counter-card.tsx +19 -0
- package/templates/bare/src/routes/__root.tsx +24 -0
- package/templates/bare/src/routes/index.tsx +36 -0
- package/templates/base/gitignore +4 -0
- package/templates/base/public/favicon.svg +5 -0
- package/templates/base/tsconfig.json +18 -0
- package/templates/modern/README.md +28 -0
- package/templates/modern/src/components/counter-card.tsx +19 -0
- package/templates/modern/src/routes/__root.tsx +42 -0
- package/templates/modern/src/routes/about.tsx +55 -0
- package/templates/modern/src/routes/index.tsx +51 -0
- package/templates/styling/css/styles.css +269 -0
- package/templates/styling/css-modules/styles.css +269 -0
- package/templates/styling/tailwind/styles.css +271 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { createContext, createEffect, createSignal, ErrorBoundary, onCleanup, sharedConfig, splitProps, useContext } from 'solid-js';
|
|
2
|
+
import { hydrate } from 'solid-js/web';
|
|
3
|
+
import { formatParsedStack, parseErrorStack } from '../debug/error-stack';
|
|
4
|
+
import { syncHead } from './head';
|
|
5
|
+
import { normalizeHref } from './path';
|
|
6
|
+
import { materializeBootstrapPayload, materializePayloadEnvelope, } from './resolve';
|
|
7
|
+
import { parseSearchString, resolveMergedSearch, resolveNavigateHref } from './search';
|
|
8
|
+
const PREFETCH_CACHE_MAX_ENTRIES = 24;
|
|
9
|
+
const PREFETCH_CACHE_TTL_MS = 30_000;
|
|
10
|
+
const isDev = import.meta.env.DEV;
|
|
11
|
+
const RouterContext = createContext();
|
|
12
|
+
const OutletContext = createContext();
|
|
13
|
+
const MatchContext = createContext();
|
|
14
|
+
const ISLAND_ROOT_ATTRIBUTE = 'data-vrz-island-root';
|
|
15
|
+
const ISLAND_CONTEXT_ATTRIBUTE = 'data-vrz-hctx';
|
|
16
|
+
const RETRY_ATTRIBUTE = 'data-vrz-retry';
|
|
17
|
+
let _activeRouter;
|
|
18
|
+
function formatMatchChain(matches) {
|
|
19
|
+
return matches.map((match) => `${match.id}:${match.hydration}`).join(' > ');
|
|
20
|
+
}
|
|
21
|
+
function logDevRouterEvent(label, details) {
|
|
22
|
+
if (!isDev || typeof window === 'undefined') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.info(`[VorzelaJs][router] ${label}`, details);
|
|
26
|
+
}
|
|
27
|
+
function logDevRouterError(scope, error, meta = {}) {
|
|
28
|
+
if (!isDev || typeof window === 'undefined') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const parsedStack = parseErrorStack(error);
|
|
32
|
+
console.groupCollapsed(`[VorzelaJs][router:${scope}] ${error instanceof Error ? error.message : 'Unexpected error'}`);
|
|
33
|
+
console.info(meta);
|
|
34
|
+
if (parsedStack) {
|
|
35
|
+
const lines = formatParsedStack(parsedStack);
|
|
36
|
+
if (lines.length > 0) {
|
|
37
|
+
console.info(lines.join('\n'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.error(error);
|
|
41
|
+
console.groupEnd();
|
|
42
|
+
}
|
|
43
|
+
function prunePrefetchCache(prefetchCache) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
for (const [href, entry] of prefetchCache) {
|
|
46
|
+
if (now - entry.createdAt > PREFETCH_CACHE_TTL_MS) {
|
|
47
|
+
prefetchCache.delete(href);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
while (prefetchCache.size > PREFETCH_CACHE_MAX_ENTRIES) {
|
|
51
|
+
const oldestKey = prefetchCache.keys().next().value;
|
|
52
|
+
if (!oldestKey) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
prefetchCache.delete(oldestKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function commitState(nextState, setState, options = {}) {
|
|
59
|
+
setState(nextState);
|
|
60
|
+
syncHead(nextState.head);
|
|
61
|
+
if (typeof window === 'undefined')
|
|
62
|
+
return;
|
|
63
|
+
const href = `${nextState.pathname}${nextState.search}`;
|
|
64
|
+
if (options.replace) {
|
|
65
|
+
window.history.replaceState({}, '', href);
|
|
66
|
+
}
|
|
67
|
+
else if (!options.force) {
|
|
68
|
+
window.history.pushState({}, '', href);
|
|
69
|
+
}
|
|
70
|
+
if (options.scroll !== false) {
|
|
71
|
+
window.scrollTo({ left: 0, top: 0 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function DefaultNotFoundComponent() {
|
|
75
|
+
return (<div class="page-card page-card--centered">
|
|
76
|
+
<p class="eyebrow">404</p>
|
|
77
|
+
<h1>Page not found</h1>
|
|
78
|
+
<p class="lead-copy">The requested route could not be resolved.</p>
|
|
79
|
+
</div>);
|
|
80
|
+
}
|
|
81
|
+
function DefaultRouteErrorComponent(props) {
|
|
82
|
+
const debugLines = () => formatParsedStack(props.error.debug);
|
|
83
|
+
return (<section class="page-card page-card--centered">
|
|
84
|
+
<p class="eyebrow">{props.error.status}</p>
|
|
85
|
+
<h1>Route failed</h1>
|
|
86
|
+
<p class="lead-copy">{props.error.message}</p>
|
|
87
|
+
<p class="mono-note">phase: {props.error.phase}</p>
|
|
88
|
+
{isDev && debugLines().length > 0 && (<details class="mono-note" open>
|
|
89
|
+
<summary>Debug stack</summary>
|
|
90
|
+
<pre>{debugLines().join('\n')}</pre>
|
|
91
|
+
</details>)}
|
|
92
|
+
<button type="button" class="button button--secondary" data-vrz-retry="">
|
|
93
|
+
Try again
|
|
94
|
+
</button>
|
|
95
|
+
</section>);
|
|
96
|
+
}
|
|
97
|
+
function createRenderError(error) {
|
|
98
|
+
const debug = isDev ? parseErrorStack(error) : undefined;
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
return {
|
|
101
|
+
debug,
|
|
102
|
+
message: error.message || 'Unexpected render error',
|
|
103
|
+
name: error.name || 'Error',
|
|
104
|
+
phase: 'render',
|
|
105
|
+
status: 500,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
debug,
|
|
110
|
+
message: typeof error === 'string' ? error : 'Unexpected render error',
|
|
111
|
+
name: 'Error',
|
|
112
|
+
phase: 'render',
|
|
113
|
+
status: 500,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function RouteErrorView(props) {
|
|
117
|
+
const ErrorComponent = props.errorComponent;
|
|
118
|
+
return ErrorComponent
|
|
119
|
+
? <ErrorComponent error={props.error} reset={props.reset}/>
|
|
120
|
+
: <DefaultRouteErrorComponent error={props.error} reset={props.reset}/>;
|
|
121
|
+
}
|
|
122
|
+
function createAfterLoadLocation(pathname, search) {
|
|
123
|
+
if (typeof window === 'undefined') {
|
|
124
|
+
const url = new URL(`http://localhost${pathname}${search}`);
|
|
125
|
+
return {
|
|
126
|
+
href: url.href,
|
|
127
|
+
pathname,
|
|
128
|
+
search,
|
|
129
|
+
searchParams: url.searchParams,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const url = new URL(`${pathname}${search}`, window.location.origin);
|
|
133
|
+
return {
|
|
134
|
+
href: url.href,
|
|
135
|
+
pathname,
|
|
136
|
+
search,
|
|
137
|
+
searchParams: url.searchParams,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function isHydrationBoundary(state, index) {
|
|
141
|
+
const currentMatch = state.matches[index];
|
|
142
|
+
const previousMatch = state.matches[index - 1];
|
|
143
|
+
return currentMatch?.hydration === 'client' && previousMatch?.hydration !== 'client';
|
|
144
|
+
}
|
|
145
|
+
function canHydrateBoundary(state, startIndex) {
|
|
146
|
+
const subtreeIds = new Set(state.matches.slice(startIndex).map((match) => match.id));
|
|
147
|
+
if (state.notFound
|
|
148
|
+
&& subtreeIds.has(state.notFound.targetId)
|
|
149
|
+
&& state.notFound.handlerId
|
|
150
|
+
&& !subtreeIds.has(state.notFound.handlerId)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (state.routeError
|
|
154
|
+
&& subtreeIds.has(state.routeError.targetId)
|
|
155
|
+
&& state.routeError.handlerId
|
|
156
|
+
&& !subtreeIds.has(state.routeError.handlerId)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
function getHydrationRoots(state) {
|
|
162
|
+
return state.matches.flatMap((match, index) => (isHydrationBoundary(state, index) && canHydrateBoundary(state, index)
|
|
163
|
+
? [{ id: match.id, index }]
|
|
164
|
+
: []));
|
|
165
|
+
}
|
|
166
|
+
function createHydrationSubtreeState(state, startIndex) {
|
|
167
|
+
const matches = state.matches.slice(startIndex);
|
|
168
|
+
const subtreeIds = new Set(matches.map((match) => match.id));
|
|
169
|
+
return {
|
|
170
|
+
matches,
|
|
171
|
+
notFound: state.notFound && subtreeIds.has(state.notFound.targetId)
|
|
172
|
+
&& (!state.notFound.handlerId || subtreeIds.has(state.notFound.handlerId))
|
|
173
|
+
? state.notFound
|
|
174
|
+
: undefined,
|
|
175
|
+
payloadHtml: undefined,
|
|
176
|
+
renderSource: 'component',
|
|
177
|
+
routeError: state.routeError && subtreeIds.has(state.routeError.targetId)
|
|
178
|
+
&& (!state.routeError.handlerId || subtreeIds.has(state.routeError.handlerId))
|
|
179
|
+
? state.routeError
|
|
180
|
+
: undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function replaceAppHtml(html) {
|
|
184
|
+
const app = document.getElementById('app');
|
|
185
|
+
if (!(app instanceof HTMLElement)) {
|
|
186
|
+
throw new Error('Missing VorzelaJs app root');
|
|
187
|
+
}
|
|
188
|
+
app.innerHTML = html;
|
|
189
|
+
return app;
|
|
190
|
+
}
|
|
191
|
+
function readIslandHydrationContext(mount) {
|
|
192
|
+
const raw = mount.getAttribute(ISLAND_CONTEXT_ATTRIBUTE);
|
|
193
|
+
if (!raw) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const sep = raw.indexOf(':');
|
|
197
|
+
if (sep < 0) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
id: raw.slice(0, sep),
|
|
202
|
+
count: Number(raw.slice(sep + 1)),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function isRouteRedirectData(value) {
|
|
206
|
+
return typeof value === 'object'
|
|
207
|
+
&& value !== null
|
|
208
|
+
&& 'to' in value
|
|
209
|
+
&& 'replace' in value
|
|
210
|
+
&& 'status' in value;
|
|
211
|
+
}
|
|
212
|
+
function isRouteRedirectEnvelope(value) {
|
|
213
|
+
return typeof value === 'object'
|
|
214
|
+
&& value !== null
|
|
215
|
+
&& 'redirect' in value
|
|
216
|
+
&& isRouteRedirectData(value.redirect);
|
|
217
|
+
}
|
|
218
|
+
function reportAfterLoadError(match, error) {
|
|
219
|
+
logDevRouterError('afterLoad', error, { routeId: match.id });
|
|
220
|
+
console.error(`[VorzelaJs] afterLoad failed for route ${match.id}`, error);
|
|
221
|
+
}
|
|
222
|
+
function runAfterLoadHooks(router, state) {
|
|
223
|
+
if (typeof window === 'undefined' || state.matches.length === 0 || state.notFound || state.routeError) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const location = createAfterLoadLocation(state.pathname, state.search);
|
|
227
|
+
for (const match of state.matches) {
|
|
228
|
+
const afterLoad = match.route.options.afterLoad;
|
|
229
|
+
if (!afterLoad) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const afterLoadContext = {
|
|
233
|
+
context: router.context,
|
|
234
|
+
loaderData: match.loaderData,
|
|
235
|
+
location,
|
|
236
|
+
params: match.params,
|
|
237
|
+
pathname: state.pathname,
|
|
238
|
+
search: match.search,
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
const result = afterLoad(afterLoadContext);
|
|
242
|
+
void Promise.resolve(result).catch((error) => {
|
|
243
|
+
reportAfterLoadError(match, error);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
reportAfterLoadError(match, error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function createRouter(initialPayload, options = {}) {
|
|
252
|
+
const [state, setState] = createSignal({
|
|
253
|
+
head: initialPayload.head,
|
|
254
|
+
matches: [],
|
|
255
|
+
notFound: initialPayload.notFound,
|
|
256
|
+
pathname: initialPayload.pathname,
|
|
257
|
+
renderSource: 'component',
|
|
258
|
+
routeError: initialPayload.routeError,
|
|
259
|
+
search: initialPayload.search,
|
|
260
|
+
});
|
|
261
|
+
const context = options.context ?? {};
|
|
262
|
+
let initialized = false;
|
|
263
|
+
let afterLoadFrame;
|
|
264
|
+
let afterLoadToken = 0;
|
|
265
|
+
const islandDisposers = [];
|
|
266
|
+
const prefetchCache = new Map();
|
|
267
|
+
const resolveTargetHref = (to) => {
|
|
268
|
+
const currentState = state();
|
|
269
|
+
return typeof to === 'string'
|
|
270
|
+
? normalizeHref(to)
|
|
271
|
+
: resolveNavigateHref(`${currentState.pathname}${currentState.search}`, to, parseSearchString(currentState.search));
|
|
272
|
+
};
|
|
273
|
+
const getCachedNavigationState = (href) => {
|
|
274
|
+
prunePrefetchCache(prefetchCache);
|
|
275
|
+
return prefetchCache.get(href)?.promise;
|
|
276
|
+
};
|
|
277
|
+
const storeNavigationState = (href, promise) => {
|
|
278
|
+
prefetchCache.set(href, {
|
|
279
|
+
createdAt: Date.now(),
|
|
280
|
+
promise,
|
|
281
|
+
});
|
|
282
|
+
prunePrefetchCache(prefetchCache);
|
|
283
|
+
return promise;
|
|
284
|
+
};
|
|
285
|
+
const disposeHydrationRoots = () => {
|
|
286
|
+
for (const dispose of islandDisposers.splice(0)) {
|
|
287
|
+
dispose();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const scheduleAfterLoad = (nextState) => {
|
|
291
|
+
if (typeof window === 'undefined') {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const token = ++afterLoadToken;
|
|
295
|
+
if (afterLoadFrame !== undefined) {
|
|
296
|
+
window.cancelAnimationFrame(afterLoadFrame);
|
|
297
|
+
}
|
|
298
|
+
afterLoadFrame = window.requestAnimationFrame(() => {
|
|
299
|
+
afterLoadFrame = undefined;
|
|
300
|
+
if (token !== afterLoadToken) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
runAfterLoadHooks(router, nextState);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const hydrateIslands = (nextState) => {
|
|
307
|
+
if (typeof window === 'undefined') {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const app = document.getElementById('app');
|
|
311
|
+
if (!(app instanceof HTMLElement)) {
|
|
312
|
+
throw new Error('Missing VorzelaJs app root');
|
|
313
|
+
}
|
|
314
|
+
const mountPoints = Array.from(app.querySelectorAll(`[${ISLAND_ROOT_ATTRIBUTE}]`));
|
|
315
|
+
const hydrationRoots = getHydrationRoots(nextState);
|
|
316
|
+
for (const root of hydrationRoots) {
|
|
317
|
+
const mount = mountPoints.find((candidate) => candidate.getAttribute(ISLAND_ROOT_ATTRIBUTE) === root.id);
|
|
318
|
+
if (!mount) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const subtreeState = createHydrationSubtreeState(nextState, root.index);
|
|
322
|
+
const retry = () => {
|
|
323
|
+
void router.navigate(`${router.state().pathname}${router.state().search}`, {
|
|
324
|
+
force: true,
|
|
325
|
+
replace: true,
|
|
326
|
+
scroll: false,
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const hctx = readIslandHydrationContext(mount);
|
|
330
|
+
islandDisposers.push(hydrate(() => {
|
|
331
|
+
if (hctx && sharedConfig.context) {
|
|
332
|
+
sharedConfig.context.count = hctx.count;
|
|
333
|
+
}
|
|
334
|
+
return renderResolvedMatches(subtreeState, { retry });
|
|
335
|
+
}, mount, {
|
|
336
|
+
renderId: hctx?.id,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
logDevRouterEvent('hydrate', {
|
|
340
|
+
islands: hydrationRoots.map((root) => root.id),
|
|
341
|
+
matches: formatMatchChain(nextState.matches),
|
|
342
|
+
pathname: `${nextState.pathname}${nextState.search}`,
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
const mountState = (nextState, navigationOptions = {}) => {
|
|
346
|
+
disposeHydrationRoots();
|
|
347
|
+
if (typeof window !== 'undefined' && nextState.payloadHtml) {
|
|
348
|
+
replaceAppHtml(nextState.payloadHtml);
|
|
349
|
+
}
|
|
350
|
+
commitState(nextState, setState, navigationOptions);
|
|
351
|
+
hydrateIslands(nextState);
|
|
352
|
+
scheduleAfterLoad(nextState);
|
|
353
|
+
if (nextState.payloadHtml) {
|
|
354
|
+
storeNavigationState(`${nextState.pathname}${nextState.search}`, Promise.resolve(nextState));
|
|
355
|
+
}
|
|
356
|
+
logDevRouterEvent('commit', {
|
|
357
|
+
matches: formatMatchChain(nextState.matches),
|
|
358
|
+
pathname: `${nextState.pathname}${nextState.search}`,
|
|
359
|
+
renderSource: nextState.renderSource,
|
|
360
|
+
routeError: nextState.routeError?.error.message,
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const fetchNavigationState = async (href) => {
|
|
364
|
+
const response = await fetch(`/__vorzela/payload?path=${encodeURIComponent(href)}`, {
|
|
365
|
+
credentials: 'same-origin',
|
|
366
|
+
headers: {
|
|
367
|
+
'X-Vorzela-Navigation': 'payload',
|
|
368
|
+
},
|
|
369
|
+
redirect: 'manual',
|
|
370
|
+
});
|
|
371
|
+
let payload;
|
|
372
|
+
try {
|
|
373
|
+
payload = await response.json();
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
throw new Error(`Failed to parse route payload for ${href}: ${error.message}`);
|
|
377
|
+
}
|
|
378
|
+
if (isRouteRedirectEnvelope(payload)) {
|
|
379
|
+
return payload.redirect;
|
|
380
|
+
}
|
|
381
|
+
if (typeof payload !== 'object'
|
|
382
|
+
|| payload === null
|
|
383
|
+
|| !('html' in payload)
|
|
384
|
+
|| !('matches' in payload)) {
|
|
385
|
+
throw new Error(`Unexpected route payload for ${href}`);
|
|
386
|
+
}
|
|
387
|
+
return materializePayloadEnvelope(payload);
|
|
388
|
+
};
|
|
389
|
+
const requestNavigationState = (href, reason) => {
|
|
390
|
+
const cached = getCachedNavigationState(href);
|
|
391
|
+
if (cached) {
|
|
392
|
+
logDevRouterEvent(`${reason}:cache-hit`, { href });
|
|
393
|
+
return cached;
|
|
394
|
+
}
|
|
395
|
+
logDevRouterEvent(`${reason}:network`, { href });
|
|
396
|
+
const pending = fetchNavigationState(href).catch((error) => {
|
|
397
|
+
prefetchCache.delete(href);
|
|
398
|
+
throw error;
|
|
399
|
+
});
|
|
400
|
+
return storeNavigationState(href, pending);
|
|
401
|
+
};
|
|
402
|
+
const prefetch = async (to) => {
|
|
403
|
+
if (typeof window === 'undefined') {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const href = resolveTargetHref(to);
|
|
407
|
+
if (href === `${state().pathname}${state().search}`) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
await requestNavigationState(href, 'prefetch');
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logDevRouterError('prefetch', error, { href });
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const navigate = async (to, options = {}) => {
|
|
418
|
+
const currentState = state();
|
|
419
|
+
const href = resolveTargetHref(to);
|
|
420
|
+
const replace = typeof to === 'string' ? options.replace : to.replace ?? options.replace;
|
|
421
|
+
const scroll = typeof to === 'string' ? options.scroll : to.scroll ?? options.scroll;
|
|
422
|
+
if (!options.force && href === `${currentState.pathname}${currentState.search}`) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
let nextState;
|
|
426
|
+
try {
|
|
427
|
+
nextState = await requestNavigationState(href, 'navigate');
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
logDevRouterError('navigate', error, { href });
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
if (isRouteRedirectData(nextState)) {
|
|
434
|
+
await navigate(nextState.to, {
|
|
435
|
+
force: true,
|
|
436
|
+
replace: nextState.replace,
|
|
437
|
+
scroll,
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
mountState(nextState, {
|
|
442
|
+
...options,
|
|
443
|
+
replace,
|
|
444
|
+
scroll,
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
const setSearch = (search, options = {}) => {
|
|
448
|
+
const currentState = state();
|
|
449
|
+
const nextSearch = resolveMergedSearch(parseSearchString(currentState.search), parseSearchString(currentState.search), search);
|
|
450
|
+
return navigate({
|
|
451
|
+
replace: options.replace,
|
|
452
|
+
scroll: options.scroll,
|
|
453
|
+
search: nextSearch,
|
|
454
|
+
to: currentState.pathname,
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
const init = async () => {
|
|
458
|
+
const nextState = await materializeBootstrapPayload(initialPayload);
|
|
459
|
+
mountState(nextState, { force: true, replace: true, scroll: false });
|
|
460
|
+
logDevRouterEvent('init', {
|
|
461
|
+
matches: formatMatchChain(nextState.matches),
|
|
462
|
+
pathname: `${nextState.pathname}${nextState.search}`,
|
|
463
|
+
});
|
|
464
|
+
if (!initialized && typeof window !== 'undefined') {
|
|
465
|
+
window.addEventListener('popstate', () => {
|
|
466
|
+
void navigate(`${window.location.pathname}${window.location.search}`, {
|
|
467
|
+
force: true,
|
|
468
|
+
replace: true,
|
|
469
|
+
scroll: false,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
document.addEventListener('click', (event) => {
|
|
473
|
+
if (event.defaultPrevented
|
|
474
|
+
|| event.button !== 0
|
|
475
|
+
|| event.metaKey
|
|
476
|
+
|| event.altKey
|
|
477
|
+
|| event.ctrlKey
|
|
478
|
+
|| event.shiftKey) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const target = event.target;
|
|
482
|
+
if (!(target instanceof Element)) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const retryButton = target.closest(`[${RETRY_ATTRIBUTE}]`);
|
|
486
|
+
if (retryButton instanceof HTMLButtonElement) {
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
void navigate(`${window.location.pathname}${window.location.search}`, {
|
|
489
|
+
force: true,
|
|
490
|
+
replace: true,
|
|
491
|
+
scroll: false,
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const anchor = target.closest('a[href]');
|
|
496
|
+
if (!(anchor instanceof HTMLAnchorElement)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (anchor.target === '_blank' || anchor.hasAttribute('download')) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const nextUrl = new URL(anchor.href, window.location.origin);
|
|
503
|
+
if (nextUrl.origin !== window.location.origin) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (nextUrl.hash
|
|
507
|
+
&& nextUrl.pathname === window.location.pathname
|
|
508
|
+
&& nextUrl.search === window.location.search) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
void navigate(`${nextUrl.pathname}${nextUrl.search}`, {
|
|
513
|
+
replace: anchor.hasAttribute('data-vrz-replace'),
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
initialized = true;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const router = {
|
|
520
|
+
context,
|
|
521
|
+
init,
|
|
522
|
+
navigate,
|
|
523
|
+
prefetch,
|
|
524
|
+
setSearch,
|
|
525
|
+
state,
|
|
526
|
+
};
|
|
527
|
+
_activeRouter = router;
|
|
528
|
+
return router;
|
|
529
|
+
}
|
|
530
|
+
function PayloadOutlet(props) {
|
|
531
|
+
return <div class="payload-fragment" innerHTML={props.html}/>;
|
|
532
|
+
}
|
|
533
|
+
export function renderResolvedMatches(state, options = {}) {
|
|
534
|
+
const renderAt = (index) => {
|
|
535
|
+
const currentMatch = state.matches[index];
|
|
536
|
+
const isIsland = options.wrapHydrationBoundaries && isHydrationBoundary(state, index);
|
|
537
|
+
const captureIslandContext = () => isIsland && sharedConfig.context
|
|
538
|
+
? `${sharedConfig.context.id}:${sharedConfig.context.count}`
|
|
539
|
+
: undefined;
|
|
540
|
+
const notFoundHandlerMatch = state.notFound?.handlerId
|
|
541
|
+
? state.matches.find((match) => match.id === state.notFound?.handlerId)
|
|
542
|
+
: undefined;
|
|
543
|
+
const routeErrorHandlerMatch = state.routeError?.handlerId
|
|
544
|
+
? state.matches.find((match) => match.id === state.routeError?.handlerId)
|
|
545
|
+
: undefined;
|
|
546
|
+
if (state.notFound && currentMatch.id === state.notFound.targetId) {
|
|
547
|
+
const NotFoundComponent = notFoundHandlerMatch?.route.options.notFoundComponent
|
|
548
|
+
?? DefaultNotFoundComponent;
|
|
549
|
+
const islandContext = captureIslandContext();
|
|
550
|
+
const content = (<MatchContext.Provider value={currentMatch}>
|
|
551
|
+
<NotFoundComponent />
|
|
552
|
+
</MatchContext.Provider>);
|
|
553
|
+
return isIsland
|
|
554
|
+
? <div data-vrz-island-root={currentMatch.id} data-vrz-hctx={islandContext}>{content}</div>
|
|
555
|
+
: content;
|
|
556
|
+
}
|
|
557
|
+
if (state.routeError && currentMatch.id === state.routeError.targetId) {
|
|
558
|
+
const errorComponent = routeErrorHandlerMatch?.route.options.errorComponent;
|
|
559
|
+
const islandContext = captureIslandContext();
|
|
560
|
+
const content = (<MatchContext.Provider value={currentMatch}>
|
|
561
|
+
<RouteErrorView error={state.routeError.error} errorComponent={errorComponent} reset={options.retry ?? (() => undefined)}/>
|
|
562
|
+
</MatchContext.Provider>);
|
|
563
|
+
return isIsland
|
|
564
|
+
? <div data-vrz-island-root={currentMatch.id} data-vrz-hctx={islandContext}>{content}</div>
|
|
565
|
+
: content;
|
|
566
|
+
}
|
|
567
|
+
if (index === state.matches.length - 1
|
|
568
|
+
&& state.renderSource === 'payload'
|
|
569
|
+
&& state.payloadHtml) {
|
|
570
|
+
const islandContext = captureIslandContext();
|
|
571
|
+
const content = <PayloadOutlet html={state.payloadHtml}/>;
|
|
572
|
+
return isIsland
|
|
573
|
+
? <div data-vrz-island-root={currentMatch.id} data-vrz-hctx={islandContext}>{content}</div>
|
|
574
|
+
: content;
|
|
575
|
+
}
|
|
576
|
+
const RouteComponent = currentMatch.route.options.component;
|
|
577
|
+
const errorComponent = currentMatch.route.options.errorComponent;
|
|
578
|
+
const outlet = index === state.matches.length - 1 ? null : renderAt(index + 1);
|
|
579
|
+
const islandContext = captureIslandContext();
|
|
580
|
+
const content = (<MatchContext.Provider value={currentMatch}>
|
|
581
|
+
<OutletContext.Provider value={outlet ?? undefined}>
|
|
582
|
+
<ErrorBoundary fallback={(error, reset) => {
|
|
583
|
+
const routeError = createRenderError(error);
|
|
584
|
+
logDevRouterError('render', error, {
|
|
585
|
+
routeId: currentMatch.id,
|
|
586
|
+
});
|
|
587
|
+
options.onRenderError?.(routeError);
|
|
588
|
+
return (<RouteErrorView error={routeError} errorComponent={errorComponent} reset={reset}/>);
|
|
589
|
+
}}>
|
|
590
|
+
<RouteComponent loaderData={currentMatch.loaderData} params={currentMatch.params} search={currentMatch.search}>
|
|
591
|
+
{outlet}
|
|
592
|
+
</RouteComponent>
|
|
593
|
+
</ErrorBoundary>
|
|
594
|
+
</OutletContext.Provider>
|
|
595
|
+
</MatchContext.Provider>);
|
|
596
|
+
return isIsland
|
|
597
|
+
? <div data-vrz-island-root={currentMatch.id} data-vrz-hctx={islandContext}>{content}</div>
|
|
598
|
+
: content;
|
|
599
|
+
};
|
|
600
|
+
if (state.matches.length === 0) {
|
|
601
|
+
return <div class="route-loading">Loading route...</div>;
|
|
602
|
+
}
|
|
603
|
+
return renderAt(0);
|
|
604
|
+
}
|
|
605
|
+
export function RouterProvider(props) {
|
|
606
|
+
let afterLoadFrame;
|
|
607
|
+
let afterLoadToken = 0;
|
|
608
|
+
let currentRenderFailed = false;
|
|
609
|
+
createEffect(() => {
|
|
610
|
+
const currentState = props.router.state();
|
|
611
|
+
if (typeof window === 'undefined') {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
currentRenderFailed = false;
|
|
615
|
+
const currentToken = ++afterLoadToken;
|
|
616
|
+
if (afterLoadFrame !== undefined) {
|
|
617
|
+
window.cancelAnimationFrame(afterLoadFrame);
|
|
618
|
+
}
|
|
619
|
+
afterLoadFrame = window.requestAnimationFrame(() => {
|
|
620
|
+
afterLoadFrame = undefined;
|
|
621
|
+
if (currentToken !== afterLoadToken) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (currentRenderFailed) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
runAfterLoadHooks(props.router, currentState);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
onCleanup(() => {
|
|
631
|
+
if (typeof window !== 'undefined' && afterLoadFrame !== undefined) {
|
|
632
|
+
window.cancelAnimationFrame(afterLoadFrame);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const retry = () => {
|
|
636
|
+
void props.router.navigate(`${props.router.state().pathname}${props.router.state().search}`, {
|
|
637
|
+
force: true,
|
|
638
|
+
replace: true,
|
|
639
|
+
scroll: false,
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
return (<RouterContext.Provider value={props.router}>
|
|
643
|
+
{renderResolvedMatches(props.router.state(), {
|
|
644
|
+
onRenderError: () => {
|
|
645
|
+
currentRenderFailed = true;
|
|
646
|
+
},
|
|
647
|
+
retry,
|
|
648
|
+
})}
|
|
649
|
+
</RouterContext.Provider>);
|
|
650
|
+
}
|
|
651
|
+
export function Outlet() {
|
|
652
|
+
return useContext(OutletContext) ?? null;
|
|
653
|
+
}
|
|
654
|
+
export function useRouter() {
|
|
655
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
656
|
+
if (!router) {
|
|
657
|
+
throw new Error('useRouter must be used inside a <RouterProvider>');
|
|
658
|
+
}
|
|
659
|
+
return router;
|
|
660
|
+
}
|
|
661
|
+
export function useNavigate() {
|
|
662
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
663
|
+
return (to, options) => {
|
|
664
|
+
if (!router) {
|
|
665
|
+
return Promise.reject(new Error('useNavigate cannot be called during SSR'));
|
|
666
|
+
}
|
|
667
|
+
return router.navigate(to, options);
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
export function useParams() {
|
|
671
|
+
const match = useContext(MatchContext);
|
|
672
|
+
if (!match) {
|
|
673
|
+
throw new Error('useParams must be used inside a route component');
|
|
674
|
+
}
|
|
675
|
+
return match.params;
|
|
676
|
+
}
|
|
677
|
+
export function useLoaderData() {
|
|
678
|
+
const match = useContext(MatchContext);
|
|
679
|
+
if (!match) {
|
|
680
|
+
throw new Error('useLoaderData must be used inside a route component');
|
|
681
|
+
}
|
|
682
|
+
return match.loaderData;
|
|
683
|
+
}
|
|
684
|
+
export function useSearch() {
|
|
685
|
+
const match = useContext(MatchContext);
|
|
686
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
687
|
+
if (!match) {
|
|
688
|
+
throw new Error('useSearch must be used inside a route component');
|
|
689
|
+
}
|
|
690
|
+
return () => {
|
|
691
|
+
if (!router) {
|
|
692
|
+
return match.search;
|
|
693
|
+
}
|
|
694
|
+
const currentMatch = router.state().matches.find((candidate) => candidate.id === match.id);
|
|
695
|
+
return (currentMatch?.search ?? match.search);
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
export function useSetSearch() {
|
|
699
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
700
|
+
const search = useSearch();
|
|
701
|
+
return (nextSearch, options = {}) => {
|
|
702
|
+
if (!router) {
|
|
703
|
+
return Promise.reject(new Error('useSetSearch cannot be called during SSR'));
|
|
704
|
+
}
|
|
705
|
+
const currentState = router.state();
|
|
706
|
+
const mergedSearch = resolveMergedSearch(parseSearchString(currentState.search), search(), nextSearch);
|
|
707
|
+
return router.navigate({
|
|
708
|
+
replace: options.replace,
|
|
709
|
+
scroll: options.scroll,
|
|
710
|
+
search: mergedSearch,
|
|
711
|
+
to: currentState.pathname,
|
|
712
|
+
});
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
export function readBootstrapPayload() {
|
|
716
|
+
const element = document.getElementById('__VORZELA_DATA__');
|
|
717
|
+
if (!(element instanceof HTMLScriptElement) || !element.textContent) {
|
|
718
|
+
throw new Error('Missing VorzelaJs bootstrap payload');
|
|
719
|
+
}
|
|
720
|
+
return JSON.parse(element.textContent);
|
|
721
|
+
}
|
|
722
|
+
export function Link(props) {
|
|
723
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
724
|
+
const [local, rest] = splitProps(props, ['children', 'onClick', 'onFocus', 'onMouseEnter', 'onTouchStart', 'replace', 'to']);
|
|
725
|
+
const onClick = local.onClick;
|
|
726
|
+
const onFocus = local.onFocus;
|
|
727
|
+
const onMouseEnter = local.onMouseEnter;
|
|
728
|
+
const onTouchStart = local.onTouchStart;
|
|
729
|
+
const prefetchLink = () => {
|
|
730
|
+
if (!router) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const nextUrl = new URL(local.to, window.location.origin);
|
|
734
|
+
if (nextUrl.origin !== window.location.origin) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
void router.prefetch(`${nextUrl.pathname}${nextUrl.search}`);
|
|
738
|
+
};
|
|
739
|
+
const handleClick = (event) => {
|
|
740
|
+
onClick?.(event);
|
|
741
|
+
if (event.defaultPrevented
|
|
742
|
+
|| event.button !== 0
|
|
743
|
+
|| event.metaKey
|
|
744
|
+
|| event.altKey
|
|
745
|
+
|| event.ctrlKey
|
|
746
|
+
|| event.shiftKey
|
|
747
|
+
|| rest.target === '_blank'
|
|
748
|
+
|| !router) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const nextUrl = new URL(local.to, window.location.origin);
|
|
752
|
+
if (nextUrl.origin !== window.location.origin) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
event.preventDefault();
|
|
756
|
+
void router.navigate(`${nextUrl.pathname}${nextUrl.search}`, { replace: local.replace });
|
|
757
|
+
};
|
|
758
|
+
const handleMouseEnter = (event) => {
|
|
759
|
+
onMouseEnter?.(event);
|
|
760
|
+
if (!event.defaultPrevented) {
|
|
761
|
+
prefetchLink();
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const handleFocus = (event) => {
|
|
765
|
+
onFocus?.(event);
|
|
766
|
+
if (!event.defaultPrevented) {
|
|
767
|
+
prefetchLink();
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const handleTouchStart = (event) => {
|
|
771
|
+
onTouchStart?.(event);
|
|
772
|
+
if (!event.defaultPrevented) {
|
|
773
|
+
prefetchLink();
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
return (<a {...rest} data-vrz-replace={local.replace ? '' : undefined} href={local.to} onClick={handleClick} onFocus={handleFocus} onMouseEnter={handleMouseEnter} onTouchStart={handleTouchStart}>
|
|
777
|
+
{local.children}
|
|
778
|
+
</a>);
|
|
779
|
+
}
|