bosia 0.6.23 → 0.6.25

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add.ts +15 -4
  3. package/src/cli/addRouter.ts +17 -6
  4. package/src/cli/block.ts +7 -0
  5. package/src/cli/create.ts +0 -1
  6. package/src/cli/feat.ts +1 -1
  7. package/src/cli/index.ts +10 -3
  8. package/src/cli/manifest.ts +16 -1
  9. package/src/cli/page.ts +141 -0
  10. package/src/cli/theme.ts +20 -0
  11. package/src/core/dev.ts +98 -2
  12. package/src/core/html.ts +10 -4
  13. package/templates/default/package.json +1 -1
  14. package/templates/default/src/app.css +6 -0
  15. package/templates/demo/package.json +1 -1
  16. package/templates/demo/src/app.css +6 -0
  17. package/templates/shop/package.json +1 -1
  18. package/templates/shop/src/app.css +6 -0
  19. package/templates/todo/.env.example +0 -2
  20. package/templates/todo/.prettierignore +0 -7
  21. package/templates/todo/.prettierrc.json +0 -9
  22. package/templates/todo/README.md +0 -69
  23. package/templates/todo/_gitignore +0 -12
  24. package/templates/todo/bosia.config.ts +0 -10
  25. package/templates/todo/instructions.txt +0 -3
  26. package/templates/todo/package.json +0 -24
  27. package/templates/todo/public/.gitkeep +0 -0
  28. package/templates/todo/public/favicon.svg +0 -14
  29. package/templates/todo/public/logo-dark.svg +0 -14
  30. package/templates/todo/public/logo-light.svg +0 -14
  31. package/templates/todo/src/app.css +0 -134
  32. package/templates/todo/src/app.d.ts +0 -14
  33. package/templates/todo/src/app.html +0 -11
  34. package/templates/todo/src/hooks.server.ts +0 -20
  35. package/templates/todo/src/lib/utils.ts +0 -1
  36. package/templates/todo/src/routes/(public)/+page.svelte +0 -53
  37. package/templates/todo/src/routes/+error.svelte +0 -19
  38. package/templates/todo/src/routes/+layout.server.ts +0 -8
  39. package/templates/todo/src/routes/+layout.svelte +0 -6
  40. package/templates/todo/template.json +0 -6
  41. package/templates/todo/tsconfig.json +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.6.23",
3
+ "version": "0.6.25",
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
@@ -33,6 +33,7 @@ export interface RegistryIndex {
33
33
  components: string[];
34
34
  features: string[];
35
35
  blocks?: string[];
36
+ pages?: string[];
36
37
  themes?: string[];
37
38
  }
38
39
 
