bosia 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/add.ts +46 -4
- package/src/cli/create.ts +36 -8
- package/src/cli/feat.ts +136 -12
- package/src/cli/index.ts +4 -2
- package/src/core/plugin.ts +4 -2
- package/src/core/routeFile.ts +45 -0
- package/templates/default/src/routes/+error.svelte +4 -4
- package/templates/demo/src/routes/+error.svelte +4 -4
- package/templates/drizzle/.env.example +2 -0
- package/templates/drizzle/README.md +69 -0
- package/templates/drizzle/drizzle.config.ts +10 -0
- package/templates/drizzle/instructions.txt +3 -0
- package/templates/drizzle/package.json +26 -0
- package/templates/drizzle/public/.gitkeep +0 -0
- package/templates/drizzle/public/favicon.svg +14 -0
- package/templates/drizzle/src/app.css +132 -0
- package/templates/drizzle/src/app.d.ts +14 -0
- package/templates/drizzle/src/features/drizzle/index.ts +15 -0
- package/templates/drizzle/src/features/drizzle/migrations/.gitkeep +0 -0
- package/templates/drizzle/src/features/drizzle/schemas.ts +1 -0
- package/templates/drizzle/src/features/drizzle/seeds/001_initial_todos.ts +11 -0
- package/templates/drizzle/src/features/drizzle/seeds/runner.ts +80 -0
- package/templates/drizzle/src/features/todo/index.ts +3 -0
- package/templates/drizzle/src/features/todo/queries.ts +36 -0
- package/templates/drizzle/src/features/todo/schemas/todo.table.ts +9 -0
- package/templates/drizzle/src/features/todo/types.ts +5 -0
- package/templates/drizzle/src/hooks.server.ts +20 -0
- package/templates/drizzle/src/lib/components/todo/index.ts +3 -0
- package/templates/drizzle/src/lib/components/todo/todo-form.svelte +23 -0
- package/templates/drizzle/src/lib/components/todo/todo-item.svelte +63 -0
- package/templates/drizzle/src/lib/components/todo/todo-list.svelte +21 -0
- package/templates/drizzle/src/lib/utils.ts +1 -0
- package/templates/drizzle/src/routes/+error.svelte +15 -0
- package/templates/drizzle/src/routes/+layout.server.ts +8 -0
- package/templates/drizzle/src/routes/+layout.svelte +6 -0
- package/templates/drizzle/src/routes/+page.svelte +55 -0
- package/templates/drizzle/src/routes/api/todos/+server.ts +18 -0
- package/templates/drizzle/src/routes/api/todos/[id]/+server.ts +42 -0
- package/templates/drizzle/src/routes/todos/+page.server.ts +52 -0
- package/templates/drizzle/src/routes/todos/+page.svelte +39 -0
- package/templates/drizzle/tsconfig.json +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
package/src/cli/add.ts
CHANGED
|
@@ -21,11 +21,23 @@ interface ComponentMeta {
|
|
|
21
21
|
npmDeps: Record<string, string>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface RegistryIndex {
|
|
25
|
+
components: string[];
|
|
26
|
+
features: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
// Track already-installed components within a session to avoid re-running deps
|
|
25
30
|
const installed = new Set<string>();
|
|
26
31
|
|
|
27
|
-
// Resolved once in runAdd, used by addComponent
|
|
32
|
+
// Resolved once in runAdd or initAddRegistry, used by addComponent
|
|
28
33
|
let registryRoot: string | null = null;
|
|
34
|
+
let registryIndex: RegistryIndex | null = null;
|
|
35
|
+
|
|
36
|
+
/** Initialize registry context so addComponent can be called externally (e.g. from feat.ts) */
|
|
37
|
+
export async function initAddRegistry(root: string | null) {
|
|
38
|
+
registryRoot = root;
|
|
39
|
+
registryIndex = await loadIndex();
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
export async function runAdd(name: string | undefined, flags: string[] = []) {
|
|
31
43
|
if (!name) {
|
|
@@ -39,17 +51,47 @@ export async function runAdd(name: string | undefined, flags: string[] = []) {
|
|
|
39
51
|
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
// Load index once to resolve component paths
|
|
55
|
+
registryIndex = await loadIndex();
|
|
56
|
+
|
|
42
57
|
ensureUtils();
|
|
43
58
|
await addComponent(name, true);
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
/**
|
|
47
|
-
* Resolve the
|
|
48
|
-
* - "
|
|
62
|
+
* Resolve the full registry path for a component using the index.
|
|
63
|
+
* - "todo" → "todo" (exact match in index)
|
|
64
|
+
* - "button" → "ui/button" (suffix match in index)
|
|
49
65
|
* - "shop/cart" → "shop/cart" (explicit path used as-is)
|
|
50
66
|
*/
|
|
51
67
|
function resolveDestPath(name: string): string {
|
|
52
|
-
|
|
68
|
+
if (name.includes("/")) return name;
|
|
69
|
+
|
|
70
|
+
if (registryIndex) {
|
|
71
|
+
// Exact match (e.g. "todo" → "todo")
|
|
72
|
+
if (registryIndex.components.includes(name)) return name;
|
|
73
|
+
// Suffix match (e.g. "button" → "ui/button")
|
|
74
|
+
const match = registryIndex.components.find(
|
|
75
|
+
(c) => c.endsWith(`/${name}`)
|
|
76
|
+
);
|
|
77
|
+
if (match) return match;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback for backwards compatibility
|
|
81
|
+
return `ui/${name}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadIndex(): Promise<RegistryIndex | null> {
|
|
85
|
+
try {
|
|
86
|
+
if (registryRoot) {
|
|
87
|
+
const path = join(registryRoot, "index.json");
|
|
88
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return await fetchJSON<RegistryIndex>(`${REMOTE_BASE}/index.json`);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
53
95
|
}
|
|
54
96
|
|
|
55
97
|
export async function addComponent(name: string, root = false) {
|
package/src/cli/create.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, join, basename } from "path";
|
|
1
|
+
import { resolve, join, basename, relative } from "path";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
@@ -12,6 +12,7 @@ const BOSIA_VERSION: string = BOSIA_PKG.version;
|
|
|
12
12
|
const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
|
|
13
13
|
default: "Minimal starter with routing and Tailwind",
|
|
14
14
|
demo: "Full-featured demo with hooks, API routes, form actions, and more",
|
|
15
|
+
drizzle: "PostgreSQL + Drizzle ORM with full CRUD todo demo",
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
@@ -34,6 +35,9 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
34
35
|
template = args[templateIdx + 1];
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Parse --local flag
|
|
39
|
+
const isLocal = args.includes("--local");
|
|
40
|
+
|
|
37
41
|
// If no --template flag, prompt interactively
|
|
38
42
|
if (!template) {
|
|
39
43
|
template = await promptTemplate();
|
|
@@ -49,7 +53,11 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
49
53
|
|
|
50
54
|
console.log(`\n⬡ Creating Bosia project: ${basename(targetDir)} (template: ${template})\n`);
|
|
51
55
|
|
|
52
|
-
copyDir(templateDir, targetDir, name);
|
|
56
|
+
copyDir(templateDir, targetDir, name, isLocal);
|
|
57
|
+
|
|
58
|
+
if (existsSync(join(targetDir, ".env.example"))) {
|
|
59
|
+
writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
|
|
60
|
+
}
|
|
53
61
|
|
|
54
62
|
console.log(`✅ Project created at ${targetDir}\n`);
|
|
55
63
|
|
|
@@ -63,7 +71,15 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
63
71
|
if (exitCode !== 0) {
|
|
64
72
|
console.warn("⚠️ bun install failed — run it manually.");
|
|
65
73
|
} else {
|
|
66
|
-
console.log(`\n🎉 Ready!\n\
|
|
74
|
+
console.log(`\n🎉 Ready!\n\ncd ${name}`);
|
|
75
|
+
|
|
76
|
+
const instPath = join(templateDir, "instructions.txt");
|
|
77
|
+
if (existsSync(instPath)) {
|
|
78
|
+
const instructions = readFileSync(instPath, "utf-8").trimEnd();
|
|
79
|
+
if (instructions) console.log(instructions);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`bun x bosia dev\n`);
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -96,17 +112,29 @@ async function promptTemplate(): Promise<string> {
|
|
|
96
112
|
return selected as string;
|
|
97
113
|
}
|
|
98
114
|
|
|
99
|
-
function copyDir(src: string, dest: string, projectName: string) {
|
|
115
|
+
function copyDir(src: string, dest: string, projectName: string, isLocal: boolean) {
|
|
100
116
|
mkdirSync(dest, { recursive: true });
|
|
101
117
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
102
118
|
const srcPath = join(src, entry.name);
|
|
103
119
|
const destPath = join(dest, entry.name);
|
|
120
|
+
|
|
121
|
+
// Do not copy instructions.txt to the final project
|
|
122
|
+
if (entry.name === "instructions.txt") continue;
|
|
123
|
+
|
|
104
124
|
if (entry.isDirectory()) {
|
|
105
|
-
copyDir(srcPath, destPath, projectName);
|
|
125
|
+
copyDir(srcPath, destPath, projectName, isLocal);
|
|
106
126
|
} else {
|
|
107
|
-
|
|
108
|
-
.replaceAll("{{PROJECT_NAME}}", projectName)
|
|
109
|
-
|
|
127
|
+
let content = readFileSync(srcPath, "utf-8")
|
|
128
|
+
.replaceAll("{{PROJECT_NAME}}", projectName);
|
|
129
|
+
|
|
130
|
+
if (entry.name === "package.json" && isLocal) {
|
|
131
|
+
const bosiaPath = resolve(import.meta.dir, "../../");
|
|
132
|
+
const relPath = relative(dest, bosiaPath);
|
|
133
|
+
content = content.replaceAll("\"^{{BOSIA_VERSION}}\"", `"file:${relPath}"`);
|
|
134
|
+
} else {
|
|
135
|
+
content = content.replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
writeFileSync(destPath, content, "utf-8");
|
|
111
139
|
}
|
|
112
140
|
}
|
package/src/cli/feat.ts
CHANGED
|
@@ -1,32 +1,64 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
|
-
import
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import { addComponent, initAddRegistry } from "./add.ts";
|
|
5
6
|
|
|
6
|
-
// ─── bosia feat <feature>
|
|
7
|
-
// Fetches a feature scaffold from the GitHub registry
|
|
8
|
-
//
|
|
7
|
+
// ─── bosia feat <feature> [--local] ──────────────────────
|
|
8
|
+
// Fetches a feature scaffold from the GitHub registry (or local
|
|
9
|
+
// registry with --local) and copies route/lib files, installs npm deps.
|
|
10
|
+
// Supports nested feature dependencies (e.g. todo → drizzle).
|
|
9
11
|
|
|
10
12
|
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
11
13
|
|
|
12
14
|
interface FeatureMeta {
|
|
13
15
|
name: string;
|
|
14
16
|
description: string;
|
|
17
|
+
features?: string[]; // other bosia features required
|
|
15
18
|
components: string[]; // bosia components to install via `bosia add`
|
|
16
19
|
files: string[]; // source filenames in the registry feature dir
|
|
17
20
|
targets: string[]; // destination paths relative to project root
|
|
18
21
|
npmDeps: Record<string, string>;
|
|
22
|
+
scripts?: Record<string, string>; // package.json scripts to add
|
|
23
|
+
envVars?: Record<string, string>; // env vars to append to .env if missing
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
let registryRoot: string | null = null;
|
|
27
|
+
|
|
28
|
+
// Track installed features to prevent circular dependencies
|
|
29
|
+
const installedFeats = new Set<string>();
|
|
30
|
+
|
|
31
|
+
export async function runFeat(name: string | undefined, flags: string[] = []) {
|
|
22
32
|
if (!name) {
|
|
23
|
-
console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature>");
|
|
33
|
+
console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature> [--local]");
|
|
24
34
|
process.exit(1);
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
if (flags.includes("--local")) {
|
|
38
|
+
registryRoot = resolveLocalRegistry();
|
|
39
|
+
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
40
|
+
}
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
// Initialize add.ts registry context so addComponent resolves paths correctly
|
|
43
|
+
await initAddRegistry(registryRoot);
|
|
44
|
+
|
|
45
|
+
await installFeature(name, true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function installFeature(name: string, isRoot: boolean) {
|
|
49
|
+
if (installedFeats.has(name)) return;
|
|
50
|
+
installedFeats.add(name);
|
|
51
|
+
|
|
52
|
+
console.log(isRoot ? `⬡ Installing feature: ${name}\n` : `\n⬡ Installing dependency feature: ${name}\n`);
|
|
53
|
+
|
|
54
|
+
const meta = await readMeta(name);
|
|
55
|
+
|
|
56
|
+
// Install required feature dependencies first (recursive)
|
|
57
|
+
if (meta.features && meta.features.length > 0) {
|
|
58
|
+
for (const feat of meta.features) {
|
|
59
|
+
await installFeature(feat, false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
30
62
|
|
|
31
63
|
// Install required UI components
|
|
32
64
|
if (meta.components.length > 0) {
|
|
@@ -41,8 +73,20 @@ export async function runFeat(name: string | undefined) {
|
|
|
41
73
|
for (let i = 0; i < meta.files.length; i++) {
|
|
42
74
|
const file = meta.files[i]!;
|
|
43
75
|
const target = meta.targets[i] ?? file;
|
|
44
|
-
const content = await fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
|
|
45
76
|
const dest = join(process.cwd(), target);
|
|
77
|
+
|
|
78
|
+
// Prompt before overwriting existing files
|
|
79
|
+
if (existsSync(dest)) {
|
|
80
|
+
const replace = await p.confirm({
|
|
81
|
+
message: `File "${target}" already exists. Replace it?`,
|
|
82
|
+
});
|
|
83
|
+
if (p.isCancel(replace) || !replace) {
|
|
84
|
+
console.log(` ⏭️ Skipped ${target}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const content = await readFile(name, file);
|
|
46
90
|
mkdirSync(dirname(dest), { recursive: true });
|
|
47
91
|
writeFileSync(dest, content, "utf-8");
|
|
48
92
|
console.log(` ✍️ ${target}`);
|
|
@@ -63,8 +107,88 @@ export async function runFeat(name: string | undefined) {
|
|
|
63
107
|
}
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
|
|
67
|
-
|
|
110
|
+
// Add package.json scripts
|
|
111
|
+
const scriptEntries = Object.entries(meta.scripts ?? {});
|
|
112
|
+
if (scriptEntries.length > 0) {
|
|
113
|
+
const pkgPath = join(process.cwd(), "package.json");
|
|
114
|
+
if (existsSync(pkgPath)) {
|
|
115
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
116
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
117
|
+
const added: string[] = [];
|
|
118
|
+
for (const [key, val] of scriptEntries) {
|
|
119
|
+
if (!pkg.scripts[key]) {
|
|
120
|
+
pkg.scripts[key] = val;
|
|
121
|
+
added.push(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (added.length > 0) {
|
|
125
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
126
|
+
console.log(`\n📜 Added scripts: ${added.join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Append env vars to .env if missing
|
|
132
|
+
const envEntries = Object.entries(meta.envVars ?? {});
|
|
133
|
+
if (envEntries.length > 0) {
|
|
134
|
+
const envPath = join(process.cwd(), ".env");
|
|
135
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
136
|
+
const toAdd: string[] = [];
|
|
137
|
+
for (const [key, val] of envEntries) {
|
|
138
|
+
if (!existing.includes(`${key}=`)) {
|
|
139
|
+
toAdd.push(`${key}=${val}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (toAdd.length > 0) {
|
|
143
|
+
const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
144
|
+
writeFileSync(envPath, existing + nl + toAdd.join("\n") + "\n", "utf-8");
|
|
145
|
+
console.log(`\n🔑 Added to .env: ${toAdd.map((l) => l.split("=")[0]).join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isRoot) {
|
|
150
|
+
console.log(`\n✅ Feature "${name}" scaffolded!`);
|
|
151
|
+
if (meta.description) console.log(` ${meta.description}`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(` ✅ Dependency feature "${name}" installed.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Registry resolvers ──────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function resolveLocalRegistry(): string {
|
|
160
|
+
let dir = dirname(new URL(import.meta.url).pathname);
|
|
161
|
+
for (let i = 0; i < 10; i++) {
|
|
162
|
+
const candidate = join(dir, "registry");
|
|
163
|
+
if (existsSync(join(candidate, "index.json"))) return candidate;
|
|
164
|
+
const parent = dirname(dir);
|
|
165
|
+
if (parent === dir) break;
|
|
166
|
+
dir = parent;
|
|
167
|
+
}
|
|
168
|
+
console.error("❌ Could not find local registry/ directory.");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readMeta(name: string): Promise<FeatureMeta> {
|
|
173
|
+
if (registryRoot) {
|
|
174
|
+
const path = join(registryRoot, "features", name, "meta.json");
|
|
175
|
+
if (!existsSync(path)) {
|
|
176
|
+
throw new Error(`Feature "${name}" not found in local registry`);
|
|
177
|
+
}
|
|
178
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
179
|
+
}
|
|
180
|
+
return fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readFile(name: string, file: string): Promise<string> {
|
|
184
|
+
if (registryRoot) {
|
|
185
|
+
const path = join(registryRoot, "features", name, file);
|
|
186
|
+
if (!existsSync(path)) {
|
|
187
|
+
throw new Error(`File "${file}" not found for feature "${name}" in local registry`);
|
|
188
|
+
}
|
|
189
|
+
return readFileSync(path, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
return fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
|
|
68
192
|
}
|
|
69
193
|
|
|
70
194
|
async function fetchJSON<T>(url: string): Promise<T> {
|
package/src/cli/index.ts
CHANGED
|
@@ -40,7 +40,9 @@ async function main() {
|
|
|
40
40
|
}
|
|
41
41
|
case "feat": {
|
|
42
42
|
const { runFeat } = await import("./feat.ts");
|
|
43
|
-
|
|
43
|
+
const featName = args.find((a) => !a.startsWith("--"));
|
|
44
|
+
const featFlags = args.filter((a) => a.startsWith("--"));
|
|
45
|
+
await runFeat(featName, featFlags);
|
|
44
46
|
break;
|
|
45
47
|
}
|
|
46
48
|
default: {
|
|
@@ -56,7 +58,7 @@ Commands:
|
|
|
56
58
|
build Build for production
|
|
57
59
|
start Run the production server
|
|
58
60
|
add <component> Add a UI component from the registry
|
|
59
|
-
feat <feature> Add a feature scaffold from the registry
|
|
61
|
+
feat <feature> Add a feature scaffold from the registry [--local]
|
|
60
62
|
|
|
61
63
|
Examples:
|
|
62
64
|
bosia create my-app
|
package/src/core/plugin.ts
CHANGED
|
@@ -22,9 +22,11 @@ export function makeBosiaPlugin(target: "browser" | "bun" = "bun") {
|
|
|
22
22
|
return {
|
|
23
23
|
name: "bosia-resolver",
|
|
24
24
|
setup(build: import("bun").PluginBuilder) {
|
|
25
|
-
// bosia:routes → .bosia/routes.ts
|
|
25
|
+
// bosia:routes → .bosia/routes.client.ts (browser) or .bosia/routes.ts (server)
|
|
26
|
+
// Client-only file excludes serverRoutes/apiRoutes to prevent the browser
|
|
27
|
+
// bundler from following server-side dynamic imports into Node builtins.
|
|
26
28
|
build.onResolve({ filter: /^bosia:routes$/ }, () => ({
|
|
27
|
-
path: join(process.cwd(), ".bosia", "routes.ts"),
|
|
29
|
+
path: join(process.cwd(), ".bosia", target === "browser" ? "routes.client.ts" : "routes.ts"),
|
|
28
30
|
}));
|
|
29
31
|
|
|
30
32
|
// $env → .bosia/env.client.ts (browser) or .bosia/env.server.ts (bun)
|
package/src/core/routeFile.ts
CHANGED
|
@@ -101,11 +101,56 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
101
101
|
|
|
102
102
|
mkdirSync(".bosia", { recursive: true });
|
|
103
103
|
writeFileSync(".bosia/routes.ts", lines.join("\n"));
|
|
104
|
+
|
|
105
|
+
// Generate client-only routes file (no server imports that could pull in Node builtins)
|
|
106
|
+
generateClientRoutesFile(pages, manifest.errorPage);
|
|
107
|
+
|
|
104
108
|
const pagePatterns = pages.map(p => p.pattern).join(", ") || "(none)";
|
|
105
109
|
console.log(`✅ Routes generated: .bosia/routes.ts`);
|
|
106
110
|
console.log(` Found ${pages.length} page route(s): ${pagePatterns}`);
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
// ─── Client-only routes file ─────────────────────────────
|
|
114
|
+
// Separate file with only clientRoutes + errorPage to prevent
|
|
115
|
+
// the browser bundler from following server-side dynamic imports
|
|
116
|
+
// (e.g. +page.server.ts → drizzle-orm → postgres → Node builtins).
|
|
117
|
+
|
|
118
|
+
function generateClientRoutesFile(
|
|
119
|
+
pages: RouteManifest["pages"],
|
|
120
|
+
errorPage: RouteManifest["errorPage"],
|
|
121
|
+
): void {
|
|
122
|
+
const lines: string[] = [
|
|
123
|
+
"// AUTO-GENERATED by bosia build — client-only routes (no server imports)\n",
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
lines.push("export const clientRoutes: Array<{");
|
|
127
|
+
lines.push(" pattern: string;");
|
|
128
|
+
lines.push(" page: () => Promise<any>;");
|
|
129
|
+
lines.push(" layouts: (() => Promise<any>)[];");
|
|
130
|
+
lines.push(" hasServerData: boolean;");
|
|
131
|
+
lines.push("}> = [");
|
|
132
|
+
for (const r of pages) {
|
|
133
|
+
const layoutImports = r.layouts
|
|
134
|
+
.map(l => `() => import(${JSON.stringify(toImportPath(l))})`)
|
|
135
|
+
.join(", ");
|
|
136
|
+
const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
|
|
137
|
+
lines.push(" {");
|
|
138
|
+
lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
|
|
139
|
+
lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
|
|
140
|
+
lines.push(` layouts: [${layoutImports}],`);
|
|
141
|
+
lines.push(` hasServerData: ${hasServerData},`);
|
|
142
|
+
lines.push(" },");
|
|
143
|
+
}
|
|
144
|
+
lines.push("];\n");
|
|
145
|
+
|
|
146
|
+
const ep = errorPage;
|
|
147
|
+
lines.push(`export const errorPage: (() => Promise<any>) | null = ${
|
|
148
|
+
ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
|
|
149
|
+
};\n`);
|
|
150
|
+
|
|
151
|
+
writeFileSync(".bosia/routes.client.ts", lines.join("\n"));
|
|
152
|
+
}
|
|
153
|
+
|
|
109
154
|
// Import path from .bosia/routes.ts to src/routes/<routePath>
|
|
110
155
|
function toImportPath(routePath: string): string {
|
|
111
156
|
return "../src/routes/" + routePath.replace(/\\/g, "/");
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
let {
|
|
2
|
+
let { error }: { error: { status: number; message: string } } = $props();
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<svelte:head>
|
|
6
|
-
<title>{
|
|
6
|
+
<title>{error.status} — {error.message}</title>
|
|
7
7
|
</svelte:head>
|
|
8
8
|
|
|
9
9
|
<main class="flex min-h-screen flex-col items-center justify-center gap-4 text-center px-4">
|
|
10
|
-
<p class="text-8xl font-bold text-gray-200">{
|
|
11
|
-
<p class="text-2xl font-semibold">{
|
|
10
|
+
<p class="text-8xl font-bold text-gray-200">{error.status}</p>
|
|
11
|
+
<p class="text-2xl font-semibold">{error.message}</p>
|
|
12
12
|
<a
|
|
13
13
|
href="/"
|
|
14
14
|
class="mt-4 rounded-md border px-4 py-2 text-sm hover:bg-muted transition-colors"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
let {
|
|
2
|
+
let { error }: { error: { status: number; message: string } } = $props();
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<svelte:head>
|
|
6
|
-
<title>{
|
|
6
|
+
<title>{error.status} — {error.message}</title>
|
|
7
7
|
</svelte:head>
|
|
8
8
|
|
|
9
9
|
<div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
|
|
10
|
-
<p class="text-8xl font-bold text-gray-200">{
|
|
11
|
-
<p class="text-2xl font-semibold text-gray-700">{
|
|
10
|
+
<p class="text-8xl font-bold text-gray-200">{error.status}</p>
|
|
11
|
+
<p class="text-2xl font-semibold text-gray-700">{error.message}</p>
|
|
12
12
|
<a href="/" class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors">
|
|
13
13
|
Go home
|
|
14
14
|
</a>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A fullstack app built with [Bosia](https://github.com/bosapi/bosia) + Drizzle ORM + PostgreSQL.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh/) v1.1+
|
|
8
|
+
- [PostgreSQL](https://www.postgresql.org/) running locally or remotely
|
|
9
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Copy env and set your DATABASE_URL
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
|
|
16
|
+
# Push schema to database
|
|
17
|
+
bun run db:push
|
|
18
|
+
|
|
19
|
+
# Seed initial data
|
|
20
|
+
bun run db:seed
|
|
21
|
+
|
|
22
|
+
# Start dev server
|
|
23
|
+
bun x bosia dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Visit [http://localhost:9000](http://localhost:9000) to see the app.
|
|
27
|
+
|
|
28
|
+
## Scripts
|
|
29
|
+
|
|
30
|
+
| Command | Description |
|
|
31
|
+
|---------|-------------|
|
|
32
|
+
| `bun x bosia dev` | Start dev server with HMR |
|
|
33
|
+
| `bun x bosia build` | Production build |
|
|
34
|
+
| `bun x bosia start` | Start production server |
|
|
35
|
+
| `bun run db:generate` | Generate migration from schema changes |
|
|
36
|
+
| `bun run db:migrate` | Apply pending migrations |
|
|
37
|
+
| `bun run db:push` | Push schema directly (dev shortcut) |
|
|
38
|
+
| `bun run db:studio` | Open Drizzle Studio GUI |
|
|
39
|
+
| `bun run db:seed` | Run pending seed files |
|
|
40
|
+
|
|
41
|
+
## Project Structure
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
├── features/
|
|
46
|
+
│ ├── drizzle/ # DB infrastructure
|
|
47
|
+
│ │ ├── index.ts # Connection singleton
|
|
48
|
+
│ │ ├── schemas.ts # Schema aggregator
|
|
49
|
+
│ │ ├── migrations/ # Drizzle Kit output
|
|
50
|
+
│ │ └── seeds/ # Seed files + runner
|
|
51
|
+
│ └── todo/ # Business feature
|
|
52
|
+
│ ├── schemas/ # Table definitions
|
|
53
|
+
│ ├── queries.ts # Typed CRUD
|
|
54
|
+
│ └── types.ts # Inferred types
|
|
55
|
+
├── lib/components/todo/ # UI components
|
|
56
|
+
└── routes/
|
|
57
|
+
├── todos/ # CRUD page with form actions
|
|
58
|
+
└── api/todos/ # REST API
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Adding Features
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Add DB support to any Bosia app
|
|
65
|
+
bosia feat drizzle
|
|
66
|
+
|
|
67
|
+
# Add the todo feature (requires drizzle)
|
|
68
|
+
bosia feat todo
|
|
69
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bosia dev",
|
|
7
|
+
"build": "bosia build",
|
|
8
|
+
"start": "bosia start",
|
|
9
|
+
"db:generate": "drizzle-kit generate",
|
|
10
|
+
"db:migrate": "drizzle-kit migrate",
|
|
11
|
+
"db:seed": "bun run src/features/drizzle/seeds/runner.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"bosia": "^{{BOSIA_VERSION}}",
|
|
15
|
+
"svelte": "^5.20.0",
|
|
16
|
+
"clsx": "^2.1.1",
|
|
17
|
+
"tailwind-merge": "^3.5.0",
|
|
18
|
+
"drizzle-orm": "^0.44.0",
|
|
19
|
+
"postgres": "^3.4.5"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"typescript": "^5",
|
|
24
|
+
"drizzle-kit": "^0.31.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="currentColor" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="currentColor" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="currentColor" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="currentColor" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="currentColor" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|