bosia 0.6.20 → 0.6.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +2 -2
  2. package/src/cli/add.ts +5 -5
  3. package/src/cli/addRouter.ts +53 -0
  4. package/src/cli/block.ts +19 -12
  5. package/src/cli/create.ts +6 -11
  6. package/src/cli/feat.ts +19 -22
  7. package/src/cli/index.ts +25 -23
  8. package/src/cli/manifest.ts +1 -1
  9. package/src/cli/registry.ts +40 -2
  10. package/src/core/build.ts +1 -3
  11. package/src/core/client/App.svelte +3 -8
  12. package/src/core/client/page.svelte.ts +28 -0
  13. package/src/core/client/router.svelte.ts +3 -8
  14. package/src/core/config.ts +1 -4
  15. package/src/core/cookies.ts +1 -2
  16. package/src/core/dev-500.ts +1 -1
  17. package/src/core/html.ts +1 -2
  18. package/src/core/plugin.ts +1 -3
  19. package/src/core/plugins/inspector/bun-plugin.ts +1 -4
  20. package/src/core/plugins/inspector/index.ts +45 -59
  21. package/src/core/renderer.ts +3 -10
  22. package/src/core/routeTypes.ts +3 -9
  23. package/src/core/scanner.ts +1 -3
  24. package/src/core/server.ts +9 -34
  25. package/src/core/staticManifest.ts +1 -3
  26. package/src/core/svelteAudit.ts +2 -5
  27. package/src/core/svelteCompiler.ts +2 -8
  28. package/src/lib/client.ts +1 -0
  29. package/templates/default/.prettierignore +1 -0
  30. package/templates/demo/.prettierignore +1 -0
  31. package/templates/shop/.env.example +12 -0
  32. package/templates/shop/.prettierignore +7 -0
  33. package/templates/shop/.prettierrc.json +9 -0
  34. package/templates/shop/README.md +62 -0
  35. package/templates/shop/_gitignore +12 -0
  36. package/templates/shop/bosia.config.ts +10 -0
  37. package/templates/shop/instructions.txt +8 -0
  38. package/templates/shop/package.json +27 -0
  39. package/templates/shop/public/favicon.svg +14 -0
  40. package/templates/shop/public/logo-dark.svg +14 -0
  41. package/templates/shop/public/logo-light.svg +14 -0
  42. package/templates/shop/src/app.css +132 -0
  43. package/templates/shop/src/app.d.ts +14 -0
  44. package/templates/shop/src/app.html +11 -0
  45. package/templates/shop/src/hooks.server.ts +21 -0
  46. package/templates/shop/src/lib/utils.ts +1 -0
  47. package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
  48. package/templates/shop/src/routes/(private)/+layout.svelte +14 -0
  49. package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
  50. package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
  51. package/templates/shop/src/routes/(public)/+page.svelte +30 -0
  52. package/templates/shop/src/routes/+error.svelte +19 -0
  53. package/templates/shop/src/routes/+layout.server.ts +9 -0
  54. package/templates/shop/src/routes/+layout.svelte +6 -0
  55. package/templates/shop/template.json +10 -0
  56. package/templates/shop/tsconfig.json +22 -0
  57. package/templates/todo/.prettierignore +1 -0
  58. package/templates/todo/template.json +4 -1
@@ -160,10 +160,7 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
160
160
  // line numbers differ. The resolver translates browser-side stack
161
161
  // frames (delivered via SSE), which run client code.
162
162
  if (dev && generate === "client" && result.js.map) {
163
- const m =
164
- typeof result.js.map === "string"
165
- ? JSON.parse(result.js.map)
166
- : result.js.map;
163
+ const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
167
164
  svelteMapCache.set(args.path, m);
168
165
  }
169
166
 
@@ -174,9 +174,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
174
174
  name: "inspector",
175
175
 
176
176
  build: {
177
- bunPlugins: (target) => [
178
- createInspectorBunPlugin({ cwd: process.cwd(), target, dev: true }),
179
- ],
177
+ bunPlugins: (target) => [createInspectorBunPlugin({ cwd: process.cwd(), target, dev: true })],
180
178
  },
181
179
 
