bosia 0.2.2 → 0.3.0

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 (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. package/templates/todo/tsconfig.json +20 -20
package/README.md CHANGED
@@ -34,32 +34,32 @@ bun run start
34
34
 
35
35
  ## Tech Stack
36
36
 
37
- | Layer | Technology |
38
- |-------|------------|
39
- | Runtime | [Bun](https://bun.sh) |
40
- | HTTP Server | [ElysiaJS](https://elysiajs.com) |
41
- | UI | [Svelte 5](https://svelte.dev) (Runes) |
42
- | CSS | [Tailwind CSS v4](https://tailwindcss.com) |
43
- | Bundler | Bun.build |
37
+ | Layer | Technology |
38
+ | ----------- | ------------------------------------------ |
39
+ | Runtime | [Bun](https://bun.sh) |
40
+ | HTTP Server | [ElysiaJS](https://elysiajs.com) |
41
+ | UI | [Svelte 5](https://svelte.dev) (Runes) |
42
+ | CSS | [Tailwind CSS v4](https://tailwindcss.com) |
43
+ | Bundler | Bun.build |
44
44
 
45
45
  ## Routing Conventions
46
46
 
47
47
  Files in `src/routes/` map to URLs automatically.
48
48
 
49
- | File | Purpose |
50
- |------|---------|
51
- | `+page.svelte` | Page component |
52
- | `+layout.svelte` | Layout that wraps child pages |
53
- | `+page.server.ts` | Server loader for a page |
54
- | `+layout.server.ts` | Server loader for a layout |
55
- | `+server.ts` | API endpoint (export HTTP verbs) |
49
+ | File | Purpose |
50
+ | ------------------- | -------------------------------- |
51
+ | `+page.svelte` | Page component |
52
+ | `+layout.svelte` | Layout that wraps child pages |
53
+ | `+page.server.ts` | Server loader for a page |
54
+ | `+layout.server.ts` | Server loader for a layout |
55
+ | `+server.ts` | API endpoint (export HTTP verbs) |
56
56
 
57
57
  ### Dynamic Routes
58
58
 
59
- | Pattern | Matches |
60
- |---------|---------|
61
- | `[param]` | `/blog/hello` → `params.param = "hello"` |
62
- | `[...rest]` | `/a/b/c` → `params.rest = "a/b/c"` |
59
+ | Pattern | Matches |
60
+ | ----------- | ---------------------------------------- |
61
+ | `[param]` | `/blog/hello` → `params.param = "hello"` |
62
+ | `[...rest]` | `/a/b/c` → `params.rest = "a/b/c"` |
63
63
 
64
64
  ### Route Groups
65
65
 
@@ -81,10 +81,10 @@ src/routes/
81
81
  import type { LoadEvent } from "bosia";
82
82
 
83
83
  export async function load({ params, url, locals, fetch, parent }: LoadEvent) {
84
- const parentData = await parent(); // data from layout loaders above
85
- return {
86
- post: await getPost(params.slug),
87
- };
84
+ const parentData = await parent(); // data from layout loaders above
85
+ return {
86
+ post: await getPost(params.slug),
87
+ };
88
88
  }
89
89
  ```
90
90
 
@@ -92,8 +92,8 @@ Data returned is passed as the `data` prop to `+page.svelte`:
92
92
 
93
93
  ```svelte
94
94
  <script lang="ts">
95
- let { data } = $props();
96
- // data.post, data.params ...
95
+ let { data } = $props();
96
+ // data.post, data.params ...
97
97
  </script>
98
98
  ```
99
99
 
@@ -106,12 +106,12 @@ Export named HTTP verb functions from `+server.ts`:
106
106
  import type { RequestEvent } from "bosia";
107
107
 
108
108
  export function GET({ params, url, locals }: RequestEvent) {
109
- return Response.json({ items: [] });
109
+ return Response.json({ items: [] });
110
110
  }
111
111
 
112
112
  export async function POST({ request }: RequestEvent) {
113
- const body = await request.json();
114
- return Response.json({ created: body }, { status: 201 });
113
+ const body = await request.json();
114
+ return Response.json({ created: body }, { status: 201 });
115
115
  }
116
116
  ```
117
117
 
@@ -124,14 +124,14 @@ import { sequence } from "bosia";
124
124
  import type { Handle } from "bosia";
125
125
 
126
126
  const authHandle: Handle = async ({ event, resolve }) => {
127
- event.locals.user = await getUser(event.request);
128
- return resolve(event);
127
+ event.locals.user = await getUser(event.request);
128
+ return resolve(event);
129
129
  };
130
130
 
131
131
  const loggingHandle: Handle = async ({ event, resolve }) => {
132
- const res = await resolve(event);
133
- console.log(`${event.request.method} ${event.url.pathname} ${res.status}`);
134
- return res;
132
+ const res = await resolve(event);
133
+ console.log(`${event.request.method} ${event.url.pathname} ${res.status}`);
134
+ return res;
135
135
  };
136
136
 
137
137
  export const handle = sequence(authHandle, loggingHandle);
@@ -146,13 +146,13 @@ import { cn, sequence } from "bosia";
146
146
  import type { RequestEvent, LoadEvent, Handle } from "bosia";
147
147
  ```
148
148
 
149
- | Export | Description |
150
- |--------|-------------|
151
- | `cn(...classes)` | Tailwind class merge utility (built-in class merging + tailwind-merge) |
152
- | `sequence(...handlers)` | Compose multiple `Handle` middleware functions |
153
- | `RequestEvent` | Type for API route and hook handlers |
154
- | `LoadEvent` | Type for `load()` in `+page.server.ts` / `+layout.server.ts` |
155
- | `Handle` | Type for a middleware function in `hooks.server.ts` |
149
+ | Export | Description |
150
+ | ----------------------- | ---------------------------------------------------------------------- |
151
+ | `cn(...classes)` | Tailwind class merge utility (built-in class merging + tailwind-merge) |
152
+ | `sequence(...handlers)` | Compose multiple `Handle` middleware functions |
153
+ | `RequestEvent` | Type for API route and hook handlers |
154
+ | `LoadEvent` | Type for `load()` in `+page.server.ts` / `+layout.server.ts` |
155
+ | `Handle` | Type for a middleware function in `hooks.server.ts` |
156
156
 
157
157
  ## Path Alias
158
158
 
package/package.json CHANGED
@@ -1,55 +1,58 @@
1
1
  {
2
- "name": "bosia",
3
- "version": "0.2.2",
4
- "type": "module",
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
- "keywords": [
7
- "bun",
8
- "svelte",
9
- "ssr",
10
- "elysia",
11
- "fullstack",
12
- "framework"
13
- ],
14
- "license": "MIT",
15
- "author": {
16
- "name": "Jekibus",
17
- "url": "https://github.com/jekibus"
18
- },
19
- "homepage": "https://github.com/bosapi/bosia#readme",
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/bosapi/bosia.git"
23
- },
24
- "bugs": {
25
- "url": "https://github.com/bosapi/bosia/issues"
26
- },
27
- "files": [
28
- "src",
29
- "templates",
30
- "README.md",
31
- "package.json"
32
- ],
33
- "exports": {
34
- ".": "./src/lib/index.ts"
35
- },
36
- "bin": {
37
- "bosia": "src/cli/index.ts"
38
- },
39
- "scripts": {
40
- "check": "tsc --noEmit"
41
- },
42
- "devDependencies": {
43
- "@types/bun": "latest",
44
- "typescript": "^5"
45
- },
46
- "dependencies": {
47
- "@clack/prompts": "^1.1.0",
48
- "@tailwindcss/cli": "^4.2.1",
49
- "bun-plugin-svelte": "^0.0.6",
50
- "elysia": "^1.4.26",
51
- "svelte": "^5.53.6",
52
- "tailwind-merge": "^3.5.0",
53
- "tailwindcss": "^4.2.1"
54
- }
2
+ "name": "bosia",
3
+ "version": "0.3.0",
4
+ "type": "module",
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
+ "keywords": [
7
+ "bun",
8
+ "svelte",
9
+ "ssr",
10
+ "elysia",
11
+ "fullstack",
12
+ "framework"
13
+ ],
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Jekibus",
17
+ "url": "https://github.com/jekibus"
18
+ },
19
+ "homepage": "https://github.com/bosapi/bosia#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/bosapi/bosia.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/bosapi/bosia/issues"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "templates",
30
+ "README.md",
31
+ "package.json"
32
+ ],
33
+ "exports": {
34
+ ".": "./src/lib/index.ts",
35
+ "./client": "./src/lib/client.ts"
36
+ },
37
+ "bin": {
38
+ "bosia": "src/cli/index.ts"
39
+ },
40
+ "scripts": {
41
+ "check": "tsc --noEmit && prettier --check .",
42
+ "test": "bun test",
43
+ "test:watch": "bun test --watch"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "latest",
47
+ "typescript": "^5"
48
+ },
49
+ "dependencies": {
50
+ "@clack/prompts": "^1.1.0",
51
+ "@tailwindcss/cli": "^4.2.1",
52
+ "bun-plugin-svelte": "^0.0.6",
53
+ "elysia": "^1.4.26",
54
+ "svelte": "^5.53.6",
55
+ "tailwind-merge": "^3.5.0",
56
+ "tailwindcss": "^4.2.1"
57
+ }
55
58
  }
@@ -0,0 +1,31 @@
1
+ // Virtual modules resolved by the bundler plugin (src/core/plugin.ts).
2
+ // Backed by .bosia/routes.ts and .bosia/routes.client.ts at build time.
3
+
4
+ declare module "bosia:routes" {
5
+ type Loader = () => Promise<any>;
6
+ type TrailingSlash = "never" | "always" | "ignore";
7
+
8
+ export const clientRoutes: Array<{
9
+ pattern: string;
10
+ page: Loader;
11
+ layouts: Loader[];
12
+ hasServerData: boolean;
13
+ trailingSlash: TrailingSlash;
14
+ }>;
15
+
16
+ export const serverRoutes: Array<{
17
+ pattern: string;
18
+ pageModule: Loader;
19
+ layoutModules: Loader[];
20
+ pageServer: Loader | null;
21
+ layoutServers: { loader: Loader; depth: number }[];
22
+ trailingSlash: TrailingSlash;
23
+ }>;
24
+
25
+ export const apiRoutes: Array<{
26
+ pattern: string;
27
+ module: Loader;
28
+ }>;
29
+
30
+ export const errorPage: Loader | null;
31
+ }
package/src/cli/add.ts CHANGED
@@ -2,13 +2,13 @@ import { join, dirname } from "path";
2
2
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
3
  import * as p from "@clack/prompts";
4
4
  import {
5
- type InstallOptions,
6
- REGISTRY_URL,
7
- resolveLocalRegistryOrExit,
8
- readRegistryJSON,
9
- readRegistryFile,
10
- mergePkgJson,
11
- bunAdd,
5
+ type InstallOptions,
6
+ REGISTRY_URL,
7
+ resolveLocalRegistryOrExit,
8
+ readRegistryJSON,
9
+ readRegistryFile,
10
+ mergePkgJson,
11
+ bunAdd,
12
12
  } from "./registry.ts";
13
13
 
14
14
  // ─── bosia add <component> ────────────────────────────────
@@ -20,16 +20,16 @@ import {
20
20
  // bosia add shop/cart → src/lib/components/shop/cart/
21
21
 
22
22
  interface ComponentMeta {
23
- name: string;
24
- description: string;
25
- dependencies: string[]; // other bosia components required
26
- files: string[];
27
- npmDeps: Record<string, string>;
23
+ name: string;
24
+ description: string;
25
+ dependencies: string[]; // other bosia components required
26
+ files: string[];
27
+ npmDeps: Record<string, string>;
28
28
  }
29
29
 
30
30
  interface RegistryIndex {
31
- components: string[];
32
- features: string[];
31
+ components: string[];
32
+ features: string[];
33
33
  }
34
34
 
35
35
  // Track already-installed components within a session to avoid re-running deps
@@ -41,26 +41,28 @@ let registryIndex: RegistryIndex | null = null;
41
41
 
42
42
  /** Initialize registry context so addComponent can be called externally (e.g. from feat.ts) */
43
43
  export async function initAddRegistry(root: string | null) {
44
- registryRoot = root;
45
- registryIndex = await loadIndex();
44
+ registryRoot = root;
45
+ registryIndex = await loadIndex();
46
46
  }
47
47
 
48
48
  export async function runAdd(name: string | undefined, flags: string[] = []) {
49
- if (!name) {
50
- console.error("āŒ Please provide a component name.\n Usage: bosia add <component> [--local]");
51
- process.exit(1);
52
- }
53
-
54
- if (flags.includes("--local")) {
55
- registryRoot = resolveLocalRegistryOrExit();
56
- console.log(`⬔ Using local registry: ${registryRoot}\n`);
57
- }
58
-
59
- // Load index once to resolve component paths
60
- registryIndex = await loadIndex();
61
-
62
- ensureUtils();
63
- await addComponent(name, true);
49
+ if (!name) {
50
+ console.error(
51
+ "āŒ Please provide a component name.\n Usage: bosia add <component> [--local]",
52
+ );
53
+ process.exit(1);
54
+ }
55
+
56
+ if (flags.includes("--local")) {
57
+ registryRoot = resolveLocalRegistryOrExit();
58
+ console.log(`⬔ Using local registry: ${registryRoot}\n`);
59
+ }
60
+
61
+ // Load index once to resolve component paths
62
+ registryIndex = await loadIndex();
63
+
64
+ ensureUtils();
65
+ await addComponent(name, true);
64
66
  }
65
67
 
66
68
  /**
@@ -70,89 +72,93 @@ export async function runAdd(name: string | undefined, flags: string[] = []) {
70
72
  * - "shop/cart" → "shop/cart" (explicit path used as-is)
71
73
  */
72
74
  function resolveDestPath(name: string): string {
73
- if (name.includes("/")) return name;
74
-
75
- if (registryIndex) {
76
- // Exact match (e.g. "todo" → "todo")
77
- if (registryIndex.components.includes(name)) return name;
78
- // Suffix match (e.g. "button" → "ui/button")
79
- const match = registryIndex.components.find(
80
- (c) => c.endsWith(`/${name}`)
81
- );
82
- if (match) return match;
83
- }
84
-
85
- // Fallback for backwards compatibility
86
- return `ui/${name}`;
75
+ if (name.includes("/")) return name;
76
+
77
+ if (registryIndex) {
78
+ // Exact match (e.g. "todo" → "todo")
79
+ if (registryIndex.components.includes(name)) return name;
80
+ // Suffix match (e.g. "button" → "ui/button")
81
+ const match = registryIndex.components.find((c) => c.endsWith(`/${name}`));
82
+ if (match) return match;
83
+ }
84
+
85
+ // Fallback for backwards compatibility
86
+ return `ui/${name}`;
87
87
  }
88
88
 
89
89
  async function loadIndex(): Promise<RegistryIndex | null> {
90
- try {
91
- if (registryRoot) {
92
- const path = join(registryRoot, "index.json");
93
- if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
94
- return null;
95
- }
96
- const res = await fetch(`${REGISTRY_URL}/index.json`);
97
- if (!res.ok) return null;
98
- return await res.json() as RegistryIndex;
99
- } catch {
100
- return null;
101
- }
90
+ try {
91
+ if (registryRoot) {
92
+ const path = join(registryRoot, "index.json");
93
+ if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
94
+ return null;
95
+ }
96
+ const res = await fetch(`${REGISTRY_URL}/index.json`);
97
+ if (!res.ok) return null;
98
+ return (await res.json()) as RegistryIndex;
99
+ } catch {
100
+ return null;
101
+ }
102
102
  }
103
103
 
104
104
  export async function addComponent(name: string, root = false, options?: InstallOptions) {
105
- // Resolve the full path (e.g. "button" → "ui/button", "shop/cart" stays "shop/cart")
106
- const fullPath = resolveDestPath(name);
107
-
108
- if (installed.has(fullPath)) return;
109
- installed.add(fullPath);
110
-
111
- const cwd = options?.cwd ?? process.cwd();
112
-
113
- console.log(root ? `⬔ Installing component: ${name}\n` : ` šŸ“¦ Dependency: ${name}`);
114
-
115
- const meta = await readRegistryJSON<ComponentMeta>(registryRoot, "components", fullPath, "meta.json");
116
-
117
- // Install component dependencies first (recursive)
118
- for (const dep of meta.dependencies) {
119
- await addComponent(dep, false, options);
120
- }
121
-
122
- // Check if component already exists (skip check entirely in non-interactive mode)
123
- const destDir = join(cwd, "src", "lib", "components", fullPath);
124
- if (!options?.skipPrompts && existsSync(destDir)) {
125
- const replace = await p.confirm({
126
- message: `Component "${name}" already exists at src/lib/components/${fullPath}/. Replace it?`,
127
- });
128
- if (p.isCancel(replace) || !replace) {
129
- console.log(` ā­ļø Skipped ${name}`);
130
- return;
131
- }
132
- }
133
-
134
- // Download/copy component files into src/lib/components/<fullPath>/
135
- mkdirSync(destDir, { recursive: true });
136
-
137
- for (const file of meta.files) {
138
- const content = await readRegistryFile(registryRoot, "components", fullPath, file);
139
- const dest = join(destDir, file);
140
- if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
141
- writeFileSync(dest, content, "utf-8");
142
- console.log(` āœļø src/lib/components/${fullPath}/${file}`);
143
- }
144
-
145
- // Install npm dependencies
146
- if (Object.keys(meta.npmDeps).length > 0) {
147
- if (options?.skipInstall) {
148
- const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
149
- if (addedDeps.length > 0) console.log(` šŸ“„ Added to package.json: ${addedDeps.join(", ")}`);
150
- } else {
151
- await bunAdd(cwd, meta.npmDeps);
152
- }
153
- }
154
-
155
- if (root) console.log(`\nāœ… ${name} installed at src/lib/components/${fullPath}/`);
105
+ // Resolve the full path (e.g. "button" → "ui/button", "shop/cart" stays "shop/cart")
106
+ const fullPath = resolveDestPath(name);
107
+
108
+ if (installed.has(fullPath)) return;
109
+ installed.add(fullPath);
110
+
111
+ const cwd = options?.cwd ?? process.cwd();
112
+
113
+ console.log(root ? `⬔ Installing component: ${name}\n` : ` šŸ“¦ Dependency: ${name}`);
114
+
115
+ const meta = await readRegistryJSON<ComponentMeta>(
116
+ registryRoot,
117
+ "components",
118
+ fullPath,
119
+ "meta.json",
120
+ );
121
+
122
+ // Install component dependencies first (recursive)
123
+ for (const dep of meta.dependencies) {
124
+ await addComponent(dep, false, options);
125
+ }
126
+
127
+ // Check if component already exists (skip check entirely in non-interactive mode)
128
+ const destDir = join(cwd, "src", "lib", "components", fullPath);
129
+ if (!options?.skipPrompts && existsSync(destDir)) {
130
+ const replace = await p.confirm({
131
+ message: `Component "${name}" already exists at src/lib/components/${fullPath}/. Replace it?`,
132
+ });
133
+ if (p.isCancel(replace) || !replace) {
134
+ console.log(` ā­ļø Skipped ${name}`);
135
+ return;
136
+ }
137
+ }
138
+
139
+ // Download/copy component files into src/lib/components/<fullPath>/
140
+ mkdirSync(destDir, { recursive: true });
141
+
142
+ for (const file of meta.files) {
143
+ const content = await readRegistryFile(registryRoot, "components", fullPath, file);
144
+ const dest = join(destDir, file);
145
+ if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
146
+ writeFileSync(dest, content, "utf-8");
147
+ console.log(` āœļø src/lib/components/${fullPath}/${file}`);
148
+ }
149
+
150
+ // Install npm dependencies
151
+ if (Object.keys(meta.npmDeps).length > 0) {
152
+ if (options?.skipInstall) {
153
+ const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
154
+ if (addedDeps.length > 0)
155
+ console.log(` šŸ“„ Added to package.json: ${addedDeps.join(", ")}`);
156
+ } else {
157
+ await bunAdd(cwd, meta.npmDeps);
158
+ }
159
+ }
160
+
161
+ if (root) console.log(`\nāœ… ${name} installed at src/lib/components/${fullPath}/`);
156
162
  }
157
163
 
158
164
  // ─── Ensure $lib/utils.ts exists ─────────────────────────────
@@ -188,10 +194,10 @@ export function cn(...inputs: ClassValue[]) {
188
194
  `;
189
195
 
190
196
  function ensureUtils() {
191
- const utilsPath = join(process.cwd(), "src", "lib", "utils.ts");
192
- if (!existsSync(utilsPath)) {
193
- mkdirSync(dirname(utilsPath), { recursive: true });
194
- writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
195
- console.log(" āœļø src/lib/utils.ts (cn utility)\n");
196
- }
197
+ const utilsPath = join(process.cwd(), "src", "lib", "utils.ts");
198
+ if (!existsSync(utilsPath)) {
199
+ mkdirSync(dirname(utilsPath), { recursive: true });
200
+ writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
201
+ console.log(" āœļø src/lib/utils.ts (cn utility)\n");
202
+ }
197
203
  }
package/src/cli/build.ts CHANGED
@@ -3,14 +3,14 @@ import { resolve } from "path";
3
3
  import { loadEnv } from "../core/env.ts";
4
4
 
5
5
  export async function runBuild() {
6
- loadEnv("production");
7
- const buildScript = resolve(import.meta.dir, "../core/build.ts");
8
- const proc = spawn(["bun", "run", buildScript], {
9
- stdout: "inherit",
10
- stderr: "inherit",
11
- cwd: process.cwd(),
12
- env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "production" },
13
- });
14
- const exitCode = await proc.exited;
15
- if (exitCode !== 0) process.exit(exitCode ?? 1);
6
+ loadEnv("production");
7
+ const buildScript = resolve(import.meta.dir, "../core/build.ts");
8
+ const proc = spawn(["bun", "run", buildScript], {
9
+ stdout: "inherit",
10
+ stderr: "inherit",
11
+ cwd: process.cwd(),
12
+ env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "production" },
13
+ });
14
+ const exitCode = await proc.exited;
15
+ if (exitCode !== 0) process.exit(exitCode ?? 1);
16
16
  }