bosia 0.7.2 → 0.7.5
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 +2 -1
- package/src/cli/create.ts +108 -12
- package/src/core/client/App.svelte +14 -2
- package/src/core/client/prefetch.ts +43 -1
- package/src/core/renderer.ts +15 -9
- package/src/core/server.ts +25 -1
- package/templates/shop/template.json +1 -0
- package/templates/store/.env.example +12 -0
- package/templates/store/.prettierignore +7 -0
- package/templates/store/.prettierrc.json +9 -0
- package/templates/store/README.md +62 -0
- package/templates/store/_gitignore +15 -0
- package/templates/store/bosia.config.ts +10 -0
- package/templates/store/instructions.txt +8 -0
- package/templates/store/package.json +26 -0
- package/templates/store/public/favicon.svg +14 -0
- package/templates/store/public/logo-dark.svg +14 -0
- package/templates/store/public/logo-light.svg +14 -0
- package/templates/store/src/app.css +140 -0
- package/templates/store/src/app.d.ts +14 -0
- package/templates/store/src/app.html +11 -0
- package/templates/store/src/hooks.server.ts +21 -0
- package/templates/store/src/lib/utils.ts +1 -0
- package/templates/store/src/routes/(private)/+layout.server.ts +10 -0
- package/templates/store/src/routes/(private)/+layout.svelte +44 -0
- package/templates/store/src/routes/(private)/dashboard/+page.svelte +11 -0
- package/templates/store/src/routes/(public)/+layout.svelte +13 -0
- package/templates/store/src/routes/(public)/+page.svelte +38 -0
- package/templates/store/src/routes/+error.svelte +19 -0
- package/templates/store/src/routes/+layout.server.ts +9 -0
- package/templates/store/src/routes/+layout.svelte +6 -0
- package/templates/store/template.json +11 -0
- package/templates/store/tsconfig.json +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"check": "tsc --noEmit && prettier --check .",
|
|
45
45
|
"check:templates": "bun scripts/check-templates.ts",
|
|
46
|
+
"build:templates": "bun scripts/build-prebuilt-templates.ts",
|
|
46
47
|
"test": "bun test",
|
|
47
48
|
"test:watch": "bun test --watch"
|
|
48
49
|
},
|
package/src/cli/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve, join, basename, relative } from "path";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
3
4
|
import { spawn } from "bun";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import { installFeature, initFeatRegistry, resolveLocalRegistry } from "./feat.ts";
|
|
@@ -15,6 +16,7 @@ const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
|
|
|
15
16
|
default: "Minimal starter with routing and Tailwind",
|
|
16
17
|
demo: "Full-featured demo with hooks, API routes, form actions, and more",
|
|
17
18
|
shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
|
|
19
|
+
store: "Online store starter (Postgres + MinIO/S3) with auth, RBAC, products/orders/cart",
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
@@ -63,6 +65,26 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
63
65
|
|
|
64
66
|
console.log(`\n⬡ Creating Bosia project: ${basename(targetDir)} (template: ${template})\n`);
|
|
65
67
|
|
|
68
|
+
const templateConfigPath = join(templateDir, "template.json");
|
|
69
|
+
const config = existsSync(templateConfigPath)
|
|
70
|
+
? JSON.parse(readFileSync(templateConfigPath, "utf-8"))
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
// Fast path: heavy templates marked `"prebuilt": true` ship a baked,
|
|
74
|
+
// version-locked artifact on GitHub Releases. One download + extract beats
|
|
75
|
+
// 150+ serial registry fetches. Skipped for `--local` (dev flow) and falls
|
|
76
|
+
// back to the registry path if the artifact is missing (offline / no asset yet).
|
|
77
|
+
// `BOSIA_BUILDING_PREBUILT` is set by the artifact generator so it scaffolds
|
|
78
|
+
// via the registry instead of trying to download the asset it's producing.
|
|
79
|
+
if (config?.prebuilt === true && !isLocal && !process.env.BOSIA_BUILDING_PREBUILT) {
|
|
80
|
+
const ok = await scaffoldFromPrebuilt(template, targetDir, name);
|
|
81
|
+
if (ok) {
|
|
82
|
+
await finishCreate(targetDir, name, templateDir, skipInstall);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log("⚠️ Prebuilt artifact unavailable — installing from registry instead.\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
copyDir(templateDir, targetDir, name, isLocal);
|
|
67
89
|
|
|
68
90
|
if (existsSync(join(targetDir, ".env.example"))) {
|
|
@@ -70,9 +92,7 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
// Install template features from registry
|
|
73
|
-
|
|
74
|
-
if (existsSync(templateConfigPath)) {
|
|
75
|
-
const config = JSON.parse(readFileSync(templateConfigPath, "utf-8"));
|
|
95
|
+
if (config) {
|
|
76
96
|
if (config.features?.length) {
|
|
77
97
|
let localRegistry: string | null = null;
|
|
78
98
|
try {
|
|
@@ -96,15 +116,29 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
|
|
119
|
+
await finishCreate(targetDir, name, templateDir, skipInstall);
|
|
120
|
+
}
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
// ─── Shared finish: optional `bun install` + printed instructions ──────────
|
|
123
|
+
async function finishCreate(
|
|
124
|
+
targetDir: string,
|
|
125
|
+
name: string,
|
|
126
|
+
templateDir: string,
|
|
127
|
+
skipInstall: boolean,
|
|
128
|
+
) {
|
|
129
|
+
const printInstructions = () => {
|
|
103
130
|
const instPath = join(templateDir, "instructions.txt");
|
|
104
131
|
if (existsSync(instPath)) {
|
|
105
132
|
const instructions = readFileSync(instPath, "utf-8").trimEnd();
|
|
106
133
|
if (instructions) console.log(instructions);
|
|
107
134
|
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
console.log(`\n✅ Project created at ${targetDir}\n`);
|
|
138
|
+
|
|
139
|
+
if (skipInstall) {
|
|
140
|
+
console.log(`Skipped \`bun install\` (--no-install).\n\ncd ${name} && bun install\n`);
|
|
141
|
+
printInstructions();
|
|
108
142
|
return;
|
|
109
143
|
}
|
|
110
144
|
|
|
@@ -119,14 +153,76 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
119
153
|
console.warn("⚠️ bun install failed — run it manually.");
|
|
120
154
|
} else {
|
|
121
155
|
console.log(`\n🎉 Ready!\n\ncd ${name}`);
|
|
156
|
+
printInstructions();
|
|
157
|
+
console.log(`bun x bosia dev\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
122
160
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
// ─── Prebuilt artifact fast path ──────────────────────────────────────────
|
|
162
|
+
// Downloads the version-locked `<template>.tar.gz` GitHub Release asset,
|
|
163
|
+
// extracts it into targetDir, then substitutes the `{{PROJECT_NAME}}`
|
|
164
|
+
// placeholder baked into the artifact. Returns false (caller falls back to the
|
|
165
|
+
// registry path) on any failure: 404, offline, or a corrupt archive.
|
|
166
|
+
async function scaffoldFromPrebuilt(
|
|
167
|
+
template: string,
|
|
168
|
+
targetDir: string,
|
|
169
|
+
name: string,
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
const url = `https://github.com/bosapi/bosia/releases/download/v${BOSIA_VERSION}/${template}.tar.gz`;
|
|
172
|
+
const tmpTar = join(tmpdir(), `bosia-${template}-${Date.now()}.tar.gz`);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
console.log(`⬇️ Downloading prebuilt template…`);
|
|
176
|
+
const res = await fetch(url);
|
|
177
|
+
if (!res.ok) return false;
|
|
178
|
+
writeFileSync(tmpTar, Buffer.from(await res.arrayBuffer()));
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(targetDir, { recursive: true });
|
|
185
|
+
const tar = spawn(["tar", "-xzf", tmpTar, "-C", targetDir], {
|
|
186
|
+
stdout: "inherit",
|
|
187
|
+
stderr: "inherit",
|
|
188
|
+
});
|
|
189
|
+
if ((await tar.exited) !== 0) return false;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
} finally {
|
|
193
|
+
try {
|
|
194
|
+
unlinkSync(tmpTar);
|
|
195
|
+
} catch {
|
|
196
|
+
// best-effort cleanup
|
|
127
197
|
}
|
|
198
|
+
}
|
|
128
199
|
|
|
129
|
-
|
|
200
|
+
substitutePlaceholder(targetDir, name);
|
|
201
|
+
|
|
202
|
+
// Artifact bakes `.env` already, but restore from `.env.example` if missing.
|
|
203
|
+
const envPath = join(targetDir, ".env");
|
|
204
|
+
const envExample = join(targetDir, ".env.example");
|
|
205
|
+
if (!existsSync(envPath) && existsSync(envExample)) {
|
|
206
|
+
writeFileSync(envPath, readFileSync(envExample, "utf-8"));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Replace the `{{PROJECT_NAME}}` placeholder (baked at generate-time) across
|
|
213
|
+
// every extracted file. Mirrors copyDir's utf-8 assumption — templates carry
|
|
214
|
+
// no binary files.
|
|
215
|
+
function substitutePlaceholder(dir: string, name: string) {
|
|
216
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
217
|
+
const full = join(dir, entry.name);
|
|
218
|
+
if (entry.isDirectory()) {
|
|
219
|
+
substitutePlaceholder(full, name);
|
|
220
|
+
} else if (entry.isFile()) {
|
|
221
|
+
const content = readFileSync(full, "utf-8");
|
|
222
|
+
if (content.includes("{{PROJECT_NAME}}")) {
|
|
223
|
+
writeFileSync(full, content.replaceAll("{{PROJECT_NAME}}", name), "utf-8");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
130
226
|
}
|
|
131
227
|
}
|
|
132
228
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { router, scrollToHash } from "./router.svelte.ts";
|
|
4
4
|
import { findMatch } from "../matcher.ts";
|
|
5
5
|
import { clientRoutes } from "bosia:routes";
|
|
6
|
-
import { consumePrefetch, prefetchCache, dataUrl } from "./prefetch.ts";
|
|
6
|
+
import { consumePrefetch, prefetchCache, dataUrl, buildParentSnapshots } from "./prefetch.ts";
|
|
7
7
|
import { appState, clearDirty } from "./appState.svelte.ts";
|
|
8
8
|
import { captureSnapshot, liveContext, shouldRerun, type CacheEntry } from "./loaderCache.ts";
|
|
9
9
|
import { pickErrorPage } from "../errorMatch.ts";
|
|
@@ -123,10 +123,22 @@
|
|
|
123
123
|
// to avoid a flash of stale/empty data before the fetch completes.
|
|
124
124
|
const cached = match.route.hasServerData ? consumePrefetch(path) : null;
|
|
125
125
|
prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
|
|
126
|
+
// Forward cached parent data for skipped layers so downstream loaders see
|
|
127
|
+
// real parent() data, not {}. POST only when there's something to carry —
|
|
128
|
+
// keeps the no-skip case a cacheable/dedupable GET.
|
|
129
|
+
const snapshots = buildParentSnapshots(path, maskBits);
|
|
130
|
+
const dataInit: RequestInit =
|
|
131
|
+
Object.keys(snapshots).length > 0
|
|
132
|
+
? {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ parentSnapshots: snapshots }),
|
|
136
|
+
}
|
|
137
|
+
: {};
|
|
126
138
|
const dataFetch = cached
|
|
127
139
|
? Promise.resolve(cached)
|
|
128
140
|
: match.route.hasServerData
|
|
129
|
-
? fetch(dataUrl(path, maskBits))
|
|
141
|
+
? fetch(dataUrl(path, maskBits), dataInit)
|
|
130
142
|
.then((r) => r.json())
|
|
131
143
|
.catch(() => null)
|
|
132
144
|
: Promise.resolve(null);
|
|
@@ -38,6 +38,36 @@ export function buildMaskBits(path: string): string | null {
|
|
|
38
38
|
return (pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Build `parentSnapshots` (layout depth → cached data) for a target path from
|
|
43
|
+
* the current loader cache, given the mask bits from `buildMaskBits`. For each
|
|
44
|
+
* layout depth whose mask bit is '0' (skipped) and whose cached entry exists,
|
|
45
|
+
* forward that layer's data so server-side downstream loaders see real
|
|
46
|
+
* `parent()` data instead of `{}`. Returns `{}` when nothing to carry.
|
|
47
|
+
*
|
|
48
|
+
* Client-supplied perf hint only — the server never trusts it for authz.
|
|
49
|
+
*/
|
|
50
|
+
export function buildParentSnapshots(
|
|
51
|
+
path: string,
|
|
52
|
+
maskBits: string,
|
|
53
|
+
): Record<number, Record<string, any>> {
|
|
54
|
+
const snapshots: Record<number, Record<string, any>> = {};
|
|
55
|
+
const url = new URL(path, window.location.origin);
|
|
56
|
+
const match = findMatch(clientRoutes, url.pathname);
|
|
57
|
+
if (!match) return snapshots;
|
|
58
|
+
const layoutIds = (match.route as any).layoutIds as (string | null)[];
|
|
59
|
+
|
|
60
|
+
layoutIds.forEach((id, depth) => {
|
|
61
|
+
// maskBits char 0 = page, char depth+1 = layout depth. '0' = skipped.
|
|
62
|
+
if (maskBits[depth + 1] !== "0") return;
|
|
63
|
+
if (id === null) return;
|
|
64
|
+
const entry = appState.loaderCache.layouts[id];
|
|
65
|
+
if (entry) snapshots[depth] = entry.data;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return snapshots;
|
|
69
|
+
}
|
|
70
|
+
|
|
41
71
|
/** Builds the `/__bosia/data/…` URL for a given client path. */
|
|
42
72
|
export function dataUrl(path: string, invalidatedBits?: string): string {
|
|
43
73
|
const url = new URL(path, window.location.origin);
|
|
@@ -78,7 +108,19 @@ export async function prefetchPath(path: string): Promise<void> {
|
|
|
78
108
|
// loaders whose tracked inputs haven't changed. Falls back to running
|
|
79
109
|
// everything when the route can't be matched (e.g. external/unknown URL).
|
|
80
110
|
const maskBits = buildMaskBits(path) ?? undefined;
|
|
81
|
-
|
|
111
|
+
// Forward cached parent data for skipped layers so a prefetched response
|
|
112
|
+
// is computed with real parent() data, not {}. POST only when there's
|
|
113
|
+
// something to carry — keeps the no-skip case a cacheable/dedupable GET.
|
|
114
|
+
const snapshots = maskBits ? buildParentSnapshots(path, maskBits) : {};
|
|
115
|
+
const init: RequestInit =
|
|
116
|
+
Object.keys(snapshots).length > 0
|
|
117
|
+
? {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
body: JSON.stringify({ parentSnapshots: snapshots }),
|
|
121
|
+
}
|
|
122
|
+
: {};
|
|
123
|
+
const res = await fetch(dataUrl(path, maskBits), init);
|
|
82
124
|
if (res.ok) {
|
|
83
125
|
if (prefetchCache.size >= MAX_PREFETCH_ENTRIES) {
|
|
84
126
|
const oldest = prefetchCache.keys().next().value;
|
package/src/core/renderer.ts
CHANGED
|
@@ -296,10 +296,15 @@ function makeDepends(deps: LoaderDeps): (...keys: string[]) => void {
|
|
|
296
296
|
// - layouts[i] === true → run that layout; false → skip, emit null
|
|
297
297
|
// - page === true → run page; false → skip, emit null
|
|
298
298
|
// When skipped, the parent() chain still receives the *combined parent
|
|
299
|
-
// data* contributed by previously-cached layers
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
299
|
+
// data* contributed by previously-cached layers. The client already holds
|
|
300
|
+
// each skipped layer's data in its loader cache and forwards it as
|
|
301
|
+
// `parentSnapshots` (depth → data) in the request body, so downstream
|
|
302
|
+
// loaders that DO re-run see real parent() data, not `{}`. The response
|
|
303
|
+
// slot stays `null` (client renders that layer from its cache).
|
|
304
|
+
//
|
|
305
|
+
// Trust boundary: parentSnapshots are a client-supplied perf hint, never
|
|
306
|
+
// authoritative. Anything authz-related must read `event.locals` (populated
|
|
307
|
+
// in hooks.server.ts), never `parent()`.
|
|
303
308
|
|
|
304
309
|
export type LoaderMask = {
|
|
305
310
|
page: boolean;
|
|
@@ -314,6 +319,7 @@ export async function loadRouteData(
|
|
|
314
319
|
metadataData: Record<string, any> | null = null,
|
|
315
320
|
match?: RouteMatch<(typeof serverRoutes)[number]> | null,
|
|
316
321
|
mask?: LoaderMask,
|
|
322
|
+
parentSnapshots?: Record<number, Record<string, any>>,
|
|
317
323
|
) {
|
|
318
324
|
match ??= findMatch(serverRoutes, url.pathname);
|
|
319
325
|
if (!match) return null;
|
|
@@ -332,11 +338,11 @@ export async function loadRouteData(
|
|
|
332
338
|
if (skip) {
|
|
333
339
|
layoutData[ls.depth] = null;
|
|
334
340
|
layoutDeps[ls.depth] = null;
|
|
335
|
-
// Skipped layers contribute
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
341
|
+
// Skipped layers contribute their client-cached data (forwarded as
|
|
342
|
+
// parentSnapshots) to the parent chain, so downstream loaders that DO
|
|
343
|
+
// re-run see real parent() data. Falls back to {} when no snapshot was
|
|
344
|
+
// sent. Perf hint only — never authoritative for authz (use locals).
|
|
345
|
+
parentData = { ...parentData, ...(parentSnapshots?.[ls.depth] ?? {}) };
|
|
340
346
|
continue;
|
|
341
347
|
}
|
|
342
348
|
const mod = await ls.loader();
|
package/src/core/server.ts
CHANGED
|
@@ -237,8 +237,32 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
237
237
|
pageMatch?.route ? ((pageMatch.route as any).layoutModules?.length ?? 0) : 0,
|
|
238
238
|
)
|
|
239
239
|
: undefined;
|
|
240
|
+
// Client forwards each skipped layout layer's cached data as
|
|
241
|
+
// parentSnapshots (depth → data) so downstream loaders see real
|
|
242
|
+
// parent() data instead of {}. Perf hint only — never authoritative;
|
|
243
|
+
// authz must read locals. Guarded: undefined for GET / malformed body.
|
|
244
|
+
let parentSnapshots: Record<number, Record<string, any>> | undefined;
|
|
245
|
+
if (method !== "GET") {
|
|
246
|
+
try {
|
|
247
|
+
const body = await request.json();
|
|
248
|
+
if (body && typeof body === "object" && body.parentSnapshots) {
|
|
249
|
+
parentSnapshots = body.parentSnapshots as Record<number, Record<string, any>>;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
parentSnapshots = undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
240
255
|
const runLoad = async () => {
|
|
241
|
-
const data = await loadRouteData(
|
|
256
|
+
const data = await loadRouteData(
|
|
257
|
+
routeUrl,
|
|
258
|
+
locals,
|
|
259
|
+
request,
|
|
260
|
+
cookies,
|
|
261
|
+
null,
|
|
262
|
+
pageMatch,
|
|
263
|
+
mask,
|
|
264
|
+
parentSnapshots,
|
|
265
|
+
);
|
|
242
266
|
|
|
243
267
|
let metadata = null;
|
|
244
268
|
if (pageMatch) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/store
|
|
2
|
+
|
|
3
|
+
SESSION_SECRET=change-me-in-production
|
|
4
|
+
|
|
5
|
+
STORAGE_DRIVER=s3
|
|
6
|
+
S3_BUCKET=uploads
|
|
7
|
+
S3_REGION=auto
|
|
8
|
+
S3_ACCESS_KEY_ID=minioadmin
|
|
9
|
+
S3_SECRET_ACCESS_KEY=minioadmin
|
|
10
|
+
S3_ENDPOINT=http://localhost:9000
|
|
11
|
+
|
|
12
|
+
PUBLIC_BASE_URL=
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
An online-store starter built with [Bosia](https://github.com/bosapi/bosia) — Postgres database, MinIO (S3) uploads, auth, RBAC, 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). Defaults target a local MinIO; point `S3_ENDPOINT` at any S3-compatible store (AWS S3, Cloudflare R2, ...):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
STORAGE_DRIVER=s3
|
|
57
|
+
S3_BUCKET=uploads
|
|
58
|
+
S3_REGION=auto
|
|
59
|
+
S3_ACCESS_KEY_ID=minioadmin
|
|
60
|
+
S3_SECRET_ACCESS_KEY=minioadmin
|
|
61
|
+
S3_ENDPOINT=http://localhost:9000 # MinIO; omit for AWS S3
|
|
62
|
+
```
|
|
@@ -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 (Postgres, e.g. postgres://user:pass@localhost:5432/store) and S3_* credentials (MinIO or any S3-compatible store).
|
|
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,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
|
+
"check": "tsc --noEmit && prettier --check .",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"format:check": "prettier --check ."
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"bosia": "^{{BOSIA_VERSION}}",
|
|
15
|
+
"svelte": "^5.56.3",
|
|
16
|
+
"tailwind-merge": "^3.5.0",
|
|
17
|
+
"drizzle-orm": "^0.44.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "latest",
|
|
21
|
+
"prettier": "^3.3.0",
|
|
22
|
+
"prettier-plugin-svelte": "^3.2.0",
|
|
23
|
+
"typescript": "^5",
|
|
24
|
+
"drizzle-kit": "^0.31.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@source "../src";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* ─── shadcn-inspired Design Tokens ──────────────────────
|
|
8
|
+
* CSS custom properties for light & dark themes.
|
|
9
|
+
* Uses HSL values so Tailwind can apply opacity modifiers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@theme {
|
|
13
|
+
--color-background: hsl(var(--background));
|
|
14
|
+
--color-foreground: hsl(var(--foreground));
|
|
15
|
+
|
|
16
|
+
--color-card: hsl(var(--card));
|
|
17
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
18
|
+
|
|
19
|
+
--color-popover: hsl(var(--popover));
|
|
20
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
21
|
+
|
|
22
|
+
--color-primary: hsl(var(--primary));
|
|
23
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
24
|
+
|
|
25
|
+
--color-secondary: hsl(var(--secondary));
|
|
26
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
27
|
+
|
|
28
|
+
--color-muted: hsl(var(--muted));
|
|
29
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
30
|
+
|
|
31
|
+
--color-accent: hsl(var(--accent));
|
|
32
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
33
|
+
|
|
34
|
+
--color-destructive: hsl(var(--destructive));
|
|
35
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
36
|
+
|
|
37
|
+
--color-border: hsl(var(--border));
|
|
38
|
+
--color-input: hsl(var(--input));
|
|
39
|
+
--color-ring: hsl(var(--ring));
|
|
40
|
+
|
|
41
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
42
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
43
|
+
--radius-lg: var(--radius);
|
|
44
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
|
|
48
|
+
between these markers (the installed theme owns these tokens). Do NOT add your
|
|
49
|
+
own :root rules inside the markers — put custom overrides after the close tag. */
|
|
50
|
+
|
|
51
|
+
/* ─── Light Theme (Default) ─────────────────────────────── */
|
|
52
|
+
|
|
53
|
+
:root {
|
|
54
|
+
--background: 0 0% 100%;
|
|
55
|
+
--foreground: 222.2 84% 4.9%;
|
|
56
|
+
|
|
57
|
+
--card: 0 0% 100%;
|
|
58
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
59
|
+
|
|
60
|
+
--popover: 0 0% 100%;
|
|
61
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
62
|
+
|
|
63
|
+
--primary: 222.2 47.4% 11.2%;
|
|
64
|
+
--primary-foreground: 210 40% 98%;
|
|
65
|
+
|
|
66
|
+
--secondary: 210 40% 96.1%;
|
|
67
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
68
|
+
|
|
69
|
+
--muted: 210 40% 96.1%;
|
|
70
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
71
|
+
|
|
72
|
+
--accent: 210 40% 96.1%;
|
|
73
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
74
|
+
|
|
75
|
+
--destructive: 0 84.2% 60.2%;
|
|
76
|
+
--destructive-foreground: 210 40% 98%;
|
|
77
|
+
|
|
78
|
+
--border: 214.3 31.8% 91.4%;
|
|
79
|
+
--input: 214.3 31.8% 91.4%;
|
|
80
|
+
--ring: 222.2 84% 4.9%;
|
|
81
|
+
|
|
82
|
+
--radius: 0.5rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ─── Dark Theme ─────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
.dark {
|
|
88
|
+
--background: 222.2 84% 4.9%;
|
|
89
|
+
--foreground: 210 40% 98%;
|
|
90
|
+
|
|
91
|
+
--card: 222.2 84% 4.9%;
|
|
92
|
+
--card-foreground: 210 40% 98%;
|
|
93
|
+
|
|
94
|
+
--popover: 222.2 84% 4.9%;
|
|
95
|
+
--popover-foreground: 210 40% 98%;
|
|
96
|
+
|
|
97
|
+
--primary: 210 40% 98%;
|
|
98
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
99
|
+
|
|
100
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
101
|
+
--secondary-foreground: 210 40% 98%;
|
|
102
|
+
|
|
103
|
+
--muted: 217.2 32.6% 17.5%;
|
|
104
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
105
|
+
|
|
106
|
+
--accent: 217.2 32.6% 17.5%;
|
|
107
|
+
--accent-foreground: 210 40% 98%;
|
|
108
|
+
|
|
109
|
+
--destructive: 0 62.8% 30.6%;
|
|
110
|
+
--destructive-foreground: 210 40% 98%;
|
|
111
|
+
|
|
112
|
+
--border: 217.2 32.6% 17.5%;
|
|
113
|
+
--input: 217.2 32.6% 17.5%;
|
|
114
|
+
--ring: 212.7 26.8% 83.9%;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* /bosia-theme-vars */
|
|
118
|
+
|
|
119
|
+
/* ─── Base Styles ────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
@layer base {
|
|
122
|
+
* {
|
|
123
|
+
border-color: theme(--color-border);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
body {
|
|
127
|
+
background-color: theme(--color-background);
|
|
128
|
+
color: theme(--color-foreground);
|
|
129
|
+
font-family:
|
|
130
|
+
"Inter",
|
|
131
|
+
system-ui,
|
|
132
|
+
-apple-system,
|
|
133
|
+
BlinkMacSystemFont,
|
|
134
|
+
"Segoe UI",
|
|
135
|
+
Roboto,
|
|
136
|
+
"Helvetica Neue",
|
|
137
|
+
Arial,
|
|
138
|
+
sans-serif;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
|
|
3
|
+
declare module "*.svelte" {
|
|
4
|
+
import type { Component } from "svelte";
|
|
5
|
+
const component: Component<Record<string, any>, Record<string, any>, any>;
|
|
6
|
+
export default component;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare namespace App {
|
|
10
|
+
interface Locals {
|
|
11
|
+
db: import("./features/drizzle").Database;
|
|
12
|
+
requestTime: number;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { sequence } from "bosia";
|
|
2
|
+
import type { Handle } from "bosia";
|
|
3
|
+
import { db } from "./features/drizzle";
|
|
4
|
+
import { authHandle } from "./features/auth";
|
|
5
|
+
|
|
6
|
+
const dbHandle: Handle = async ({ event, resolve }) => {
|
|
7
|
+
event.locals.db = db;
|
|
8
|
+
return resolve(event);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
event.locals.requestTime = start;
|
|
14
|
+
const res = await resolve(event);
|
|
15
|
+
const ms = Date.now() - start;
|
|
16
|
+
console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
|
|
17
|
+
res.headers.set("X-Response-Time", `${ms}ms`);
|
|
18
|
+
return res;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const handle = sequence(dbHandle, authHandle, loggingHandle);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from "bosia";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { redirect } from "bosia";
|
|
2
|
+
import type { LoadEvent } from "bosia";
|
|
3
|
+
|
|
4
|
+
export async function load({ locals, url }: LoadEvent) {
|
|
5
|
+
if (!locals.user) {
|
|
6
|
+
const next = encodeURIComponent(url.pathname + url.search);
|
|
7
|
+
throw redirect(303, `/login?next=${next}`);
|
|
8
|
+
}
|
|
9
|
+
return { user: locals.user };
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "bosia/client";
|
|
3
|
+
import AdminSidebar from "$lib/components/AdminSidebar.svelte";
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbList,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbLink,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
BreadcrumbSeparator,
|
|
11
|
+
} from "$lib/components/ui/breadcrumb";
|
|
12
|
+
|
|
13
|
+
let { data, children }: { data: { user: { id: string; email: string } }; children: any } =
|
|
14
|
+
$props();
|
|
15
|
+
|
|
16
|
+
const segments = $derived(page.url.pathname.split("/").filter(Boolean));
|
|
17
|
+
const label = (s: string) => s[0].toUpperCase() + s.slice(1);
|
|
18
|
+
const hrefAt = (i: number) => "/" + segments.slice(0, i + 1).join("/");
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="flex min-h-screen">
|
|
22
|
+
<AdminSidebar currentPath={page.url.pathname} user={data.user} />
|
|
23
|
+
<main class="flex-1 overflow-x-hidden p-6">
|
|
24
|
+
{#if segments.length > 0}
|
|
25
|
+
<Breadcrumb class="mb-4">
|
|
26
|
+
<BreadcrumbList>
|
|
27
|
+
{#each segments as segment, i}
|
|
28
|
+
<BreadcrumbItem>
|
|
29
|
+
{#if i === segments.length - 1}
|
|
30
|
+
<BreadcrumbPage>{label(segment)}</BreadcrumbPage>
|
|
31
|
+
{:else}
|
|
32
|
+
<BreadcrumbLink href={hrefAt(i)}>{label(segment)}</BreadcrumbLink>
|
|
33
|
+
{/if}
|
|
34
|
+
</BreadcrumbItem>
|
|
35
|
+
{#if i < segments.length - 1}
|
|
36
|
+
<BreadcrumbSeparator />
|
|
37
|
+
{/if}
|
|
38
|
+
{/each}
|
|
39
|
+
</BreadcrumbList>
|
|
40
|
+
</Breadcrumb>
|
|
41
|
+
{/if}
|
|
42
|
+
{@render children()}
|
|
43
|
+
</main>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- EDIT THIS FILE: add cards, KPIs, recent orders, sales charts, etc. -->
|
|
2
|
+
<svelte:head>
|
|
3
|
+
<title>Dashboard</title>
|
|
4
|
+
</svelte:head>
|
|
5
|
+
|
|
6
|
+
<div class="flex flex-col gap-4">
|
|
7
|
+
<h1 class="text-2xl font-bold tracking-tight">Dashboard</h1>
|
|
8
|
+
<p class="text-muted-foreground text-sm">
|
|
9
|
+
This is your admin home. Add widgets, KPI cards, and recent activity here.
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "bosia/client";
|
|
3
|
+
import PublicNavbar from "$lib/components/PublicNavbar.svelte";
|
|
4
|
+
|
|
5
|
+
let { data, children }: { data: { user: any }; children: any } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div class="flex min-h-screen flex-col">
|
|
9
|
+
<PublicNavbar currentPath={page.url.pathname} user={data.user} />
|
|
10
|
+
<div class="flex-1">
|
|
11
|
+
{@render children()}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<svelte:head>
|
|
2
|
+
<title>Welcome to your shop</title>
|
|
3
|
+
<meta
|
|
4
|
+
name="description"
|
|
5
|
+
content="A Bosia shop starter — auth, RBAC, S3 uploads, products & cart."
|
|
6
|
+
/>
|
|
7
|
+
</svelte:head>
|
|
8
|
+
|
|
9
|
+
<main class="flex min-h-[80vh] flex-col items-center justify-center gap-6 p-8">
|
|
10
|
+
<div class="flex flex-col items-center gap-3 text-center">
|
|
11
|
+
<img src="/favicon.svg" alt="" class="size-16" />
|
|
12
|
+
<h1 class="text-4xl font-bold tracking-tight">Welcome to your shop</h1>
|
|
13
|
+
<p class="text-muted-foreground text-lg">
|
|
14
|
+
A Bosia shop starter — auth, RBAC, S3 uploads, products & cart.
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="mt-4 flex gap-3">
|
|
19
|
+
<a
|
|
20
|
+
href="/products"
|
|
21
|
+
class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
|
|
22
|
+
>
|
|
23
|
+
Browse products
|
|
24
|
+
</a>
|
|
25
|
+
<a
|
|
26
|
+
href="/login"
|
|
27
|
+
class="border-border bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-md border px-4 py-2 text-sm font-medium transition-colors"
|
|
28
|
+
>
|
|
29
|
+
Sign in
|
|
30
|
+
</a>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<p class="text-muted-foreground mt-6 text-sm">
|
|
34
|
+
Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
|
|
35
|
+
>src/routes/(public)/+page.svelte</code
|
|
36
|
+
> to get started
|
|
37
|
+
</p>
|
|
38
|
+
</main>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ErrorProps } from "./$types";
|
|
3
|
+
let { error }: ErrorProps = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<svelte:head>
|
|
7
|
+
<title>{error.status} — {error.message}</title>
|
|
8
|
+
</svelte:head>
|
|
9
|
+
|
|
10
|
+
<div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
|
|
11
|
+
<p class="text-8xl font-bold text-gray-200">{error.status}</p>
|
|
12
|
+
<p class="text-2xl font-semibold text-gray-700">{error.message}</p>
|
|
13
|
+
<a
|
|
14
|
+
href="/"
|
|
15
|
+
class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors"
|
|
16
|
+
>
|
|
17
|
+
Go home
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"prebuilt": true,
|
|
3
|
+
"features": ["auth", "rbac", "file-upload", "shop"],
|
|
4
|
+
"featureOptions": {
|
|
5
|
+
"drizzle.dialect": "postgres",
|
|
6
|
+
"auth.dialect": "postgres",
|
|
7
|
+
"rbac.dialect": "postgres",
|
|
8
|
+
"file-upload.dialect": "postgres",
|
|
9
|
+
"shop.dialect": "postgres"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"types": ["bun-types"],
|
|
13
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
14
|
+
"rootDirs": [".", ".bosia/types"],
|
|
15
|
+
"paths": {
|
|
16
|
+
"$lib": ["./src/lib"],
|
|
17
|
+
"$lib/*": ["./src/lib/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*", ".bosia/types/**/*.d.ts"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|