182
180
  backend: {
@@ -195,13 +193,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
195
193
  const line = Number.isFinite(data.line) ? Number(data.line) : null;
196
194
  const col = Number.isFinite(data.col) ? Number(data.col) : 1;
197
195
  if (!file || line === null) {
198
- return new Response(
199
- JSON.stringify({ ok: false, error: "missing file/line" }),
200
- {
201
- status: 400,
202
- headers: { "content-type": "application/json" },
203
- },
204
- );
196
+ return new Response(JSON.stringify({ ok: false, error: "missing file/line" }), {
197
+ status: 400,
198
+ headers: { "content-type": "application/json" },
199
+ });
205
200
  }
206
201
 
207
202
  const comment = typeof data.comment === "string" ? data.comment.trim() : "";
@@ -245,13 +240,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
245
240
  return { ok: true, mode: "ai" as const };
246
241
  } catch (err) {
247
242
  console.error("[inspector] aiEndpoint POST failed:", err);
248
- return new Response(
249
- JSON.stringify({ ok: false, error: "ai endpoint failed" }),
250
- {
251
- status: 502,
252
- headers: { "content-type": "application/json" },
253
- },
254
- );
243
+ return new Response(JSON.stringify({ ok: false, error: "ai endpoint failed" }), {
244
+ status: 502,
245
+ headers: { "content-type": "application/json" },
246
+ });
255
247
  }
256
248
  }
257
249
 
@@ -266,13 +258,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
266
258
  });
267
259
  } catch (err) {
268
260
  console.error(`[inspector] failed to launch "${editor}":`, err);
269
- return new Response(
270
- JSON.stringify({ ok: false, error: "editor launch failed" }),
271
- {
272
- status: 500,
273
- headers: { "content-type": "application/json" },
274
- },
275
- );
261
+ return new Response(JSON.stringify({ ok: false, error: "editor launch failed" }), {
262
+ status: 500,
263
+ headers: { "content-type": "application/json" },
264
+ });
276
265
  }
277
266
  return { ok: true, mode: "editor" as const };
278
267
  });
@@ -297,46 +286,43 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
297
286
  // Live SSE stream. New clients also get a flush of the bounded
298
287
  // replay buffer so errors that fired during a failing render
299
288
  // (before the 500 page's overlay could subscribe) are visible.
300
- chained = chained.get(
301
- "/__bosia/errors",
302
- ({ request }: { request: Request }) => {
303
- const stream = new ReadableStream<Uint8Array>({
304
- start(ctrl) {
305
- sseClients.add(ctrl);
289
+ chained = chained.get("/__bosia/errors", ({ request }: { request: Request }) => {
290
+ const stream = new ReadableStream<Uint8Array>({
291
+ start(ctrl) {
292
+ sseClients.add(ctrl);
293
+ try {
294
+ ctrl.enqueue(encode(":ok\n\n"));
295
+ } catch {
296
+ sseClients.delete(ctrl);
297
+ return;
298
+ }
299
+ flushReplay(ctrl);
300
+ const ping = setInterval(() => {
306
301
  try {
307
- ctrl.enqueue(encode(":ok\n\n"));
302
+ ctrl.enqueue(encode(":ping\n\n"));
308
303
  } catch {
304
+ clearInterval(ping);
309
305
  sseClients.delete(ctrl);
310
- return;
311
306
  }
312
- flushReplay(ctrl);
313
- const ping = setInterval(() => {
314
- try {
315
- ctrl.enqueue(encode(":ping\n\n"));
316
- } catch {
317
- clearInterval(ping);
318
- sseClients.delete(ctrl);
319
- }
320
- }, 25_000);
307
+ }, 25_000);
321
308
 
322
- request.signal.addEventListener("abort", () => {
323
- clearInterval(ping);
324
- sseClients.delete(ctrl);
325
- try {
326
- ctrl.close();
327
- } catch {}
328
- });
329
- },
330
- });
331
- return new Response(stream, {
332
- headers: {
333
- "Content-Type": "text/event-stream; charset=utf-8",
334
- "Cache-Control": "no-cache",
335
- Connection: "keep-alive",
336
- },
337
- });
338
- },
339
- ) as unknown as Elysia;
309
+ request.signal.addEventListener("abort", () => {
310
+ clearInterval(ping);
311
+ sseClients.delete(ctrl);
312
+ try {
313
+ ctrl.close();
314
+ } catch {}
315
+ });
316
+ },
317
+ });
318
+ return new Response(stream, {
319
+ headers: {
320
+ "Content-Type": "text/event-stream; charset=utf-8",
321
+ "Cache-Control": "no-cache",
322
+ Connection: "keep-alive",
323
+ },
324
+ });
325
+ }) as unknown as Elysia;
340
326
  }
