@matthesketh/utopia-router 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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/index.cjs +531 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.js +491 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Hesketh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @matthesketh/utopia-router
|
|
2
|
+
|
|
3
|
+
File-based routing for UtopiaJS with History API, navigation guards, and reactive route state. SvelteKit-style file conventions (`+page.utopia`, `+layout.utopia`, `[param]`, `[...rest]`, `(group)`).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @matthesketh/utopia-router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createRouter, currentRoute, navigate } from '@matthesketh/utopia-router';
|
|
15
|
+
import { buildRouteTable } from '@matthesketh/utopia-router';
|
|
16
|
+
|
|
17
|
+
const routes = buildRouteTable(routeManifest);
|
|
18
|
+
createRouter(routes);
|
|
19
|
+
|
|
20
|
+
// Reactive route state
|
|
21
|
+
console.log(currentRoute().pathname);
|
|
22
|
+
|
|
23
|
+
// Programmatic navigation
|
|
24
|
+
navigate('/about');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## File Conventions
|
|
28
|
+
|
|
29
|
+
| File Path | URL Pattern |
|
|
30
|
+
|-----------|-------------|
|
|
31
|
+
| `src/routes/+page.utopia` | `/` |
|
|
32
|
+
| `src/routes/about/+page.utopia` | `/about` |
|
|
33
|
+
| `src/routes/blog/[slug]/+page.utopia` | `/blog/:slug` |
|
|
34
|
+
| `src/routes/[...rest]/+page.utopia` | `/*rest` (catch-all) |
|
|
35
|
+
| `src/routes/(auth)/login/+page.utopia` | `/login` (route group) |
|
|
36
|
+
| `src/routes/+layout.utopia` | Layout wrapper |
|
|
37
|
+
| `src/routes/+error.utopia` | Error boundary |
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
**Route matching:**
|
|
42
|
+
|
|
43
|
+
| Export | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `filePathToRoute(path)` | Convert a file path to a route pattern |
|
|
46
|
+
| `compilePattern(pattern)` | Compile a route pattern to a regex matcher |
|
|
47
|
+
| `matchRoute(routes, url)` | Match a URL against a route table |
|
|
48
|
+
| `buildRouteTable(manifest)` | Build a route table from a file manifest |
|
|
49
|
+
|
|
50
|
+
**Client-side router:**
|
|
51
|
+
|
|
52
|
+
| Export | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `createRouter(routes)` | Initialize the client router |
|
|
55
|
+
| `navigate(url)` | Programmatic navigation |
|
|
56
|
+
| `back()` | Go back in history |
|
|
57
|
+
| `forward()` | Go forward in history |
|
|
58
|
+
| `beforeNavigate(hook)` | Register a navigation guard |
|
|
59
|
+
| `destroy()` | Tear down the router |
|
|
60
|
+
| `currentRoute` | Reactive signal with the current route |
|
|
61
|
+
| `isNavigating` | Reactive signal indicating navigation in progress |
|
|
62
|
+
|
|
63
|
+
**Components:**
|
|
64
|
+
|
|
65
|
+
| Export | Description |
|
|
66
|
+
|--------|-------------|
|
|
67
|
+
| `createRouterView()` | Render the matched route component |
|
|
68
|
+
| `createLink(props)` | Create a client-side navigation link |
|
|
69
|
+
|
|
70
|
+
See [docs/architecture.md](../../docs/architecture.md) for full routing details.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
back: () => back,
|
|
24
|
+
beforeNavigate: () => beforeNavigate,
|
|
25
|
+
buildRouteTable: () => buildRouteTable,
|
|
26
|
+
compilePattern: () => compilePattern,
|
|
27
|
+
createLink: () => createLink,
|
|
28
|
+
createRouter: () => createRouter,
|
|
29
|
+
createRouterView: () => createRouterView,
|
|
30
|
+
currentRoute: () => currentRoute,
|
|
31
|
+
destroy: () => destroy,
|
|
32
|
+
filePathToRoute: () => filePathToRoute,
|
|
33
|
+
forward: () => forward,
|
|
34
|
+
isNavigating: () => isNavigating,
|
|
35
|
+
matchRoute: () => matchRoute,
|
|
36
|
+
navigate: () => navigate
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/matcher.ts
|
|
41
|
+
function filePathToRoute(filePath) {
|
|
42
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
43
|
+
const routesIdx = normalized.indexOf("routes/");
|
|
44
|
+
if (routesIdx !== -1) {
|
|
45
|
+
normalized = normalized.slice(routesIdx + "routes/".length);
|
|
46
|
+
}
|
|
47
|
+
normalized = normalized.replace(/\/?\+page\.\w+$/, "");
|
|
48
|
+
normalized = normalized.replace(/\/?\+(layout|error)\.\w+$/, "");
|
|
49
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
50
|
+
const routeSegments = [];
|
|
51
|
+
for (const segment of segments) {
|
|
52
|
+
if (/^\(.+\)$/.test(segment)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (/^\[\.\.\..+\]$/.test(segment)) {
|
|
56
|
+
const paramName = segment.slice(4, -1);
|
|
57
|
+
routeSegments.push(`*${paramName}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (/^\[.+\]$/.test(segment)) {
|
|
61
|
+
const paramName = segment.slice(1, -1);
|
|
62
|
+
routeSegments.push(`:${paramName}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
routeSegments.push(segment);
|
|
66
|
+
}
|
|
67
|
+
const path = "/" + routeSegments.join("/");
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
function compilePattern(pattern) {
|
|
71
|
+
const params = [];
|
|
72
|
+
if (pattern === "/") {
|
|
73
|
+
return {
|
|
74
|
+
regex: /^\/$/,
|
|
75
|
+
params: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
79
|
+
let regexStr = "";
|
|
80
|
+
for (let i = 0; i < segments.length; i++) {
|
|
81
|
+
const segment = segments[i];
|
|
82
|
+
if (segment.startsWith("*")) {
|
|
83
|
+
const paramName = segment.slice(1);
|
|
84
|
+
params.push(paramName);
|
|
85
|
+
regexStr += "/(.+)";
|
|
86
|
+
} else if (segment.startsWith(":")) {
|
|
87
|
+
const paramName = segment.slice(1);
|
|
88
|
+
params.push(paramName);
|
|
89
|
+
regexStr += "/([^/]+)";
|
|
90
|
+
} else {
|
|
91
|
+
regexStr += "/" + escapeRegex(segment);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
regexStr = "^" + regexStr + "/?$";
|
|
95
|
+
return {
|
|
96
|
+
regex: new RegExp(regexStr),
|
|
97
|
+
params
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function escapeRegex(str) {
|
|
101
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
102
|
+
}
|
|
103
|
+
function matchRoute(url, routes2) {
|
|
104
|
+
const pathname = url.pathname;
|
|
105
|
+
for (const route of routes2) {
|
|
106
|
+
const match = route.pattern.exec(pathname);
|
|
107
|
+
if (match) {
|
|
108
|
+
const params = {};
|
|
109
|
+
for (let i = 0; i < route.params.length; i++) {
|
|
110
|
+
params[route.params[i]] = decodeURIComponent(match[i + 1]);
|
|
111
|
+
}
|
|
112
|
+
return { route, params, url };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function buildRouteTable(manifest) {
|
|
118
|
+
const pages = /* @__PURE__ */ new Map();
|
|
119
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
120
|
+
const errors = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const [filePath, importFn] of Object.entries(manifest)) {
|
|
122
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
123
|
+
if (/\+page\.\w+$/.test(normalized)) {
|
|
124
|
+
pages.set(normalized, importFn);
|
|
125
|
+
} else if (/\+layout\.\w+$/.test(normalized)) {
|
|
126
|
+
layouts.set(normalized, importFn);
|
|
127
|
+
} else if (/\+error\.\w+$/.test(normalized)) {
|
|
128
|
+
errors.set(normalized, importFn);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const routes2 = [];
|
|
132
|
+
for (const [filePath, importFn] of pages) {
|
|
133
|
+
const path = filePathToRoute(filePath);
|
|
134
|
+
const { regex, params } = compilePattern(path);
|
|
135
|
+
const layout = findNearestSpecialFile(filePath, layouts);
|
|
136
|
+
const error = findNearestSpecialFile(filePath, errors);
|
|
137
|
+
routes2.push({
|
|
138
|
+
path,
|
|
139
|
+
pattern: regex,
|
|
140
|
+
params,
|
|
141
|
+
component: importFn,
|
|
142
|
+
layout,
|
|
143
|
+
error
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
routes2.sort((a, b) => {
|
|
147
|
+
const scoreA = routeSpecificity(a.path);
|
|
148
|
+
const scoreB = routeSpecificity(b.path);
|
|
149
|
+
if (scoreA !== scoreB) {
|
|
150
|
+
return scoreB - scoreA;
|
|
151
|
+
}
|
|
152
|
+
return a.path.localeCompare(b.path);
|
|
153
|
+
});
|
|
154
|
+
return routes2;
|
|
155
|
+
}
|
|
156
|
+
function routeSpecificity(path) {
|
|
157
|
+
if (path === "/") {
|
|
158
|
+
return 10;
|
|
159
|
+
}
|
|
160
|
+
const segments = path.split("/").filter(Boolean);
|
|
161
|
+
let score = 0;
|
|
162
|
+
for (const segment of segments) {
|
|
163
|
+
if (segment.startsWith("*")) {
|
|
164
|
+
score += 1;
|
|
165
|
+
} else if (segment.startsWith(":")) {
|
|
166
|
+
score += 2;
|
|
167
|
+
} else {
|
|
168
|
+
score += 3;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return score;
|
|
172
|
+
}
|
|
173
|
+
function findNearestSpecialFile(pageFilePath, specialFiles) {
|
|
174
|
+
const normalized = pageFilePath.replace(/\\/g, "/");
|
|
175
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
176
|
+
let dir = lastSlash !== -1 ? normalized.slice(0, lastSlash) : "";
|
|
177
|
+
while (dir) {
|
|
178
|
+
for (const [specialPath, importFn] of specialFiles) {
|
|
179
|
+
const specialNorm = specialPath.replace(/\\/g, "/");
|
|
180
|
+
const specialLastSlash = specialNorm.lastIndexOf("/");
|
|
181
|
+
const specialDir = specialLastSlash !== -1 ? specialNorm.slice(0, specialLastSlash) : "";
|
|
182
|
+
if (specialDir === dir) {
|
|
183
|
+
return importFn;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const parentSlash = dir.lastIndexOf("/");
|
|
187
|
+
if (parentSlash === -1) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
dir = dir.slice(0, parentSlash);
|
|
191
|
+
}
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/router.ts
|
|
196
|
+
var import_utopia_core = require("@matthesketh/utopia-core");
|
|
197
|
+
var currentRoute = (0, import_utopia_core.signal)(null);
|
|
198
|
+
var isNavigating = (0, import_utopia_core.signal)(false);
|
|
199
|
+
var routes = [];
|
|
200
|
+
var beforeNavigateHooks = [];
|
|
201
|
+
var scrollPositions = /* @__PURE__ */ new Map();
|
|
202
|
+
var navIndex = 0;
|
|
203
|
+
var cleanup = null;
|
|
204
|
+
function createRouter(routeTable) {
|
|
205
|
+
if (cleanup) {
|
|
206
|
+
cleanup();
|
|
207
|
+
}
|
|
208
|
+
routes = routeTable;
|
|
209
|
+
beforeNavigateHooks = [];
|
|
210
|
+
scrollPositions.clear();
|
|
211
|
+
navIndex = 0;
|
|
212
|
+
if (typeof window !== "undefined" && typeof history !== "undefined") {
|
|
213
|
+
history.replaceState({ _utopiaNavIndex: navIndex }, "");
|
|
214
|
+
}
|
|
215
|
+
if (typeof window !== "undefined") {
|
|
216
|
+
const url = new URL(window.location.href);
|
|
217
|
+
const match = matchRoute(url, routes);
|
|
218
|
+
currentRoute.set(match);
|
|
219
|
+
}
|
|
220
|
+
if (typeof window !== "undefined") {
|
|
221
|
+
const handlePopState = (event) => {
|
|
222
|
+
const state = event.state;
|
|
223
|
+
const targetIndex = state?._utopiaNavIndex ?? 0;
|
|
224
|
+
scrollPositions.set(navIndex, { x: window.scrollX, y: window.scrollY });
|
|
225
|
+
navIndex = targetIndex;
|
|
226
|
+
const url = new URL(window.location.href);
|
|
227
|
+
const match = matchRoute(url, routes);
|
|
228
|
+
runBeforeNavigateHooks(currentRoute.peek(), match).then((result) => {
|
|
229
|
+
if (result === false) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (typeof result === "string") {
|
|
233
|
+
navigate(result, { replace: true });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
currentRoute.set(match);
|
|
237
|
+
const savedPos = scrollPositions.get(targetIndex);
|
|
238
|
+
if (savedPos) {
|
|
239
|
+
requestAnimationFrame(() => {
|
|
240
|
+
window.scrollTo(savedPos.x, savedPos.y);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
const handleClick = (event) => {
|
|
246
|
+
if (event.button !== 0) return;
|
|
247
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
248
|
+
if (event.defaultPrevented) return;
|
|
249
|
+
const anchor = findAnchorElement(event.target);
|
|
250
|
+
if (!anchor) return;
|
|
251
|
+
if (anchor.hasAttribute("target") || anchor.hasAttribute("download")) return;
|
|
252
|
+
const href = anchor.getAttribute("href");
|
|
253
|
+
if (!href) return;
|
|
254
|
+
if (!href.startsWith("/") && !href.startsWith(window.location.origin)) return;
|
|
255
|
+
if (href.startsWith("#")) return;
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
navigate(href);
|
|
258
|
+
};
|
|
259
|
+
window.addEventListener("popstate", handlePopState);
|
|
260
|
+
document.addEventListener("click", handleClick);
|
|
261
|
+
cleanup = () => {
|
|
262
|
+
window.removeEventListener("popstate", handlePopState);
|
|
263
|
+
document.removeEventListener("click", handleClick);
|
|
264
|
+
cleanup = null;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function navigate(url, options = {}) {
|
|
269
|
+
if (typeof window === "undefined") return;
|
|
270
|
+
isNavigating.set(true);
|
|
271
|
+
try {
|
|
272
|
+
const fullUrl = new URL(url, window.location.origin);
|
|
273
|
+
const match = matchRoute(fullUrl, routes);
|
|
274
|
+
const hookResult = await runBeforeNavigateHooks(currentRoute.peek(), match);
|
|
275
|
+
if (hookResult === false) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (typeof hookResult === "string") {
|
|
279
|
+
await navigate(hookResult, options);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
scrollPositions.set(navIndex, { x: window.scrollX, y: window.scrollY });
|
|
283
|
+
navIndex++;
|
|
284
|
+
const state = { _utopiaNavIndex: navIndex };
|
|
285
|
+
if (options.replace) {
|
|
286
|
+
history.replaceState(state, "", fullUrl.href);
|
|
287
|
+
} else {
|
|
288
|
+
history.pushState(state, "", fullUrl.href);
|
|
289
|
+
}
|
|
290
|
+
currentRoute.set(match);
|
|
291
|
+
requestAnimationFrame(() => {
|
|
292
|
+
if (fullUrl.hash) {
|
|
293
|
+
const el = document.getElementById(fullUrl.hash.slice(1));
|
|
294
|
+
if (el) {
|
|
295
|
+
el.scrollIntoView();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
window.scrollTo(0, 0);
|
|
300
|
+
});
|
|
301
|
+
} finally {
|
|
302
|
+
isNavigating.set(false);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function back() {
|
|
306
|
+
if (typeof window !== "undefined") {
|
|
307
|
+
history.back();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function forward() {
|
|
311
|
+
if (typeof window !== "undefined") {
|
|
312
|
+
history.forward();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function beforeNavigate(hook) {
|
|
316
|
+
beforeNavigateHooks.push(hook);
|
|
317
|
+
return () => {
|
|
318
|
+
const idx = beforeNavigateHooks.indexOf(hook);
|
|
319
|
+
if (idx !== -1) {
|
|
320
|
+
beforeNavigateHooks.splice(idx, 1);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function destroy() {
|
|
325
|
+
if (cleanup) {
|
|
326
|
+
cleanup();
|
|
327
|
+
}
|
|
328
|
+
routes = [];
|
|
329
|
+
beforeNavigateHooks = [];
|
|
330
|
+
scrollPositions.clear();
|
|
331
|
+
navIndex = 0;
|
|
332
|
+
currentRoute.set(null);
|
|
333
|
+
isNavigating.set(false);
|
|
334
|
+
}
|
|
335
|
+
function findAnchorElement(target) {
|
|
336
|
+
while (target) {
|
|
337
|
+
if (target.tagName === "A") {
|
|
338
|
+
return target;
|
|
339
|
+
}
|
|
340
|
+
target = target.parentElement;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
async function runBeforeNavigateHooks(from, to) {
|
|
345
|
+
for (const hook of beforeNavigateHooks) {
|
|
346
|
+
const result = await hook(from, to);
|
|
347
|
+
if (result === false) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (typeof result === "string") {
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/components.ts
|
|
358
|
+
var import_utopia_core2 = require("@matthesketh/utopia-core");
|
|
359
|
+
function createRouterView() {
|
|
360
|
+
const container = document.createElement("div");
|
|
361
|
+
container.setAttribute("data-utopia-router-view", "");
|
|
362
|
+
let currentCleanup = null;
|
|
363
|
+
let currentMatch = null;
|
|
364
|
+
(0, import_utopia_core2.effect)(() => {
|
|
365
|
+
const match = currentRoute();
|
|
366
|
+
if (match === currentMatch) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
currentMatch = match;
|
|
370
|
+
if (currentCleanup) {
|
|
371
|
+
currentCleanup();
|
|
372
|
+
currentCleanup = null;
|
|
373
|
+
}
|
|
374
|
+
while (container.firstChild) {
|
|
375
|
+
container.removeChild(container.firstChild);
|
|
376
|
+
}
|
|
377
|
+
if (!match) {
|
|
378
|
+
const notFound = document.createElement("div");
|
|
379
|
+
notFound.setAttribute("data-utopia-not-found", "");
|
|
380
|
+
notFound.textContent = "Page not found";
|
|
381
|
+
container.appendChild(notFound);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
loadRouteComponent(match, container).then((cleanupFn) => {
|
|
385
|
+
currentCleanup = cleanupFn;
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return container;
|
|
389
|
+
}
|
|
390
|
+
async function loadRouteComponent(match, container) {
|
|
391
|
+
try {
|
|
392
|
+
const promises = [match.route.component()];
|
|
393
|
+
if (match.route.layout) {
|
|
394
|
+
promises.push(match.route.layout());
|
|
395
|
+
}
|
|
396
|
+
const modules = await Promise.all(promises);
|
|
397
|
+
const pageModule = modules[0];
|
|
398
|
+
const layoutModule = modules.length > 1 ? modules[1] : null;
|
|
399
|
+
if (currentRoute.peek() !== match) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const PageComponent = pageModule.default ?? pageModule;
|
|
403
|
+
const LayoutComponent = layoutModule ? layoutModule.default ?? layoutModule : null;
|
|
404
|
+
while (container.firstChild) {
|
|
405
|
+
container.removeChild(container.firstChild);
|
|
406
|
+
}
|
|
407
|
+
const pageNode = renderComponent(PageComponent, {
|
|
408
|
+
params: match.params,
|
|
409
|
+
url: match.url
|
|
410
|
+
});
|
|
411
|
+
if (LayoutComponent) {
|
|
412
|
+
const layoutNode = renderComponent(LayoutComponent, {
|
|
413
|
+
params: match.params,
|
|
414
|
+
url: match.url,
|
|
415
|
+
children: pageNode
|
|
416
|
+
});
|
|
417
|
+
container.appendChild(layoutNode);
|
|
418
|
+
} else {
|
|
419
|
+
container.appendChild(pageNode);
|
|
420
|
+
}
|
|
421
|
+
return () => {
|
|
422
|
+
while (container.firstChild) {
|
|
423
|
+
container.removeChild(container.firstChild);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (match.route.error) {
|
|
428
|
+
try {
|
|
429
|
+
const errorModule = await match.route.error();
|
|
430
|
+
const ErrorComponent = errorModule.default ?? errorModule;
|
|
431
|
+
while (container.firstChild) {
|
|
432
|
+
container.removeChild(container.firstChild);
|
|
433
|
+
}
|
|
434
|
+
const errorNode = renderComponent(ErrorComponent, {
|
|
435
|
+
error: err,
|
|
436
|
+
params: match.params,
|
|
437
|
+
url: match.url
|
|
438
|
+
});
|
|
439
|
+
container.appendChild(errorNode);
|
|
440
|
+
} catch {
|
|
441
|
+
renderFallbackError(container, err);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
renderFallbackError(container, err);
|
|
445
|
+
}
|
|
446
|
+
return () => {
|
|
447
|
+
while (container.firstChild) {
|
|
448
|
+
container.removeChild(container.firstChild);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function renderComponent(component, props) {
|
|
454
|
+
if (typeof component === "function") {
|
|
455
|
+
const result = component(props);
|
|
456
|
+
if (result instanceof Node) {
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
if (typeof result === "string") {
|
|
460
|
+
return document.createTextNode(result);
|
|
461
|
+
}
|
|
462
|
+
if (result && typeof result.render === "function") {
|
|
463
|
+
return result.render();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (component && typeof component.render === "function") {
|
|
467
|
+
return component.render(props);
|
|
468
|
+
}
|
|
469
|
+
const div = document.createElement("div");
|
|
470
|
+
div.textContent = "[Component render error]";
|
|
471
|
+
return div;
|
|
472
|
+
}
|
|
473
|
+
function renderFallbackError(container, error) {
|
|
474
|
+
while (container.firstChild) {
|
|
475
|
+
container.removeChild(container.firstChild);
|
|
476
|
+
}
|
|
477
|
+
const errorDiv = document.createElement("div");
|
|
478
|
+
errorDiv.setAttribute("data-utopia-error", "");
|
|
479
|
+
errorDiv.style.cssText = "padding:2rem;color:#dc2626;font-family:monospace;";
|
|
480
|
+
errorDiv.innerHTML = `
|
|
481
|
+
<h2 style="margin:0 0 1rem">Route Error</h2>
|
|
482
|
+
<pre style="white-space:pre-wrap;word-break:break-word;">${escapeHtml(
|
|
483
|
+
error instanceof Error ? error.message : String(error)
|
|
484
|
+
)}</pre>
|
|
485
|
+
`;
|
|
486
|
+
container.appendChild(errorDiv);
|
|
487
|
+
}
|
|
488
|
+
function createLink(props) {
|
|
489
|
+
const anchor = document.createElement("a");
|
|
490
|
+
anchor.href = props.href;
|
|
491
|
+
if (props.class) {
|
|
492
|
+
anchor.className = props.class;
|
|
493
|
+
}
|
|
494
|
+
if (typeof props.children === "string") {
|
|
495
|
+
anchor.textContent = props.children;
|
|
496
|
+
} else {
|
|
497
|
+
anchor.appendChild(props.children);
|
|
498
|
+
}
|
|
499
|
+
if (props.activeClass) {
|
|
500
|
+
(0, import_utopia_core2.effect)(() => {
|
|
501
|
+
const match = currentRoute();
|
|
502
|
+
const isActive = match ? match.url.pathname === props.href || match.url.pathname.startsWith(props.href + "/") : false;
|
|
503
|
+
if (isActive) {
|
|
504
|
+
anchor.classList.add(props.activeClass);
|
|
505
|
+
} else {
|
|
506
|
+
anchor.classList.remove(props.activeClass);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return anchor;
|
|
511
|
+
}
|
|
512
|
+
function escapeHtml(str) {
|
|
513
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
514
|
+
}
|
|
515
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
516
|
+
0 && (module.exports = {
|
|
517
|
+
back,
|
|
518
|
+
beforeNavigate,
|
|
519
|
+
buildRouteTable,
|
|
520
|
+
compilePattern,
|
|
521
|
+
createLink,
|
|
522
|
+
createRouter,
|
|
523
|
+
createRouterView,
|
|
524
|
+
currentRoute,
|
|
525
|
+
destroy,
|
|
526
|
+
filePathToRoute,
|
|
527
|
+
forward,
|
|
528
|
+
isNavigating,
|
|
529
|
+
matchRoute,
|
|
530
|
+
navigate
|
|
531
|
+
});
|