@ternkit/tern 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/index.d.ts +18 -0
- package/dist/index.js +15 -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/navigation.d.ts +55 -0
- package/dist/runtime/navigation.js +69 -0
- package/dist/runtime/router.svelte.d.ts +19 -0
- package/dist/runtime/router.svelte.js +506 -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 +101 -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/intl.d.ts +100 -0
- package/dist/server/intl.js +222 -0
- package/dist/server/polyfills.d.ts +2 -0
- package/dist/server/polyfills.js +47 -0
- package/dist/vite/index.d.ts +23 -0
- package/dist/vite/index.js +118 -0
- package/package.json +72 -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,18 @@
|
|
|
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, 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';
|
|
14
|
+
export { enhance } from './runtime/forms.js';
|
|
15
|
+
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
16
|
+
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
17
|
+
/** The version of the Tern client runtime. */
|
|
18
|
+
export declare const VERSION = "0.4.0";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
|
+
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
|
+
export { enhance } from './runtime/forms.js';
|
|
14
|
+
/** The version of the Tern client runtime. */
|
|
15
|
+
export const VERSION = '0.4.0';
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
/**
|
|
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>;
|
|
11
|
+
/** Reruns every load for the current page. */
|
|
12
|
+
export declare function invalidateAll(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Loads the data and code for a URL so a later navigation to it is instant.
|
|
15
|
+
* Tern does this automatically for links; see data-tern-preload.
|
|
16
|
+
*/
|
|
17
|
+
export declare function preloadData(href: string | URL): Promise<Payload | null>;
|
|
18
|
+
/** Applies a form action result to the page, as `use:enhance` does. */
|
|
19
|
+
export declare function applyAction(result: ActionResult): Promise<void>;
|