bosia 0.6.24 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.6.24",
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
 
@@ -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/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)
@@ -139,6 +144,8 @@ Examples:
139
144
  bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
140
145
  bun x bosia@latest add block cards/feature
141
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
+ }
@@ -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 {
@@ -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 {
@@ -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 {