bosia 0.1.2 → 0.1.3

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add.ts +46 -4
  3. package/src/cli/create.ts +36 -8
  4. package/src/cli/feat.ts +136 -12
  5. package/src/cli/index.ts +4 -2
  6. package/src/core/plugin.ts +4 -2
  7. package/src/core/routeFile.ts +45 -0
  8. package/templates/default/src/routes/+error.svelte +4 -4
  9. package/templates/demo/src/routes/+error.svelte +4 -4
  10. package/templates/drizzle/.env.example +2 -0
  11. package/templates/drizzle/README.md +69 -0
  12. package/templates/drizzle/drizzle.config.ts +10 -0
  13. package/templates/drizzle/instructions.txt +3 -0
  14. package/templates/drizzle/package.json +26 -0
  15. package/templates/drizzle/public/.gitkeep +0 -0
  16. package/templates/drizzle/public/favicon.svg +14 -0
  17. package/templates/drizzle/src/app.css +132 -0
  18. package/templates/drizzle/src/app.d.ts +14 -0
  19. package/templates/drizzle/src/features/drizzle/index.ts +15 -0
  20. package/templates/drizzle/src/features/drizzle/migrations/.gitkeep +0 -0
  21. package/templates/drizzle/src/features/drizzle/schemas.ts +1 -0
  22. package/templates/drizzle/src/features/drizzle/seeds/001_initial_todos.ts +11 -0
  23. package/templates/drizzle/src/features/drizzle/seeds/runner.ts +80 -0
  24. package/templates/drizzle/src/features/todo/index.ts +3 -0
  25. package/templates/drizzle/src/features/todo/queries.ts +36 -0
  26. package/templates/drizzle/src/features/todo/schemas/todo.table.ts +9 -0
  27. package/templates/drizzle/src/features/todo/types.ts +5 -0
  28. package/templates/drizzle/src/hooks.server.ts +20 -0
  29. package/templates/drizzle/src/lib/components/todo/index.ts +3 -0
  30. package/templates/drizzle/src/lib/components/todo/todo-form.svelte +23 -0
  31. package/templates/drizzle/src/lib/components/todo/todo-item.svelte +63 -0
  32. package/templates/drizzle/src/lib/components/todo/todo-list.svelte +21 -0
  33. package/templates/drizzle/src/lib/utils.ts +1 -0
  34. package/templates/drizzle/src/routes/+error.svelte +15 -0
  35. package/templates/drizzle/src/routes/+layout.server.ts +8 -0
  36. package/templates/drizzle/src/routes/+layout.svelte +6 -0
  37. package/templates/drizzle/src/routes/+page.svelte +55 -0
  38. package/templates/drizzle/src/routes/api/todos/+server.ts +18 -0
  39. package/templates/drizzle/src/routes/api/todos/[id]/+server.ts +42 -0
  40. package/templates/drizzle/src/routes/todos/+page.server.ts +52 -0
  41. package/templates/drizzle/src/routes/todos/+page.svelte +39 -0
  42. package/templates/drizzle/tsconfig.json +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/cli/add.ts CHANGED
@@ -21,11 +21,23 @@ interface ComponentMeta {
21
21
  npmDeps: Record<string, string>;
22
22
  }
23
23
 
24
+ interface RegistryIndex {
25
+ components: string[];
26
+ features: string[];
27
+ }
28
+
24
29
  // Track already-installed components within a session to avoid re-running deps
25
30
  const installed = new Set<string>();
26
31
 
27
- // Resolved once in runAdd, used by addComponent
32
+ // Resolved once in runAdd or initAddRegistry, used by addComponent
28
33
  let registryRoot: string | null = null;
34
+ let registryIndex: RegistryIndex | null = null;
35
+
36
+ /** Initialize registry context so addComponent can be called externally (e.g. from feat.ts) */
37
+ export async function initAddRegistry(root: string | null) {
38
+ registryRoot = root;
39
+ registryIndex = await loadIndex();
40
+ }
29
41
 
