bosia 0.6.20 → 0.6.22

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 (58) hide show
  1. package/package.json +2 -2
  2. package/src/cli/add.ts +5 -5
  3. package/src/cli/addRouter.ts +53 -0
  4. package/src/cli/block.ts +19 -12
  5. package/src/cli/create.ts +6 -11
  6. package/src/cli/feat.ts +19 -22
  7. package/src/cli/index.ts +25 -23
  8. package/src/cli/manifest.ts +1 -1
  9. package/src/cli/registry.ts +40 -2
  10. package/src/core/build.ts +1 -3
  11. package/src/core/client/App.svelte +3 -8
  12. package/src/core/client/page.svelte.ts +28 -0
  13. package/src/core/client/router.svelte.ts +3 -8
  14. package/src/core/config.ts +1 -4
  15. package/src/core/cookies.ts +1 -2
  16. package/src/core/dev-500.ts +1 -1
  17. package/src/core/html.ts +1 -2
  18. package/src/core/plugin.ts +1 -3
  19. package/src/core/plugins/inspector/bun-plugin.ts +1 -4
  20. package/src/core/plugins/inspector/index.ts +45 -59
  21. package/src/core/renderer.ts +3 -10
  22. package/src/core/routeTypes.ts +3 -9
  23. package/src/core/scanner.ts +1 -3
  24. package/src/core/server.ts +9 -34
  25. package/src/core/staticManifest.ts +1 -3
  26. package/src/core/svelteAudit.ts +2 -5
  27. package/src/core/svelteCompiler.ts +2 -8
  28. package/src/lib/client.ts +1 -0
  29. package/templates/default/.prettierignore +1 -0
  30. package/templates/demo/.prettierignore +1 -0
  31. package/templates/shop/.env.example +12 -0
  32. package/templates/shop/.prettierignore +7 -0
  33. package/templates/shop/.prettierrc.json +9 -0
  34. package/templates/shop/README.md +62 -0
  35. package/templates/shop/_gitignore +12 -0
  36. package/templates/shop/bosia.config.ts +10 -0
  37. package/templates/shop/instructions.txt +8 -0
  38. package/templates/shop/package.json +27 -0
  39. package/templates/shop/public/favicon.svg +14 -0
  40. package/templates/shop/public/logo-dark.svg +14 -0
  41. package/templates/shop/public/logo-light.svg +14 -0
  42. package/templates/shop/src/app.css +132 -0
  43. package/templates/shop/src/app.d.ts +14 -0
  44. package/templates/shop/src/app.html +11 -0
  45. package/templates/shop/src/hooks.server.ts +21 -0
  46. package/templates/shop/src/lib/utils.ts +1 -0
  47. package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
  48. package/templates/shop/src/routes/(private)/+layout.svelte +14 -0
  49. package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
  50. package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
  51. package/templates/shop/src/routes/(public)/+page.svelte +30 -0
  52. package/templates/shop/src/routes/+error.svelte +19 -0
  53. package/templates/shop/src/routes/+layout.server.ts +9 -0
  54. package/templates/shop/src/routes/+layout.svelte +6 -0
  55. package/templates/shop/template.json +10 -0
  56. package/templates/shop/tsconfig.json +22 -0
  57. package/templates/todo/.prettierignore +1 -0
  58. package/templates/todo/template.json +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
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": [
@@ -56,7 +56,7 @@
56
56
  "@tailwindcss/cli": "^4.2.1",
57
57
  "elysia": "^1.4.26",
58
58
  "magic-string": "^0.30.0",
59
- "svelte": "^5.53.6",
59
+ "svelte": "^5.56.3",
60
60
  "tailwind-merge": "^3.5.0",
61
61
  "tailwindcss": "^4.2.1"
62
62
  }
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
 
