bosia 0.6.20 → 0.6.21

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.20",
3
+ "version": "0.6.21",
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
@@ -7,6 +7,7 @@ import {
7
7
  resolveLocalRegistryOrExit,
8
8
  readRegistryJSON,
9
9
  readRegistryFile,
10
+ writeRegistryFile,
10
11
  mergePkgJson,
11
12
  bunAdd,
12
13
  } from "./registry.ts";
@@ -150,7 +151,7 @@ export async function addComponent(name: string, root = false, options?: Install
150
151
  const content = await readRegistryFile(registryRoot, "components", fullPath, file);
151
152
  const dest = join(destDir, file);
152
153
  if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
153
- writeFileSync(dest, content, "utf-8");
154
+ writeRegistryFile(dest, content);
154
155
  console.log(` ✍️ src/lib/components/${fullPath}/${file}`);
155
156
  }
156
157
 
@@ -0,0 +1,53 @@
1
+ // ─── Dispatch logic for `bosia add ...` ──────────────────
2
+ // Split out from index.ts so the routing can be unit-tested with injected
3
+ // runners (index.ts does top-level `process.argv` parsing on import).
4
+
5
+ export interface AddRunners {
6
+ runAdd: (names: string[], flags: string[]) => Promise<void> | void;
7
+ runAddBlock: (name: string | undefined, flags: string[]) => Promise<void> | void;
8
+ runAddTheme: (name: string | undefined, flags: string[]) => Promise<void> | void;
9
+ runAddFont: (family: string | undefined, url: string | undefined) => Promise<void> | void;
10
+ runAddList: () => Promise<void> | void;
11
+ }
12
+
13
+ export async function routeAdd(args: string[], runners: AddRunners): Promise<void> {
14
+ const positional = args.filter((a) => !a.startsWith("-"));
15
+ const flags = args.filter((a) => a.startsWith("-"));
16
+ const sub = positional[0];
17
+
18
+ if (sub === "block") {
19
+ await runners.runAddBlock(positional[1], flags);
20
+ return;
21
+ }
22
+ if (sub === "theme") {
23
+ const themeFlags = args.filter((a) => a.startsWith("--"));
24
+ await runners.runAddTheme(positional[1], themeFlags);
25
+ return;
26
+ }
27
+ if (sub === "font") {
28
+ await runners.runAddFont(positional[1], positional[2]);
29
+ return;
30
+ }
31
+ if (sub === "list") {
32
+ await runners.runAddList();
33
+ return;
34
+ }
35
+
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.
40
+ const blockTokens = positional.filter((p) => p.startsWith("blocks/"));
41
+ const componentTokens = positional.filter((p) => !p.startsWith("blocks/"));
42
+ if (blockTokens.length > 0) {
43
+ if (componentTokens.length > 0) {
44
+ await runners.runAdd(componentTokens, flags);
45
+ }
46
+ for (const token of blockTokens) {
47
+ await runners.runAddBlock(token.slice("blocks/".length), flags);
48
+ }
49
+ return;
50
+ }
51
+
52
+ await runners.runAdd(positional, flags);
53
+ }
package/src/cli/block.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { join, dirname } from "path";
2
- import { mkdirSync, writeFileSync, existsSync } from "fs";
2
+ import { mkdirSync, existsSync } from "fs";
3
3
  import * as p from "@clack/prompts";
4
4
  import {
5
5
  type InstallOptions,
6
6
  resolveLocalRegistryOrExit,
7
7
  readRegistryJSON,
8
8
  readRegistryFile,
9
+ writeRegistryFile,
9
10
  mergePkgJson,
10
11
  bunAdd,
11
12
  } from "./registry.ts";
@@ -83,7 +84,7 @@ export async function runAddBlock(
83
84
  const content = await readRegistryFile(registryRoot, "blocks", name, file);
84
85
  const dest = join(destDir, file);
85
86
  if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
86
- writeFileSync(dest, content, "utf-8");
87
+ writeRegistryFile(dest, content);
87
88
  console.log(` ✍️ src/lib/blocks/${name}/${file}`);
88
89
  }
89
90
 
package/src/cli/index.ts CHANGED
@@ -59,27 +59,29 @@ async function main() {
59
59
  break;
60
60
  }
61
61
  case "add": {
62
- const positional = args.filter((a) => !a.startsWith("-"));
63
- const flags = args.filter((a) => a.startsWith("-"));
64
- const sub = positional[0];
65
- if (sub === "block") {
66
- const blockFlags = flags;
67
- const { runAddBlock } = await import("./block.ts");
68
- await runAddBlock(positional[1], blockFlags);
69
- } else if (sub === "theme") {
70
- const themeFlags = args.filter((a) => a.startsWith("--"));
71
- const { runAddTheme } = await import("./theme.ts");
72
- await runAddTheme(positional[1], themeFlags);
73
- } else if (sub === "font") {
74
- const { runAddFont } = await import("./font.ts");
75
- await runAddFont(positional[1], positional[2]);
76
- } else if (sub === "list") {
77
- const { runAddList } = await import("./add.ts");
78
- runAddList();
79
- } else {
80
- const { runAdd } = await import("./add.ts");
81
- await runAdd(positional, flags);
82
- }
62
+ const { routeAdd } = await import("./addRouter.ts");
63
+ await routeAdd(args, {
64
+ runAdd: async (names, flags) => {
65
+ const { runAdd } = await import("./add.ts");
66
+ await runAdd(names, flags);
67
+ },
68
+ runAddBlock: async (name, flags) => {
69
+ const { runAddBlock } = await import("./block.ts");
70
+ await runAddBlock(name, flags);
71
+ },
72
+ runAddTheme: async (name, flags) => {
73
+ const { runAddTheme } = await import("./theme.ts");
74
+ await runAddTheme(name, flags);
75
+ },
76
+ runAddFont: async (family, url) => {
77
+ const { runAddFont } = await import("./font.ts");
78
+ await runAddFont(family, url);
79
+ },
80
+ runAddList: async () => {
81
+ const { runAddList } = await import("./add.ts");
82
+ runAddList();
83
+ },
84
+ });
83
85
  break;
84
86
  }
