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
package/dist/node.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { n as set, t as security } from "./chunks/headers-BDszzwLw.js";
|
|
2
|
+
import polka from "polka";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import * as url from "node:url";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import * as http from "node:http";
|
|
7
|
+
import * as vite from "vite";
|
|
8
|
+
import sirv from "sirv";
|
|
9
|
+
//#region packages/ajo-kit/src/node.ts
|
|
10
|
+
var fallback = `<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
+
<!-- ssr:head -->
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<!-- ssr:data -->
|
|
19
|
+
<div id="root"><!-- ssr:root --></div>
|
|
20
|
+
<script src="/src/client" type="module"><\/script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>`;
|
|
23
|
+
var markers = /<!--\s*ssr:([A-Za-z0-9_]+)\s*-->/g;
|
|
24
|
+
/** Compiles an HTML file with ssr:* comments into a slot renderer. */
|
|
25
|
+
function compile(html) {
|
|
26
|
+
const parts = html.split(markers);
|
|
27
|
+
return (slots) => parts.map((part, index) => index % 2 ? slots[part] ?? "" : part).join("");
|
|
28
|
+
}
|
|
29
|
+
async function html() {
|
|
30
|
+
try {
|
|
31
|
+
return await fs.readFile("./index.html", "utf-8");
|
|
32
|
+
} catch {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Creates the development Polka app with Vite middleware and route reloads. */
|
|
37
|
+
async function dev(options = {}) {
|
|
38
|
+
const app = polka();
|
|
39
|
+
const server = await vite.createServer({
|
|
40
|
+
server: {
|
|
41
|
+
middlewareMode: true,
|
|
42
|
+
...options.hmr !== void 0 && { hmr: options.hmr }
|
|
43
|
+
},
|
|
44
|
+
appType: "custom"
|
|
45
|
+
});
|
|
46
|
+
app.use(server.middlewares);
|
|
47
|
+
let raw = await html();
|
|
48
|
+
raw = await server.transformIndexHtml("/", raw);
|
|
49
|
+
const template = compile(raw);
|
|
50
|
+
const { create } = await server.ssrLoadModule("ajo-kit/server");
|
|
51
|
+
let inner = await create(template);
|
|
52
|
+
app.use((req, res) => inner.handler(req, res));
|
|
53
|
+
const route = /(handler|wares|page|layout)\.[jt]sx?$/;
|
|
54
|
+
const reload = async (file) => {
|
|
55
|
+
if (!route.test(file)) return;
|
|
56
|
+
try {
|
|
57
|
+
const { create } = await server.ssrLoadModule("ajo-kit/server");
|
|
58
|
+
inner = await create(template);
|
|
59
|
+
console.log("\x1B[32m✓\x1B[0m Server routes reloaded");
|
|
60
|
+
if (/(page|layout)\.[jt]sx?$/.test(file)) server.ws.send({
|
|
61
|
+
type: "full-reload",
|
|
62
|
+
path: "*"
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error("\x1B[31m✗\x1B[0m Failed to reload routes:");
|
|
66
|
+
console.error(error);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
server.watcher.on("change", reload);
|
|
70
|
+
server.watcher.on("add", reload);
|
|
71
|
+
server.watcher.on("unlink", reload);
|
|
72
|
+
return app;
|
|
73
|
+
}
|
|
74
|
+
/** Creates the production Polka app from dist/client and dist/server. */
|
|
75
|
+
async function start() {
|
|
76
|
+
const app = polka();
|
|
77
|
+
const { create } = await import(url.pathToFileURL(join(process.cwd(), "dist/server/server.js")).href);
|
|
78
|
+
const inner = await create(compile(await fs.readFile(join(process.cwd(), "dist/client/index.html"), "utf-8")));
|
|
79
|
+
app.use(sirv(join(process.cwd(), "dist/client"), {
|
|
80
|
+
extensions: [],
|
|
81
|
+
setHeaders: (res) => set(res, security(), true)
|
|
82
|
+
}));
|
|
83
|
+
app.use((req, res) => inner.handler(req, res));
|
|
84
|
+
return app;
|
|
85
|
+
}
|
|
86
|
+
/** Builds client and server bundles into dist/. */
|
|
87
|
+
async function build() {
|
|
88
|
+
await vite.build({ build: { outDir: "dist/client" } });
|
|
89
|
+
const entry = url.fileURLToPath(import.meta.resolve("ajo-kit/server"));
|
|
90
|
+
await vite.build({ build: {
|
|
91
|
+
outDir: "dist/server",
|
|
92
|
+
ssr: entry,
|
|
93
|
+
rollupOptions: { output: { entryFileNames: "server.js" } }
|
|
94
|
+
} });
|
|
95
|
+
}
|
|
96
|
+
/** Starts an app, incrementing the port when it is busy unless strict is set. */
|
|
97
|
+
var listen = (app, port = 5173, options = {}) => new Promise((resolve, reject) => {
|
|
98
|
+
http.createServer(app.handler).listen(port, () => {
|
|
99
|
+
console.log(`Server started at http://localhost:${port}`);
|
|
100
|
+
resolve(port);
|
|
101
|
+
}).once("error", (error) => error.code === "EADDRINUSE" && !options.strict ? resolve(listen(app, port + 1, options)) : reject(error));
|
|
102
|
+
});
|
|
103
|
+
//#endregion
|
|
104
|
+
export { build, compile, dev, listen, start };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { c as api, d as links, n as Failure, o as ajax, p as normalize, s as ancestors } from "./chunks/constants-5Eu0naK3.js";
|
|
2
|
+
import { c as pages, d as resolve, i as error, l as parents, m as render, n as script, o as layouts, p as merge, r as App, s as match, u as parts } from "./chunks/ssr-CCxCGcm6.js";
|
|
3
|
+
import { n as set, t as security } from "./chunks/headers-BDszzwLw.js";
|
|
4
|
+
import * as html from "ajo/html";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import polka from "polka";
|
|
7
|
+
import { json } from "@polka/parse";
|
|
8
|
+
import { jsx } from "ajo/jsx-runtime";
|
|
9
|
+
import send, { default as send$1 } from "@polka/send";
|
|
10
|
+
import { handlers, wares } from "virtual:ajo/handlers";
|
|
11
|
+
//#region packages/ajo-kit/src/freshness.ts
|
|
12
|
+
var versions = /* @__PURE__ */ new Map();
|
|
13
|
+
function hash(value) {
|
|
14
|
+
let h = 2166136261;
|
|
15
|
+
for (let i = 0; i < value.length; i++) {
|
|
16
|
+
h ^= value.charCodeAt(i);
|
|
17
|
+
h = Math.imul(h, 16777619);
|
|
18
|
+
}
|
|
19
|
+
return (h >>> 0).toString(36);
|
|
20
|
+
}
|
|
21
|
+
function topics(topic) {
|
|
22
|
+
return [...new Set(Array.isArray(topic) ? topic : [topic])].filter(Boolean).sort();
|
|
23
|
+
}
|
|
24
|
+
function bump(topic) {
|
|
25
|
+
const list = topics(topic);
|
|
26
|
+
for (const t of list) versions.set(t, (versions.get(t) ?? 0) + 1);
|
|
27
|
+
return list;
|
|
28
|
+
}
|
|
29
|
+
function snapshot(keys) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const topic of [...keys].sort()) result[topic] = versions.get(topic) ?? 0;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function parse(raw) {
|
|
35
|
+
if (!raw || Array.isArray(raw)) return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [topic, version] of Object.entries(parsed)) {
|
|
41
|
+
if (typeof version !== "number" || !Number.isFinite(version)) return null;
|
|
42
|
+
result[topic] = version;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function fresh(client) {
|
|
50
|
+
const entries = Object.entries(client ?? {});
|
|
51
|
+
return entries.length > 0 && entries.every(([topic, version]) => (versions.get(topic) ?? 0) === version);
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region packages/ajo-kit/src/timing.ts
|
|
55
|
+
var disabled = new Set([
|
|
56
|
+
"",
|
|
57
|
+
"0",
|
|
58
|
+
"false",
|
|
59
|
+
"off"
|
|
60
|
+
]);
|
|
61
|
+
var active = () => {
|
|
62
|
+
const value = typeof process === "undefined" ? void 0 : process.env.AJO_TIMING;
|
|
63
|
+
return !!value && !disabled.has(value.toLowerCase());
|
|
64
|
+
};
|
|
65
|
+
var round = (value) => Math.round(value * 10) / 10;
|
|
66
|
+
var start = () => active() ? { start: performance.now() } : void 0;
|
|
67
|
+
var elapsed = (start) => round(performance.now() - start);
|
|
68
|
+
var finish = (timing, result) => timing && {
|
|
69
|
+
...timing,
|
|
70
|
+
...result,
|
|
71
|
+
total: elapsed(timing.start)
|
|
72
|
+
};
|
|
73
|
+
var header = (result) => [
|
|
74
|
+
`total;dur=${result.total}`,
|
|
75
|
+
result.loader !== void 0 && `loader;dur=${result.loader}`,
|
|
76
|
+
result.render !== void 0 && `render;dur=${result.render}`
|
|
77
|
+
].filter(Boolean).join(", ");
|
|
78
|
+
var log = (label, result) => {
|
|
79
|
+
const cache = result.cache ? ` ${result.cache}` : "";
|
|
80
|
+
const loader = result.loader === void 0 ? "-" : `${result.loader}ms`;
|
|
81
|
+
const render = result.render === void 0 ? "-" : `${result.render}ms`;
|
|
82
|
+
console.log(`[ajo] ${label} ${result.status}${cache} total=${result.total}ms loader=${loader} render=${render} bytes=${result.bytes}`);
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region packages/ajo-kit/src/server.tsx
|
|
86
|
+
var emitted = new AsyncLocalStorage();
|
|
87
|
+
var payload = (head, entries) => ({
|
|
88
|
+
data: entries,
|
|
89
|
+
head
|
|
90
|
+
});
|
|
91
|
+
var digest = (head, entries) => hash(JSON.stringify(payload(head, entries)));
|
|
92
|
+
var metadata = (topics) => {
|
|
93
|
+
const list = [...topics].sort();
|
|
94
|
+
return {
|
|
95
|
+
topics: list,
|
|
96
|
+
versions: snapshot(list)
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
var size = (body) => Buffer.byteLength(body);
|
|
100
|
+
var vary = "Accept, Cookie";
|
|
101
|
+
var base = (type) => ({
|
|
102
|
+
"Cache-Control": "no-store",
|
|
103
|
+
Vary: vary,
|
|
104
|
+
...type && { "Content-Type": type }
|
|
105
|
+
});
|
|
106
|
+
var done = (req, res, status, bytes, cache) => {
|
|
107
|
+
const result = finish(req.timing, {
|
|
108
|
+
status,
|
|
109
|
+
bytes,
|
|
110
|
+
cache
|
|
111
|
+
});
|
|
112
|
+
if (!result) return;
|
|
113
|
+
res.setHeader("Server-Timing", header(result));
|
|
114
|
+
res.setHeader("X-Ajo-Bytes", String(bytes));
|
|
115
|
+
log(`${req.method} ${req.originalUrl}`, result);
|
|
116
|
+
};
|
|
117
|
+
var write = (req, res, hash, early = false) => {
|
|
118
|
+
const cache = early ? "fresh" : "revalidated";
|
|
119
|
+
res.statusCode = 304;
|
|
120
|
+
set(res, base());
|
|
121
|
+
res.setHeader("X-Ajo-Cache", cache);
|
|
122
|
+
if (hash) res.setHeader("ETag", `"${hash}"`);
|
|
123
|
+
done(req, res, 304, 0, cache);
|
|
124
|
+
res.end();
|
|
125
|
+
};
|
|
126
|
+
var connections = /* @__PURE__ */ new Set();
|
|
127
|
+
var pending = /* @__PURE__ */ new Set();
|
|
128
|
+
var debounce = null;
|
|
129
|
+
var limit = 4;
|
|
130
|
+
var mode = (req) => {
|
|
131
|
+
if (req.token) return "bearer";
|
|
132
|
+
if (req.session) return "session";
|
|
133
|
+
if (req.user) return "user";
|
|
134
|
+
return "anonymous";
|
|
135
|
+
};
|
|
136
|
+
var matches = (conn, topics) => {
|
|
137
|
+
return [...topics].some((topic) => conn.topics.has(topic));
|
|
138
|
+
};
|
|
139
|
+
var probe = () => {
|
|
140
|
+
const headers = /* @__PURE__ */ new Map();
|
|
141
|
+
const res = {
|
|
142
|
+
statusCode: 200,
|
|
143
|
+
writableEnded: false,
|
|
144
|
+
setHeader(key, value) {
|
|
145
|
+
headers.set(key.toLowerCase(), value);
|
|
146
|
+
return res;
|
|
147
|
+
},
|
|
148
|
+
getHeader(key) {
|
|
149
|
+
return headers.get(key.toLowerCase());
|
|
150
|
+
},
|
|
151
|
+
hasHeader(key) {
|
|
152
|
+
return headers.has(key.toLowerCase());
|
|
153
|
+
},
|
|
154
|
+
removeHeader(key) {
|
|
155
|
+
headers.delete(key.toLowerCase());
|
|
156
|
+
},
|
|
157
|
+
writeHead(status, values) {
|
|
158
|
+
res.statusCode = status;
|
|
159
|
+
if (values) {
|
|
160
|
+
for (const [key, value] of Object.entries(values)) if (value !== void 0) res.setHeader(key, value);
|
|
161
|
+
}
|
|
162
|
+
return res;
|
|
163
|
+
},
|
|
164
|
+
write() {
|
|
165
|
+
res.writableEnded = true;
|
|
166
|
+
return true;
|
|
167
|
+
},
|
|
168
|
+
end() {
|
|
169
|
+
res.writableEnded = true;
|
|
170
|
+
return res;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return res;
|
|
174
|
+
};
|
|
175
|
+
var run = (ware, req) => new Promise((resolve, reject) => {
|
|
176
|
+
const res = probe();
|
|
177
|
+
let settled = false;
|
|
178
|
+
const settle = (value) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
resolve(value);
|
|
182
|
+
};
|
|
183
|
+
const fail = (err) => {
|
|
184
|
+
if (settled) return;
|
|
185
|
+
settled = true;
|
|
186
|
+
reject(err);
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
const result = ware(req, res, (err) => err ? fail(err) : settle(true));
|
|
190
|
+
Promise.resolve(result).then(() => {
|
|
191
|
+
if (!settled) settle(false);
|
|
192
|
+
}, fail);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
fail(err);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
var verify = async (req, wares) => {
|
|
198
|
+
for (const ware of wares) if (!await run(ware, req)) return false;
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
var each = async (items, limit, run) => {
|
|
202
|
+
let index = 0;
|
|
203
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
204
|
+
while (index < items.length) await run(items[index++]);
|
|
205
|
+
});
|
|
206
|
+
await Promise.all(workers);
|
|
207
|
+
};
|
|
208
|
+
var close = (conn, reason) => {
|
|
209
|
+
if (reason) console.warn("[SSE] Closing live connection:", {
|
|
210
|
+
reason,
|
|
211
|
+
path: conn.req.path,
|
|
212
|
+
auth: conn.auth
|
|
213
|
+
});
|
|
214
|
+
connections.delete(conn);
|
|
215
|
+
conn.close();
|
|
216
|
+
};
|
|
217
|
+
var revalidate = async (conn) => {
|
|
218
|
+
try {
|
|
219
|
+
if (!connections.has(conn)) return;
|
|
220
|
+
if (conn.verify && !await conn.verify()) {
|
|
221
|
+
close(conn, "credential revalidation failed");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
conn.req.topics = /* @__PURE__ */ new Set();
|
|
225
|
+
const data = await conn.revalidate();
|
|
226
|
+
conn.topics = conn.req.topics ?? /* @__PURE__ */ new Set();
|
|
227
|
+
const [head, ...entries] = data;
|
|
228
|
+
const hash = digest(head, entries);
|
|
229
|
+
if (hash === conn.hash) return;
|
|
230
|
+
conn.hash = hash;
|
|
231
|
+
conn.send({
|
|
232
|
+
data,
|
|
233
|
+
hash,
|
|
234
|
+
...metadata(conn.topics)
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error("[SSE] Live update failed:", err);
|
|
238
|
+
close(conn);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
/** Marks live data topics as changed and notifies matching SSE clients. */
|
|
242
|
+
function emit(topic) {
|
|
243
|
+
const topics = bump(topic);
|
|
244
|
+
const store = emitted.getStore();
|
|
245
|
+
topics.forEach((t) => {
|
|
246
|
+
pending.add(t);
|
|
247
|
+
store?.add(t);
|
|
248
|
+
});
|
|
249
|
+
if (debounce) return;
|
|
250
|
+
debounce = setTimeout(async () => {
|
|
251
|
+
const current = new Set(pending);
|
|
252
|
+
pending.clear();
|
|
253
|
+
debounce = null;
|
|
254
|
+
await each([...connections].filter((conn) => matches(conn, current)), limit, revalidate);
|
|
255
|
+
}, 10);
|
|
256
|
+
}
|
|
257
|
+
var methods = [
|
|
258
|
+
"get",
|
|
259
|
+
"post",
|
|
260
|
+
"put",
|
|
261
|
+
"patch",
|
|
262
|
+
"delete",
|
|
263
|
+
"options",
|
|
264
|
+
"head"
|
|
265
|
+
];
|
|
266
|
+
var parser = json();
|
|
267
|
+
var body = (req, res, next) => {
|
|
268
|
+
let called = false;
|
|
269
|
+
const done = (err) => {
|
|
270
|
+
if (called) return;
|
|
271
|
+
called = true;
|
|
272
|
+
next(err);
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
parser(req, res, done);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
done(err);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
/** Creates the SSR Polka app from an HTML slot template. */
|
|
281
|
+
async function create(template) {
|
|
282
|
+
const secure = (_, res, next) => {
|
|
283
|
+
set(res, security(), true);
|
|
284
|
+
next();
|
|
285
|
+
};
|
|
286
|
+
const timing = (req, _, next) => {
|
|
287
|
+
req.timing = start();
|
|
288
|
+
next();
|
|
289
|
+
};
|
|
290
|
+
const data = (page, stack) => async (req, res, next) => {
|
|
291
|
+
req.topics = /* @__PURE__ */ new Set();
|
|
292
|
+
req.verifyLive = () => verify(req, stack);
|
|
293
|
+
req.track = (topic) => {
|
|
294
|
+
if (Array.isArray(topic)) topic.forEach((t) => req.topics.add(t));
|
|
295
|
+
else req.topics.add(topic);
|
|
296
|
+
};
|
|
297
|
+
const paths = parents(page.segments);
|
|
298
|
+
const key = page.segments.join("/");
|
|
299
|
+
if (ajax(req) && fresh(parse(req.headers["x-ajo-versions"]))) {
|
|
300
|
+
if (req.timing) req.timing.loader = 0;
|
|
301
|
+
write(req, res, req.headers["x-have"]?.toString(), true);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const execute = async () => {
|
|
305
|
+
req.topics.clear();
|
|
306
|
+
const chain = links(paths.length + 1);
|
|
307
|
+
const run = async (loader, depth) => {
|
|
308
|
+
const { parent, deferred } = chain[depth];
|
|
309
|
+
try {
|
|
310
|
+
const result = await (loader?.(req, parent) ?? Promise.resolve({}));
|
|
311
|
+
deferred.resolve(result);
|
|
312
|
+
return result;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
deferred.reject(normalize(err));
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const layout = await Promise.all(paths.map((path, depth) => run(handlers$1.get(path)?.layout, depth)));
|
|
319
|
+
const entry = await run(handlers$1.get(key)?.page, paths.length);
|
|
320
|
+
const data = [...layout, entry];
|
|
321
|
+
return [merge(...await Promise.all([...paths.map((path, index) => handlers$1.get(path)?.head?.(req, async () => data[index]) ?? Promise.resolve({})), handlers$1.get(key)?.head?.(req, async () => entry) ?? Promise.resolve({})])), ...data];
|
|
322
|
+
};
|
|
323
|
+
const begun = performance.now();
|
|
324
|
+
try {
|
|
325
|
+
const result = await execute();
|
|
326
|
+
if (req.timing) req.timing.loader = elapsed(begun);
|
|
327
|
+
req.revalidate = execute;
|
|
328
|
+
req.head = result[0];
|
|
329
|
+
req.entries = result.slice(1);
|
|
330
|
+
next();
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (req.timing) req.timing.loader = elapsed(begun);
|
|
333
|
+
next(normalize(err));
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const sse = (req, res, next) => {
|
|
337
|
+
if (req.headers.accept !== "text/event-stream") return next();
|
|
338
|
+
res.writeHead(200, {
|
|
339
|
+
"Content-Type": "text/event-stream",
|
|
340
|
+
"Cache-Control": "no-cache",
|
|
341
|
+
"Connection": "keep-alive"
|
|
342
|
+
});
|
|
343
|
+
res.flushHeaders();
|
|
344
|
+
const hash = digest(req.head ?? {}, req.entries ?? []);
|
|
345
|
+
const conn = {
|
|
346
|
+
req,
|
|
347
|
+
auth: mode(req),
|
|
348
|
+
topics: req.topics ?? /* @__PURE__ */ new Set(),
|
|
349
|
+
hash,
|
|
350
|
+
verify: req.verifyLive,
|
|
351
|
+
revalidate: req.revalidate,
|
|
352
|
+
send: (message) => res.write(`data: ${JSON.stringify(message)}\n\n`),
|
|
353
|
+
close: () => {}
|
|
354
|
+
};
|
|
355
|
+
connections.add(conn);
|
|
356
|
+
const heartbeat = setInterval(() => res.write(":hb\n\n"), 3e4);
|
|
357
|
+
let closed = false;
|
|
358
|
+
const cleanup = () => {
|
|
359
|
+
if (closed) return;
|
|
360
|
+
closed = true;
|
|
361
|
+
clearInterval(heartbeat);
|
|
362
|
+
connections.delete(conn);
|
|
363
|
+
};
|
|
364
|
+
conn.close = () => {
|
|
365
|
+
cleanup();
|
|
366
|
+
if (!res.writableEnded) res.end();
|
|
367
|
+
};
|
|
368
|
+
req.socket?.on("close", cleanup);
|
|
369
|
+
};
|
|
370
|
+
const render$1 = async (req, res, page, error) => {
|
|
371
|
+
const begun = performance.now();
|
|
372
|
+
const head = req.head ?? {};
|
|
373
|
+
const entries = req.entries ?? [];
|
|
374
|
+
if (ajax(req)) {
|
|
375
|
+
set(res, base("application/json; charset=utf-8"));
|
|
376
|
+
if (error) {
|
|
377
|
+
const body = JSON.stringify({ error: error.toJSON() });
|
|
378
|
+
if (req.timing) req.timing.render = elapsed(begun);
|
|
379
|
+
done(req, res, error.status, size(body));
|
|
380
|
+
return send$1(res, error.status, body);
|
|
381
|
+
}
|
|
382
|
+
const body = payload(head, entries);
|
|
383
|
+
const hash = digest(head, entries);
|
|
384
|
+
const match = req.headers["x-have"] === hash || req.headers["if-none-match"] === `"${hash}"`;
|
|
385
|
+
const meta = metadata(req.topics ?? /* @__PURE__ */ new Set());
|
|
386
|
+
res.setHeader("ETag", `"${hash}"`);
|
|
387
|
+
if (match) {
|
|
388
|
+
if (req.timing) req.timing.render = elapsed(begun);
|
|
389
|
+
write(req, res, hash);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
res.setHeader("X-Ajo-Cache", "miss");
|
|
393
|
+
const response = JSON.stringify({
|
|
394
|
+
...body,
|
|
395
|
+
hash,
|
|
396
|
+
...meta
|
|
397
|
+
});
|
|
398
|
+
if (req.timing) req.timing.render = elapsed(begun);
|
|
399
|
+
done(req, res, 200, size(response), "miss");
|
|
400
|
+
return send$1(res, 200, response);
|
|
401
|
+
}
|
|
402
|
+
let resolved;
|
|
403
|
+
for await (const r of resolve(req.originalUrl, layouts, page, entries, error)) resolved = r;
|
|
404
|
+
const hash = error ? void 0 : digest(head, entries);
|
|
405
|
+
const meta = metadata(req.topics ?? /* @__PURE__ */ new Set());
|
|
406
|
+
const status = resolved?.state?.error?.status ?? error?.status ?? 200;
|
|
407
|
+
const state = {
|
|
408
|
+
...resolved.state,
|
|
409
|
+
error: resolved.state?.error?.toJSON?.() ?? resolved.state?.error,
|
|
410
|
+
head,
|
|
411
|
+
hash,
|
|
412
|
+
...meta
|
|
413
|
+
};
|
|
414
|
+
const body = template({
|
|
415
|
+
head: render(head),
|
|
416
|
+
data: script(state),
|
|
417
|
+
root: html.render(/* @__PURE__ */ jsx(App, { page: resolved.page }))
|
|
418
|
+
});
|
|
419
|
+
if (req.timing) req.timing.render = elapsed(begun);
|
|
420
|
+
done(req, res, status, size(body));
|
|
421
|
+
send$1(res, status, body, base("text/html; charset=utf-8"));
|
|
422
|
+
};
|
|
423
|
+
const action = (segments) => async (req, res) => {
|
|
424
|
+
const name = [...new URL(req.originalUrl, `http://${req.headers.host}`).searchParams.keys()].find((key) => key.startsWith("/"))?.slice(1) || "default";
|
|
425
|
+
let handler;
|
|
426
|
+
for (const path of ancestors(segments).filter((path) => handlers$1.has(path)).reverse()) {
|
|
427
|
+
handler = handlers$1.get(path)?.actions?.[name];
|
|
428
|
+
if (handler) break;
|
|
429
|
+
}
|
|
430
|
+
if (!handler) throw new Failure(400, `Action '${name}' not found`);
|
|
431
|
+
const topics$1 = /* @__PURE__ */ new Set();
|
|
432
|
+
const result = await emitted.run(topics$1, () => handler(req, res));
|
|
433
|
+
if (ajax(req)) {
|
|
434
|
+
const body = result?.redirect ? { redirect: result.redirect } : result ?? { ok: true };
|
|
435
|
+
const sent = topics([...topics$1]);
|
|
436
|
+
const payload = {
|
|
437
|
+
...body,
|
|
438
|
+
...sent.length > 0 && {
|
|
439
|
+
topics: sent,
|
|
440
|
+
versions: snapshot(sent)
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
set(res, base("application/json; charset=utf-8"));
|
|
444
|
+
send$1(res, 200, JSON.stringify(payload));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
res.statusCode = 302;
|
|
448
|
+
res.setHeader("Location", result?.redirect ?? req.originalUrl.split("?")[0]);
|
|
449
|
+
res.end();
|
|
450
|
+
};
|
|
451
|
+
const app = polka({
|
|
452
|
+
onError: (err, req, res) => {
|
|
453
|
+
const normalized = normalize(err);
|
|
454
|
+
if (!(err instanceof Failure) && normalized.status >= 500) console.error(err);
|
|
455
|
+
if (api(req)) send$1(res, normalized.status, normalized.toJSON());
|
|
456
|
+
else render$1(req, res, error(), normalized);
|
|
457
|
+
},
|
|
458
|
+
onNoMatch: (req, res) => {
|
|
459
|
+
const missing = new Failure(404, "Not found");
|
|
460
|
+
if (api(req)) send$1(res, 404, missing.toJSON());
|
|
461
|
+
else render$1(req, res, error(), missing);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
app.use(secure);
|
|
465
|
+
const collect = (segments) => ancestors(segments).flatMap((path) => wares$1.get(path) ?? []);
|
|
466
|
+
const wares$1 = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const [file, loader] of Object.entries(wares)) {
|
|
468
|
+
const exports = await loader();
|
|
469
|
+
const key = parts(file).join("/");
|
|
470
|
+
const items = Array.isArray(exports.default) ? exports.default : [exports.default];
|
|
471
|
+
wares$1.set(key, (wares$1.get(key) ?? []).concat(items));
|
|
472
|
+
}
|
|
473
|
+
const handlers$1 = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const [file, loader] of Object.entries(handlers)) {
|
|
475
|
+
const exports = await loader();
|
|
476
|
+
const segments = parts(file);
|
|
477
|
+
const key = segments.join("/");
|
|
478
|
+
const pattern = match(segments);
|
|
479
|
+
const { default: api, page, layout, head, actions } = exports;
|
|
480
|
+
handlers$1.set(key, {
|
|
481
|
+
page,
|
|
482
|
+
layout,
|
|
483
|
+
head,
|
|
484
|
+
actions
|
|
485
|
+
});
|
|
486
|
+
if (api) for (const method of methods) {
|
|
487
|
+
const route = api[method];
|
|
488
|
+
if (!route) continue;
|
|
489
|
+
app[method](`api/${pattern}`, body, ...collect(segments), route);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const page of pages) {
|
|
493
|
+
const { pattern, segments } = page;
|
|
494
|
+
const path = `/${pattern || ""}`;
|
|
495
|
+
const stack = collect(segments);
|
|
496
|
+
app.get(path, timing, ...stack, data(page, stack), sse, (req, res) => render$1(req, res, page));
|
|
497
|
+
app.post(path, body, ...stack, action(segments));
|
|
498
|
+
}
|
|
499
|
+
return app;
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
502
|
+
export { create, emit, send };
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { i as Invalid } from "./chunks/constants-5Eu0naK3.js";
|
|
2
|
+
import { array, boolean, email, flatten, forward, literal, maxLength, minLength, number, object, optional, partialCheck, pipe, safeParse, string, toLowerCase, transform, trim, unknown } from "valibot";
|
|
3
|
+
//#region packages/ajo-kit/src/validate.ts
|
|
4
|
+
/** Parses data with Valibot and throws Invalid with field errors on failure. */
|
|
5
|
+
function parse(schema, data) {
|
|
6
|
+
const result = safeParse(schema, data);
|
|
7
|
+
if (result.success) return result.output;
|
|
8
|
+
const flat = flatten(result.issues);
|
|
9
|
+
const fields = { ...flat.nested };
|
|
10
|
+
if (flat.root?.length) fields._form = flat.root;
|
|
11
|
+
const nested = flat.nested;
|
|
12
|
+
throw new Invalid(fields, flat.root?.[0] ?? Object.values(nested ?? {})[0]?.[0] ?? "Validation failed");
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { array, boolean, email, forward, literal, maxLength, minLength, number, object, optional, parse, partialCheck, pipe, string, toLowerCase, transform, trim, unknown };
|