@ternkit/tern 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/index.d.ts +16 -0
- package/dist/index.js +14 -0
- package/dist/runtime/DefaultError.svelte +8 -0
- package/dist/runtime/DefaultError.svelte.d.ts +18 -0
- package/dist/runtime/Passthrough.svelte +8 -0
- package/dist/runtime/Passthrough.svelte.d.ts +8 -0
- package/dist/runtime/Root.svelte +28 -0
- package/dist/runtime/Root.svelte.d.ts +11 -0
- package/dist/runtime/client.d.ts +1 -0
- package/dist/runtime/client.js +2 -0
- package/dist/runtime/deferred.d.ts +29 -0
- package/dist/runtime/deferred.js +111 -0
- package/dist/runtime/forms.d.ts +39 -0
- package/dist/runtime/forms.js +91 -0
- package/dist/runtime/router.svelte.d.ts +14 -0
- package/dist/runtime/router.svelte.js +388 -0
- package/dist/runtime/select.d.ts +38 -0
- package/dist/runtime/select.js +45 -0
- package/dist/runtime/state.svelte.d.ts +51 -0
- package/dist/runtime/state.svelte.js +62 -0
- package/dist/runtime/types.d.ts +91 -0
- package/dist/runtime/types.js +1 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.js +40 -0
- package/dist/server/polyfills.d.ts +2 -0
- package/dist/server/polyfills.js +38 -0
- package/dist/vite/index.d.ts +23 -0
- package/dist/vite/index.js +110 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Tern Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ternkit/tern
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The client runtime and Vite plugin for [Tern](https://github.com/Unfield/Tern), a full-stack framework with Svelte 5 on the client and Go on the server.
|
|
4
|
+
|
|
5
|
+
You normally get this package from a new project rather than installing it by hand:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
go run github.com/Unfield/Tern/cmd/tern@latest new my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it provides
|
|
12
|
+
|
|
13
|
+
**`@ternkit/tern`**: the API you use in components.
|
|
14
|
+
|
|
15
|
+
```svelte
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { page, navigating, goto, enhance } from '@ternkit/tern';
|
|
18
|
+
</script>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Export | Purpose |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `page` | reactive state for the current page: `url`, `params`, `route`, `status`, `error`, `data`, `form` |
|
|
24
|
+
| `navigating` | the navigation in progress, or `null` |
|
|
25
|
+
| `goto`, `invalidateAll`, `preloadData` | client-side navigation and data |
|
|
26
|
+
| `enhance`, `applyAction` | progressive enhancement for forms that post to Go actions |
|
|
27
|
+
|
|
28
|
+
**`@ternkit/tern/vite`**: the Vite plugin. It includes the Svelte plugin, so it is the only plugin a Tern app needs.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// vite.config.js
|
|
32
|
+
import { tern } from '@ternkit/tern/vite';
|
|
33
|
+
import { defineConfig } from 'vite';
|
|
34
|
+
|
|
35
|
+
export default defineConfig({ plugins: [tern()] });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Svelte 5.46 or newer
|
|
41
|
+
- Vite 8
|
|
42
|
+
- The matching version of the Tern Go module and CLI
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
See the [Tern guide](https://github.com/Unfield/Tern/blob/main/docs/guide.md).
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client API for Tern apps.
|
|
3
|
+
*
|
|
4
|
+
* ```svelte
|
|
5
|
+
* <script>
|
|
6
|
+
* import { page, goto, enhance } from '@ternkit/tern';
|
|
7
|
+
* </script>
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
+
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
|
+
export { enhance } from './runtime/forms.js';
|
|
13
|
+
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
14
|
+
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
15
|
+
/** The version of the Tern client runtime. */
|
|
16
|
+
export declare const VERSION = "0.3.1";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client API for Tern apps.
|
|
3
|
+
*
|
|
4
|
+
* ```svelte
|
|
5
|
+
* <script>
|
|
6
|
+
* import { page, goto, enhance } from '@ternkit/tern';
|
|
7
|
+
* </script>
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { page, navigating } from './runtime/state.svelte.js';
|
|
11
|
+
export { goto, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
|
+
export { enhance } from './runtime/forms.js';
|
|
13
|
+
/** The version of the Tern client runtime. */
|
|
14
|
+
export const VERSION = '0.3.1';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const DefaultError: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type DefaultError = InstanceType<typeof DefaultError>;
|
|
18
|
+
export default DefaultError;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children?: Snippet;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
};
|
|
6
|
+
declare const Passthrough: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Passthrough = ReturnType<typeof Passthrough>;
|
|
8
|
+
export default Passthrough;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Layout components, outermost first, then the page or error component. */
|
|
6
|
+
components: Component<any>[];
|
|
7
|
+
/** Data for each component, already merged with its parents' data. */
|
|
8
|
+
data: Record<string, unknown>[];
|
|
9
|
+
form: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { components, data, form }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#snippet level(i: number)}
|
|
16
|
+
{@const Current = components[i]}
|
|
17
|
+
{#if Current}
|
|
18
|
+
{#if i === components.length - 1}
|
|
19
|
+
<Current data={data[i]} {form} />
|
|
20
|
+
{:else}
|
|
21
|
+
<Current data={data[i]}>
|
|
22
|
+
{@render level(i + 1)}
|
|
23
|
+
</Current>
|
|
24
|
+
{/if}
|
|
25
|
+
{/if}
|
|
26
|
+
{/snippet}
|
|
27
|
+
|
|
28
|
+
{@render level(0)}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Layout components, outermost first, then the page or error component. */
|
|
4
|
+
components: Component<any>[];
|
|
5
|
+
/** Data for each component, already merged with its parents' data. */
|
|
6
|
+
data: Record<string, unknown>[];
|
|
7
|
+
form: unknown;
|
|
8
|
+
}
|
|
9
|
+
declare const Root: Component<Props, {}, "">;
|
|
10
|
+
type Root = ReturnType<typeof Root>;
|
|
11
|
+
export default Root;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { start } from './router.svelte.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A resolved or failed deferred value, as streamed by the server. */
|
|
2
|
+
export interface Chunk {
|
|
3
|
+
id: number;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
error?: {
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Connects the deferred placeholders of one response to the chunks that
|
|
11
|
+
* resolve them. IDs are only unique within a response.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DeferredScope {
|
|
14
|
+
#private;
|
|
15
|
+
promise(id: number): Promise<unknown>;
|
|
16
|
+
settle(chunk: Chunk): void;
|
|
17
|
+
/** Rejects every value still pending, e.g. when the stream ends early. */
|
|
18
|
+
close(reason: string): void;
|
|
19
|
+
}
|
|
20
|
+
/** Replaces deferred placeholders in data with promises from scope. */
|
|
21
|
+
export declare function revive<T>(value: T, promise: (id: number) => Promise<unknown>): T;
|
|
22
|
+
/**
|
|
23
|
+
* Reads a response that holds one JSON payload line, optionally followed
|
|
24
|
+
* by one line per resolved deferred value. Resolves once the payload is
|
|
25
|
+
* read; deferred values keep streaming in the background.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readStreamed<P extends {
|
|
28
|
+
nodes?: unknown;
|
|
29
|
+
}>(res: Response): Promise<P>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connects the deferred placeholders of one response to the chunks that
|
|
3
|
+
* resolve them. IDs are only unique within a response.
|
|
4
|
+
*/
|
|
5
|
+
export class DeferredScope {
|
|
6
|
+
#pending = new Map();
|
|
7
|
+
#early = new Map();
|
|
8
|
+
promise(id) {
|
|
9
|
+
const early = this.#early.get(id);
|
|
10
|
+
if (early) {
|
|
11
|
+
this.#early.delete(id);
|
|
12
|
+
return early.error ? Promise.reject(new Error(early.error.message)) : Promise.resolve(early.data);
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => this.#pending.set(id, { resolve, reject }));
|
|
15
|
+
}
|
|
16
|
+
settle(chunk) {
|
|
17
|
+
const p = this.#pending.get(chunk.id);
|
|
18
|
+
if (!p) {
|
|
19
|
+
this.#early.set(chunk.id, chunk);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.#pending.delete(chunk.id);
|
|
23
|
+
if (chunk.error)
|
|
24
|
+
p.reject(new Error(chunk.error.message));
|
|
25
|
+
else
|
|
26
|
+
p.resolve(chunk.data);
|
|
27
|
+
}
|
|
28
|
+
/** Rejects every value still pending, e.g. when the stream ends early. */
|
|
29
|
+
close(reason) {
|
|
30
|
+
for (const p of this.#pending.values())
|
|
31
|
+
p.reject(new Error(reason));
|
|
32
|
+
this.#pending.clear();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Replaces deferred placeholders in data with promises from scope. */
|
|
36
|
+
export function revive(value, promise) {
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map((v) => revive(v, promise));
|
|
39
|
+
}
|
|
40
|
+
if (value && typeof value === 'object') {
|
|
41
|
+
const obj = value;
|
|
42
|
+
const keys = Object.keys(obj);
|
|
43
|
+
if (keys.length === 1 && keys[0] === '__tern_deferred' && typeof obj.__tern_deferred === 'number') {
|
|
44
|
+
return promise(obj.__tern_deferred);
|
|
45
|
+
}
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const k of keys)
|
|
48
|
+
out[k] = revive(obj[k], promise);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads a response that holds one JSON payload line, optionally followed
|
|
55
|
+
* by one line per resolved deferred value. Resolves once the payload is
|
|
56
|
+
* read; deferred values keep streaming in the background.
|
|
57
|
+
*/
|
|
58
|
+
export async function readStreamed(res) {
|
|
59
|
+
if (!res.body) {
|
|
60
|
+
const [first, ...rest] = (await res.text()).split('\n');
|
|
61
|
+
const scope = new DeferredScope();
|
|
62
|
+
const payload = JSON.parse(first);
|
|
63
|
+
payload.nodes = revive(payload.nodes, (id) => scope.promise(id));
|
|
64
|
+
for (const line of rest)
|
|
65
|
+
if (line.trim())
|
|
66
|
+
scope.settle(JSON.parse(line));
|
|
67
|
+
scope.close('The server did not send this value');
|
|
68
|
+
return payload;
|
|
69
|
+
}
|
|
70
|
+
const reader = res.body.getReader();
|
|
71
|
+
const decoder = new TextDecoder();
|
|
72
|
+
let buffer = '';
|
|
73
|
+
let done = false;
|
|
74
|
+
while (!buffer.includes('\n') && !done) {
|
|
75
|
+
const r = await reader.read();
|
|
76
|
+
done = r.done;
|
|
77
|
+
if (r.value)
|
|
78
|
+
buffer += decoder.decode(r.value, { stream: true });
|
|
79
|
+
}
|
|
80
|
+
const newline = buffer.indexOf('\n');
|
|
81
|
+
const first = newline < 0 ? buffer : buffer.slice(0, newline);
|
|
82
|
+
buffer = newline < 0 ? '' : buffer.slice(newline + 1);
|
|
83
|
+
const scope = new DeferredScope();
|
|
84
|
+
const payload = JSON.parse(first);
|
|
85
|
+
payload.nodes = revive(payload.nodes, (id) => scope.promise(id));
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
for (;;) {
|
|
89
|
+
let i;
|
|
90
|
+
while ((i = buffer.indexOf('\n')) >= 0) {
|
|
91
|
+
const line = buffer.slice(0, i);
|
|
92
|
+
buffer = buffer.slice(i + 1);
|
|
93
|
+
if (line.trim())
|
|
94
|
+
scope.settle(JSON.parse(line));
|
|
95
|
+
}
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
const r = await reader.read();
|
|
99
|
+
done = r.done;
|
|
100
|
+
if (r.value)
|
|
101
|
+
buffer += decoder.decode(r.value, { stream: true });
|
|
102
|
+
if (done && buffer.trim())
|
|
103
|
+
buffer += '\n';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
scope.close('The connection closed before this value arrived');
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
return payload;
|
|
111
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ActionReturn } from 'svelte/action';
|
|
2
|
+
import type { ActionResult } from './types.js';
|
|
3
|
+
export interface SubmitInput {
|
|
4
|
+
action: URL;
|
|
5
|
+
formData: FormData;
|
|
6
|
+
formElement: HTMLFormElement;
|
|
7
|
+
controller: AbortController;
|
|
8
|
+
submitter: HTMLElement | null;
|
|
9
|
+
/** Stops the submission. */
|
|
10
|
+
cancel(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface SubmitResult<Success, Failure> {
|
|
13
|
+
action: URL;
|
|
14
|
+
formData: FormData;
|
|
15
|
+
formElement: HTMLFormElement;
|
|
16
|
+
result: ActionResult<Success, Failure>;
|
|
17
|
+
/**
|
|
18
|
+
* Does what `use:enhance` does by default: reset the form and rerun
|
|
19
|
+
* loads after a success, then apply the result to the page.
|
|
20
|
+
*/
|
|
21
|
+
update(opts?: {
|
|
22
|
+
reset?: boolean;
|
|
23
|
+
invalidateAll?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Runs before a form is submitted. Return a function to handle the
|
|
28
|
+
* result yourself; call `update()` in it to keep the default behavior.
|
|
29
|
+
*/
|
|
30
|
+
export type SubmitFunction<Success = Record<string, unknown>, Failure = Record<string, unknown>> = (input: SubmitInput) => void | ((result: SubmitResult<Success, Failure>) => void | Promise<void>) | Promise<void | ((result: SubmitResult<Success, Failure>) => void | Promise<void>)>;
|
|
31
|
+
/**
|
|
32
|
+
* Progressively enhances a form that posts to a page action. Without
|
|
33
|
+
* JavaScript the form still works; with it, the page updates in place.
|
|
34
|
+
*
|
|
35
|
+
* ```svelte
|
|
36
|
+
* <form method="POST" action="?/create" use:enhance>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function enhance<Success = Record<string, unknown>, Failure = Record<string, unknown>>(form: HTMLFormElement, submit?: SubmitFunction<Success, Failure>): ActionReturn<SubmitFunction<Success, Failure> | undefined>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { applyAction, invalidateAll } from './router.svelte.js';
|
|
2
|
+
// Cloning a form without children reads its own action/method/enctype,
|
|
3
|
+
// even if it contains inputs named "action" or "method".
|
|
4
|
+
function attrs(form) {
|
|
5
|
+
return HTMLFormElement.prototype.cloneNode.call(form);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Progressively enhances a form that posts to a page action. Without
|
|
9
|
+
* JavaScript the form still works; with it, the page updates in place.
|
|
10
|
+
*
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <form method="POST" action="?/create" use:enhance>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function enhance(form, submit = () => { }) {
|
|
16
|
+
async function defaultUpdate(action, formElement, result, { reset = true, invalidateAll: invalidate = true } = {}) {
|
|
17
|
+
if (result.type === 'success') {
|
|
18
|
+
if (reset)
|
|
19
|
+
HTMLFormElement.prototype.reset.call(formElement);
|
|
20
|
+
if (invalidate)
|
|
21
|
+
await invalidateAll();
|
|
22
|
+
}
|
|
23
|
+
const samePage = location.origin + location.pathname === action.origin + action.pathname;
|
|
24
|
+
if (samePage || result.type === 'redirect' || result.type === 'error') {
|
|
25
|
+
await applyAction(result);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function onSubmit(event) {
|
|
29
|
+
const submitter = event.submitter;
|
|
30
|
+
const own = attrs(form);
|
|
31
|
+
const method = submitter?.hasAttribute('formmethod') ? submitter.formMethod : own.method;
|
|
32
|
+
if (method !== 'post')
|
|
33
|
+
return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
const action = new URL(submitter?.hasAttribute('formaction') ? submitter.formAction : own.action);
|
|
36
|
+
const enctype = submitter?.hasAttribute('formenctype') ? submitter.formEnctype : own.enctype;
|
|
37
|
+
const formData = new FormData(form, submitter);
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
const callback = await submit({
|
|
41
|
+
action,
|
|
42
|
+
formData,
|
|
43
|
+
formElement: form,
|
|
44
|
+
controller,
|
|
45
|
+
submitter,
|
|
46
|
+
cancel: () => (cancelled = true)
|
|
47
|
+
});
|
|
48
|
+
if (cancelled)
|
|
49
|
+
return;
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(action, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { accept: 'application/json', 'x-tern-action': 'true' },
|
|
55
|
+
body: enctype === 'multipart/form-data'
|
|
56
|
+
? formData
|
|
57
|
+
: new URLSearchParams(formData),
|
|
58
|
+
cache: 'no-store',
|
|
59
|
+
credentials: 'same-origin',
|
|
60
|
+
signal: controller.signal
|
|
61
|
+
});
|
|
62
|
+
if (res.headers.get('content-type')?.includes('application/json')) {
|
|
63
|
+
result = await res.json();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
result = { type: 'error', status: res.status, error: { message: res.statusText || 'Request failed' } };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (controller.signal.aborted)
|
|
71
|
+
return;
|
|
72
|
+
result = { type: 'error', status: 500, error: { message: err instanceof Error ? err.message : String(err) } };
|
|
73
|
+
}
|
|
74
|
+
const update = (opts) => defaultUpdate(action, form, result, opts);
|
|
75
|
+
if (callback) {
|
|
76
|
+
await callback({ action, formData, formElement: form, result, update });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
await update();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
form.addEventListener('submit', onSubmit);
|
|
83
|
+
return {
|
|
84
|
+
update(next) {
|
|
85
|
+
submit = next ?? (() => { });
|
|
86
|
+
},
|
|
87
|
+
destroy() {
|
|
88
|
+
form.removeEventListener('submit', onSubmit);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActionResult, ClientManifest, GotoOptions, Payload } from './types.js';
|
|
2
|
+
/** Starts the client. Called by the generated entry module. */
|
|
3
|
+
export declare function start(m: ClientManifest): Promise<void>;
|
|
4
|
+
/** Navigates to href, which may be relative to the current URL. */
|
|
5
|
+
export declare function goto(href: string | URL, opts?: GotoOptions): Promise<void>;
|
|
6
|
+
/** Reruns every load for the current page. */
|
|
7
|
+
export declare function invalidateAll(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Loads the data and code for a URL so a later navigation to it is instant.
|
|
10
|
+
* Tern does this automatically for links; see data-tern-preload.
|
|
11
|
+
*/
|
|
12
|
+
export declare function preloadData(href: string | URL): Promise<Payload | null>;
|
|
13
|
+
/** Applies a form action result to the page, as `use:enhance` does. */
|
|
14
|
+
export declare function applyAction(result: ActionResult): Promise<void>;
|