@ternkit/tern 0.3.1 → 0.4.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/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/runtime/navigation.d.ts +55 -0
- package/dist/runtime/navigation.js +69 -0
- package/dist/runtime/router.svelte.d.ts +5 -0
- package/dist/runtime/router.svelte.js +131 -13
- package/dist/runtime/types.d.ts +11 -1
- package/dist/server/intl.d.ts +100 -0
- package/dist/server/intl.js +222 -0
- package/dist/server/polyfills.js +9 -0
- package/dist/vite/index.js +11 -3
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
-
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
11
|
+
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
|
+
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
|
+
export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.js';
|
|
12
14
|
export { enhance } from './runtime/forms.js';
|
|
13
15
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
14
16
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
15
17
|
/** The version of the Tern client runtime. */
|
|
16
|
-
export declare const VERSION = "0.
|
|
18
|
+
export declare const VERSION = "0.4.0";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
-
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
11
|
+
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
|
+
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
12
13
|
export { enhance } from './runtime/forms.js';
|
|
13
14
|
/** The version of the Tern client runtime. */
|
|
14
|
-
export const VERSION = '0.
|
|
15
|
+
export const VERSION = '0.4.0';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Navigation } from './types.js';
|
|
2
|
+
/** Passed to beforeNavigate callbacks. */
|
|
3
|
+
export interface BeforeNavigateEvent extends Navigation {
|
|
4
|
+
/** True when the browser is about to leave the app (a reload, closing the tab, or another site). */
|
|
5
|
+
willUnload: boolean;
|
|
6
|
+
/** Stops the navigation. When leaving the page, the browser asks the user to confirm. */
|
|
7
|
+
cancel(): void;
|
|
8
|
+
}
|
|
9
|
+
/** Passed to onNavigate callbacks. */
|
|
10
|
+
export interface OnNavigateEvent extends Navigation {
|
|
11
|
+
/** Resolves when the new page has rendered. */
|
|
12
|
+
complete: Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
type BeforeFn = (nav: BeforeNavigateEvent) => void;
|
|
15
|
+
type OnFn = (nav: OnNavigateEvent) => void | Promise<void> | (() => void) | Promise<() => void>;
|
|
16
|
+
type AfterFn = (nav: Navigation) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Runs before every navigation, including leaving the app. Call
|
|
19
|
+
* `nav.cancel()` to stop it, for example to keep unsaved changes:
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
* beforeNavigate((nav) => {
|
|
23
|
+
* if (dirty && !confirm('Discard your changes?')) nav.cancel();
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Call it while a component initializes; it is removed when the component is destroyed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function beforeNavigate(fn: BeforeFn): void;
|
|
30
|
+
/**
|
|
31
|
+
* Runs when a client-side navigation has loaded its data, right before the
|
|
32
|
+
* new page renders. Return a promise to delay rendering, for example to
|
|
33
|
+
* start a view transition; return a function to run after the navigation.
|
|
34
|
+
*
|
|
35
|
+
* ```js
|
|
36
|
+
* onNavigate((nav) => {
|
|
37
|
+
* if (!document.startViewTransition) return;
|
|
38
|
+
* return new Promise((resolve) => {
|
|
39
|
+
* document.startViewTransition(async () => {
|
|
40
|
+
* resolve();
|
|
41
|
+
* await nav.complete;
|
|
42
|
+
* });
|
|
43
|
+
* });
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function onNavigate(fn: OnFn): void;
|
|
48
|
+
/** Runs after every navigation, including the first page load. */
|
|
49
|
+
export declare function afterNavigate(fn: AfterFn): void;
|
|
50
|
+
/** Runs beforeNavigate callbacks; reports whether the navigation may go ahead. */
|
|
51
|
+
export declare function runBefore(nav: Navigation, willUnload: boolean): boolean;
|
|
52
|
+
/** Runs onNavigate callbacks and returns the cleanups to call afterwards. */
|
|
53
|
+
export declare function runOn(nav: Navigation, complete: Promise<void>): Promise<(() => void)[]>;
|
|
54
|
+
export declare function runAfter(nav: Navigation): void;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { onMount } from 'svelte';
|
|
2
|
+
const before = new Set();
|
|
3
|
+
const on = new Set();
|
|
4
|
+
const after = new Set();
|
|
5
|
+
/**
|
|
6
|
+
* Runs before every navigation, including leaving the app. Call
|
|
7
|
+
* `nav.cancel()` to stop it, for example to keep unsaved changes:
|
|
8
|
+
*
|
|
9
|
+
* ```js
|
|
10
|
+
* beforeNavigate((nav) => {
|
|
11
|
+
* if (dirty && !confirm('Discard your changes?')) nav.cancel();
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Call it while a component initializes; it is removed when the component is destroyed.
|
|
16
|
+
*/
|
|
17
|
+
export function beforeNavigate(fn) {
|
|
18
|
+
onMount(() => {
|
|
19
|
+
before.add(fn);
|
|
20
|
+
return () => before.delete(fn);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Runs when a client-side navigation has loaded its data, right before the
|
|
25
|
+
* new page renders. Return a promise to delay rendering, for example to
|
|
26
|
+
* start a view transition; return a function to run after the navigation.
|
|
27
|
+
*
|
|
28
|
+
* ```js
|
|
29
|
+
* onNavigate((nav) => {
|
|
30
|
+
* if (!document.startViewTransition) return;
|
|
31
|
+
* return new Promise((resolve) => {
|
|
32
|
+
* document.startViewTransition(async () => {
|
|
33
|
+
* resolve();
|
|
34
|
+
* await nav.complete;
|
|
35
|
+
* });
|
|
36
|
+
* });
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function onNavigate(fn) {
|
|
41
|
+
onMount(() => {
|
|
42
|
+
on.add(fn);
|
|
43
|
+
return () => on.delete(fn);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Runs after every navigation, including the first page load. */
|
|
47
|
+
export function afterNavigate(fn) {
|
|
48
|
+
onMount(() => {
|
|
49
|
+
after.add(fn);
|
|
50
|
+
return () => after.delete(fn);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Runs beforeNavigate callbacks; reports whether the navigation may go ahead. */
|
|
54
|
+
export function runBefore(nav, willUnload) {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
const event = { ...nav, willUnload, cancel: () => (cancelled = true) };
|
|
57
|
+
for (const fn of before)
|
|
58
|
+
fn(event);
|
|
59
|
+
return !cancelled;
|
|
60
|
+
}
|
|
61
|
+
/** Runs onNavigate callbacks and returns the cleanups to call afterwards. */
|
|
62
|
+
export async function runOn(nav, complete) {
|
|
63
|
+
const results = await Promise.all([...on].map((fn) => fn({ ...nav, complete })));
|
|
64
|
+
return results.filter((r) => typeof r === 'function');
|
|
65
|
+
}
|
|
66
|
+
export function runAfter(nav) {
|
|
67
|
+
for (const fn of after)
|
|
68
|
+
fn(nav);
|
|
69
|
+
}
|
|
@@ -3,6 +3,11 @@ import type { ActionResult, ClientManifest, GotoOptions, Payload } from './types
|
|
|
3
3
|
export declare function start(m: ClientManifest): Promise<void>;
|
|
4
4
|
/** Navigates to href, which may be relative to the current URL. */
|
|
5
5
|
export declare function goto(href: string | URL, opts?: GotoOptions): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Reruns the loads that depend on key, as declared with ctx.Depends(key)
|
|
8
|
+
* in Go, plus the current page's load.
|
|
9
|
+
*/
|
|
10
|
+
export declare function invalidate(key: string): Promise<void>;
|
|
6
11
|
/** Reruns every load for the current page. */
|
|
7
12
|
export declare function invalidateAll(): Promise<void>;
|
|
8
13
|
/**
|
|
@@ -3,6 +3,7 @@ import DefaultError from './DefaultError.svelte';
|
|
|
3
3
|
import Passthrough from './Passthrough.svelte';
|
|
4
4
|
import Root from './Root.svelte';
|
|
5
5
|
import { DeferredScope, readStreamed, revive } from './deferred.js';
|
|
6
|
+
import { runAfter, runBefore, runOn } from './navigation.js';
|
|
6
7
|
import { plan } from './select.js';
|
|
7
8
|
import { applyPayloadToPage, page, setNavigating } from './state.svelte.js';
|
|
8
9
|
const MAX_REDIRECTS = 20;
|
|
@@ -22,6 +23,11 @@ const props = $state({
|
|
|
22
23
|
form: null
|
|
23
24
|
});
|
|
24
25
|
const preloads = new Map();
|
|
26
|
+
const held = new Map();
|
|
27
|
+
const invalidated = new Set();
|
|
28
|
+
let ignoreNextPop = false;
|
|
29
|
+
/** Marks invalidate() reloads, which are not navigations: no hooks, no navigating state. */
|
|
30
|
+
const RELOAD = Symbol('reload');
|
|
25
31
|
/** Starts the client. Called by the generated entry module. */
|
|
26
32
|
export async function start(m) {
|
|
27
33
|
manifest = m;
|
|
@@ -36,7 +42,7 @@ export async function start(m) {
|
|
|
36
42
|
catch {
|
|
37
43
|
scrollPositions = {};
|
|
38
44
|
}
|
|
39
|
-
historyIndex = history.state?.ternIndex ??
|
|
45
|
+
historyIndex = history.state?.ternIndex ?? 0;
|
|
40
46
|
if (history.state?.ternIndex == null) {
|
|
41
47
|
history.replaceState({ ...history.state, ternIndex: historyIndex, ternState: {} }, '');
|
|
42
48
|
}
|
|
@@ -58,6 +64,8 @@ export async function start(m) {
|
|
|
58
64
|
mount(Root, { target, props });
|
|
59
65
|
}
|
|
60
66
|
started = true;
|
|
67
|
+
await tick();
|
|
68
|
+
runAfter({ from: null, to: current(), type: 'enter' });
|
|
61
69
|
const saved = scrollPositions[historyIndex];
|
|
62
70
|
if (saved) {
|
|
63
71
|
scrollTo(saved.x, saved.y);
|
|
@@ -79,6 +87,21 @@ export async function goto(href, opts = {}) {
|
|
|
79
87
|
}
|
|
80
88
|
await navigate(url, opts, 'goto', 0);
|
|
81
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Reruns the loads that depend on key, as declared with ctx.Depends(key)
|
|
92
|
+
* in Go, plus the current page's load.
|
|
93
|
+
*/
|
|
94
|
+
export function invalidate(key) {
|
|
95
|
+
invalidated.add(key);
|
|
96
|
+
preloads.clear();
|
|
97
|
+
return navigate(new URL(location.href), {
|
|
98
|
+
replaceState: true,
|
|
99
|
+
noScroll: true,
|
|
100
|
+
keepFocus: true,
|
|
101
|
+
state: page.state,
|
|
102
|
+
[RELOAD]: true
|
|
103
|
+
}, 'goto', 0);
|
|
104
|
+
}
|
|
82
105
|
/** Reruns every load for the current page. */
|
|
83
106
|
export function invalidateAll() {
|
|
84
107
|
return navigate(new URL(location.href), {
|
|
@@ -86,7 +109,8 @@ export function invalidateAll() {
|
|
|
86
109
|
noScroll: true,
|
|
87
110
|
keepFocus: true,
|
|
88
111
|
invalidateAll: true,
|
|
89
|
-
state: page.state
|
|
112
|
+
state: page.state,
|
|
113
|
+
[RELOAD]: true
|
|
90
114
|
}, 'goto', 0);
|
|
91
115
|
}
|
|
92
116
|
/**
|
|
@@ -136,13 +160,33 @@ function setForm(form) {
|
|
|
136
160
|
page.form = form;
|
|
137
161
|
props.form = form;
|
|
138
162
|
}
|
|
139
|
-
function dataURL(url) {
|
|
163
|
+
function dataURL(url, reuse) {
|
|
140
164
|
const path = url.pathname.replace(/\/$/, '');
|
|
141
|
-
|
|
165
|
+
let target = `${path}/__data.json${url.search}`;
|
|
166
|
+
if (reuse) {
|
|
167
|
+
const here = url.pathname + url.search;
|
|
168
|
+
const entries = [];
|
|
169
|
+
for (const [i, h] of held) {
|
|
170
|
+
if (!h.uses)
|
|
171
|
+
continue; // no load: nothing to reuse
|
|
172
|
+
if (h.uses.url && h.url !== here)
|
|
173
|
+
continue;
|
|
174
|
+
if (h.uses.deps?.some((d) => invalidated.has(d)))
|
|
175
|
+
continue;
|
|
176
|
+
const p = {};
|
|
177
|
+
for (const name of h.uses.params ?? [])
|
|
178
|
+
p[name] = h.params[name] ?? '';
|
|
179
|
+
entries.push({ i, p });
|
|
180
|
+
}
|
|
181
|
+
if (entries.length) {
|
|
182
|
+
target += (url.search ? '&' : '?') + 'x-tern-reuse=' + encodeURIComponent(JSON.stringify(entries));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return target;
|
|
142
186
|
}
|
|
143
|
-
async function fetchPayload(url) {
|
|
187
|
+
async function fetchPayload(url, reuse = true) {
|
|
144
188
|
try {
|
|
145
|
-
const res = await fetch(dataURL(url), {
|
|
189
|
+
const res = await fetch(dataURL(url, reuse), {
|
|
146
190
|
headers: { accept: 'application/json' },
|
|
147
191
|
credentials: 'same-origin'
|
|
148
192
|
});
|
|
@@ -164,18 +208,30 @@ function takePreload(url) {
|
|
|
164
208
|
}
|
|
165
209
|
return undefined;
|
|
166
210
|
}
|
|
167
|
-
async function navigate(url, opts, type, redirects, popIndex) {
|
|
211
|
+
async function navigate(url, opts, type, redirects, popIndex, fromIndex) {
|
|
168
212
|
const id = ++token;
|
|
169
213
|
// A change of hash alone is a scroll, not a navigation.
|
|
170
214
|
if (type !== 'popstate' && !opts.invalidateAll && url.hash && samePage(url)) {
|
|
171
215
|
saveScroll();
|
|
172
|
-
history.pushState({ ternIndex: (historyIndex =
|
|
216
|
+
history.pushState({ ternIndex: (historyIndex = historyIndex + 1), ternState: $state.snapshot(opts.state ?? {}) }, '', url);
|
|
173
217
|
scrollToHash(url.hash);
|
|
174
218
|
return;
|
|
175
219
|
}
|
|
176
220
|
const from = current();
|
|
177
|
-
|
|
178
|
-
const
|
|
221
|
+
const nav = { from, to: { url, params: null, route: { id: null } }, type };
|
|
222
|
+
const reload = opts[RELOAD] === true;
|
|
223
|
+
if (!reload && !runBefore(nav, false)) {
|
|
224
|
+
if (type === 'popstate' && popIndex != null && fromIndex != null) {
|
|
225
|
+
// The browser already moved; move back to where we were.
|
|
226
|
+
ignoreNextPop = true;
|
|
227
|
+
historyIndex = fromIndex;
|
|
228
|
+
history.go(fromIndex - popIndex);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!reload)
|
|
233
|
+
setNavigating(nav);
|
|
234
|
+
const payload = await (opts.invalidateAll ? undefined : takePreload(url)) ?? (await fetchPayload(url, !opts.invalidateAll));
|
|
179
235
|
if (id !== token)
|
|
180
236
|
return;
|
|
181
237
|
if (!payload) {
|
|
@@ -188,11 +244,17 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
188
244
|
location.href = next.href;
|
|
189
245
|
return;
|
|
190
246
|
}
|
|
191
|
-
return navigate(next, { ...opts, replaceState: true }, type, redirects + 1);
|
|
247
|
+
return navigate(next, { ...opts, replaceState: true, [RELOAD]: false }, type, redirects + 1);
|
|
192
248
|
}
|
|
193
249
|
if (type !== 'popstate')
|
|
194
250
|
saveScroll(); // popstate saved before historyIndex changed
|
|
195
251
|
const state = opts.state ?? (type === 'popstate' ? history.state?.ternState ?? {} : {});
|
|
252
|
+
nav.to = { url, params: payload.params ?? {}, route: { id: payload.route ?? null } };
|
|
253
|
+
let finish;
|
|
254
|
+
const complete = new Promise((resolve) => (finish = resolve));
|
|
255
|
+
const cleanups = reload ? [] : await runOn(nav, complete);
|
|
256
|
+
if (id !== token)
|
|
257
|
+
return;
|
|
196
258
|
const ok = await render(payload, url, state, id);
|
|
197
259
|
if (id !== token)
|
|
198
260
|
return;
|
|
@@ -207,7 +269,7 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
207
269
|
history.replaceState(entry, '', url);
|
|
208
270
|
}
|
|
209
271
|
else {
|
|
210
|
-
entry.ternIndex = historyIndex =
|
|
272
|
+
entry.ternIndex = historyIndex = historyIndex + 1;
|
|
211
273
|
history.pushState(entry, '', url);
|
|
212
274
|
}
|
|
213
275
|
}
|
|
@@ -226,6 +288,12 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
226
288
|
if (!opts.keepFocus) {
|
|
227
289
|
resetFocus();
|
|
228
290
|
}
|
|
291
|
+
invalidated.clear();
|
|
292
|
+
finish();
|
|
293
|
+
if (!reload)
|
|
294
|
+
runAfter(nav);
|
|
295
|
+
for (const fn of cleanups)
|
|
296
|
+
fn();
|
|
229
297
|
}
|
|
230
298
|
function samePage(url) {
|
|
231
299
|
return url.pathname === location.pathname && url.search === location.search;
|
|
@@ -249,6 +317,16 @@ function loadRoute(id) {
|
|
|
249
317
|
}
|
|
250
318
|
/** Renders a payload. It reports false if the payload is not a page. */
|
|
251
319
|
async function render(p, url, state, id) {
|
|
320
|
+
const route = p.route ? manifest.routes[p.route] : undefined;
|
|
321
|
+
const layouts = route ? route.layouts : manifest.fallback.layouts;
|
|
322
|
+
// Fill in the layout data the server skipped because we already have it.
|
|
323
|
+
if (p.reused?.length) {
|
|
324
|
+
p.nodes = [...(p.nodes ?? [])];
|
|
325
|
+
for (const k of p.reused) {
|
|
326
|
+
const li = layouts[k];
|
|
327
|
+
p.nodes[k] = li === undefined ? null : (held.get(li)?.data ?? null);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
252
330
|
const what = plan(manifest, p);
|
|
253
331
|
let components;
|
|
254
332
|
switch (what.kind) {
|
|
@@ -274,6 +352,14 @@ async function render(p, url, state, id) {
|
|
|
274
352
|
nodes = p.nodes ?? [];
|
|
275
353
|
if (id !== token)
|
|
276
354
|
return true; // superseded; the newer navigation renders
|
|
355
|
+
if (p.type === 'data') {
|
|
356
|
+
const here = url.pathname + url.search;
|
|
357
|
+
layouts.forEach((li, k) => {
|
|
358
|
+
if (p.reused?.includes(k))
|
|
359
|
+
return;
|
|
360
|
+
held.set(li, { data: p.nodes?.[k] ?? null, uses: p.uses?.[k] ?? null, params: p.params ?? {}, url: here });
|
|
361
|
+
});
|
|
362
|
+
}
|
|
277
363
|
applyPayloadToPage(p, url, state, what.data);
|
|
278
364
|
props.components = components;
|
|
279
365
|
props.data = what.data;
|
|
@@ -376,13 +462,45 @@ function listen() {
|
|
|
376
462
|
};
|
|
377
463
|
document.addEventListener('mousedown', onTap);
|
|
378
464
|
document.addEventListener('touchstart', onTap, { passive: true });
|
|
465
|
+
// GET forms navigate on the client, like links. POST forms are handled
|
|
466
|
+
// by use:enhance or submit normally.
|
|
467
|
+
document.addEventListener('submit', (event) => {
|
|
468
|
+
if (event.defaultPrevented)
|
|
469
|
+
return;
|
|
470
|
+
const form = event.target;
|
|
471
|
+
const submitter = event.submitter;
|
|
472
|
+
const own = HTMLFormElement.prototype.cloneNode.call(form);
|
|
473
|
+
const method = submitter?.hasAttribute('formmethod') ? submitter.formMethod : own.method;
|
|
474
|
+
const target = submitter?.getAttribute('formtarget') ?? own.target;
|
|
475
|
+
if (method !== 'get' || (target && target !== '_self') || form.closest('[data-tern-reload]:not([data-tern-reload="off"])')) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const url = new URL(submitter?.hasAttribute('formaction') ? submitter.formAction : own.action);
|
|
479
|
+
if (url.origin !== location.origin)
|
|
480
|
+
return;
|
|
481
|
+
event.preventDefault();
|
|
482
|
+
url.search = new URLSearchParams(new FormData(form, submitter)).toString();
|
|
483
|
+
const filtering = url.pathname === location.pathname;
|
|
484
|
+
void navigate(url, { keepFocus: filtering, noScroll: filtering }, 'form', 0);
|
|
485
|
+
});
|
|
486
|
+
addEventListener('beforeunload', (event) => {
|
|
487
|
+
if (!runBefore({ from: current(), to: null, type: 'leave' }, true)) {
|
|
488
|
+
event.preventDefault();
|
|
489
|
+
event.returnValue = '';
|
|
490
|
+
}
|
|
491
|
+
});
|
|
379
492
|
addEventListener('popstate', (event) => {
|
|
493
|
+
if (ignoreNextPop) {
|
|
494
|
+
ignoreNextPop = false;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
380
497
|
const index = event.state?.ternIndex;
|
|
381
498
|
if (index == null)
|
|
382
499
|
return;
|
|
383
500
|
saveScroll();
|
|
501
|
+
const fromIndex = historyIndex;
|
|
384
502
|
historyIndex = index;
|
|
385
|
-
void navigate(new URL(location.href), { state: event.state?.ternState ?? {} }, 'popstate', 0, index);
|
|
503
|
+
void navigate(new URL(location.href), { state: event.state?.ternState ?? {} }, 'popstate', 0, index, fromIndex);
|
|
386
504
|
});
|
|
387
505
|
addEventListener('pagehide', saveScroll);
|
|
388
506
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -43,6 +43,16 @@ export interface Payload {
|
|
|
43
43
|
data?: unknown;
|
|
44
44
|
form?: unknown;
|
|
45
45
|
hasForm?: boolean;
|
|
46
|
+
/** What each node's load read, so the client knows when to rerun it. */
|
|
47
|
+
uses?: (NodeUses | null)[];
|
|
48
|
+
/** Nodes the server skipped because the client already has their data. */
|
|
49
|
+
reused?: number[];
|
|
50
|
+
}
|
|
51
|
+
export interface NodeUses {
|
|
52
|
+
url?: boolean;
|
|
53
|
+
params?: string[];
|
|
54
|
+
deps?: string[];
|
|
55
|
+
parent?: boolean;
|
|
46
56
|
}
|
|
47
57
|
/** The result of submitting a form action with `use:enhance`. */
|
|
48
58
|
export type ActionResult<Success = Record<string, unknown>, Failure = Record<string, unknown>> = {
|
|
@@ -71,7 +81,7 @@ export interface NavigationTarget {
|
|
|
71
81
|
id: string | null;
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
|
-
export type NavigationType = 'enter' | 'link' | 'goto' | 'popstate' | 'form';
|
|
84
|
+
export type NavigationType = 'enter' | 'link' | 'goto' | 'popstate' | 'form' | 'leave';
|
|
75
85
|
export interface Navigation {
|
|
76
86
|
from: NavigationTarget | null;
|
|
77
87
|
to: NavigationTarget | null;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
type Opts = Record<string, unknown>;
|
|
2
|
+
declare class NumberFormat {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(_locale?: unknown, options?: Opts);
|
|
5
|
+
format(value: number | bigint): string;
|
|
6
|
+
formatToParts(value: number): {
|
|
7
|
+
type: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}[];
|
|
10
|
+
resolvedOptions(): {
|
|
11
|
+
locale: string;
|
|
12
|
+
numberingSystem: string;
|
|
13
|
+
};
|
|
14
|
+
static supportedLocalesOf(): string[];
|
|
15
|
+
}
|
|
16
|
+
declare class DateTimeFormat {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(_locale?: unknown, options?: Opts);
|
|
19
|
+
format(value?: Date | number): string;
|
|
20
|
+
formatToParts(value?: Date | number): {
|
|
21
|
+
type: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}[];
|
|
24
|
+
formatRange(a: Date | number, b: Date | number): string;
|
|
25
|
+
resolvedOptions(): {
|
|
26
|
+
locale: string;
|
|
27
|
+
calendar: string;
|
|
28
|
+
numberingSystem: string;
|
|
29
|
+
timeZone: string;
|
|
30
|
+
};
|
|
31
|
+
static supportedLocalesOf(): string[];
|
|
32
|
+
}
|
|
33
|
+
declare class PluralRules {
|
|
34
|
+
select(n: number): "one" | "other";
|
|
35
|
+
resolvedOptions(): {
|
|
36
|
+
locale: string;
|
|
37
|
+
pluralCategories: string[];
|
|
38
|
+
};
|
|
39
|
+
static supportedLocalesOf(): string[];
|
|
40
|
+
}
|
|
41
|
+
declare class Collator {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(_locale?: unknown, options?: Opts);
|
|
44
|
+
compare: (a: string, b: string) => number;
|
|
45
|
+
resolvedOptions(): {
|
|
46
|
+
locale: string;
|
|
47
|
+
usage: string;
|
|
48
|
+
sensitivity: string;
|
|
49
|
+
};
|
|
50
|
+
static supportedLocalesOf(): string[];
|
|
51
|
+
}
|
|
52
|
+
declare class RelativeTimeFormat {
|
|
53
|
+
format(value: number, unit: string): string;
|
|
54
|
+
static supportedLocalesOf(): string[];
|
|
55
|
+
}
|
|
56
|
+
declare class ListFormat {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(_locale?: unknown, options?: Opts);
|
|
59
|
+
format(list: Iterable<string>): string;
|
|
60
|
+
static supportedLocalesOf(): string[];
|
|
61
|
+
}
|
|
62
|
+
declare class Locale {
|
|
63
|
+
baseName: string;
|
|
64
|
+
language: string;
|
|
65
|
+
region?: string;
|
|
66
|
+
constructor(tag?: string);
|
|
67
|
+
get weekInfo(): {
|
|
68
|
+
firstDay: number;
|
|
69
|
+
weekend: number[];
|
|
70
|
+
minimalDays: number;
|
|
71
|
+
};
|
|
72
|
+
getWeekInfo(): {
|
|
73
|
+
firstDay: number;
|
|
74
|
+
weekend: number[];
|
|
75
|
+
minimalDays: number;
|
|
76
|
+
};
|
|
77
|
+
maximize(): this;
|
|
78
|
+
minimize(): this;
|
|
79
|
+
toString(): string;
|
|
80
|
+
}
|
|
81
|
+
declare class Segmenter {
|
|
82
|
+
segment(input: string): {
|
|
83
|
+
segment: string;
|
|
84
|
+
index: number;
|
|
85
|
+
input: string;
|
|
86
|
+
}[];
|
|
87
|
+
}
|
|
88
|
+
export declare const IntlShim: {
|
|
89
|
+
NumberFormat: typeof NumberFormat;
|
|
90
|
+
DateTimeFormat: typeof DateTimeFormat;
|
|
91
|
+
PluralRules: typeof PluralRules;
|
|
92
|
+
Collator: typeof Collator;
|
|
93
|
+
RelativeTimeFormat: typeof RelativeTimeFormat;
|
|
94
|
+
ListFormat: typeof ListFormat;
|
|
95
|
+
Locale: typeof Locale;
|
|
96
|
+
Segmenter: typeof Segmenter;
|
|
97
|
+
getCanonicalLocales: (l?: string | string[]) => string[];
|
|
98
|
+
supportedValuesOf: () => never[];
|
|
99
|
+
};
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// A small Intl for the server renderer. The embedded engine (QuickJS) has
|
|
2
|
+
// no Intl, and many libraries create formatters when they load. This shim
|
|
3
|
+
// formats like en-US, which is good enough for server-rendered HTML: the
|
|
4
|
+
// browser hydrates with the user's real Intl and locale.
|
|
5
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
6
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
7
|
+
function group(int) {
|
|
8
|
+
return int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
9
|
+
}
|
|
10
|
+
class NumberFormat {
|
|
11
|
+
#o;
|
|
12
|
+
constructor(_locale, options = {}) {
|
|
13
|
+
this.#o = options;
|
|
14
|
+
}
|
|
15
|
+
format(value) {
|
|
16
|
+
let n = Number(value);
|
|
17
|
+
const o = this.#o;
|
|
18
|
+
if (!Number.isFinite(n))
|
|
19
|
+
return Number.isNaN(n) ? 'NaN' : n > 0 ? '∞' : '-∞';
|
|
20
|
+
let suffix = '';
|
|
21
|
+
if (o.style === 'percent') {
|
|
22
|
+
n *= 100;
|
|
23
|
+
suffix = '%';
|
|
24
|
+
}
|
|
25
|
+
if (o.notation === 'compact') {
|
|
26
|
+
const abs = Math.abs(n);
|
|
27
|
+
for (const [limit, s] of [[1e12, 'T'], [1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {
|
|
28
|
+
if (abs >= limit) {
|
|
29
|
+
n /= limit;
|
|
30
|
+
suffix = s + suffix;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const isCurrency = o.style === 'currency';
|
|
36
|
+
const max = o.maximumFractionDigits ?? (isCurrency ? 2 : o.notation === 'compact' ? 1 : 3);
|
|
37
|
+
const min = o.minimumFractionDigits ?? (isCurrency ? 2 : 0);
|
|
38
|
+
let s = Math.abs(n).toFixed(Math.max(min, Math.min(max, 20)));
|
|
39
|
+
if (s.includes('.')) {
|
|
40
|
+
let [int, frac = ''] = s.split('.');
|
|
41
|
+
while (frac.length > min && frac.endsWith('0'))
|
|
42
|
+
frac = frac.slice(0, -1);
|
|
43
|
+
s = frac ? `${int}.${frac}` : int;
|
|
44
|
+
}
|
|
45
|
+
const [int, frac] = s.split('.');
|
|
46
|
+
s = (o.useGrouping === false ? int : group(int)) + (frac ? `.${frac}` : '');
|
|
47
|
+
if (isCurrency) {
|
|
48
|
+
const code = String(o.currency ?? 'USD').toUpperCase();
|
|
49
|
+
s = (code === 'USD' ? '$' : code === 'EUR' ? '€' : code === 'GBP' ? '£' : `${code} `) + s;
|
|
50
|
+
}
|
|
51
|
+
return (n < 0 ? '-' : '') + s + suffix;
|
|
52
|
+
}
|
|
53
|
+
formatToParts(value) {
|
|
54
|
+
return [{ type: 'literal', value: this.format(value) }];
|
|
55
|
+
}
|
|
56
|
+
resolvedOptions() {
|
|
57
|
+
return { locale: 'en-US', numberingSystem: 'latn', ...this.#o };
|
|
58
|
+
}
|
|
59
|
+
static supportedLocalesOf() {
|
|
60
|
+
return ['en-US'];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class DateTimeFormat {
|
|
64
|
+
#o;
|
|
65
|
+
constructor(_locale, options = {}) {
|
|
66
|
+
this.#o = options;
|
|
67
|
+
}
|
|
68
|
+
format(value) {
|
|
69
|
+
const d = value === undefined ? new Date() : new Date(value);
|
|
70
|
+
if (Number.isNaN(d.getTime()))
|
|
71
|
+
return 'Invalid Date';
|
|
72
|
+
const o = this.#o;
|
|
73
|
+
const utc = o.timeZone === 'UTC';
|
|
74
|
+
const get = {
|
|
75
|
+
y: utc ? d.getUTCFullYear() : d.getFullYear(),
|
|
76
|
+
m: utc ? d.getUTCMonth() : d.getMonth(),
|
|
77
|
+
d: utc ? d.getUTCDate() : d.getDate(),
|
|
78
|
+
w: utc ? d.getUTCDay() : d.getDay(),
|
|
79
|
+
h: utc ? d.getUTCHours() : d.getHours(),
|
|
80
|
+
min: utc ? d.getUTCMinutes() : d.getMinutes()
|
|
81
|
+
};
|
|
82
|
+
const hasDate = o.year || o.month || o.day || o.weekday || o.dateStyle;
|
|
83
|
+
const hasTime = o.hour || o.minute || o.timeStyle;
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (hasDate || !hasTime) {
|
|
86
|
+
const month = o.month ?? (o.dateStyle ? 'short' : 'numeric');
|
|
87
|
+
if (o.weekday)
|
|
88
|
+
parts.push((o.weekday === 'long' ? DAYS[get.w] : DAYS[get.w].slice(0, 3)) + ',');
|
|
89
|
+
if (month === 'numeric' || month === '2-digit') {
|
|
90
|
+
parts.push(`${get.m + 1}/${get.d}/${get.y}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const name = month === 'long' ? MONTHS[get.m] : MONTHS[get.m].slice(0, 3);
|
|
94
|
+
const day = o.day !== undefined || o.dateStyle ? ` ${get.d}` : '';
|
|
95
|
+
const year = o.year !== undefined || o.dateStyle ? `, ${get.y}` : '';
|
|
96
|
+
parts.push(name + day + (day ? year : year.replace(', ', ' ')));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (hasTime) {
|
|
100
|
+
const h12 = o.hour12 !== false;
|
|
101
|
+
const hour = h12 ? get.h % 12 || 12 : get.h;
|
|
102
|
+
const mm = String(get.min).padStart(2, '0');
|
|
103
|
+
parts.push(`${hour}:${mm}${h12 ? (get.h < 12 ? ' AM' : ' PM') : ''}`);
|
|
104
|
+
}
|
|
105
|
+
return parts.join(' ');
|
|
106
|
+
}
|
|
107
|
+
formatToParts(value) {
|
|
108
|
+
return [{ type: 'literal', value: this.format(value) }];
|
|
109
|
+
}
|
|
110
|
+
formatRange(a, b) {
|
|
111
|
+
return `${this.format(a)} – ${this.format(b)}`;
|
|
112
|
+
}
|
|
113
|
+
resolvedOptions() {
|
|
114
|
+
return { locale: 'en-US', calendar: 'gregory', numberingSystem: 'latn', timeZone: 'UTC', ...this.#o };
|
|
115
|
+
}
|
|
116
|
+
static supportedLocalesOf() {
|
|
117
|
+
return ['en-US'];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
class PluralRules {
|
|
121
|
+
select(n) {
|
|
122
|
+
return n === 1 ? 'one' : 'other';
|
|
123
|
+
}
|
|
124
|
+
resolvedOptions() {
|
|
125
|
+
return { locale: 'en-US', pluralCategories: ['one', 'other'] };
|
|
126
|
+
}
|
|
127
|
+
static supportedLocalesOf() {
|
|
128
|
+
return ['en-US'];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
class Collator {
|
|
132
|
+
#numeric;
|
|
133
|
+
constructor(_locale, options = {}) {
|
|
134
|
+
this.#numeric = options.numeric === true;
|
|
135
|
+
}
|
|
136
|
+
compare = (a, b) => {
|
|
137
|
+
if (this.#numeric) {
|
|
138
|
+
const na = Number(a);
|
|
139
|
+
const nb = Number(b);
|
|
140
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb))
|
|
141
|
+
return na - nb;
|
|
142
|
+
}
|
|
143
|
+
const x = String(a).toLowerCase();
|
|
144
|
+
const y = String(b).toLowerCase();
|
|
145
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
146
|
+
};
|
|
147
|
+
resolvedOptions() {
|
|
148
|
+
return { locale: 'en-US', usage: 'sort', sensitivity: 'variant' };
|
|
149
|
+
}
|
|
150
|
+
static supportedLocalesOf() {
|
|
151
|
+
return ['en-US'];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
class RelativeTimeFormat {
|
|
155
|
+
format(value, unit) {
|
|
156
|
+
const u = unit.replace(/s$/, '');
|
|
157
|
+
const n = Math.abs(value);
|
|
158
|
+
const label = `${n} ${u}${n === 1 ? '' : 's'}`;
|
|
159
|
+
return value < 0 ? `${label} ago` : `in ${label}`;
|
|
160
|
+
}
|
|
161
|
+
static supportedLocalesOf() {
|
|
162
|
+
return ['en-US'];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
class ListFormat {
|
|
166
|
+
#type;
|
|
167
|
+
constructor(_locale, options = {}) {
|
|
168
|
+
this.#type = options.type === 'disjunction' ? 'or' : 'and';
|
|
169
|
+
}
|
|
170
|
+
format(list) {
|
|
171
|
+
const items = [...list];
|
|
172
|
+
if (items.length < 3)
|
|
173
|
+
return items.join(` ${this.#type} `);
|
|
174
|
+
return `${items.slice(0, -1).join(', ')}, ${this.#type} ${items[items.length - 1]}`;
|
|
175
|
+
}
|
|
176
|
+
static supportedLocalesOf() {
|
|
177
|
+
return ['en-US'];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
class Locale {
|
|
181
|
+
baseName;
|
|
182
|
+
language;
|
|
183
|
+
region;
|
|
184
|
+
constructor(tag = 'en-US') {
|
|
185
|
+
this.baseName = String(tag);
|
|
186
|
+
const [language, region] = this.baseName.split(/[-_]/);
|
|
187
|
+
this.language = language || 'en';
|
|
188
|
+
this.region = region;
|
|
189
|
+
}
|
|
190
|
+
get weekInfo() {
|
|
191
|
+
return this.getWeekInfo();
|
|
192
|
+
}
|
|
193
|
+
getWeekInfo() {
|
|
194
|
+
return { firstDay: 7, weekend: [6, 7], minimalDays: 1 };
|
|
195
|
+
}
|
|
196
|
+
maximize() {
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
minimize() {
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
toString() {
|
|
203
|
+
return this.baseName;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
class Segmenter {
|
|
207
|
+
segment(input) {
|
|
208
|
+
return [...input].map((segment, index) => ({ segment, index, input }));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export const IntlShim = {
|
|
212
|
+
NumberFormat,
|
|
213
|
+
DateTimeFormat,
|
|
214
|
+
PluralRules,
|
|
215
|
+
Collator,
|
|
216
|
+
RelativeTimeFormat,
|
|
217
|
+
ListFormat,
|
|
218
|
+
Locale,
|
|
219
|
+
Segmenter,
|
|
220
|
+
getCanonicalLocales: (l) => (l === undefined ? [] : Array.isArray(l) ? l : [l]),
|
|
221
|
+
supportedValuesOf: () => []
|
|
222
|
+
};
|
package/dist/server/polyfills.js
CHANGED
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
// during development.
|
|
5
5
|
import 'core-js/actual/url';
|
|
6
6
|
import 'core-js/actual/url-search-params';
|
|
7
|
+
import { IntlShim } from './intl.js';
|
|
7
8
|
const g = globalThis;
|
|
9
|
+
// Some libraries read process.env.SOMETHING at runtime. Give them an empty
|
|
10
|
+
// environment instead of a ReferenceError.
|
|
11
|
+
if (typeof g.process === 'undefined') {
|
|
12
|
+
g.process = { env: { NODE_ENV: 'production' }, browser: false, version: '', versions: {} };
|
|
13
|
+
}
|
|
14
|
+
if (typeof g.Intl === 'undefined') {
|
|
15
|
+
g.Intl = IntlShim;
|
|
16
|
+
}
|
|
8
17
|
if (typeof g.queueMicrotask !== 'function') {
|
|
9
18
|
g.queueMicrotask = (fn) => void Promise.resolve().then(fn);
|
|
10
19
|
}
|
package/dist/vite/index.js
CHANGED
|
@@ -30,7 +30,12 @@ export function tern(options = {}) {
|
|
|
30
30
|
root = path.resolve(config.root ?? process.cwd());
|
|
31
31
|
const shared = {
|
|
32
32
|
appType: 'custom',
|
|
33
|
-
resolve: {
|
|
33
|
+
resolve: {
|
|
34
|
+
alias: {
|
|
35
|
+
$lib: path.join(root, 'src/lib'),
|
|
36
|
+
$routes: path.join(root, '.tern/generated/routes.js')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
34
39
|
};
|
|
35
40
|
if (env.isSsrBuild) {
|
|
36
41
|
return {
|
|
@@ -49,7 +54,10 @@ export function tern(options = {}) {
|
|
|
49
54
|
},
|
|
50
55
|
// One self-contained file: the renderer runs in an embedded engine
|
|
51
56
|
// without a module loader or Node built-ins.
|
|
52
|
-
ssr: { noExternal: true }
|
|
57
|
+
ssr: { noExternal: true },
|
|
58
|
+
// Libraries often read process.env.NODE_ENV; the engine has no
|
|
59
|
+
// process object, so replace it at build time.
|
|
60
|
+
define: { 'process.env.NODE_ENV': JSON.stringify('production') }
|
|
53
61
|
};
|
|
54
62
|
}
|
|
55
63
|
const vitePort = Number(process.env.TERN_VITE_PORT) || undefined;
|
|
@@ -71,7 +79,7 @@ export function tern(options = {}) {
|
|
|
71
79
|
port: vitePort,
|
|
72
80
|
strictPort: vitePort !== undefined,
|
|
73
81
|
// The browser talks to the Go app, which proxies HMR to Vite.
|
|
74
|
-
|
|
82
|
+
ws: appPort ? { clientPort: appPort } : undefined,
|
|
75
83
|
fs: { allow: [root] }
|
|
76
84
|
},
|
|
77
85
|
optimizeDeps: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ternkit/tern",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Client runtime and Vite plugin for Tern: Svelte on the client, Go on the server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "svelte-package -i src -o dist",
|
|
53
53
|
"prepare": "svelte-package -i src -o dist",
|
|
54
|
-
"typecheck": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
|
|
54
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
|
|
55
|
+
"test": "vitest run"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"svelte": "^5.46.4",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"svelte": "^5.57.3",
|
|
68
69
|
"svelte-check": "^4.7.8",
|
|
69
70
|
"typescript": "^6.0.0",
|
|
70
|
-
"vite": "^8.3.7"
|
|
71
|
+
"vite": "^8.3.7",
|
|
72
|
+
"vitest": "^5.1.0"
|
|
71
73
|
}
|
|
72
74
|
}
|