@@ -158,8 +159,7 @@ export async function addComponent(name: string, root = false, options?: Install
158
159
  if (Object.keys(meta.npmDeps).length > 0) {
159
160
  if (options?.skipInstall) {
160
161
  const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
161
- if (addedDeps.length > 0)
162
- console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
162
+ if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
163
163
  } else {
164
164
  await bunAdd(cwd, meta.npmDeps);
165
165
  }
@@ -238,8 +238,8 @@ export function cn(...inputs: ClassValue[]) {
238
238
  }
239
239
  `;
240
240
 
241
- export function ensureUtils() {
242
- const utilsPath = join(process.cwd(), "src", "lib", "utils.ts");
241
+ export function ensureUtils(cwd: string = process.cwd()) {
242
+ const utilsPath = join(cwd, "src", "lib", "utils.ts");
243
243
  if (!existsSync(utilsPath)) {
244
244
  mkdirSync(dirname(utilsPath), { recursive: true });
245
245
  writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
@@ -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";
@@ -43,24 +44,33 @@ export async function runAddBlock(
43
44
 
44
45
  const local = flags.includes("--local");
45
46
  const flagYes = flags.includes("-y") || flags.includes("--yes");
46
- const registryRoot = local ? resolveLocalRegistryOrExit() : null;
47
- if (local) console.log(`⬡ Using local registry: ${registryRoot}\n`);
47
+ // Honor an inherited registry root from options (e.g. when called from feat.ts in --local mode).
48
+ const inheritedRoot = options?.registryRoot ?? null;
49
+ const registryRoot = inheritedRoot ?? (local ? resolveLocalRegistryOrExit() : null);
50
+ if (local && !inheritedRoot) console.log(`⬡ Using local registry: ${registryRoot}\n`);
48
51
 
49
52
  const resolvedOptions: InstallOptions = {
50
53
  ...(options ?? {}),
54
+ registryRoot,
51
55
  skipPrompts: options?.skipPrompts ?? flagYes,
52
56
  };
53
57
 
54
58
  await initAddRegistry(registryRoot);
55
- ensureUtils();
59
+ ensureUtils(resolvedOptions.cwd);
56
60
 
57
61
  console.log(`⬡ Installing block: ${name}\n`);
58
62
 
59
63
  const meta = await readRegistryJSON<BlockMeta>(registryRoot, "blocks", name, "meta.json");
60
64
 
61
- // 1. Install primitive dependencies first
65
+ // 1. Install primitive dependencies first.
66
+ // Component deps (e.g. "ui/button") go through addComponent.
67
+ // Block deps (e.g. "blocks/files/upload-area") recurse into runAddBlock.
62
68
  for (const dep of meta.dependencies ?? []) {
63
- await addComponent(dep, false, resolvedOptions);
69
+ if (dep.startsWith("blocks/")) {
70
+ await runAddBlock(dep.slice("blocks/".length), [], resolvedOptions);
71
+ } else {
72
+ await addComponent(dep, false, resolvedOptions);
73
+ }
64
74
  }
65
75
 
66
76
  // 2. Copy block files to src/lib/blocks/<path>/
@@ -83,7 +93,7 @@ export async function runAddBlock(
83
93
  const content = await readRegistryFile(registryRoot, "blocks", name, file);
84
94
  const dest = join(destDir, file);
85
95
  if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
86
- writeFileSync(dest, content, "utf-8");
96
+ writeRegistryFile(dest, content);
87
97
  console.log(` ✍️ src/lib/blocks/${name}/${file}`);
88
98
  }
89
99
 
@@ -102,8 +112,7 @@ export async function runAddBlock(
102
112
  if (meta.npmDeps && Object.keys(meta.npmDeps).length > 0) {
103
113
  if (resolvedOptions.skipInstall) {
104
114
  const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
105
- if (addedDeps.length > 0)
106
- console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
115
+ if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
107
116
  } else {
108
117
  await bunAdd(cwd, meta.npmDeps);
109
118
  }
@@ -118,9 +127,7 @@ export async function runAddBlock(
118
127
  ...(meta.dependencies && meta.dependencies.length > 0
119
128
  ? { dependencies: meta.dependencies }
120
129
  : {}),
121
- ...(meta.fonts && Object.keys(meta.fonts).length > 0
122
- ? { fonts: Object.keys(meta.fonts) }
123
- : {}),
130
+ ...(meta.fonts && Object.keys(meta.fonts).length > 0 ? { fonts: Object.keys(meta.fonts) } : {}),
124
131
  });
125
132
 
126
133
  console.log(`\n✅ ${name} installed at src/lib/blocks/${name}/`);
package/src/cli/create.ts CHANGED
@@ -15,13 +15,12 @@ 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
17
  todo: "Todo app with PostgreSQL + Drizzle ORM",
18
+ shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
18
19
  };
19
20
 
20
21
  export async function runCreate(name: string | undefined, args: string[] = []) {
21
22
  if (!name) {
22
- console.error(
23
- "❌ Please provide a project name.\n Usage: bun x bosia@latest create my-app",
24
- );
23
+ console.error("❌ Please provide a project name.\n Usage: bun x bosia@latest create my-app");
25
24
  process.exit(1);
26
25
  }
27
26
 
@@ -68,10 +67,7 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
68
67
  copyDir(templateDir, targetDir, name, isLocal);
69
68
 
70
69
  if (existsSync(join(targetDir, ".env.example"))) {
71
- writeFileSync(
72
- join(targetDir, ".env"),
73
- readFileSync(join(targetDir, ".env.example"), "utf-8"),
74
- );
70
+ writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
75
71
  }
76
72
 
77
73
  // Install template features from registry
@@ -89,11 +85,13 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
89
85
  await initAddRegistry(localRegistry);
90
86
  initFeatRegistry(localRegistry);
91
87
 
88
+ const featureOptions: Record<string, string> = config.featureOptions ?? {};
92
89
  for (const feat of config.features) {
93
90
  await installFeature(feat, true, {
94
91
  skipInstall: true,
95
92
  skipPrompts: true,
96
93
  cwd: targetDir,
94
+ featureOptions,
97
95
  });
98
96
  }
99
97
  }
@@ -177,10 +175,7 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
177
175
  if (entry.isDirectory()) {
178
176
  copyDir(srcPath, destPath, projectName, isLocal);
179
177
  } else {
180
- let content = readFileSync(srcPath, "utf-8").replaceAll(
181
- "{{PROJECT_NAME}}",
182
- projectName,
183
- );
178
+ let content = readFileSync(srcPath, "utf-8").replaceAll("{{PROJECT_NAME}}", projectName);
184
179
 
185
180
  if (entry.name === "package.json" && isLocal) {
186
181
  const bosiaPath = resolve(import.meta.dir, "../../");
package/src/cli/feat.ts CHANGED
@@ -109,8 +109,11 @@ async function resolveFeatureOptions(
109
109
  options: FeatureOption[],
110
110
  args: string[],
111
111
  skipPrompts: boolean,
112
+ seed: Record<string, string> = {},
112
113
  ): Promise<Record<string, string>> {
113
- const values: Record<string, string> = {};
114
+ // Seed values come from inherited featureOptions (e.g. template-level defaults).
115
+ // They beat per-feature `default` but lose to explicit CLI args.
116
+ const values: Record<string, string> = { ...seed };
114
117
  const byFlag = new Map<string, FeatureOption>();
115
118
  for (const opt of options) {
116
119
  if (opt.flag) byFlag.set(opt.flag, opt);
@@ -153,9 +156,7 @@ async function resolveFeatureOptions(
153
156
  continue;
154
157
  }
155
158
  if (opt.required) {
156
- console.error(
157
- `❌ Feature "${featName}" requires "${opt.flag ?? opt.long ?? opt.name}".`,
158
- );
159
+ console.error(`❌ Feature "${featName}" requires "${opt.flag ?? opt.long ?? opt.name}".`);
159
160
  process.exit(1);
160
161
  }
161
162
  continue;
@@ -217,19 +218,22 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
217
218
  const inheritedOptions = options?.featureOptions ?? {};
218
219
  let myOptions: Record<string, string> = {};
219
220
  if (meta.options && meta.options.length > 0) {
221
+ // Extract this feature's seed values from the namespaced inherited map.
222
+ const seed: Record<string, string> = {};
223
+ for (const [k, v] of Object.entries(inheritedOptions)) {
224
+ const [feat, optName] = k.split(".");
225
+ if (feat === name) seed[optName] = v;
226
+ }
220
227
  myOptions = isRoot
221
228
  ? await resolveFeatureOptions(
222
229
  name,
223
230
  meta.options,
224
231
  options?.featureArgs ?? [],
225
232
  options?.skipPrompts ?? false,
233
+ seed,
226
234
  )
227
235
  : // Dependency features inherit any caller-provided values; prompt only for unresolved required opts.
228
- await resolveFeatureOptions(name, meta.options, [], options?.skipPrompts ?? false);
229
- for (const [k, v] of Object.entries(inheritedOptions)) {
230
- const [feat, optName] = k.split(".");
231
- if (feat === name && !(optName in myOptions)) myOptions[optName] = v;
232
- }
236
+ await resolveFeatureOptions(name, meta.options, [], options?.skipPrompts ?? false, seed);
233
237
  }
234
238
 
235
239
  // Merge into the namespaced map for downstream dependency features.
@@ -262,7 +266,7 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
262
266
  if (meta.blocks && meta.blocks.length > 0) {
263
267
  console.log("🧱 Installing required blocks...");
264
268
  for (const blockName of meta.blocks) {
265
- await runAddBlock(blockName, [], options);
269
+ await runAddBlock(blockName, [], { ...options, registryRoot });
266
270
  }
267
271
  console.log("");
268
272
  }
@@ -308,16 +312,13 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
308
312
  devDeps: meta.npmDevDeps,
309
313
  scripts: meta.scripts,
310
314
  });
311
- if (addedDeps.length > 0)
312
- console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
313
- if (addedScripts.length > 0)
314
- console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
315
+ if (addedDeps.length > 0) console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
316
+ if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
315
317
  } else {
316
318
  await bunAdd(cwd, meta.npmDeps, meta.npmDevDeps);
317
319
  if (hasScripts) {
318
320
  const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
319
- if (addedScripts.length > 0)
320
- console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
321
+ if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
321
322
  }
322
323
  }
323
324
  } else if (hasScripts) {
@@ -359,9 +360,7 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
359
360
  (meta.blocks && meta.blocks.length > 0)
360
361
  ? {
361
362
  deps: {
362
- ...(meta.features && meta.features.length > 0
363
- ? { features: meta.features }
364
- : {}),
363
+ ...(meta.features && meta.features.length > 0 ? { features: meta.features } : {}),
365
364
  ...(meta.components.length > 0 ? { components: meta.components } : {}),
366
365
  ...(meta.blocks && meta.blocks.length > 0 ? { blocks: meta.blocks } : {}),
367
366
  },
@@ -439,9 +438,7 @@ async function applyStrategy(args: StrategyArgs): Promise<void> {
439
438
 
440
439
  const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
441
440
  writeFileSync(dest, existing + nl + newLines.join("\n") + "\n", "utf-8");
442
- console.log(
443
- ` ➕ ${target} (+${newLines.length} line${newLines.length === 1 ? "" : "s"})`,
444
- );
441
+ console.log(` ➕ ${target} (+${newLines.length} line${newLines.length === 1 ? "" : "s"})`);
445
442
  return;
446
443
  }
447
444
 
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": {
@@ -94,8 +96,7 @@ async function main() {
94
96
  // First non-flag token is the feature name; everything else flows through to the
95
97
  // feature's own option parser. Global flags (-y, --local) are also accepted here
96
98
  // and get split out inside runFeat.
97
- const rest =
98
- nameIdx === -1 ? args : [...args.slice(0, nameIdx), ...args.slice(nameIdx + 1)];
99
+ const rest = nameIdx === -1 ? args : [...args.slice(0, nameIdx), ...args.slice(nameIdx + 1)];
99
100
  await runFeat(featName, rest);
100
101
  break;
101
102
  }
@@ -137,6 +138,7 @@ Examples:
137
138
  bun x bosia@latest add -y button card → auto-confirm overwrites (CI / scripts)
138
139
  bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
139
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)
140
142
  bun x bosia@latest add theme editorial
141
143
  bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
142
144
  bun x bosia@latest feat login
@@ -67,7 +67,7 @@ export function readManifest(cwd: string = process.cwd()): Manifest {
67
67
 
68
68
  export function writeManifest(manifest: Manifest, cwd: string = process.cwd()): void {
69
69
  const path = join(cwd, MANIFEST_FILE);
70
- writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
70
+ writeFileSync(path, JSON.stringify(manifest, null, "\t") + "\n", "utf-8");
71
71
  }
72
72
 
73
73
  export function recordFeature(
@@ -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 ─────
@@ -10,6 +10,8 @@ export interface InstallOptions {
10
10
  skipInstall?: boolean; // write deps to package.json instead of `bun add`
11
11
  skipPrompts?: boolean; // auto-overwrite, no interactive prompts
12
12
  cwd?: string; // override process.cwd() for file operations
13
+ /** When set, use this absolute path as the local registry root instead of fetching from GitHub. */
14
+ registryRoot?: string | null;
13
15
  /** Pre-resolved feature-specific option values, keyed by `featureName.optionName`. */
14
16
  featureOptions?: Record<string, string>;
15
17
  /** Remaining argv tokens to be parsed as the root feature's own options. */
@@ -80,6 +82,42 @@ export async function readRegistryFile(
80
82
  return fetchText(`${REGISTRY_URL}/${category}/${name}/${file}`);
81
83
  }
82
84
 
85
+ // ─── Registry file writer ─────────────────────────────────
86
+
87
+ /**
88
+ * Write a registry file with one EACCES/EPERM recovery attempt.
89
+ *
90
+ * Component/block file loops abort mid-install when a target path is owned by a
91
+ * different uid — bosapi tenant apps run sandboxed as `bosapi-app-N`, so a
92
+ * subsequent install from the bosapi user hits EACCES on the first foreign-owned
93
+ * file and leaves a partial install behind. Unlink + retry recovers; only the
94
+ * unrecoverable case surfaces the chown hint.
95
+ */
96
+ export function writeRegistryFile(dest: string, content: string): void {
97
+ try {
98
+ writeFileSync(dest, content, "utf-8");
99
+ return;
100
+ } catch (err) {
101
+ const code = (err as NodeJS.ErrnoException).code;
102
+ if (code !== "EACCES" && code !== "EPERM") throw err;
103
+ }
104
+ try {
105
+ unlinkSync(dest);
106
+ } catch {
107
+ // ignore — retry will surface the real error
108
+ }
109
+ try {
110
+ writeFileSync(dest, content, "utf-8");
111
+ } catch (retry) {
112
+ const e = retry as NodeJS.ErrnoException;
113
+ throw new Error(
114
+ `Cannot write ${dest}: ${e.code}. ` +
115
+ `The existing file is owned by a different user (likely created from inside ` +
116
+ `the app sandbox). Fix from the project root: chown -R $(whoami) src/lib`,
117
+ );
118
+ }
119
+ }
120
+
83
121
  // ─── package.json helpers ─────────────────────────────────
84
122
 
85
123
  export interface PkgDeps {
@@ -138,7 +176,7 @@ export function mergePkgJson(
138
176
  }
139
177
 
140
178
  if (changed) {
141
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
179
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n", "utf-8");
142
180
  }
143
181
 
144
182
  return { addedDeps, addedScripts };
package/src/core/build.ts CHANGED
@@ -88,9 +88,7 @@ let appHtml: any;
88
88
  try {
89
89
  appHtml = loadAppHtmlTemplate(process.cwd());
90
90
  console.log(
91
- "📄 Loaded src/app.html (favicon override: " +
92
- (appHtml.hasCustomFavicon ? "yes" : "no") +
93
- ")",
91
+ "📄 Loaded src/app.html (favicon override: " + (appHtml.hasCustomFavicon ? "yes" : "no") + ")",
94
92
  );
95
93
  } catch (err) {
96
94
  console.error(`❌ src/app.html validation failed:\n${(err as Error).message}`);
@@ -114,8 +114,7 @@
114
114
  // We always issue the fetch (even when all loaders skip) so page-level
115
115
  // metadata stays fresh on every navigation; only the loaders flagged in
116
116
  // the mask actually run server-side.
117
- const maskBits =
118
- (pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
117
+ const maskBits = (pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
119
118
 
120
119
  // Clear dirty set now — we've baked it into the mask.
121
120
  clearDirty();
@@ -173,9 +172,7 @@
173
172
  ? errInfo
174
173
  : "Internal Server Error";
175
174
  const errDepth: number =
176
- typeof result?.errorDepth === "number"
177
- ? result.errorDepth
178
- : match.route.layouts.length;
175
+ typeof result?.errorDepth === "number" ? result.errorDepth : match.route.layouts.length;
179
176
  const errOrigin = result?.errorOrigin === "layout" ? "layout" : "page";
180
177
  const picked = pickErrorPage(match.route.errorPages ?? [], errDepth, errOrigin);
181
178
  if (!picked) {
@@ -309,9 +306,7 @@
309
306
  if (result?.metadata) {
310
307
  if (result.metadata.title) document.title = result.metadata.title;
311
308
  if (result.metadata.description) {
312
- let meta = document.querySelector(
313
- 'meta[name="description"]',
314
- ) as HTMLMetaElement | null;
309
+ let meta = document.querySelector('meta[name="description"]') as HTMLMetaElement | null;
315
310
  if (!meta) {
316
311
  meta = document.createElement("meta");
317
312
  meta.name = "description";
@@ -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();
@@ -57,10 +57,7 @@ export const router = new (class Router {
57
57
  return;
58
58
  }
59
59
  // Canonicalize trailing slash before navigating (matches server 308 behavior)
60
- const canonical = canonicalPathname(
61
- pathname,
62
- (match.route as any).trailingSlash ?? "never",
63
- );
60
+ const canonical = canonicalPathname(pathname, (match.route as any).trailingSlash ?? "never");
64
61
  const finalPath = canonical !== null ? canonical + queryHash : path;
65
62
 
66
63
  const navType: NavType = opts.source ?? "link";
@@ -108,8 +105,7 @@ export const router = new (class Router {
108
105
  // Same-page hash navigation: skip page reload, just update URL and scroll
109
106
  // to the target element. Mirrors browser default for in-page anchors.
110
107
  const samePage =
111
- anchor.pathname === window.location.pathname &&
112
- anchor.search === window.location.search;
108
+ anchor.pathname === window.location.pathname && anchor.search === window.location.search;
113
109
  if (samePage && anchor.hash) {
114
110
  e.preventDefault();
115
111
  const finalPath = anchor.pathname + anchor.search + anchor.hash;
@@ -127,8 +123,7 @@ export const router = new (class Router {
127
123
 
128
124
  // Browser back/forward
129
125
  window.addEventListener("popstate", () => {
130
- const finalPath =
131
- window.location.pathname + window.location.search + window.location.hash;
126
+ const finalPath = window.location.pathname + window.location.search + window.location.hash;
132
127
  // Fire beforeNavigate listeners; popstate can't be reliably cancelled
133
128
  // (browser history already advanced), so we surface the event for
134
129
  // observation only — `cancel()` is a no-op for this source.
@@ -70,10 +70,7 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
70
70
  // the project's own node_modules. /tmp would have no node_modules to walk into.
71
71
  const cacheDir = join(cwd, ".bosia");
72
72
  mkdirSync(cacheDir, { recursive: true });
73
- const tmpFile = join(
74
- cacheDir,
75
- `config.${Date.now()}.${Math.random().toString(36).slice(2)}.mjs`,
76
- );
73
+ const tmpFile = join(cacheDir, `config.${Date.now()}.${Math.random().toString(36).slice(2)}.mjs`);
77
74
  await Bun.write(tmpFile, code);
78
75
 
79
76
  let mod: { default?: BosiaConfig };
@@ -98,8 +98,7 @@ export class CookieJar implements Cookies {
98
98
  }
99
99
  let header = `${name}=${encodeURIComponent(value)}`;
100
100
  if (opts.path) {
101
- if (UNSAFE_COOKIE_VALUE.test(opts.path))
102
- throw new Error(`Invalid cookie path: ${opts.path}`);
101
+ if (UNSAFE_COOKIE_VALUE.test(opts.path)) throw new Error(`Invalid cookie path: ${opts.path}`);
103
102
  header += `; Path=${opts.path}`;
104
103
  }
105
104
  if (opts.domain) {
@@ -115,7 +115,7 @@ export function dev500Response({
115
115
  message,
116
116
  stack: detail,
117
117
  },
118
- ])}</script>
118
+ ])}</script>
119
119
  ${bodyEndExtras?.join("\n") ?? ""}
120
120
  </body>
121
121
  </html>`;
package/src/core/html.ts CHANGED
@@ -122,8 +122,7 @@ export function buildHtml(
122
122
  ? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
123
123
  : "";
124
124
 
125
- const sysScript =
126
- ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
125
+ const sysScript = ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
127
126
 
128
127
  const dataIslands = csr
129
128
  ? `\n <script${n} type="application/json" id="__bosia-page-data__">${safeJsonForScript(pageData)}</script>` +
@@ -60,9 +60,7 @@ export function makeBosiaPlugin(target: "browser" | "bun" = "bun") {
60
60
  let svelteBrowserEntry: string | null = null;
61
61
  if (target === "browser") {
62
62
  try {
63
- const svelteDir = dirname(
64
- require.resolve("svelte/package.json", { paths: [appDir] }),
65
- );
63
+ const svelteDir = dirname(require.resolve("svelte/package.json", { paths: [appDir] }));
66
64
  const pkg = require(join(svelteDir, "package.json"));
67
65
  const dotExport = pkg.exports?.["."];
68
66
  const browserPath = typeof dotExport === "object" ? dotExport.browser : null;