341
327
 
342
328
  return chained;
@@ -148,8 +148,7 @@ function makeFetch(req: Request, url: URL) {
148
148
 
149
149
  const headers = new Headers(init?.headers);
150
150
  const trusted =
151
- targetOrigin !== null &&
152
- (targetOrigin === sameOrigin || INTERNAL_HOSTS.has(targetOrigin));
151
+ targetOrigin !== null && (targetOrigin === sameOrigin || INTERNAL_HOSTS.has(targetOrigin));
153
152
  if (cookie && trusted && !headers.has("cookie")) headers.set("cookie", cookie);
154
153
 
155
154
  return globalThis.fetch(resolved, { ...init, headers });
@@ -927,9 +926,7 @@ export async function renderErrorPage(
927
926
  const K = picked.depth;
928
927
  const [errorMod, layoutMods] = await Promise.all([
929
928
  picked.loader(),
930
- Promise.all(
931
- route.layoutModules.slice(0, K).map((l: () => Promise<any>) => l()),
932
- ),
929
+ Promise.all(route.layoutModules.slice(0, K).map((l: () => Promise<any>) => l())),
933
930
  ]);
934
931
  const layoutData: Record<string, any>[] = [];
935
932
  for (let i = 0; i < K; i++) layoutData.push(partialLayoutData?.[i] ?? {});
@@ -962,11 +959,7 @@ export async function renderErrorPage(
962
959
  return compress(html, "text/html; charset=utf-8", req, status);
963
960
  } catch (err) {
964
961
  if (isDev) console.error("Nested error page render failed:", err);
965
- else
966
- console.error(
967
- "Nested error page render failed:",
968
- (err as Error).message ?? err,
969
- );
962
+ else console.error("Nested error page render failed:", (err as Error).message ?? err);
970
963
  if (isDev) reportDevErrorFromCatch(err);
971
964
  // fall through to global / text fallback
972
965
  }
@@ -78,9 +78,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
78
78
  params.length === 0 ? `{}` : `{ ${params.map((p) => `${p}: string`).join("; ")} }`;
79
79
 
80
80
  const lines: string[] = ["// AUTO-GENERATED by bosia — do not edit\n"];
81
- lines.push(
82
- `import type { LoadEvent, MetadataEvent, RequestEvent, Metadata } from 'bosia';`,
83
- );
81
+ lines.push(`import type { LoadEvent, MetadataEvent, RequestEvent, Metadata } from 'bosia';`);
84
82
  lines.push(``);
85
83
  lines.push(`export type Params = ${paramsType};`);
86
84
  lines.push(``);
@@ -126,9 +124,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
126
124
  lines.push(
127
125
  `type _ActionReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;`,
128
126
  );
129
- lines.push(
130
- `type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`,
131
- );
127
+ lines.push(`type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`);
132
128
  lines.push(
133
129
  `export type ActionData = _actions extends Record<string, (...args: any[]) => any>`,
134
130
  );
@@ -140,9 +136,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
140
136
  lines.push(`\nimport type { load as _layoutLoad } from '${srcBase}+layout.server.ts';`);
141
137
  lines.push(`export type LayoutServerLoad = (event: _LoadEvent) => any;`);
142
138
  lines.push(`export type LayoutData = Awaited<ReturnType<typeof _layoutLoad>>;`);
143
- lines.push(
144
- `export type LayoutProps = { data: LayoutData; params: Params; children: any };`,
145
- );
139
+ lines.push(`export type LayoutProps = { data: LayoutData; params: Params; children: any };`);
146
140
  }
147
141
 
148
142
  const outDir = join(process.cwd(), ".bosia", "types", "src", "routes", ...segments);
@@ -93,9 +93,7 @@ export function scanRoutes(): RouteManifest {
93
93
  ? join(dir, "+page.server.ts")
94
94
  : null;
95
95
 
96
- const pageTs = pageServerFile
97
- ? readTrailingSlash(join(ROUTES_DIR, pageServerFile))
98
- : null;
96
+ const pageTs = pageServerFile ? readTrailingSlash(join(ROUTES_DIR, pageServerFile)) : null;
99
97
  const effectiveTs: TrailingSlash = pageTs ?? currentTrailingSlash;
100
98
 
101
99
  pages.push({
@@ -234,21 +234,11 @@ async function resolve(event: RequestEvent): Promise<Response> {
234
234
  const mask = invalidatedBits
235
235
  ? buildMaskFromBits(
236
236
  invalidatedBits,
237
- pageMatch?.route
238
- ? ((pageMatch.route as any).layoutModules?.length ?? 0)
239
- : 0,
237
+ pageMatch?.route ? ((pageMatch.route as any).layoutModules?.length ?? 0) : 0,
240
238
  )
241
239
  : undefined;
242
240
  const runLoad = async () => {
243
- const data = await loadRouteData(
244
- routeUrl,
245
- locals,
246
- request,
247
- cookies,
248
- null,
249
- pageMatch,
250
- mask,
251
- );
241
+ const data = await loadRouteData(routeUrl, locals, request, cookies, null, pageMatch, mask);
252
242
 
253
243
  let metadata = null;
254
244
  if (pageMatch) {
@@ -278,14 +268,10 @@ async function resolve(event: RequestEvent): Promise<Response> {
278
268
  ? `${dedupKey(routeUrl)}|m=${invalidatedBits}`
279
269
  : dedupKey(routeUrl);
280
270
  const result =
281
- pageMatch?.route.scope === "private"
282
- ? await runLoad()
283
- : await dedup(dedupK, runLoad);
271
+ pageMatch?.route.scope === "private" ? await runLoad() : await dedup(dedupK, runLoad);
284
272
 
285
273
  const cookiesWereAccessed = (cookies as CookieJar).accessed || result.cookiesAccessed;
286
- const cc = cookiesWereAccessed
287
- ? "private, no-cache"
288
- : "public, max-age=0, must-revalidate";
274
+ const cc = cookiesWereAccessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
289
275
 
290
276
  if (!result.data) {
291
277
  return compress(
@@ -461,9 +447,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
461
447
  if (hit) {
462
448
  return new Response(
463
449
  Bun.file(hit.absPath),
464
- hit.cacheControl
465
- ? { headers: { "Cache-Control": hit.cacheControl } }
466
- : undefined,
450
+ hit.cacheControl ? { headers: { "Cache-Control": hit.cacheControl } } : undefined,
467
451
  );
468
452
  }
469
453
  return new Response("Not Found", { status: 404 });
@@ -510,9 +494,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
510
494
  if (!isDev) {
511
495
  // Try both `<path>/index.html` (always/ignore mode) and `<path>.html` (never mode)
512
496
  const prerenderCandidates =
513
- path === "/"
514
- ? ["index.html"]
515
- : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
497
+ path === "/" ? ["index.html"] : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
516
498
  for (const candidate of prerenderCandidates) {
517
499
  const prerenderPath = safePath(`${OUT_DIR}/prerendered`, candidate);
518
500
  if (!prerenderPath) continue;
@@ -533,10 +515,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
533
515
 
534
516
  // Trailing-slash canonicalization — 308 preserves method (form POSTs included)
535
517
  if (pageMatch) {
536
- const canonical = canonicalPathname(
537
- path,
538
- (pageMatch.route as any).trailingSlash ?? "never",
539
- );
518
+ const canonical = canonicalPathname(path, (pageMatch.route as any).trailingSlash ?? "never");
540
519
  if (canonical !== null) {
541
520
  return new Response(null, {
542
521
  status: 308,
@@ -854,15 +833,11 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
854
833
  function parseCorsMaxAge(value?: string): number | undefined {
855
834
  if (!value) return undefined;
856
835
  if (!/^\d+$/.test(value)) {
857
- throw new Error(
858
- `Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`,
859
- );
836
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
860
837
  }
861
838
  const n = parseInt(value, 10);
862
839
  if (!Number.isFinite(n) || n > Number.MAX_SAFE_INTEGER) {
863
- throw new Error(
864
- `Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`,
865
- );
840
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
866
841
  }
867
842
  return n;
868
843
  }
@@ -50,9 +50,7 @@ export function buildStaticManifest(outDir: string): StaticManifest {
50
50
  const clientRoot = join(outAbs, "client");
51
51
  if (existsSync(clientRoot)) {
52
52
  for (const { abs, rel } of walk(clientRoot)) {
53
- const cacheControl = HASHED_BASENAME.test(basename(rel))
54
- ? IMMUTABLE_CACHE
55
- : DEFAULT_CACHE;
53
+ const cacheControl = HASHED_BASENAME.test(basename(rel)) ? IMMUTABLE_CACHE : DEFAULT_CACHE;
56
54
  addOnce(manifest, `/dist/client/${rel}`, { absPath: abs, cacheControl });
57
55
  }
58
56
  }
@@ -121,9 +121,7 @@ function extractBindings(ast: AnyNode): Binding[] {
121
121
  case "ImportDeclaration": {
122
122
  const sourceNode = stmt.source as AnyNode | undefined;
123
123
  const source =
124
- sourceNode && typeof sourceNode.value === "string"
125
- ? (sourceNode.value as string)
126
- : "";
124
+ sourceNode && typeof sourceNode.value === "string" ? (sourceNode.value as string) : "";
127
125
  const specs = stmt.specifiers as AnyNode[] | undefined;
128
126
  if (!Array.isArray(specs)) break;
129
127
  for (const spec of specs) {
@@ -344,8 +342,7 @@ function collectTemplateRefs(source: string, fragment: AnyNode): TemplateRef[] {
344
342
  // name into the surrounding scope so `<MySnippet/>` doesn't false-
345
343
  // positive. The expression's name is the snippet's identifier.
346
344
  const expr = n.expression as AnyNode | undefined;
347
- const snippetName =
348
- expr && typeof expr.name === "string" ? (expr.name as string) : null;
345
+ const snippetName = expr && typeof expr.name === "string" ? (expr.name as string) : null;
349
346
  if (snippetName && scopeStack.length > 0) {
350
347
  scopeStack[scopeStack.length - 1].add(snippetName);
351
348
  } else if (snippetName) {
@@ -113,10 +113,7 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
113
113
  // Server (Bun) compile output has different line numbers and would
114
114
  // clobber the client entry under the same cache key.
115
115
  if (dev && target === "browser" && result.js.map) {
116
- const m =
117
- typeof result.js.map === "string"
118
- ? JSON.parse(result.js.map)
119
- : result.js.map;
116
+ const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
120
117
  svelteMapCache.set(args.path, m);
121
118
  }
122
119
  const contents = dev ? fixBindShadow(result.js.code) : result.js.code;
@@ -134,10 +131,7 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
134
131
  filename: args.path,
135
132
  });
136
133
  if (dev && target === "browser" && result.js.map) {
137
- const m =
138
- typeof result.js.map === "string"
139
- ? JSON.parse(result.js.map)
140
- : result.js.map;
134
+ const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
141
135
  svelteMapCache.set(args.path, m);
142
136
  }
143
137
  return { contents: result.js.code, loader: "js" };
package/src/lib/client.ts CHANGED
@@ -18,3 +18,4 @@ export {
18
18
  invalidateAll,
19
19
  } from "../core/client/navigation.ts";
20
20
  export type { GotoOptions, Navigation, NavigationTarget } from "../core/client/navigation.ts";
21
+ export { page } from "../core/client/page.svelte.ts";
@@ -4,3 +4,4 @@ build
4
4
  .bosia
5
5
  bun.lock
6
6
  public/bosia-tw.css
7
+ bosia.json
@@ -4,3 +4,4 @@ build
4
4
  .bosia
5
5
  bun.lock
6
6
  public/bosia-tw.css
7
+ bosia.json
@@ -0,0 +1,12 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/shop
2
+
3
+ SESSION_SECRET=change-me-in-production
4
+
5
+ STORAGE_DRIVER=s3
6
+ S3_BUCKET=
7
+ S3_REGION=auto
8
+ S3_ACCESS_KEY_ID=
9
+ S3_SECRET_ACCESS_KEY=
10
+ S3_ENDPOINT=
11
+
12
+ PUBLIC_BASE_URL=
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ .bosia
5
+ bun.lock
6
+ public/bosia-tw.css
7
+ bosia.json
@@ -0,0 +1,9 @@
1
+ {
2
+ "useTabs": true,
3
+ "tabWidth": 2,
4
+ "singleQuote": false,
5
+ "trailingComma": "all",
6
+ "printWidth": 100,
7
+ "plugins": ["prettier-plugin-svelte"],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
@@ -0,0 +1,62 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ An online-store starter built with [Bosia](https://github.com/bosapi/bosia) — auth, RBAC, S3-backed uploads, and the shop domain (products / orders / cart).
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) v1.1+
8
+ - PostgreSQL running locally or remotely
9
+ - An S3-compatible bucket (AWS S3, Cloudflare R2, MinIO, ...)
10
+
11
+ ## Getting Started
12
+
13
+ ```bash
14
+ cp .env.example .env
15
+ # fill DATABASE_URL, SESSION_SECRET, and S3_* in .env
16
+
17
+ bun run db:generate
18
+ bun run db:migrate
19
+ bun run db:seed
20
+
21
+ bun x bosia dev
22
+ ```
23
+
24
+ Visit [http://localhost:9000](http://localhost:9000). The **first account you register becomes the admin** (gets `('*','*')` via the RBAC bootstrap seed).
25
+
26
+ ## What ships
27
+
28
+ | Feature | Path |
29
+ | ------------- | ---------------------------------------------------------------------- |
30
+ | `auth` | `src/features/auth/`, `(public)/login`, `(public)/register`, `/logout` |
31
+ | `rbac` | `src/features/rbac/`, `locals.can(r,a,scope?)` |
32
+ | `file-upload` | `src/features/file-upload/`, `POST /api/files` (S3 via `Bun.s3`) |
33
+ | `shop` | `src/features/shop/` (products / orders / cart services) |
34
+
35
+ ## Routes
36
+
37
+ - `/` — public landing
38
+ - `/login`, `/register`, `POST /logout`
39
+ - `/dashboard` — gated; redirects to `/login` if unauthenticated
40
+
41
+ ## Scripts
42
+
43
+ | Command | Description |
44
+ | --------------------- | --------------------------------------------- |
45
+ | `bun x bosia dev` | Dev server with HMR |
46
+ | `bun x bosia build` | Production build |
47
+ | `bun run db:generate` | Generate migration from schema changes |
48
+ | `bun run db:migrate` | Apply pending migrations |
49
+ | `bun run db:seed` | Run pending seed files (incl. RBAC bootstrap) |
50
+
51
+ ## S3 storage
52
+
53
+ Uses native `Bun.s3` (no `@aws-sdk/*` dependency). Set the standard env vars:
54
+
55
+ ```
56
+ STORAGE_DRIVER=s3
57
+ S3_BUCKET=...
58
+ S3_REGION=...
59
+ S3_ACCESS_KEY_ID=...
60
+ S3_SECRET_ACCESS_KEY=...
61
+ S3_ENDPOINT= # optional, for R2/MinIO
62
+ ```
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ .bosia/
4
+ .DS_Store
5
+ *.log
6
+
7
+ # Generated Tailwind output
8
+ public/bosia-tw.css
9
+
10
+ # Local env overrides — never commit secrets
11
+ .env*.local
12
+ .env
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });
@@ -0,0 +1,8 @@
1
+ Update .env with your DATABASE_URL (PostgreSQL) and S3_* credentials.
2
+ Pick a strong SESSION_SECRET.
3
+
4
+ bun run db:generate
5
+ bun run db:migrate
6
+ bun run db:seed
7
+
8
+ The first account you register becomes the admin.
@@ -0,0 +1,27 @@
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
+ "check": "tsc --noEmit && prettier --check .",
10
+ "format": "prettier --write .",
11
+ "format:check": "prettier --check ."
12
+ },
13
+ "dependencies": {
14
+ "bosia": "^{{BOSIA_VERSION}}",
15
+ "svelte": "^5.20.0",
16
+ "tailwind-merge": "^3.5.0",
17
+ "drizzle-orm": "^0.44.0",
18
+ "postgres": "^3.4.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "latest",
22
+ "prettier": "^3.3.0",
23
+ "prettier-plugin-svelte": "^3.2.0",
24
+ "typescript": "^5",
25
+ "drizzle-kit": "^0.31.0"
26
+ }
27
+ }
@@ -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>
@@ -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="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -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="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>