bosbun 0.0.6 → 0.0.7

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 (40) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/cli/add.ts +1 -1
  4. package/src/cli/create.ts +65 -5
  5. package/src/cli/feat.ts +1 -1
  6. package/src/cli/index.ts +4 -3
  7. package/src/cli/start.ts +2 -4
  8. package/src/core/build.ts +3 -3
  9. package/src/core/dev.ts +4 -3
  10. package/src/core/html.ts +2 -2
  11. package/src/core/paths.ts +41 -0
  12. package/src/core/plugin.ts +35 -1
  13. package/src/core/prerender.ts +3 -2
  14. package/src/core/server.ts +1 -1
  15. package/templates/default/public/favicon.svg +14 -0
  16. package/templates/default/src/routes/+page.svelte +1 -1
  17. package/templates/demo/.env.example +52 -0
  18. package/templates/demo/README.md +23 -0
  19. package/templates/demo/package.json +20 -0
  20. package/templates/demo/public/.gitkeep +0 -0
  21. package/templates/demo/public/favicon.svg +14 -0
  22. package/templates/demo/src/app.css +132 -0
  23. package/templates/demo/src/app.d.ts +7 -0
  24. package/templates/demo/src/hooks.server.ts +21 -0
  25. package/templates/demo/src/lib/utils.ts +1 -0
  26. package/templates/demo/src/routes/(public)/+layout.svelte +31 -0
  27. package/templates/demo/src/routes/(public)/+page.svelte +79 -0
  28. package/templates/demo/src/routes/(public)/about/+page.server.ts +1 -0
  29. package/templates/demo/src/routes/(public)/about/+page.svelte +31 -0
  30. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +38 -0
  31. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -0
  32. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +62 -0
  33. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +53 -0
  34. package/templates/demo/src/routes/+error.svelte +15 -0
  35. package/templates/demo/src/routes/+layout.server.ts +10 -0
  36. package/templates/demo/src/routes/+layout.svelte +6 -0
  37. package/templates/demo/src/routes/actions-test/+page.server.ts +28 -0
  38. package/templates/demo/src/routes/actions-test/+page.svelte +60 -0
  39. package/templates/demo/src/routes/api/hello/+server.ts +44 -0
  40. package/templates/demo/tsconfig.json +22 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@ bun add bosbun
11
11
  Or scaffold a new project:
12
12
 
13
13
  ```bash
14
- bunx bosbun create my-app
14
+ bun x bosbun create my-app
15
15
  ```
16
16
 
17
17
  ## CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosbun",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "A minimalist fullstack framework — SSR + Svelte 5 Runes + Bun + ElysiaJS",
