@ternkit/tern 1.14.0 → 1.15.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 +3 -2
- package/dist/index.js +2 -2
- package/dist/runtime/api.d.ts +12 -1
- package/dist/runtime/api.js +130 -31
- package/dist/runtime/builtin.js +34 -0
- package/dist/runtime/dev.d.ts +35 -0
- package/dist/runtime/dev.js +32 -0
- package/dist/runtime/forms.js +3 -0
- package/dist/runtime/router.svelte.js +3 -0
- package/dist/runtime/types.d.ts +6 -0
- package/dist/ui/Admin.svelte +485 -0
- package/dist/ui/Admin.svelte.d.ts +11 -0
- package/dist/ui/Highlight.svelte +23 -0
- package/dist/ui/Highlight.svelte.d.ts +9 -0
- package/dist/ui/SearchBox.svelte +344 -0
- package/dist/ui/SearchBox.svelte.d.ts +25 -0
- package/dist/ui/admin.d.ts +67 -0
- package/dist/ui/admin.js +32 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/search.d.ts +16 -0
- package/dist/ui/search.js +1 -0
- package/dist/vite/goscript.js +3 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export { enhance } from './runtime/forms.js';
|
|
|
15
15
|
export { validate, checkField, type FormField, type FormErrors } from './runtime/validate.js';
|
|
16
16
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
17
17
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
18
|
-
export { ApiError, createApi } from './runtime/api.js';
|
|
18
|
+
export { ApiError, createApi, createStream } from './runtime/api.js';
|
|
19
|
+
export type { ApiStream } from './runtime/api.js';
|
|
19
20
|
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
20
21
|
export type { ConnectionStatus, StreamOptions, SocketOptions } from './runtime/realtime.svelte.js';
|
|
21
22
|
export { default as Image } from './image/Image.svelte';
|
|
@@ -30,4 +31,4 @@ export type { MessageKey } from './runtime/i18n.svelte.js';
|
|
|
30
31
|
export interface Messages {
|
|
31
32
|
}
|
|
32
33
|
/** The version of the Tern client runtime. */
|
|
33
|
-
export declare const VERSION = "1.
|
|
34
|
+
export declare const VERSION = "1.15.0";
|
package/dist/index.js
CHANGED
|
@@ -12,10 +12,10 @@ export { goto, invalidate, invalidateAll, preloadData, applyAction } from './run
|
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export { enhance } from './runtime/forms.js';
|
|
14
14
|
export { validate, checkField } from './runtime/validate.js';
|
|
15
|
-
export { ApiError, createApi } from './runtime/api.js';
|
|
15
|
+
export { ApiError, createApi, createStream } from './runtime/api.js';
|
|
16
16
|
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
17
17
|
export { default as Image } from './image/Image.svelte';
|
|
18
18
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
19
19
|
export { passkeysSupported, registerPasskey, signInWithPasskey, PasskeyError } from './runtime/passkeys.js';
|
|
20
20
|
/** The version of the Tern client runtime. */
|
|
21
|
-
export const VERSION = '1.
|
|
21
|
+
export const VERSION = '1.15.0';
|
package/dist/runtime/api.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ type Options = {
|
|
|
13
13
|
signal?: AbortSignal;
|
|
14
14
|
fetch?: typeof fetch;
|
|
15
15
|
};
|
|
16
|
+
type Route = (pattern: string, params?: Record<string, unknown>) => string;
|
|
16
17
|
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
17
|
-
export declare function createApi(route:
|
|
18
|
+
export declare function createApi(route: Route): (endpoint: string, options?: Options) => Promise<unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* A streaming endpoint's answer: iterate it with for await to get each
|
|
21
|
+
* value as the server sends it.
|
|
22
|
+
*/
|
|
23
|
+
export interface ApiStream<T> extends AsyncIterable<T> {
|
|
24
|
+
/** Stops the stream: the loop ends, and the server's request is cancelled. */
|
|
25
|
+
abort(): void;
|
|
26
|
+
}
|
|
27
|
+
/** Creates $api's stream from $routes' route(); generated code calls it. */
|
|
28
|
+
export declare function createStream(route: Route): (endpoint: string, options?: Options) => ApiStream<unknown>;
|
|
18
29
|
export {};
|
package/dist/runtime/api.js
CHANGED
|
@@ -11,45 +11,144 @@ export class ApiError extends Error {
|
|
|
11
11
|
this.body = body;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
function request(route, endpoint, options, accept) {
|
|
15
|
+
const space = endpoint.indexOf(' ');
|
|
16
|
+
const method = endpoint.slice(0, space);
|
|
17
|
+
let url = route(endpoint.slice(space + 1), options.params ?? {});
|
|
18
|
+
const headers = { Accept: accept };
|
|
19
|
+
const init = { method, headers, credentials: 'same-origin', signal: options.signal };
|
|
20
|
+
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
|
|
21
|
+
const q = new URLSearchParams();
|
|
22
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
23
|
+
if (v === undefined || v === null)
|
|
24
|
+
continue;
|
|
25
|
+
for (const item of Array.isArray(v) ? v : [v])
|
|
26
|
+
q.append(k, item instanceof Date ? item.toISOString() : String(item));
|
|
27
|
+
}
|
|
28
|
+
const qs = q.toString();
|
|
29
|
+
if (qs)
|
|
30
|
+
url += '?' + qs;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
headers['Content-Type'] = 'application/json';
|
|
34
|
+
init.body = JSON.stringify(options.body ?? {});
|
|
35
|
+
}
|
|
36
|
+
return { url, init };
|
|
37
|
+
}
|
|
38
|
+
async function failure(res) {
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
let data = text || null;
|
|
41
|
+
try {
|
|
42
|
+
data = text ? JSON.parse(text) : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// not JSON: keep the text
|
|
46
|
+
}
|
|
47
|
+
const message = data?.message ?? res.statusText;
|
|
48
|
+
return new ApiError(res.status, message, data);
|
|
49
|
+
}
|
|
14
50
|
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
15
51
|
export function createApi(route) {
|
|
16
52
|
return async function api(endpoint, options = {}) {
|
|
17
|
-
const
|
|
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
|
-
}
|
|
53
|
+
const { url, init } = request(route, endpoint, options, 'application/json');
|
|
38
54
|
const res = await (options.fetch ?? fetch)(url, init);
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw await failure(res);
|
|
39
57
|
const text = await res.text();
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
if (!text)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Creates $api's stream from $routes' route(); generated code calls it. */
|
|
69
|
+
export function createStream(route) {
|
|
70
|
+
return function stream(endpoint, options = {}) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason), { once: true });
|
|
73
|
+
let started = false;
|
|
74
|
+
async function* read() {
|
|
75
|
+
const { url, init } = request(route, endpoint, options, 'text/event-stream');
|
|
76
|
+
init.signal = controller.signal;
|
|
77
|
+
let res;
|
|
42
78
|
try {
|
|
43
|
-
|
|
79
|
+
res = await (options.fetch ?? fetch)(url, init);
|
|
44
80
|
}
|
|
45
|
-
catch {
|
|
46
|
-
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (controller.signal.aborted)
|
|
83
|
+
return;
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
throw await failure(res);
|
|
88
|
+
if (!res.body || !res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
89
|
+
throw new ApiError(res.status, `${endpoint} didn’t answer with a stream`, null);
|
|
90
|
+
}
|
|
91
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
92
|
+
let buf = '';
|
|
93
|
+
try {
|
|
94
|
+
for (;;) {
|
|
95
|
+
let chunk;
|
|
96
|
+
try {
|
|
97
|
+
chunk = await reader.read();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (controller.signal.aborted)
|
|
101
|
+
return;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
if (chunk.done) {
|
|
105
|
+
// The body ended without the server's done: it was cut off.
|
|
106
|
+
if (controller.signal.aborted)
|
|
107
|
+
return;
|
|
108
|
+
throw new ApiError(0, 'The stream ended before it was done', null);
|
|
109
|
+
}
|
|
110
|
+
buf += chunk.value.replace(/\r\n/g, '\n');
|
|
111
|
+
let end;
|
|
112
|
+
while ((end = buf.indexOf('\n\n')) >= 0) {
|
|
113
|
+
if (controller.signal.aborted)
|
|
114
|
+
return;
|
|
115
|
+
const raw = buf.slice(0, end);
|
|
116
|
+
buf = buf.slice(end + 2);
|
|
117
|
+
let event = 'message';
|
|
118
|
+
const data = [];
|
|
119
|
+
for (const line of raw.split('\n')) {
|
|
120
|
+
if (line.startsWith('event:'))
|
|
121
|
+
event = line.slice(6).trim();
|
|
122
|
+
else if (line.startsWith('data:'))
|
|
123
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
124
|
+
}
|
|
125
|
+
if (!data.length)
|
|
126
|
+
continue;
|
|
127
|
+
const value = JSON.parse(data.join('\n'));
|
|
128
|
+
if (event === 'done')
|
|
129
|
+
return;
|
|
130
|
+
if (event === 'error') {
|
|
131
|
+
const e = value;
|
|
132
|
+
throw new ApiError(e.status ?? 500, e.message ?? 'The stream failed', value);
|
|
133
|
+
}
|
|
134
|
+
yield value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
reader.releaseLock();
|
|
140
|
+
// Leaving the loop early, with break or an error, stops the request.
|
|
141
|
+
controller.abort();
|
|
47
142
|
}
|
|
48
143
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
144
|
+
return {
|
|
145
|
+
[Symbol.asyncIterator]() {
|
|
146
|
+
if (started)
|
|
147
|
+
throw new Error('A stream can be read only once');
|
|
148
|
+
started = true;
|
|
149
|
+
return read();
|
|
150
|
+
},
|
|
151
|
+
abort: () => controller.abort()
|
|
152
|
+
};
|
|
54
153
|
};
|
|
55
154
|
}
|
package/dist/runtime/builtin.js
CHANGED
|
@@ -33,6 +33,33 @@ export const builtin = {
|
|
|
33
33
|
"tern.accounts.verify.button": "Confirm my email address",
|
|
34
34
|
"tern.accounts.verify.subject": "Confirm your email address",
|
|
35
35
|
"tern.accounts.verify.text": "Hello {name},\n\nConfirm your email address by opening this link:\n{link}\n\nIf you didn’t create an account, you can ignore this email.",
|
|
36
|
+
"tern.admin.all": "All",
|
|
37
|
+
"tern.admin.back": "All {name}",
|
|
38
|
+
"tern.admin.clear": "Clear",
|
|
39
|
+
"tern.admin.count": "{count, plural, one {# record} other {# records}}",
|
|
40
|
+
"tern.admin.create": "Create",
|
|
41
|
+
"tern.admin.delete": "Delete",
|
|
42
|
+
"tern.admin.deleteDescription": "This can’t be undone.",
|
|
43
|
+
"tern.admin.deleteTitle": "Delete this {name}?",
|
|
44
|
+
"tern.admin.done.created": "Created.",
|
|
45
|
+
"tern.admin.done.deleted": "Deleted.",
|
|
46
|
+
"tern.admin.done.saved": "Saved.",
|
|
47
|
+
"tern.admin.edit": "Edit {name}",
|
|
48
|
+
"tern.admin.empty": "There are no {name} yet.",
|
|
49
|
+
"tern.admin.error.referenced": "Other records refer to this {name}, so it can’t be deleted.",
|
|
50
|
+
"tern.admin.error.unique": "There’s already a {name} with one of these values.",
|
|
51
|
+
"tern.admin.home": "Overview",
|
|
52
|
+
"tern.admin.menu": "Sections",
|
|
53
|
+
"tern.admin.new": "New {name}",
|
|
54
|
+
"tern.admin.no": "No",
|
|
55
|
+
"tern.admin.noMatches": "Nothing matches.",
|
|
56
|
+
"tern.admin.passwordKept": "Leave it empty to keep the current one.",
|
|
57
|
+
"tern.admin.save": "Save",
|
|
58
|
+
"tern.admin.search": "Search",
|
|
59
|
+
"tern.admin.searchLabel": "Search {name}",
|
|
60
|
+
"tern.admin.showing": "{from}–{to} of {total}",
|
|
61
|
+
"tern.admin.sortBy": "Sort by {field}",
|
|
62
|
+
"tern.admin.yes": "Yes",
|
|
36
63
|
"tern.billing.limit": "The {plan} plan allows {max} {limit}. Upgrade to add more.",
|
|
37
64
|
"tern.ui.activity.description": "Sign-ins and changes to the account. If something wasn’t you, change your password.",
|
|
38
65
|
"tern.ui.activity.empty": "Nothing yet.",
|
|
@@ -168,6 +195,13 @@ export const builtin = {
|
|
|
168
195
|
"tern.ui.resetPassword.submit": "Save password",
|
|
169
196
|
"tern.ui.resetPassword.title": "Choose a new password",
|
|
170
197
|
"tern.ui.resizable.label": "Resize",
|
|
198
|
+
"tern.ui.search.all": "All results for “{query}”",
|
|
199
|
+
"tern.ui.search.empty": "Nothing matches “{query}”.",
|
|
200
|
+
"tern.ui.search.failed": "Searching didn’t work. Try again.",
|
|
201
|
+
"tern.ui.search.label": "Search",
|
|
202
|
+
"tern.ui.search.placeholder": "Search…",
|
|
203
|
+
"tern.ui.search.results": "{n, plural, one {# result} other {# results}}",
|
|
204
|
+
"tern.ui.search.searching": "Searching…",
|
|
171
205
|
"tern.ui.selectMenu.placeholder": "Choose…",
|
|
172
206
|
"tern.ui.sidebar.label": "Main",
|
|
173
207
|
"tern.ui.sidebar.open": "Open navigation",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Payload } from './types.js';
|
|
2
|
+
/** What the server tells the overlay about a response, in dev only. */
|
|
3
|
+
export type DevInfo = {
|
|
4
|
+
requestId: string;
|
|
5
|
+
components: string[];
|
|
6
|
+
flags?: Record<string, boolean>;
|
|
7
|
+
};
|
|
8
|
+
export type DevEvent = {
|
|
9
|
+
kind: 'page';
|
|
10
|
+
url: string;
|
|
11
|
+
route: string | null;
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
status: number;
|
|
14
|
+
error: string | null;
|
|
15
|
+
/** The node whose load failed, for an error. */
|
|
16
|
+
failed: number | null;
|
|
17
|
+
nodes: unknown[];
|
|
18
|
+
requestId: string;
|
|
19
|
+
components: string[];
|
|
20
|
+
/** The feature flags the request checked, and what each decided. */
|
|
21
|
+
flags: Record<string, boolean>;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'action';
|
|
24
|
+
url: string;
|
|
25
|
+
type: string;
|
|
26
|
+
status: number;
|
|
27
|
+
requestId: string;
|
|
28
|
+
data: unknown;
|
|
29
|
+
};
|
|
30
|
+
export declare function devPage(p: Payload, url: URL): void;
|
|
31
|
+
export declare function devAction(action: URL, result: {
|
|
32
|
+
type: string;
|
|
33
|
+
status?: number;
|
|
34
|
+
data?: unknown;
|
|
35
|
+
}, requestId: string): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Under tern dev, the router and forms tell the dev overlay what each
|
|
2
|
+
// page and action did, with a "tern:dev" event on window. The overlay
|
|
3
|
+
// (tern dev's /__tern/dev/client.js) looks up the request's logs, spans
|
|
4
|
+
// and errors by its ID. Builds leave this out: callers check
|
|
5
|
+
// import.meta.env.DEV first.
|
|
6
|
+
function emit(detail) {
|
|
7
|
+
const w = window;
|
|
8
|
+
// The overlay may load after the first page renders: it reads what it missed here.
|
|
9
|
+
(w.__ternDev ??= []).push(detail);
|
|
10
|
+
if (w.__ternDev.length > 50)
|
|
11
|
+
w.__ternDev.shift();
|
|
12
|
+
dispatchEvent(new CustomEvent('tern:dev', { detail }));
|
|
13
|
+
}
|
|
14
|
+
export function devPage(p, url) {
|
|
15
|
+
const dev = p.dev;
|
|
16
|
+
emit({
|
|
17
|
+
kind: 'page',
|
|
18
|
+
url: url.pathname + url.search,
|
|
19
|
+
route: p.route ?? null,
|
|
20
|
+
params: p.params ?? {},
|
|
21
|
+
status: p.status,
|
|
22
|
+
error: p.error?.message ?? null,
|
|
23
|
+
failed: p.failed ?? null,
|
|
24
|
+
nodes: p.nodes ?? [],
|
|
25
|
+
requestId: dev?.requestId ?? '',
|
|
26
|
+
components: dev?.components ?? [],
|
|
27
|
+
flags: dev?.flags ?? {}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function devAction(action, result, requestId) {
|
|
31
|
+
emit({ kind: 'action', url: action.pathname + action.search, type: result.type, status: result.status ?? 200, requestId, data: result.data ?? null });
|
|
32
|
+
}
|
package/dist/runtime/forms.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyAction, invalidateAll } from './router.svelte.js';
|
|
2
|
+
import { devAction } from './dev.js';
|
|
2
3
|
// Cloning a form without children reads its own action/method/enctype,
|
|
3
4
|
// even if it contains inputs named "action" or "method".
|
|
4
5
|
function attrs(form) {
|
|
@@ -65,6 +66,8 @@ export function enhance(form, submit = () => { }) {
|
|
|
65
66
|
else {
|
|
66
67
|
result = { type: 'error', status: res.status, error: { message: res.statusText || 'Request failed' } };
|
|
67
68
|
}
|
|
69
|
+
if (import.meta.env?.DEV)
|
|
70
|
+
devAction(action, result, res.headers.get('x-request-id') ?? '');
|
|
68
71
|
}
|
|
69
72
|
catch (err) {
|
|
70
73
|
if (controller.signal.aborted)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hydrate, mount, tick } from 'svelte';
|
|
2
|
+
import { devPage } from './dev.js';
|
|
2
3
|
import DefaultError from './DefaultError.svelte';
|
|
3
4
|
import Passthrough from './Passthrough.svelte';
|
|
4
5
|
import Root from './Root.svelte';
|
|
@@ -426,6 +427,8 @@ async function render(p, url, state, id) {
|
|
|
426
427
|
});
|
|
427
428
|
}
|
|
428
429
|
applyPayloadToPage(p, url, state, what.data);
|
|
430
|
+
if (import.meta.env?.DEV)
|
|
431
|
+
devPage(p, url);
|
|
429
432
|
props.components = components;
|
|
430
433
|
props.data = what.data;
|
|
431
434
|
props.form = page.form;
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface Payload {
|
|
|
61
61
|
env?: Record<string, string>;
|
|
62
62
|
/** The request's locale, when the app has src/locales. */
|
|
63
63
|
locale?: string;
|
|
64
|
+
/** Under tern dev: the request's ID and the route's files, for the dev overlay. */
|
|
65
|
+
dev?: {
|
|
66
|
+
requestId: string;
|
|
67
|
+
components: string[];
|
|
68
|
+
flags?: Record<string, boolean>;
|
|
69
|
+
};
|
|
64
70
|
}
|
|
65
71
|
export interface NodeUses {
|
|
66
72
|
url?: boolean;
|