@ternkit/tern 1.0.0 → 1.2.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/goscript/index.svelte.d.ts +44 -0
- package/dist/goscript/index.svelte.js +204 -0
- package/dist/image/Image.svelte +62 -0
- package/dist/image/Image.svelte.d.ts +26 -0
- package/dist/image/types.d.ts +15 -0
- package/dist/image/types.js +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +5 -1
- package/dist/runtime/api.d.ts +18 -0
- package/dist/runtime/api.js +55 -0
- package/dist/runtime/base.d.ts +2 -0
- package/dist/runtime/base.js +10 -0
- package/dist/runtime/format.d.ts +19 -0
- package/dist/runtime/format.js +279 -0
- package/dist/runtime/i18n.svelte.d.ts +50 -0
- package/dist/runtime/i18n.svelte.js +105 -0
- package/dist/runtime/realtime.svelte.d.ts +78 -0
- package/dist/runtime/realtime.svelte.js +163 -0
- package/dist/runtime/router.svelte.js +16 -0
- package/dist/runtime/types.d.ts +6 -0
- package/dist/server/email.d.ts +18 -0
- package/dist/server/email.js +34 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/server/intl.js +1 -1
- package/dist/server/polyfills.d.ts +1 -2
- package/dist/server/polyfills.js +29 -2
- package/dist/server/url.d.ts +56 -0
- package/dist/server/url.js +594 -0
- package/dist/server/web.d.ts +74 -0
- package/dist/server/web.js +395 -0
- package/dist/vite/goscript.d.ts +12 -0
- package/dist/vite/goscript.js +226 -0
- package/dist/vite/image.d.ts +4 -0
- package/dist/vite/image.js +180 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +161 -4
- package/package.json +14 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Instance = {
|
|
2
|
+
call(name: string, args: string): string | null;
|
|
3
|
+
destroy(): void;
|
|
4
|
+
};
|
|
5
|
+
type GoGlobal = {
|
|
6
|
+
mount(id: string, props: string, emit: (name: string, value: string) => void): Instance | Error;
|
|
7
|
+
};
|
|
8
|
+
declare global {
|
|
9
|
+
var __tern_go: GoGlobal | undefined;
|
|
10
|
+
var __tern_go_ready: (() => void) | undefined;
|
|
11
|
+
var Go: (new () => {
|
|
12
|
+
importObject: WebAssembly.Imports;
|
|
13
|
+
run(instance: WebAssembly.Instance): Promise<void>;
|
|
14
|
+
}) | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface GoComponent {
|
|
17
|
+
/** The values Go published with State and Derived. */
|
|
18
|
+
readonly state: Record<string, unknown>;
|
|
19
|
+
/** Whether the Go instance is running; until then the state is the build-time initial state. */
|
|
20
|
+
readonly ready: boolean;
|
|
21
|
+
/** Calls a Func; returns its result, or undefined before the module loads. */
|
|
22
|
+
call(name: string, ...args: unknown[]): unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Mounts a Go component; code generated from <script lang="go"> calls it.
|
|
26
|
+
* initial is the state Setup produced at build time, so server rendering and
|
|
27
|
+
* the first paint match what Go will compute.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useGo(id: string, initial: Record<string, unknown>, runtime: () => Promise<unknown>, wasm: string, props: () => Record<string, unknown>): GoComponent;
|
|
30
|
+
declare global {
|
|
31
|
+
var __tern_live_initial: ((id: string, props: string) => string) | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface LiveComponent extends GoComponent {
|
|
34
|
+
/** The server-rendered state, embedded in the page for hydration. */
|
|
35
|
+
readonly stateScript: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
39
|
+
* calls it. On the server, Setup runs in Go for the render, and the state
|
|
40
|
+
* goes into the page; in the browser, the component connects to
|
|
41
|
+
* /_tern/live, where its Go instance runs.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useLive(id: string, uid: string, props: () => Record<string, unknown>): LiveComponent;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// The browser side of the experimental <script lang="go">: loads the app's
|
|
2
|
+
// Go WebAssembly module once, mounts a Go instance per component, and keeps
|
|
3
|
+
// the values Go publishes in Svelte state. See docs/experimental-go-script.md.
|
|
4
|
+
import { getBase } from '../runtime/base.js';
|
|
5
|
+
let loading = null;
|
|
6
|
+
function load(runtime, wasm) {
|
|
7
|
+
loading ??= (async () => {
|
|
8
|
+
await runtime(); // defines globalThis.Go
|
|
9
|
+
const go = new globalThis.Go();
|
|
10
|
+
const ready = new Promise((resolve) => (globalThis.__tern_go_ready = resolve));
|
|
11
|
+
const res = await WebAssembly.instantiateStreaming(fetch(wasm), go.importObject);
|
|
12
|
+
void go.run(res.instance);
|
|
13
|
+
await ready;
|
|
14
|
+
return globalThis.__tern_go;
|
|
15
|
+
})();
|
|
16
|
+
return loading;
|
|
17
|
+
}
|
|
18
|
+
function toArg(a) {
|
|
19
|
+
if (typeof Event !== 'undefined' && a instanceof Event) {
|
|
20
|
+
const t = a.target;
|
|
21
|
+
return { type: a.type, value: t?.value ?? '', checked: !!t?.checked, key: a.key ?? '' };
|
|
22
|
+
}
|
|
23
|
+
return a;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mounts a Go component; code generated from <script lang="go"> calls it.
|
|
27
|
+
* initial is the state Setup produced at build time, so server rendering and
|
|
28
|
+
* the first paint match what Go will compute.
|
|
29
|
+
*/
|
|
30
|
+
export function useGo(id, initial, runtime, wasm, props) {
|
|
31
|
+
const state = $state({ ...initial });
|
|
32
|
+
let instance = null;
|
|
33
|
+
let ready = $state(false);
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
$effect(() => {
|
|
36
|
+
let alive = true;
|
|
37
|
+
const json = JSON.stringify(props());
|
|
38
|
+
void load(runtime, wasm).then((go) => {
|
|
39
|
+
if (!alive)
|
|
40
|
+
return;
|
|
41
|
+
const m = go.mount(id, json, (name, value) => (state[name] = JSON.parse(value)));
|
|
42
|
+
if (m instanceof Error)
|
|
43
|
+
throw m;
|
|
44
|
+
instance = m;
|
|
45
|
+
ready = true;
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
alive = false;
|
|
49
|
+
instance?.destroy();
|
|
50
|
+
instance = null;
|
|
51
|
+
ready = false;
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
state,
|
|
57
|
+
get ready() {
|
|
58
|
+
return ready;
|
|
59
|
+
},
|
|
60
|
+
call(name, ...args) {
|
|
61
|
+
if (!instance)
|
|
62
|
+
return undefined;
|
|
63
|
+
const out = instance.call(name, JSON.stringify(args.map(toArg)));
|
|
64
|
+
return out === null ? undefined : JSON.parse(out);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
class LiveConnection {
|
|
69
|
+
#ws = null;
|
|
70
|
+
#refs = new Map();
|
|
71
|
+
#queue = [];
|
|
72
|
+
#nextRef = 1;
|
|
73
|
+
#seq = 1;
|
|
74
|
+
#attempt = 0;
|
|
75
|
+
#url;
|
|
76
|
+
constructor(url) {
|
|
77
|
+
this.#url = url;
|
|
78
|
+
}
|
|
79
|
+
#connect() {
|
|
80
|
+
const u = new URL(this.#url, location.href);
|
|
81
|
+
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
82
|
+
const ws = new WebSocket(u);
|
|
83
|
+
this.#ws = ws;
|
|
84
|
+
ws.onopen = () => {
|
|
85
|
+
this.#attempt = 0;
|
|
86
|
+
// After a reconnect, the server has forgotten every instance.
|
|
87
|
+
for (const [ref, r] of this.#refs)
|
|
88
|
+
ws.send(JSON.stringify({ op: 'mount', ref, id: r.id, props: r.props() }));
|
|
89
|
+
for (const m of this.#queue.splice(0))
|
|
90
|
+
ws.send(m);
|
|
91
|
+
};
|
|
92
|
+
ws.onmessage = (e) => {
|
|
93
|
+
const m = JSON.parse(e.data);
|
|
94
|
+
const r = this.#refs.get(m.ref);
|
|
95
|
+
if (!r)
|
|
96
|
+
return;
|
|
97
|
+
if (m.op === 'state' && m.name)
|
|
98
|
+
r.state[m.name] = m.value;
|
|
99
|
+
else if (m.op === 'result') {
|
|
100
|
+
r.pending.get(m.seq)?.(m.error ? Promise.reject(new Error(m.error)) : m.value);
|
|
101
|
+
r.pending.delete(m.seq);
|
|
102
|
+
}
|
|
103
|
+
else if (m.op === 'error')
|
|
104
|
+
console.error(`[tern] live component ${r.id}: ${m.error}`);
|
|
105
|
+
};
|
|
106
|
+
ws.onclose = () => {
|
|
107
|
+
this.#ws = null;
|
|
108
|
+
if (this.#refs.size === 0)
|
|
109
|
+
return;
|
|
110
|
+
const delay = Math.min(10_000, 300 * 2 ** this.#attempt++);
|
|
111
|
+
setTimeout(() => this.#refs.size && this.#connect(), delay);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
#send(m) {
|
|
115
|
+
const data = JSON.stringify(m);
|
|
116
|
+
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
117
|
+
this.#ws.send(data);
|
|
118
|
+
else {
|
|
119
|
+
this.#queue.push(data);
|
|
120
|
+
if (!this.#ws)
|
|
121
|
+
this.#connect();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
mount(r) {
|
|
125
|
+
const ref = this.#nextRef++;
|
|
126
|
+
this.#refs.set(ref, r);
|
|
127
|
+
this.#send({ op: 'mount', ref, id: r.id, props: r.props() });
|
|
128
|
+
return ref;
|
|
129
|
+
}
|
|
130
|
+
unmount(ref) {
|
|
131
|
+
this.#refs.delete(ref);
|
|
132
|
+
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
133
|
+
this.#ws.send(JSON.stringify({ op: 'unmount', ref }));
|
|
134
|
+
if (this.#refs.size === 0) {
|
|
135
|
+
this.#ws?.close(1000);
|
|
136
|
+
this.#ws = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
call(ref, name, args) {
|
|
140
|
+
const r = this.#refs.get(ref);
|
|
141
|
+
if (!r)
|
|
142
|
+
return Promise.resolve(undefined);
|
|
143
|
+
const seq = this.#seq++;
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
r.pending.set(seq, resolve);
|
|
146
|
+
this.#send({ op: 'call', ref, seq, name, args });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let connection = null;
|
|
151
|
+
function escapeJSON(s) {
|
|
152
|
+
return s.replace(/</g, '\\u003c');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
156
|
+
* calls it. On the server, Setup runs in Go for the render, and the state
|
|
157
|
+
* goes into the page; in the browser, the component connects to
|
|
158
|
+
* /_tern/live, where its Go instance runs.
|
|
159
|
+
*/
|
|
160
|
+
export function useLive(id, uid, props) {
|
|
161
|
+
const elementID = `tern-live-${uid}`;
|
|
162
|
+
let initial = {};
|
|
163
|
+
let stateScript = '';
|
|
164
|
+
if (typeof window === 'undefined') {
|
|
165
|
+
const raw = globalThis.__tern_live_initial?.(id, JSON.stringify(props())) ?? '{}';
|
|
166
|
+
initial = JSON.parse(raw);
|
|
167
|
+
stateScript = `<script type="application/json" id="${elementID}">${escapeJSON(raw)}</script>`;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const el = document.getElementById(elementID);
|
|
171
|
+
if (el) {
|
|
172
|
+
initial = JSON.parse(el.textContent || '{}');
|
|
173
|
+
stateScript = el.outerHTML;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const state = $state(initial);
|
|
177
|
+
let ref = 0;
|
|
178
|
+
let ready = $state(false);
|
|
179
|
+
if (typeof window !== 'undefined') {
|
|
180
|
+
$effect(() => {
|
|
181
|
+
connection ??= new LiveConnection(`${getBase()}/_tern/live`);
|
|
182
|
+
const live = { id, props, state, pending: new Map() };
|
|
183
|
+
ref = connection.mount(live);
|
|
184
|
+
ready = true;
|
|
185
|
+
return () => {
|
|
186
|
+
connection?.unmount(ref);
|
|
187
|
+
ref = 0;
|
|
188
|
+
ready = false;
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
state,
|
|
194
|
+
stateScript,
|
|
195
|
+
get ready() {
|
|
196
|
+
return ready;
|
|
197
|
+
},
|
|
198
|
+
call(name, ...args) {
|
|
199
|
+
if (!ref || !connection)
|
|
200
|
+
return undefined;
|
|
201
|
+
return connection.call(ref, name, args.map(toArg));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
|
+
import { getBase } from '../runtime/base.js';
|
|
4
|
+
import type { Picture } from './types.js';
|
|
5
|
+
|
|
6
|
+
interface Props extends Omit<HTMLImgAttributes, 'src' | 'srcset' | 'width' | 'height'> {
|
|
7
|
+
/**
|
|
8
|
+
* The image: from `import picture from './photo.jpg?image'`, or the URL
|
|
9
|
+
* of a remote image, which the images plugin resizes (give its width
|
|
10
|
+
* and height then).
|
|
11
|
+
*/
|
|
12
|
+
src: Picture | string;
|
|
13
|
+
/** Describes the image; use "" for decorative images. */
|
|
14
|
+
alt: string;
|
|
15
|
+
/** The width the image is shown at, in CSS pixels; required for URLs. */
|
|
16
|
+
width?: number;
|
|
17
|
+
/** The height for that width; required for URLs. */
|
|
18
|
+
height?: number;
|
|
19
|
+
/**
|
|
20
|
+
* How wide the image is displayed, so the browser picks the right file,
|
|
21
|
+
* such as "(min-width: 768px) 50vw, 100vw". Defaults to its full width.
|
|
22
|
+
*/
|
|
23
|
+
sizes?: string;
|
|
24
|
+
/** Load at once and with high priority, for the main image of a page. */
|
|
25
|
+
priority?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { src, alt, width, height, sizes, priority = false, ...rest }: Props = $props();
|
|
29
|
+
|
|
30
|
+
// The widths the images plugin serves (plugins/images.Widths).
|
|
31
|
+
const widths = [64, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
32
|
+
|
|
33
|
+
const picture = $derived.by((): Picture => {
|
|
34
|
+
if (typeof src !== 'string') return src;
|
|
35
|
+
const w = width ?? 1200;
|
|
36
|
+
const h = height ?? Math.round(w * 0.75);
|
|
37
|
+
// Up to twice the shown width, for high-density screens.
|
|
38
|
+
const chosen = widths.filter((x) => x <= w * 2);
|
|
39
|
+
if (!chosen.length || chosen.at(-1)! < w * 2) chosen.push(widths.find((x) => x >= w * 2) ?? widths.at(-1)!);
|
|
40
|
+
const url = (x: number) => `${getBase()}/_tern/image?url=${encodeURIComponent(src as string)}&w=${x}`;
|
|
41
|
+
return { src: url(chosen.find((x) => x >= w) ?? chosen.at(-1)!), width: w, height: h, srcset: chosen.map((x) => `${url(x)} ${x}w`).join(', '), sources: [] };
|
|
42
|
+
});
|
|
43
|
+
const fallbackSizes = $derived(`(min-width: ${picture.width}px) ${picture.width}px, 100vw`);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<picture>
|
|
47
|
+
{#each picture.sources as source (source.type)}
|
|
48
|
+
<source type={source.type} srcset={source.srcset} sizes={sizes ?? fallbackSizes} />
|
|
49
|
+
{/each}
|
|
50
|
+
<img
|
|
51
|
+
src={picture.src}
|
|
52
|
+
srcset={picture.srcset}
|
|
53
|
+
sizes={sizes ?? fallbackSizes}
|
|
54
|
+
width={picture.width}
|
|
55
|
+
height={picture.height}
|
|
56
|
+
{alt}
|
|
57
|
+
loading={priority ? 'eager' : 'lazy'}
|
|
58
|
+
fetchpriority={priority ? 'high' : undefined}
|
|
59
|
+
decoding="async"
|
|
60
|
+
{...rest}
|
|
61
|
+
/>
|
|
62
|
+
</picture>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Picture } from './types.js';
|
|
3
|
+
interface Props extends Omit<HTMLImgAttributes, 'src' | 'srcset' | 'width' | 'height'> {
|
|
4
|
+
/**
|
|
5
|
+
* The image: from `import picture from './photo.jpg?image'`, or the URL
|
|
6
|
+
* of a remote image, which the images plugin resizes (give its width
|
|
7
|
+
* and height then).
|
|
8
|
+
*/
|
|
9
|
+
src: Picture | string;
|
|
10
|
+
/** Describes the image; use "" for decorative images. */
|
|
11
|
+
alt: string;
|
|
12
|
+
/** The width the image is shown at, in CSS pixels; required for URLs. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** The height for that width; required for URLs. */
|
|
15
|
+
height?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How wide the image is displayed, so the browser picks the right file,
|
|
18
|
+
* such as "(min-width: 768px) 50vw, 100vw". Defaults to its full width.
|
|
19
|
+
*/
|
|
20
|
+
sizes?: string;
|
|
21
|
+
/** Load at once and with high priority, for the main image of a page. */
|
|
22
|
+
priority?: boolean;
|
|
23
|
+
}
|
|
24
|
+
declare const Image: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type Image = ReturnType<typeof Image>;
|
|
26
|
+
export default Image;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A responsive image from an `import x from './x.jpg?image'`. */
|
|
2
|
+
export interface Picture {
|
|
3
|
+
/** The largest version in the image's own format, for browsers without srcset. */
|
|
4
|
+
src: string;
|
|
5
|
+
/** The largest version's size, so the browser reserves space before it loads. */
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
/** Every width in the image's own format. */
|
|
9
|
+
srcset: string;
|
|
10
|
+
/** AVIF and WebP versions, best first. */
|
|
11
|
+
sources: {
|
|
12
|
+
type: string;
|
|
13
|
+
srcset: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -14,5 +14,18 @@ export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.
|
|
|
14
14
|
export { enhance } from './runtime/forms.js';
|
|
15
15
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
16
16
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
17
|
+
export { ApiError, createApi } from './runtime/api.js';
|
|
18
|
+
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
19
|
+
export type { ConnectionStatus, StreamOptions, SocketOptions } from './runtime/realtime.svelte.js';
|
|
20
|
+
export { default as Image } from './image/Image.svelte';
|
|
21
|
+
export type { Picture } from './image/types.js';
|
|
22
|
+
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
23
|
+
export type { MessageKey } from './runtime/i18n.svelte.js';
|
|
24
|
+
/**
|
|
25
|
+
* The app's message keys. `tern gen` fills this in from src/locales, so
|
|
26
|
+
* t() only accepts keys that exist.
|
|
27
|
+
*/
|
|
28
|
+
export interface Messages {
|
|
29
|
+
}
|
|
17
30
|
/** The version of the Tern client runtime. */
|
|
18
|
-
export declare const VERSION = "1.
|
|
31
|
+
export declare const VERSION = "1.2.0";
|
package/dist/index.js
CHANGED
|
@@ -11,5 +11,9 @@ export { page, navigating, env } from './runtime/state.svelte.js';
|
|
|
11
11
|
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export { enhance } from './runtime/forms.js';
|
|
14
|
+
export { ApiError, createApi } from './runtime/api.js';
|
|
15
|
+
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
16
|
+
export { default as Image } from './image/Image.svelte';
|
|
17
|
+
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
14
18
|
/** The version of the Tern client runtime. */
|
|
15
|
-
export const VERSION = '1.
|
|
19
|
+
export const VERSION = '1.2.0';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** An error answer from a typed endpoint. */
|
|
2
|
+
export declare class ApiError extends Error {
|
|
3
|
+
/** The HTTP status. */
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** The decoded response body, if it was JSON. */
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
constructor(status: number, message: string, body: unknown);
|
|
8
|
+
}
|
|
9
|
+
type Options = {
|
|
10
|
+
params?: Record<string, unknown>;
|
|
11
|
+
query?: Record<string, unknown>;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
};
|
|
16
|
+
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
17
|
+
export declare function createApi(route: (pattern: string, params?: Record<string, unknown>) => string): (endpoint: string, options?: Options) => Promise<unknown>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** An error answer from a typed endpoint. */
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
/** The HTTP status. */
|
|
4
|
+
status;
|
|
5
|
+
/** The decoded response body, if it was JSON. */
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, message, body) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
15
|
+
export function createApi(route) {
|
|
16
|
+
return async function api(endpoint, options = {}) {
|
|
17
|
+
const space = endpoint.indexOf(' ');
|
|
18
|
+
const method = endpoint.slice(0, space);
|
|
19
|
+
let url = route(endpoint.slice(space + 1), options.params ?? {});
|
|
20
|
+
const headers = { Accept: 'application/json' };
|
|
21
|
+
const init = { method, headers, credentials: 'same-origin', signal: options.signal };
|
|
22
|
+
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
|
|
23
|
+
const q = new URLSearchParams();
|
|
24
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
continue;
|
|
27
|
+
for (const item of Array.isArray(v) ? v : [v])
|
|
28
|
+
q.append(k, item instanceof Date ? item.toISOString() : String(item));
|
|
29
|
+
}
|
|
30
|
+
const qs = q.toString();
|
|
31
|
+
if (qs)
|
|
32
|
+
url += '?' + qs;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
headers['Content-Type'] = 'application/json';
|
|
36
|
+
init.body = JSON.stringify(options.body ?? {});
|
|
37
|
+
}
|
|
38
|
+
const res = await (options.fetch ?? fetch)(url, init);
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
let data = null;
|
|
41
|
+
if (text) {
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
data = text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const message = data?.message ?? res.statusText;
|
|
51
|
+
throw new ApiError(res.status, message, data);
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The app's base path, for components that build URLs outside $routes
|
|
2
|
+
// (such as <Image>). The router sets it in the browser and the renderer on
|
|
3
|
+
// the server, so both render the same URLs.
|
|
4
|
+
let base = '';
|
|
5
|
+
export function setBase(b) {
|
|
6
|
+
base = b;
|
|
7
|
+
}
|
|
8
|
+
export function getBase() {
|
|
9
|
+
return base;
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Node = {
|
|
2
|
+
type: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'hash';
|
|
6
|
+
} | {
|
|
7
|
+
type: 'arg';
|
|
8
|
+
name: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'plural' | 'select';
|
|
11
|
+
name: string;
|
|
12
|
+
options: Record<string, Node[]>;
|
|
13
|
+
};
|
|
14
|
+
/** Formats one message in the given locale. */
|
|
15
|
+
export declare function format(locale: string, message: string, params?: Record<string, unknown>): string;
|
|
16
|
+
export declare function parse(message: string): Node[];
|
|
17
|
+
/** The CLDR cardinal plural category of n in locale (see the Go version). */
|
|
18
|
+
export declare function pluralCategory(locale: string, n: number): string;
|
|
19
|
+
export {};
|