6
6
  "keywords": [
package/src/cli/add.ts CHANGED
@@ -31,7 +31,7 @@ export async function addComponent(name: string, root = false) {
31
31
  if (installed.has(name)) return;
32
32
  installed.add(name);
33
33
 
34
- console.log(root ? `🐰 Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
34
+ console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
35
35
 
36
36
  const meta = await fetchJSON<ComponentMeta>(`${REGISTRY_BASE}/components/${name}/meta.json`);
37
37
 
package/src/cli/create.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { resolve, join, basename } from "path";
2
2
  import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
3
3
  import { spawn } from "bun";
4
+ import * as readline from "readline";
4
5
 
5
- // ─── bosbun create <name> ──────────────────────────────────
6
+ // ─── bosbun create <name> [--template <name>] ──────────────
6
7
 
7
- const TEMPLATE_DIR = resolve(import.meta.dir, "../../templates/default");
8
+ const TEMPLATES_DIR = resolve(import.meta.dir, "../../templates");
8
9
 
9
- export async function runCreate(name: string | undefined) {
10
+ const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
11
+ default: "Minimal starter with routing and Tailwind",
12
+ demo: "Full-featured demo with hooks, API routes, form actions, and more",
13
+ };
14
+
15
+ export async function runCreate(name: string | undefined, args: string[] = []) {
10
16
  if (!name) {
11
17
  console.error("❌ Please provide a project name.\n Usage: bosbun create my-app");
12
18
  process.exit(1);
@@ -19,9 +25,29 @@ export async function runCreate(name: string | undefined) {
19
25
  process.exit(1);
20
26
  }
21
27
 
22
- console.log(`🐰 Creating Bosbun project: ${basename(targetDir)}\n`);
28
+ // Parse --template flag
29
+ let template: string | undefined;
30
+ const templateIdx = args.indexOf("--template");
31
+ if (templateIdx !== -1 && args[templateIdx + 1]) {
32
+ template = args[templateIdx + 1];
33
+ }
34
+
35
+ // If no --template flag, prompt interactively
36
+ if (!template) {
37
+ template = await promptTemplate();
38
+ }
39
+
40
+ // Validate template exists
41
+ const templateDir = resolve(TEMPLATES_DIR, template);
42
+ if (!existsSync(templateDir)) {
43
+ const available = getAvailableTemplates().join(", ");
44
+ console.error(`❌ Unknown template: "${template}"\n Available: ${available}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ console.log(`\n⬡ Creating Bosbun project: ${basename(targetDir)} (template: ${template})\n`);
23
49
 
24
- copyDir(TEMPLATE_DIR, targetDir, name);
50
+ copyDir(templateDir, targetDir, name);
25
51
 
26
52
  console.log(`✅ Project created at ${targetDir}\n`);
27
53
 
@@ -39,6 +65,40 @@ export async function runCreate(name: string | undefined) {
39
65
  }
40
66
  }
41
67
 
68
+ function getAvailableTemplates(): string[] {
69
+ return readdirSync(TEMPLATES_DIR, { withFileTypes: true })
70
+ .filter((d) => d.isDirectory())
71
+ .map((d) => d.name)
72
+ .sort((a, b) => (a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b)));
73
+ }
74
+
75
+ async function promptTemplate(): Promise<string> {
76
+ const templates = getAvailableTemplates();
77
+
78
+ if (templates.length === 1) return templates[0];
79
+
80
+ console.log("\n? Which template?\n");
81
+ templates.forEach((t, i) => {
82
+ const desc = TEMPLATE_DESCRIPTIONS[t] ?? "";
83
+ const marker = i === 0 ? "❯" : " ";
84
+ console.log(` ${marker} ${t}${desc ? ` — ${desc}` : ""}`);
85
+ });
86
+ console.log();
87
+
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+
93
+ return new Promise<string>((resolvePromise) => {
94
+ rl.question(` Template name (default): `, (answer) => {
95
+ rl.close();
96
+ const trimmed = answer.trim();
97
+ resolvePromise(trimmed || "default");
98
+ });
99
+ });
100
+ }
101
+
42
102
  function copyDir(src: string, dest: string, projectName: string) {
43
103
  mkdirSync(dest, { recursive: true });
44
104
  for (const entry of readdirSync(src, { withFileTypes: true })) {
package/src/cli/feat.ts CHANGED
@@ -24,7 +24,7 @@ export async function runFeat(name: string | undefined) {
24
24
  process.exit(1);
25
25
  }
26
26
 
27
- console.log(`🐰 Installing feature: ${name}\n`);
27
+ console.log(`⬡ Installing feature: ${name}\n`);
28
28
 
29
29
  const meta = await fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
30
30
 
package/src/cli/index.ts CHANGED
@@ -13,7 +13,7 @@ async function main() {
13
13
  switch (command) {
14
14
  case "create": {
15
15
  const { runCreate } = await import("./create.ts");
16
- await runCreate(args[0]);
16
+ await runCreate(args[0], args.slice(1));
17
17
  break;
18
18
  }
19
19
  case "dev": {
@@ -43,13 +43,13 @@ async function main() {
43
43
  }
44
44
  default: {
45
45
  console.log(`
46
- 🐰 Bosbun
46
+ Bosbun
47
47
 
48
48
  Usage:
49
49
  bosbun <command> [options]
50
50
 
51
51
  Commands:
52
- create <name> Scaffold a new Bosbun project
52
+ create <name> [--template <t>] Scaffold a new Bosbun project
53
53
  dev Start the development server
54
54
  build Build for production
55
55
  start Run the production server
@@ -58,6 +58,7 @@ Commands:
58
58
 
59
59
  Examples:
60
60
  bosbun create my-app
61
+ bosbun create my-app --template demo
61
62
  bosbun dev
62
63
  bosbun build
63
64
  bosbun start
package/src/cli/start.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { spawn } from "bun";
2
- import { join } from "path";
3
2
  import { loadEnv } from "../core/env.ts";
4
-
5
- const BOSBUN_NODE_MODULES = join(import.meta.dir, "..", "..", "node_modules");
3
+ import { BOSBUN_NODE_PATH } from "../core/paths.ts";
6
4
 
7
5
  export async function runStart() {
8
6
  loadEnv("production");
@@ -20,7 +18,7 @@ export async function runStart() {
20
18
  env: {
21
19
  ...process.env,
22
20
  NODE_ENV: "production",
23
- NODE_PATH: BOSBUN_NODE_MODULES,
21
+ NODE_PATH: BOSBUN_NODE_PATH,
24
22
  },
25
23
  });
26
24
 
package/src/core/build.ts CHANGED
@@ -10,10 +10,10 @@ import { makeBosbunPlugin } from "./plugin.ts";
10
10
  import { prerenderStaticRoutes } from "./prerender.ts";
11
11
  import { loadEnv, classifyEnvVars } from "./env.ts";
12
12
  import { generateEnvModules } from "./envCodegen.ts";
13
+ import { BOSBUN_NODE_PATH, resolveBosbunBin } from "./paths.ts";
13
14
 
14
15
  // Resolved from this file's location inside the bosbun package
15
16
  const CORE_DIR = import.meta.dir;
16
- const BOSBUN_NODE_MODULES = join(CORE_DIR, "..", "..", "node_modules");
17
17
 
18
18
  // ─── Entry Point ─────────────────────────────────────────
19
19
 
@@ -57,12 +57,12 @@ generateEnvModules(classifiedEnv);
57
57
 
58
58
  // 3. Build Tailwind CSS
59
59
  console.log("\n🎨 Building Tailwind CSS...");
60
- const tailwindBin = join(BOSBUN_NODE_MODULES, ".bin", "tailwindcss");
60
+ const tailwindBin = resolveBosbunBin("tailwindcss");
61
61
  const tailwindResult = spawnSync(
62
62
  [tailwindBin, "-i", "./src/app.css", "-o", "./public/bosbun-tw.css", ...(isProduction ? ["--minify"] : [])],
63
63
  {
64
64
  cwd: process.cwd(),
65
- env: { ...process.env, NODE_PATH: BOSBUN_NODE_MODULES },
65
+ env: { ...process.env, NODE_PATH: BOSBUN_NODE_PATH },
66
66
  },
67
67
  );
68
68
  if (tailwindResult.exitCode !== 0) {
package/src/core/dev.ts CHANGED
@@ -2,7 +2,7 @@ import { spawn, type Subprocess } from "bun";
2
2
  import { watch } from "fs";
3
3
  import { join } from "path";
4
4
 
5
- console.log("🐰 Bosbun dev server starting...\n");
5
+ console.log(" Bosbun dev server starting...\n");
6
6
 
7
7
  // ─── State ────────────────────────────────────────────────
8
8
 
@@ -27,8 +27,9 @@ function broadcastReload() {
27
27
 
28
28
  // ─── Build ────────────────────────────────────────────────
29
29
 
30
+ import { BOSBUN_NODE_PATH } from "./paths.ts";
31
+
30
32
  const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
31
- const BOSBUN_NODE_MODULES = join(import.meta.dir, "..", "..", "node_modules");
32
33
 
33
34
  async function runBuild(): Promise<boolean> {
34
35
  console.log("🏗️ Building...");
@@ -68,7 +69,7 @@ async function startAppServer() {
68
69
  // Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
69
70
  PORT: String(APP_PORT),
70
71
  // Allow externalized deps (elysia, etc.) to resolve from bosbun's node_modules
71
- NODE_PATH: BOSBUN_NODE_MODULES,
72
+ NODE_PATH: BOSBUN_NODE_PATH,
72
73
  },
73
74
  });
74
75
  }
package/src/core/html.ts CHANGED
@@ -93,7 +93,7 @@ export function buildHtml(
93
93
  <meta charset="UTF-8">
94
94
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
95
  ${fallbackTitle}
96
- <link rel="icon" href="data:,">
96
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
97
97
  ${head}
98
98
  ${cssLinks}
99
99
  <link rel="stylesheet" href="/bosbun-tw.css${cacheBust}">
@@ -129,7 +129,7 @@ export function buildHtmlShellOpen(): string {
129
129
  _shellOpen = `<!DOCTYPE html>\n<html lang="en">\n<head>\n` +
130
130
  ` <meta charset="UTF-8">\n` +
131
131
  ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
132
- ` <link rel="icon" href="data:,">\n` +
132
+ ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
133
133
  ` ${cssLinks}\n` +
134
134
  ` <link rel="stylesheet" href="/bosbun-tw.css${cacheBust}">\n` +
135
135
  ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
@@ -0,0 +1,41 @@
1
+ import { join, dirname, resolve } from "path";
2
+ import { existsSync } from "fs";
3
+
4
+ // This file lives at src/core/paths.ts → package root is ../..
5
+ const BOSBUN_PKG_DIR = join(import.meta.dir, "..", "..");
6
+
7
+ // Bun hoists dependencies flat, so bosbun's deps may live in the parent
8
+ // node_modules rather than a nested node_modules/bosbun/node_modules.
9
+ const NESTED_NM = join(BOSBUN_PKG_DIR, "node_modules");
10
+
11
+ // Walk up from the package dir to find the nearest ancestor node_modules.
12
+ // In a workspace, bosbun lives at packages/bosbun/ so we need to find
13
+ // the workspace root's node_modules (not just the parent directory).
14
+ function findAncestorNodeModules(from: string): string | null {
15
+ let dir = resolve(from, "..");
16
+ const root = dirname(dir); // stop at filesystem root
17
+ while (dir !== root) {
18
+ const candidate = join(dir, "node_modules");
19
+ if (candidate !== NESTED_NM && existsSync(candidate)) return candidate;
20
+ dir = dirname(dir);
21
+ }
22
+ return null;
23
+ }
24
+
25
+ const HOISTED_NM = findAncestorNodeModules(BOSBUN_PKG_DIR);
26
+
27
+ /** NODE_PATH value covering both nested and hoisted dependency locations */
28
+ export const BOSBUN_NODE_PATH = HOISTED_NM
29
+ ? [NESTED_NM, HOISTED_NM].join(":")
30
+ : NESTED_NM;
31
+
32
+ /** Find a binary from bosbun's dependencies (handles hoisting) */
33
+ export function resolveBosbunBin(name: string): string {
34
+ const nested = join(NESTED_NM, ".bin", name);
35
+ if (existsSync(nested)) return nested;
36
+ if (HOISTED_NM) {
37
+ const hoisted = join(HOISTED_NM, ".bin", name);
38
+ if (existsSync(hoisted)) return hoisted;
39
+ }
40
+ return nested; // fallback — will produce a clear ENOENT
41
+ }
@@ -1,4 +1,4 @@
1
- import { join } from "path";
1
+ import { join, dirname } from "path";
2
2
 
3
3
  // ─── Bun Build Plugin ─────────────────────────────────────
4
4
  // Resolves:
@@ -31,6 +31,40 @@ export function makeBosbunPlugin(target: "browser" | "bun" = "bun") {
31
31
  return { path: await resolveWithExts(base) };
32
32
  });
33
33
 
34
+ // Force svelte imports to resolve from the app's node_modules.
35
+ // Without this, when bosbun is symlinked (bun link / workspace),
36
+ // hydrate.ts resolves "svelte" from the framework's location while
37
+ // compiled components resolve "svelte/internal/client" from the app's.
38
+ // Two different Svelte copies = duplicate runtime state = broken hydration.
39
+ //
40
+ // require.resolve uses the "default" export condition, which for
41
+ // bare "svelte" returns index-server.js. For browser builds we need
42
+ // index-client.js, so we read the "browser" condition from package.json.
43
+ const appDir = process.cwd();
44
+ let svelteBrowserEntry: string | null = null;
45
+ if (target === "browser") {
46
+ try {
47
+ const svelteDir = dirname(require.resolve("svelte/package.json", { paths: [appDir] }));
48
+ const pkg = require(join(svelteDir, "package.json"));
49
+ const dotExport = pkg.exports?.["."];
50
+ const browserPath = typeof dotExport === "object" ? dotExport.browser : null;
51
+ if (browserPath) {
52
+ svelteBrowserEntry = join(svelteDir, browserPath);
53
+ }
54
+ } catch { }
55
+ }
56
+ build.onResolve({ filter: /^svelte(\/.*)?$/ }, (args) => {
57
+ try {
58
+ // Bare "svelte" in browser build: use the "browser" export condition
59
+ if (args.path === "svelte" && svelteBrowserEntry) {
60
+ return { path: svelteBrowserEntry };
61
+ }
62
+ return { path: require.resolve(args.path, { paths: [appDir] }) };
63
+ } catch {
64
+ return undefined; // fall through to default resolution
65
+ }
66
+ });
67
+
34
68
  // "tailwindcss" inside app.css is a Tailwind CLI directive —
35
69
  // it's already compiled to public/bosbun-tw.css by the CLI step.
36
70
  // Return an empty CSS module so Bun's CSS bundler doesn't choke on it.
@@ -2,8 +2,9 @@ import { writeFileSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
3
  import type { RouteManifest } from "./types.ts";
4
4
 
5
+ import { BOSBUN_NODE_PATH } from "./paths.ts";
6
+
5
7
  const CORE_DIR = import.meta.dir;
6
- const BOSBUN_NODE_MODULES = join(CORE_DIR, "..", "..", "node_modules");
7
8
 
8
9
  const PRERENDER_TIMEOUT = Number(process.env.PRERENDER_TIMEOUT) || 5_000; // 5s default
9
10
 
@@ -36,7 +37,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
36
37
  const child = Bun.spawn(
37
38
  ["bun", "run", "./dist/server/index.js"],
38
39
  {
39
- env: { ...process.env, NODE_ENV: "production", PORT: String(port), NODE_PATH: BOSBUN_NODE_MODULES },
40
+ env: { ...process.env, NODE_ENV: "production", PORT: String(port), NODE_PATH: BOSBUN_NODE_PATH },
40
41
  stdout: "ignore",
41
42
  stderr: "ignore",
42
43
  },
@@ -398,7 +398,7 @@ const app = new Elysia({ serve: { maxRequestBodySize: BODY_SIZE_LIMIT } })
398
398
 
399
399
  app.listen(PORT, () => {
400
400
  // In dev mode the proxy owns the user-facing port — don't print the internal port
401
- if (!isDev) console.log(`🐰 Bosbun server running at http://localhost:${PORT}`);
401
+ if (!isDev) console.log(`⬡ Bosbun server running at http://localhost:${PORT}`);
402
402
  });
403
403
 
404
404
  function shutdown() {
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="currentColor" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="currentColor" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="currentColor" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="currentColor" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="currentColor" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <main class="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
6
6
  <div class="flex flex-col items-center gap-3 text-center">
7
- <p class="text-6xl">🐰</p>
7
+ <img src="/favicon.svg" alt="" class="size-16" />
8
8
  <h1 class="text-4xl font-bold tracking-tight">{name}</h1>
9
9
  <p class="text-muted-foreground text-lg">
10
10
  A Bosbun project — SSR + Svelte 5 + Bun + ElysiaJS
@@ -0,0 +1,52 @@
1
+ # Server port. Defaults to 9000 in production, 9001 in dev (proxied via :9000).
2
+ PORT=9000
3
+
4
+ # Maximum request body size. Supports K/M/G suffixes or "Infinity". Defaults to 512K.
5
+ BODY_SIZE_LIMIT=512K
6
+
7
+ # Comma-separated list of allowed CORS origins for CSRF validation.
8
+ # Leave unset to allow same-origin requests only.
9
+ # Example: https://app.example.com, https://admin.example.com
10
+ CSRF_ALLOWED_ORIGINS=
11
+
12
+ # Comma-separated list of origins allowed to make cross-origin requests.
13
+ # Leave unset to disable CORS (browsers block cross-origin requests by default).
14
+ # Example: https://app.example.com, http://localhost:5173
15
+ CORS_ALLOWED_ORIGINS=
16
+
17
+ # Comma-separated HTTP methods to allow in CORS requests.
18
+ # Default: GET, HEAD, PUT, PATCH, POST, DELETE
19
+ # CORS_ALLOWED_METHODS=GET, POST
20
+
21
+ # Comma-separated request headers to allow in CORS requests.
22
+ # Default: Content-Type, Authorization
23
+ # CORS_ALLOWED_HEADERS=Content-Type, Authorization, X-Custom-Header
24
+
25
+ # Comma-separated response headers to expose to the browser.
26
+ # Default: none
27
+ # CORS_EXPOSED_HEADERS=X-Total-Count, X-Request-Id
28
+
29
+ # Whether to allow cookies and auth credentials in cross-origin requests.
30
+ # Must be "true" to enable; requires CORS_ALLOWED_ORIGINS to be set (not wildcard).
31
+ # Default: false
32
+ # CORS_CREDENTIALS=true
33
+
34
+ # Preflight response cache duration in seconds.
35
+ # Default: 86400 (24 hours)
36
+ # CORS_MAX_AGE=86400
37
+
38
+ # Timeout for load() functions (layout + page) in milliseconds. Defaults to 5000 (5s).
39
+ # Set to 0 or Infinity to disable.
40
+ # LOAD_TIMEOUT=5000
41
+
42
+ # Timeout for metadata() functions in milliseconds. Defaults to 3000 (3s).
43
+ # Set to 0 or Infinity to disable.
44
+ # METADATA_TIMEOUT=3000
45
+
46
+ # Timeout for prerender fetches during build in milliseconds. Defaults to 5000 (5s).
47
+ # Set to 0 or Infinity to disable.
48
+ # PRERENDER_TIMEOUT=5000
49
+
50
+ # Set automatically by the framework (bosbun dev / bosbun build / bosbun start).
51
+ # Do not set manually.
52
+ # NODE_ENV=production
@@ -0,0 +1,23 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A [Bosbun](https://github.com/nicholascostadev/bosbun) project with demo routes, hooks, API endpoints, and form actions.
4
+
5
+ ## Running
6
+
7
+ ```bash
8
+ bun x bosbun dev # http://localhost:9000
9
+ bun x bosbun build # production build
10
+ bun x bosbun start # run production server
11
+ ```
12
+
13
+ ## Routes
14
+
15
+ | URL | File | Description |
16
+ |-----|------|-------------|
17
+ | `/` | `(public)/+page.svelte` | Home page |
18
+ | `/about` | `(public)/about/+page.svelte` | About page |
19
+ | `/blog` | `(public)/blog/+page.svelte` | Blog listing |
20
+ | `/blog/:slug` | `(public)/blog/[slug]/+page.svelte` | Blog post — fetched via server loader |
21
+ | `/api/hello` | `api/hello/+server.ts` | Multi-method JSON API |
22
+ | `/actions-test` | `actions-test/+page.svelte` | Form actions demo |
23
+ | `/*` | `(public)/[...catchall]/+page.svelte` | 404 catch-all |
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bosbun dev",
7
+ "build": "bosbun build",
8
+ "start": "bosbun start"
9
+ },
10
+ "dependencies": {
11
+ "bosbun": "*",
12
+ "svelte": "^5.20.0",
13
+ "clsx": "^2.1.1",
14
+ "tailwind-merge": "^3.5.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "latest",
18
+ "typescript": "^5"
19
+ }
20
+ }
File without changes
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="currentColor" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="currentColor" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="currentColor" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="currentColor" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="currentColor" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,132 @@
1
+ @import "tailwindcss";
2
+ @source "../src";
3
+
4
+ /*
5
+ * ─── shadcn-inspired Design Tokens ──────────────────────
6
+ * CSS custom properties for light & dark themes.
7
+ * Uses HSL values so Tailwind can apply opacity modifiers.
8
+ */
9
+
10
+ @theme {
11
+ --color-background: hsl(var(--background));
12
+ --color-foreground: hsl(var(--foreground));
13
+
14
+ --color-card: hsl(var(--card));
15
+ --color-card-foreground: hsl(var(--card-foreground));
16
+
17
+ --color-popover: hsl(var(--popover));
18
+ --color-popover-foreground: hsl(var(--popover-foreground));
19
+
20
+ --color-primary: hsl(var(--primary));
21
+ --color-primary-foreground: hsl(var(--primary-foreground));
22
+
23
+ --color-secondary: hsl(var(--secondary));
24
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
25
+
26
+ --color-muted: hsl(var(--muted));
27
+ --color-muted-foreground: hsl(var(--muted-foreground));
28
+
29
+ --color-accent: hsl(var(--accent));
30
+ --color-accent-foreground: hsl(var(--accent-foreground));
31
+
32
+ --color-destructive: hsl(var(--destructive));
33
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
34
+
35
+ --color-border: hsl(var(--border));
36
+ --color-input: hsl(var(--input));
37
+ --color-ring: hsl(var(--ring));
38
+
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+ }
44
+
45
+ /* ─── Light Theme (Default) ─────────────────────────────── */
46
+
47
+ :root {
48
+ --background: 0 0% 100%;
49
+ --foreground: 222.2 84% 4.9%;
50
+
51
+ --card: 0 0% 100%;
52
+ --card-foreground: 222.2 84% 4.9%;
53
+
54
+ --popover: 0 0% 100%;
55
+ --popover-foreground: 222.2 84% 4.9%;
56
+
57
+ --primary: 222.2 47.4% 11.2%;
58
+ --primary-foreground: 210 40% 98%;
59
+
60
+ --secondary: 210 40% 96.1%;
61
+ --secondary-foreground: 222.2 47.4% 11.2%;
62
+
63
+ --muted: 210 40% 96.1%;
64
+ --muted-foreground: 215.4 16.3% 46.9%;
65
+
66
+ --accent: 210 40% 96.1%;
67
+ --accent-foreground: 222.2 47.4% 11.2%;
68
+
69
+ --destructive: 0 84.2% 60.2%;
70
+ --destructive-foreground: 210 40% 98%;
71
+
72
+ --border: 214.3 31.8% 91.4%;
73
+ --input: 214.3 31.8% 91.4%;
74
+ --ring: 222.2 84% 4.9%;
75
+
76
+ --radius: 0.5rem;
77
+ }
78
+
79
+ /* ─── Dark Theme ─────────────────────────────────────────── */
80
+
81
+ .dark {
82
+ --background: 222.2 84% 4.9%;
83
+ --foreground: 210 40% 98%;
84
+
85
+ --card: 222.2 84% 4.9%;
86
+ --card-foreground: 210 40% 98%;
87
+
88
+ --popover: 222.2 84% 4.9%;
89
+ --popover-foreground: 210 40% 98%;
90
+
91
+ --primary: 210 40% 98%;
92
+ --primary-foreground: 222.2 47.4% 11.2%;
93
+
94
+ --secondary: 217.2 32.6% 17.5%;
95
+ --secondary-foreground: 210 40% 98%;
96
+
97
+ --muted: 217.2 32.6% 17.5%;
98
+ --muted-foreground: 215 20.2% 65.1%;
99
+
100
+ --accent: 217.2 32.6% 17.5%;
101
+ --accent-foreground: 210 40% 98%;
102
+
103
+ --destructive: 0 62.8% 30.6%;
104
+ --destructive-foreground: 210 40% 98%;
105
+
106
+ --border: 217.2 32.6% 17.5%;
107
+ --input: 217.2 32.6% 17.5%;
108
+ --ring: 212.7 26.8% 83.9%;
109
+ }
110
+
111
+ /* ─── Base Styles ────────────────────────────────────────── */
112
+
113
+ @layer base {
114
+ * {
115
+ border-color: theme(--color-border);
116
+ }
117
+
118
+ body {
119
+ background-color: theme(--color-background);
120
+ color: theme(--color-foreground);
121
+ font-family:
122
+ "Inter",
123
+ system-ui,
124
+ -apple-system,
125
+ BlinkMacSystemFont,
126
+ "Segoe UI",
127
+ Roboto,
128
+ "Helvetica Neue",
129
+ Arial,
130
+ sans-serif;
131
+ }
132
+ }
@@ -0,0 +1,7 @@
1
+ /// <reference types="svelte" />
2
+
3
+ declare module "*.svelte" {
4
+ import type { Component } from "svelte";
5
+ const component: Component<Record<string, any>, Record<string, any>, any>;
6
+ export default component;
7
+ }
@@ -0,0 +1,21 @@
1
+ import { sequence } from "bosbun";
2
+ import type { Handle } from "bosbun";
3
+
4
+ // Sets locals that every loader and API handler can read
5
+ const authHandle: Handle = async ({ event, resolve }) => {
6
+ event.locals.requestTime = Date.now();
7
+ event.locals.user = null; // replace with real session logic
8
+ return resolve(event);
9
+ };
10
+
11
+ // Logs each request with method, path, status, and duration
12
+ const loggingHandle: Handle = async ({ event, resolve }) => {
13
+ const start = Date.now();
14
+ const res = await resolve(event);
15
+ const ms = Date.now() - start;
16
+ console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
17
+ res.headers.set("X-Response-Time", `${ms}ms`);
18
+ return res;
19
+ };
20
+
21
+ export const handle = sequence(authHandle, loggingHandle);
@@ -0,0 +1 @@
1
+ export { cn } from "bosbun";
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import type { LayoutData } from '../$types';
3
+
4
+ let { children, data }: { children: any; data: LayoutData } = $props();
5
+ </script>
6
+
7
+ <div class="flex min-h-screen flex-col bg-background text-foreground">
8
+ <header class="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
9
+ <nav class="mx-auto flex max-w-4xl items-center gap-6 px-4 py-3">
10
+ <a href="/" class="font-bold tracking-tight flex items-center gap-2"><img src="/favicon.svg" alt="" class="size-5" /> {{PROJECT_NAME}}</a>
11
+ <a href="/" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
12
+ <a href="/about" class="text-sm text-muted-foreground hover:text-foreground transition-colors">About</a>
13
+ <a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Blog</a>
14
+ <a href="/all/foo/bar" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Catch-all</a>
15
+ <a href="/api/hello" target="_blank" class="text-sm text-muted-foreground hover:text-foreground transition-colors">API</a>
16
+ </nav>
17
+ </header>
18
+
19
+ <main class="mx-auto w-full max-w-4xl flex-1 px-4 py-8">
20
+ {@render children()}
21
+ </main>
22
+
23
+ <footer class="border-t py-4 text-center text-sm text-muted-foreground">
24
+ Powered by Bosbun
25
+ {#if data.requestTime}
26
+ <span class="ml-2 opacity-40 font-mono text-xs">
27
+ req at {new Date(data.requestTime).toISOString()}
28
+ </span>
29
+ {/if}
30
+ </footer>
31
+ </div>
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ let count = $state(0);
3
+
4
+ const features = [
5
+ { icon: "📂", label: "File-based routing", desc: "+page.svelte, +layout.svelte, route groups, dynamic [params]" },
6
+ { icon: "⚡", label: "SSR + Hydration", desc: "Server renders HTML, Svelte hydrates on the client" },
7
+ { icon: "🔁", label: "Server loaders", desc: "+page.server.ts and +layout.server.ts with parent() threading" },
8
+ { icon: "🪝", label: "Hooks", desc: "sequence() middleware — auth, logging, locals" },
9
+ { icon: "📡", label: "API routes", desc: "+server.ts exports GET, POST, PUT, PATCH, DELETE" },
10
+ { icon: "🧩", label: "Component registry", desc: "bosbun add button — shadcn-style, code you own" },
11
+ { icon: "✨", label: "feat system", desc: "bosbun feat login — scaffold entire features, not just components" },
12
+ ];
13
+ </script>
14
+
15
+ <svelte:head>
16
+ <title>{{PROJECT_NAME}}</title>
17
+ <meta name="description" content="{{PROJECT_NAME}} — SSR + Svelte 5 + Bun + ElysiaJS" />
18
+ </svelte:head>
19
+
20
+ <div class="space-y-12">
21
+ <!-- Hero -->
22
+ <div class="space-y-3 pt-4">
23
+ <h1 class="text-5xl font-bold tracking-tight flex items-center gap-3"><img src="/favicon.svg" alt="" class="size-10" /> {{PROJECT_NAME}}</h1>
24
+ <p class="text-xl text-muted-foreground max-w-xl">
25
+ A minimalist fullstack framework — SSR, Svelte 5 Runes, Bun, and ElysiaJS.
26
+ </p>
27
+ </div>
28
+
29
+ <!-- Svelte 5 reactivity demo -->
30
+ <div class="rounded-lg border bg-card p-6 space-y-3 max-w-sm">
31
+ <p class="text-sm font-medium text-muted-foreground">Svelte 5 $state demo</p>
32
+ <p class="text-5xl font-bold tabular-nums">{count}</p>
33
+ <div class="flex gap-2">
34
+ <button
35
+ onclick={() => count++}
36
+ class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
37
+ >
38
+ +1
39
+ </button>
40
+ <button
41
+ onclick={() => count = 0}
42
+ class="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors"
43
+ >
44
+ Reset
45
+ </button>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Features grid -->
50
+ <div class="space-y-4">
51
+ <h2 class="text-2xl font-semibold tracking-tight">Features</h2>
52
+ <div class="grid gap-4 sm:grid-cols-2">
53
+ {#each features as f}
54
+ <div class="rounded-lg border bg-card p-4 space-y-1">
55
+ <p class="font-medium">{f.icon} {f.label}</p>
56
+ <p class="text-sm text-muted-foreground">{f.desc}</p>
57
+ </div>
58
+ {/each}
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Navigation demo -->
63
+ <div class="space-y-4">
64
+ <h2 class="text-2xl font-semibold tracking-tight">Routes in this demo</h2>
65
+ <div class="flex flex-wrap gap-2">
66
+ {#each ["/", "/about", "/blog", "/blog/hello-world", "/blog/route-groups", "/all/foo/bar", "/missing-page"] as href}
67
+ <a
68
+ {href}
69
+ class="rounded-md border px-3 py-1.5 text-sm font-mono hover:bg-muted transition-colors"
70
+ >{href}</a>
71
+ {/each}
72
+ <a
73
+ href="/api/hello"
74
+ target="_blank"
75
+ class="rounded-md border px-3 py-1.5 text-sm font-mono hover:bg-muted transition-colors"
76
+ >/api/hello ↗</a>
77
+ </div>
78
+ </div>
79
+ </div>
@@ -0,0 +1 @@
1
+ export const prerender = true;
@@ -0,0 +1,31 @@
1
+ <svelte:head>
2
+ <title>About | {{PROJECT_NAME}}</title>
3
+ </svelte:head>
4
+
5
+ <div class="space-y-6 max-w-2xl">
6
+ <h1 class="text-4xl font-bold tracking-tight">About {{PROJECT_NAME}}</h1>
7
+ <p class="text-muted-foreground text-lg">
8
+ A minimalist fullstack framework built on Bun, ElysiaJS, and Svelte 5.
9
+ </p>
10
+
11
+ <ul class="space-y-2 text-foreground">
12
+ {#each [
13
+ "Bun runtime — fast builds, native TypeScript",
14
+ "ElysiaJS — HTTP server with type-safe routing",
15
+ "Svelte 5 Runes — fine-grained reactivity",
16
+ "Isomorphic SSR with client hydration",
17
+ "File-based routing (SvelteKit-compatible conventions)",
18
+ "Nested layouts and route groups (public), (auth), (admin)",
19
+ "Dynamic params [slug] and catch-all [...rest]",
20
+ "Server loaders with parent() data threading",
21
+ "Hooks — sequence() middleware for auth, logging, etc.",
22
+ "Component registry — bosbun add button",
23
+ "Feature registry — bosbun feat login",
24
+ ] as item}
25
+ <li class="flex items-start gap-2">
26
+ <span class="text-primary mt-0.5">✓</span>
27
+ <span>{item}</span>
28
+ </li>
29
+ {/each}
30
+ </ul>
31
+ </div>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ let { data = {} }: { data?: Record<string, any> } = $props();
3
+ const segments = $derived(
4
+ ((data as any)?.params?.catchall ?? "")
5
+ .split("/")
6
+ .filter(Boolean)
7
+ );
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>Catch-all Demo | {{PROJECT_NAME}}</title>
12
+ </svelte:head>
13
+
14
+ <div class="flex flex-col items-center justify-center py-24 text-center space-y-6">
15
+ <h1 class="text-3xl font-bold">Catch-all Route Demo</h1>
16
+ <p class="text-muted-foreground text-sm">
17
+ This page matches <code class="bg-muted rounded px-1 font-mono">/all/[...catchall]</code>
18
+ </p>
19
+
20
+ {#if segments.length > 0}
21
+ <div class="mt-2 space-y-1">
22
+ <p class="text-sm text-muted-foreground">Captured segments:</p>
23
+ <div class="flex gap-2 flex-wrap justify-center">
24
+ {#each segments as segment, i}
25
+ <span class="bg-muted rounded px-2 py-1 font-mono text-sm">
26
+ [{i}] {segment}
27
+ </span>
28
+ {/each}
29
+ </div>
30
+ </div>
31
+ {:else}
32
+ <p class="text-muted-foreground text-sm italic">No segments captured</p>
33
+ {/if}
34
+
35
+ <a href="/" class="mt-4 rounded-md border px-4 py-2 text-sm hover:bg-muted transition-colors">
36
+ Go Home
37
+ </a>
38
+ </div>
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ const posts = [
3
+ {
4
+ slug: "hello-world",
5
+ title: "Hello, World!",
6
+ date: "2026-03-05",
7
+ excerpt: "The first post in the demo — a quick intro to the framework.",
8
+ tags: ["intro", "bosbun"],
9
+ },
10
+ {
11
+ slug: "route-groups",
12
+ title: "Route Groups Explained",
13
+ date: "2026-03-04",
14
+ excerpt: "How (public), (auth), (admin) groups work — invisible in URLs, share layouts.",
15
+ tags: ["routing", "layouts"],
16
+ },
17
+ {
18
+ slug: "dynamic-params",
19
+ title: "Dynamic Params with [slug]",
20
+ date: "2026-03-03",
21
+ excerpt: "Using [slug] segments to match any value and pass it to server loaders.",
22
+ tags: ["routing", "dynamic"],
23
+ },
24
+ ];
25
+ </script>
26
+
27
+ <svelte:head>
28
+ <title>Blog | {{PROJECT_NAME}}</title>
29
+ </svelte:head>
30
+
31
+ <div class="space-y-8">
32
+ <div class="space-y-2">
33
+ <h1 class="text-4xl font-bold tracking-tight">Blog</h1>
34
+ <p class="text-muted-foreground">Routing patterns and framework internals.</p>
35
+ </div>
36
+
37
+ <div class="grid gap-4" data-bosbun-preload="hover">
38
+ {#each posts as post}
39
+ <a href="/blog/{post.slug}" class="group block rounded-lg border bg-card p-5 hover:border-primary transition-colors">
40
+ <div class="flex items-start justify-between gap-4">
41
+ <div class="space-y-1">
42
+ <p class="font-semibold group-hover:text-primary transition-colors">{post.title}</p>
43
+ <p class="text-xs text-muted-foreground font-mono">{post.date}</p>
44
+ </div>
45
+ <div class="flex gap-1 shrink-0">
46
+ {#each post.tags as tag}
47
+ <span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">{tag}</span>
48
+ {/each}
49
+ </div>
50
+ </div>
51
+ <p class="mt-2 text-sm text-muted-foreground">{post.excerpt}</p>
52
+ </a>
53
+ {/each}
54
+ </div>
55
+ </div>
@@ -0,0 +1,62 @@
1
+ import type { LoadEvent, MetadataEvent } from "bosbun";
2
+
3
+ const posts: Record<string, { title: string; date: string; tags: string[]; content: string }> = {
4
+ "hello-world": {
5
+ title: "Hello, World!",
6
+ date: "2026-03-05",
7
+ tags: ["intro", "bosbun"],
8
+ content: `Welcome to Bosbun! This page was loaded by a +page.server.ts file.
9
+
10
+ The slug param was extracted from the URL by the route matcher and passed to the load() function as params.slug.
11
+
12
+ This is standard SvelteKit-compatible server loading.`,
13
+ },
14
+ "route-groups": {
15
+ title: "Route Groups Explained",
16
+ date: "2026-03-04",
17
+ tags: ["routing", "layouts"],
18
+ content: `Route groups like (public), (auth), and (admin) are directory names that are invisible in the URL.
19
+
20
+ They let you share layouts across a set of routes without adding a URL segment. A directory named (public) applies its +layout.svelte to all routes inside it, but /public never appears in the browser URL.
21
+
22
+ This page lives at routes/(public)/blog/[slug]/+page.svelte but is served at /blog/route-groups.`,
23
+ },
24
+ "dynamic-params": {
25
+ title: "Dynamic Params with [slug]",
26
+ date: "2026-03-03",
27
+ tags: ["routing", "dynamic"],
28
+ content: `A directory named [slug] creates a dynamic route segment that matches any URL value.
29
+
30
+ The matched value is available as params.slug inside +page.server.ts load() and inside the page component via data.params.slug.
31
+
32
+ The route matcher uses 3-pass priority: exact matches first, then dynamic segments, then catch-all routes.`,
33
+ },
34
+ };
35
+
36
+ export function metadata({ params }: MetadataEvent) {
37
+ // In production this would be a DB query for the post
38
+ const post = posts[params.slug] ?? null;
39
+ return {
40
+ title: post ? `${post.title} — Blog` : `Post not found`,
41
+ description: post ? `A blog post about ${params.slug}` : undefined,
42
+ meta: post
43
+ ? [{ property: "og:title", content: post.title }]
44
+ : [],
45
+ // Pass fetched post to load() — avoids duplicate query
46
+ data: { post },
47
+ };
48
+ }
49
+
50
+ export async function load({ params, parent, metadata }: LoadEvent) {
51
+ // parent() gives us data from +layout.server.ts (appName, requestTime)
52
+ const parentData = await parent();
53
+
54
+ // Reuse post from metadata() — no duplicate DB query
55
+ const post = metadata?.post ?? posts[params.slug] ?? null;
56
+
57
+ return {
58
+ post,
59
+ slug: params.slug,
60
+ appName: parentData.appName as string,
61
+ };
62
+ }
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { PageData } from './$types';
3
+
4
+ let { data }: { data: PageData } = $props();
5
+
6
+ const post = $derived(data.post);
7
+ const slug = $derived(data.slug ?? data.params.slug ?? "");
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>{post ? post.title : "Post Not Found"} | {{PROJECT_NAME}}</title>
12
+ </svelte:head>
13
+
14
+ {#if post}
15
+ <article class="space-y-6 max-w-2xl">
16
+ <a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors">← Blog</a>
17
+
18
+ <div class="space-y-2">
19
+ <div class="flex flex-wrap gap-1">
20
+ {#each post.tags as tag}
21
+ <span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">{tag}</span>
22
+ {/each}
23
+ </div>
24
+ <h1 class="text-4xl font-bold tracking-tight">{post.title}</h1>
25
+ <p class="text-sm text-muted-foreground font-mono">{post.date}</p>
26
+ </div>
27
+
28
+ <hr class="border-border" />
29
+
30
+ <div class="space-y-4">
31
+ {#each post.content.split("\n\n") as paragraph}
32
+ <p class="text-foreground leading-relaxed">{paragraph}</p>
33
+ {/each}
34
+ </div>
35
+
36
+ <hr class="border-border" />
37
+
38
+ <!-- Route debug box — demonstrates params flowing from server to client -->
39
+ <div class="rounded-lg border bg-muted/40 p-4 space-y-1 text-sm font-mono">
40
+ <p class="font-sans text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">Route debug</p>
41
+ <p><span class="text-muted-foreground">pattern: </span>/blog/[slug]</p>
42
+ <p><span class="text-muted-foreground">params.slug: </span><span class="text-primary font-semibold">{slug}</span></p>
43
+ <p><span class="text-muted-foreground">loaded by: </span>+page.server.ts</p>
44
+ <p><span class="text-muted-foreground">parent data: </span>{data.appName}</p>
45
+ </div>
46
+ </article>
47
+ {:else}
48
+ <div class="flex flex-col items-center justify-center py-20 text-center space-y-4">
49
+ <p class="text-7xl font-bold text-destructive">404</p>
50
+ <p class="text-xl text-muted-foreground">Post "<span class="font-mono">{slug}</span>" not found.</p>
51
+ <a href="/blog" class="rounded-md border px-4 py-2 text-sm hover:bg-muted transition-colors">Back to Blog</a>
52
+ </div>
53
+ {/if}
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ let { data }: { data: { status: number; message: string } } = $props();
3
+ </script>
4
+
5
+ <svelte:head>
6
+ <title>{data.status} — {data.message}</title>
7
+ </svelte:head>
8
+
9
+ <div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
10
+ <p class="text-8xl font-bold text-gray-200">{data.status}</p>
11
+ <p class="text-2xl font-semibold text-gray-700">{data.message}</p>
12
+ <a href="/" class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors">
13
+ Go home
14
+ </a>
15
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { LoadEvent } from "bosbun";
2
+
3
+ // Data returned here is available to all child loaders via parent()
4
+ // and to all layouts via the `data` prop.
5
+ export async function load({ locals }: LoadEvent) {
6
+ return {
7
+ appName: "{{PROJECT_NAME}}",
8
+ requestTime: locals.requestTime as number | null ?? null,
9
+ };
10
+ }
@@ -0,0 +1,6 @@
1
+ <script lang="ts">
2
+ import "../app.css";
3
+ let { children }: { children: any } = $props();
4
+ </script>
5
+
6
+ {@render children()}
@@ -0,0 +1,28 @@
1
+ import { fail, redirect } from "bosbun";
2
+ import type { RequestEvent } from "bosbun";
3
+
4
+ export async function load() {
5
+ return { greeting: "Test form actions" };
6
+ }
7
+
8
+ export const actions = {
9
+ default: async ({ request }: RequestEvent) => {
10
+ const data = await request.formData();
11
+ const email = data.get("email") as string;
12
+ const name = data.get("name") as string;
13
+
14
+ const errors: Record<string, string> = {};
15
+ if (!email) errors.email = "Email is required";
16
+ if (!name) errors.name = "Name is required";
17
+
18
+ if (Object.keys(errors).length > 0) {
19
+ return fail(400, { email, name, errors });
20
+ }
21
+
22
+ return { success: true, email, name };
23
+ },
24
+
25
+ reset: async () => {
26
+ return { cleared: true };
27
+ },
28
+ };
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { PageData, ActionData } from './$types';
3
+ let { data, form }: { data: PageData; form: ActionData } = $props();
4
+ </script>
5
+
6
+ <div class="max-w-md mx-auto mt-10 p-6">
7
+ <h1 class="text-2xl font-bold mb-4">{data.greeting}</h1>
8
+
9
+ {#if form?.success}
10
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
11
+ Welcome, {form.name} ({form.email})!
12
+ </div>
13
+ {/if}
14
+
15
+ {#if form?.cleared}
16
+ <div class="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded mb-4">
17
+ Form cleared.
18
+ </div>
19
+ {/if}
20
+
21
+ <form method="POST" class="space-y-4">
22
+ <div>
23
+ <label for="name" class="block text-sm font-medium">Name</label>
24
+ <input
25
+ id="name"
26
+ name="name"
27
+ type="text"
28
+ value={form?.name ?? ''}
29
+ class="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
30
+ />
31
+ {#if form?.errors?.name}
32
+ <p class="text-red-500 text-sm mt-1">{form.errors.name}</p>
33
+ {/if}
34
+ </div>
35
+
36
+ <div>
37
+ <label for="email" class="block text-sm font-medium">Email</label>
38
+ <input
39
+ id="email"
40
+ name="email"
41
+ type="text"
42
+ value={form?.email ?? ''}
43
+ class="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
44
+ />
45
+ {#if form?.errors?.email}
46
+ <p class="text-red-500 text-sm mt-1">{form.errors.email}</p>
47
+ {/if}
48
+ </div>
49
+
50
+ <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
51
+ Submit
52
+ </button>
53
+ </form>
54
+
55
+ <form method="POST" action="?/reset" class="mt-4">
56
+ <button type="submit" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
57
+ Reset (named action)
58
+ </button>
59
+ </form>
60
+ </div>
@@ -0,0 +1,44 @@
1
+ import type { RequestEvent } from "bosbun";
2
+
3
+ export function GET({ params, locals }: RequestEvent) {
4
+ return Response.json({
5
+ method: "GET",
6
+ message: "Hello from Bosbun API!",
7
+ params,
8
+ locals: {
9
+ requestTime: locals.requestTime ?? null,
10
+ user: locals.user ?? null,
11
+ },
12
+ });
13
+ }
14
+
15
+ export async function POST({ request, locals }: RequestEvent) {
16
+ const body = await request.json().catch(() => ({}));
17
+ return Response.json({ method: "POST", received: body, locals });
18
+ }
19
+
20
+ export async function PUT({ request, locals }: RequestEvent) {
21
+ const body = await request.json().catch(() => ({}));
22
+ return Response.json({ method: "PUT", received: body, locals });
23
+ }
24
+
25
+ export async function PATCH({ request, locals }: RequestEvent) {
26
+ const body = await request.json().catch(() => ({}));
27
+ return Response.json({ method: "PATCH", received: body, locals });
28
+ }
29
+
30
+ export function DELETE({ params, locals }: RequestEvent) {
31
+ return Response.json({ method: "DELETE", deleted: true, params, locals });
32
+ }
33
+
34
+ export function OPTIONS() {
35
+ return new Response(null, {
36
+ status: 204,
37
+ headers: {
38
+ Allow: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
39
+ "Access-Control-Allow-Origin": "*",
40
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
41
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "allowJs": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "noEmit": true,
11
+ "verbatimModuleSyntax": true,
12
+ "types": ["bun-types"],
13
+ "lib": ["dom", "dom.iterable", "esnext"],
14
+ "rootDirs": [".", ".bosbun/types"],
15
+ "paths": {
16
+ "$lib": ["./src/lib"],
17
+ "$lib/*": ["./src/lib/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }