bosia 0.1.6 → 0.1.8

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add.ts +29 -75
  3. package/src/cli/create.ts +32 -5
  4. package/src/cli/feat.ts +55 -93
  5. package/src/cli/registry.ts +168 -0
  6. package/src/core/cookies.ts +31 -15
  7. package/src/core/server.ts +1 -1
  8. package/templates/drizzle/package.json +3 -9
  9. package/templates/drizzle/template.json +3 -0
  10. package/templates/drizzle/drizzle.config.ts +0 -10
  11. package/templates/drizzle/src/features/drizzle/index.ts +0 -15
  12. package/templates/drizzle/src/features/drizzle/migrations/.gitkeep +0 -0
  13. package/templates/drizzle/src/features/drizzle/schemas.ts +0 -1
  14. package/templates/drizzle/src/features/drizzle/seeds/001_initial_todos.ts +0 -11
  15. package/templates/drizzle/src/features/drizzle/seeds/runner.ts +0 -80
  16. package/templates/drizzle/src/features/todo/index.ts +0 -3
  17. package/templates/drizzle/src/features/todo/queries.ts +0 -36
  18. package/templates/drizzle/src/features/todo/schemas/todo.table.ts +0 -9
  19. package/templates/drizzle/src/features/todo/types.ts +0 -5
  20. package/templates/drizzle/src/lib/components/todo/index.ts +0 -3
  21. package/templates/drizzle/src/lib/components/todo/todo-form.svelte +0 -23
  22. package/templates/drizzle/src/lib/components/todo/todo-item.svelte +0 -63
  23. package/templates/drizzle/src/lib/components/todo/todo-list.svelte +0 -21
  24. package/templates/drizzle/src/routes/api/todos/+server.ts +0 -18
  25. package/templates/drizzle/src/routes/api/todos/[id]/+server.ts +0 -42
  26. package/templates/drizzle/src/routes/todos/+page.server.ts +0 -52
  27. package/templates/drizzle/src/routes/todos/+page.svelte +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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
@@ -1,7 +1,15 @@
1
1
  import { join, dirname } from "path";
2
2
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
- import { spawn } from "bun";
4
3
  import * as p from "@clack/prompts";
4
+ import {
5
+ type InstallOptions,
6
+ REGISTRY_URL,
7
+ resolveLocalRegistryOrExit,
8
+ readRegistryJSON,
9
+ readRegistryFile,
10
+ mergePkgJson,
11
+ bunAdd,
12
+ } from "./registry.ts";
5
13
 
6
14
  // ─── bosia add <component> ────────────────────────────────
7
15
  // Fetches a component from the GitHub registry (or local registry
@@ -11,8 +19,6 @@ import * as p from "@clack/prompts";
11
19
  // bosia add button → src/lib/components/ui/button/
12
20
  // bosia add shop/cart → src/lib/components/shop/cart/
13
21
 
14
- const REMOTE_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
15
-
16
22
  interface ComponentMeta {
17
23
  name: string;
18
24
  description: string;
@@ -46,8 +52,7 @@ export async function runAdd(name: string | undefined, flags: string[] = []) {
46
52
  }
47
53
 
48
54
  if (flags.includes("--local")) {
49
- // Walk up from this file to find the repo's registry/ directory
50
- registryRoot = resolveLocalRegistry();
55
+ registryRoot = resolveLocalRegistryOrExit();
51
56
  console.log(`⬡ Using local registry: ${registryRoot}\n`);
52
57
  }
53
58
 
@@ -88,31 +93,35 @@ async function loadIndex(): Promise<RegistryIndex | null> {
88
93
  if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
89
94
  return null;
90
95
  }
91
- return await fetchJSON<RegistryIndex>(`${REMOTE_BASE}/index.json`);
96
+ const res = await fetch(`${REGISTRY_URL}/index.json`);
97
+ if (!res.ok) return null;
98
+ return await res.json() as RegistryIndex;
92
99
  } catch {
93
100
  return null;
94
101
  }
95
102
  }
96
103
 
97
- export async function addComponent(name: string, root = false) {
104
+ export async function addComponent(name: string, root = false, options?: InstallOptions) {
98
105
  // Resolve the full path (e.g. "button" → "ui/button", "shop/cart" stays "shop/cart")
99
106
  const fullPath = resolveDestPath(name);
100
107
 
101
108
  if (installed.has(fullPath)) return;
102
109
  installed.add(fullPath);
103
110
 
111
+ const cwd = options?.cwd ?? process.cwd();
112
+
104
113
  console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
105
114
 
106
- const meta = await readMeta(fullPath);
115
+ const meta = await readRegistryJSON<ComponentMeta>(registryRoot, "components", fullPath, "meta.json");
107
116
 
108
117
  // Install component dependencies first (recursive)
109
118
  for (const dep of meta.dependencies) {
110
- await addComponent(dep, false);
119
+ await addComponent(dep, false, options);
111
120
  }
112
121
 
113
- // Check if component already exists
114
- const destDir = join(process.cwd(), "src", "lib", "components", fullPath);
115
- if (existsSync(destDir)) {
122
+ // Check if component already exists (skip check entirely in non-interactive mode)
123
+ const destDir = join(cwd, "src", "lib", "components", fullPath);
124
+ if (!options?.skipPrompts && existsSync(destDir)) {
116
125
  const replace = await p.confirm({
117
126
  message: `Component "${name}" already exists at src/lib/components/${fullPath}/. Replace it?`,
118
127
  });
@@ -126,25 +135,20 @@ export async function addComponent(name: string, root = false) {
126
135
  mkdirSync(destDir, { recursive: true });
127
136
 
128
137
  for (const file of meta.files) {
129
- const content = await readFile(fullPath, file);
138
+ const content = await readRegistryFile(registryRoot, "components", fullPath, file);
130
139
  const dest = join(destDir, file);
131
- mkdirSync(dirname(dest), { recursive: true });
140
+ if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
132
141
  writeFileSync(dest, content, "utf-8");
133
142
  console.log(` ✍️ src/lib/components/${fullPath}/${file}`);
134
143
  }
135
144
 
136
145
  // Install npm dependencies
137
- const npmEntries = Object.entries(meta.npmDeps);
138
- if (npmEntries.length > 0) {
139
- const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
140
- console.log(` 📥 npm: ${packages.join(", ")}`);
141
- const proc = spawn(["bun", "add", ...packages], {
142
- stdout: "inherit",
143
- stderr: "inherit",
144
- cwd: process.cwd(),
145
- });
146
- if ((await proc.exited) !== 0) {
147
- console.warn(` ⚠️ bun add failed for: ${packages.join(", ")}`);
146
+ if (Object.keys(meta.npmDeps).length > 0) {
147
+ if (options?.skipInstall) {
148
+ const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
149
+ if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
150
+ } else {
151
+ await bunAdd(cwd, meta.npmDeps);
148
152
  }
149
153
  }
150
154
 
@@ -169,53 +173,3 @@ function ensureUtils() {
169
173
  console.log(" ✍️ src/lib/utils.ts (cn utility)\n");
170
174
  }
171
175
  }
172
-
173
- // ─── Registry resolvers ──────────────────────────────────────
174
-
175
- function resolveLocalRegistry(): string {
176
- // Walk up from this file's directory to find registry/
177
- let dir = dirname(new URL(import.meta.url).pathname);
178
- for (let i = 0; i < 10; i++) {
179
- const candidate = join(dir, "registry");
180
- if (existsSync(join(candidate, "index.json"))) return candidate;
181
- const parent = dirname(dir);
182
- if (parent === dir) break;
183
- dir = parent;
184
- }
185
- console.error("❌ Could not find local registry/ directory.");
186
- process.exit(1);
187
- }
188
-
189
- async function readMeta(name: string): Promise<ComponentMeta> {
190
- if (registryRoot) {
191
- const path = join(registryRoot, "components", name, "meta.json");
192
- if (!existsSync(path)) {
193
- throw new Error(`Component "${name}" not found in local registry`);
194
- }
195
- return JSON.parse(readFileSync(path, "utf-8"));
196
- }
197
- return fetchJSON<ComponentMeta>(`${REMOTE_BASE}/components/${name}/meta.json`);
198
- }
199
-
200
- async function readFile(name: string, file: string): Promise<string> {
201
- if (registryRoot) {
202
- const path = join(registryRoot, "components", name, file);
203
- if (!existsSync(path)) {
204
- throw new Error(`File "${file}" not found for component "${name}" in local registry`);
205
- }
206
- return readFileSync(path, "utf-8");
207
- }
208
- return fetchText(`${REMOTE_BASE}/components/${name}/${file}`);
209
- }
210
-
211
- async function fetchJSON<T>(url: string): Promise<T> {
212
- const res = await fetch(url);
213
- if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
214
- return res.json() as Promise<T>;
215
- }
216
-
217
- async function fetchText(url: string): Promise<string> {
218
- const res = await fetch(url);
219
- if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
220
- return res.text();
221
- }
package/src/cli/create.ts CHANGED
@@ -2,6 +2,8 @@ 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";
5
+ import { installFeature, initFeatRegistry, resolveLocalRegistry } from "./feat.ts";
6
+ import { initAddRegistry } from "./add.ts";
5
7
 
6
8
  // ─── bosia create <name> [--template <name>] ──────────────
7
9
 
@@ -59,7 +61,32 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
59
61
  writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
60
62
  }
61
63
 
62
- console.log(`✅ Project created at ${targetDir}\n`);
64
+ // Install template features from registry
65
+ const templateConfigPath = join(templateDir, "template.json");
66
+ if (existsSync(templateConfigPath)) {
67
+ const config = JSON.parse(readFileSync(templateConfigPath, "utf-8"));
68
+ if (config.features?.length) {
69
+ let localRegistry: string | null = null;
70
+ try {
71
+ localRegistry = resolveLocalRegistry();
72
+ } catch {
73
+ // Local registry not found — will use remote
74
+ }
75
+
76
+ await initAddRegistry(localRegistry);
77
+ initFeatRegistry(localRegistry);
78
+
79
+ for (const feat of config.features) {
80
+ await installFeature(feat, true, {
81
+ skipInstall: true,
82
+ skipPrompts: true,
83
+ cwd: targetDir,
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ console.log(`\n✅ Project created at ${targetDir}\n`);
63
90
 
64
91
  console.log("Installing dependencies...");
65
92
  const proc = spawn(["bun", "install"], {
@@ -72,7 +99,7 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
72
99
  console.warn("⚠️ bun install failed — run it manually.");
73
100
  } else {
74
101
  console.log(`\n🎉 Ready!\n\ncd ${name}`);
75
-
102
+
76
103
  const instPath = join(templateDir, "instructions.txt");
77
104
  if (existsSync(instPath)) {
78
105
  const instructions = readFileSync(instPath, "utf-8").trimEnd();
@@ -117,9 +144,9 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
117
144
  for (const entry of readdirSync(src, { withFileTypes: true })) {
118
145
  const srcPath = join(src, entry.name);
119
146
  const destPath = join(dest, entry.name);
120
-
121
- // Do not copy instructions.txt to the final project
122
- if (entry.name === "instructions.txt") continue;
147
+
148
+ // Do not copy instructions.txt or template.json to the final project
149
+ if (entry.name === "instructions.txt" || entry.name === "template.json") continue;
123
150
 
124
151
  if (entry.isDirectory()) {
125
152
  copyDir(srcPath, destPath, projectName, isLocal);
package/src/cli/feat.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  import { join, dirname } from "path";
2
2
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
- import { spawn } from "bun";
4
3
  import * as p from "@clack/prompts";
5
4
  import { addComponent, initAddRegistry } from "./add.ts";
5
+ import {
6
+ type InstallOptions,
7
+ resolveLocalRegistryOrExit,
8
+ readRegistryJSON,
9
+ readRegistryFile,
10
+ mergePkgJson,
11
+ bunAdd,
12
+ } from "./registry.ts";
6
13
 
7
14
  // ─── bosia feat <feature> [--local] ──────────────────────
8
15
  // Fetches a feature scaffold from the GitHub registry (or local
9
16
  // registry with --local) and copies route/lib files, installs npm deps.
10
17
  // Supports nested feature dependencies (e.g. todo → drizzle).
11
18
 
12
- const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
13
-
14
19
  interface FeatureMeta {
15
20
  name: string;
16
21
  description: string;
@@ -19,6 +24,7 @@ interface FeatureMeta {
19
24
  files: string[]; // source filenames in the registry feature dir
20
25
  targets: string[]; // destination paths relative to project root
21
26
  npmDeps: Record<string, string>;
27
+ npmDevDeps?: Record<string, string>;
22
28
  scripts?: Record<string, string>; // package.json scripts to add
23
29
  envVars?: Record<string, string>; // env vars to append to .env if missing
24
30
  }
@@ -35,7 +41,7 @@ export async function runFeat(name: string | undefined, flags: string[] = []) {
35
41
  }
36
42
 
37
43
  if (flags.includes("--local")) {
38
- registryRoot = resolveLocalRegistry();
44
+ registryRoot = resolveLocalRegistryOrExit();
39
45
  console.log(`⬡ Using local registry: ${registryRoot}\n`);
40
46
  }
41
47
 
@@ -45,18 +51,25 @@ export async function runFeat(name: string | undefined, flags: string[] = []) {
45
51
  await installFeature(name, true);
46
52
  }
47
53
 
48
- async function installFeature(name: string, isRoot: boolean) {
54
+ /** Set the registry root for feature resolution. Called by create.ts for template features. */
55
+ export function initFeatRegistry(root: string | null) {
56
+ registryRoot = root;
57
+ }
58
+
59
+ export async function installFeature(name: string, isRoot: boolean, options?: InstallOptions) {
49
60
  if (installedFeats.has(name)) return;
50
61
  installedFeats.add(name);
51
62
 
63
+ const cwd = options?.cwd ?? process.cwd();
64
+
52
65
  console.log(isRoot ? `⬡ Installing feature: ${name}\n` : `\n⬡ Installing dependency feature: ${name}\n`);
53
66
 
54
- const meta = await readMeta(name);
67
+ const meta = await readRegistryJSON<FeatureMeta>(registryRoot, "features", name, "meta.json");
55
68
 
56
69
  // Install required feature dependencies first (recursive)
57
70
  if (meta.features && meta.features.length > 0) {
58
71
  for (const feat of meta.features) {
59
- await installFeature(feat, false);
72
+ await installFeature(feat, false, options);
60
73
  }
61
74
  }
62
75
 
@@ -64,19 +77,20 @@ async function installFeature(name: string, isRoot: boolean) {
64
77
  if (meta.components.length > 0) {
65
78
  console.log("📦 Installing required components...");
66
79
  for (const comp of meta.components) {
67
- await addComponent(comp, false);
80
+ await addComponent(comp, false, options);
68
81
  }
69
82
  console.log("");
70
83
  }
71
84
 
72
85
  // Copy feature files to their target paths
86
+ const createdDirs = new Set<string>();
73
87
  for (let i = 0; i < meta.files.length; i++) {
74
88
  const file = meta.files[i]!;
75
89
  const target = meta.targets[i] ?? file;
76
- const dest = join(process.cwd(), target);
90
+ const dest = join(cwd, target);
77
91
 
78
- // Prompt before overwriting existing files
79
- if (existsSync(dest)) {
92
+ // Prompt before overwriting existing files (skip check entirely in non-interactive mode)
93
+ if (!options?.skipPrompts && existsSync(dest)) {
80
94
  const replace = await p.confirm({
81
95
  message: `File "${target}" already exists. Replace it?`,
82
96
  });
@@ -86,52 +100,46 @@ async function installFeature(name: string, isRoot: boolean) {
86
100
  }
87
101
  }
88
102
 
89
- const content = await readFile(name, file);
90
- mkdirSync(dirname(dest), { recursive: true });
103
+ const content = await readRegistryFile(registryRoot, "features", name, file);
104
+ const dir = dirname(dest);
105
+ if (!createdDirs.has(dir)) {
106
+ mkdirSync(dir, { recursive: true });
107
+ createdDirs.add(dir);
108
+ }
91
109
  writeFileSync(dest, content, "utf-8");
92
110
  console.log(` ✍️ ${target}`);
93
111
  }
94
112
 
95
113
  // Install npm dependencies
96
- const npmEntries = Object.entries(meta.npmDeps);
97
- if (npmEntries.length > 0) {
98
- const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
99
- console.log(`\n📥 npm: ${packages.join(", ")}`);
100
- const proc = spawn(["bun", "add", ...packages], {
101
- stdout: "inherit",
102
- stderr: "inherit",
103
- cwd: process.cwd(),
104
- });
105
- if ((await proc.exited) !== 0) {
106
- console.warn(`⚠️ bun add failed for: ${packages.join(", ")}`);
107
- }
108
- }
109
-
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(", ")}`);
114
+ const hasDeps = Object.keys(meta.npmDeps).length > 0;
115
+ const hasDevDeps = Object.keys(meta.npmDevDeps ?? {}).length > 0;
116
+ const hasScripts = Object.keys(meta.scripts ?? {}).length > 0;
117
+
118
+ if (hasDeps || hasDevDeps) {
119
+ if (options?.skipInstall) {
120
+ const { addedDeps, addedScripts } = mergePkgJson(cwd, {
121
+ deps: meta.npmDeps,
122
+ devDeps: meta.npmDevDeps,
123
+ scripts: meta.scripts,
124
+ });
125
+ if (addedDeps.length > 0) console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
126
+ if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
127
+ } else {
128
+ await bunAdd(cwd, meta.npmDeps, meta.npmDevDeps);
129
+ if (hasScripts) {
130
+ const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
131
+ if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
127
132
  }
128
133
  }
134
+ } else if (hasScripts) {
135
+ const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
136
+ if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
129
137
  }
130
138
 
131
139
  // Append env vars to .env if missing
132
140
  const envEntries = Object.entries(meta.envVars ?? {});
133
141
  if (envEntries.length > 0) {
134
- const envPath = join(process.cwd(), ".env");
142
+ const envPath = join(cwd, ".env");
135
143
  const existing = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
136
144
  const toAdd: string[] = [];
137
145
  for (const [key, val] of envEntries) {
@@ -154,51 +162,5 @@ async function installFeature(name: string, isRoot: boolean) {
154
162
  }
155
163
  }
156
164
 
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}`);
192
- }
193
-
194
- async function fetchJSON<T>(url: string): Promise<T> {
195
- const res = await fetch(url);
196
- if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
197
- return res.json() as Promise<T>;
198
- }
199
-
200
- async function fetchText(url: string): Promise<string> {
201
- const res = await fetch(url);
202
- if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
203
- return res.text();
204
- }
165
+ // Re-exports for create.ts
166
+ export { resolveLocalRegistry, type InstallOptions } from "./registry.ts";
@@ -0,0 +1,168 @@
1
+ import { join, dirname } from "path";
2
+ import { writeFileSync, readFileSync, existsSync } from "fs";
3
+ import { spawn } from "bun";
4
+
5
+ // ─── Shared registry utilities for feat.ts and add.ts ─────
6
+
7
+ export const REGISTRY_URL = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
8
+
9
+ export interface InstallOptions {
10
+ skipInstall?: boolean; // write deps to package.json instead of `bun add`
11
+ skipPrompts?: boolean; // auto-overwrite, no interactive prompts
12
+ cwd?: string; // override process.cwd() for file operations
13
+ }
14
+
15
+ // ─── Local registry resolution ────────────────────────────
16
+
17
+ export function resolveLocalRegistry(): string {
18
+ let dir = dirname(new URL(import.meta.url).pathname);
19
+ for (let i = 0; i < 10; i++) {
20
+ const candidate = join(dir, "registry");
21
+ if (existsSync(join(candidate, "index.json"))) return candidate;
22
+ const parent = dirname(dir);
23
+ if (parent === dir) break;
24
+ dir = parent;
25
+ }
26
+ throw new Error("Could not find local registry/ directory.");
27
+ }
28
+
29
+ /** Resolve local registry, exiting with error message on failure. For CLI entry points. */
30
+ export function resolveLocalRegistryOrExit(): string {
31
+ try {
32
+ return resolveLocalRegistry();
33
+ } catch {
34
+ console.error("❌ Could not find local registry/ directory.");
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ // ─── Registry file readers ────────────────────────────────
40
+
41
+ /** Read and parse a JSON file from the registry (local or remote). */
42
+ export async function readRegistryJSON<T>(
43
+ registryRoot: string | null,
44
+ category: string,
45
+ name: string,
46
+ file: string,
47
+ ): Promise<T> {
48
+ if (registryRoot) {
49
+ const path = join(registryRoot, category, name, file);
50
+ if (!existsSync(path)) {
51
+ throw new Error(`"${file}" not found for ${category.slice(0, -1)} "${name}" in local registry`);
52
+ }
53
+ return JSON.parse(readFileSync(path, "utf-8"));
54
+ }
55
+ return fetchJSON<T>(`${REGISTRY_URL}/${category}/${name}/${file}`);
56
+ }
57
+
58
+ /** Read a text file from the registry (local or remote). */
59
+ export async function readRegistryFile(
60
+ registryRoot: string | null,
61
+ category: string,
62
+ name: string,
63
+ file: string,
64
+ ): Promise<string> {
65
+ if (registryRoot) {
66
+ const path = join(registryRoot, category, name, file);
67
+ if (!existsSync(path)) {
68
+ throw new Error(`File "${file}" not found for ${category.slice(0, -1)} "${name}" in local registry`);
69
+ }
70
+ return readFileSync(path, "utf-8");
71
+ }
72
+ return fetchText(`${REGISTRY_URL}/${category}/${name}/${file}`);
73
+ }
74
+
75
+ // ─── package.json helpers ─────────────────────────────────
76
+
77
+ export interface PkgDeps {
78
+ deps?: Record<string, string>;
79
+ devDeps?: Record<string, string>;
80
+ scripts?: Record<string, string>;
81
+ }
82
+
83
+ /**
84
+ * Merge dependencies and scripts into package.json in a single read/write.
85
+ * Returns the list of added keys, or empty arrays if nothing changed.
86
+ */
87
+ export function mergePkgJson(cwd: string, changes: PkgDeps): { addedDeps: string[]; addedScripts: string[] } {
88
+ const pkgPath = join(cwd, "package.json");
89
+ if (!existsSync(pkgPath)) return { addedDeps: [], addedScripts: [] };
90
+
91
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
92
+ let changed = false;
93
+ const addedDeps: string[] = [];
94
+ const addedScripts: string[] = [];
95
+
96
+ if (changes.deps && Object.keys(changes.deps).length > 0) {
97
+ pkg.dependencies = pkg.dependencies ?? {};
98
+ for (const [name, ver] of Object.entries(changes.deps)) {
99
+ if (!pkg.dependencies[name]) {
100
+ pkg.dependencies[name] = ver;
101
+ addedDeps.push(name);
102
+ changed = true;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (changes.devDeps && Object.keys(changes.devDeps).length > 0) {
108
+ pkg.devDependencies = pkg.devDependencies ?? {};
109
+ for (const [name, ver] of Object.entries(changes.devDeps)) {
110
+ if (!pkg.devDependencies[name]) {
111
+ pkg.devDependencies[name] = ver;
112
+ addedDeps.push(name);
113
+ changed = true;
114
+ }
115
+ }
116
+ }
117
+
118
+ if (changes.scripts && Object.keys(changes.scripts).length > 0) {
119
+ pkg.scripts = pkg.scripts ?? {};
120
+ for (const [key, val] of Object.entries(changes.scripts)) {
121
+ if (!pkg.scripts[key]) {
122
+ pkg.scripts[key] = val;
123
+ addedScripts.push(key);
124
+ changed = true;
125
+ }
126
+ }
127
+ }
128
+
129
+ if (changed) {
130
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
131
+ }
132
+
133
+ return { addedDeps, addedScripts };
134
+ }
135
+
136
+ /** Run `bun add` for deps and optionally `bun add --dev` for devDeps. */
137
+ export async function bunAdd(cwd: string, deps: Record<string, string>, devDeps?: Record<string, string>): Promise<void> {
138
+ const packages = Object.entries(deps).map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
139
+ if (packages.length > 0) {
140
+ console.log(`\n📥 npm: ${packages.join(", ")}`);
141
+ const proc = spawn(["bun", "add", ...packages], { stdout: "inherit", stderr: "inherit", cwd });
142
+ if ((await proc.exited) !== 0) {
143
+ console.warn(`⚠️ bun add failed for: ${packages.join(", ")}`);
144
+ }
145
+ }
146
+ const devPackages = Object.entries(devDeps ?? {}).map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
147
+ if (devPackages.length > 0) {
148
+ console.log(`\n📥 npm (dev): ${devPackages.join(", ")}`);
149
+ const proc = spawn(["bun", "add", "--dev", ...devPackages], { stdout: "inherit", stderr: "inherit", cwd });
150
+ if ((await proc.exited) !== 0) {
151
+ console.warn(`⚠️ bun add --dev failed for: ${devPackages.join(", ")}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ // ─── HTTP helpers ─────────────────────────────────────────
157
+
158
+ async function fetchJSON<T>(url: string): Promise<T> {
159
+ const res = await fetch(url);
160
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
161
+ return res.json() as Promise<T>;
162
+ }
163
+
164
+ async function fetchText(url: string): Promise<string> {
165
+ const res = await fetch(url);
166
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
167
+ return res.text();
168
+ }
@@ -11,6 +11,15 @@ const VALID_SAMESITE = new Set(["Strict", "Lax", "None"]);
11
11
  */
12
12
  const VALID_COOKIE_NAME = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
13
13
 
14
+ // ─── Cookie Defaults ─────────────────────────────────────
15
+ /** Secure defaults matching SvelteKit conventions. */
16
+ const COOKIE_DEFAULTS: CookieOptions = {
17
+ path: "/",
18
+ httpOnly: true,
19
+ secure: true,
20
+ sameSite: "Lax",
21
+ };
22
+
14
23
  // ─── Cookie Helpers ──────────────────────────────────────
15
24
 
16
25
  function parseCookies(header: string): Record<string, string> {
@@ -31,9 +40,14 @@ function parseCookies(header: string): Record<string, string> {
31
40
  export class CookieJar implements Cookies {
32
41
  private _incoming: Record<string, string>;
33
42
  private _outgoing: string[] = [];
43
+ private _defaults: CookieOptions;
34
44
 
35
- constructor(cookieHeader: string) {
45
+ constructor(cookieHeader: string, dev = false) {
36
46
  this._incoming = parseCookies(cookieHeader);
47
+ // In dev mode, omit Secure — browsers reject Secure cookies over http://localhost
48
+ this._defaults = dev
49
+ ? { ...COOKIE_DEFAULTS, secure: false }
50
+ : COOKIE_DEFAULTS;
37
51
  }
38
52
 
39
53
  get(name: string): string | undefined {
@@ -46,27 +60,29 @@ export class CookieJar implements Cookies {
46
60
 
47
61
  set(name: string, value: string, options?: CookieOptions): void {
48
62
  if (!VALID_COOKIE_NAME.test(name)) throw new Error(`Invalid cookie name: ${name}`);
63
+ const opts = { ...this._defaults, ...options };
49
64
  let header = `${name}=${encodeURIComponent(value)}`;
50
- const path = options?.path ?? "/";
51
- if (UNSAFE_COOKIE_VALUE.test(path)) throw new Error(`Invalid cookie path: ${path}`);
52
- header += `; Path=${path}`;
53
- if (options?.domain) {
54
- if (UNSAFE_COOKIE_VALUE.test(options.domain)) throw new Error(`Invalid cookie domain: ${options.domain}`);
55
- header += `; Domain=${options.domain}`;
65
+ if (opts.path) {
66
+ if (UNSAFE_COOKIE_VALUE.test(opts.path)) throw new Error(`Invalid cookie path: ${opts.path}`);
67
+ header += `; Path=${opts.path}`;
68
+ }
69
+ if (opts.domain) {
70
+ if (UNSAFE_COOKIE_VALUE.test(opts.domain)) throw new Error(`Invalid cookie domain: ${opts.domain}`);
71
+ header += `; Domain=${opts.domain}`;
56
72
  }
57
- if (options?.maxAge != null) header += `; Max-Age=${options.maxAge}`;
58
- if (options?.expires) header += `; Expires=${options.expires.toUTCString()}`;
59
- if (options?.httpOnly) header += "; HttpOnly";
60
- if (options?.secure) header += "; Secure";
61
- if (options?.sameSite) {
62
- if (!VALID_SAMESITE.has(options.sameSite)) throw new Error(`Invalid cookie sameSite: ${options.sameSite}`);
63
- header += `; SameSite=${options.sameSite}`;
73
+ if (opts.maxAge != null) header += `; Max-Age=${opts.maxAge}`;
74
+ if (opts.expires) header += `; Expires=${opts.expires.toUTCString()}`;
75
+ if (opts.httpOnly) header += "; HttpOnly";
76
+ if (opts.secure) header += "; Secure";
77
+ if (opts.sameSite) {
78
+ if (!VALID_SAMESITE.has(opts.sameSite)) throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
79
+ header += `; SameSite=${opts.sameSite}`;
64
80
  }
65
81
  this._outgoing.push(header);
66
82
  }
67
83
 
68
84
  delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void {
69
- this.set(name, "", { path: options?.path, domain: options?.domain, maxAge: 0 });
85
+ this.set(name, "", { ...options, maxAge: 0 });
70
86
  }
71
87
 
72
88
  get outgoing(): readonly string[] {
@@ -328,7 +328,7 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
328
328
  return Response.json({ error: "Forbidden", message: csrfError }, { status: 403 });
329
329
  }
330
330
 
331
- const cookieJar = new CookieJar(request.headers.get("cookie") ?? "");
331
+ const cookieJar = new CookieJar(request.headers.get("cookie") ?? "", isDev);
332
332
  const event: RequestEvent = { request, url, locals: {}, params: {}, cookies: cookieJar };
333
333
  const response = userHandle
334
334
  ? await userHandle({ event, resolve })
@@ -5,22 +5,16 @@
5
5
  "scripts": {
6
6
  "dev": "bosia dev",
7
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"
8
+ "start": "bosia start"
12
9
  },
13
10
  "dependencies": {
14
11
  "bosia": "^{{BOSIA_VERSION}}",
15
12
  "svelte": "^5.20.0",
16
13
  "clsx": "^2.1.1",
17
- "tailwind-merge": "^3.5.0",
18
- "drizzle-orm": "^0.44.0",
19
- "postgres": "^3.4.5"
14
+ "tailwind-merge": "^3.5.0"
20
15
  },
21
16
  "devDependencies": {
22
17
  "@types/bun": "latest",
23
- "typescript": "^5",
24
- "drizzle-kit": "^0.31.0"
18
+ "typescript": "^5"
25
19
  }
26
20
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "features": ["todo"]
3
+ }
@@ -1,10 +0,0 @@
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
- });
@@ -1,15 +0,0 @@
1
- import { drizzle } from "drizzle-orm/postgres-js";
2
- import postgres from "postgres";
3
- import * as schema from "./schemas";
4
-
5
- const connectionString = process.env.DATABASE_URL;
6
-
7
- if (!connectionString) {
8
- console.warn("⚠️ DATABASE_URL is not set. Database queries will fail.");
9
- }
10
-
11
- const client = postgres(connectionString || "postgres://localhost/dummy");
12
-
13
- export const db = drizzle(client, { schema });
14
-
15
- export type Database = typeof db;
@@ -1 +0,0 @@
1
- export * from "../todo/schemas/todo.table";
@@ -1,11 +0,0 @@
1
- import type { Database } from "../index";
2
- import { todos } from "../../todo/schemas/todo.table";
3
-
4
- export async function seed(db: Database) {
5
- await db.insert(todos).values([
6
- { title: "Learn Bosia framework" },
7
- { title: "Set up PostgreSQL with Drizzle" },
8
- { title: "Build a full-stack app", completed: true },
9
- { title: "Deploy to production" },
10
- ]);
11
- }
@@ -1,80 +0,0 @@
1
- import { sql } from "drizzle-orm";
2
- import { db } from "../index";
3
-
4
- // Seed tracking table in the "drizzle" schema (same schema Drizzle Kit uses for migrations)
5
- const SEEDS_TABLE = "__bosia_seeds";
6
-
7
- interface SeedModule {
8
- seed: (db: typeof import("../index").db) => Promise<void>;
9
- }
10
-
11
- async function ensureSeedsTable() {
12
- await db.execute(sql`
13
- CREATE SCHEMA IF NOT EXISTS drizzle
14
- `);
15
- await db.execute(sql`
16
- CREATE TABLE IF NOT EXISTS drizzle.${sql.identifier(SEEDS_TABLE)} (
17
- id SERIAL PRIMARY KEY,
18
- name TEXT NOT NULL UNIQUE,
19
- applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
20
- )
21
- `);
22
- }
23
-
24
- async function getAppliedSeeds(): Promise<Set<string>> {
25
- const rows = await db.execute<{ name: string }>(
26
- sql`SELECT name FROM drizzle.${sql.identifier(SEEDS_TABLE)} ORDER BY id`
27
- );
28
- return new Set(rows.map((r) => r.name));
29
- }
30
-
31
- async function markSeedApplied(name: string) {
32
- await db.execute(
33
- sql`INSERT INTO drizzle.${sql.identifier(SEEDS_TABLE)} (name) VALUES (${name})`
34
- );
35
- }
36
-
37
- async function run() {
38
- console.log("Running seeds...\n");
39
-
40
- await ensureSeedsTable();
41
- const applied = await getAppliedSeeds();
42
-
43
- // Discover seed files (*.ts, excluding runner.ts)
44
- const glob = new Bun.Glob("*.ts");
45
- const seedDir = import.meta.dir;
46
- const files = Array.from(glob.scanSync(seedDir))
47
- .filter((f) => f !== "runner.ts")
48
- .sort();
49
-
50
- let count = 0;
51
-
52
- for (const file of files) {
53
- const name = file.replace(/\.ts$/, "");
54
-
55
- if (applied.has(name)) {
56
- console.log(` skip ${name} (already applied)`);
57
- continue;
58
- }
59
-
60
- console.log(` seed ${name}`);
61
- const mod: SeedModule = await import(`./${file}`);
62
-
63
- if (typeof mod.seed !== "function") {
64
- console.error(` ERROR ${file} does not export a seed() function, skipping`);
65
- continue;
66
- }
67
-
68
- await mod.seed(db);
69
- await markSeedApplied(name);
70
- count++;
71
- }
72
-
73
- console.log(`\nDone. Applied ${count} seed(s).`);
74
- process.exit(0);
75
- }
76
-
77
- run().catch((err) => {
78
- console.error("Seed runner failed:", err);
79
- process.exit(1);
80
- });
@@ -1,3 +0,0 @@
1
- export * from "./schemas/todo.table";
2
- export * from "./types";
3
- export * as todoQueries from "./queries";
@@ -1,36 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
- import { db } from "../drizzle";
3
- import { todos } from "./schemas/todo.table";
4
- import type { NewTodo } from "./types";
5
-
6
- export function getAll() {
7
- return db.query.todos.findMany({
8
- orderBy: (t, { desc }) => [desc(t.createdAt)],
9
- });
10
- }
11
-
12
- export function getById(id: string) {
13
- return db.query.todos.findFirst({
14
- where: eq(todos.id, id),
15
- });
16
- }
17
-
18
- export function create(data: Pick<NewTodo, "title">) {
19
- return db.insert(todos).values(data).returning();
20
- }
21
-
22
- export function update(id: string, data: Partial<Pick<NewTodo, "title" | "completed">>) {
23
- return db
24
- .update(todos)
25
- .set({ ...data, updatedAt: new Date() })
26
- .where(eq(todos.id, id))
27
- .returning();
28
- }
29
-
30
- export function toggle(id: string, completed: boolean) {
31
- return update(id, { completed });
32
- }
33
-
34
- export function remove(id: string) {
35
- return db.delete(todos).where(eq(todos.id, id)).returning();
36
- }
@@ -1,9 +0,0 @@
1
- import { pgTable, uuid, text, boolean, timestamp } from "drizzle-orm/pg-core";
2
-
3
- export const todos = pgTable("todos", {
4
- id: uuid("id").defaultRandom().primaryKey(),
5
- title: text("title").notNull(),
6
- completed: boolean("completed").notNull().default(false),
7
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
8
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
9
- });
@@ -1,5 +0,0 @@
1
- import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
2
- import type { todos } from "./schemas/todo.table";
3
-
4
- export type Todo = InferSelectModel<typeof todos>;
5
- export type NewTodo = InferInsertModel<typeof todos>;
@@ -1,3 +0,0 @@
1
- export { default as TodoForm } from "./todo-form.svelte";
2
- export { default as TodoItem } from "./todo-item.svelte";
3
- export { default as TodoList } from "./todo-list.svelte";
@@ -1,23 +0,0 @@
1
- <script lang="ts">
2
- let { error }: { error?: string } = $props();
3
- </script>
4
-
5
- <form method="POST" action="?/create" class="flex gap-2">
6
- <input
7
- name="title"
8
- type="text"
9
- placeholder="What needs to be done?"
10
- required
11
- class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
12
- />
13
- <button
14
- type="submit"
15
- class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
16
- >
17
- Add
18
- </button>
19
- </form>
20
-
21
- {#if error}
22
- <p class="mt-2 text-sm text-destructive">{error}</p>
23
- {/if}
@@ -1,63 +0,0 @@
1
- <script lang="ts">
2
- import type { Todo } from "../../../features/todo/types";
3
-
4
- let { todo }: { todo: Todo } = $props();
5
- let editing = $state(false);
6
- let editTitle = $state(todo.title);
7
- </script>
8
-
9
- <li class="group flex items-center gap-3 rounded-md border border-border px-4 py-3 transition-colors hover:bg-accent/50">
10
- <!-- Toggle -->
11
- <form method="POST" action="?/toggle">
12
- <input type="hidden" name="id" value={todo.id} />
13
- <input type="hidden" name="completed" value={String(!todo.completed)} />
14
- <button
15
- type="submit"
16
- class="flex size-5 items-center justify-center rounded border border-input transition-colors hover:border-primary {todo.completed ? 'bg-primary border-primary' : ''}"
17
- aria-label={todo.completed ? 'Mark incomplete' : 'Mark complete'}
18
- >
19
- {#if todo.completed}
20
- <svg class="size-3 text-primary-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
21
- <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
22
- </svg>
23
- {/if}
24
- </button>
25
- </form>
26
-
27
- <!-- Title / Edit -->
28
- {#if editing}
29
- <form method="POST" action="?/update" class="flex flex-1 gap-2" onsubmit={() => (editing = false)}>
30
- <input type="hidden" name="id" value={todo.id} />
31
- <input
32
- name="title"
33
- type="text"
34
- bind:value={editTitle}
35
- required
36
- class="flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
37
- />
38
- <button type="submit" class="text-sm text-primary hover:underline">Save</button>
39
- <button type="button" class="text-sm text-muted-foreground hover:underline" onclick={() => { editing = false; editTitle = todo.title; }}>Cancel</button>
40
- </form>
41
- {:else}
42
- <span
43
- class="flex-1 text-sm {todo.completed ? 'line-through text-muted-foreground' : ''}"
44
- ondblclick={() => (editing = true)}
45
- >
46
- {todo.title}
47
- </span>
48
- {/if}
49
-
50
- <!-- Delete -->
51
- <form method="POST" action="?/delete">
52
- <input type="hidden" name="id" value={todo.id} />
53
- <button
54
- type="submit"
55
- class="text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive"
56
- aria-label="Delete todo"
57
- >
58
- <svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
59
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
60
- </svg>
61
- </button>
62
- </form>
63
- </li>
@@ -1,21 +0,0 @@
1
- <script lang="ts">
2
- import type { Todo } from "../../../features/todo/types";
3
- import TodoItem from "./todo-item.svelte";
4
-
5
- let { todos }: { todos: Todo[] } = $props();
6
- </script>
7
-
8
- {#if todos.length === 0}
9
- <div class="rounded-md border border-dashed border-border py-12 text-center">
10
- <p class="text-muted-foreground">No todos yet. Add one above!</p>
11
- </div>
12
- {:else}
13
- <ul class="space-y-2">
14
- {#each todos as todo (todo.id)}
15
- <TodoItem {todo} />
16
- {/each}
17
- </ul>
18
- <p class="mt-3 text-xs text-muted-foreground">
19
- {todos.filter(t => t.completed).length} of {todos.length} completed
20
- </p>
21
- {/if}
@@ -1,18 +0,0 @@
1
- import type { RequestEvent } from "bosia";
2
- import { todoQueries } from "../../../features/todo";
3
-
4
- export async function GET() {
5
- const todos = await todoQueries.getAll();
6
- return Response.json(todos);
7
- }
8
-
9
- export async function POST({ request }: RequestEvent) {
10
- const body = await request.json().catch(() => null);
11
-
12
- if (!body?.title?.trim()) {
13
- return Response.json({ error: "Title is required" }, { status: 400 });
14
- }
15
-
16
- const [todo] = await todoQueries.create({ title: body.title.trim() });
17
- return Response.json(todo, { status: 201 });
18
- }
@@ -1,42 +0,0 @@
1
- import type { RequestEvent } from "bosia";
2
- import { todoQueries } from "../../../../features/todo";
3
-
4
- export async function GET({ params }: RequestEvent) {
5
- const todo = await todoQueries.getById(params.id);
6
-
7
- if (!todo) {
8
- return Response.json({ error: "Todo not found" }, { status: 404 });
9
- }
10
-
11
- return Response.json(todo);
12
- }
13
-
14
- export async function PUT({ params, request }: RequestEvent) {
15
- const body = await request.json().catch(() => null);
16
-
17
- if (!body || (body.title !== undefined && !body.title?.trim())) {
18
- return Response.json({ error: "Invalid body" }, { status: 400 });
19
- }
20
-
21
- const data: { title?: string; completed?: boolean } = {};
22
- if (body.title !== undefined) data.title = body.title.trim();
23
- if (body.completed !== undefined) data.completed = body.completed;
24
-
25
- const [todo] = await todoQueries.update(params.id, data);
26
-
27
- if (!todo) {
28
- return Response.json({ error: "Todo not found" }, { status: 404 });
29
- }
30
-
31
- return Response.json(todo);
32
- }
33
-
34
- export async function DELETE({ params }: RequestEvent) {
35
- const [todo] = await todoQueries.remove(params.id);
36
-
37
- if (!todo) {
38
- return Response.json({ error: "Todo not found" }, { status: 404 });
39
- }
40
-
41
- return Response.json(todo);
42
- }
@@ -1,52 +0,0 @@
1
- import { fail } from "bosia";
2
- import type { RequestEvent } from "bosia";
3
- import { todoQueries } from "../../features/todo";
4
-
5
- export async function load() {
6
- const todos = await todoQueries.getAll();
7
- return { todos };
8
- }
9
-
10
- export const actions = {
11
- create: async ({ request }: RequestEvent) => {
12
- const data = await request.formData();
13
- const title = (data.get("title") as string)?.trim();
14
-
15
- if (!title) {
16
- return fail(400, { error: "Title is required" });
17
- }
18
-
19
- await todoQueries.create({ title });
20
- return { success: true };
21
- },
22
-
23
- toggle: async ({ request }: RequestEvent) => {
24
- const data = await request.formData();
25
- const id = data.get("id") as string;
26
- const completed = data.get("completed") === "true";
27
-
28
- await todoQueries.toggle(id, completed);
29
- return { success: true };
30
- },
31
-
32
- update: async ({ request }: RequestEvent) => {
33
- const data = await request.formData();
34
- const id = data.get("id") as string;
35
- const title = (data.get("title") as string)?.trim();
36
-
37
- if (!title) {
38
- return fail(400, { error: "Title is required" });
39
- }
40
-
41
- await todoQueries.update(id, { title });
42
- return { success: true };
43
- },
44
-
45
- delete: async ({ request }: RequestEvent) => {
46
- const data = await request.formData();
47
- const id = data.get("id") as string;
48
-
49
- await todoQueries.remove(id);
50
- return { success: true };
51
- },
52
- };
@@ -1,39 +0,0 @@
1
- <script lang="ts">
2
- import type { PageData, ActionData } from './$types';
3
- import { TodoForm, TodoList } from "$lib/components/todo";
4
-
5
- let { data, form }: { data: PageData; form: ActionData } = $props();
6
- </script>
7
-
8
- <svelte:head>
9
- <title>Todos</title>
10
- </svelte:head>
11
-
12
- <div class="flex min-h-screen flex-col bg-background text-foreground">
13
- <header class="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
14
- <nav class="mx-auto flex max-w-2xl items-center gap-6 px-4 py-3">
15
- <a href="/" class="font-bold tracking-tight flex items-center gap-2">
16
- <img src="/favicon.svg" alt="" class="size-5" />
17
- Todos
18
- </a>
19
- <a href="/" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
20
- <a href="/api/todos" target="_blank" class="text-sm text-muted-foreground hover:text-foreground transition-colors">API</a>
21
- </nav>
22
- </header>
23
-
24
- <main class="mx-auto w-full max-w-2xl flex-1 px-4 py-8">
25
- <div class="space-y-6">
26
- <div>
27
- <h1 class="text-2xl font-bold tracking-tight">Todos</h1>
28
- <p class="mt-1 text-sm text-muted-foreground">A full-stack CRUD demo with Drizzle ORM</p>
29
- </div>
30
-
31
- <TodoForm error={form?.error} />
32
- <TodoList todos={data.todos} />
33
- </div>
34
- </main>
35
-
36
- <footer class="border-t py-4 text-center text-sm text-muted-foreground">
37
- Powered by Bosia
38
- </footer>
39
- </div>