30
42
  export async function runAdd(name: string | undefined, flags: string[] = []) {
31
43
  if (!name) {
@@ -39,17 +51,47 @@ export async function runAdd(name: string | undefined, flags: string[] = []) {
39
51
  console.log(`⬡ Using local registry: ${registryRoot}\n`);
40
52
  }
41
53
 
54
+ // Load index once to resolve component paths
55
+ registryIndex = await loadIndex();
56
+
42
57
  ensureUtils();
43
58
  await addComponent(name, true);
44
59
  }
45
60
 
46
61
  /**
47
- * Resolve the destination path for a component.
48
- * - "button" → "ui/button" (default ui/ prefix)
62
+ * Resolve the full registry path for a component using the index.
63
+ * - "todo" → "todo" (exact match in index)
64
+ * - "button" → "ui/button" (suffix match in index)
49
65
  * - "shop/cart" → "shop/cart" (explicit path used as-is)
50
66
  */
51
67
  function resolveDestPath(name: string): string {
52
- return name.includes("/") ? name : `ui/${name}`;
68
+ if (name.includes("/")) return name;
69
+
70
+ if (registryIndex) {
71
+ // Exact match (e.g. "todo" → "todo")
72
+ if (registryIndex.components.includes(name)) return name;
73
+ // Suffix match (e.g. "button" → "ui/button")
74
+ const match = registryIndex.components.find(
75
+ (c) => c.endsWith(`/${name}`)
76
+ );
77
+ if (match) return match;
78
+ }
79
+
80
+ // Fallback for backwards compatibility
81
+ return `ui/${name}`;
82
+ }
83
+
84
+ async function loadIndex(): Promise<RegistryIndex | null> {
85
+ try {
86
+ if (registryRoot) {
87
+ const path = join(registryRoot, "index.json");
88
+ if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
89
+ return null;
90
+ }
91
+ return await fetchJSON<RegistryIndex>(`${REMOTE_BASE}/index.json`);
92
+ } catch {
93
+ return null;
94
+ }
53
95
  }
54
96
 
55
97
  export async function addComponent(name: string, root = false) {
package/src/cli/create.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve, join, basename } from "path";
1
+ import { resolve, join, basename, relative } from "path";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { spawn } from "bun";
4
4
  import * as p from "@clack/prompts";
@@ -12,6 +12,7 @@ const BOSIA_VERSION: string = BOSIA_PKG.version;
12
12
  const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
13
13
  default: "Minimal starter with routing and Tailwind",
14
14
  demo: "Full-featured demo with hooks, API routes, form actions, and more",
15
+ drizzle: "PostgreSQL + Drizzle ORM with full CRUD todo demo",
15
16
  };
16
17
 
17
18
  export async function runCreate(name: string | undefined, args: string[] = []) {
@@ -34,6 +35,9 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
34
35
  template = args[templateIdx + 1];
35
36
  }
36
37
 
38
+ // Parse --local flag
39
+ const isLocal = args.includes("--local");
40
+
37
41
  // If no --template flag, prompt interactively
38
42
  if (!template) {
39
43
  template = await promptTemplate();
@@ -49,7 +53,11 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
49
53
 
50
54
  console.log(`\n⬡ Creating Bosia project: ${basename(targetDir)} (template: ${template})\n`);
51
55
 
52
- copyDir(templateDir, targetDir, name);
56
+ copyDir(templateDir, targetDir, name, isLocal);
57
+
58
+ if (existsSync(join(targetDir, ".env.example"))) {
59
+ writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
60
+ }
53
61
 
54
62
  console.log(`✅ Project created at ${targetDir}\n`);
55
63
 
@@ -63,7 +71,15 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
63
71
  if (exitCode !== 0) {
64
72
  console.warn("⚠️ bun install failed — run it manually.");
65
73
  } else {
66
- console.log(`\n🎉 Ready!\n\n cd ${name}\n bun x bosia dev\n`);
74
+ console.log(`\n🎉 Ready!\n\ncd ${name}`);
75
+
76
+ const instPath = join(templateDir, "instructions.txt");
77
+ if (existsSync(instPath)) {
78
+ const instructions = readFileSync(instPath, "utf-8").trimEnd();
79
+ if (instructions) console.log(instructions);
80
+ }
81
+
82
+ console.log(`bun x bosia dev\n`);
67
83
  }
68
84
  }
