ajo-kit 0.1.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 +15 -0
- package/README.md +468 -0
- package/dist/bin/kit.js +109 -0
- package/dist/chunks/constants-5Eu0naK3.js +168 -0
- package/dist/chunks/discover-D-b8Pqm8.js +34 -0
- package/dist/chunks/headers-BDszzwLw.js +23 -0
- package/dist/chunks/migrate-C9Iytqx4.js +89 -0
- package/dist/chunks/ssr-CCxCGcm6.js +421 -0
- package/dist/client.js +134 -0
- package/dist/database.js +38 -0
- package/dist/index.js +2 -0
- package/dist/mail.js +15 -0
- package/dist/node.js +104 -0
- package/dist/server.js +502 -0
- package/dist/validate.js +15 -0
- package/dist/vite.js +110 -0
- package/package.json +106 -0
- package/src/app.tsx +484 -0
- package/src/cache.ts +85 -0
- package/src/client.tsx +167 -0
- package/src/constants.ts +339 -0
- package/src/database.ts +55 -0
- package/src/discover.ts +42 -0
- package/src/form.ts +39 -0
- package/src/freshness.ts +63 -0
- package/src/head.tsx +125 -0
- package/src/headers.ts +32 -0
- package/src/index.ts +31 -0
- package/src/mail/index.ts +25 -0
- package/src/migrate.ts +132 -0
- package/src/node.ts +134 -0
- package/src/server.tsx +584 -0
- package/src/ssr.ts +8 -0
- package/src/timing.ts +60 -0
- package/src/validate.ts +33 -0
- package/src/virtual.d.ts +10 -0
- package/src/vite.ts +166 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { f as navigate, n as Failure, s as ancestors } from "./constants-5Eu0naK3.js";
|
|
2
|
+
import { jsx } from "ajo/jsx-runtime";
|
|
3
|
+
import navaid from "navaid";
|
|
4
|
+
import { routes } from "virtual:ajo/routes";
|
|
5
|
+
import * as devalue from "devalue";
|
|
6
|
+
//#region packages/ajo-kit/src/head.tsx
|
|
7
|
+
var key = {
|
|
8
|
+
meta: (entry) => "name" in entry ? entry.name : "property" in entry ? entry.property : entry.httpEquiv,
|
|
9
|
+
link: (entry) => entry.rel
|
|
10
|
+
};
|
|
11
|
+
var append = (items, index, entry, id) => {
|
|
12
|
+
const position = index.get(id);
|
|
13
|
+
if (position === void 0) {
|
|
14
|
+
index.set(id, items.length);
|
|
15
|
+
items.push(entry);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
items[position] = entry;
|
|
19
|
+
};
|
|
20
|
+
/** Merges route heads, letting later meta/link entries win by key. */
|
|
21
|
+
function merge(...heads) {
|
|
22
|
+
const result = {};
|
|
23
|
+
const meta = [];
|
|
24
|
+
const link = [];
|
|
25
|
+
const index = {
|
|
26
|
+
meta: /* @__PURE__ */ new Map(),
|
|
27
|
+
link: /* @__PURE__ */ new Map()
|
|
28
|
+
};
|
|
29
|
+
for (const head of heads) {
|
|
30
|
+
if (!head) continue;
|
|
31
|
+
if (head.title) result.title = head.title;
|
|
32
|
+
for (const entry of head.meta ?? []) append(meta, index.meta, entry, key.meta(entry));
|
|
33
|
+
for (const entry of head.link ?? []) append(link, index.link, entry, key.link(entry));
|
|
34
|
+
}
|
|
35
|
+
if (meta.length) result.meta = meta;
|
|
36
|
+
if (link.length) result.link = link;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
var text = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
40
|
+
var attribute = (value) => text(value).replace(/"/g, """);
|
|
41
|
+
var attrs = (entries) => Object.entries(entries).filter((entry) => entry[1] !== void 0).map(([name, value]) => `${name}="${attribute(value)}"`).join(" ");
|
|
42
|
+
var tag = (name, entries) => `<${name} ${attrs(entries)}>`;
|
|
43
|
+
/** Renders a Head object into SSR-safe HTML tags. */
|
|
44
|
+
function render(head = {}) {
|
|
45
|
+
const tags = [];
|
|
46
|
+
if (head.title) tags.push(`<title>${text(head.title)}</title>`);
|
|
47
|
+
for (const entry of head.meta ?? []) tags.push(tag("meta", entry));
|
|
48
|
+
for (const entry of head.link ?? []) tags.push(tag("link", entry));
|
|
49
|
+
return tags.join("\n ");
|
|
50
|
+
}
|
|
51
|
+
/** Applies a Head object to document.head during client navigation. */
|
|
52
|
+
function apply(head = {}) {
|
|
53
|
+
if (head.title && document.title !== head.title) document.title = head.title;
|
|
54
|
+
const upsert = (selector, attrs) => {
|
|
55
|
+
let node = document.head.querySelector(selector);
|
|
56
|
+
if (!node) {
|
|
57
|
+
node = document.createElement(selector.startsWith("link") ? "link" : "meta");
|
|
58
|
+
for (const [attr, value] of Object.entries(attrs)) node.setAttribute(attr, value);
|
|
59
|
+
document.head.appendChild(node);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const [attr, value] of Object.entries(attrs)) if (node.getAttribute(attr) !== value) node.setAttribute(attr, value);
|
|
63
|
+
};
|
|
64
|
+
for (const entry of head.meta ?? []) {
|
|
65
|
+
const id = key.meta(entry);
|
|
66
|
+
upsert("name" in entry ? `meta[name="${id}"]` : "property" in entry ? `meta[property="${id}"]` : `meta[http-equiv="${id}"]`, entry);
|
|
67
|
+
}
|
|
68
|
+
for (const entry of head.link ?? []) upsert(`link[rel="${entry.rel}"]`, entry);
|
|
69
|
+
}
|
|
70
|
+
var cache = /* @__PURE__ */ new Map();
|
|
71
|
+
var meta = /* @__PURE__ */ new Map();
|
|
72
|
+
var now = () => Date.now();
|
|
73
|
+
var remove = (url) => {
|
|
74
|
+
cache.delete(url);
|
|
75
|
+
meta.delete(url);
|
|
76
|
+
};
|
|
77
|
+
var clear = () => {
|
|
78
|
+
cache.clear();
|
|
79
|
+
meta.clear();
|
|
80
|
+
};
|
|
81
|
+
var get = (url, time = now()) => {
|
|
82
|
+
const state = cache.get(url);
|
|
83
|
+
const info = meta.get(url);
|
|
84
|
+
if (!state || !info) return;
|
|
85
|
+
if (time - info.cached > 3e5) {
|
|
86
|
+
remove(url);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
info.used = time;
|
|
90
|
+
return state;
|
|
91
|
+
};
|
|
92
|
+
var prune = (active, time = now()) => {
|
|
93
|
+
for (const [url, info] of meta) if (url !== active && time - info.cached > 3e5) remove(url);
|
|
94
|
+
while (cache.size > 50) {
|
|
95
|
+
let candidate;
|
|
96
|
+
let oldest = Infinity;
|
|
97
|
+
for (const [url, info] of meta) {
|
|
98
|
+
if (url === active) continue;
|
|
99
|
+
if (info.used < oldest) {
|
|
100
|
+
oldest = info.used;
|
|
101
|
+
candidate = url;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!candidate) break;
|
|
105
|
+
remove(candidate);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var set = (url, state, options) => {
|
|
109
|
+
const time = options?.now ?? now();
|
|
110
|
+
cache.set(url, state);
|
|
111
|
+
meta.set(url, {
|
|
112
|
+
cached: time,
|
|
113
|
+
used: time
|
|
114
|
+
});
|
|
115
|
+
prune(options?.active ?? url, time);
|
|
116
|
+
};
|
|
117
|
+
var invalidate = (topics) => {
|
|
118
|
+
if (!topics?.length) {
|
|
119
|
+
clear();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const changed = new Set(topics);
|
|
123
|
+
for (const [url, state] of cache) if (!state.topics?.length || state.topics.some((topic) => changed.has(topic))) remove(url);
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region packages/ajo-kit/src/app.tsx
|
|
127
|
+
var group = /^\(.*\)$/;
|
|
128
|
+
var dynamic = /^\[(.+?)\]$/;
|
|
129
|
+
var match = (segments) => segments.filter((segment) => segment && !group.test(segment)).map((segment) => segment.replace(dynamic, (_, name) => name === "..." ? "*" : `:${name}`)).join("/");
|
|
130
|
+
var parts = (path) => path.slice(4).split("/").slice(0, -1);
|
|
131
|
+
var initial;
|
|
132
|
+
function init(state) {
|
|
133
|
+
initial = state ?? void 0;
|
|
134
|
+
}
|
|
135
|
+
var error = () => ({
|
|
136
|
+
segments: [""],
|
|
137
|
+
loader: async () => ({ default: () => null })
|
|
138
|
+
});
|
|
139
|
+
var layouts = /* @__PURE__ */ new Map();
|
|
140
|
+
var pages = [];
|
|
141
|
+
var parents = (segments) => ancestors(segments).filter((path) => layouts.has(path));
|
|
142
|
+
for (const [path, loader] of Object.entries(routes)) {
|
|
143
|
+
const segments = parts(path);
|
|
144
|
+
const kind = path.split("/").pop()?.split(".")[0];
|
|
145
|
+
if (kind === "layout") layouts.set(segments.join("/"), loader);
|
|
146
|
+
if (kind === "page") pages.push({
|
|
147
|
+
pattern: match(segments),
|
|
148
|
+
segments,
|
|
149
|
+
loader
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function compose(page, tree, paths, state) {
|
|
153
|
+
const Page = page.default;
|
|
154
|
+
const boundary = page.pending ? "page" : tree.findLast((entry) => entry.module.pending)?.path;
|
|
155
|
+
return tree.reduceRight((Child, { path, module }, depth) => {
|
|
156
|
+
const Layout = module.default;
|
|
157
|
+
return () => /* @__PURE__ */ jsx(Layout, {
|
|
158
|
+
params: state.params,
|
|
159
|
+
data: state.data[depth],
|
|
160
|
+
loading: state.loading && boundary === path,
|
|
161
|
+
error: state.error,
|
|
162
|
+
children: /* @__PURE__ */ jsx(Child, {})
|
|
163
|
+
}, path);
|
|
164
|
+
}, () => /* @__PURE__ */ jsx(Page, {
|
|
165
|
+
params: state.params,
|
|
166
|
+
data: state.data.at(-1),
|
|
167
|
+
loading: state.loading && boundary === "page",
|
|
168
|
+
error: state.error
|
|
169
|
+
}, paths.join("/")));
|
|
170
|
+
}
|
|
171
|
+
async function load(url) {
|
|
172
|
+
const cached = get(url);
|
|
173
|
+
const versions = cached?.versions ? JSON.stringify(cached.versions) : void 0;
|
|
174
|
+
const response = await fetch(url, {
|
|
175
|
+
credentials: "include",
|
|
176
|
+
cache: "no-store",
|
|
177
|
+
headers: {
|
|
178
|
+
Accept: "application/json",
|
|
179
|
+
...cached?.hash && { "X-Have": cached.hash },
|
|
180
|
+
...versions && { "X-Ajo-Versions": versions }
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
if (response.status === 304 && cached) return {
|
|
184
|
+
data: cached.data,
|
|
185
|
+
head: cached.head,
|
|
186
|
+
hash: cached.hash,
|
|
187
|
+
topics: cached.topics,
|
|
188
|
+
versions: cached.versions
|
|
189
|
+
};
|
|
190
|
+
const json = await response.json().catch(() => null);
|
|
191
|
+
if (!json || !response.ok) return {
|
|
192
|
+
data: [],
|
|
193
|
+
error: new Failure(json?.error?.status ?? response.status, json?.error?.message ?? "Load failed")
|
|
194
|
+
};
|
|
195
|
+
if (json.redirect) return {
|
|
196
|
+
data: [],
|
|
197
|
+
redirect: json.redirect
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
data: json.data ?? [],
|
|
201
|
+
head: json.head,
|
|
202
|
+
hash: json.hash,
|
|
203
|
+
topics: json.topics,
|
|
204
|
+
versions: json.versions
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function* resolve(url, layouts, page, data, error) {
|
|
208
|
+
const { loader, segments, params = {} } = page;
|
|
209
|
+
const paths = parents(segments);
|
|
210
|
+
const [target, ...tree] = await Promise.all([loader(), ...paths.map((path) => layouts.get(path)().then((module) => ({
|
|
211
|
+
path,
|
|
212
|
+
module
|
|
213
|
+
})))]);
|
|
214
|
+
if (error) {
|
|
215
|
+
const state = {
|
|
216
|
+
url,
|
|
217
|
+
params,
|
|
218
|
+
data: [],
|
|
219
|
+
loading: false,
|
|
220
|
+
error
|
|
221
|
+
};
|
|
222
|
+
yield {
|
|
223
|
+
page: compose(target, tree, paths, state),
|
|
224
|
+
state
|
|
225
|
+
};
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const cached = initial?.url === url ? initial : void 0;
|
|
229
|
+
if (cached) {
|
|
230
|
+
initial = void 0;
|
|
231
|
+
if (cached.hash) set(url, cached);
|
|
232
|
+
yield {
|
|
233
|
+
page: compose(target, tree, paths, cached),
|
|
234
|
+
state: cached
|
|
235
|
+
};
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
yield { page: compose(target, tree, paths, {
|
|
239
|
+
url,
|
|
240
|
+
params,
|
|
241
|
+
data: [],
|
|
242
|
+
loading: true
|
|
243
|
+
}) };
|
|
244
|
+
const server = data ? { data } : await load(url);
|
|
245
|
+
if (server.redirect) {
|
|
246
|
+
navigate(server.redirect);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (server.error) {
|
|
250
|
+
const state = {
|
|
251
|
+
url,
|
|
252
|
+
params,
|
|
253
|
+
data: [],
|
|
254
|
+
loading: false,
|
|
255
|
+
error: server.error
|
|
256
|
+
};
|
|
257
|
+
yield {
|
|
258
|
+
page: compose(target, tree, paths, state),
|
|
259
|
+
state
|
|
260
|
+
};
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const state = {
|
|
264
|
+
url,
|
|
265
|
+
params,
|
|
266
|
+
data: server.data,
|
|
267
|
+
loading: false,
|
|
268
|
+
head: server.head,
|
|
269
|
+
hash: server.hash,
|
|
270
|
+
topics: server.topics,
|
|
271
|
+
versions: server.versions
|
|
272
|
+
};
|
|
273
|
+
if (state.hash) set(url, state);
|
|
274
|
+
yield {
|
|
275
|
+
page: compose(target, tree, paths, state),
|
|
276
|
+
state
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function stream(update, notify) {
|
|
280
|
+
let source = null;
|
|
281
|
+
const status = (value) => notify?.(value);
|
|
282
|
+
const connect = (path) => {
|
|
283
|
+
source?.close();
|
|
284
|
+
if (globalThis.__AJO_DISABLE_SSE__) {
|
|
285
|
+
status("closed");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
status("connecting");
|
|
289
|
+
source = new EventSource(path);
|
|
290
|
+
source.onopen = () => status("open");
|
|
291
|
+
source.onmessage = (event) => {
|
|
292
|
+
const message = JSON.parse(event.data);
|
|
293
|
+
if (message.data) update(message);
|
|
294
|
+
};
|
|
295
|
+
source.onerror = () => status("connecting");
|
|
296
|
+
};
|
|
297
|
+
const close = () => {
|
|
298
|
+
source?.close();
|
|
299
|
+
source = null;
|
|
300
|
+
status("closed");
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
connect,
|
|
304
|
+
close
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
var App = function* ({ page }) {
|
|
308
|
+
let Page = page ?? (() => null);
|
|
309
|
+
if (page) return /* @__PURE__ */ jsx(Page, {});
|
|
310
|
+
let hmr = false;
|
|
311
|
+
let active = null;
|
|
312
|
+
let timer = null;
|
|
313
|
+
let generation = 0;
|
|
314
|
+
let live = 0;
|
|
315
|
+
let phase = "closed";
|
|
316
|
+
const sse = stream((message) => {
|
|
317
|
+
if (!active || !message.data) return;
|
|
318
|
+
live++;
|
|
319
|
+
const [head, ...entries] = message.data;
|
|
320
|
+
active.data = entries;
|
|
321
|
+
active.hash = message.hash ?? active.hash;
|
|
322
|
+
active.topics = message.topics ?? active.topics;
|
|
323
|
+
active.versions = message.versions ?? active.versions;
|
|
324
|
+
if (head) apply(active.head = head);
|
|
325
|
+
if (active.hash) set(active.url, active);
|
|
326
|
+
this.next();
|
|
327
|
+
}, (status) => phase = status);
|
|
328
|
+
const go = async (target, options = {}) => {
|
|
329
|
+
const gen = ++generation;
|
|
330
|
+
const url = location.pathname + location.search;
|
|
331
|
+
const scroll = options.scroll ?? true;
|
|
332
|
+
sse.close();
|
|
333
|
+
try {
|
|
334
|
+
for await (const { page, state } of resolve(url, layouts, target)) {
|
|
335
|
+
if (gen !== generation) return;
|
|
336
|
+
if (hmr && !state) continue;
|
|
337
|
+
this.next(() => Page = page);
|
|
338
|
+
if (state?.head) apply(state.head);
|
|
339
|
+
if (state && !state.loading) active = state;
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (gen !== generation) return;
|
|
343
|
+
err = err instanceof Failure ? err : new Failure(500, err instanceof Error ? err.message : "Navigation failed");
|
|
344
|
+
for await (const { page } of resolve(url, layouts, error(), void 0, err)) {
|
|
345
|
+
if (gen !== generation) return;
|
|
346
|
+
this.next(() => Page = page);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (gen !== generation) return;
|
|
351
|
+
if (!hmr) {
|
|
352
|
+
sse.connect(url);
|
|
353
|
+
if (scroll) requestAnimationFrame(() => scrollTo({
|
|
354
|
+
top: 0,
|
|
355
|
+
behavior: "smooth"
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
hmr = false;
|
|
359
|
+
};
|
|
360
|
+
const refresh = async () => {
|
|
361
|
+
if (!active) return;
|
|
362
|
+
const gen = generation;
|
|
363
|
+
const state = active;
|
|
364
|
+
const server = await load(state.url);
|
|
365
|
+
if (gen !== generation || active !== state) return;
|
|
366
|
+
if (server.redirect) {
|
|
367
|
+
navigate(server.redirect);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (server.error) {
|
|
371
|
+
state.data = [];
|
|
372
|
+
state.error = server.error;
|
|
373
|
+
state.loading = false;
|
|
374
|
+
} else {
|
|
375
|
+
state.data = server.data;
|
|
376
|
+
state.error = void 0;
|
|
377
|
+
state.head = server.head;
|
|
378
|
+
state.hash = server.hash;
|
|
379
|
+
state.topics = server.topics;
|
|
380
|
+
state.versions = server.versions;
|
|
381
|
+
if (server.head) apply(server.head);
|
|
382
|
+
if (state.hash) set(state.url, state);
|
|
383
|
+
}
|
|
384
|
+
this.next();
|
|
385
|
+
};
|
|
386
|
+
const reconcile = (topics) => {
|
|
387
|
+
if (!topics?.length || !active?.topics?.length) return;
|
|
388
|
+
const changed = new Set(topics);
|
|
389
|
+
if (!active.topics.some((topic) => changed.has(topic))) return;
|
|
390
|
+
const seen = live;
|
|
391
|
+
const delay = phase === "open" ? 250 : 0;
|
|
392
|
+
if (timer) clearTimeout(timer);
|
|
393
|
+
timer = setTimeout(() => {
|
|
394
|
+
timer = null;
|
|
395
|
+
if (live !== seen) return;
|
|
396
|
+
refresh();
|
|
397
|
+
}, delay);
|
|
398
|
+
};
|
|
399
|
+
const router = navaid("/", () => go(error()));
|
|
400
|
+
for (const config of pages) router.on(config.pattern, (params) => go({
|
|
401
|
+
...config,
|
|
402
|
+
params
|
|
403
|
+
}));
|
|
404
|
+
router.listen();
|
|
405
|
+
addEventListener("ajo:navigate", () => router.run(), { signal: this.signal });
|
|
406
|
+
addEventListener("ajo:action", (event) => reconcile(event.detail?.topics), { signal: this.signal });
|
|
407
|
+
this.signal.addEventListener("abort", () => {
|
|
408
|
+
if (timer) clearTimeout(timer);
|
|
409
|
+
sse.close();
|
|
410
|
+
router.unlisten?.();
|
|
411
|
+
});
|
|
412
|
+
while (true) yield /* @__PURE__ */ jsx(Page, {});
|
|
413
|
+
};
|
|
414
|
+
App.attrs = { class: "h-full" };
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region packages/ajo-kit/src/ssr.ts
|
|
417
|
+
var serialize = (value) => devalue.stringify(value);
|
|
418
|
+
var parse = (value) => devalue.parse(value);
|
|
419
|
+
var script = (value) => `<script type="application/json" id="__SSR__">${serialize(value)}<\/script>`;
|
|
420
|
+
//#endregion
|
|
421
|
+
export { init as a, pages as c, resolve as d, invalidate as f, error as i, parents as l, render as m, script as n, layouts as o, merge as p, App as r, match as s, parse as t, parts as u };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { f as navigate } from "./chunks/constants-5Eu0naK3.js";
|
|
2
|
+
import { a as init, f as invalidate, r as App, t as parse } from "./chunks/ssr-CCxCGcm6.js";
|
|
3
|
+
import { jsx } from "ajo/jsx-runtime";
|
|
4
|
+
import { render } from "ajo";
|
|
5
|
+
import { current } from "ajo/context";
|
|
6
|
+
//#region packages/ajo-kit/src/form.ts
|
|
7
|
+
function fields(form) {
|
|
8
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9
|
+
const arrays = /* @__PURE__ */ new Set();
|
|
10
|
+
for (const element of Array.from(form.elements)) {
|
|
11
|
+
const control = element;
|
|
12
|
+
const name = control.name;
|
|
13
|
+
if (!name) continue;
|
|
14
|
+
if (seen.has(name) || control instanceof HTMLSelectElement && control.multiple) arrays.add(name);
|
|
15
|
+
else seen.add(name);
|
|
16
|
+
}
|
|
17
|
+
return arrays;
|
|
18
|
+
}
|
|
19
|
+
function body(data, arrays = /* @__PURE__ */ new Set()) {
|
|
20
|
+
const body = {};
|
|
21
|
+
for (const [name, value] of data) {
|
|
22
|
+
if (typeof value !== "string") continue;
|
|
23
|
+
const current = body[name];
|
|
24
|
+
if (current === void 0) body[name] = arrays.has(name) ? [value] : value;
|
|
25
|
+
else if (Array.isArray(current)) current.push(value);
|
|
26
|
+
else body[name] = [current, value];
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region packages/ajo-kit/src/client.tsx
|
|
32
|
+
/** Creates state and submit/invoke helpers for a route action. */
|
|
33
|
+
function action(name, init) {
|
|
34
|
+
const component = current();
|
|
35
|
+
if (!component) throw new Error("action() must be called inside a stateful component.");
|
|
36
|
+
let controller;
|
|
37
|
+
const state = {
|
|
38
|
+
loading: false,
|
|
39
|
+
data: void 0,
|
|
40
|
+
error: void 0,
|
|
41
|
+
submit: () => {},
|
|
42
|
+
invoke: (value) => run(value),
|
|
43
|
+
reset: () => {
|
|
44
|
+
const current = controller;
|
|
45
|
+
controller = void 0;
|
|
46
|
+
current?.abort();
|
|
47
|
+
state.loading = false;
|
|
48
|
+
state.data = void 0;
|
|
49
|
+
state.error = void 0;
|
|
50
|
+
component.next();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const run = async (value) => {
|
|
54
|
+
controller?.abort();
|
|
55
|
+
const current = controller = new AbortController();
|
|
56
|
+
const forwardAbort = (signal) => {
|
|
57
|
+
if (!signal) return;
|
|
58
|
+
if (signal.aborted) current.abort(signal.reason);
|
|
59
|
+
else signal.addEventListener("abort", () => current.abort(signal.reason), {
|
|
60
|
+
once: true,
|
|
61
|
+
signal: current.signal
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
forwardAbort(component.signal);
|
|
65
|
+
forwardAbort(init?.signal);
|
|
66
|
+
state.loading = true;
|
|
67
|
+
state.error = void 0;
|
|
68
|
+
component.next();
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(name ? `?/${name}` : "", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
credentials: "include",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Accept": "application/json"
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify(value),
|
|
78
|
+
...init,
|
|
79
|
+
signal: current.signal
|
|
80
|
+
});
|
|
81
|
+
const json = await response.json().catch(() => null);
|
|
82
|
+
if (controller !== current || current.signal.aborted) return;
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
state.error = {
|
|
85
|
+
status: json?.error?.status ?? response.status,
|
|
86
|
+
message: json?.error?.message ?? json?.message ?? "Action failed",
|
|
87
|
+
fields: json?.error?.fields ?? json?.fields
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
invalidate(json?.topics);
|
|
92
|
+
if (json?.redirect) {
|
|
93
|
+
navigate(json.redirect);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
state.data = json ?? {};
|
|
97
|
+
globalThis.dispatchEvent?.(new CustomEvent("ajo:action", { detail: json ?? {} }));
|
|
98
|
+
return state.data;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (current.signal.aborted || error instanceof Error && error.name === "AbortError") return;
|
|
101
|
+
if (controller !== current) return;
|
|
102
|
+
state.error = {
|
|
103
|
+
status: 500,
|
|
104
|
+
message: error instanceof Error ? error.message : "Action failed"
|
|
105
|
+
};
|
|
106
|
+
return;
|
|
107
|
+
} finally {
|
|
108
|
+
if (controller === current) {
|
|
109
|
+
controller = void 0;
|
|
110
|
+
state.loading = false;
|
|
111
|
+
component.next();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
state.submit = (event) => {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
const form = event.target;
|
|
118
|
+
run(body(new FormData(form), fields(form))).then(() => {
|
|
119
|
+
if (!state.error) form.reset();
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
124
|
+
{
|
|
125
|
+
const script = globalThis.document?.getElementById("__SSR__");
|
|
126
|
+
init(script?.textContent ? parse(script.textContent) : null);
|
|
127
|
+
}
|
|
128
|
+
var root = globalThis?.document?.getElementById("root");
|
|
129
|
+
if (root) {
|
|
130
|
+
render(/* @__PURE__ */ jsx(App, {}), root);
|
|
131
|
+
document.documentElement.dataset.ajoReady = "true";
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { action };
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Kysely, SqliteDialect, sql } from "kysely";
|
|
3
|
+
/** better-sqlite3 constructor resolved from ajo-kit's own installation. */
|
|
4
|
+
var Database = createRequire(import.meta.resolve("ajo-kit"))("better-sqlite3");
|
|
5
|
+
var sqlite = null;
|
|
6
|
+
var instance = null;
|
|
7
|
+
/** Opens the SQLite database and configures safe defaults. */
|
|
8
|
+
function connect(path = "./database.sqlite") {
|
|
9
|
+
sqlite = new Database(path);
|
|
10
|
+
sqlite.pragma("journal_mode = WAL");
|
|
11
|
+
sqlite.pragma("foreign_keys = ON");
|
|
12
|
+
sqlite.pragma("busy_timeout = 5000");
|
|
13
|
+
sqlite.pragma("synchronous = NORMAL");
|
|
14
|
+
return sqlite;
|
|
15
|
+
}
|
|
16
|
+
/** Returns the shared Kysely instance, opening SQLite on first use. */
|
|
17
|
+
function db() {
|
|
18
|
+
if (!sqlite) connect();
|
|
19
|
+
return instance ??= new Kysely({ dialect: new SqliteDialect({ database: sqlite }) });
|
|
20
|
+
}
|
|
21
|
+
/** Returns the shared raw better-sqlite3 handle. */
|
|
22
|
+
function raw() {
|
|
23
|
+
if (!sqlite) connect();
|
|
24
|
+
return sqlite;
|
|
25
|
+
}
|
|
26
|
+
/** Destroys the shared Kysely instance and closes SQLite. */
|
|
27
|
+
async function close() {
|
|
28
|
+
if (instance) {
|
|
29
|
+
await instance.destroy();
|
|
30
|
+
instance = null;
|
|
31
|
+
}
|
|
32
|
+
if (sqlite) {
|
|
33
|
+
sqlite.close();
|
|
34
|
+
sqlite = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { Database, close, connect, db, raw, sql };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Missing, c as api, f as navigate, i as Invalid, l as date, m as origin, n as Failure, o as ajax, p as normalize, r as Forbidden, t as Denied, u as ip } from "./chunks/constants-5Eu0naK3.js";
|
|
2
|
+
export { Denied, Failure, Forbidden, Invalid, Missing, ajax, api, date, ip, navigate, normalize, origin };
|
package/dist/mail.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region packages/ajo-kit/src/mail/index.ts
|
|
2
|
+
var transport = async (mail) => {
|
|
3
|
+
console.log("📧 Email:", mail.to, "-", mail.subject);
|
|
4
|
+
console.log(mail.text);
|
|
5
|
+
};
|
|
6
|
+
/** Sets the process-wide mail transport. */
|
|
7
|
+
function configure(handler) {
|
|
8
|
+
transport = handler;
|
|
9
|
+
}
|
|
10
|
+
/** Sends one email through the configured transport. */
|
|
11
|
+
async function send(mail) {
|
|
12
|
+
await transport(mail);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { configure, send };
|