tachyon-dom 0.1.0
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 +339 -0
- package/dist/adapters/lambda.d.ts +65 -0
- package/dist/adapters/lambda.js +211 -0
- package/dist/adapters/node.d.ts +25 -0
- package/dist/adapters/node.js +360 -0
- package/dist/adapters/workers.d.ts +149 -0
- package/dist/adapters/workers.js +272 -0
- package/dist/adapters.d.ts +3 -0
- package/dist/adapters.js +3 -0
- package/dist/app.d.ts +98 -0
- package/dist/app.js +306 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +711 -0
- package/dist/compiler/expression.d.ts +55 -0
- package/dist/compiler/expression.js +858 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +50 -0
- package/dist/compiler/ir.d.ts +4 -0
- package/dist/compiler/ir.js +255 -0
- package/dist/compiler/parser.d.ts +3 -0
- package/dist/compiler/parser.js +236 -0
- package/dist/compiler/sfc.d.ts +33 -0
- package/dist/compiler/sfc.js +439 -0
- package/dist/compiler/targets/client.d.ts +3 -0
- package/dist/compiler/targets/client.js +568 -0
- package/dist/compiler/targets/server.d.ts +7 -0
- package/dist/compiler/targets/server.js +396 -0
- package/dist/compiler/targets/stream.d.ts +2 -0
- package/dist/compiler/targets/stream.js +204 -0
- package/dist/compiler/types.d.ts +185 -0
- package/dist/compiler/types.js +1 -0
- package/dist/compiler/utils.d.ts +40 -0
- package/dist/compiler/utils.js +185 -0
- package/dist/compiler/whitespace.d.ts +2 -0
- package/dist/compiler/whitespace.js +66 -0
- package/dist/compiler.d.ts +1 -0
- package/dist/compiler.js +1 -0
- package/dist/constant-time.d.ts +1 -0
- package/dist/constant-time.js +26 -0
- package/dist/cookies.d.ts +44 -0
- package/dist/cookies.js +199 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +45 -0
- package/dist/env.d.ts +27 -0
- package/dist/env.js +37 -0
- package/dist/html-escape.d.ts +2 -0
- package/dist/html-escape.js +20 -0
- package/dist/html-whitespace.d.ts +6 -0
- package/dist/html-whitespace.js +83 -0
- package/dist/i18n.d.ts +19 -0
- package/dist/i18n.js +93 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/language-server.d.ts +17 -0
- package/dist/language-server.js +79 -0
- package/dist/package-integrity.d.ts +9 -0
- package/dist/package-integrity.js +64 -0
- package/dist/result.d.ts +13 -0
- package/dist/result.js +4 -0
- package/dist/router-node.d.ts +2 -0
- package/dist/router-node.js +12 -0
- package/dist/router.d.ts +324 -0
- package/dist/router.js +1024 -0
- package/dist/runtime/attr.d.ts +3 -0
- package/dist/runtime/attr.js +66 -0
- package/dist/runtime/class.d.ts +2 -0
- package/dist/runtime/class.js +10 -0
- package/dist/runtime/conditional.d.ts +78 -0
- package/dist/runtime/conditional.js +140 -0
- package/dist/runtime/enhancement.d.ts +11 -0
- package/dist/runtime/enhancement.js +70 -0
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +27 -0
- package/dist/runtime/event.d.ts +1 -0
- package/dist/runtime/event.js +11 -0
- package/dist/runtime/form.d.ts +41 -0
- package/dist/runtime/form.js +167 -0
- package/dist/runtime/fragment.d.ts +6 -0
- package/dist/runtime/fragment.js +18 -0
- package/dist/runtime/html.d.ts +8 -0
- package/dist/runtime/html.js +6 -0
- package/dist/runtime/hydrate.d.ts +68 -0
- package/dist/runtime/hydrate.js +205 -0
- package/dist/runtime/keyed-rows.d.ts +61 -0
- package/dist/runtime/keyed-rows.js +192 -0
- package/dist/runtime/list.d.ts +84 -0
- package/dist/runtime/list.js +424 -0
- package/dist/runtime/portal.d.ts +6 -0
- package/dist/runtime/portal.js +12 -0
- package/dist/runtime/router.d.ts +102 -0
- package/dist/runtime/router.js +649 -0
- package/dist/runtime/signal.d.ts +30 -0
- package/dist/runtime/signal.js +275 -0
- package/dist/runtime/store.d.ts +1 -0
- package/dist/runtime/store.js +1 -0
- package/dist/runtime/stream-client.d.ts +8 -0
- package/dist/runtime/stream-client.js +33 -0
- package/dist/runtime/text.d.ts +2 -0
- package/dist/runtime/text.js +10 -0
- package/dist/runtime/virtual-list.d.ts +15 -0
- package/dist/runtime/virtual-list.js +89 -0
- package/dist/security.d.ts +27 -0
- package/dist/security.js +173 -0
- package/dist/server/form-action.d.ts +52 -0
- package/dist/server/form-action.js +102 -0
- package/dist/server/html.d.ts +19 -0
- package/dist/server/html.js +101 -0
- package/dist/server/stream.d.ts +4 -0
- package/dist/server/stream.js +72 -0
- package/dist/source-map.d.ts +17 -0
- package/dist/source-map.js +18 -0
- package/dist/tachyon-html.d.ts +110 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +36 -0
- package/dist/typed.d.ts +9 -0
- package/dist/typed.js +4 -0
- package/dist/vite.d.ts +84 -0
- package/dist/vite.js +426 -0
- package/package.json +604 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { isClientHtml, rawHtml } from "./html.js";
|
|
2
|
+
export { rawHtml } from "./html.js";
|
|
3
|
+
const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
|
|
4
|
+
const joinRoutePaths = (parent, child) => {
|
|
5
|
+
if (child === "*") {
|
|
6
|
+
return "*";
|
|
7
|
+
}
|
|
8
|
+
if (child.startsWith("/")) {
|
|
9
|
+
return child === "/" ? "/" : `/${trimSlashes(child)}`;
|
|
10
|
+
}
|
|
11
|
+
const joined = [trimSlashes(parent), trimSlashes(child)].filter(Boolean).join("/");
|
|
12
|
+
return joined ? `/${joined}` : "/";
|
|
13
|
+
};
|
|
14
|
+
const compileRoutePath = (path) => {
|
|
15
|
+
if (path === "*") {
|
|
16
|
+
return { regex: /^.*$/, names: [], wildcard: true };
|
|
17
|
+
}
|
|
18
|
+
const names = [];
|
|
19
|
+
const segments = trimSlashes(path).split("/").filter(Boolean);
|
|
20
|
+
if (segments.length === 0) {
|
|
21
|
+
return { regex: /^\/?$/, names, wildcard: false };
|
|
22
|
+
}
|
|
23
|
+
const source = segments
|
|
24
|
+
.map((segment) => {
|
|
25
|
+
if (segment.startsWith(":")) {
|
|
26
|
+
names.push(segment.slice(1));
|
|
27
|
+
return "([^/]+)";
|
|
28
|
+
}
|
|
29
|
+
if (segment === "*" || segment.startsWith("*")) {
|
|
30
|
+
names.push(segment.slice(1) || "wildcard");
|
|
31
|
+
return "(.*)";
|
|
32
|
+
}
|
|
33
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
})
|
|
35
|
+
.join("/");
|
|
36
|
+
return { regex: new RegExp(`^/${source}/?$`), names, wildcard: false };
|
|
37
|
+
};
|
|
38
|
+
const routeSegmentScore = (segment) => {
|
|
39
|
+
if (segment === "*" || segment.startsWith("*")) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
if (segment.startsWith(":")) {
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
return 2;
|
|
46
|
+
};
|
|
47
|
+
const routeSpecificity = (path) => {
|
|
48
|
+
if (path === "*") {
|
|
49
|
+
return [-1, 0, 0];
|
|
50
|
+
}
|
|
51
|
+
const segments = trimSlashes(path).split("/").filter(Boolean);
|
|
52
|
+
const segmentScores = segments.map(routeSegmentScore);
|
|
53
|
+
return [
|
|
54
|
+
segmentScores.reduce((total, score) => total + score, 0),
|
|
55
|
+
segments.length,
|
|
56
|
+
segmentScores.filter((score) => score === 2).length,
|
|
57
|
+
];
|
|
58
|
+
};
|
|
59
|
+
const compareCompiledClientRoutes = (left, right) => {
|
|
60
|
+
for (let index = 0; index < left.specificity.length; index += 1) {
|
|
61
|
+
const difference = (right.specificity[index] ?? 0) - (left.specificity[index] ?? 0);
|
|
62
|
+
if (difference !== 0) {
|
|
63
|
+
return difference;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return left.order - right.order;
|
|
67
|
+
};
|
|
68
|
+
const flattenClientRoutes = (routes, parentPath = "", parentBranch = [], orderOffset = { value: 0 }) => {
|
|
69
|
+
const entries = [];
|
|
70
|
+
for (const route of routes) {
|
|
71
|
+
const fullPath = joinRoutePaths(parentPath, route.path);
|
|
72
|
+
const compiled = compileRoutePath(fullPath);
|
|
73
|
+
const branch = [...parentBranch, route];
|
|
74
|
+
entries.push({
|
|
75
|
+
route,
|
|
76
|
+
branch,
|
|
77
|
+
fullPath,
|
|
78
|
+
order: orderOffset.value++,
|
|
79
|
+
names: compiled.names,
|
|
80
|
+
regex: compiled.regex,
|
|
81
|
+
specificity: routeSpecificity(fullPath),
|
|
82
|
+
wildcard: compiled.wildcard,
|
|
83
|
+
});
|
|
84
|
+
if (route.children) {
|
|
85
|
+
entries.push(...flattenClientRoutes(route.children, fullPath, branch, orderOffset));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
};
|
|
90
|
+
const compileClientRoutes = (routes) => flattenClientRoutes(routes).sort(compareCompiledClientRoutes);
|
|
91
|
+
const matchClientRoute = (routes, pathname) => {
|
|
92
|
+
let fallback;
|
|
93
|
+
for (const routeEntry of routes) {
|
|
94
|
+
const match = routeEntry.regex.exec(pathname);
|
|
95
|
+
if (!match) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const params = Object.fromEntries(routeEntry.names.map((name, index) => [name, decodeURIComponent(match[index + 1] ?? "")]));
|
|
99
|
+
const matched = { route: routeEntry.route, branch: routeEntry.branch, params };
|
|
100
|
+
if (routeEntry.wildcard) {
|
|
101
|
+
fallback = matched;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
return matched;
|
|
105
|
+
}
|
|
106
|
+
return fallback;
|
|
107
|
+
};
|
|
108
|
+
const toUrl = (href, baseUrl) => new URL(href, baseUrl);
|
|
109
|
+
const renderInto = (root, value) => {
|
|
110
|
+
root.replaceChildren();
|
|
111
|
+
if (isClientHtml(value)) {
|
|
112
|
+
root.innerHTML = value.toString();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
root.textContent = value;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (value instanceof DocumentFragment) {
|
|
120
|
+
root.appendChild(value);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (value instanceof Node) {
|
|
124
|
+
root.appendChild(value);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
root.append(...value);
|
|
128
|
+
};
|
|
129
|
+
const focusRouteContent = (root, selector) => {
|
|
130
|
+
const target = root.querySelector(selector);
|
|
131
|
+
if (target instanceof HTMLElement) {
|
|
132
|
+
target.focus({ preventScroll: true });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const managedHeadSelector = `[data-tachyon-head="route"]`;
|
|
136
|
+
const maxScrollPositions = 50;
|
|
137
|
+
const appendManagedHeadElement = (tagName, attributes) => {
|
|
138
|
+
const element = document.createElement(tagName);
|
|
139
|
+
element.setAttribute("data-tachyon-head", "route");
|
|
140
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
141
|
+
if (/^on/i.test(name)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
element.setAttribute(name, value);
|
|
145
|
+
}
|
|
146
|
+
document.head.appendChild(element);
|
|
147
|
+
};
|
|
148
|
+
const applyHead = (descriptor) => {
|
|
149
|
+
document.head.querySelectorAll(managedHeadSelector).forEach((element) => element.remove());
|
|
150
|
+
if (!descriptor) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (descriptor.title !== undefined) {
|
|
154
|
+
document.title = descriptor.title;
|
|
155
|
+
}
|
|
156
|
+
for (const meta of descriptor.metas ?? []) {
|
|
157
|
+
appendManagedHeadElement("meta", meta);
|
|
158
|
+
}
|
|
159
|
+
for (const link of descriptor.links ?? []) {
|
|
160
|
+
appendManagedHeadElement("link", link);
|
|
161
|
+
}
|
|
162
|
+
for (const script of descriptor.scripts ?? []) {
|
|
163
|
+
appendManagedHeadElement("script", script);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const routeTargetFor = (root, match, url, data) => {
|
|
167
|
+
const target = match.route.target;
|
|
168
|
+
if (!target) {
|
|
169
|
+
return root;
|
|
170
|
+
}
|
|
171
|
+
if (typeof target === "string") {
|
|
172
|
+
return root.querySelector(target) ?? root;
|
|
173
|
+
}
|
|
174
|
+
if (target instanceof Element) {
|
|
175
|
+
return target;
|
|
176
|
+
}
|
|
177
|
+
return target({ root, url, params: match.params, data }) ?? root;
|
|
178
|
+
};
|
|
179
|
+
const outletFor = (root) => root.querySelector("[data-tachyon-outlet], [data-tachyon-route-outlet], tachyon-outlet") ?? undefined;
|
|
180
|
+
const isModifiedClick = (event) => event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
181
|
+
export const createClientRouter = (options) => {
|
|
182
|
+
const baseUrl = options.baseUrl ?? location.href;
|
|
183
|
+
const routes = compileClientRoutes(options.routes);
|
|
184
|
+
const scrollTo = options.scrollTo ??
|
|
185
|
+
((x, y) => {
|
|
186
|
+
if (!navigator.userAgent.includes("jsdom")) {
|
|
187
|
+
window.scrollTo(x, y);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const focusSelector = options.focusSelector ?? "[autofocus],h1,[data-route-focus],main";
|
|
191
|
+
let controller;
|
|
192
|
+
let actionController;
|
|
193
|
+
let actionVersion = 0;
|
|
194
|
+
let currentNavigation = Promise.resolve();
|
|
195
|
+
const cache = new Map();
|
|
196
|
+
const layoutRoots = new WeakMap();
|
|
197
|
+
const prefetchControllers = new Map();
|
|
198
|
+
const eagerlyNavigated = new WeakSet();
|
|
199
|
+
const scrollPositions = new Map();
|
|
200
|
+
let nextScrollKey = 1;
|
|
201
|
+
const cacheKey = (url) => `${url.pathname}${url.search}`;
|
|
202
|
+
for (const entry of options.initialCache ?? []) {
|
|
203
|
+
cache.set(cacheKey(toUrl(entry.href, baseUrl)), entry.data);
|
|
204
|
+
}
|
|
205
|
+
const renderNotFound = (url) => {
|
|
206
|
+
renderInto(options.root, options.notFound ? options.notFound({ url }) : rawHtml(`<h1>Not Found</h1>`));
|
|
207
|
+
};
|
|
208
|
+
const renderError = (url, error) => {
|
|
209
|
+
renderInto(options.root, options.error ? options.error({ url, error }) : rawHtml(`<h1>Navigation Error</h1>`));
|
|
210
|
+
};
|
|
211
|
+
const loadData = async (url, match, signal) => {
|
|
212
|
+
const key = cacheKey(url);
|
|
213
|
+
if (options.cache && cache.has(key)) {
|
|
214
|
+
return cache.get(key);
|
|
215
|
+
}
|
|
216
|
+
const data = match.route.load ? await match.route.load({ url, params: match.params, signal }) : undefined;
|
|
217
|
+
if (options.cache && !signal.aborted) {
|
|
218
|
+
cache.set(key, data);
|
|
219
|
+
}
|
|
220
|
+
return data;
|
|
221
|
+
};
|
|
222
|
+
const updateA11y = (url, data) => {
|
|
223
|
+
if (options.title) {
|
|
224
|
+
document.title = options.title({ url, data });
|
|
225
|
+
}
|
|
226
|
+
if (options.liveRegion) {
|
|
227
|
+
options.liveRegion.textContent = `Navigated to ${url.pathname}`;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const updateHead = async (url, match, data, signal) => {
|
|
231
|
+
if (!match.route.head) {
|
|
232
|
+
applyHead(undefined);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const descriptor = await match.route.head({ url, params: match.params, data, signal });
|
|
236
|
+
if (!signal.aborted) {
|
|
237
|
+
applyHead(descriptor);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const shouldUseViewTransition = (url, match, data) => {
|
|
241
|
+
const setting = options.viewTransition;
|
|
242
|
+
if (!setting) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (typeof document.startViewTransition !== "function") {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return typeof setting === "function" ? setting({ url, params: match.params, data }) : true;
|
|
252
|
+
};
|
|
253
|
+
const commitNavigation = async (url, match, data, rendered, navigateOptions, signal) => {
|
|
254
|
+
const target = routeTargetFor(options.root, match, url, data);
|
|
255
|
+
let committedTarget = target;
|
|
256
|
+
const renderNestedBranch = async () => {
|
|
257
|
+
let parentTarget = target;
|
|
258
|
+
for (const layoutRoute of match.branch.slice(0, -1)) {
|
|
259
|
+
let layoutRoot = layoutRoots.get(layoutRoute);
|
|
260
|
+
if (!layoutRoot?.isConnected) {
|
|
261
|
+
const layoutValue = await layoutRoute.render({
|
|
262
|
+
url,
|
|
263
|
+
params: match.params,
|
|
264
|
+
data,
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
renderInto(parentTarget, layoutValue);
|
|
268
|
+
layoutRoot = parentTarget.firstElementChild ?? parentTarget;
|
|
269
|
+
layoutRoots.set(layoutRoute, layoutRoot);
|
|
270
|
+
}
|
|
271
|
+
const outlet = outletFor(layoutRoot);
|
|
272
|
+
if (!outlet) {
|
|
273
|
+
throw new Error(`Nested client route ${layoutRoute.id ?? layoutRoute.path} must render a route outlet.`);
|
|
274
|
+
}
|
|
275
|
+
parentTarget = outlet;
|
|
276
|
+
}
|
|
277
|
+
renderInto(parentTarget, rendered);
|
|
278
|
+
committedTarget = parentTarget;
|
|
279
|
+
};
|
|
280
|
+
const commit = async () => {
|
|
281
|
+
if (match.branch.length > 1) {
|
|
282
|
+
await renderNestedBranch();
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
renderInto(target, rendered);
|
|
286
|
+
}
|
|
287
|
+
await updateHead(url, match, data, signal);
|
|
288
|
+
if (signal.aborted) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
restoreOrScroll(url, navigateOptions);
|
|
292
|
+
focusRouteContent(committedTarget, focusSelector);
|
|
293
|
+
updateA11y(url, data);
|
|
294
|
+
};
|
|
295
|
+
if (!shouldUseViewTransition(url, match, data)) {
|
|
296
|
+
await commit();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const transition = document.startViewTransition?.(commit);
|
|
300
|
+
await (transition?.updateCallbackDone ?? transition?.finished ?? Promise.resolve());
|
|
301
|
+
};
|
|
302
|
+
const scrollKeyFor = (state) => {
|
|
303
|
+
if (!state || typeof state !== "object") {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const value = state.__tachyonScrollKey;
|
|
307
|
+
return typeof value === "number" ? value : undefined;
|
|
308
|
+
};
|
|
309
|
+
const ensureScrollState = () => {
|
|
310
|
+
const existing = scrollKeyFor(history.state);
|
|
311
|
+
if (existing !== undefined) {
|
|
312
|
+
return existing;
|
|
313
|
+
}
|
|
314
|
+
const key = nextScrollKey++;
|
|
315
|
+
history.replaceState({ ...(history.state && typeof history.state === "object" ? history.state : {}), __tachyonScrollKey: key }, "", location.href);
|
|
316
|
+
return key;
|
|
317
|
+
};
|
|
318
|
+
const currentScrollPosition = () => ({
|
|
319
|
+
x: window.scrollX ?? window.pageXOffset ?? 0,
|
|
320
|
+
y: window.scrollY ?? window.pageYOffset ?? 0,
|
|
321
|
+
});
|
|
322
|
+
const rememberScrollPosition = (key, position) => {
|
|
323
|
+
if (scrollPositions.has(key)) {
|
|
324
|
+
scrollPositions.delete(key);
|
|
325
|
+
}
|
|
326
|
+
scrollPositions.set(key, position);
|
|
327
|
+
while (scrollPositions.size > maxScrollPositions) {
|
|
328
|
+
const oldestKey = scrollPositions.keys().next().value;
|
|
329
|
+
if (oldestKey === undefined) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
scrollPositions.delete(oldestKey);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const saveCurrentScrollPosition = () => {
|
|
336
|
+
rememberScrollPosition(ensureScrollState(), currentScrollPosition());
|
|
337
|
+
};
|
|
338
|
+
const writeHistory = (url, navigateOptions) => {
|
|
339
|
+
const state = {
|
|
340
|
+
...(history.state && typeof history.state === "object" ? history.state : {}),
|
|
341
|
+
__tachyonScrollKey: navigateOptions.restoreScroll
|
|
342
|
+
? (scrollKeyFor(history.state) ?? nextScrollKey++)
|
|
343
|
+
: nextScrollKey++,
|
|
344
|
+
};
|
|
345
|
+
if (navigateOptions.restoreScroll) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (navigateOptions.replace) {
|
|
349
|
+
history.replaceState(state, "", url);
|
|
350
|
+
}
|
|
351
|
+
else if (location.pathname !== url.pathname || location.search !== url.search || location.hash !== url.hash) {
|
|
352
|
+
history.pushState(state, "", url);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const scrollHashIntoView = (url) => {
|
|
356
|
+
if (!url.hash) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const id = decodeURIComponent(url.hash.slice(1));
|
|
360
|
+
const namedTarget = typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
|
361
|
+
? document.querySelector(`[name="${CSS.escape(id)}"]`)
|
|
362
|
+
: null;
|
|
363
|
+
const target = document.getElementById(id) ?? namedTarget;
|
|
364
|
+
if (target instanceof HTMLElement || target instanceof SVGElement) {
|
|
365
|
+
target.scrollIntoView();
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
};
|
|
370
|
+
const restoreOrScroll = (url, navigateOptions) => {
|
|
371
|
+
if (navigateOptions.restoreScroll) {
|
|
372
|
+
const key = scrollKeyFor(history.state);
|
|
373
|
+
const position = key === undefined ? undefined : scrollPositions.get(key);
|
|
374
|
+
if (key !== undefined && position) {
|
|
375
|
+
rememberScrollPosition(key, position);
|
|
376
|
+
}
|
|
377
|
+
scrollTo(position?.x ?? 0, position?.y ?? 0);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!scrollHashIntoView(url)) {
|
|
381
|
+
scrollTo(0, 0);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const prefetch = async (href) => {
|
|
385
|
+
const url = toUrl(href, location.href || baseUrl);
|
|
386
|
+
const match = matchClientRoute(routes, url.pathname);
|
|
387
|
+
if (!match) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const key = cacheKey(url);
|
|
391
|
+
prefetchControllers.get(key)?.abort();
|
|
392
|
+
const prefetchController = new AbortController();
|
|
393
|
+
prefetchControllers.set(key, prefetchController);
|
|
394
|
+
try {
|
|
395
|
+
await loadData(url, match, prefetchController.signal);
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
if (prefetchControllers.get(key) === prefetchController) {
|
|
399
|
+
prefetchControllers.delete(key);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const revalidate = async (hrefs) => {
|
|
404
|
+
if (!hrefs) {
|
|
405
|
+
cache.clear();
|
|
406
|
+
prefetchControllers.forEach((controller) => controller.abort());
|
|
407
|
+
prefetchControllers.clear();
|
|
408
|
+
}
|
|
409
|
+
else if (typeof hrefs === "string") {
|
|
410
|
+
const key = cacheKey(toUrl(hrefs, location.href || baseUrl));
|
|
411
|
+
cache.delete(key);
|
|
412
|
+
prefetchControllers.get(key)?.abort();
|
|
413
|
+
prefetchControllers.delete(key);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
for (const href of hrefs) {
|
|
417
|
+
const key = cacheKey(toUrl(href, location.href || baseUrl));
|
|
418
|
+
cache.delete(key);
|
|
419
|
+
prefetchControllers.get(key)?.abort();
|
|
420
|
+
prefetchControllers.delete(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
await navigate(location.pathname + location.search + location.hash, { replace: true });
|
|
424
|
+
};
|
|
425
|
+
const hrefsForAction = (url, match, response) => {
|
|
426
|
+
const policy = match.route.revalidateOnAction ?? "self";
|
|
427
|
+
if (policy === "all") {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
if (policy === "self") {
|
|
431
|
+
return url.pathname + url.search;
|
|
432
|
+
}
|
|
433
|
+
if (typeof policy === "function") {
|
|
434
|
+
return policy({ url, response });
|
|
435
|
+
}
|
|
436
|
+
return policy;
|
|
437
|
+
};
|
|
438
|
+
const submit = async (href, init = {}) => {
|
|
439
|
+
const url = toUrl(href, location.href || baseUrl);
|
|
440
|
+
const match = matchClientRoute(routes, url.pathname);
|
|
441
|
+
if (!match?.route.action) {
|
|
442
|
+
throw new Error(`No action route matched ${url.pathname}.`);
|
|
443
|
+
}
|
|
444
|
+
actionController?.abort();
|
|
445
|
+
const nextActionController = new AbortController();
|
|
446
|
+
actionController = nextActionController;
|
|
447
|
+
const version = ++actionVersion;
|
|
448
|
+
const callerSignal = init.signal;
|
|
449
|
+
const abortFromCaller = () => nextActionController.abort(callerSignal?.reason);
|
|
450
|
+
if (callerSignal?.aborted) {
|
|
451
|
+
abortFromCaller();
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
455
|
+
}
|
|
456
|
+
const request = new Request(url, { method: init.method ?? "POST", ...init, signal: nextActionController.signal });
|
|
457
|
+
try {
|
|
458
|
+
const response = await match.route.action({
|
|
459
|
+
url,
|
|
460
|
+
params: match.params,
|
|
461
|
+
signal: nextActionController.signal,
|
|
462
|
+
request,
|
|
463
|
+
});
|
|
464
|
+
if (nextActionController.signal.aborted || version !== actionVersion) {
|
|
465
|
+
return response;
|
|
466
|
+
}
|
|
467
|
+
const locationHeader = response.headers.get("location");
|
|
468
|
+
if (response.status >= 300 && response.status < 400 && locationHeader) {
|
|
469
|
+
await navigate(locationHeader, { replace: true });
|
|
470
|
+
return response;
|
|
471
|
+
}
|
|
472
|
+
await revalidate(hrefsForAction(url, match, response));
|
|
473
|
+
return response;
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
477
|
+
if (actionController === nextActionController) {
|
|
478
|
+
actionController = undefined;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
const navigate = async (href, navigateOptions = {}) => {
|
|
483
|
+
controller?.abort();
|
|
484
|
+
actionController?.abort();
|
|
485
|
+
const nextController = new AbortController();
|
|
486
|
+
controller = nextController;
|
|
487
|
+
const url = toUrl(href, location.href || baseUrl);
|
|
488
|
+
if (!navigateOptions.restoreScroll) {
|
|
489
|
+
saveCurrentScrollPosition();
|
|
490
|
+
}
|
|
491
|
+
if (location.pathname === url.pathname && location.search === url.search && location.hash !== url.hash) {
|
|
492
|
+
writeHistory(url, navigateOptions);
|
|
493
|
+
restoreOrScroll(url, navigateOptions);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const match = matchClientRoute(routes, url.pathname);
|
|
497
|
+
const task = (async () => {
|
|
498
|
+
if (!match) {
|
|
499
|
+
writeHistory(url, navigateOptions);
|
|
500
|
+
renderNotFound(url);
|
|
501
|
+
restoreOrScroll(url, navigateOptions);
|
|
502
|
+
focusRouteContent(options.root, focusSelector);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let data;
|
|
506
|
+
try {
|
|
507
|
+
data = await loadData(url, match, nextController.signal);
|
|
508
|
+
if (nextController.signal.aborted) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const rendered = await match.route.render({ url, params: match.params, data, signal: nextController.signal });
|
|
512
|
+
if (nextController.signal.aborted) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
writeHistory(url, navigateOptions);
|
|
516
|
+
await commitNavigation(url, match, data, rendered, navigateOptions, nextController.signal);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
if (!nextController.signal.aborted) {
|
|
520
|
+
renderError(url, error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
currentNavigation = task;
|
|
525
|
+
await task;
|
|
526
|
+
};
|
|
527
|
+
const routeLinkForEvent = (event) => {
|
|
528
|
+
if (!(event instanceof MouseEvent) || isModifiedClick(event)) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const target = event.target instanceof Element ? event.target : undefined;
|
|
532
|
+
const link = target?.closest("a[href]");
|
|
533
|
+
if (!(link instanceof HTMLAnchorElement) || !options.root.contains(link)) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
if (link.target || link.hasAttribute("download")) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
const url = new URL(link.href);
|
|
540
|
+
if (url.origin !== location.origin || !matchClientRoute(routes, url.pathname)) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return { link, url };
|
|
544
|
+
};
|
|
545
|
+
const onPointerDown = (event) => {
|
|
546
|
+
if (!options.eager) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const routeLink = routeLinkForEvent(event);
|
|
550
|
+
if (!routeLink || !cache.has(cacheKey(routeLink.url))) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
if (eagerlyNavigated.has(routeLink.link)) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
eagerlyNavigated.add(routeLink.link);
|
|
558
|
+
void navigate(routeLink.url.pathname + routeLink.url.search + routeLink.url.hash);
|
|
559
|
+
};
|
|
560
|
+
const onClick = (event) => {
|
|
561
|
+
const routeLink = routeLinkForEvent(event);
|
|
562
|
+
if (!routeLink) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
event.preventDefault();
|
|
566
|
+
if (eagerlyNavigated.has(routeLink.link)) {
|
|
567
|
+
eagerlyNavigated.delete(routeLink.link);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
void navigate(routeLink.url.pathname + routeLink.url.search + routeLink.url.hash);
|
|
571
|
+
};
|
|
572
|
+
const onPointerOver = (event) => {
|
|
573
|
+
const target = event.target instanceof Element ? event.target : undefined;
|
|
574
|
+
const link = target?.closest("a[href][data-prefetch]");
|
|
575
|
+
if (!(link instanceof HTMLAnchorElement) || !options.root.contains(link)) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const url = new URL(link.href);
|
|
579
|
+
if (url.origin === location.origin) {
|
|
580
|
+
void prefetch(url.pathname + url.search + url.hash);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const onPopState = () => {
|
|
584
|
+
void navigate(location.pathname + location.search + location.hash, { replace: true, restoreScroll: true });
|
|
585
|
+
};
|
|
586
|
+
return {
|
|
587
|
+
start: async () => {
|
|
588
|
+
options.root.addEventListener("click", onClick);
|
|
589
|
+
options.root.addEventListener("pointerdown", onPointerDown);
|
|
590
|
+
options.root.addEventListener("mousedown", onPointerDown);
|
|
591
|
+
options.root.addEventListener("pointerover", onPointerOver);
|
|
592
|
+
addEventListener("popstate", onPopState);
|
|
593
|
+
await navigate(location.pathname + location.search + location.hash, { replace: true });
|
|
594
|
+
},
|
|
595
|
+
navigate,
|
|
596
|
+
submit,
|
|
597
|
+
prefetch,
|
|
598
|
+
revalidate,
|
|
599
|
+
invalidate: (href) => {
|
|
600
|
+
if (!href) {
|
|
601
|
+
cache.clear();
|
|
602
|
+
prefetchControllers.forEach((prefetchController) => prefetchController.abort());
|
|
603
|
+
prefetchControllers.clear();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const key = cacheKey(toUrl(href, location.href || baseUrl));
|
|
607
|
+
cache.delete(key);
|
|
608
|
+
prefetchControllers.get(key)?.abort();
|
|
609
|
+
prefetchControllers.delete(key);
|
|
610
|
+
},
|
|
611
|
+
settled: () => currentNavigation,
|
|
612
|
+
dispose: () => {
|
|
613
|
+
controller?.abort();
|
|
614
|
+
actionController?.abort();
|
|
615
|
+
prefetchControllers.forEach((prefetchController) => prefetchController.abort());
|
|
616
|
+
prefetchControllers.clear();
|
|
617
|
+
options.root.removeEventListener("click", onClick);
|
|
618
|
+
options.root.removeEventListener("pointerdown", onPointerDown);
|
|
619
|
+
options.root.removeEventListener("mousedown", onPointerDown);
|
|
620
|
+
options.root.removeEventListener("pointerover", onPointerOver);
|
|
621
|
+
removeEventListener("popstate", onPopState);
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
};
|
|
625
|
+
export const createRouteHotReloader = (options) => ({
|
|
626
|
+
accept: async (update = {}) => {
|
|
627
|
+
const href = update.href ?? options.currentPath?.() ?? `${location.pathname}${location.search}${location.hash}`;
|
|
628
|
+
options.invalidate(href);
|
|
629
|
+
await options.onUpdate?.({ ...update, href });
|
|
630
|
+
await options.navigate(href, { replace: true });
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
export const connectRouteHotReloader = (hot, reloader) => {
|
|
634
|
+
if (!hot) {
|
|
635
|
+
return () => undefined;
|
|
636
|
+
}
|
|
637
|
+
let disposed = false;
|
|
638
|
+
hot.on("tachyon-dom:routes-update", (payload) => {
|
|
639
|
+
if (!disposed) {
|
|
640
|
+
void reloader.accept(payload);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
hot.dispose?.(() => {
|
|
644
|
+
disposed = true;
|
|
645
|
+
});
|
|
646
|
+
return () => {
|
|
647
|
+
disposed = true;
|
|
648
|
+
};
|
|
649
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
declare const signalBrand: unique symbol;
|
|
2
|
+
export type Accessor<T> = (() => T) & {
|
|
3
|
+
readonly [signalBrand]: true;
|
|
4
|
+
};
|
|
5
|
+
export type Signal<T> = Accessor<T> & {
|
|
6
|
+
set: (value: T) => void;
|
|
7
|
+
update: (updater: (value: T) => T) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare const isSignal: (value: unknown) => value is Accessor<unknown>;
|
|
10
|
+
export declare function read<T>(value: Accessor<T>): T;
|
|
11
|
+
export declare function read<T>(value: T): T;
|
|
12
|
+
export declare const untrack: <T>(fn: () => T) => T;
|
|
13
|
+
export declare const batch: <T>(fn: () => T) => T;
|
|
14
|
+
export declare const createSignal: <T>(initial: T) => Signal<T>;
|
|
15
|
+
export declare const createMemo: <T>(fn: () => T) => Accessor<T>;
|
|
16
|
+
export declare const createStore: <T extends Record<PropertyKey, unknown>>(initial: T) => T;
|
|
17
|
+
export declare const effect: (fn: () => void) => (() => void);
|
|
18
|
+
export declare const catchError: (fn: () => void, onError: (error: unknown) => void) => (() => void);
|
|
19
|
+
export type Resource<T> = {
|
|
20
|
+
data: Accessor<T | undefined>;
|
|
21
|
+
error: Accessor<unknown | undefined>;
|
|
22
|
+
loading: Accessor<boolean>;
|
|
23
|
+
refetch: () => Promise<T | undefined>;
|
|
24
|
+
dispose: () => void;
|
|
25
|
+
};
|
|
26
|
+
export type ResourceFetcherContext = {
|
|
27
|
+
signal: AbortSignal;
|
|
28
|
+
};
|
|
29
|
+
export declare const createResource: <Source, T>(source: Source | Accessor<Source>, fetcher: (source: Source, context: ResourceFetcherContext) => Promise<T> | T) => Resource<T>;
|
|
30
|
+
export {};
|