69
85
 
@@ -96,17 +112,29 @@ async function promptTemplate(): Promise<string> {
96
112
  return selected as string;
97
113
  }
98
114
 
99
- function copyDir(src: string, dest: string, projectName: string) {
115
+ function copyDir(src: string, dest: string, projectName: string, isLocal: boolean) {
100
116
  mkdirSync(dest, { recursive: true });
101
117
  for (const entry of readdirSync(src, { withFileTypes: true })) {
102
118
  const srcPath = join(src, entry.name);
103
119
  const destPath = join(dest, entry.name);
120
+
121
+ // Do not copy instructions.txt to the final project
122
+ if (entry.name === "instructions.txt") continue;
123
+
104
124
  if (entry.isDirectory()) {
105
- copyDir(srcPath, destPath, projectName);
125
+ copyDir(srcPath, destPath, projectName, isLocal);
106
126
  } else {
107
- const content = readFileSync(srcPath, "utf-8")
108
- .replaceAll("{{PROJECT_NAME}}", projectName)
109
- .replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
127
+ let content = readFileSync(srcPath, "utf-8")
128
+ .replaceAll("{{PROJECT_NAME}}", projectName);
129
+
130
+ if (entry.name === "package.json" && isLocal) {
131
+ const bosiaPath = resolve(import.meta.dir, "../../");
132
+ const relPath = relative(dest, bosiaPath);
133
+ content = content.replaceAll("\"^{{BOSIA_VERSION}}\"", `"file:${relPath}"`);
134
+ } else {
135
+ content = content.replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
136
+ }
137
+
110
138
  writeFileSync(destPath, content, "utf-8");
111
139
  }
112
140
  }
package/src/cli/feat.ts CHANGED
@@ -1,32 +1,64 @@
1
1
  import { join, dirname } from "path";
2
- import { mkdirSync, writeFileSync } from "fs";
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
3
  import { spawn } from "bun";
4
- import { addComponent } from "./add.ts";
4
+ import * as p from "@clack/prompts";
5
+ import { addComponent, initAddRegistry } from "./add.ts";
5
6
 
6
- // ─── bosia feat <feature> ─────────────────────────────────
7
- // Fetches a feature scaffold from the GitHub registry.
8
- // Installs required components, copies route/lib files, installs npm deps.
7
+ // ─── bosia feat <feature> [--local] ──────────────────────
8
+ // Fetches a feature scaffold from the GitHub registry (or local
9
+ // registry with --local) and copies route/lib files, installs npm deps.
10
+ // Supports nested feature dependencies (e.g. todo → drizzle).
9
11
 
10
12
  const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
11
13
 
12
14
  interface FeatureMeta {
13
15
  name: string;
14
16
  description: string;
17
+ features?: string[]; // other bosia features required
15
18
  components: string[]; // bosia components to install via `bosia add`
16
19
  files: string[]; // source filenames in the registry feature dir
17
20
  targets: string[]; // destination paths relative to project root
18
21
  npmDeps: Record<string, string>;
22
+ scripts?: Record<string, string>; // package.json scripts to add
23
+ envVars?: Record<string, string>; // env vars to append to .env if missing
19
24
  }
20
25
 
21
- export async function runFeat(name: string | undefined) {
26
+ let registryRoot: string | null = null;
27
+
28
+ // Track installed features to prevent circular dependencies
29
+ const installedFeats = new Set<string>();
30
+
31
+ export async function runFeat(name: string | undefined, flags: string[] = []) {
22
32
  if (!name) {
23
- console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature>");
33
+ console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature> [--local]");
24
34
  process.exit(1);
25
35
  }
26
36
 
27
- console.log(`⬡ Installing feature: ${name}\n`);
37
+ if (flags.includes("--local")) {
38
+ registryRoot = resolveLocalRegistry();
39
+ console.log(`⬡ Using local registry: ${registryRoot}\n`);
40
+ }
28
41
 
29
- const meta = await fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
42
+ // Initialize add.ts registry context so addComponent resolves paths correctly
43
+ await initAddRegistry(registryRoot);
44
+
45
+ await installFeature(name, true);
46
+ }
47
+
48
+ async function installFeature(name: string, isRoot: boolean) {
49
+ if (installedFeats.has(name)) return;
50
+ installedFeats.add(name);
51
+
52
+ console.log(isRoot ? `⬡ Installing feature: ${name}\n` : `\n⬡ Installing dependency feature: ${name}\n`);
53
+
54
+ const meta = await readMeta(name);
55
+
56
+ // Install required feature dependencies first (recursive)
57
+ if (meta.features && meta.features.length > 0) {
58
+ for (const feat of meta.features) {
59
+ await installFeature(feat, false);
60
+ }
61
+ }
30
62
 
31
63
  // Install required UI components
32
64
  if (meta.components.length > 0) {
@@ -41,8 +73,20 @@ export async function runFeat(name: string | undefined) {
41
73
  for (let i = 0; i < meta.files.length; i++) {
42
74
  const file = meta.files[i]!;
43
75
  const target = meta.targets[i] ?? file;
44
- const content = await fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
45
76
  const dest = join(process.cwd(), target);
77
+
78
+ // Prompt before overwriting existing files
79
+ if (existsSync(dest)) {
80
+ const replace = await p.confirm({
81
+ message: `File "${target}" already exists. Replace it?`,
82
+ });
83
+ if (p.isCancel(replace) || !replace) {
84
+ console.log(` ⏭️ Skipped ${target}`);
85
+ continue;
86
+ }
87
+ }
88
+
89
+ const content = await readFile(name, file);
46
90
  mkdirSync(dirname(dest), { recursive: true });
47
91
  writeFileSync(dest, content, "utf-8");
48
92
  console.log(` ✍️ ${target}`);
@@ -63,8 +107,88 @@ export async function runFeat(name: string | undefined) {
63
107
  }
64
108
  }
65
109
 
66
- console.log(`\n✅ Feature "${name}" scaffolded!`);
67
- if (meta.description) console.log(` ${meta.description}`);
110
+ // Add package.json scripts
111
+ const scriptEntries = Object.entries(meta.scripts ?? {});
112
+ if (scriptEntries.length > 0) {
113
+ const pkgPath = join(process.cwd(), "package.json");
114
+ if (existsSync(pkgPath)) {
115
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
116
+ pkg.scripts = pkg.scripts ?? {};
117
+ const added: string[] = [];
118
+ for (const [key, val] of scriptEntries) {
119
+ if (!pkg.scripts[key]) {
120
+ pkg.scripts[key] = val;
121
+ added.push(key);
122
+ }
123
+ }
124
+ if (added.length > 0) {
125
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
126
+ console.log(`\n📜 Added scripts: ${added.join(", ")}`);
127
+ }
128
+ }
129
+ }
130
+
131
+ // Append env vars to .env if missing
132
+ const envEntries = Object.entries(meta.envVars ?? {});
133
+ if (envEntries.length > 0) {
134
+ const envPath = join(process.cwd(), ".env");
135
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
136
+ const toAdd: string[] = [];
137
+ for (const [key, val] of envEntries) {
138
+ if (!existing.includes(`${key}=`)) {
139
+ toAdd.push(`${key}=${val}`);
140
+ }
141
+ }
142
+ if (toAdd.length > 0) {
143
+ const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
144
+ writeFileSync(envPath, existing + nl + toAdd.join("\n") + "\n", "utf-8");
145
+ console.log(`\n🔑 Added to .env: ${toAdd.map((l) => l.split("=")[0]).join(", ")}`);
146
+ }
147
+ }
148
+
149
+ if (isRoot) {
150
+ console.log(`\n✅ Feature "${name}" scaffolded!`);
151
+ if (meta.description) console.log(` ${meta.description}`);
152
+ } else {
153
+ console.log(` ✅ Dependency feature "${name}" installed.`);
154
+ }
155
+ }
156
+
157
+ // ─── Registry resolvers ──────────────────────────────────────
158
+
159
+ function resolveLocalRegistry(): string {
160
+ let dir = dirname(new URL(import.meta.url).pathname);
161
+ for (let i = 0; i < 10; i++) {
162
+ const candidate = join(dir, "registry");
163
+ if (existsSync(join(candidate, "index.json"))) return candidate;
164
+ const parent = dirname(dir);
165
+ if (parent === dir) break;
166
+ dir = parent;
167
+ }
168
+ console.error("❌ Could not find local registry/ directory.");
169
+ process.exit(1);
170
+ }
171
+
172
+ async function readMeta(name: string): Promise<FeatureMeta> {
173
+ if (registryRoot) {
174
+ const path = join(registryRoot, "features", name, "meta.json");
175
+ if (!existsSync(path)) {
176
+ throw new Error(`Feature "${name}" not found in local registry`);
177
+ }
178
+ return JSON.parse(readFileSync(path, "utf-8"));
179
+ }
180
+ return fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
181
+ }
182
+
183
+ async function readFile(name: string, file: string): Promise<string> {
184
+ if (registryRoot) {
185
+ const path = join(registryRoot, "features", name, file);
186
+ if (!existsSync(path)) {
187
+ throw new Error(`File "${file}" not found for feature "${name}" in local registry`);
188
+ }
189
+ return readFileSync(path, "utf-8");
190
+ }
191
+ return fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
68
192
  }
69
193
 
70
194
  async function fetchJSON<T>(url: string): Promise<T> {
package/src/cli/index.ts CHANGED
@@ -40,7 +40,9 @@ async function main() {
40
40
  }
41
41
  case "feat": {
42
42
  const { runFeat } = await import("./feat.ts");
43
- await runFeat(args[0]);
43
+ const featName = args.find((a) => !a.startsWith("--"));
44
+ const featFlags = args.filter((a) => a.startsWith("--"));
45
+ await runFeat(featName, featFlags);
44
46
  break;
45
47
  }
46
48
  default: {
@@ -56,7 +58,7 @@ Commands:
56
58
  build Build for production
57
59
  start Run the production server
58
60
  add <component> Add a UI component from the registry
59
- feat <feature> Add a feature scaffold from the registry
61
+ feat <feature> Add a feature scaffold from the registry [--local]
60
62
 
61
63
  Examples:
62
64
  bosia create my-app
@@ -22,9 +22,11 @@ export function makeBosiaPlugin(target: "browser" | "bun" = "bun") {
22
22
  return {
23
23
  name: "bosia-resolver",
24
24
  setup(build: import("bun").PluginBuilder) {
25
- // bosia:routes → .bosia/routes.ts
25
+ // bosia:routes → .bosia/routes.client.ts (browser) or .bosia/routes.ts (server)
26
+ // Client-only file excludes serverRoutes/apiRoutes to prevent the browser
27
+ // bundler from following server-side dynamic imports into Node builtins.
26
28
  build.onResolve({ filter: /^bosia:routes$/ }, () => ({
27
- path: join(process.cwd(), ".bosia", "routes.ts"),
29
+ path: join(process.cwd(), ".bosia", target === "browser" ? "routes.client.ts" : "routes.ts"),
28
30
  }));
29
31
 
30
32
  // $env → .bosia/env.client.ts (browser) or .bosia/env.server.ts (bun)
@@ -101,11 +101,56 @@ export function generateRoutesFile(manifest: RouteManifest): void {
101
101
 
102
102
  mkdirSync(".bosia", { recursive: true });
103
103
  writeFileSync(".bosia/routes.ts", lines.join("\n"));
104
+
105
+ // Generate client-only routes file (no server imports that could pull in Node builtins)
106
+ generateClientRoutesFile(pages, manifest.errorPage);
107
+
104
108
  const pagePatterns = pages.map(p => p.pattern).join(", ") || "(none)";
105
109
  console.log(`✅ Routes generated: .bosia/routes.ts`);
106
110
  console.log(` Found ${pages.length} page route(s): ${pagePatterns}`);
107
111
  }
108
112
 
113
+ // ─── Client-only routes file ─────────────────────────────
114
+ // Separate file with only clientRoutes + errorPage to prevent
115
+ // the browser bundler from following server-side dynamic imports
116
+ // (e.g. +page.server.ts → drizzle-orm → postgres → Node builtins).
117
+
118
+ function generateClientRoutesFile(
119
+ pages: RouteManifest["pages"],
120
+ errorPage: RouteManifest["errorPage"],
121
+ ): void {
122
+ const lines: string[] = [
123
+ "// AUTO-GENERATED by bosia build — client-only routes (no server imports)\n",
124
+ ];
125
+
126
+ lines.push("export const clientRoutes: Array<{");
127
+ lines.push(" pattern: string;");
128
+ lines.push(" page: () => Promise<any>;");
129
+ lines.push(" layouts: (() => Promise<any>)[];");
130
+ lines.push(" hasServerData: boolean;");
131
+ lines.push("}> = [");
132
+ for (const r of pages) {
133
+ const layoutImports = r.layouts
134
+ .map(l => `() => import(${JSON.stringify(toImportPath(l))})`)
135
+ .join(", ");
136
+ const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
137
+ lines.push(" {");
138
+ lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
139
+ lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
140
+ lines.push(` layouts: [${layoutImports}],`);
141
+ lines.push(` hasServerData: ${hasServerData},`);
142
+ lines.push(" },");
143
+ }
144
+ lines.push("];\n");
145
+
146
+ const ep = errorPage;
147
+ lines.push(`export const errorPage: (() => Promise<any>) | null = ${
148
+ ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
149
+ };\n`);
150
+
151
+ writeFileSync(".bosia/routes.client.ts", lines.join("\n"));
152
+ }
153
+
109
154
  // Import path from .bosia/routes.ts to src/routes/<routePath>
110
155
  function toImportPath(routePath: string): string {
111
156
  return "../src/routes/" + routePath.replace(/\\/g, "/");
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
- let { data }: { data: { status: number; message: string } } = $props();
2
+ let { error }: { error: { status: number; message: string } } = $props();
3
3
  </script>
4
4
 
5
5
  <svelte:head>
6
- <title>{data.status} — {data.message}</title>
6
+ <title>{error.status} — {error.message}</title>
7
7
  </svelte:head>
8
8
 
9
9
  <main class="flex min-h-screen 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">{data.message}</p>
10
+ <p class="text-8xl font-bold text-gray-200">{error.status}</p>
11
+ <p class="text-2xl font-semibold">{error.message}</p>
12
12
  <a
13
13
  href="/"
14
14
  class="mt-4 rounded-md border px-4 py-2 text-sm hover:bg-muted transition-colors"
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
- let { data }: { data: { status: number; message: string } } = $props();
2
+ let { error }: { error: { status: number; message: string } } = $props();
3
3
  </script>
4
4
 
5
5
  <svelte:head>
6
- <title>{data.status} — {data.message}</title>
6
+ <title>{error.status} — {error.message}</title>
7
7
  </svelte:head>
8
8
 
9
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>
10
+ <p class="text-8xl font-bold text-gray-200">{error.status}</p>
11
+ <p class="text-2xl font-semibold text-gray-700">{error.message}</p>
12
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
13
  Go home
14
14
  </a>
@@ -0,0 +1,2 @@
1
+ # Database
2
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
@@ -0,0 +1,69 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A fullstack app built with [Bosia](https://github.com/bosapi/bosia) + Drizzle ORM + PostgreSQL.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) v1.1+
8
+ - [PostgreSQL](https://www.postgresql.org/) running locally or remotely
9
+
10
+ ## Getting Started
11
+
12
+ ```bash
13
+ # Copy env and set your DATABASE_URL
14
+ cp .env.example .env
15
+
16
+ # Push schema to database
17
+ bun run db:push
18
+
19
+ # Seed initial data
20
+ bun run db:seed
21
+
22
+ # Start dev server
23
+ bun x bosia dev
24
+ ```
25
+
26
+ Visit [http://localhost:9000](http://localhost:9000) to see the app.
27
+
28
+ ## Scripts
29
+
30
+ | Command | Description |
31
+ |---------|-------------|
32
+ | `bun x bosia dev` | Start dev server with HMR |
33
+ | `bun x bosia build` | Production build |
34
+ | `bun x bosia start` | Start production server |
35
+ | `bun run db:generate` | Generate migration from schema changes |
36
+ | `bun run db:migrate` | Apply pending migrations |
37
+ | `bun run db:push` | Push schema directly (dev shortcut) |
38
+ | `bun run db:studio` | Open Drizzle Studio GUI |
39
+ | `bun run db:seed` | Run pending seed files |
40
+
41
+ ## Project Structure
42
+
43
+ ```
44
+ src/
45
+ ├── features/
46
+ │ ├── drizzle/ # DB infrastructure
47
+ │ │ ├── index.ts # Connection singleton
48
+ │ │ ├── schemas.ts # Schema aggregator
49
+ │ │ ├── migrations/ # Drizzle Kit output
50
+ │ │ └── seeds/ # Seed files + runner
51
+ │ └── todo/ # Business feature
52
+ │ ├── schemas/ # Table definitions
53
+ │ ├── queries.ts # Typed CRUD
54
+ │ └── types.ts # Inferred types
55
+ ├── lib/components/todo/ # UI components
56
+ └── routes/
57
+ ├── todos/ # CRUD page with form actions
58
+ └── api/todos/ # REST API
59
+ ```
60
+
61
+ ## Adding Features
62
+
63
+ ```bash
64
+ # Add DB support to any Bosia app
65
+ bosia feat drizzle
66
+
67
+ # Add the todo feature (requires drizzle)
68
+ bosia feat todo
69
+ ```
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/features/drizzle/schemas.ts",
5
+ out: "./src/features/drizzle/migrations",
6
+ dialect: "postgresql",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,3 @@
1
+ Update .env with your DATABASE_URL
2
+ bun run db:generate
3
+ bun run db:migrate
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bosia dev",
7
+ "build": "bosia build",
8
+ "start": "bosia start",
9
+ "db:generate": "drizzle-kit generate",
10
+ "db:migrate": "drizzle-kit migrate",
11
+ "db:seed": "bun run src/features/drizzle/seeds/runner.ts"
12
+ },
13
+ "dependencies": {
14
+ "bosia": "^{{BOSIA_VERSION}}",
15
+ "svelte": "^5.20.0",
16
+ "clsx": "^2.1.1",
17
+ "tailwind-merge": "^3.5.0",
18
+ "drizzle-orm": "^0.44.0",
19
+ "postgres": "^3.4.5"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "typescript": "^5",
24
+ "drizzle-kit": "^0.31.0"
25
+ }
26
+ }
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>