bosbun 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/package.json +56 -0
- package/src/cli/add.ts +83 -0
- package/src/cli/build.ts +16 -0
- package/src/cli/create.ts +54 -0
- package/src/cli/dev.ts +14 -0
- package/src/cli/feat.ts +80 -0
- package/src/cli/index.ts +75 -0
- package/src/cli/start.ts +28 -0
- package/src/core/build.ts +157 -0
- package/src/core/client/App.svelte +147 -0
- package/src/core/client/hydrate.ts +78 -0
- package/src/core/client/router.svelte.ts +46 -0
- package/src/core/cookies.ts +52 -0
- package/src/core/cors.ts +60 -0
- package/src/core/csrf.ts +65 -0
- package/src/core/dev.ts +193 -0
- package/src/core/env.ts +135 -0
- package/src/core/envCodegen.ts +94 -0
- package/src/core/errors.ts +23 -0
- package/src/core/hooks.ts +74 -0
- package/src/core/html.ts +170 -0
- package/src/core/matcher.ts +85 -0
- package/src/core/plugin.ts +59 -0
- package/src/core/prerender.ts +79 -0
- package/src/core/renderer.ts +222 -0
- package/src/core/routeFile.ts +88 -0
- package/src/core/routeTypes.ts +95 -0
- package/src/core/scanner.ts +99 -0
- package/src/core/server.ts +320 -0
- package/src/core/types.ts +37 -0
- package/src/lib/index.ts +19 -0
- package/src/lib/utils.ts +24 -0
- package/templates/default/.env.example +34 -0
- package/templates/default/README.md +102 -0
- package/templates/default/package.json +21 -0
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/src/app.css +132 -0
- package/templates/default/src/app.d.ts +7 -0
- package/templates/default/src/lib/.gitkeep +0 -0
- package/templates/default/src/routes/+error.svelte +18 -0
- package/templates/default/src/routes/+layout.svelte +6 -0
- package/templates/default/src/routes/+page.svelte +36 -0
- package/templates/default/src/routes/about/+page.server.ts +1 -0
- package/templates/default/src/routes/about/+page.svelte +8 -0
- package/templates/default/tsconfig.json +22 -0
package/src/core/dev.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { spawn, type Subprocess } from "bun";
|
|
2
|
+
import { watch } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
console.log("🐰 Bunia dev server starting...\n");
|
|
6
|
+
|
|
7
|
+
// ─── State ────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
let appProcess: Subprocess | null = null;
|
|
10
|
+
let sseClients = new Set<ReadableStreamDefaultController>();
|
|
11
|
+
|
|
12
|
+
// ─── SSE Broadcast ────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function broadcastReload() {
|
|
15
|
+
const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
|
|
16
|
+
for (const ctrl of sseClients) {
|
|
17
|
+
try {
|
|
18
|
+
ctrl.enqueue(msg);
|
|
19
|
+
} catch {
|
|
20
|
+
sseClients.delete(ctrl);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (sseClients.size > 0) {
|
|
24
|
+
console.log(`📡 Reload sent to ${sseClients.size} client(s)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Build ────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
|
|
31
|
+
const BUNIA_NODE_MODULES = join(import.meta.dir, "..", "..", "node_modules");
|
|
32
|
+
|
|
33
|
+
async function runBuild(): Promise<boolean> {
|
|
34
|
+
console.log("🏗️ Building...");
|
|
35
|
+
const proc = spawn(["bun", "run", BUILD_SCRIPT], {
|
|
36
|
+
stdout: "inherit",
|
|
37
|
+
stderr: "inherit",
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
});
|
|
40
|
+
return (await proc.exited) === 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Ports ────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const DEV_PORT = Number(process.env.PORT) || 9000;
|
|
46
|
+
const APP_PORT = DEV_PORT + 1; // internal, hidden from user
|
|
47
|
+
|
|
48
|
+
async function startAppServer() {
|
|
49
|
+
if (appProcess) {
|
|
50
|
+
appProcess.kill();
|
|
51
|
+
await appProcess.exited;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read the server entry filename from the manifest written by build.ts
|
|
55
|
+
let serverEntry = "index.js";
|
|
56
|
+
try {
|
|
57
|
+
const manifest = await Bun.file("./dist/manifest.json").json();
|
|
58
|
+
serverEntry = manifest.serverEntry ?? "index.js";
|
|
59
|
+
} catch { }
|
|
60
|
+
|
|
61
|
+
appProcess = spawn(["bun", "run", `dist/server/${serverEntry}`], {
|
|
62
|
+
stdout: "inherit",
|
|
63
|
+
stderr: "inherit",
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
NODE_ENV: "development",
|
|
68
|
+
// Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
|
|
69
|
+
PORT: String(APP_PORT),
|
|
70
|
+
// Allow externalized deps (elysia, etc.) to resolve from bunia's node_modules
|
|
71
|
+
NODE_PATH: BUNIA_NODE_MODULES,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Build & Restart ──────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
let buildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
79
|
+
|
|
80
|
+
async function buildAndRestart() {
|
|
81
|
+
const ok = await runBuild();
|
|
82
|
+
if (!ok) {
|
|
83
|
+
console.error("❌ Build failed — fix errors and save again");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await startAppServer();
|
|
87
|
+
// Give the app server a moment to bind its port
|
|
88
|
+
await Bun.sleep(200);
|
|
89
|
+
broadcastReload();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scheduleBuild() {
|
|
93
|
+
if (buildTimer) clearTimeout(buildTimer);
|
|
94
|
+
buildTimer = setTimeout(buildAndRestart, 300);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Dev Proxy ────────────────────────────────────────────
|
|
98
|
+
// Owns the SSE connection so it survives app server restarts.
|
|
99
|
+
// All other requests are proxied to the app server.
|
|
100
|
+
|
|
101
|
+
Bun.serve({
|
|
102
|
+
port: DEV_PORT,
|
|
103
|
+
idleTimeout: 255,
|
|
104
|
+
async fetch(req) {
|
|
105
|
+
const url = new URL(req.url);
|
|
106
|
+
|
|
107
|
+
// SSE endpoint — owned by dev server, not the app
|
|
108
|
+
if (url.pathname === "/__bunia/sse") {
|
|
109
|
+
return new Response(
|
|
110
|
+
new ReadableStream({
|
|
111
|
+
start(ctrl) {
|
|
112
|
+
sseClients.add(ctrl);
|
|
113
|
+
// Initial keepalive so the browser knows the connection is open
|
|
114
|
+
ctrl.enqueue(new TextEncoder().encode(":ok\n\n"));
|
|
115
|
+
|
|
116
|
+
// Ping every 25s to prevent idle timeout
|
|
117
|
+
const ping = setInterval(() => {
|
|
118
|
+
try {
|
|
119
|
+
ctrl.enqueue(new TextEncoder().encode(":ping\n\n"));
|
|
120
|
+
} catch {
|
|
121
|
+
clearInterval(ping);
|
|
122
|
+
sseClients.delete(ctrl);
|
|
123
|
+
}
|
|
124
|
+
}, 25_000);
|
|
125
|
+
|
|
126
|
+
req.signal.addEventListener("abort", () => {
|
|
127
|
+
clearInterval(ping);
|
|
128
|
+
sseClients.delete(ctrl);
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
{
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
135
|
+
"Cache-Control": "no-cache",
|
|
136
|
+
Connection: "keep-alive",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Proxy everything else to the app server
|
|
143
|
+
try {
|
|
144
|
+
const target = new URL(req.url);
|
|
145
|
+
target.hostname = "localhost";
|
|
146
|
+
target.port = String(APP_PORT);
|
|
147
|
+
|
|
148
|
+
return await fetch(new Request(target.toString(), {
|
|
149
|
+
method: req.method,
|
|
150
|
+
headers: req.headers,
|
|
151
|
+
body: req.body,
|
|
152
|
+
redirect: "manual",
|
|
153
|
+
}));
|
|
154
|
+
} catch {
|
|
155
|
+
return new Response("App server is starting...", {
|
|
156
|
+
status: 503,
|
|
157
|
+
headers: { "Content-Type": "text/plain", "Retry-After": "1" },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─── Initial Build ────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
await buildAndRestart();
|
|
166
|
+
|
|
167
|
+
console.log(`\n🌐 Open http://localhost:${DEV_PORT}\n`);
|
|
168
|
+
|
|
169
|
+
// ─── File Watcher ─────────────────────────────────────────
|
|
170
|
+
// Watch src/ recursively. Skip generated files to avoid loops.
|
|
171
|
+
|
|
172
|
+
const GENERATED = [
|
|
173
|
+
join(process.cwd(), ".bunia"),
|
|
174
|
+
join(process.cwd(), "public", "bunia-tw.css"),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
function isGenerated(path: string): boolean {
|
|
178
|
+
return GENERATED.some(g => path.startsWith(g));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
watch(
|
|
182
|
+
join(process.cwd(), "src"),
|
|
183
|
+
{ recursive: true },
|
|
184
|
+
(_event, filename) => {
|
|
185
|
+
if (!filename) return;
|
|
186
|
+
const abs = join(process.cwd(), "src", filename);
|
|
187
|
+
if (isGenerated(abs)) return;
|
|
188
|
+
console.log(`[watch] changed: ${filename}`);
|
|
189
|
+
scheduleBuild();
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
console.log("👀 Watching src/ for changes...\n");
|
package/src/core/env.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
// ─── Framework-reserved vars ─────────────────────────────
|
|
5
|
+
// These are controlled by Bunia itself — users access them via process.env directly.
|
|
6
|
+
const FRAMEWORK_VARS = new Set([
|
|
7
|
+
"PORT",
|
|
8
|
+
"NODE_ENV",
|
|
9
|
+
"BODY_SIZE_LIMIT",
|
|
10
|
+
"CSRF_ALLOWED_ORIGINS",
|
|
11
|
+
"CORS_ALLOWED_ORIGINS",
|
|
12
|
+
"CORS_ALLOWED_METHODS",
|
|
13
|
+
"CORS_ALLOWED_HEADERS",
|
|
14
|
+
"CORS_EXPOSED_HEADERS",
|
|
15
|
+
"CORS_CREDENTIALS",
|
|
16
|
+
"CORS_MAX_AGE",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// ─── .env File Parser ────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Parse a .env file content into key/value pairs. Skips comments and empty lines. */
|
|
22
|
+
function parseEnvFile(content: string): Record<string, string> {
|
|
23
|
+
const result: Record<string, string> = {};
|
|
24
|
+
for (const line of content.split("\n")) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
27
|
+
const eqIdx = trimmed.indexOf("=");
|
|
28
|
+
if (eqIdx === -1) continue;
|
|
29
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
30
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
31
|
+
// Strip surrounding quotes (single or double)
|
|
32
|
+
if (
|
|
33
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
34
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
35
|
+
) {
|
|
36
|
+
value = value.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
if (key) result[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Env Loader ──────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load .env files in order (later overrides earlier), then apply to process.env.
|
|
47
|
+
* System env vars take highest precedence (never overwritten).
|
|
48
|
+
*
|
|
49
|
+
* Load order:
|
|
50
|
+
* 1. .env
|
|
51
|
+
* 2. .env.local
|
|
52
|
+
* 3. .env.[mode]
|
|
53
|
+
* 4. .env.[mode].local
|
|
54
|
+
*
|
|
55
|
+
* @param mode "development" | "production"
|
|
56
|
+
* @param dir directory to look in (defaults to cwd)
|
|
57
|
+
* @returns merged env record (only vars from .env files, excluding framework vars)
|
|
58
|
+
*/
|
|
59
|
+
export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
60
|
+
const root = dir ?? process.cwd();
|
|
61
|
+
const files = [
|
|
62
|
+
".env",
|
|
63
|
+
".env.local",
|
|
64
|
+
`.env.${mode}`,
|
|
65
|
+
`.env.${mode}.local`,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
let merged: Record<string, string> = {};
|
|
69
|
+
const loaded: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const filename of files) {
|
|
72
|
+
const filepath = join(root, filename);
|
|
73
|
+
if (!existsSync(filepath)) continue;
|
|
74
|
+
const content = readFileSync(filepath, "utf-8");
|
|
75
|
+
const parsed = parseEnvFile(content);
|
|
76
|
+
merged = { ...merged, ...parsed };
|
|
77
|
+
loaded.push(filename);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (loaded.length > 0) {
|
|
81
|
+
console.log(`✓ Loaded ${loaded.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply to process.env — system env wins (don't overwrite existing)
|
|
85
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
86
|
+
if (!(key in process.env)) {
|
|
87
|
+
process.env[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Return only non-framework vars
|
|
92
|
+
const result: Record<string, string> = {};
|
|
93
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
94
|
+
if (!FRAMEWORK_VARS.has(key)) {
|
|
95
|
+
result[key] = process.env[key] ?? value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Classifier ──────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export interface ClassifiedEnv {
|
|
105
|
+
/** PUBLIC_STATIC_* — client+server, build-time inlined */
|
|
106
|
+
publicStatic: Record<string, string>;
|
|
107
|
+
/** PUBLIC_* (excluding PUBLIC_STATIC_*) — client+server, runtime */
|
|
108
|
+
publicDynamic: Record<string, string>;
|
|
109
|
+
/** STATIC_* — server only, build-time inlined */
|
|
110
|
+
privateStatic: Record<string, string>;
|
|
111
|
+
/** everything else — server only, runtime */
|
|
112
|
+
privateDynamic: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Classify env vars by prefix into the four visibility/timing buckets. */
|
|
116
|
+
export function classifyEnvVars(env: Record<string, string>): ClassifiedEnv {
|
|
117
|
+
const publicStatic: Record<string, string> = {};
|
|
118
|
+
const publicDynamic: Record<string, string> = {};
|
|
119
|
+
const privateStatic: Record<string, string> = {};
|
|
120
|
+
const privateDynamic: Record<string, string> = {};
|
|
121
|
+
|
|
122
|
+
for (const [key, value] of Object.entries(env)) {
|
|
123
|
+
if (key.startsWith("PUBLIC_STATIC_")) {
|
|
124
|
+
publicStatic[key] = value;
|
|
125
|
+
} else if (key.startsWith("PUBLIC_")) {
|
|
126
|
+
publicDynamic[key] = value;
|
|
127
|
+
} else if (key.startsWith("STATIC_")) {
|
|
128
|
+
privateStatic[key] = value;
|
|
129
|
+
} else {
|
|
130
|
+
privateDynamic[key] = value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { publicStatic, publicDynamic, privateStatic, privateDynamic };
|
|
135
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { ClassifiedEnv } from "./env.ts";
|
|
4
|
+
|
|
5
|
+
// ─── Env Module Codegen ──────────────────────────────────
|
|
6
|
+
// Generates three files in .bunia/:
|
|
7
|
+
// env.server.ts — all vars (static inlined, dynamic via process.env)
|
|
8
|
+
// env.client.ts — only PUBLIC_* vars (PUBLIC_STATIC_* inlined, PUBLIC_* via window.__BUNIA_ENV__)
|
|
9
|
+
// types/env.d.ts — declare module 'bunia:env' for IDE autocomplete
|
|
10
|
+
|
|
11
|
+
export function generateEnvModules(classified: ClassifiedEnv): void {
|
|
12
|
+
const buniaDir = join(process.cwd(), ".bunia");
|
|
13
|
+
const typesDir = join(buniaDir, "types");
|
|
14
|
+
mkdirSync(buniaDir, { recursive: true });
|
|
15
|
+
mkdirSync(typesDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
writeServerEnv(classified, buniaDir);
|
|
18
|
+
writeClientEnv(classified, buniaDir);
|
|
19
|
+
writeEnvTypes(classified, typesDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeServerEnv(classified: ClassifiedEnv, buniaDir: string): void {
|
|
23
|
+
const lines: string[] = [
|
|
24
|
+
"// Auto-generated by Bunia. Do not edit.",
|
|
25
|
+
"// bunia:env → server — all vars",
|
|
26
|
+
"",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// PUBLIC_STATIC_* — inlined literals
|
|
30
|
+
for (const [key, value] of Object.entries(classified.publicStatic)) {
|
|
31
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// PUBLIC_* dynamic — read from process.env at runtime
|
|
35
|
+
for (const key of Object.keys(classified.publicDynamic)) {
|
|
36
|
+
lines.push(`export const ${key} = process.env.${key} ?? "";`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// STATIC_* — inlined literals
|
|
40
|
+
for (const [key, value] of Object.entries(classified.privateStatic)) {
|
|
41
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// private dynamic — read from process.env at runtime
|
|
45
|
+
for (const key of Object.keys(classified.privateDynamic)) {
|
|
46
|
+
lines.push(`export const ${key} = process.env.${key} ?? "";`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeFileSync(join(buniaDir, "env.server.ts"), lines.join("\n") + "\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeClientEnv(classified: ClassifiedEnv, buniaDir: string): void {
|
|
53
|
+
const lines: string[] = [
|
|
54
|
+
"// Auto-generated by Bunia. Do not edit.",
|
|
55
|
+
"// bunia:env → client — PUBLIC_* vars only",
|
|
56
|
+
"",
|
|
57
|
+
"const __env: Record<string, string> =",
|
|
58
|
+
" typeof window !== 'undefined' && (window as any).__BUNIA_ENV__ || {};",
|
|
59
|
+
"",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// PUBLIC_STATIC_* — inlined literals
|
|
63
|
+
for (const [key, value] of Object.entries(classified.publicStatic)) {
|
|
64
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PUBLIC_* dynamic — read from window.__BUNIA_ENV__ at runtime
|
|
68
|
+
for (const key of Object.keys(classified.publicDynamic)) {
|
|
69
|
+
lines.push(`export const ${key} = __env.${key} ?? "";`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
writeFileSync(join(buniaDir, "env.client.ts"), lines.join("\n") + "\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeEnvTypes(classified: ClassifiedEnv, typesDir: string): void {
|
|
76
|
+
const allKeys = [
|
|
77
|
+
...Object.keys(classified.publicStatic),
|
|
78
|
+
...Object.keys(classified.publicDynamic),
|
|
79
|
+
...Object.keys(classified.privateStatic),
|
|
80
|
+
...Object.keys(classified.privateDynamic),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const declarations = allKeys.map(key => ` export const ${key}: string;`);
|
|
84
|
+
|
|
85
|
+
const content = [
|
|
86
|
+
"// Auto-generated by Bunia. Do not edit.",
|
|
87
|
+
"declare module 'bunia:env' {",
|
|
88
|
+
...declarations,
|
|
89
|
+
"}",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
writeFileSync(join(typesDir, "env.d.ts"), content);
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ─── Error / Redirect Helpers ────────────────────────────
|
|
2
|
+
// Throw these from load() functions; the server catches and handles them.
|
|
3
|
+
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
constructor(public status: number, message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "HttpError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Redirect {
|
|
12
|
+
constructor(public status: number, public location: string) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Throw an HTTP error from a load() function. */
|
|
16
|
+
export function error(status: number, message: string): never {
|
|
17
|
+
throw new HttpError(status, message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Redirect the user from a load() function. */
|
|
21
|
+
export function redirect(status: number, location: string): never {
|
|
22
|
+
throw new Redirect(status, location);
|
|
23
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ─── Bunia Hooks ─────────────────────────────────────────
|
|
2
|
+
// SvelteKit-compatible middleware API.
|
|
3
|
+
// Usage in src/hooks.server.ts:
|
|
4
|
+
//
|
|
5
|
+
// import { sequence } from "bunia";
|
|
6
|
+
// export const handle = sequence(authHandle, loggingHandle);
|
|
7
|
+
|
|
8
|
+
// ─── Cookie Types ─────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface CookieOptions {
|
|
11
|
+
path?: string;
|
|
12
|
+
domain?: string;
|
|
13
|
+
/** Max-Age in seconds */
|
|
14
|
+
maxAge?: number;
|
|
15
|
+
expires?: Date;
|
|
16
|
+
httpOnly?: boolean;
|
|
17
|
+
secure?: boolean;
|
|
18
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Cookies {
|
|
22
|
+
/** Get a cookie value by name */
|
|
23
|
+
get(name: string): string | undefined;
|
|
24
|
+
/** Get all incoming cookies as a plain object */
|
|
25
|
+
getAll(): Record<string, string>;
|
|
26
|
+
/** Set a cookie (added to the response as Set-Cookie) */
|
|
27
|
+
set(name: string, value: string, options?: CookieOptions): void;
|
|
28
|
+
/** Delete a cookie by setting Max-Age=0 */
|
|
29
|
+
delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Event Types ──────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export type RequestEvent = {
|
|
35
|
+
request: Request;
|
|
36
|
+
url: URL;
|
|
37
|
+
locals: Record<string, any>;
|
|
38
|
+
params: Record<string, string>;
|
|
39
|
+
cookies: Cookies;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LoadEvent = {
|
|
43
|
+
url: URL;
|
|
44
|
+
params: Record<string, string>;
|
|
45
|
+
locals: Record<string, any>;
|
|
46
|
+
cookies: Cookies;
|
|
47
|
+
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
48
|
+
parent: () => Promise<Record<string, any>>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ResolveFunction = (event: RequestEvent) => MaybePromise<Response>;
|
|
52
|
+
|
|
53
|
+
export type Handle = (input: {
|
|
54
|
+
event: RequestEvent;
|
|
55
|
+
resolve: ResolveFunction;
|
|
56
|
+
}) => MaybePromise<Response>;
|
|
57
|
+
|
|
58
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
59
|
+
|
|
60
|
+
// ─── Middleware Composition ────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compose multiple `handle` functions into a single handler.
|
|
64
|
+
* Each handler's `resolve` points to the next handler in the chain.
|
|
65
|
+
*/
|
|
66
|
+
export function sequence(...handlers: Handle[]): Handle {
|
|
67
|
+
return ({ event, resolve }) => {
|
|
68
|
+
function apply(i: number, e: RequestEvent): MaybePromise<Response> {
|
|
69
|
+
if (i >= handlers.length) return resolve(e);
|
|
70
|
+
return handlers[i]!({ event: e, resolve: (next) => apply(i + 1, next) });
|
|
71
|
+
}
|
|
72
|
+
return apply(0, event);
|
|
73
|
+
};
|
|
74
|
+
}
|