bosia 0.0.0 → 0.1.1
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 +49 -5
- package/src/cli/add.ts +151 -0
- package/src/cli/build.ts +16 -0
- package/src/cli/create.ts +113 -0
- package/src/cli/dev.ts +14 -0
- package/src/cli/feat.ts +80 -0
- package/src/cli/index.ts +78 -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,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"module": "index.ts",
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"@clack/prompts": "^1.1.0",
|
|
48
|
+
"@tailwindcss/cli": "^4.2.1",
|
|
49
|
+
"bun-plugin-svelte": "^0.0.6",
|
|
50
|
+
"clsx": "^2.1.1",
|
|
51
|
+
"elysia": "^1.4.26",
|
|
52
|
+
"svelte": "^5.53.6",
|
|
53
|
+
"tailwind-merge": "^3.5.0",
|
|
54
|
+
"tailwindcss": "^4.2.1"
|
|
11
55
|
}
|
|
12
56
|
}
|
package/src/cli/add.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
|
|
5
|
+
// ─── bosia add <component> ────────────────────────────────
|
|
6
|
+
// Fetches a component from the GitHub registry (or local registry
|
|
7
|
+
// with --local) and copies it into src/lib/components/ui/<name>/.
|
|
8
|
+
|
|
9
|
+
const REMOTE_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
|
+
// Resolved once in runAdd, used by addComponent
|
|
23
|
+
let registryRoot: string | null = null;
|
|
24
|
+
|
|
25
|
+
export async function runAdd(name: string | undefined, flags: string[] = []) {
|
|
26
|
+
if (!name) {
|
|
27
|
+
console.error("❌ Please provide a component name.\n Usage: bosia add <component> [--local]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (flags.includes("--local")) {
|
|
32
|
+
// Walk up from this file to find the repo's registry/ directory
|
|
33
|
+
registryRoot = resolveLocalRegistry();
|
|
34
|
+
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ensureUtils();
|
|
38
|
+
await addComponent(name, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function addComponent(name: string, root = false) {
|
|
42
|
+
if (installed.has(name)) return;
|
|
43
|
+
installed.add(name);
|
|
44
|
+
|
|
45
|
+
console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
|
|
46
|
+
|
|
47
|
+
const meta = await readMeta(name);
|
|
48
|
+
|
|
49
|
+
// Install component dependencies first (recursive)
|
|
50
|
+
for (const dep of meta.dependencies) {
|
|
51
|
+
await addComponent(dep, false);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Download/copy component files into src/lib/components/ui/<name>/
|
|
55
|
+
const destDir = join(process.cwd(), "src", "lib", "components", "ui", name);
|
|
56
|
+
mkdirSync(destDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
for (const file of meta.files) {
|
|
59
|
+
const content = await readFile(name, file);
|
|
60
|
+
const dest = join(destDir, file);
|
|
61
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
62
|
+
writeFileSync(dest, content, "utf-8");
|
|
63
|
+
console.log(` ✍️ src/lib/components/ui/${name}/${file}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Install npm dependencies
|
|
67
|
+
const npmEntries = Object.entries(meta.npmDeps);
|
|
68
|
+
if (npmEntries.length > 0) {
|
|
69
|
+
const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
|
|
70
|
+
console.log(` 📥 npm: ${packages.join(", ")}`);
|
|
71
|
+
const proc = spawn(["bun", "add", ...packages], {
|
|
72
|
+
stdout: "inherit",
|
|
73
|
+
stderr: "inherit",
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
});
|
|
76
|
+
if ((await proc.exited) !== 0) {
|
|
77
|
+
console.warn(` ⚠️ bun add failed for: ${packages.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (root) console.log(`\n✅ ${name} installed at src/lib/components/ui/${name}/`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Ensure $lib/utils.ts exists ─────────────────────────────
|
|
85
|
+
|
|
86
|
+
const UTILS_CONTENT = `import { clsx, type ClassValue } from "clsx";
|
|
87
|
+
import { twMerge } from "tailwind-merge";
|
|
88
|
+
|
|
89
|
+
export function cn(...inputs: ClassValue[]) {
|
|
90
|
+
return twMerge(clsx(inputs));
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
function ensureUtils() {
|
|
95
|
+
const utilsPath = join(process.cwd(), "src", "lib", "utils.ts");
|
|
96
|
+
if (!existsSync(utilsPath)) {
|
|
97
|
+
mkdirSync(dirname(utilsPath), { recursive: true });
|
|
98
|
+
writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
|
|
99
|
+
console.log(" ✍️ src/lib/utils.ts (cn utility)\n");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Registry resolvers ──────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function resolveLocalRegistry(): string {
|
|
106
|
+
// Walk up from this file's directory to find registry/
|
|
107
|
+
let dir = dirname(new URL(import.meta.url).pathname);
|
|
108
|
+
for (let i = 0; i < 10; i++) {
|
|
109
|
+
const candidate = join(dir, "registry");
|
|
110
|
+
if (existsSync(join(candidate, "index.json"))) return candidate;
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
console.error("❌ Could not find local registry/ directory.");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function readMeta(name: string): Promise<ComponentMeta> {
|
|
120
|
+
if (registryRoot) {
|
|
121
|
+
const path = join(registryRoot, "components", name, "meta.json");
|
|
122
|
+
if (!existsSync(path)) {
|
|
123
|
+
throw new Error(`Component "${name}" not found in local registry`);
|
|
124
|
+
}
|
|
125
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
126
|
+
}
|
|
127
|
+
return fetchJSON<ComponentMeta>(`${REMOTE_BASE}/components/${name}/meta.json`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function readFile(name: string, file: string): Promise<string> {
|
|
131
|
+
if (registryRoot) {
|
|
132
|
+
const path = join(registryRoot, "components", name, file);
|
|
133
|
+
if (!existsSync(path)) {
|
|
134
|
+
throw new Error(`File "${file}" not found for component "${name}" in local registry`);
|
|
135
|
+
}
|
|
136
|
+
return readFileSync(path, "utf-8");
|
|
137
|
+
}
|
|
138
|
+
return fetchText(`${REMOTE_BASE}/components/${name}/${file}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchJSON<T>(url: string): Promise<T> {
|
|
142
|
+
const res = await fetch(url);
|
|
143
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
144
|
+
return res.json() as Promise<T>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function fetchText(url: string): Promise<string> {
|
|
148
|
+
const res = await fetch(url);
|
|
149
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
150
|
+
return res.text();
|
|
151
|
+
}
|
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,113 @@
|
|
|
1
|
+
import { resolve, join, basename } from "path";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
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
|
+
const selected = await p.select({
|
|
83
|
+
message: "Which template?",
|
|
84
|
+
options: templates.map((t) => ({
|
|
85
|
+
value: t,
|
|
86
|
+
label: t,
|
|
87
|
+
hint: TEMPLATE_DESCRIPTIONS[t],
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (p.isCancel(selected)) {
|
|
92
|
+
p.cancel("Operation cancelled.");
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return selected as string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyDir(src: string, dest: string, projectName: string) {
|
|
100
|
+
mkdirSync(dest, { recursive: true });
|
|
101
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
102
|
+
const srcPath = join(src, entry.name);
|
|
103
|
+
const destPath = join(dest, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
copyDir(srcPath, destPath, projectName);
|
|
106
|
+
} else {
|
|
107
|
+
const content = readFileSync(srcPath, "utf-8")
|
|
108
|
+
.replaceAll("{{PROJECT_NAME}}", projectName)
|
|
109
|
+
.replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
|
|
110
|
+
writeFileSync(destPath, content, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
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
|
+
}
|