85
87
  case "feat": {
@@ -137,6 +139,7 @@ Examples:
137
139
  bun x bosia@latest add -y button card → auto-confirm overwrites (CI / scripts)
138
140
  bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
139
141
  bun x bosia@latest add block cards/feature-editorial
142
+ bun x bosia@latest add blocks/cards/feature-editorial (alias for: add block cards/feature-editorial)
140
143
  bun x bosia@latest add theme editorial
141
144
  bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
142
145
  bun x bosia@latest feat login
@@ -1,5 +1,5 @@
1
1
  import { join, dirname } from "path";
2
- import { writeFileSync, readFileSync, existsSync } from "fs";
2
+ import { writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
3
3
  import { spawn } from "bun";
4
4
 
5
5
  // ─── Shared registry utilities for feat.ts and add.ts ─────
@@ -80,6 +80,42 @@ export async function readRegistryFile(
80
80
  return fetchText(`${REGISTRY_URL}/${category}/${name}/${file}`);
81
81
  }
82
82
 
83
+ // ─── Registry file writer ─────────────────────────────────
84
+
85
+ /**
86
+ * Write a registry file with one EACCES/EPERM recovery attempt.
87
+ *
88
+ * Component/block file loops abort mid-install when a target path is owned by a
89
+ * different uid — bosapi tenant apps run sandboxed as `bosapi-app-N`, so a
90
+ * subsequent install from the bosapi user hits EACCES on the first foreign-owned
91
+ * file and leaves a partial install behind. Unlink + retry recovers; only the
92
+ * unrecoverable case surfaces the chown hint.
93
+ */
94
+ export function writeRegistryFile(dest: string, content: string): void {
95
+ try {
96
+ writeFileSync(dest, content, "utf-8");
97
+ return;
98
+ } catch (err) {
99
+ const code = (err as NodeJS.ErrnoException).code;
100
+ if (code !== "EACCES" && code !== "EPERM") throw err;
101
+ }
102
+ try {
103
+ unlinkSync(dest);
104
+ } catch {
105
+ // ignore — retry will surface the real error
106
+ }
107
+ try {
108
+ writeFileSync(dest, content, "utf-8");
109
+ } catch (retry) {
110
+ const e = retry as NodeJS.ErrnoException;
111
+ throw new Error(
112
+ `Cannot write ${dest}: ${e.code}. ` +
113
+ `The existing file is owned by a different user (likely created from inside ` +
114
+ `the app sandbox). Fix from the project root: chown -R $(whoami) src/lib`,
115
+ );
116
+ }
117
+ }
118
+
83
119
  // ─── package.json helpers ─────────────────────────────────
84
120
 
85
121
  export interface PkgDeps {
@@ -0,0 +1,28 @@
1
+ // ─── Reactive page object ─────────────────────────────────
2
+ // Mirrors what user-facing skills (bosia-page-shell, bosia-seo,
3
+ // bosia-navigation) teach: `import { page } from "bosia/client"` then read
4
+ // `page.url.pathname`. Backed by `router.currentRoute` (`$state` in
5
+ // router.svelte.ts), so the `$derived` URL re-runs on every nav.
6
+ //
7
+ // No `params` getter on purpose — Bosia already passes `params` as a prop to
8
+ // `+page.svelte` / `+layout.svelte` (see App.svelte), mirroring the modern
9
+ // SvelteKit `$app/state` direction. Route components should destructure
10
+ // `params` from `$props()`, not from here.
11
+
12
+ import { router } from "./router.svelte.ts";
13
+
14
+ class Page {
15
+ #url = $derived.by(() => {
16
+ if (typeof window === "undefined") return new URL("http://localhost/");
17
+ return new URL(router.currentRoute, window.location.origin);
18
+ });
19
+
20
+ get url() {
21
+ return this.#url;
22
+ }
23
+ get pathname() {
24
+ return this.#url.pathname;
25
+ }
26
+ }
27
+
28
+ export const page = new Page();
package/src/lib/client.ts CHANGED
@@ -18,3 +18,4 @@ export {
18
18
  invalidateAll,
19
19
  } from "../core/client/navigation.ts";
20
20
  export type { GotoOptions, Navigation, NavigationTarget } from "../core/client/navigation.ts";
21
+ export { page } from "../core/client/page.svelte.ts";