bosia 0.0.0 → 0.1.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.
- package/README.md +173 -7
- package/package.json +48 -5
- package/src/cli/add.ts +83 -0
- package/src/cli/build.ts +16 -0
- package/src/cli/create.ts +118 -0
- package/src/cli/dev.ts +14 -0
- package/src/cli/feat.ts +80 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/start.ts +26 -0
- package/src/core/build.ts +157 -0
- package/src/core/client/App.svelte +157 -0
- package/src/core/client/hydrate.ts +81 -0
- package/src/core/client/prefetch.ts +109 -0
- package/src/core/client/router.svelte.ts +47 -0
- package/src/core/cookies.ts +68 -0
- package/src/core/cors.ts +60 -0
- package/src/core/csrf.ts +65 -0
- package/src/core/dev.ts +225 -0
- package/src/core/env.ts +153 -0
- package/src/core/envCodegen.ts +94 -0
- package/src/core/errors.ts +35 -0
- package/src/core/hooks.ts +92 -0
- package/src/core/html.ts +212 -0
- package/src/core/matcher.ts +80 -0
- package/src/core/paths.ts +32 -0
- package/src/core/plugin.ts +93 -0
- package/src/core/prerender.ts +86 -0
- package/src/core/renderer.ts +314 -0
- package/src/core/routeFile.ts +110 -0
- package/src/core/routeTypes.ts +106 -0
- package/src/core/scanner.ts +99 -0
- package/src/core/server.ts +414 -0
- package/src/core/types.ts +37 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/utils.ts +24 -0
- package/templates/default/.env.example +75 -0
- package/templates/default/README.md +102 -0
- package/templates/default/package.json +21 -0
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/public/favicon.svg +14 -0
- package/templates/default/src/app.css +132 -0
- package/templates/default/src/app.d.ts +7 -0
- package/templates/default/src/lib/.gitkeep +0 -0
- package/templates/default/src/routes/+error.svelte +18 -0
- package/templates/default/src/routes/+layout.svelte +6 -0
- package/templates/default/src/routes/+page.svelte +36 -0
- package/templates/default/src/routes/about/+page.server.ts +1 -0
- package/templates/default/src/routes/about/+page.svelte +8 -0
- package/templates/default/tsconfig.json +22 -0
- package/templates/demo/.env.example +52 -0
- package/templates/demo/README.md +29 -0
- package/templates/demo/package.json +20 -0
- package/templates/demo/public/.gitkeep +0 -0
- package/templates/demo/public/favicon.svg +14 -0
- package/templates/demo/src/app.css +132 -0
- package/templates/demo/src/app.d.ts +7 -0
- package/templates/demo/src/hooks.server.ts +21 -0
- package/templates/demo/src/lib/utils.ts +1 -0
- package/templates/demo/src/routes/(public)/+layout.svelte +31 -0
- package/templates/demo/src/routes/(public)/+page.svelte +79 -0
- package/templates/demo/src/routes/(public)/about/+page.server.ts +1 -0
- package/templates/demo/src/routes/(public)/about/+page.svelte +31 -0
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +38 -0
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -0
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +62 -0
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +53 -0
- package/templates/demo/src/routes/+error.svelte +15 -0
- package/templates/demo/src/routes/+layout.server.ts +10 -0
- package/templates/demo/src/routes/+layout.svelte +6 -0
- package/templates/demo/src/routes/actions-test/+page.server.ts +28 -0
- package/templates/demo/src/routes/actions-test/+page.svelte +60 -0
- package/templates/demo/src/routes/api/hello/+server.ts +44 -0
- package/templates/demo/tsconfig.json +22 -0
- package/CLAUDE.md +0 -106
- package/bun.lock +0 -25
- package/index.ts +0 -1
- package/tsconfig.json +0 -29
package/README.md
CHANGED
|
@@ -1,15 +1,181 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Bosia
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Full documentation: [bosia.bosapi.com](https://bosia.bosapi.com)
|
|
4
|
+
|
|
5
|
+
A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS.
|
|
6
|
+
|
|
7
|
+
File-based routing inspired by SvelteKit, built on top of the Bun runtime and ElysiaJS HTTP server. No Node.js, no Vite, no adapters.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **File-based routing** — `+page.svelte`, `+layout.svelte`, `+server.ts`, route groups, dynamic segments, catch-all routes
|
|
12
|
+
- **Server-side rendering** — every page is rendered on the server with full hydration
|
|
13
|
+
- **Server loaders** — `+page.server.ts` and `+layout.server.ts` with `parent()` data threading
|
|
14
|
+
- **API routes** — `+server.ts` exports HTTP verbs (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`)
|
|
15
|
+
- **Middleware hooks** — `hooks.server.ts` with `sequence()` for auth, logging, locals
|
|
16
|
+
- **Dev server with HMR** — file watcher + SSE browser reload, no page blink
|
|
17
|
+
- **Tailwind CSS v4** — compiled at build time, shadcn-inspired design tokens out of the box
|
|
18
|
+
- **CLI** — `bosia create`, `bosia dev`, `bosia build`, `bosia add`, `bosia feat`
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
4
21
|
|
|
5
22
|
```bash
|
|
6
|
-
|
|
23
|
+
# Scaffold a new project
|
|
24
|
+
bun x bosia create my-app
|
|
25
|
+
cd my-app
|
|
26
|
+
|
|
27
|
+
# Start development
|
|
28
|
+
bun run dev
|
|
29
|
+
|
|
30
|
+
# Build for production
|
|
31
|
+
bun run build
|
|
32
|
+
bun run start
|
|
7
33
|
```
|
|
8
34
|
|
|
9
|
-
|
|
35
|
+
## Tech Stack
|
|
10
36
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
|
|
45
|
+
## Routing Conventions
|
|
46
|
+
|
|
47
|
+
Files in `src/routes/` map to URLs automatically.
|
|
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) |
|
|
56
|
+
|
|
57
|
+
### Dynamic Routes
|
|
58
|
+
|
|
59
|
+
| Pattern | Matches |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| `[param]` | `/blog/hello` → `params.param = "hello"` |
|
|
62
|
+
| `[...rest]` | `/a/b/c` → `params.rest = "a/b/c"` |
|
|
63
|
+
|
|
64
|
+
### Route Groups
|
|
65
|
+
|
|
66
|
+
Wrap a directory in parentheses to share a layout without affecting the URL:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
src/routes/
|
|
70
|
+
└── (marketing)/
|
|
71
|
+
├── +layout.svelte # shared layout
|
|
72
|
+
├── +page.svelte # /
|
|
73
|
+
└── about/
|
|
74
|
+
└── +page.svelte # /about
|
|
13
75
|
```
|
|
14
76
|
|
|
15
|
-
|
|
77
|
+
## Server Loaders
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// src/routes/blog/[slug]/+page.server.ts
|
|
81
|
+
import type { LoadEvent } from "bosia";
|
|
82
|
+
|
|
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
|
+
};
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Data returned is passed as the `data` prop to `+page.svelte`:
|
|
92
|
+
|
|
93
|
+
```svelte
|
|
94
|
+
<script lang="ts">
|
|
95
|
+
let { data } = $props();
|
|
96
|
+
// data.post, data.params ...
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Routes
|
|
101
|
+
|
|
102
|
+
Export named HTTP verb functions from `+server.ts`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// src/routes/api/items/+server.ts
|
|
106
|
+
import type { RequestEvent } from "bosia";
|
|
107
|
+
|
|
108
|
+
export function GET({ params, url, locals }: RequestEvent) {
|
|
109
|
+
return Response.json({ items: [] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function POST({ request }: RequestEvent) {
|
|
113
|
+
const body = await request.json();
|
|
114
|
+
return Response.json({ created: body }, { status: 201 });
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Middleware Hooks
|
|
119
|
+
|
|
120
|
+
Create `src/hooks.server.ts` to intercept every request:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { sequence } from "bosia";
|
|
124
|
+
import type { Handle } from "bosia";
|
|
125
|
+
|
|
126
|
+
const authHandle: Handle = async ({ event, resolve }) => {
|
|
127
|
+
event.locals.user = await getUser(event.request);
|
|
128
|
+
return resolve(event);
|
|
129
|
+
};
|
|
130
|
+
|
|
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;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const handle = sequence(authHandle, loggingHandle);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`locals` set here are available in every loader and API handler.
|
|
141
|
+
|
|
142
|
+
## Public API
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { cn, sequence } from "bosia";
|
|
146
|
+
import type { RequestEvent, LoadEvent, Handle } from "bosia";
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Export | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `cn(...classes)` | Tailwind class merge utility (clsx + 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
|
+
|
|
157
|
+
## Path Alias
|
|
158
|
+
|
|
159
|
+
`$lib` maps to `src/lib/` out of the box:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { myUtil } from "$lib/utils";
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
my-app/
|
|
169
|
+
├── src/
|
|
170
|
+
│ ├── app.css # Global styles + Tailwind config
|
|
171
|
+
│ ├── hooks.server.ts # Optional request middleware
|
|
172
|
+
│ ├── lib/ # Shared utilities ($lib alias)
|
|
173
|
+
│ └── routes/ # File-based routes
|
|
174
|
+
├── public/ # Static assets (served as-is)
|
|
175
|
+
├── dist/ # Build output (git-ignored)
|
|
176
|
+
└── package.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"module": "index.ts",
|
|
3
|
+
"version": "0.1.0",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
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"
|
|
8
35
|
},
|
|
9
|
-
"
|
|
36
|
+
"bin": {
|
|
37
|
+
"bosia": "src/cli/index.ts"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"check": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "latest",
|
|
10
44
|
"typescript": "^5"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@tailwindcss/cli": "^4.2.1",
|
|
48
|
+
"bun-plugin-svelte": "^0.0.6",
|
|
49
|
+
"clsx": "^2.1.1",
|
|
50
|
+
"elysia": "^1.4.26",
|
|
51
|
+
"svelte": "^5.53.6",
|
|
52
|
+
"tailwind-merge": "^3.5.0",
|
|
53
|
+
"tailwindcss": "^4.2.1"
|
|
11
54
|
}
|
|
12
55
|
}
|
package/src/cli/add.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
|
|
5
|
+
// ─── bosia add <component> ────────────────────────────────
|
|
6
|
+
// Fetches a component from the GitHub registry and copies it
|
|
7
|
+
// into the user's src/lib/components/ui/<name>/ directory.
|
|
8
|
+
|
|
9
|
+
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
10
|
+
|
|
11
|
+
interface ComponentMeta {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
dependencies: string[]; // other bosia components required
|
|
15
|
+
files: string[];
|
|
16
|
+
npmDeps: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Track already-installed components within a session to avoid re-running deps
|
|
20
|
+
const installed = new Set<string>();
|
|
21
|
+
|
|
22
|
+
export async function runAdd(name: string | undefined) {
|
|
23
|
+
if (!name) {
|
|
24
|
+
console.error("❌ Please provide a component name.\n Usage: bosia add <component>");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
await addComponent(name, true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function addComponent(name: string, root = false) {
|
|
31
|
+
if (installed.has(name)) return;
|
|
32
|
+
installed.add(name);
|
|
33
|
+
|
|
34
|
+
console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
|
|
35
|
+
|
|
36
|
+
const meta = await fetchJSON<ComponentMeta>(`${REGISTRY_BASE}/components/${name}/meta.json`);
|
|
37
|
+
|
|
38
|
+
// Install component dependencies first (recursive)
|
|
39
|
+
for (const dep of meta.dependencies) {
|
|
40
|
+
await addComponent(dep, false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Download component files into src/lib/components/ui/<name>/
|
|
44
|
+
const destDir = join(process.cwd(), "src", "lib", "components", "ui", name);
|
|
45
|
+
mkdirSync(destDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
for (const file of meta.files) {
|
|
48
|
+
const content = await fetchText(`${REGISTRY_BASE}/components/${name}/${file}`);
|
|
49
|
+
const dest = join(destDir, file);
|
|
50
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
51
|
+
writeFileSync(dest, content, "utf-8");
|
|
52
|
+
console.log(` ✍️ src/lib/components/ui/${name}/${file}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Install npm dependencies
|
|
56
|
+
const npmEntries = Object.entries(meta.npmDeps);
|
|
57
|
+
if (npmEntries.length > 0) {
|
|
58
|
+
const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
|
|
59
|
+
console.log(` 📥 npm: ${packages.join(", ")}`);
|
|
60
|
+
const proc = spawn(["bun", "add", ...packages], {
|
|
61
|
+
stdout: "inherit",
|
|
62
|
+
stderr: "inherit",
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
});
|
|
65
|
+
if ((await proc.exited) !== 0) {
|
|
66
|
+
console.warn(` ⚠️ bun add failed for: ${packages.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (root) console.log(`\n✅ ${name} installed at src/lib/components/ui/${name}/`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchJSON<T>(url: string): Promise<T> {
|
|
74
|
+
const res = await fetch(url);
|
|
75
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
76
|
+
return res.json() as Promise<T>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchText(url: string): Promise<string> {
|
|
80
|
+
const res = await fetch(url);
|
|
81
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
82
|
+
return res.text();
|
|
83
|
+
}
|
package/src/cli/build.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { loadEnv } from "../core/env.ts";
|
|
4
|
+
|
|
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);
|
|
16
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { resolve, join, basename } from "path";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
|
|
6
|
+
// ─── bosia create <name> [--template <name>] ──────────────
|
|
7
|
+
|
|
8
|
+
const TEMPLATES_DIR = resolve(import.meta.dir, "../../templates");
|
|
9
|
+
const BOSIA_PKG = JSON.parse(readFileSync(resolve(import.meta.dir, "../../package.json"), "utf-8"));
|
|
10
|
+
const BOSIA_VERSION: string = BOSIA_PKG.version;
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
|
|
13
|
+
default: "Minimal starter with routing and Tailwind",
|
|
14
|
+
demo: "Full-featured demo with hooks, API routes, form actions, and more",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
18
|
+
if (!name) {
|
|
19
|
+
console.error("❌ Please provide a project name.\n Usage: bosia create my-app");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const targetDir = resolve(process.cwd(), name);
|
|
24
|
+
|
|
25
|
+
if (existsSync(targetDir)) {
|
|
26
|
+
console.error(`❌ Directory already exists: ${targetDir}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse --template flag
|
|
31
|
+
let template: string | undefined;
|
|
32
|
+
const templateIdx = args.indexOf("--template");
|
|
33
|
+
if (templateIdx !== -1 && args[templateIdx + 1]) {
|
|
34
|
+
template = args[templateIdx + 1];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If no --template flag, prompt interactively
|
|
38
|
+
if (!template) {
|
|
39
|
+
template = await promptTemplate();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate template exists
|
|
43
|
+
const templateDir = resolve(TEMPLATES_DIR, template);
|
|
44
|
+
if (!existsSync(templateDir)) {
|
|
45
|
+
const available = getAvailableTemplates().join(", ");
|
|
46
|
+
console.error(`❌ Unknown template: "${template}"\n Available: ${available}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`\n⬡ Creating Bosia project: ${basename(targetDir)} (template: ${template})\n`);
|
|
51
|
+
|
|
52
|
+
copyDir(templateDir, targetDir, name);
|
|
53
|
+
|
|
54
|
+
console.log(`✅ Project created at ${targetDir}\n`);
|
|
55
|
+
|
|
56
|
+
console.log("Installing dependencies...");
|
|
57
|
+
const proc = spawn(["bun", "install"], {
|
|
58
|
+
stdout: "inherit",
|
|
59
|
+
stderr: "inherit",
|
|
60
|
+
cwd: targetDir,
|
|
61
|
+
});
|
|
62
|
+
const exitCode = await proc.exited;
|
|
63
|
+
if (exitCode !== 0) {
|
|
64
|
+
console.warn("⚠️ bun install failed — run it manually.");
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`\n🎉 Ready!\n\n cd ${name}\n bun x bosia dev\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getAvailableTemplates(): string[] {
|
|
71
|
+
return readdirSync(TEMPLATES_DIR, { withFileTypes: true })
|
|
72
|
+
.filter((d) => d.isDirectory())
|
|
73
|
+
.map((d) => d.name)
|
|
74
|
+
.sort((a, b) => (a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function promptTemplate(): Promise<string> {
|
|
78
|
+
const templates = getAvailableTemplates();
|
|
79
|
+
|
|
80
|
+
if (templates.length === 1) return templates[0];
|
|
81
|
+
|
|
82
|
+
console.log("\n? Which template?\n");
|
|
83
|
+
templates.forEach((t, i) => {
|
|
84
|
+
const desc = TEMPLATE_DESCRIPTIONS[t] ?? "";
|
|
85
|
+
const marker = i === 0 ? "❯" : " ";
|
|
86
|
+
console.log(` ${marker} ${t}${desc ? ` — ${desc}` : ""}`);
|
|
87
|
+
});
|
|
88
|
+
console.log();
|
|
89
|
+
|
|
90
|
+
const rl = readline.createInterface({
|
|
91
|
+
input: process.stdin,
|
|
92
|
+
output: process.stdout,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return new Promise<string>((resolvePromise) => {
|
|
96
|
+
rl.question(` Template name (default): `, (answer) => {
|
|
97
|
+
rl.close();
|
|
98
|
+
const trimmed = answer.trim();
|
|
99
|
+
resolvePromise(trimmed || "default");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function copyDir(src: string, dest: string, projectName: string) {
|
|
105
|
+
mkdirSync(dest, { recursive: true });
|
|
106
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
107
|
+
const srcPath = join(src, entry.name);
|
|
108
|
+
const destPath = join(dest, entry.name);
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
copyDir(srcPath, destPath, projectName);
|
|
111
|
+
} else {
|
|
112
|
+
const content = readFileSync(srcPath, "utf-8")
|
|
113
|
+
.replaceAll("{{PROJECT_NAME}}", projectName)
|
|
114
|
+
.replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
|
|
115
|
+
writeFileSync(destPath, content, "utf-8");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/cli/dev.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { loadEnv } from "../core/env.ts";
|
|
4
|
+
|
|
5
|
+
export async function runDev() {
|
|
6
|
+
loadEnv("development");
|
|
7
|
+
const devScript = resolve(import.meta.dir, "../core/dev.ts");
|
|
8
|
+
const proc = spawn(["bun", "run", devScript], {
|
|
9
|
+
stdout: "inherit",
|
|
10
|
+
stderr: "inherit",
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
});
|
|
13
|
+
await proc.exited;
|
|
14
|
+
}
|
package/src/cli/feat.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
import { addComponent } from "./add.ts";
|
|
5
|
+
|
|
6
|
+
// ─── bosia feat <feature> ─────────────────────────────────
|
|
7
|
+
// Fetches a feature scaffold from the GitHub registry.
|
|
8
|
+
// Installs required components, copies route/lib files, installs npm deps.
|
|
9
|
+
|
|
10
|
+
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
11
|
+
|
|
12
|
+
interface FeatureMeta {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
components: string[]; // bosia components to install via `bosia add`
|
|
16
|
+
files: string[]; // source filenames in the registry feature dir
|
|
17
|
+
targets: string[]; // destination paths relative to project root
|
|
18
|
+
npmDeps: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runFeat(name: string | undefined) {
|
|
22
|
+
if (!name) {
|
|
23
|
+
console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature>");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`⬡ Installing feature: ${name}\n`);
|
|
28
|
+
|
|
29
|
+
const meta = await fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
|
|
30
|
+
|
|
31
|
+
// Install required UI components
|
|
32
|
+
if (meta.components.length > 0) {
|
|
33
|
+
console.log("📦 Installing required components...");
|
|
34
|
+
for (const comp of meta.components) {
|
|
35
|
+
await addComponent(comp, false);
|
|
36
|
+
}
|
|
37
|
+
console.log("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Copy feature files to their target paths
|
|
41
|
+
for (let i = 0; i < meta.files.length; i++) {
|
|
42
|
+
const file = meta.files[i]!;
|
|
43
|
+
const target = meta.targets[i] ?? file;
|
|
44
|
+
const content = await fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
|
|
45
|
+
const dest = join(process.cwd(), target);
|
|
46
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
47
|
+
writeFileSync(dest, content, "utf-8");
|
|
48
|
+
console.log(` ✍️ ${target}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Install npm dependencies
|
|
52
|
+
const npmEntries = Object.entries(meta.npmDeps);
|
|
53
|
+
if (npmEntries.length > 0) {
|
|
54
|
+
const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
|
|
55
|
+
console.log(`\n📥 npm: ${packages.join(", ")}`);
|
|
56
|
+
const proc = spawn(["bun", "add", ...packages], {
|
|
57
|
+
stdout: "inherit",
|
|
58
|
+
stderr: "inherit",
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
});
|
|
61
|
+
if ((await proc.exited) !== 0) {
|
|
62
|
+
console.warn(`⚠️ bun add failed for: ${packages.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`\n✅ Feature "${name}" scaffolded!`);
|
|
67
|
+
if (meta.description) console.log(` ${meta.description}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchJSON<T>(url: string): Promise<T> {
|
|
71
|
+
const res = await fetch(url);
|
|
72
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
73
|
+
return res.json() as Promise<T>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchText(url: string): Promise<string> {
|
|
77
|
+
const res = await fetch(url);
|
|
78
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
79
|
+
return res.text();
|
|
80
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// ─── Bosia CLI ────────────────────────────────────────────
|
|
3
|
+
// bosia create <name> scaffold a new project
|
|
4
|
+
// bosia dev start the development server
|
|
5
|
+
// bosia build build for production
|
|
6
|
+
// bosia start run the production server
|
|
7
|
+
// bosia add <name> add a UI component from the registry
|
|
8
|
+
// bosia feat <name> add a feature scaffold from the registry
|
|
9
|
+
|
|
10
|
+
const [, , command, ...args] = process.argv;
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "create": {
|
|
15
|
+
const { runCreate } = await import("./create.ts");
|
|
16
|
+
await runCreate(args[0], args.slice(1));
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case "dev": {
|
|
20
|
+
const { runDev } = await import("./dev.ts");
|
|
21
|
+
await runDev();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "build": {
|
|
25
|
+
const { runBuild } = await import("./build.ts");
|
|
26
|
+
await runBuild();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "start": {
|
|
30
|
+
const { runStart } = await import("./start.ts");
|
|
31
|
+
await runStart();
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "add": {
|
|
35
|
+
const { runAdd } = await import("./add.ts");
|
|
36
|
+
await runAdd(args[0]);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "feat": {
|
|
40
|
+
const { runFeat } = await import("./feat.ts");
|
|
41
|
+
await runFeat(args[0]);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default: {
|
|
45
|
+
console.log(`
|
|
46
|
+
⬡ Bosia
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
bosia <command> [options]
|
|
50
|
+
|
|
51
|
+
Commands:
|
|
52
|
+
create <name> [--template <t>] Scaffold a new Bosia project
|
|
53
|
+
dev Start the development server
|
|
54
|
+
build Build for production
|
|
55
|
+
start Run the production server
|
|
56
|
+
add <component> Add a UI component from the registry
|
|
57
|
+
feat <feature> Add a feature scaffold from the registry
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
bosia create my-app
|
|
61
|
+
bosia create my-app --template demo
|
|
62
|
+
bosia dev
|
|
63
|
+
bosia build
|
|
64
|
+
bosia start
|
|
65
|
+
bosia add button
|
|
66
|
+
bosia feat login
|
|
67
|
+
`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error("❌", err.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
package/src/cli/start.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { loadEnv } from "../core/env.ts";
|
|
3
|
+
import { BOSIA_NODE_PATH } from "../core/paths.ts";
|
|
4
|
+
|
|
5
|
+
export async function runStart() {
|
|
6
|
+
loadEnv("production");
|
|
7
|
+
|
|
8
|
+
let serverEntry = "index.js";
|
|
9
|
+
try {
|
|
10
|
+
const manifest = await Bun.file("./dist/manifest.json").json();
|
|
11
|
+
serverEntry = manifest.serverEntry ?? "index.js";
|
|
12
|
+
} catch { }
|
|
13
|
+
|
|
14
|
+
const proc = spawn(["bun", "run", `dist/server/${serverEntry}`], {
|
|
15
|
+
stdout: "inherit",
|
|
16
|
+
stderr: "inherit",
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
NODE_ENV: "production",
|
|
21
|
+
NODE_PATH: BOSIA_NODE_PATH,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await proc.exited;
|
|
26
|
+
}
|