@@ -75,7 +76,7 @@ export async function runAdd(names: string[], flags: string[] = []) {
75
76
 
76
77
  /**
77
78
  * Resolve the full registry path for a component using the index.
78
- * - "todo" → "todo" (exact match in index)
79
+ * - "<name>" → "<name>" (exact match in index)
79
80
  * - "button" → "ui/button" (suffix match in index)
80
81
  * - "shop/cart" → "shop/cart" (explicit path used as-is)
81
82
  */
@@ -83,7 +84,7 @@ function resolveDestPath(name: string): string {
83
84
  if (name.includes("/")) return name;
84
85
 
85
86
  if (registryIndex) {
86
- // Exact match (e.g. "todo" "todo")
87
+ // Exact match (bare name present in index)
87
88
  if (registryIndex.components.includes(name)) return name;
88
89
  // Suffix match (e.g. "button" → "ui/button")
89
90
  const match = registryIndex.components.find((c) => c.endsWith(`/${name}`));
@@ -181,9 +182,10 @@ export function runAddList(): void {
181
182
  const manifest = readManifest();
182
183
  const components = Object.entries(manifest.components);
183
184
  const blocks = Object.entries(manifest.blocks);
185
+ const pages = Object.entries(manifest.pages);
184
186
 
185
- if (components.length === 0 && blocks.length === 0) {
186
- console.log("No components or blocks installed.");
187
+ if (components.length === 0 && blocks.length === 0 && pages.length === 0) {
188
+ console.log("No components, blocks, or pages installed.");
187
189
  console.log("Install one with: bun x bosia@latest add <component>");
188
190
  return;
189
191
  }
@@ -204,6 +206,15 @@ export function runAddList(): void {
204
206
  console.log(` ${name.padEnd(w)} ${entry.installedAt.slice(0, 10)}`);
205
207
  }
206
208
  }
209
+
210
+ if (pages.length > 0) {
211
+ if (components.length > 0 || blocks.length > 0) console.log("");
212
+ console.log(`⬡ Installed pages (${pages.length}):\n`);
213
+ const w = Math.max(...pages.map(([n]) => n.length));
214
+ for (const [name, entry] of pages) {
215
+ console.log(` ${name.padEnd(w)} ${entry.installedAt.slice(0, 10)}`);
216
+ }
217
+ }
207
218
  }
208
219
 
209
220
  // ─── Ensure $lib/utils.ts exists ─────────────────────────────
@@ -5,6 +5,7 @@
5
5
  export interface AddRunners {
6
6
  runAdd: (names: string[], flags: string[]) => Promise<void> | void;
7
7
  runAddBlock: (name: string | undefined, flags: string[]) => Promise<void> | void;
8
+ runAddPage: (name: string | undefined, flags: string[]) => Promise<void> | void;
8
9
  runAddTheme: (name: string | undefined, flags: string[]) => Promise<void> | void;
9
10
  runAddFont: (family: string | undefined, url: string | undefined) => Promise<void> | void;
10
11
  runAddList: () => Promise<void> | void;
@@ -19,6 +20,10 @@ export async function routeAdd(args: string[], runners: AddRunners): Promise<voi
19
20
  await runners.runAddBlock(positional[1], flags);
20
21
  return;
21
22
  }
23
+ if (sub === "page") {
24
+ await runners.runAddPage(positional[1], flags);
25
+ return;
26
+ }
22
27
  if (sub === "theme") {
23
28
  const themeFlags = args.filter((a) => a.startsWith("--"));
24
29
  await runners.runAddTheme(positional[1], themeFlags);
@@ -33,19 +38,25 @@ export async function routeAdd(args: string[], runners: AddRunners): Promise<voi
33
38
  return;
34
39
  }
35
40
 
36
- // Alias: `blocks/<cat>/<name>` tokens dispatch to runAddBlock.
37
- // Skills/AI agents frequently emit the plural `blocks/...` form alongside
38
- // `ui/*` components; route those to the block installer transparently
39
- // and let any remaining plain component names fall through to runAdd.
41
+ // Alias: `blocks/<cat>/<name>` / `pages/<cat>/<name>` tokens dispatch to the
42
+ // matching installer. Skills/AI agents frequently emit these plural forms
43
+ // alongside `ui/*` components; route those transparently and let any
44
+ // remaining plain component names fall through to runAdd.
40
45
  const blockTokens = positional.filter((p) => p.startsWith("blocks/"));
41
- const componentTokens = positional.filter((p) => !p.startsWith("blocks/"));
42
- if (blockTokens.length > 0) {
46
+ const pageTokens = positional.filter((p) => p.startsWith("pages/"));
47
+ const componentTokens = positional.filter(
48
+ (p) => !p.startsWith("blocks/") && !p.startsWith("pages/"),
49
+ );
50
+ if (blockTokens.length > 0 || pageTokens.length > 0) {
43
51
  if (componentTokens.length > 0) {
44
52
  await runners.runAdd(componentTokens, flags);
45
53
  }
46
54
  for (const token of blockTokens) {
47
55
  await runners.runAddBlock(token.slice("blocks/".length), flags);
48
56
  }
57
+ for (const token of pageTokens) {
58
+ await runners.runAddPage(token.slice("pages/".length), flags);
59
+ }
49
60
  return;
50
61
  }
51
62
 
package/src/cli/block.ts CHANGED
@@ -30,6 +30,10 @@ interface BlockMeta {
30
30
  npmDeps: Record<string, string>;
31
31
  }
32
32
 
33
+ // Track already-installed blocks within a session to avoid redundant work
34
+ // when multiple features/blocks declare the same block dependency.
35
+ const installed = new Set<string>();
36
+
33
37
  export async function runAddBlock(
34
38
  name: string | undefined,
35
39
  flags: string[] = [],
@@ -58,6 +62,9 @@ export async function runAddBlock(
58
62
  await initAddRegistry(registryRoot);
59
63
  ensureUtils(resolvedOptions.cwd);
60
64
 
65
+ if (installed.has(name)) return;
66
+ installed.add(name);
67
+
61
68
  console.log(`⬡ Installing block: ${name}\n`);
62
69
 
63
70
  const meta = await readRegistryJSON<BlockMeta>(registryRoot, "blocks", name, "meta.json");
package/src/cli/create.ts CHANGED
@@ -14,7 +14,6 @@ const BOSIA_VERSION: string = BOSIA_PKG.version;
14
14
  const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
15
15
  default: "Minimal starter with routing and Tailwind",
16
16
  demo: "Full-featured demo with hooks, API routes, form actions, and more",
17
- todo: "Todo app with PostgreSQL + Drizzle ORM",
18
17
  shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
19
18
  };
20
19
 
package/src/cli/feat.ts CHANGED
@@ -16,7 +16,7 @@ import { recordFeature, readManifest } from "./manifest.ts";
16
16
  // ─── bun x bosia@latest feat <feature> [--local] ─────────
17
17
  // Fetches a feature scaffold from the GitHub registry (or local
18
18
  // registry with --local) and copies route/lib files, installs npm deps.
19
- // Supports nested feature dependencies (e.g. tododrizzle).
19
+ // Supports nested feature dependencies (e.g. shopauth).
20
20
 
21
21
  type FileStrategy =
22
22
  | "write" // overwrite (prompt if interactive)
package/src/cli/index.ts CHANGED
@@ -69,6 +69,10 @@ async function main() {
69
69
  const { runAddBlock } = await import("./block.ts");
70
70
  await runAddBlock(name, flags);
71
71
  },
72
+ runAddPage: async (name, flags) => {
73
+ const { runAddPage } = await import("./page.ts");
74
+ await runAddPage(name, flags);
75
+ },
72
76
  runAddTheme: async (name, flags) => {
73
77
  const { runAddTheme } = await import("./theme.ts");
74
78
  await runAddTheme(name, flags);
@@ -116,6 +120,7 @@ Commands:
116
120
  test [args] Run tests with bun test (auto-loads .env.test, sets BOSIA_ENV=test)
117
121
  add <component...> [-y] Add one or more UI components from the registry
118
122
  add block <cat>/<name> Add a composed block from the registry
123
+ add page <cat>/<name> Add a full page (a group of blocks) from the registry
119
124
  add theme <name> Add a theme (tokens.css) from the registry
120
125
  add font <family> <url> Prepend an @import url(...) for a font family to src/app.css
121
126
  add list List installed components and blocks (reads bosia.json)
@@ -126,7 +131,7 @@ Commands:
126
131
 
127
132
  Examples:
128
133
  bun x bosia@latest create my-app
129
- bun x bosia@latest create my-app --template todo
134
+ bun x bosia@latest create my-app --template shop
130
135
  bun x bosia dev
131
136
  bun x bosia build
132
137
  bun x bosia start
@@ -137,8 +142,10 @@ Examples:
137
142
  bun x bosia@latest add button card input → install multiple at once
138
143
  bun x bosia@latest add -y button card → auto-confirm overwrites (CI / scripts)
139
144
  bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
140
- bun x bosia@latest add block cards/feature-editorial
141
- bun x bosia@latest add blocks/cards/feature-editorial (alias for: add block cards/feature-editorial)
145
+ bun x bosia@latest add block cards/feature
146
+ bun x bosia@latest add blocks/cards/feature (alias for: add block cards/feature)
147
+ bun x bosia@latest add page storefront/home
148
+ bun x bosia@latest add pages/storefront/home (alias for: add page storefront/home)
142
149
  bun x bosia@latest add theme editorial
143
150
  bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
144
151
  bun x bosia@latest feat login
@@ -38,15 +38,19 @@ export interface BlockManifestEntry {
38
38
  fonts?: string[];
39
39
  }
40
40
 
41
+ // A page is a group of blocks (no backend); reuse the block entry shape.
42
+ export type PageManifestEntry = BlockManifestEntry;
43
+
41
44
  export interface Manifest {
42
45
  version: number;
43
46
  features: Record<string, FeatureManifestEntry>;
44
47
  components: Record<string, ComponentManifestEntry>;
45
48
  blocks: Record<string, BlockManifestEntry>;
49
+ pages: Record<string, PageManifestEntry>;
46
50
  }
47
51
 
48
52
  function emptyManifest(): Manifest {
49
- return { version: MANIFEST_VERSION, features: {}, components: {}, blocks: {} };
53
+ return { version: MANIFEST_VERSION, features: {}, components: {}, blocks: {}, pages: {} };
50
54
  }
51
55
 
52
56
  export function readManifest(cwd: string = process.cwd()): Manifest {
@@ -59,6 +63,7 @@ export function readManifest(cwd: string = process.cwd()): Manifest {
59
63
  features: parsed.features ?? {},
60
64
  components: parsed.components ?? {},
61
65
  blocks: parsed.blocks ?? {},
66
+ pages: parsed.pages ?? {},
62
67
  };
63
68
  } catch {
64
69
  return emptyManifest();
@@ -99,3 +104,13 @@ export function recordBlock(
99
104
  manifest.blocks[name] = { installedAt: new Date().toISOString(), ...entry };
100
105
  writeManifest(manifest, cwd);
101
106
  }
107
+
108
+ export function recordPage(
109
+ cwd: string,
110
+ name: string,
111
+ entry: Omit<PageManifestEntry, "installedAt">,
112
+ ): void {
113
+ const manifest = readManifest(cwd);
114
+ manifest.pages[name] = { installedAt: new Date().toISOString(), ...entry };
115
+ writeManifest(manifest, cwd);
116
+ }
@@ -0,0 +1,141 @@
1
+ import { join, dirname } from "path";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ import * as p from "@clack/prompts";
4
+ import {
5
+ type InstallOptions,
6
+ resolveLocalRegistryOrExit,
7
+ readRegistryJSON,
8
+ readRegistryFile,
9
+ writeRegistryFile,
10
+ mergePkgJson,
11
+ bunAdd,
12
+ } from "./registry.ts";
13
+ import { addComponent, initAddRegistry, ensureUtils } from "./add.ts";
14
+ import { runAddBlock } from "./block.ts";
15
+ import { mergeFontImports } from "./fonts.ts";
16
+ import { recordPage } from "./manifest.ts";
17
+
18
+ // ─── bun x bosia@latest add page <category>/<name> ───────
19
+ // Installs a full page (a group of blocks, no backend) into
20
+ // src/lib/pages/<path>/. Recursively installs the block and
21
+ // component dependencies the page composes, plus npm deps and
22
+ // optional Google Fonts @imports into app.css.
23
+
24
+ interface PageMeta {
25
+ name: string;
26
+ description: string;
27
+ category: string;
28
+ themes?: string[];
29
+ dependencies: string[]; // "blocks/..." or "ui/..." entries
30
+ files: string[];
31
+ fonts?: Record<string, string>; // family → @import URL
32
+ npmDeps: Record<string, string>;
33
+ }
34
+
35
+ // Track already-installed pages within a session to avoid redundant work.
36
+ const installed = new Set<string>();
37
+
38
+ export async function runAddPage(
39
+ name: string | undefined,
40
+ flags: string[] = [],
41
+ options?: InstallOptions,
42
+ ) {
43
+ if (!name || !name.includes("/")) {
44
+ console.error(
45
+ "❌ Please provide a page path.\n Usage: bun x bosia@latest add page <category>/<name> [-y] [--local]",
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ const local = flags.includes("--local");
51
+ const flagYes = flags.includes("-y") || flags.includes("--yes");
52
+ const inheritedRoot = options?.registryRoot ?? null;
53
+ const registryRoot = inheritedRoot ?? (local ? resolveLocalRegistryOrExit() : null);
54
+ if (local && !inheritedRoot) console.log(`⬡ Using local registry: ${registryRoot}\n`);
55
+
56
+ const resolvedOptions: InstallOptions = {
57
+ ...(options ?? {}),
58
+ registryRoot,
59
+ skipPrompts: options?.skipPrompts ?? flagYes,
60
+ };
61
+
62
+ await initAddRegistry(registryRoot);
63
+ ensureUtils(resolvedOptions.cwd);
64
+
65
+ if (installed.has(name)) return;
66
+ installed.add(name);
67
+
68
+ console.log(`⬡ Installing page: ${name}\n`);
69
+
70
+ const meta = await readRegistryJSON<PageMeta>(registryRoot, "pages", name, "meta.json");
71
+
72
+ // 1. Install dependencies first.
73
+ // Block deps (e.g. "blocks/storefront/header") recurse into runAddBlock.
74
+ // Component deps (e.g. "ui/button") go through addComponent.
75
+ for (const dep of meta.dependencies ?? []) {
76
+ if (dep.startsWith("blocks/")) {
77
+ await runAddBlock(dep.slice("blocks/".length), [], resolvedOptions);
78
+ } else {
79
+ await addComponent(dep, false, resolvedOptions);
80
+ }
81
+ }
82
+
83
+ // 2. Copy page files to src/lib/pages/<path>/
84
+ const cwd = resolvedOptions.cwd ?? process.cwd();
85
+ const destDir = join(cwd, "src", "lib", "pages", name);
86
+
87
+ if (!resolvedOptions.skipPrompts && existsSync(destDir)) {
88
+ const replace = await p.confirm({
89
+ message: `Page "${name}" already exists at src/lib/pages/${name}/. Replace it?`,
90
+ });
91
+ if (p.isCancel(replace) || !replace) {
92
+ console.log(` ⏭️ Skipped ${name}`);
93
+ return;
94
+ }
95
+ }
96
+
97
+ mkdirSync(destDir, { recursive: true });
98
+
99
+ for (const file of meta.files) {
100
+ const content = await readRegistryFile(registryRoot, "pages", name, file);
101
+ const dest = join(destDir, file);
102
+ if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
103
+ writeRegistryFile(dest, content);
104
+ console.log(` ✍️ src/lib/pages/${name}/${file}`);
105
+ }
106
+
107
+ // 3. Merge font @imports into app.css (idempotent)
108
+ if (meta.fonts && Object.keys(meta.fonts).length > 0) {
109
+ const cssPath = join(cwd, "src", "app.css");
110
+ if (existsSync(cssPath)) {
111
+ const added = mergeFontImports(cssPath, meta.fonts);
112
+ if (added.length > 0) {
113
+ console.log(` 🔤 Added fonts to app.css: ${added.join(", ")}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ // 4. npm deps
119
+ if (meta.npmDeps && Object.keys(meta.npmDeps).length > 0) {
120
+ if (resolvedOptions.skipInstall) {
121
+ const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
122
+ if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
123
+ } else {
124
+ await bunAdd(cwd, meta.npmDeps);
125
+ }
126
+ }
127
+
128
+ // 5. Record install in bosia.json manifest.
129
+ recordPage(cwd, name, {
130
+ files: meta.files,
131
+ ...(meta.npmDeps && Object.keys(meta.npmDeps).length > 0
132
+ ? { npmDeps: Object.keys(meta.npmDeps) }
133
+ : {}),
134
+ ...(meta.dependencies && meta.dependencies.length > 0
135
+ ? { dependencies: meta.dependencies }
136
+ : {}),
137
+ ...(meta.fonts && Object.keys(meta.fonts).length > 0 ? { fonts: Object.keys(meta.fonts) } : {}),
138
+ });
139
+
140
+ console.log(`\n✅ ${name} installed at src/lib/pages/${name}/`);
141
+ }
package/src/cli/theme.ts CHANGED
@@ -18,6 +18,9 @@ interface ThemeMeta {
18
18
 
19
19
  const THEME_IMPORT_RE = /^@import\s+["']\.\/lib\/themes\/[^"']+["'];?\s*$/m;
20
20
  const THEME_MARKER = "/* bosia-theme */";
21
+ // Sentinel-bounded :root/.dark block the templates ship (see templates/*/src/app.css).
22
+ // Removing it targets exactly the template defaults and never user-authored :root rules.
23
+ const THEME_VARS_RE = /\/\* bosia-theme-vars[\s\S]*?\/\* \/bosia-theme-vars \*\//;
21
24
 
22
25
  export async function runAddTheme(name: string | undefined, flags: string[] = []) {
23
26
  if (!name) {
@@ -52,6 +55,11 @@ export async function runAddTheme(name: string | undefined, flags: string[] = []
52
55
  if (existsSync(appCssPath)) {
53
56
  patchAppCssThemeImport(appCssPath, name);
54
57
  console.log(` 🎨 app.css → @import "./lib/themes/${name}.css"`);
58
+ // The installed theme css owns :root/.dark now — drop the template's default
59
+ // block so two same-specificity :root rules don't fight (template wins by order).
60
+ if (stripTemplateThemeVars(appCssPath)) {
61
+ console.log(` 🧹 app.css → removed template :root/.dark vars (theme owns them)`);
62
+ }
55
63
  } else {
56
64
  console.warn(` ⚠️ src/app.css not found — theme import not wired automatically.`);
57
65
  }
@@ -86,3 +94,15 @@ function patchAppCssThemeImport(appCssPath: string, themeName: string) {
86
94
  }
87
95
  writeFileSync(appCssPath, next, "utf-8");
88
96
  }
97
+
98
+ // Strip the sentinel-bounded template :root/.dark block. Idempotent: once removed
99
+ // (or on an app that never had it), the marker is gone and this is a no-op. Only
100
+ // the raw token blocks go — `@theme {}` (Tailwind mapping) lives above the markers
101
+ // and is untouched.
102
+ function stripTemplateThemeVars(appCssPath: string): boolean {
103
+ const src = readFileSync(appCssPath, "utf-8");
104
+ if (!THEME_VARS_RE.test(src)) return false;
105
+ const next = src.replace(THEME_VARS_RE, "").replace(/\n{3,}/g, "\n\n");
106
+ writeFileSync(appCssPath, next, "utf-8");
107
+ return true;
108
+ }
package/src/core/dev.ts CHANGED
@@ -42,7 +42,27 @@ const BACKOFF_SCHEDULE_MS = [500, 1_000, 2_000, 4_000, 5_000];
42
42
 
43
43
  // ─── SSE Broadcast ────────────────────────────────────────
44
44
 
45
+ // Reload-hold control (driven by titoko via /__bosia/hold + /__bosia/resume).
46
+ // While held, rebuilds keep happening (latest code stays ready) but the reload
47
+ // broadcast is suppressed; on resume a single reload is flushed if any rebuild
48
+ // fired meanwhile. Defaults to off, so a plain `bosia dev` developer never sees
49
+ // any behaviour change.
50
+ let reloadHeld = false;
51
+ let reloadQueuedWhileHeld = false;
52
+ let holdSafetyTimer: ReturnType<typeof setTimeout> | null = null;
53
+ // Safety net for a *dead* orchestrator only — NOT a task duration cap. While an
54
+ // agent run is healthy the orchestrator heartbeats `/__bosia/hold` (re-arming
55
+ // this timer), so it never fires mid-task no matter how long the task runs. It
56
+ // fires only if the heartbeats stop (titoko crash, network partition), so a
57
+ // missed resume can't freeze the preview forever. Must comfortably exceed the
58
+ // heartbeat interval so one dropped ping doesn't trip it.
59
+ const HOLD_SAFETY_MS = 90_000;
60
+
45
61
  function broadcastReload() {
62
+ if (reloadHeld) {
63
+ reloadQueuedWhileHeld = true;
64
+ return;
65
+ }
46
66
  const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
47
67
  for (const ctrl of sseClients) {
48
68
  try {
@@ -56,6 +76,41 @@ function broadcastReload() {
56
76
  }
57
77
  }
58
78
 
79
+ // 503 body served while the inner app server is mid-restart (rebuild after an
80
+ // edit). Must be HTML carrying the SAME SSE reload client as the dev-500 page —
81
+ // the bare text/plain version had no live `/__bosia/sse` connection, so once an
82
+ // iframe landed here (e.g. a reload racing into a rebuild window) it stayed stuck
83
+ // until a manual reload. With the client, the next `broadcastReload()` once the
84
+ // app binds reloads this page automatically. Keep the literal phrase
85
+ // "App server is starting" in the body: titoko's proxy retry matches on it.
86
+ const STARTING_PAGE = `<!doctype html>
87
+ <html lang="en">
88
+ <head>
89
+ <meta charset="utf-8" />
90
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
91
+ <title>Starting…</title>
92
+ <style>
93
+ html,body{margin:0;padding:0;height:100%;background:#0a0a0a;color:#e5e5e5;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
94
+ .wrap{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
95
+ .dot{display:inline-block;width:10px;height:10px;background:#16a34a;border-radius:50%;margin-right:8px;vertical-align:middle;animation:p 1.4s ease-in-out infinite}
96
+ @keyframes p{0%,100%{opacity:1}50%{opacity:.3}}
97
+ h1{font-size:16px;font-weight:600;margin:0}
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="wrap"><h1><span class="dot"></span>App server is starting…</h1></div>
102
+ <script>
103
+ !function r(){
104
+ try{
105
+ var e=new EventSource("/__bosia/sse");
106
+ e.addEventListener("reload",function(){location.reload()});
107
+ e.onerror=function(){e.close();setTimeout(r,2000)};
108
+ }catch(_){setTimeout(r,2000)}
109
+ }();
110
+ </script>
111
+ </body>
112
+ </html>`;
113
+
59
114
  // ─── Build ────────────────────────────────────────────────
60
115
 
61
116
  const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
@@ -235,6 +290,47 @@ const devServer = Bun.serve({
235
290
  );
236
291
  }
237
292
 
293
+ // Reload-hold control — host orchestrator (titoko) brackets an AI agent run
294
+ // so the preview reloads once when the agent finishes, not once per file
295
+ // edit. Both routes are idempotent and return small JSON.
296
+ //
297
+ // POST /__bosia/hold doubles as the heartbeat: the FIRST hold opens a fresh
298
+ // window (clears any stale queued reload); subsequent holds only re-arm the
299
+ // safety timer and MUST preserve `reloadQueuedWhileHeld`, or a heartbeat
300
+ // landing after a suppressed rebuild would drop the pending reload and
301
+ // resume would flush nothing.
302
+ if (url.pathname === "/__bosia/hold" && req.method === "POST") {
303
+ if (!reloadHeld) {
304
+ reloadHeld = true;
305
+ reloadQueuedWhileHeld = false;
306
+ }
307
+ if (holdSafetyTimer) clearTimeout(holdSafetyTimer);
308
+ holdSafetyTimer = setTimeout(() => {
309
+ holdSafetyTimer = null;
310
+ if (!reloadHeld) return;
311
+ console.warn("⏱️ Reload hold safety timeout — auto-resuming");
312
+ reloadHeld = false;
313
+ if (reloadQueuedWhileHeld) {
314
+ reloadQueuedWhileHeld = false;
315
+ broadcastReload();
316
+ }
317
+ }, HOLD_SAFETY_MS);
318
+ holdSafetyTimer.unref?.();
319
+ return Response.json({ ok: true, held: true });
320
+ }
321
+
322
+ if (url.pathname === "/__bosia/resume" && req.method === "POST") {
323
+ if (holdSafetyTimer) {
324
+ clearTimeout(holdSafetyTimer);
325
+ holdSafetyTimer = null;
326
+ }
327
+ reloadHeld = false;
328
+ const flushed = reloadQueuedWhileHeld;
329
+ reloadQueuedWhileHeld = false;
330
+ if (flushed) broadcastReload();
331
+ return Response.json({ ok: true, held: false, flushed });
332
+ }
333
+
238
334
  // Proxy everything else to the app server. Inject X-Forwarded-Host/Proto so
239
335
  // the app's CSRF origin check (gated behind TRUST_PROXY=true, also set in the
240
336
  // app env above) reconstructs the public-facing origin from the dev proxy
@@ -283,9 +379,9 @@ const devServer = Bun.serve({
283
379
  await Bun.sleep(250);
284
380
  continue;
285
381
  }
286
- return new Response("App server is starting...", {
382
+ return new Response(STARTING_PAGE, {
287
383
  status: 503,
288
- headers: { "Content-Type": "text/plain", "Retry-After": "1" },
384
+ headers: { "Content-Type": "text/html; charset=utf-8", "Retry-After": "1" },
289
385
  });
290
386
  }
291
387
  }
package/src/core/html.ts CHANGED
@@ -19,6 +19,12 @@ export const distManifest: { js: string[]; css: string[]; entry: string } = (()
19
19
  export const isDev = process.env.NODE_ENV !== "production";
20
20
  const cacheBust = isDev ? `?v=${Date.now()}` : "";
21
21
 
22
+ /** Inline theme bootstrap — runs before paint to avoid FOUC. theme ∈ light|dark|system (missing = system). */
23
+ const THEME_INIT_JS =
24
+ "try{var t=localStorage.getItem('theme');" +
25
+ "document.documentElement.classList.toggle('dark'," +
26
+ "t==='dark'||((t===null||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches))}catch(_){}";
27
+
22
28
  // ─── Safe JSON Serialization ──────────────────────────────
23
29
 
24
30
  /** Escapes JSON for safe embedding inside <script> tags. Prevents XSS via </script> injection. */
@@ -156,7 +162,7 @@ export function buildHtml(
156
162
  headOpenInterpolated +
157
163
  `\n ${faviconLine}${cssLinks}\n` +
158
164
  ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
159
- ` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
165
+ ` <script${n}>${THEME_INIT_JS}</script>\n` +
160
166
  ` ${fallbackTitle}${head}` +
161
167
  headCloseInterpolated +
162
168
  (body ? "" : `\n${SPINNER}`) +
@@ -175,7 +181,7 @@ export function buildHtml(
175
181
  ${head}
176
182
  ${cssLinks}
177
183
  <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">
178
- <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>
184
+ <script${n}>${THEME_INIT_JS}</script>
179
185
  </head>
180
186
  <body>
181
187
  <div id="app">${body}</div>${scripts}${bodyEnd}
@@ -208,7 +214,7 @@ export function buildHtmlShellOpen(
208
214
  headOpenInterpolated +
209
215
  `\n ${faviconLine}${cssLinks}\n` +
210
216
  ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
211
- ` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
217
+ ` <script${n}>${THEME_INIT_JS}</script>\n` +
212
218
  ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
213
219
  );
214
220
  }
@@ -220,7 +226,7 @@ export function buildHtmlShellOpen(
220
226
  ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
221
227
  ` ${cssLinks}\n` +
222
228
  ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
223
- ` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
229
+ ` <script${n}>${THEME_INIT_JS}</script>\n` +
224
230
  ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
225
231
  );
226
232
  }
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "bosia": "^{{BOSIA_VERSION}}",
15
- "svelte": "^5.20.0",
15
+ "svelte": "^5.56.3",
16
16
  "tailwind-merge": "^3.5.0"
17
17
  },
18
18
  "devDependencies": {
@@ -44,6 +44,10 @@
44
44
  --radius-xl: calc(var(--radius) + 4px);
45
45
  }
46
46
 
47
+ /* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
48
+ between these markers (the installed theme owns these tokens). Do NOT add your
49
+ own :root rules inside the markers — put custom overrides after the close tag. */
50
+
47
51
  /* ─── Light Theme (Default) ─────────────────────────────── */
48
52
 
49
53
  :root {
@@ -110,6 +114,8 @@
110
114
  --ring: 212.7 26.8% 83.9%;
111
115
  }
112
116
 
117
+ /* /bosia-theme-vars */
118
+
113
119
  /* ─── Base Styles ────────────────────────────────────────── */
114
120
 
115
121
  @layer base {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "bosia": "^{{BOSIA_VERSION}}",
15
- "svelte": "^5.20.0",
15
+ "svelte": "^5.56.3",
16
16
  "tailwind-merge": "^3.5.0"
17
17
  },
18
18
  "devDependencies": {
@@ -44,6 +44,10 @@
44
44
  --radius-xl: calc(var(--radius) + 4px);
45
45
  }
46
46
 
47
+ /* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
48
+ between these markers (the installed theme owns these tokens). Do NOT add your
49
+ own :root rules inside the markers — put custom overrides after the close tag. */
50
+
47
51
  /* ─── Light Theme (Default) ─────────────────────────────── */
48
52
 
49
53
  :root {
@@ -110,6 +114,8 @@
110
114
  --ring: 212.7 26.8% 83.9%;
111
115
  }
112
116
 
117
+ /* /bosia-theme-vars */
118
+
113
119
  /* ─── Base Styles ────────────────────────────────────────── */
114
120
 
115
121
  @layer base {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "bosia": "^{{BOSIA_VERSION}}",
15
- "svelte": "^5.20.0",
15
+ "svelte": "^5.56.3",
16
16
  "tailwind-merge": "^3.5.0",
17
17
  "drizzle-orm": "^0.44.0"
18
18
  },
@@ -44,6 +44,10 @@
44
44
  --radius-xl: calc(var(--radius) + 4px);
45
45
  }
46
46
 
47
+ /* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
48
+ between these markers (the installed theme owns these tokens). Do NOT add your
49
+ own :root rules inside the markers — put custom overrides after the close tag. */
50
+
47
51
  /* ─── Light Theme (Default) ─────────────────────────────── */
48
52
 
49
53
  :root {
@@ -110,6 +114,8 @@
110
114
  --ring: 212.7 26.8% 83.9%;
111
115
  }
112
116
 
117
+ /* /bosia-theme-vars */
118
+
113
119
  /* ─── Base Styles ────────────────────────────────────────── */
114
120
 
115
121
  @layer base {
@@ -1,2 +0,0 @@
1
- # Database
2
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
@@ -1,7 +0,0 @@
1
- node_modules
2
- dist
3
- build
4
- .bosia
5
- bun.lock
6
- public/bosia-tw.css
7
- bosia.json
@@ -1,9 +0,0 @@
1
- {
2
- "useTabs": true,
3
- "tabWidth": 2,
4
- "singleQuote": false,
5
- "trailingComma": "all",
6
- "printWidth": 100,
7
- "plugins": ["prettier-plugin-svelte"],
8
- "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
- }
@@ -1,69 +0,0 @@
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
- ```
@@ -1,12 +0,0 @@
1
- node_modules/
2
- dist/
3
- .bosia/
4
- .DS_Store
5
- *.log
6
-
7
- # Generated Tailwind output
8
- public/bosia-tw.css
9
-
10
- # Local env overrides — never commit secrets
11
- .env*.local
12
- .env
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "bosia";
2
- import { inspector } from "bosia/plugins/inspector";
3
-
4
- export default defineConfig({
5
- plugins: [
6
- // Dev-only: Alt+click any element on the page to open its source in your editor.
7
- // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
- inspector({ editor: "code" }),
9
- ],
10
- });
@@ -1,3 +0,0 @@
1
- Update .env with your DATABASE_URL
2
- bun run db:generate
3
- bun run db:migrate
@@ -1,24 +0,0 @@
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
- "check": "tsc --noEmit && prettier --check .",
10
- "format": "prettier --write .",
11
- "format:check": "prettier --check ."
12
- },
13
- "dependencies": {
14
- "bosia": "^{{BOSIA_VERSION}}",
15
- "svelte": "^5.20.0",
16
- "tailwind-merge": "^3.5.0"
17
- },
18
- "devDependencies": {
19
- "@types/bun": "latest",
20
- "prettier": "^3.3.0",
21
- "prettier-plugin-svelte": "^3.2.0",
22
- "typescript": "^5"
23
- }
24
- }
File without changes
@@ -1,14 +0,0 @@
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>
@@ -1,14 +0,0 @@
1
- <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
- <!-- Top block -->
3
- <rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
4
- <rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
5
-
6
- <!-- Middle block -->
7
- <rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
8
-
9
- <!-- Bottom block -->
10
- <rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
11
-
12
- <!-- Connector bar on left -->
13
- <rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
14
- </svg>
@@ -1,14 +0,0 @@
1
- <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
- <!-- Top block -->
3
- <rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
4
- <rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
5
-
6
- <!-- Middle block -->
7
- <rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
8
-
9
- <!-- Bottom block -->
10
- <rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
11
-
12
- <!-- Connector bar on left -->
13
- <rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
14
- </svg>
@@ -1,134 +0,0 @@
1
- @import "tailwindcss";
2
- @source "../src";
3
-
4
- @custom-variant dark (&:where(.dark, .dark *));
5
-
6
- /*
7
- * ─── shadcn-inspired Design Tokens ──────────────────────
8
- * CSS custom properties for light & dark themes.
9
- * Uses HSL values so Tailwind can apply opacity modifiers.
10
- */
11
-
12
- @theme {
13
- --color-background: hsl(var(--background));
14
- --color-foreground: hsl(var(--foreground));
15
-
16
- --color-card: hsl(var(--card));
17
- --color-card-foreground: hsl(var(--card-foreground));
18
-
19
- --color-popover: hsl(var(--popover));
20
- --color-popover-foreground: hsl(var(--popover-foreground));
21
-
22
- --color-primary: hsl(var(--primary));
23
- --color-primary-foreground: hsl(var(--primary-foreground));
24
-
25
- --color-secondary: hsl(var(--secondary));
26
- --color-secondary-foreground: hsl(var(--secondary-foreground));
27
-
28
- --color-muted: hsl(var(--muted));
29
- --color-muted-foreground: hsl(var(--muted-foreground));
30
-
31
- --color-accent: hsl(var(--accent));
32
- --color-accent-foreground: hsl(var(--accent-foreground));
33
-
34
- --color-destructive: hsl(var(--destructive));
35
- --color-destructive-foreground: hsl(var(--destructive-foreground));
36
-
37
- --color-border: hsl(var(--border));
38
- --color-input: hsl(var(--input));
39
- --color-ring: hsl(var(--ring));
40
-
41
- --radius-sm: calc(var(--radius) - 4px);
42
- --radius-md: calc(var(--radius) - 2px);
43
- --radius-lg: var(--radius);
44
- --radius-xl: calc(var(--radius) + 4px);
45
- }
46
-
47
- /* ─── Light Theme (Default) ─────────────────────────────── */
48
-
49
- :root {
50
- --background: 0 0% 100%;
51
- --foreground: 222.2 84% 4.9%;
52
-
53
- --card: 0 0% 100%;
54
- --card-foreground: 222.2 84% 4.9%;
55
-
56
- --popover: 0 0% 100%;
57
- --popover-foreground: 222.2 84% 4.9%;
58
-
59
- --primary: 222.2 47.4% 11.2%;
60
- --primary-foreground: 210 40% 98%;
61
-
62
- --secondary: 210 40% 96.1%;
63
- --secondary-foreground: 222.2 47.4% 11.2%;
64
-
65
- --muted: 210 40% 96.1%;
66
- --muted-foreground: 215.4 16.3% 46.9%;
67
-
68
- --accent: 210 40% 96.1%;
69
- --accent-foreground: 222.2 47.4% 11.2%;
70
-
71
- --destructive: 0 84.2% 60.2%;
72
- --destructive-foreground: 210 40% 98%;
73
-
74
- --border: 214.3 31.8% 91.4%;
75
- --input: 214.3 31.8% 91.4%;
76
- --ring: 222.2 84% 4.9%;
77
-
78
- --radius: 0.5rem;
79
- }
80
-
81
- /* ─── Dark Theme ─────────────────────────────────────────── */
82
-
83
- .dark {
84
- --background: 222.2 84% 4.9%;
85
- --foreground: 210 40% 98%;
86
-
87
- --card: 222.2 84% 4.9%;
88
- --card-foreground: 210 40% 98%;
89
-
90
- --popover: 222.2 84% 4.9%;
91
- --popover-foreground: 210 40% 98%;
92
-
93
- --primary: 210 40% 98%;
94
- --primary-foreground: 222.2 47.4% 11.2%;
95
-
96
- --secondary: 217.2 32.6% 17.5%;
97
- --secondary-foreground: 210 40% 98%;
98
-
99
- --muted: 217.2 32.6% 17.5%;
100
- --muted-foreground: 215 20.2% 65.1%;
101
-
102
- --accent: 217.2 32.6% 17.5%;
103
- --accent-foreground: 210 40% 98%;
104
-
105
- --destructive: 0 62.8% 30.6%;
106
- --destructive-foreground: 210 40% 98%;
107
-
108
- --border: 217.2 32.6% 17.5%;
109
- --input: 217.2 32.6% 17.5%;
110
- --ring: 212.7 26.8% 83.9%;
111
- }
112
-
113
- /* ─── Base Styles ────────────────────────────────────────── */
114
-
115
- @layer base {
116
- * {
117
- border-color: theme(--color-border);
118
- }
119
-
120
- body {
121
- background-color: theme(--color-background);
122
- color: theme(--color-foreground);
123
- font-family:
124
- "Inter",
125
- system-ui,
126
- -apple-system,
127
- BlinkMacSystemFont,
128
- "Segoe UI",
129
- Roboto,
130
- "Helvetica Neue",
131
- Arial,
132
- sans-serif;
133
- }
134
- }
@@ -1,14 +0,0 @@
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
- }
8
-
9
- declare namespace App {
10
- interface Locals {
11
- db: import("./features/drizzle").Database;
12
- requestTime: number;
13
- }
14
- }
@@ -1,11 +0,0 @@
1
- <!doctype html>
2
- <html lang="%bosia.lang%">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- %bosia.head%
7
- </head>
8
- <body>
9
- %bosia.body%
10
- </body>
11
- </html>
@@ -1,20 +0,0 @@
1
- import { sequence } from "bosia";
2
- import type { Handle } from "bosia";
3
- import { db } from "./features/drizzle";
4
-
5
- const dbHandle: Handle = async ({ event, resolve }) => {
6
- event.locals.db = db;
7
- return resolve(event);
8
- };
9
-
10
- const loggingHandle: Handle = async ({ event, resolve }) => {
11
- const start = Date.now();
12
- event.locals.requestTime = start;
13
- const res = await resolve(event);
14
- const ms = Date.now() - start;
15
- console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
16
- res.headers.set("X-Response-Time", `${ms}ms`);
17
- return res;
18
- };
19
-
20
- export const handle = sequence(dbHandle, loggingHandle);
@@ -1 +0,0 @@
1
- export { cn } from "bosia";
@@ -1,53 +0,0 @@
1
- <script lang="ts">
2
- import type { LayoutData } from "./$types";
3
- let { data }: { data: LayoutData } = $props();
4
- </script>
5
-
6
- <svelte:head>
7
- <title>{data.appName}</title>
8
- </svelte:head>
9
-
10
- <div class="flex min-h-screen flex-col bg-background text-foreground">
11
- <div class="flex flex-1 flex-col items-center justify-center gap-8 px-4">
12
- <div class="text-center">
13
- <img src="/favicon.svg" alt="" class="mx-auto mb-6 size-16" />
14
- <h1 class="text-4xl font-bold tracking-tight">{data.appName}</h1>
15
- <p class="mt-2 text-lg text-muted-foreground">
16
- Fullstack app powered by Bosia + Drizzle ORM + PostgreSQL
17
- </p>
18
- </div>
19
-
20
- <div class="flex gap-4">
21
- <a
22
- href="/todos"
23
- class="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
24
- >
25
- Todo App
26
- </a>
27
- <a
28
- href="/api/todos"
29
- target="_blank"
30
- class="inline-flex items-center justify-center rounded-md border border-input bg-background px-6 py-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"
31
- >
32
- REST API
33
- </a>
34
- </div>
35
-
36
- <div class="mt-8 grid max-w-2xl grid-cols-1 gap-4 sm:grid-cols-3">
37
- <div class="rounded-lg border border-border p-4 text-center">
38
- <p class="text-sm font-medium">Bosia</p>
39
- <p class="mt-1 text-xs text-muted-foreground">Bun + Svelte 5 + Elysia</p>
40
- </div>
41
- <div class="rounded-lg border border-border p-4 text-center">
42
- <p class="text-sm font-medium">Drizzle ORM</p>
43
- <p class="mt-1 text-xs text-muted-foreground">Type-safe SQL queries</p>
44
- </div>
45
- <div class="rounded-lg border border-border p-4 text-center">
46
- <p class="text-sm font-medium">PostgreSQL</p>
47
- <p class="mt-1 text-xs text-muted-foreground">Production-ready database</p>
48
- </div>
49
- </div>
50
- </div>
51
-
52
- <footer class="border-t py-4 text-center text-sm text-muted-foreground">Powered by Bosia</footer>
53
- </div>
@@ -1,19 +0,0 @@
1
- <script lang="ts">
2
- import type { ErrorProps } from "./$types";
3
- let { error }: ErrorProps = $props();
4
- </script>
5
-
6
- <svelte:head>
7
- <title>{error.status} — {error.message}</title>
8
- </svelte:head>
9
-
10
- <div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
11
- <p class="text-8xl font-bold text-gray-200">{error.status}</p>
12
- <p class="text-2xl font-semibold text-gray-700">{error.message}</p>
13
- <a
14
- href="/"
15
- class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors"
16
- >
17
- Go home
18
- </a>
19
- </div>
@@ -1,8 +0,0 @@
1
- import type { LoadEvent } from "bosia";
2
-
3
- export async function load({ locals }: LoadEvent) {
4
- return {
5
- appName: "Bosia Todo",
6
- requestTime: (locals.requestTime as number | null) ?? null,
7
- };
8
- }
@@ -1,6 +0,0 @@
1
- <script lang="ts">
2
- import "../app.css";
3
- let { children }: { children: any } = $props();
4
- </script>
5
-
6
- {@render children()}
@@ -1,6 +0,0 @@
1
- {
2
- "features": ["todo"],
3
- "featureOptions": {
4
- "drizzle.dialect": "postgres"
5
- }
6
- }
@@ -1,22 +0,0 @@
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": [".", ".bosia/types"],
15
- "paths": {
16
- "$lib": ["./src/lib"],
17
- "$lib/*": ["./src/lib/*"]
18
- }
19
- },
20
- "include": ["src/**/*"],
21
- "exclude": ["node_modules", "dist"]
22
- }