bosia 0.2.2 → 0.3.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.
Files changed (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. package/templates/todo/tsconfig.json +20 -20
@@ -1,111 +1,125 @@
1
1
  <script lang="ts">
2
- import { router } from "./router.svelte.ts";
3
- import { findMatch } from "../matcher.ts";
4
- import { clientRoutes } from "bosia:routes";
5
- import { consumePrefetch, prefetchCache, dataUrl } from "./prefetch.ts";
6
-
7
- let {
8
- ssrMode = false,
9
- ssrPageComponent = null,
10
- ssrLayoutComponents = [],
11
- ssrPageData = {},
12
- ssrLayoutData = [],
13
- ssrFormData = null,
14
- }: {
15
- ssrMode?: boolean;
16
- ssrPageComponent?: any;
17
- ssrLayoutComponents?: any[];
18
- ssrPageData?: Record<string, any>;
19
- ssrLayoutData?: Record<string, any>[];
20
- ssrFormData?: any;
21
- } = $props();
22
-
23
- let PageComponent = $state<any>(ssrPageComponent);
24
- let layoutComponents = $state<any[]>(ssrLayoutComponents ?? []);
25
- let pageData = $state<Record<string, any>>(ssrPageData ?? {});
26
- let layoutData = $state<Record<string, any>[]>(ssrLayoutData ?? []);
27
- // Kept separate to avoid a readwrite cycle inside the $effect below
28
- let routeParams = $state<Record<string, string>>(ssrPageData?.params ?? {});
29
- let formData = $state<any>(ssrFormData);
30
- let navigating = $state(false);
31
- let navDone = $state(false);
32
- // Skip bar on the very first effect run (initial hydration data already present)
33
- let firstNav = true;
34
- let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
35
-
36
- $effect(() => {
37
- if (ssrMode) return;
38
-
39
- const path = router.currentRoute;
40
- const pathname = path.split("?")[0].split("#")[0];
41
- const match = findMatch(clientRoutes, pathname);
42
- if (!match) return;
43
-
44
- let cancelled = false;
45
-
46
- const isFirst = firstNav;
47
- firstNav = false;
48
- if (isFirst) return; // Initial hydration — data already in SSR props, no fetch needed
49
-
50
- formData = null;
51
- if (navDoneTimer) { clearTimeout(navDoneTimer); navDoneTimer = null; }
52
- navDone = false;
53
- navigating = true;
54
-
55
- // Load components + data in parallel, then update state atomically
56
- // to avoid a flash of stale/empty data before the fetch completes.
57
- const cached = match.route.hasServerData ? consumePrefetch(path) : null;
58
- prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
59
- const dataFetch = cached
60
- ? Promise.resolve(cached)
61
- : match.route.hasServerData
62
- ? fetch(dataUrl(path)).then(r => r.json()).catch(() => null)
63
- : Promise.resolve(null);
64
-
65
- Promise.all([
66
- match.route.page(),
67
- Promise.all(match.route.layouts.map((l: any) => l())),
68
- dataFetch,
69
- ]).then(([pageMod, layoutMods, result]: [any, any[], any]) => {
70
- if (cancelled) return;
71
- navigating = false;
72
- navDone = true;
73
- navDoneTimer = setTimeout(() => { navDone = false; }, 400);
74
- if (result?.redirect) {
75
- router.navigate(result.redirect);
76
- return;
77
- }
78
- if (result?.error || (result === null && match.route.hasServerData)) {
79
- // Data fetch failed (e.g. static hosting with no server) — full page load
80
- window.location.href = path;
81
- return;
82
- }
83
- PageComponent = pageMod.default;
84
- layoutComponents = layoutMods.map((m: any) => m.default);
85
- pageData = result?.pageData ?? {};
86
- layoutData = result?.layoutData ?? [];
87
- routeParams = result?.pageData?.params ?? match.params;
88
-
89
- // Scroll to top on forward navigation (not on popstate/back-forward)
90
- if (router.isPush) window.scrollTo(0, 0);
91
-
92
- // Update document title and meta description from server metadata
93
- if (result?.metadata) {
94
- if (result.metadata.title) document.title = result.metadata.title;
95
- if (result.metadata.description) {
96
- let meta = document.querySelector('meta[name="description"]') as HTMLMetaElement | null;
97
- if (!meta) {
98
- meta = document.createElement("meta");
99
- meta.name = "description";
100
- document.head.appendChild(meta);
101
- }
102
- meta.content = result.metadata.description;
103
- }
104
- }
105
- });
106
-
107
- return () => { cancelled = true; };
108
- });
2
+ import { router } from "./router.svelte.ts";
3
+ import { findMatch } from "../matcher.ts";
4
+ import { clientRoutes } from "bosia:routes";
5
+ import { consumePrefetch, prefetchCache, dataUrl } from "./prefetch.ts";
6
+ import { appState } from "./appState.svelte.ts";
7
+
8
+ let {
9
+ ssrMode = false,
10
+ ssrPageComponent = null,
11
+ ssrLayoutComponents = [],
12
+ ssrPageData = {},
13
+ ssrLayoutData = [],
14
+ ssrFormData = null,
15
+ }: {
16
+ ssrMode?: boolean;
17
+ ssrPageComponent?: any;
18
+ ssrLayoutComponents?: any[];
19
+ ssrPageData?: Record<string, any>;
20
+ ssrLayoutData?: Record<string, any>[];
21
+ ssrFormData?: any;
22
+ } = $props();
23
+
24
+ let PageComponent = $state<any>(ssrPageComponent);
25
+ let layoutComponents = $state<any[]>(ssrLayoutComponents ?? []);
26
+ // In SSR mode, render directly from props (server module singletons must
27
+ // not hold per-request state). On the client, read/write through `appState`
28
+ // so `use:enhance` and other helpers can update the same cells.
29
+ const pageData = $derived(ssrMode ? (ssrPageData ?? {}) : appState.pageData);
30
+ const layoutData = $derived(ssrMode ? (ssrLayoutData ?? []) : appState.layoutData);
31
+ const routeParams = $derived(ssrMode ? (ssrPageData?.params ?? {}) : appState.routeParams);
32
+ const formData = $derived(ssrMode ? ssrFormData : appState.form);
33
+ let navigating = $state(false);
34
+ let navDone = $state(false);
35
+ // Skip bar on the very first effect run (initial hydration — data already present)
36
+ let firstNav = true;
37
+ let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
38
+
39
+ $effect(() => {
40
+ if (ssrMode) return;
41
+
42
+ const path = router.currentRoute;
43
+ const pathname = path.split("?")[0].split("#")[0];
44
+ const match = findMatch(clientRoutes, pathname);
45
+ if (!match) return;
46
+
47
+ let cancelled = false;
48
+
49
+ const isFirst = firstNav;
50
+ firstNav = false;
51
+ if (isFirst) return; // Initial hydration — data already in SSR props, no fetch needed
52
+
53
+ appState.form = null;
54
+ if (navDoneTimer) {
55
+ clearTimeout(navDoneTimer);
56
+ navDoneTimer = null;
57
+ }
58
+ navDone = false;
59
+ navigating = true;
60
+
61
+ // Load components + data in parallel, then update state atomically
62
+ // to avoid a flash of stale/empty data before the fetch completes.
63
+ const cached = match.route.hasServerData ? consumePrefetch(path) : null;
64
+ prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
65
+ const dataFetch = cached
66
+ ? Promise.resolve(cached)
67
+ : match.route.hasServerData
68
+ ? fetch(dataUrl(path))
69
+ .then((r) => r.json())
70
+ .catch(() => null)
71
+ : Promise.resolve(null);
72
+
73
+ Promise.all([
74
+ match.route.page(),
75
+ Promise.all(match.route.layouts.map((l: any) => l())),
76
+ dataFetch,
77
+ ]).then(([pageMod, layoutMods, result]: [any, any[], any]) => {
78
+ if (cancelled) return;
79
+ navigating = false;
80
+ navDone = true;
81
+ navDoneTimer = setTimeout(() => {
82
+ navDone = false;
83
+ }, 400);
84
+ if (result?.redirect) {
85
+ router.navigate(result.redirect);
86
+ return;
87
+ }
88
+ if (result?.error || (result === null && match.route.hasServerData)) {
89
+ // Data fetch failed (e.g. static hosting with no server) — full page load
90
+ window.location.href = path;
91
+ return;
92
+ }
93
+ PageComponent = pageMod.default;
94
+ layoutComponents = layoutMods.map((m: any) => m.default);
95
+ appState.pageData = result?.pageData ?? {};
96
+ appState.layoutData = result?.layoutData ?? [];
97
+ appState.routeParams = result?.pageData?.params ?? match.params;
98
+
99
+ // Scroll to top on forward navigation (not on popstate/back-forward)
100
+ if (router.isPush) window.scrollTo(0, 0);
101
+
102
+ // Update document title and meta description from server metadata
103
+ if (result?.metadata) {
104
+ if (result.metadata.title) document.title = result.metadata.title;
105
+ if (result.metadata.description) {
106
+ let meta = document.querySelector(
107
+ 'meta[name="description"]',
108
+ ) as HTMLMetaElement | null;
109
+ if (!meta) {
110
+ meta = document.createElement("meta");
111
+ meta.name = "description";
112
+ document.head.appendChild(meta);
113
+ }
114
+ meta.content = result.metadata.description;
115
+ }
116
+ }
117
+ });
118
+
119
+ return () => {
120
+ cancelled = true;
121
+ };
122
+ });
109
123
  </script>
110
124
 
111
125
  <!--
@@ -114,62 +128,72 @@
114
128
  -->
115
129
 
116
130
  {#if navigating}
117
- <div class="bosia-bar loading"></div>
131
+ <div class="bosia-bar loading"></div>
118
132
  {:else if navDone}
119
- <div class="bosia-bar done"></div>
133
+ <div class="bosia-bar done"></div>
120
134
  {/if}
121
135
 
122
136
  {#if layoutComponents.length > 0}
123
- {@render renderLayout(0)}
137
+ {@render renderLayout(0)}
124
138
  {:else if PageComponent}
125
- <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
139
+ <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
126
140
  {:else}
127
- <p>Loading...</p>
141
+ <p>Loading...</p>
128
142
  {/if}
129
143
 
130
144
  {#snippet renderLayout(index: number)}
131
- {@const Layout = layoutComponents[index]}
132
- {@const data = layoutData[index] ?? {}}
133
-
134
- {#if index < layoutComponents.length - 1}
135
- <Layout {data}>
136
- {@render renderLayout(index + 1)}
137
- </Layout>
138
- {:else}
139
- <Layout {data}>
140
- {#if PageComponent}
141
- <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
142
- {:else}
143
- <p>Loading...</p>
144
- {/if}
145
- </Layout>
146
- {/if}
145
+ {@const Layout = layoutComponents[index]}
146
+ {@const data = layoutData[index] ?? {}}
147
+
148
+ {#if index < layoutComponents.length - 1}
149
+ <Layout {data}>
150
+ {@render renderLayout(index + 1)}
151
+ </Layout>
152
+ {:else}
153
+ <Layout {data}>
154
+ {#if PageComponent}
155
+ <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
156
+ {:else}
157
+ <p>Loading...</p>
158
+ {/if}
159
+ </Layout>
160
+ {/if}
147
161
  {/snippet}
148
162
 
149
163
  <style>
150
- .bosia-bar {
151
- position: fixed;
152
- top: 0;
153
- left: 0;
154
- height: 2px;
155
- width: 100%;
156
- background: var(--bosia-loading-color, #f73b27);
157
- z-index: 9999;
158
- pointer-events: none;
159
- transform-origin: left center;
160
- }
161
- .bosia-bar.loading {
162
- animation: bosia-load 8s cubic-bezier(0.02, 0.5, 0.5, 1) forwards;
163
- }
164
- .bosia-bar.done {
165
- animation: bosia-done 0.35s ease forwards;
166
- }
167
- @keyframes bosia-load {
168
- from { transform: scaleX(0); }
169
- to { transform: scaleX(0.85); }
170
- }
171
- @keyframes bosia-done {
172
- from { transform: scaleX(1); opacity: 1; }
173
- to { transform: scaleX(1); opacity: 0; }
174
- }
164
+ .bosia-bar {
165
+ position: fixed;
166
+ top: 0;
167
+ left: 0;
168
+ height: 2px;
169
+ width: 100%;
170
+ background: var(--bosia-loading-color, #f73b27);
171
+ z-index: 9999;
172
+ pointer-events: none;
173
+ transform-origin: left center;
174
+ }
175
+ .bosia-bar.loading {
176
+ animation: bosia-load 8s cubic-bezier(0.02, 0.5, 0.5, 1) forwards;
177
+ }
178
+ .bosia-bar.done {
179
+ animation: bosia-done 0.35s ease forwards;
180
+ }
181
+ @keyframes bosia-load {
182
+ from {
183
+ transform: scaleX(0);
184
+ }
185
+ to {
186
+ transform: scaleX(0.85);
187
+ }
188
+ }
189
+ @keyframes bosia-done {
190
+ from {
191
+ transform: scaleX(1);
192
+ opacity: 1;
193
+ }
194
+ to {
195
+ transform: scaleX(1);
196
+ opacity: 0;
197
+ }
198
+ }
175
199
  </style>
@@ -0,0 +1,57 @@
1
+ // ─── Shared App State ─────────────────────────────────────
2
+ // Singleton holding reactive cells that App.svelte renders from.
3
+ // Lives in a module so client-side helpers (e.g. `use:enhance`)
4
+ // can read and update the same state without going through props.
5
+ //
6
+ // Server-side: never touched. App.svelte's template branches on
7
+ // `ssrMode` and reads from `ssrXxx` props directly during SSR,
8
+ // so concurrent requests don't share these cells.
9
+
10
+ import { dataUrl } from "./prefetch.ts";
11
+ import { router } from "./router.svelte.ts";
12
+
13
+ class AppState {
14
+ pageData = $state<Record<string, any>>({});
15
+ layoutData = $state<Record<string, any>[]>([]);
16
+ routeParams = $state<Record<string, string>>({});
17
+ form = $state<any>(null);
18
+ }
19
+
20
+ export const appState = new AppState();
21
+
22
+ /**
23
+ * Re-fetch loader data for the given path and apply to `appState`.
24
+ * Used by `use:enhance` after a successful action — mirrors SvelteKit's
25
+ * `invalidateAll` default. No-op if the fetch fails or returns an error.
26
+ */
27
+ export async function refreshData(path: string): Promise<void> {
28
+ try {
29
+ const res = await fetch(dataUrl(path));
30
+ if (!res.ok) return;
31
+ const result = await res.json();
32
+ if (result?.redirect) {
33
+ router.navigate(result.redirect);
34
+ return;
35
+ }
36
+ if (result?.error) return;
37
+ appState.pageData = result?.pageData ?? {};
38
+ appState.layoutData = result?.layoutData ?? [];
39
+ appState.routeParams = result?.pageData?.params ?? appState.routeParams;
40
+ if (result?.metadata) {
41
+ if (result.metadata.title) document.title = result.metadata.title;
42
+ if (result.metadata.description) {
43
+ let meta = document.querySelector(
44
+ 'meta[name="description"]',
45
+ ) as HTMLMetaElement | null;
46
+ if (!meta) {
47
+ meta = document.createElement("meta");
48
+ meta.name = "description";
49
+ document.head.appendChild(meta);
50
+ }
51
+ meta.content = result.metadata.description;
52
+ }
53
+ }
54
+ } catch {
55
+ // best-effort — silently swallow
56
+ }
57
+ }
@@ -0,0 +1,112 @@
1
+ // ─── use:enhance ──────────────────────────────────────────
2
+ // Progressive enhancement for `<form method="POST">`. Intercepts
3
+ // submission, POSTs via fetch with `x-bosia-action: 1`, parses the
4
+ // JSON action result, and updates shared form/page state without a
5
+ // full page reload. Falls back to native form submission when JS is
6
+ // disabled because nothing is wired up until this action runs.
7
+
8
+ import { appState, refreshData } from "./appState.svelte.ts";
9
+ import { router } from "./router.svelte.ts";
10
+
11
+ export type ActionResult =
12
+ | { type: "success"; status: number; data: any }
13
+ | { type: "failure"; status: number; data: any }
14
+ | { type: "redirect"; status: number; location: string }
15
+ | { type: "error"; status: number; message: string };
16
+
17
+ export type SubmitFunction = (input: {
18
+ formData: FormData;
19
+ formElement: HTMLFormElement;
20
+ action: URL;
21
+ cancel: () => void;
22
+ submitter: HTMLElement | null;
23
+ }) =>
24
+ | void
25
+ | ((opts: {
26
+ result: ActionResult;
27
+ formElement: HTMLFormElement;
28
+ update: (opts?: { reset?: boolean; invalidateAll?: boolean }) => Promise<void>;
29
+ }) => void | Promise<void>);
30
+
31
+ async function applyResult(
32
+ result: ActionResult,
33
+ form: HTMLFormElement,
34
+ opts: { reset?: boolean; invalidateAll?: boolean } = {},
35
+ ): Promise<void> {
36
+ const reset = opts.reset !== false;
37
+ const invalidateAll = opts.invalidateAll !== false;
38
+
39
+ if (result.type === "redirect") {
40
+ router.navigate(result.location);
41
+ return;
42
+ }
43
+ if (result.type === "error") {
44
+ appState.form = { error: { message: result.message, status: result.status } };
45
+ console.warn(`[bosia] action error ${result.status}: ${result.message}`);
46
+ return;
47
+ }
48
+ if (result.type === "failure") {
49
+ appState.form = result.data;
50
+ return;
51
+ }
52
+ // success
53
+ appState.form = result.data;
54
+ if (reset) form.reset();
55
+ if (invalidateAll) {
56
+ await refreshData(window.location.pathname + window.location.search);
57
+ }
58
+ }
59
+
60
+ export function enhance(form: HTMLFormElement, submit?: SubmitFunction) {
61
+ async function handleSubmit(event: SubmitEvent) {
62
+ event.preventDefault();
63
+
64
+ const submitter = (event.submitter as HTMLElement | null) ?? null;
65
+ const formData = new FormData(form, submitter as HTMLElement | undefined);
66
+
67
+ // Resolve action URL — preserve `?/actionName` if the submitter or form sets it
68
+ const actionAttr =
69
+ (submitter as HTMLButtonElement | HTMLInputElement | null)?.formAction ||
70
+ form.action ||
71
+ window.location.href;
72
+ const action = new URL(actionAttr, window.location.href);
73
+
74
+ let cancelled = false;
75
+ const cancel = () => {
76
+ cancelled = true;
77
+ };
78
+
79
+ const callback = submit?.({ formData, formElement: form, action, cancel, submitter });
80
+ if (cancelled) return;
81
+
82
+ let result: ActionResult;
83
+ try {
84
+ const res = await fetch(action, {
85
+ method: "POST",
86
+ body: formData,
87
+ headers: { "x-bosia-action": "1", accept: "application/json" },
88
+ });
89
+ result = (await res.json()) as ActionResult;
90
+ } catch (err) {
91
+ const message = (err as Error)?.message ?? "Network error";
92
+ result = { type: "error", status: 0, message };
93
+ console.warn("[bosia] enhance: submission failed", err);
94
+ }
95
+
96
+ const update = (opts?: { reset?: boolean; invalidateAll?: boolean }) =>
97
+ applyResult(result, form, opts ?? {});
98
+
99
+ if (typeof callback === "function") {
100
+ await callback({ result, formElement: form, update });
101
+ } else {
102
+ await update();
103
+ }
104
+ }
105
+
106
+ form.addEventListener("submit", handleSubmit);
107
+ return {
108
+ destroy() {
109
+ form.removeEventListener("submit", handleSubmit);
110
+ },
111
+ };
112
+ }