@ternkit/tern 0.3.1 → 1.0.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 +5 -3
- package/dist/index.js +4 -3
- 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 +142 -16
- package/dist/runtime/state.svelte.d.ts +11 -0
- package/dist/runtime/state.svelte.js +16 -0
- package/dist/runtime/types.d.ts +15 -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 +24 -3
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* </script>
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
-
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
-
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
10
|
+
export { page, navigating, env } from './runtime/state.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 = "1.0.0";
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* </script>
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
-
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
-
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
10
|
+
export { page, navigating, env } from './runtime/state.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 = '1.0.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);
|
|
@@ -73,12 +81,27 @@ export async function goto(href, opts = {}) {
|
|
|
73
81
|
throw new Error('Tern: goto() was called before the app started');
|
|
74
82
|
}
|
|
75
83
|
const url = new URL(href, location.href);
|
|
76
|
-
if (url
|
|
84
|
+
if (!inApp(url)) {
|
|
77
85
|
location.href = url.href;
|
|
78
86
|
return;
|
|
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,34 @@ 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
|
+
// The base path is part of the pathname; the server strips it.
|
|
166
|
+
let target = `${path}/__data.json${url.search}`;
|
|
167
|
+
if (reuse) {
|
|
168
|
+
const here = url.pathname + url.search;
|
|
169
|
+
const entries = [];
|
|
170
|
+
for (const [i, h] of held) {
|
|
171
|
+
if (!h.uses)
|
|
172
|
+
continue; // no load: nothing to reuse
|
|
173
|
+
if (h.uses.url && h.url !== here)
|
|
174
|
+
continue;
|
|
175
|
+
if (h.uses.deps?.some((d) => invalidated.has(d)))
|
|
176
|
+
continue;
|
|
177
|
+
const p = {};
|
|
178
|
+
for (const name of h.uses.params ?? [])
|
|
179
|
+
p[name] = h.params[name] ?? '';
|
|
180
|
+
entries.push({ i, p });
|
|
181
|
+
}
|
|
182
|
+
if (entries.length) {
|
|
183
|
+
target += (url.search ? '&' : '?') + 'x-tern-reuse=' + encodeURIComponent(JSON.stringify(entries));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return target;
|
|
142
187
|
}
|
|
143
|
-
async function fetchPayload(url) {
|
|
188
|
+
async function fetchPayload(url, reuse = true) {
|
|
144
189
|
try {
|
|
145
|
-
const res = await fetch(dataURL(url), {
|
|
190
|
+
const res = await fetch(dataURL(url, reuse), {
|
|
146
191
|
headers: { accept: 'application/json' },
|
|
147
192
|
credentials: 'same-origin'
|
|
148
193
|
});
|
|
@@ -164,18 +209,30 @@ function takePreload(url) {
|
|
|
164
209
|
}
|
|
165
210
|
return undefined;
|
|
166
211
|
}
|
|
167
|
-
async function navigate(url, opts, type, redirects, popIndex) {
|
|
212
|
+
async function navigate(url, opts, type, redirects, popIndex, fromIndex) {
|
|
168
213
|
const id = ++token;
|
|
169
214
|
// A change of hash alone is a scroll, not a navigation.
|
|
170
215
|
if (type !== 'popstate' && !opts.invalidateAll && url.hash && samePage(url)) {
|
|
171
216
|
saveScroll();
|
|
172
|
-
history.pushState({ ternIndex: (historyIndex =
|
|
217
|
+
history.pushState({ ternIndex: (historyIndex = historyIndex + 1), ternState: $state.snapshot(opts.state ?? {}) }, '', url);
|
|
173
218
|
scrollToHash(url.hash);
|
|
174
219
|
return;
|
|
175
220
|
}
|
|
176
221
|
const from = current();
|
|
177
|
-
|
|
178
|
-
const
|
|
222
|
+
const nav = { from, to: { url, params: null, route: { id: null } }, type };
|
|
223
|
+
const reload = opts[RELOAD] === true;
|
|
224
|
+
if (!reload && !runBefore(nav, false)) {
|
|
225
|
+
if (type === 'popstate' && popIndex != null && fromIndex != null) {
|
|
226
|
+
// The browser already moved; move back to where we were.
|
|
227
|
+
ignoreNextPop = true;
|
|
228
|
+
historyIndex = fromIndex;
|
|
229
|
+
history.go(fromIndex - popIndex);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!reload)
|
|
234
|
+
setNavigating(nav);
|
|
235
|
+
const payload = await (opts.invalidateAll ? undefined : takePreload(url)) ?? (await fetchPayload(url, !opts.invalidateAll));
|
|
179
236
|
if (id !== token)
|
|
180
237
|
return;
|
|
181
238
|
if (!payload) {
|
|
@@ -184,15 +241,21 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
184
241
|
}
|
|
185
242
|
if (payload.type === 'redirect' && payload.location) {
|
|
186
243
|
const next = new URL(payload.location, url);
|
|
187
|
-
if (redirects >= MAX_REDIRECTS || next
|
|
244
|
+
if (redirects >= MAX_REDIRECTS || !inApp(next)) {
|
|
188
245
|
location.href = next.href;
|
|
189
246
|
return;
|
|
190
247
|
}
|
|
191
|
-
return navigate(next, { ...opts, replaceState: true }, type, redirects + 1);
|
|
248
|
+
return navigate(next, { ...opts, replaceState: true, [RELOAD]: false }, type, redirects + 1);
|
|
192
249
|
}
|
|
193
250
|
if (type !== 'popstate')
|
|
194
251
|
saveScroll(); // popstate saved before historyIndex changed
|
|
195
252
|
const state = opts.state ?? (type === 'popstate' ? history.state?.ternState ?? {} : {});
|
|
253
|
+
nav.to = { url, params: payload.params ?? {}, route: { id: payload.route ?? null } };
|
|
254
|
+
let finish;
|
|
255
|
+
const complete = new Promise((resolve) => (finish = resolve));
|
|
256
|
+
const cleanups = reload ? [] : await runOn(nav, complete);
|
|
257
|
+
if (id !== token)
|
|
258
|
+
return;
|
|
196
259
|
const ok = await render(payload, url, state, id);
|
|
197
260
|
if (id !== token)
|
|
198
261
|
return;
|
|
@@ -207,7 +270,7 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
207
270
|
history.replaceState(entry, '', url);
|
|
208
271
|
}
|
|
209
272
|
else {
|
|
210
|
-
entry.ternIndex = historyIndex =
|
|
273
|
+
entry.ternIndex = historyIndex = historyIndex + 1;
|
|
211
274
|
history.pushState(entry, '', url);
|
|
212
275
|
}
|
|
213
276
|
}
|
|
@@ -226,6 +289,12 @@ async function navigate(url, opts, type, redirects, popIndex) {
|
|
|
226
289
|
if (!opts.keepFocus) {
|
|
227
290
|
resetFocus();
|
|
228
291
|
}
|
|
292
|
+
invalidated.clear();
|
|
293
|
+
finish();
|
|
294
|
+
if (!reload)
|
|
295
|
+
runAfter(nav);
|
|
296
|
+
for (const fn of cleanups)
|
|
297
|
+
fn();
|
|
229
298
|
}
|
|
230
299
|
function samePage(url) {
|
|
231
300
|
return url.pathname === location.pathname && url.search === location.search;
|
|
@@ -249,6 +318,16 @@ function loadRoute(id) {
|
|
|
249
318
|
}
|
|
250
319
|
/** Renders a payload. It reports false if the payload is not a page. */
|
|
251
320
|
async function render(p, url, state, id) {
|
|
321
|
+
const route = p.route ? manifest.routes[p.route] : undefined;
|
|
322
|
+
const layouts = route ? route.layouts : manifest.fallback.layouts;
|
|
323
|
+
// Fill in the layout data the server skipped because we already have it.
|
|
324
|
+
if (p.reused?.length) {
|
|
325
|
+
p.nodes = [...(p.nodes ?? [])];
|
|
326
|
+
for (const k of p.reused) {
|
|
327
|
+
const li = layouts[k];
|
|
328
|
+
p.nodes[k] = li === undefined ? null : (held.get(li)?.data ?? null);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
252
331
|
const what = plan(manifest, p);
|
|
253
332
|
let components;
|
|
254
333
|
switch (what.kind) {
|
|
@@ -274,6 +353,14 @@ async function render(p, url, state, id) {
|
|
|
274
353
|
nodes = p.nodes ?? [];
|
|
275
354
|
if (id !== token)
|
|
276
355
|
return true; // superseded; the newer navigation renders
|
|
356
|
+
if (p.type === 'data') {
|
|
357
|
+
const here = url.pathname + url.search;
|
|
358
|
+
layouts.forEach((li, k) => {
|
|
359
|
+
if (p.reused?.includes(k))
|
|
360
|
+
return;
|
|
361
|
+
held.set(li, { data: p.nodes?.[k] ?? null, uses: p.uses?.[k] ?? null, params: p.params ?? {}, url: here });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
277
364
|
applyPayloadToPage(p, url, state, what.data);
|
|
278
365
|
props.components = components;
|
|
279
366
|
props.data = what.data;
|
|
@@ -324,12 +411,19 @@ function findAnchor(event) {
|
|
|
324
411
|
}
|
|
325
412
|
return null;
|
|
326
413
|
}
|
|
414
|
+
/** Reports whether url belongs to this app (its origin and base path). */
|
|
415
|
+
function inApp(url) {
|
|
416
|
+
if (url.origin !== location.origin)
|
|
417
|
+
return false;
|
|
418
|
+
const base = manifest.base ?? '';
|
|
419
|
+
return !base || url.pathname === base || url.pathname.startsWith(base + '/');
|
|
420
|
+
}
|
|
327
421
|
function anchorURL(a) {
|
|
328
422
|
const href = a instanceof SVGAElement ? a.href.baseVal : a.href;
|
|
329
423
|
if (!href)
|
|
330
424
|
return null;
|
|
331
425
|
const url = new URL(href, document.baseURI);
|
|
332
|
-
if (url
|
|
426
|
+
if (!inApp(url) || !/^https?:$/.test(url.protocol))
|
|
333
427
|
return null;
|
|
334
428
|
const target = a instanceof SVGAElement ? a.target.baseVal : a.target;
|
|
335
429
|
if (target && target !== '_self')
|
|
@@ -376,13 +470,45 @@ function listen() {
|
|
|
376
470
|
};
|
|
377
471
|
document.addEventListener('mousedown', onTap);
|
|
378
472
|
document.addEventListener('touchstart', onTap, { passive: true });
|
|
473
|
+
// GET forms navigate on the client, like links. POST forms are handled
|
|
474
|
+
// by use:enhance or submit normally.
|
|
475
|
+
document.addEventListener('submit', (event) => {
|
|
476
|
+
if (event.defaultPrevented)
|
|
477
|
+
return;
|
|
478
|
+
const form = event.target;
|
|
479
|
+
const submitter = event.submitter;
|
|
480
|
+
const own = HTMLFormElement.prototype.cloneNode.call(form);
|
|
481
|
+
const method = submitter?.hasAttribute('formmethod') ? submitter.formMethod : own.method;
|
|
482
|
+
const target = submitter?.getAttribute('formtarget') ?? own.target;
|
|
483
|
+
if (method !== 'get' || (target && target !== '_self') || form.closest('[data-tern-reload]:not([data-tern-reload="off"])')) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const url = new URL(submitter?.hasAttribute('formaction') ? submitter.formAction : own.action);
|
|
487
|
+
if (!inApp(url))
|
|
488
|
+
return;
|
|
489
|
+
event.preventDefault();
|
|
490
|
+
url.search = new URLSearchParams(new FormData(form, submitter)).toString();
|
|
491
|
+
const filtering = url.pathname === location.pathname;
|
|
492
|
+
void navigate(url, { keepFocus: filtering, noScroll: filtering }, 'form', 0);
|
|
493
|
+
});
|
|
494
|
+
addEventListener('beforeunload', (event) => {
|
|
495
|
+
if (!runBefore({ from: current(), to: null, type: 'leave' }, true)) {
|
|
496
|
+
event.preventDefault();
|
|
497
|
+
event.returnValue = '';
|
|
498
|
+
}
|
|
499
|
+
});
|
|
379
500
|
addEventListener('popstate', (event) => {
|
|
501
|
+
if (ignoreNextPop) {
|
|
502
|
+
ignoreNextPop = false;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
380
505
|
const index = event.state?.ternIndex;
|
|
381
506
|
if (index == null)
|
|
382
507
|
return;
|
|
383
508
|
saveScroll();
|
|
509
|
+
const fromIndex = historyIndex;
|
|
384
510
|
historyIndex = index;
|
|
385
|
-
void navigate(new URL(location.href), { state: event.state?.ternState ?? {} }, 'popstate', 0, index);
|
|
511
|
+
void navigate(new URL(location.href), { state: event.state?.ternState ?? {} }, 'popstate', 0, index, fromIndex);
|
|
386
512
|
});
|
|
387
513
|
addEventListener('pagehide', saveScroll);
|
|
388
514
|
}
|
|
@@ -46,6 +46,17 @@ export declare const navigating: {
|
|
|
46
46
|
readonly current: Navigation | null;
|
|
47
47
|
};
|
|
48
48
|
export declare function setNavigating(n: Navigation | null): void;
|
|
49
|
+
/**
|
|
50
|
+
* The server's PUBLIC_ environment variables, read when it started:
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* import { env } from '@ternkit/tern';
|
|
54
|
+
* fetch(`${env.PUBLIC_API_URL}/status`);
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Only variables whose names start with PUBLIC_ are sent to the browser.
|
|
58
|
+
*/
|
|
59
|
+
export declare const env: Record<string, string | undefined>;
|
|
49
60
|
/** Updates page state for a rendered payload. */
|
|
50
61
|
export declare function applyPayloadToPage(p: Payload, url: URL, state: Record<string, unknown>, data: Record<string, unknown>[]): void;
|
|
51
62
|
export {};
|
|
@@ -49,8 +49,24 @@ export const navigating = {
|
|
|
49
49
|
export function setNavigating(n) {
|
|
50
50
|
navigatingState.current = n;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The server's PUBLIC_ environment variables, read when it started:
|
|
54
|
+
*
|
|
55
|
+
* ```js
|
|
56
|
+
* import { env } from '@ternkit/tern';
|
|
57
|
+
* fetch(`${env.PUBLIC_API_URL}/status`);
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Only variables whose names start with PUBLIC_ are sent to the browser.
|
|
61
|
+
*/
|
|
62
|
+
export const env = {};
|
|
52
63
|
/** Updates page state for a rendered payload. */
|
|
53
64
|
export function applyPayloadToPage(p, url, state, data) {
|
|
65
|
+
if (p.env) {
|
|
66
|
+
for (const k of Object.keys(env))
|
|
67
|
+
delete env[k];
|
|
68
|
+
Object.assign(env, p.env);
|
|
69
|
+
}
|
|
54
70
|
page.url = url;
|
|
55
71
|
page.params = p.params ?? {};
|
|
56
72
|
page.route = { id: p.route ?? null };
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface RouteEntry {
|
|
|
21
21
|
}
|
|
22
22
|
/** The client manifest generated by `tern gen` in .tern/generated/manifest.js. */
|
|
23
23
|
export interface ClientManifest {
|
|
24
|
+
/** The base path from tern.json, such as '/app', or ''. */
|
|
25
|
+
base?: string;
|
|
24
26
|
layouts: LayoutEntry[];
|
|
25
27
|
routes: Record<string, RouteEntry>;
|
|
26
28
|
fallback: {
|
|
@@ -43,6 +45,18 @@ export interface Payload {
|
|
|
43
45
|
data?: unknown;
|
|
44
46
|
form?: unknown;
|
|
45
47
|
hasForm?: boolean;
|
|
48
|
+
/** What each node's load read, so the client knows when to rerun it. */
|
|
49
|
+
uses?: (NodeUses | null)[];
|
|
50
|
+
/** Nodes the server skipped because the client already has their data. */
|
|
51
|
+
reused?: number[];
|
|
52
|
+
/** PUBLIC_ environment variables, on full page loads. */
|
|
53
|
+
env?: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
export interface NodeUses {
|
|
56
|
+
url?: boolean;
|
|
57
|
+
params?: string[];
|
|
58
|
+
deps?: string[];
|
|
59
|
+
parent?: boolean;
|
|
46
60
|
}
|
|
47
61
|
/** The result of submitting a form action with `use:enhance`. */
|
|
48
62
|
export type ActionResult<Success = Record<string, unknown>, Failure = Record<string, unknown>> = {
|
|
@@ -71,7 +85,7 @@ export interface NavigationTarget {
|
|
|
71
85
|
id: string | null;
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
|
-
export type NavigationType = 'enter' | 'link' | 'goto' | 'popstate' | 'form';
|
|
88
|
+
export type NavigationType = 'enter' | 'link' | 'goto' | 'popstate' | 'form' | 'leave';
|
|
75
89
|
export interface Navigation {
|
|
76
90
|
from: NavigationTarget | null;
|
|
77
91
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
/** Paths shared with the Go code generator; keep in sync with internal/codegen. */
|
|
4
5
|
const CLIENT_ENTRY = '.tern/generated/client.js';
|
|
@@ -28,9 +29,16 @@ export function tern(options = {}) {
|
|
|
28
29
|
name: 'tern',
|
|
29
30
|
config(config, env) {
|
|
30
31
|
root = path.resolve(config.root ?? process.cwd());
|
|
32
|
+
const basePath = readBasePath(root);
|
|
31
33
|
const shared = {
|
|
32
34
|
appType: 'custom',
|
|
33
|
-
|
|
35
|
+
base: basePath + '/',
|
|
36
|
+
resolve: {
|
|
37
|
+
alias: {
|
|
38
|
+
$lib: path.join(root, 'src/lib'),
|
|
39
|
+
$routes: path.join(root, '.tern/generated/routes.js')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
34
42
|
};
|
|
35
43
|
if (env.isSsrBuild) {
|
|
36
44
|
return {
|
|
@@ -49,7 +57,10 @@ export function tern(options = {}) {
|
|
|
49
57
|
},
|
|
50
58
|
// One self-contained file: the renderer runs in an embedded engine
|
|
51
59
|
// without a module loader or Node built-ins.
|
|
52
|
-
ssr: { noExternal: true }
|
|
60
|
+
ssr: { noExternal: true },
|
|
61
|
+
// Libraries often read process.env.NODE_ENV; the engine has no
|
|
62
|
+
// process object, so replace it at build time.
|
|
63
|
+
define: { 'process.env.NODE_ENV': JSON.stringify('production') }
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
66
|
const vitePort = Number(process.env.TERN_VITE_PORT) || undefined;
|
|
@@ -71,7 +82,7 @@ export function tern(options = {}) {
|
|
|
71
82
|
port: vitePort,
|
|
72
83
|
strictPort: vitePort !== undefined,
|
|
73
84
|
// The browser talks to the Go app, which proxies HMR to Vite.
|
|
74
|
-
|
|
85
|
+
ws: appPort ? { clientPort: appPort } : undefined,
|
|
75
86
|
fs: { allow: [root] }
|
|
76
87
|
},
|
|
77
88
|
optimizeDeps: {
|
|
@@ -107,4 +118,14 @@ export function tern(options = {}) {
|
|
|
107
118
|
}
|
|
108
119
|
return [...svelte(options.svelte), plugin];
|
|
109
120
|
}
|
|
121
|
+
/** Reads basePath from tern.json; the Go side validates it. */
|
|
122
|
+
function readBasePath(root) {
|
|
123
|
+
try {
|
|
124
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'tern.json'), 'utf8'));
|
|
125
|
+
return config.basePath ?? '';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
110
131
|
export default tern;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ternkit/tern",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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
|
}
|