bosbun 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/package.json +5 -5
- package/src/cli/add.ts +4 -4
- package/src/cli/create.ts +4 -4
- package/src/cli/feat.ts +4 -4
- package/src/cli/index.ts +16 -16
- package/src/cli/start.ts +2 -2
- package/src/core/build.ts +17 -17
- package/src/core/client/App.svelte +19 -14
- package/src/core/client/hydrate.ts +9 -7
- package/src/core/client/prefetch.ts +109 -0
- package/src/core/client/router.svelte.ts +1 -1
- package/src/core/dev.ts +7 -7
- package/src/core/env.ts +1 -1
- package/src/core/envCodegen.ts +20 -20
- package/src/core/hooks.ts +2 -2
- package/src/core/html.ts +19 -18
- package/src/core/plugin.ts +13 -13
- package/src/core/prerender.ts +2 -2
- package/src/core/renderer.ts +3 -3
- package/src/core/routeFile.ts +6 -6
- package/src/core/routeTypes.ts +10 -10
- package/src/core/server.ts +4 -4
- package/src/core/types.ts +1 -1
- package/src/lib/index.ts +3 -3
- package/templates/default/.env.example +6 -6
- package/templates/default/README.md +3 -3
- package/templates/default/package.json +4 -4
- package/templates/default/src/routes/+page.svelte +3 -3
- package/templates/default/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bosbun
|
|
2
2
|
|
|
3
|
-
The `
|
|
3
|
+
The `bosbun` package — framework core + CLI.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add
|
|
8
|
+
bun add bosbun
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or scaffold a new project:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
bunx
|
|
14
|
+
bunx bosbun create my-app
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## CLI
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
|
|
20
|
+
bosbun <command>
|
|
21
21
|
|
|
22
22
|
Commands:
|
|
23
|
-
create <name> Scaffold a new
|
|
23
|
+
create <name> Scaffold a new Bosbun project
|
|
24
24
|
dev Start the development server (port 3000)
|
|
25
25
|
build Build for production
|
|
26
26
|
start Run the production server
|
|
@@ -64,7 +64,7 @@ src/routes/
|
|
|
64
64
|
|
|
65
65
|
```typescript
|
|
66
66
|
// src/routes/blog/[slug]/+page.server.ts
|
|
67
|
-
import type { LoadEvent } from "
|
|
67
|
+
import type { LoadEvent } from "bosbun";
|
|
68
68
|
|
|
69
69
|
export async function load({ params, url, locals, fetch, parent }: LoadEvent) {
|
|
70
70
|
const parentData = await parent(); // data from layout loaders above
|
|
@@ -89,7 +89,7 @@ Export named HTTP verb functions from `+server.ts`:
|
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
91
|
// src/routes/api/items/+server.ts
|
|
92
|
-
import type { RequestEvent } from "
|
|
92
|
+
import type { RequestEvent } from "bosbun";
|
|
93
93
|
|
|
94
94
|
export function GET({ params, url, locals }: RequestEvent) {
|
|
95
95
|
return Response.json({ items: [] });
|
|
@@ -106,8 +106,8 @@ export async function POST({ request }: RequestEvent) {
|
|
|
106
106
|
Create `src/hooks.server.ts` to intercept every request:
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
|
-
import { sequence } from "
|
|
110
|
-
import type { Handle } from "
|
|
109
|
+
import { sequence } from "bosbun";
|
|
110
|
+
import type { Handle } from "bosbun";
|
|
111
111
|
|
|
112
112
|
const authHandle: Handle = async ({ event, resolve }) => {
|
|
113
113
|
event.locals.user = await getUser(event.request);
|
|
@@ -128,8 +128,8 @@ export const handle = sequence(authHandle, loggingHandle);
|
|
|
128
128
|
## Public API
|
|
129
129
|
|
|
130
130
|
```typescript
|
|
131
|
-
import { cn, sequence } from "
|
|
132
|
-
import type { RequestEvent, LoadEvent, Handle } from "
|
|
131
|
+
import { cn, sequence } from "bosbun";
|
|
132
|
+
import type { RequestEvent, LoadEvent, Handle } from "bosbun";
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
| Export | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosbun",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A minimalist fullstack framework — SSR + Svelte 5 Runes + Bun + ElysiaJS",
|
|
6
6
|
"keywords": [
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"name": "Jekibus",
|
|
17
17
|
"url": "https://bosapi.com"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/bosapi/
|
|
19
|
+
"homepage": "https://github.com/bosapi/bosbun#readme",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/bosapi/
|
|
22
|
+
"url": "git+https://github.com/bosapi/bosbun.git"
|
|
23
23
|
},
|
|
24
24
|
"bugs": {
|
|
25
|
-
"url": "https://github.com/bosapi/
|
|
25
|
+
"url": "https://github.com/bosapi/bosbun/issues"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
".": "./src/lib/index.ts"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"
|
|
37
|
+
"bosbun": "src/cli/index.ts"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"check": "tsc --noEmit"
|
package/src/cli/add.ts
CHANGED
|
@@ -2,16 +2,16 @@ import { join, dirname } from "path";
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
|
|
5
|
-
// ───
|
|
5
|
+
// ─── bosbun add <component> ────────────────────────────────
|
|
6
6
|
// Fetches a component from the GitHub registry and copies it
|
|
7
7
|
// into the user's src/lib/components/ui/<name>/ directory.
|
|
8
8
|
|
|
9
|
-
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/
|
|
9
|
+
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosbun/main/registry";
|
|
10
10
|
|
|
11
11
|
interface ComponentMeta {
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
14
|
-
dependencies: string[]; // other
|
|
14
|
+
dependencies: string[]; // other bosbun components required
|
|
15
15
|
files: string[];
|
|
16
16
|
npmDeps: Record<string, string>;
|
|
17
17
|
}
|
|
@@ -21,7 +21,7 @@ const installed = new Set<string>();
|
|
|
21
21
|
|
|
22
22
|
export async function runAdd(name: string | undefined) {
|
|
23
23
|
if (!name) {
|
|
24
|
-
console.error("❌ Please provide a component name.\n Usage:
|
|
24
|
+
console.error("❌ Please provide a component name.\n Usage: bosbun add <component>");
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
await addComponent(name, true);
|
package/src/cli/create.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { resolve, join, basename } from "path";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
|
|
5
|
-
// ───
|
|
5
|
+
// ─── bosbun create <name> ──────────────────────────────────
|
|
6
6
|
|
|
7
7
|
const TEMPLATE_DIR = resolve(import.meta.dir, "../../templates/default");
|
|
8
8
|
|
|
9
9
|
export async function runCreate(name: string | undefined) {
|
|
10
10
|
if (!name) {
|
|
11
|
-
console.error("❌ Please provide a project name.\n Usage:
|
|
11
|
+
console.error("❌ Please provide a project name.\n Usage: bosbun create my-app");
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ export async function runCreate(name: string | undefined) {
|
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
console.log(`🐰 Creating
|
|
22
|
+
console.log(`🐰 Creating Bosbun project: ${basename(targetDir)}\n`);
|
|
23
23
|
|
|
24
24
|
copyDir(TEMPLATE_DIR, targetDir, name);
|
|
25
25
|
|
|
@@ -35,7 +35,7 @@ export async function runCreate(name: string | undefined) {
|
|
|
35
35
|
if (exitCode !== 0) {
|
|
36
36
|
console.warn("⚠️ bun install failed — run it manually.");
|
|
37
37
|
} else {
|
|
38
|
-
console.log(`\n🎉 Ready!\n\n cd ${name}\n bun x
|
|
38
|
+
console.log(`\n🎉 Ready!\n\n cd ${name}\n bun x bosbun dev\n`);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
package/src/cli/feat.ts
CHANGED
|
@@ -3,16 +3,16 @@ import { mkdirSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
import { addComponent } from "./add.ts";
|
|
5
5
|
|
|
6
|
-
// ───
|
|
6
|
+
// ─── bosbun feat <feature> ─────────────────────────────────
|
|
7
7
|
// Fetches a feature scaffold from the GitHub registry.
|
|
8
8
|
// Installs required components, copies route/lib files, installs npm deps.
|
|
9
9
|
|
|
10
|
-
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/
|
|
10
|
+
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosbun/main/registry";
|
|
11
11
|
|
|
12
12
|
interface FeatureMeta {
|
|
13
13
|
name: string;
|
|
14
14
|
description: string;
|
|
15
|
-
components: string[]; //
|
|
15
|
+
components: string[]; // bosbun components to install via `bosbun add`
|
|
16
16
|
files: string[]; // source filenames in the registry feature dir
|
|
17
17
|
targets: string[]; // destination paths relative to project root
|
|
18
18
|
npmDeps: Record<string, string>;
|
|
@@ -20,7 +20,7 @@ interface FeatureMeta {
|
|
|
20
20
|
|
|
21
21
|
export async function runFeat(name: string | undefined) {
|
|
22
22
|
if (!name) {
|
|
23
|
-
console.error("❌ Please provide a feature name.\n Usage:
|
|
23
|
+
console.error("❌ Please provide a feature name.\n Usage: bosbun feat <feature>");
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
|
package/src/cli/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// ───
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
2
|
+
// ─── Bosbun CLI ────────────────────────────────────────────
|
|
3
|
+
// bosbun create <name> scaffold a new project
|
|
4
|
+
// bosbun dev start the development server
|
|
5
|
+
// bosbun build build for production
|
|
6
|
+
// bosbun start run the production server
|
|
7
|
+
// bosbun add <name> add a UI component from the registry
|
|
8
|
+
// bosbun feat <name> add a feature scaffold from the registry
|
|
9
9
|
|
|
10
10
|
const [, , command, ...args] = process.argv;
|
|
11
11
|
|
|
@@ -43,13 +43,13 @@ async function main() {
|
|
|
43
43
|
}
|
|
44
44
|
default: {
|
|
45
45
|
console.log(`
|
|
46
|
-
🐰
|
|
46
|
+
🐰 Bosbun
|
|
47
47
|
|
|
48
48
|
Usage:
|
|
49
|
-
|
|
49
|
+
bosbun <command> [options]
|
|
50
50
|
|
|
51
51
|
Commands:
|
|
52
|
-
create <name> Scaffold a new
|
|
52
|
+
create <name> Scaffold a new Bosbun project
|
|
53
53
|
dev Start the development server
|
|
54
54
|
build Build for production
|
|
55
55
|
start Run the production server
|
|
@@ -57,12 +57,12 @@ Commands:
|
|
|
57
57
|
feat <feature> Add a feature scaffold from the registry
|
|
58
58
|
|
|
59
59
|
Examples:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
bosbun create my-app
|
|
61
|
+
bosbun dev
|
|
62
|
+
bosbun build
|
|
63
|
+
bosbun start
|
|
64
|
+
bosbun add button
|
|
65
|
+
bosbun feat login
|
|
66
66
|
`);
|
|
67
67
|
break;
|
|
68
68
|
}
|
package/src/cli/start.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "bun";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { loadEnv } from "../core/env.ts";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const BOSBUN_NODE_MODULES = join(import.meta.dir, "..", "..", "node_modules");
|
|
6
6
|
|
|
7
7
|
export async function runStart() {
|
|
8
8
|
loadEnv("production");
|
|
@@ -20,7 +20,7 @@ export async function runStart() {
|
|
|
20
20
|
env: {
|
|
21
21
|
...process.env,
|
|
22
22
|
NODE_ENV: "production",
|
|
23
|
-
NODE_PATH:
|
|
23
|
+
NODE_PATH: BOSBUN_NODE_MODULES,
|
|
24
24
|
},
|
|
25
25
|
});
|
|
26
26
|
|
package/src/core/build.ts
CHANGED
|
@@ -6,29 +6,29 @@ import { spawnSync } from "bun";
|
|
|
6
6
|
import { scanRoutes } from "./scanner.ts";
|
|
7
7
|
import { generateRoutesFile } from "./routeFile.ts";
|
|
8
8
|
import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { makeBosbunPlugin } from "./plugin.ts";
|
|
10
10
|
import { prerenderStaticRoutes } from "./prerender.ts";
|
|
11
11
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
12
12
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
13
13
|
|
|
14
|
-
// Resolved from this file's location inside the
|
|
14
|
+
// Resolved from this file's location inside the bosbun package
|
|
15
15
|
const CORE_DIR = import.meta.dir;
|
|
16
|
-
const
|
|
16
|
+
const BOSBUN_NODE_MODULES = join(CORE_DIR, "..", "..", "node_modules");
|
|
17
17
|
|
|
18
18
|
// ─── Entry Point ─────────────────────────────────────────
|
|
19
19
|
|
|
20
20
|
const isProduction = process.env.NODE_ENV === "production";
|
|
21
21
|
|
|
22
|
-
console.log("🏗️ Starting
|
|
22
|
+
console.log("🏗️ Starting Bosbun build...\n");
|
|
23
23
|
|
|
24
|
-
// 0. Load .env files (before cleaning .
|
|
24
|
+
// 0. Load .env files (before cleaning .bosbun so loadEnv can set process.env early)
|
|
25
25
|
const envMode = isProduction ? "production" : "development";
|
|
26
26
|
const envVars = loadEnv(envMode);
|
|
27
27
|
const classifiedEnv = classifyEnvVars(envVars);
|
|
28
28
|
|
|
29
29
|
// 0b. Clean all generated output first
|
|
30
30
|
try { rmSync("./dist", { recursive: true, force: true }); } catch { }
|
|
31
|
-
try { rmSync("./.
|
|
31
|
+
try { rmSync("./.bosbun", { recursive: true, force: true }); } catch { }
|
|
32
32
|
|
|
33
33
|
// 1. Scan routes
|
|
34
34
|
const manifest = scanRoutes();
|
|
@@ -43,37 +43,37 @@ if (manifest.apis.length > 0) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// 2. Generate .
|
|
46
|
+
// 2. Generate .bosbun/routes.ts (single file replaces all old code generators)
|
|
47
47
|
generateRoutesFile(manifest);
|
|
48
48
|
|
|
49
|
-
// 2b. Generate .
|
|
49
|
+
// 2b. Generate .bosbun/types/src/routes/**/$types.d.ts for IDE type inference
|
|
50
50
|
generateRouteTypes(manifest);
|
|
51
51
|
|
|
52
|
-
// 2c. Ensure tsconfig.json has rootDirs pointing at .
|
|
52
|
+
// 2c. Ensure tsconfig.json has rootDirs pointing at .bosbun/types
|
|
53
53
|
ensureRootDirs();
|
|
54
54
|
|
|
55
|
-
// 2d. Generate .
|
|
55
|
+
// 2d. Generate .bosbun/env.server.ts, .bosbun/env.client.ts, .bosbun/types/env.d.ts
|
|
56
56
|
generateEnvModules(classifiedEnv);
|
|
57
57
|
|
|
58
58
|
// 3. Build Tailwind CSS
|
|
59
59
|
console.log("\n🎨 Building Tailwind CSS...");
|
|
60
|
-
const tailwindBin = join(
|
|
60
|
+
const tailwindBin = join(BOSBUN_NODE_MODULES, ".bin", "tailwindcss");
|
|
61
61
|
const tailwindResult = spawnSync(
|
|
62
|
-
[tailwindBin, "-i", "./src/app.css", "-o", "./public/
|
|
62
|
+
[tailwindBin, "-i", "./src/app.css", "-o", "./public/bosbun-tw.css", ...(isProduction ? ["--minify"] : [])],
|
|
63
63
|
{
|
|
64
64
|
cwd: process.cwd(),
|
|
65
|
-
env: { ...process.env, NODE_PATH:
|
|
65
|
+
env: { ...process.env, NODE_PATH: BOSBUN_NODE_MODULES },
|
|
66
66
|
},
|
|
67
67
|
);
|
|
68
68
|
if (tailwindResult.exitCode !== 0) {
|
|
69
69
|
console.error("❌ Tailwind CSS build failed:\n" + tailwindResult.stderr.toString());
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
|
-
console.log("✅ Tailwind CSS built: public/
|
|
72
|
+
console.log("✅ Tailwind CSS built: public/bosbun-tw.css");
|
|
73
73
|
|
|
74
|
-
// Separate plugin instances per build target (
|
|
75
|
-
const clientPlugin =
|
|
76
|
-
const serverPlugin =
|
|
74
|
+
// Separate plugin instances per build target (bosbun:env resolves differently)
|
|
75
|
+
const clientPlugin = makeBosbunPlugin("browser");
|
|
76
|
+
const serverPlugin = makeBosbunPlugin("bun");
|
|
77
77
|
|
|
78
78
|
// Build-time defines: inline PUBLIC_STATIC_* and STATIC_* vars
|
|
79
79
|
const staticDefines: Record<string, string> = {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { router } from "./router.svelte.ts";
|
|
3
3
|
import { findMatch } from "../matcher.ts";
|
|
4
|
-
import { clientRoutes } from "
|
|
4
|
+
import { clientRoutes } from "bosbun:routes";
|
|
5
|
+
import { consumePrefetch, prefetchCache } from "./prefetch.ts";
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
ssrMode = false,
|
|
@@ -52,9 +53,13 @@
|
|
|
52
53
|
|
|
53
54
|
// Load components + data in parallel, then update state atomically
|
|
54
55
|
// to avoid a flash of stale/empty data before the fetch completes.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const cached = match.route.hasServerData ? consumePrefetch(path) : null;
|
|
57
|
+
prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
|
|
58
|
+
const dataFetch = cached
|
|
59
|
+
? Promise.resolve(cached)
|
|
60
|
+
: match.route.hasServerData
|
|
61
|
+
? fetch(`/__bosbun/data?path=${encodeURIComponent(path)}`).then(r => r.json()).catch(() => null)
|
|
62
|
+
: Promise.resolve(null);
|
|
58
63
|
|
|
59
64
|
Promise.all([
|
|
60
65
|
match.route.page(),
|
|
@@ -90,9 +95,9 @@
|
|
|
90
95
|
-->
|
|
91
96
|
|
|
92
97
|
{#if navigating}
|
|
93
|
-
<div class="
|
|
98
|
+
<div class="bosbun-bar loading"></div>
|
|
94
99
|
{:else if navDone}
|
|
95
|
-
<div class="
|
|
100
|
+
<div class="bosbun-bar done"></div>
|
|
96
101
|
{/if}
|
|
97
102
|
|
|
98
103
|
{#if layoutComponents.length > 0}
|
|
@@ -123,28 +128,28 @@
|
|
|
123
128
|
{/snippet}
|
|
124
129
|
|
|
125
130
|
<style>
|
|
126
|
-
.
|
|
131
|
+
.bosbun-bar {
|
|
127
132
|
position: fixed;
|
|
128
133
|
top: 0;
|
|
129
134
|
left: 0;
|
|
130
135
|
height: 2px;
|
|
131
136
|
width: 100%;
|
|
132
|
-
background: var(--
|
|
137
|
+
background: var(--bosbun-loading-color, #f73b27);
|
|
133
138
|
z-index: 9999;
|
|
134
139
|
pointer-events: none;
|
|
135
140
|
transform-origin: left center;
|
|
136
141
|
}
|
|
137
|
-
.
|
|
138
|
-
animation:
|
|
142
|
+
.bosbun-bar.loading {
|
|
143
|
+
animation: bosbun-load 8s cubic-bezier(0.02, 0.5, 0.5, 1) forwards;
|
|
139
144
|
}
|
|
140
|
-
.
|
|
141
|
-
animation:
|
|
145
|
+
.bosbun-bar.done {
|
|
146
|
+
animation: bosbun-done 0.35s ease forwards;
|
|
142
147
|
}
|
|
143
|
-
@keyframes
|
|
148
|
+
@keyframes bosbun-load {
|
|
144
149
|
from { transform: scaleX(0); }
|
|
145
150
|
to { transform: scaleX(0.85); }
|
|
146
151
|
}
|
|
147
|
-
@keyframes
|
|
152
|
+
@keyframes bosbun-done {
|
|
148
153
|
from { transform: scaleX(1); opacity: 1; }
|
|
149
154
|
to { transform: scaleX(1); opacity: 0; }
|
|
150
155
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hydrate } from "svelte";
|
|
2
2
|
import App from "./App.svelte";
|
|
3
3
|
import { router } from "./router.svelte.ts";
|
|
4
|
+
import { initPrefetch } from "./prefetch.ts";
|
|
4
5
|
import { findMatch } from "../matcher.ts";
|
|
5
|
-
import { clientRoutes } from "
|
|
6
|
+
import { clientRoutes } from "bosbun:routes";
|
|
6
7
|
|
|
7
8
|
// ─── Hydration ────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ async function main() {
|
|
|
11
12
|
|
|
12
13
|
router.init();
|
|
13
14
|
router.currentRoute = path;
|
|
15
|
+
initPrefetch();
|
|
14
16
|
|
|
15
17
|
// Resolve the current route so we can pre-load the components
|
|
16
18
|
// before handing off to App.svelte (avoids a flash of "Loading...")
|
|
@@ -35,9 +37,9 @@ async function main() {
|
|
|
35
37
|
ssrMode: false,
|
|
36
38
|
ssrPageComponent,
|
|
37
39
|
ssrLayoutComponents,
|
|
38
|
-
ssrPageData: (window as any).
|
|
39
|
-
ssrLayoutData: (window as any).
|
|
40
|
-
ssrFormData: (window as any).
|
|
40
|
+
ssrPageData: (window as any).__BOSBUN_PAGE_DATA__ ?? {},
|
|
41
|
+
ssrLayoutData: (window as any).__BOSBUN_LAYOUT_DATA__ ?? [],
|
|
42
|
+
ssrFormData: (window as any).__BOSBUN_FORM_DATA__ ?? null,
|
|
41
43
|
},
|
|
42
44
|
});
|
|
43
45
|
}
|
|
@@ -51,10 +53,10 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
51
53
|
let retryDelay = 1000;
|
|
52
54
|
|
|
53
55
|
function connectSSE() {
|
|
54
|
-
const es = new EventSource("/
|
|
56
|
+
const es = new EventSource("/__bosbun/sse");
|
|
55
57
|
|
|
56
58
|
es.addEventListener("reload", () => {
|
|
57
|
-
console.log("[
|
|
59
|
+
console.log("[Bosbun] Reloading...");
|
|
58
60
|
window.location.reload();
|
|
59
61
|
});
|
|
60
62
|
|
|
@@ -69,7 +71,7 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
69
71
|
|
|
70
72
|
es.onerror = () => {
|
|
71
73
|
es.close();
|
|
72
|
-
console.log(`[
|
|
74
|
+
console.log(`[Bosbun] SSE disconnected. Retrying in ${retryDelay / 1000}s...`);
|
|
73
75
|
setTimeout(connectSSE, retryDelay);
|
|
74
76
|
retryDelay = Math.min(retryDelay + 1000, 5000);
|
|
75
77
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ─── Link Prefetching ─────────────────────────────────────
|
|
2
|
+
// Supports `data-bosbun-preload="hover"` and `data-bosbun-preload="viewport"`
|
|
3
|
+
// on <a> elements or their ancestors.
|
|
4
|
+
|
|
5
|
+
export const prefetchCache = new Map<string, any>();
|
|
6
|
+
|
|
7
|
+
// In-flight fetch deduplication
|
|
8
|
+
const pending = new Set<string>();
|
|
9
|
+
|
|
10
|
+
/** Returns cached prefetch data for a path and removes it from cache. */
|
|
11
|
+
export function consumePrefetch(path: string): any | null {
|
|
12
|
+
const data = prefetchCache.get(path);
|
|
13
|
+
if (data === undefined) return null;
|
|
14
|
+
prefetchCache.delete(path);
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Prefetches data for a path and stores in cache. No-op if already cached/in-flight. */
|
|
19
|
+
export async function prefetchPath(path: string): Promise<void> {
|
|
20
|
+
if (prefetchCache.has(path)) return;
|
|
21
|
+
if (pending.has(path)) return;
|
|
22
|
+
|
|
23
|
+
pending.add(path);
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`/__bosbun/data?path=${encodeURIComponent(path)}`);
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
prefetchCache.set(path, await res.json());
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Silently ignore — prefetch is best-effort
|
|
31
|
+
} finally {
|
|
32
|
+
pending.delete(path);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getLinkHref(anchor: HTMLAnchorElement): string | null {
|
|
37
|
+
if (anchor.origin !== window.location.origin) return null;
|
|
38
|
+
if (anchor.target) return null;
|
|
39
|
+
if (anchor.hasAttribute("download")) return null;
|
|
40
|
+
return anchor.pathname + anchor.search;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function observeViewportLinks(container: Element | Document = document) {
|
|
44
|
+
const observer = new IntersectionObserver((entries) => {
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isIntersecting) continue;
|
|
47
|
+
const anchor = entry.target as HTMLAnchorElement;
|
|
48
|
+
const href = getLinkHref(anchor);
|
|
49
|
+
if (href) prefetchPath(href);
|
|
50
|
+
observer.unobserve(anchor);
|
|
51
|
+
}
|
|
52
|
+
}, { rootMargin: "0px" });
|
|
53
|
+
|
|
54
|
+
const links = (container === document ? document : container as Element)
|
|
55
|
+
.querySelectorAll<HTMLAnchorElement>("a[data-bosbun-preload='viewport']");
|
|
56
|
+
|
|
57
|
+
for (const link of links) {
|
|
58
|
+
observer.observe(link);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return observer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function initPrefetch(): void {
|
|
65
|
+
// ── Hover strategy (event delegation, 20ms debounce) ─────
|
|
66
|
+
let hoverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
+
|
|
68
|
+
document.addEventListener("mouseover", (e) => {
|
|
69
|
+
if (!(e.target instanceof Element)) return;
|
|
70
|
+
// Early exit: skip if no [data-bosbun-preload="hover"] ancestor exists
|
|
71
|
+
const preloadEl = e.target.closest("[data-bosbun-preload]");
|
|
72
|
+
if (!preloadEl || preloadEl.getAttribute("data-bosbun-preload") !== "hover") return;
|
|
73
|
+
const anchor = e.target.closest("a") as HTMLAnchorElement | null;
|
|
74
|
+
if (!anchor) return;
|
|
75
|
+
const href = getLinkHref(anchor);
|
|
76
|
+
if (!href) return;
|
|
77
|
+
|
|
78
|
+
if (hoverTimer) clearTimeout(hoverTimer);
|
|
79
|
+
hoverTimer = setTimeout(() => prefetchPath(href), 100);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
document.addEventListener("mouseout", () => {
|
|
83
|
+
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── Viewport strategy ─────────────────────────────────────
|
|
87
|
+
const observer = observeViewportLinks();
|
|
88
|
+
|
|
89
|
+
// Pick up links added after initial render (e.g., after client navigation)
|
|
90
|
+
const mutation = new MutationObserver((records) => {
|
|
91
|
+
for (const record of records) {
|
|
92
|
+
for (const node of record.addedNodes) {
|
|
93
|
+
if (!(node instanceof Element)) continue;
|
|
94
|
+
// The node itself might be a viewport link
|
|
95
|
+
if (node.matches("a[data-bosbun-preload='viewport']")) {
|
|
96
|
+
observer.observe(node as HTMLAnchorElement);
|
|
97
|
+
}
|
|
98
|
+
// Or it might contain viewport links
|
|
99
|
+
for (const link of node.querySelectorAll<HTMLAnchorElement>(
|
|
100
|
+
"a[data-bosbun-preload='viewport']"
|
|
101
|
+
)) {
|
|
102
|
+
observer.observe(link);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
mutation.observe(document.body, { childList: true, subtree: true });
|
|
109
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Singleton used by App.svelte and hydrate.ts.
|
|
4
4
|
|
|
5
5
|
import { findMatch } from "../matcher.ts";
|
|
6
|
-
import { clientRoutes } from "
|
|
6
|
+
import { clientRoutes } from "bosbun:routes";
|
|
7
7
|
|
|
8
8
|
export const router = new class Router {
|
|
9
9
|
currentRoute = $state(typeof window !== "undefined" ? window.location.pathname : "/");
|
package/src/core/dev.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, type Subprocess } from "bun";
|
|
|
2
2
|
import { watch } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
|
-
console.log("🐰
|
|
5
|
+
console.log("🐰 Bosbun dev server starting...\n");
|
|
6
6
|
|
|
7
7
|
// ─── State ────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ function broadcastReload() {
|
|
|
28
28
|
// ─── Build ────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
|
|
31
|
-
const
|
|
31
|
+
const BOSBUN_NODE_MODULES = join(import.meta.dir, "..", "..", "node_modules");
|
|
32
32
|
|
|
33
33
|
async function runBuild(): Promise<boolean> {
|
|
34
34
|
console.log("🏗️ Building...");
|
|
@@ -67,8 +67,8 @@ async function startAppServer() {
|
|
|
67
67
|
NODE_ENV: "development",
|
|
68
68
|
// Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
|
|
69
69
|
PORT: String(APP_PORT),
|
|
70
|
-
// Allow externalized deps (elysia, etc.) to resolve from
|
|
71
|
-
NODE_PATH:
|
|
70
|
+
// Allow externalized deps (elysia, etc.) to resolve from bosbun's node_modules
|
|
71
|
+
NODE_PATH: BOSBUN_NODE_MODULES,
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
}
|
|
@@ -105,7 +105,7 @@ Bun.serve({
|
|
|
105
105
|
const url = new URL(req.url);
|
|
106
106
|
|
|
107
107
|
// SSE endpoint — owned by dev server, not the app
|
|
108
|
-
if (url.pathname === "/
|
|
108
|
+
if (url.pathname === "/__bosbun/sse") {
|
|
109
109
|
return new Response(
|
|
110
110
|
new ReadableStream({
|
|
111
111
|
start(ctrl) {
|
|
@@ -170,8 +170,8 @@ console.log(`\n🌐 Open http://localhost:${DEV_PORT}\n`);
|
|
|
170
170
|
// Watch src/ recursively. Skip generated files to avoid loops.
|
|
171
171
|
|
|
172
172
|
const GENERATED = [
|
|
173
|
-
join(process.cwd(), ".
|
|
174
|
-
join(process.cwd(), "public", "
|
|
173
|
+
join(process.cwd(), ".bosbun"),
|
|
174
|
+
join(process.cwd(), "public", "bosbun-tw.css"),
|
|
175
175
|
];
|
|
176
176
|
|
|
177
177
|
function isGenerated(path: string): boolean {
|
package/src/core/env.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
// ─── Framework-reserved vars ─────────────────────────────
|
|
5
|
-
// These are controlled by
|
|
5
|
+
// These are controlled by Bosbun itself — users access them via process.env directly.
|
|
6
6
|
const FRAMEWORK_VARS = new Set([
|
|
7
7
|
"PORT",
|
|
8
8
|
"NODE_ENV",
|
package/src/core/envCodegen.ts
CHANGED
|
@@ -3,26 +3,26 @@ import { join } from "path";
|
|
|
3
3
|
import type { ClassifiedEnv } from "./env.ts";
|
|
4
4
|
|
|
5
5
|
// ─── Env Module Codegen ──────────────────────────────────
|
|
6
|
-
// Generates three files in .
|
|
6
|
+
// Generates three files in .bosbun/:
|
|
7
7
|
// env.server.ts — all vars (static inlined, dynamic via process.env)
|
|
8
|
-
// env.client.ts — only PUBLIC_* vars (PUBLIC_STATIC_* inlined, PUBLIC_* via window.
|
|
9
|
-
// types/env.d.ts — declare module '
|
|
8
|
+
// env.client.ts — only PUBLIC_* vars (PUBLIC_STATIC_* inlined, PUBLIC_* via window.__BOSBUN_ENV__)
|
|
9
|
+
// types/env.d.ts — declare module 'bosbun:env' for IDE autocomplete
|
|
10
10
|
|
|
11
11
|
export function generateEnvModules(classified: ClassifiedEnv): void {
|
|
12
|
-
const
|
|
13
|
-
const typesDir = join(
|
|
14
|
-
mkdirSync(
|
|
12
|
+
const bosbunDir = join(process.cwd(), ".bosbun");
|
|
13
|
+
const typesDir = join(bosbunDir, "types");
|
|
14
|
+
mkdirSync(bosbunDir, { recursive: true });
|
|
15
15
|
mkdirSync(typesDir, { recursive: true });
|
|
16
16
|
|
|
17
|
-
writeServerEnv(classified,
|
|
18
|
-
writeClientEnv(classified,
|
|
17
|
+
writeServerEnv(classified, bosbunDir);
|
|
18
|
+
writeClientEnv(classified, bosbunDir);
|
|
19
19
|
writeEnvTypes(classified, typesDir);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function writeServerEnv(classified: ClassifiedEnv,
|
|
22
|
+
function writeServerEnv(classified: ClassifiedEnv, bosbunDir: string): void {
|
|
23
23
|
const lines: string[] = [
|
|
24
|
-
"// Auto-generated by
|
|
25
|
-
"//
|
|
24
|
+
"// Auto-generated by Bosbun. Do not edit.",
|
|
25
|
+
"// bosbun:env → server — all vars",
|
|
26
26
|
"",
|
|
27
27
|
];
|
|
28
28
|
|
|
@@ -46,16 +46,16 @@ function writeServerEnv(classified: ClassifiedEnv, buniaDir: string): void {
|
|
|
46
46
|
lines.push(`export const ${key} = process.env.${key} ?? "";`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
writeFileSync(join(
|
|
49
|
+
writeFileSync(join(bosbunDir, "env.server.ts"), lines.join("\n") + "\n");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function writeClientEnv(classified: ClassifiedEnv,
|
|
52
|
+
function writeClientEnv(classified: ClassifiedEnv, bosbunDir: string): void {
|
|
53
53
|
const lines: string[] = [
|
|
54
|
-
"// Auto-generated by
|
|
55
|
-
"//
|
|
54
|
+
"// Auto-generated by Bosbun. Do not edit.",
|
|
55
|
+
"// bosbun:env → client — PUBLIC_* vars only",
|
|
56
56
|
"",
|
|
57
57
|
"const __env: Record<string, string> =",
|
|
58
|
-
" typeof window !== 'undefined' && (window as any).
|
|
58
|
+
" typeof window !== 'undefined' && (window as any).__BOSBUN_ENV__ || {};",
|
|
59
59
|
"",
|
|
60
60
|
];
|
|
61
61
|
|
|
@@ -64,12 +64,12 @@ function writeClientEnv(classified: ClassifiedEnv, buniaDir: string): void {
|
|
|
64
64
|
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// PUBLIC_* dynamic — read from window.
|
|
67
|
+
// PUBLIC_* dynamic — read from window.__BOSBUN_ENV__ at runtime
|
|
68
68
|
for (const key of Object.keys(classified.publicDynamic)) {
|
|
69
69
|
lines.push(`export const ${key} = __env.${key} ?? "";`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
writeFileSync(join(
|
|
72
|
+
writeFileSync(join(bosbunDir, "env.client.ts"), lines.join("\n") + "\n");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function writeEnvTypes(classified: ClassifiedEnv, typesDir: string): void {
|
|
@@ -83,8 +83,8 @@ function writeEnvTypes(classified: ClassifiedEnv, typesDir: string): void {
|
|
|
83
83
|
const declarations = allKeys.map(key => ` export const ${key}: string;`);
|
|
84
84
|
|
|
85
85
|
const content = [
|
|
86
|
-
"// Auto-generated by
|
|
87
|
-
"declare module '
|
|
86
|
+
"// Auto-generated by Bosbun. Do not edit.",
|
|
87
|
+
"declare module 'bosbun:env' {",
|
|
88
88
|
...declarations,
|
|
89
89
|
"}",
|
|
90
90
|
"",
|
package/src/core/hooks.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// ───
|
|
1
|
+
// ─── Bosbun Hooks ─────────────────────────────────────────
|
|
2
2
|
// SvelteKit-compatible middleware API.
|
|
3
3
|
// Usage in src/hooks.server.ts:
|
|
4
4
|
//
|
|
5
|
-
// import { sequence } from "
|
|
5
|
+
// import { sequence } from "bosbun";
|
|
6
6
|
// export const handle = sequence(authHandle, loggingHandle);
|
|
7
7
|
|
|
8
8
|
// ─── Cookie Types ─────────────────────────────────────────
|
package/src/core/html.ts
CHANGED
|
@@ -70,21 +70,21 @@ export function buildHtml(
|
|
|
70
70
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
71
71
|
.join("\n ");
|
|
72
72
|
|
|
73
|
-
const fallbackTitle = head.includes("<title>") ? "" : "<title>
|
|
73
|
+
const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosbun App</title>";
|
|
74
74
|
|
|
75
75
|
const publicEnv = getPublicDynamicEnv();
|
|
76
76
|
const envScript = Object.keys(publicEnv).length > 0
|
|
77
|
-
? `\n <script>window.
|
|
77
|
+
? `\n <script>window.__BOSBUN_ENV__=${safeJsonStringify(publicEnv)};</script>`
|
|
78
78
|
: "";
|
|
79
79
|
|
|
80
80
|
const formScript = formData != null
|
|
81
|
-
? `window.
|
|
81
|
+
? `window.__BOSBUN_FORM_DATA__=${safeJsonStringify(formData)};`
|
|
82
82
|
: "";
|
|
83
83
|
|
|
84
84
|
const scripts = csr
|
|
85
|
-
? `${envScript}\n <script>window.
|
|
85
|
+
? `${envScript}\n <script>window.__BOSBUN_PAGE_DATA__=${safeJsonStringify(pageData)};window.__BOSBUN_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formScript}</script>\n <script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`
|
|
86
86
|
: isDev
|
|
87
|
-
? `\n <script>!function r(){var e=new EventSource("/
|
|
87
|
+
? `\n <script>!function r(){var e=new EventSource("/__bosbun/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
|
|
88
88
|
: "";
|
|
89
89
|
|
|
90
90
|
return `<!DOCTYPE html>
|
|
@@ -96,9 +96,9 @@ export function buildHtml(
|
|
|
96
96
|
<link rel="icon" href="data:,">
|
|
97
97
|
${head}
|
|
98
98
|
${cssLinks}
|
|
99
|
-
<link rel="stylesheet" href="/
|
|
99
|
+
<link rel="stylesheet" href="/bosbun-tw.css${cacheBust}">
|
|
100
100
|
</head>
|
|
101
|
-
<body>
|
|
101
|
+
<body data-bosbun-preload="hover">
|
|
102
102
|
<div id="app">${body}</div>${scripts}
|
|
103
103
|
</body>
|
|
104
104
|
</html>`;
|
|
@@ -116,6 +116,7 @@ export function buildHtmlShell(): string {
|
|
|
116
116
|
return _shell;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
|
|
119
120
|
let _shellOpen: string | null = null;
|
|
120
121
|
|
|
121
122
|
/** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
|
|
@@ -130,15 +131,15 @@ export function buildHtmlShellOpen(): string {
|
|
|
130
131
|
` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
|
|
131
132
|
` <link rel="icon" href="data:,">\n` +
|
|
132
133
|
` ${cssLinks}\n` +
|
|
133
|
-
` <link rel="stylesheet" href="/
|
|
134
|
+
` <link rel="stylesheet" href="/bosbun-tw.css${cacheBust}">\n` +
|
|
134
135
|
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
|
|
135
136
|
return _shellOpen;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const SPINNER = `<div id="__bs__"><style>` +
|
|
139
|
-
`:root{--
|
|
140
|
+
`:root{--bosbun-loading-color:#f73b27}` +
|
|
140
141
|
`#__bs__{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}` +
|
|
141
|
-
`#__bs__ i{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--
|
|
142
|
+
`#__bs__ i{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--bosbun-loading-color);` +
|
|
142
143
|
`border-radius:50%;animation:__bs__ .8s linear infinite}` +
|
|
143
144
|
`@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
|
|
144
145
|
|
|
@@ -157,9 +158,9 @@ export function buildMetadataChunk(metadata: Metadata | null): string {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
} else {
|
|
160
|
-
out += ` <title>
|
|
161
|
+
out += ` <title>Bosbun App</title>\n`;
|
|
161
162
|
}
|
|
162
|
-
out += `</head>\n<body>\n${SPINNER}`;
|
|
163
|
+
out += `</head>\n<body data-bosbun-preload="hover">\n${SPINNER}`;
|
|
163
164
|
return out;
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -186,14 +187,14 @@ export function buildHtmlTail(
|
|
|
186
187
|
if (csr) {
|
|
187
188
|
const publicEnv = getPublicDynamicEnv();
|
|
188
189
|
if (Object.keys(publicEnv).length > 0) {
|
|
189
|
-
out += `\n<script>window.
|
|
190
|
+
out += `\n<script>window.__BOSBUN_ENV__=${safeJsonStringify(publicEnv)};</script>`;
|
|
190
191
|
}
|
|
191
|
-
const formInject = formData != null ? `window.
|
|
192
|
-
out += `\n<script>window.
|
|
193
|
-
`window.
|
|
192
|
+
const formInject = formData != null ? `window.__BOSBUN_FORM_DATA__=${safeJsonStringify(formData)};` : "";
|
|
193
|
+
out += `\n<script>window.__BOSBUN_PAGE_DATA__=${safeJsonStringify(pageData)};` +
|
|
194
|
+
`window.__BOSBUN_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formInject}</script>`;
|
|
194
195
|
out += `\n<script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`;
|
|
195
196
|
} else if (isDev) {
|
|
196
|
-
out += `\n<script>!function r(){var e=new EventSource("/
|
|
197
|
+
out += `\n<script>!function r(){var e=new EventSource("/__bosbun/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
|
|
197
198
|
}
|
|
198
199
|
out += `\n</body>\n</html>`;
|
|
199
200
|
return out;
|
|
@@ -217,7 +218,7 @@ export function compress(body: string, contentType: string, req: Request, status
|
|
|
217
218
|
export const STATIC_EXTS = new Set([".ico", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".css", ".js", ".woff", ".woff2", ".ttf"]);
|
|
218
219
|
|
|
219
220
|
export function isStaticPath(path: string): boolean {
|
|
220
|
-
if (path.startsWith("/dist/") || path.startsWith("/
|
|
221
|
+
if (path.startsWith("/dist/") || path.startsWith("/__bosbun/")) return true;
|
|
221
222
|
const dot = path.lastIndexOf(".");
|
|
222
223
|
return dot !== -1 && STATIC_EXTS.has(path.slice(dot));
|
|
223
224
|
}
|
package/src/core/plugin.ts
CHANGED
|
@@ -2,24 +2,24 @@ import { join } from "path";
|
|
|
2
2
|
|
|
3
3
|
// ─── Bun Build Plugin ─────────────────────────────────────
|
|
4
4
|
// Resolves:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// bosbun:routes → .bosbun/routes.ts (generated route map)
|
|
6
|
+
// bosbun:env → .bosbun/env.server.ts (bun) or .bosbun/env.client.ts (browser)
|
|
7
7
|
// $lib/* → src/lib/* (user library alias)
|
|
8
8
|
|
|
9
|
-
export function
|
|
9
|
+
export function makeBosbunPlugin(target: "browser" | "bun" = "bun") {
|
|
10
10
|
return {
|
|
11
|
-
name: "
|
|
11
|
+
name: "bosbun-resolver",
|
|
12
12
|
setup(build: import("bun").PluginBuilder) {
|
|
13
|
-
//
|
|
14
|
-
build.onResolve({ filter: /^
|
|
15
|
-
path: join(process.cwd(), ".
|
|
13
|
+
// bosbun:routes → .bosbun/routes.ts
|
|
14
|
+
build.onResolve({ filter: /^bosbun:routes$/ }, () => ({
|
|
15
|
+
path: join(process.cwd(), ".bosbun", "routes.ts"),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
build.onResolve({ filter: /^
|
|
18
|
+
// bosbun:env → .bosbun/env.client.ts (browser) or .bosbun/env.server.ts (bun)
|
|
19
|
+
build.onResolve({ filter: /^bosbun:env$/ }, () => ({
|
|
20
20
|
path: join(
|
|
21
21
|
process.cwd(),
|
|
22
|
-
".
|
|
22
|
+
".bosbun",
|
|
23
23
|
target === "browser" ? "env.client.ts" : "env.server.ts",
|
|
24
24
|
),
|
|
25
25
|
}));
|
|
@@ -32,13 +32,13 @@ export function makeBuniaPlugin(target: "browser" | "bun" = "bun") {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// "tailwindcss" inside app.css is a Tailwind CLI directive —
|
|
35
|
-
// it's already compiled to public/
|
|
35
|
+
// it's already compiled to public/bosbun-tw.css by the CLI step.
|
|
36
36
|
// Return an empty CSS module so Bun's CSS bundler doesn't choke on it.
|
|
37
37
|
build.onResolve({ filter: /^tailwindcss$/ }, () => ({
|
|
38
38
|
path: "tailwindcss",
|
|
39
|
-
namespace: "
|
|
39
|
+
namespace: "bosbun-empty-css",
|
|
40
40
|
}));
|
|
41
|
-
build.onLoad({ filter: /.*/, namespace: "
|
|
41
|
+
build.onLoad({ filter: /.*/, namespace: "bosbun-empty-css" }, () => ({
|
|
42
42
|
contents: "",
|
|
43
43
|
loader: "css",
|
|
44
44
|
}));
|
package/src/core/prerender.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import type { RouteManifest } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
const CORE_DIR = import.meta.dir;
|
|
6
|
-
const
|
|
6
|
+
const BOSBUN_NODE_MODULES = join(CORE_DIR, "..", "..", "node_modules");
|
|
7
7
|
|
|
8
8
|
const PRERENDER_TIMEOUT = Number(process.env.PRERENDER_TIMEOUT) || 5_000; // 5s default
|
|
9
9
|
|
|
@@ -36,7 +36,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
36
36
|
const child = Bun.spawn(
|
|
37
37
|
["bun", "run", "./dist/server/index.js"],
|
|
38
38
|
{
|
|
39
|
-
env: { ...process.env, NODE_ENV: "production", PORT: String(port), NODE_PATH:
|
|
39
|
+
env: { ...process.env, NODE_ENV: "production", PORT: String(port), NODE_PATH: BOSBUN_NODE_MODULES },
|
|
40
40
|
stdout: "ignore",
|
|
41
41
|
stderr: "ignore",
|
|
42
42
|
},
|
package/src/core/renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render } from "svelte/server";
|
|
2
2
|
|
|
3
3
|
import { findMatch } from "./matcher.ts";
|
|
4
|
-
import { serverRoutes, errorPage } from "
|
|
4
|
+
import { serverRoutes, errorPage } from "bosbun:routes";
|
|
5
5
|
import type { Cookies } from "./hooks.ts";
|
|
6
6
|
import { HttpError, Redirect } from "./errors.ts";
|
|
7
7
|
import App from "./client/App.svelte";
|
|
@@ -59,7 +59,7 @@ function makeFetch(req: Request, url: URL) {
|
|
|
59
59
|
|
|
60
60
|
// ─── Route Data Loader ───────────────────────────────────
|
|
61
61
|
// Runs layout + page server loaders for a given URL.
|
|
62
|
-
// Used by both SSR and the /
|
|
62
|
+
// Used by both SSR and the /__bosbun/data JSON endpoint.
|
|
63
63
|
|
|
64
64
|
export async function loadRouteData(
|
|
65
65
|
url: URL,
|
|
@@ -258,7 +258,7 @@ export async function renderSSRStream(
|
|
|
258
258
|
}
|
|
259
259
|
if (err instanceof HttpError) {
|
|
260
260
|
controller.enqueue(enc.encode(
|
|
261
|
-
`<script>location.replace("/
|
|
261
|
+
`<script>location.replace("/__bosbun/error?status=${err.status}&message="+encodeURIComponent(${safeJsonStringify(err.message)}))</script></body></html>`
|
|
262
262
|
));
|
|
263
263
|
controller.close();
|
|
264
264
|
return;
|
package/src/core/routeFile.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { writeFileSync, mkdirSync } from "fs";
|
|
|
2
2
|
import type { RouteManifest } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
// ─── Route File Generator ─────────────────────────────────
|
|
5
|
-
// Generates .
|
|
5
|
+
// Generates .bosbun/routes.ts — ONE file with three exports:
|
|
6
6
|
// clientRoutes — used by client hydrator (page + layout lazy imports)
|
|
7
7
|
// serverRoutes — used by SSR renderer (+ pageServer + layoutServers)
|
|
8
8
|
// apiRoutes — used by API handler
|
|
@@ -28,7 +28,7 @@ function sortRoutes<T extends { pattern: string }>(routes: T[]): T[] {
|
|
|
28
28
|
|
|
29
29
|
export function generateRoutesFile(manifest: RouteManifest): void {
|
|
30
30
|
const lines: string[] = [
|
|
31
|
-
"// AUTO-GENERATED by
|
|
31
|
+
"// AUTO-GENERATED by bosbun build — do not edit\n",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
const pages = sortRoutes(manifest.pages);
|
|
@@ -99,12 +99,12 @@ export function generateRoutesFile(manifest: RouteManifest): void {
|
|
|
99
99
|
ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
|
|
100
100
|
};\n`);
|
|
101
101
|
|
|
102
|
-
mkdirSync(".
|
|
103
|
-
writeFileSync(".
|
|
104
|
-
console.log("✅ Routes generated: .
|
|
102
|
+
mkdirSync(".bosbun", { recursive: true });
|
|
103
|
+
writeFileSync(".bosbun/routes.ts", lines.join("\n"));
|
|
104
|
+
console.log("✅ Routes generated: .bosbun/routes.ts");
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// Import path from .
|
|
107
|
+
// Import path from .bosbun/routes.ts to src/routes/<routePath>
|
|
108
108
|
function toImportPath(routePath: string): string {
|
|
109
109
|
return "../src/routes/" + routePath.replace(/\\/g, "/");
|
|
110
110
|
}
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import type { RouteManifest } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
// ─── Route Types Generator ────────────────────────────────
|
|
6
|
-
// Generates .
|
|
6
|
+
// Generates .bosbun/types/src/routes/**/$types.d.ts for each
|
|
7
7
|
// route directory. Combined with rootDirs in tsconfig.json,
|
|
8
8
|
// this allows `import type { PageData } from './$types'` to
|
|
9
9
|
// work in +page.svelte files — identical to SvelteKit's API.
|
|
@@ -36,12 +36,12 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
36
36
|
const segments = dir === "." ? [] : dir.split("/").filter(Boolean);
|
|
37
37
|
|
|
38
38
|
// Depth of the generated file from project root:
|
|
39
|
-
// .
|
|
39
|
+
// .bosbun/ + types/ + src/ + routes/ + ...segments
|
|
40
40
|
const depth = 4 + segments.length;
|
|
41
41
|
const up = "../".repeat(depth);
|
|
42
42
|
const srcBase = `${up}src/routes/${segments.length ? segments.join("/") + "/" : ""}`;
|
|
43
43
|
|
|
44
|
-
const lines: string[] = ["// AUTO-GENERATED by
|
|
44
|
+
const lines: string[] = ["// AUTO-GENERATED by bosbun — do not edit\n"];
|
|
45
45
|
|
|
46
46
|
if (info.pageServer) {
|
|
47
47
|
lines.push(`import type { load as _pageLoad } from '${srcBase}+page.server.ts';`);
|
|
@@ -68,16 +68,16 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
68
68
|
lines.push(`export type LayoutProps = { data: LayoutData; children: any };`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const outDir = join(process.cwd(), ".
|
|
71
|
+
const outDir = join(process.cwd(), ".bosbun", "types", "src", "routes", ...segments);
|
|
72
72
|
mkdirSync(outDir, { recursive: true });
|
|
73
73
|
writeFileSync(join(outDir, "$types.d.ts"), lines.join("\n") + "\n");
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
console.log(`✅ Route types generated: .
|
|
76
|
+
console.log(`✅ Route types generated: .bosbun/types/ (${dirs.size} route director${dirs.size === 1 ? "y" : "ies"})`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ─── Ensure tsconfig rootDirs ─────────────────────────────
|
|
80
|
-
// Adds ".
|
|
80
|
+
// Adds ".bosbun/types" to rootDirs so TypeScript resolves
|
|
81
81
|
// `./$types` imports via the generated declaration files.
|
|
82
82
|
// Only patches the file if rootDirs is not already set.
|
|
83
83
|
|
|
@@ -90,17 +90,17 @@ export function ensureRootDirs(): void {
|
|
|
90
90
|
tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
|
|
91
91
|
} catch {
|
|
92
92
|
console.warn("⚠️ Could not parse tsconfig.json — add rootDirs manually:\n" +
|
|
93
|
-
' "rootDirs": [".", ".
|
|
93
|
+
' "rootDirs": [".", ".bosbun/types"]');
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const rootDirs: string[] = tsconfig.compilerOptions?.rootDirs ?? [];
|
|
98
|
-
if (rootDirs.includes(".
|
|
98
|
+
if (rootDirs.includes(".bosbun/types")) return;
|
|
99
99
|
|
|
100
100
|
tsconfig.compilerOptions ??= {};
|
|
101
|
-
tsconfig.compilerOptions.rootDirs = [".", ".
|
|
101
|
+
tsconfig.compilerOptions.rootDirs = [".", ".bosbun/types",
|
|
102
102
|
...rootDirs.filter((d: string) => d !== ".")];
|
|
103
103
|
|
|
104
104
|
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
105
|
-
console.log("✅ tsconfig.json: added .
|
|
105
|
+
console.log("✅ tsconfig.json: added .bosbun/types to rootDirs");
|
|
106
106
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "fs";
|
|
|
4
4
|
import { join, resolve as resolvePath } from "path";
|
|
5
5
|
|
|
6
6
|
import { findMatch } from "./matcher.ts";
|
|
7
|
-
import { apiRoutes, serverRoutes } from "
|
|
7
|
+
import { apiRoutes, serverRoutes } from "bosbun:routes";
|
|
8
8
|
import type { Handle, RequestEvent } from "./hooks.ts";
|
|
9
9
|
import { HttpError, Redirect, ActionFailure } from "./errors.ts";
|
|
10
10
|
import { CookieJar } from "./cookies.ts";
|
|
@@ -121,13 +121,13 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Data endpoint — returns server loader data as JSON for client-side navigation
|
|
124
|
-
if (path === "/
|
|
124
|
+
if (path === "/__bosbun/data") {
|
|
125
125
|
const routePath = url.searchParams.get("path") ?? "/";
|
|
126
126
|
if (!isValidRoutePath(routePath, url.origin)) {
|
|
127
127
|
return Response.json({ error: "Invalid path", status: 400 }, { status: 400 });
|
|
128
128
|
}
|
|
129
129
|
const routeUrl = new URL(routePath, url.origin);
|
|
130
|
-
// Rewrite event.url so logging middleware sees the real page path, not /
|
|
130
|
+
// Rewrite event.url so logging middleware sees the real page path, not /__bosbun/data
|
|
131
131
|
event.url = routeUrl;
|
|
132
132
|
try {
|
|
133
133
|
const data = await loadRouteData(routeUrl, locals, request, cookies);
|
|
@@ -398,7 +398,7 @@ const app = new Elysia({ serve: { maxRequestBodySize: BODY_SIZE_LIMIT } })
|
|
|
398
398
|
|
|
399
399
|
app.listen(PORT, () => {
|
|
400
400
|
// In dev mode the proxy owns the user-facing port — don't print the internal port
|
|
401
|
-
if (!isDev) console.log(`🐰
|
|
401
|
+
if (!isDev) console.log(`🐰 Bosbun server running at http://localhost:${PORT}`);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
404
|
function shutdown() {
|
package/src/core/types.ts
CHANGED
package/src/lib/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// ───
|
|
1
|
+
// ─── Bosbun Public API ─────────────────────────────────────
|
|
2
2
|
// Usage in user apps:
|
|
3
|
-
// import { cn, sequence } from "
|
|
4
|
-
// import type { RequestEvent, LoadEvent, Handle, Cookies } from "
|
|
3
|
+
// import { cn, sequence } from "bosbun"
|
|
4
|
+
// import type { RequestEvent, LoadEvent, Handle, Cookies } from "bosbun"
|
|
5
5
|
|
|
6
6
|
export { cn, getServerTime } from "./utils.ts";
|
|
7
7
|
export { sequence } from "../core/hooks.ts";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ───
|
|
1
|
+
# ─── Bosbun Environment Variables ───────────────────────────────────────────────
|
|
2
2
|
#
|
|
3
3
|
# Copy this file to .env and fill in your values.
|
|
4
4
|
# .env.local overrides .env (gitignored — for local secrets).
|
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
# Prefix convention:
|
|
8
8
|
#
|
|
9
9
|
# PUBLIC_STATIC_* Client + server, inlined at build time (e.g. app name)
|
|
10
|
-
# PUBLIC_* Client + server, injected at runtime via window.
|
|
10
|
+
# PUBLIC_* Client + server, injected at runtime via window.__BOSBUN_ENV__
|
|
11
11
|
# STATIC_* Server only, inlined at build time
|
|
12
12
|
# (no prefix) Server only, read from process.env at runtime (secrets, DB creds)
|
|
13
13
|
#
|
|
14
14
|
# Import in your code:
|
|
15
|
-
# import { PUBLIC_STATIC_APP_NAME, DB_PASSWORD } from '
|
|
15
|
+
# import { PUBLIC_STATIC_APP_NAME, DB_PASSWORD } from 'bosbun:env';
|
|
16
16
|
#
|
|
17
17
|
# Framework vars (PORT, NODE_ENV, BODY_SIZE_LIMIT, CSRF_ALLOWED_ORIGINS,
|
|
18
|
-
# CORS_*, LOAD_TIMEOUT, METADATA_TIMEOUT, PRERENDER_TIMEOUT) are NOT exposed via
|
|
18
|
+
# CORS_*, LOAD_TIMEOUT, METADATA_TIMEOUT, PRERENDER_TIMEOUT) are NOT exposed via bosbun:env —
|
|
19
19
|
# access them via process.env directly.
|
|
20
20
|
# ────────────────────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
# Public build-time constant (safe to expose to client)
|
|
23
|
-
PUBLIC_STATIC_APP_NAME=My
|
|
23
|
+
PUBLIC_STATIC_APP_NAME=My Bosbun App
|
|
24
24
|
|
|
25
|
-
# ─── Framework vars — access via process.env (not via
|
|
25
|
+
# ─── Framework vars — access via process.env (not via bosbun:env) ─────────────
|
|
26
26
|
|
|
27
27
|
# Server port. Defaults to 9000 in production, 9001 in dev (proxied via :9000).
|
|
28
28
|
# PORT=9000
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
A [
|
|
3
|
+
A [Bosbun](https://github.com/bosapi/bosbun) project — SSR · Svelte 5 · Bun · ElysiaJS.
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
@@ -45,7 +45,7 @@ Fetch data on the server before rendering:
|
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
47
|
// src/routes/blog/[slug]/+page.server.ts
|
|
48
|
-
import type { LoadEvent } from "
|
|
48
|
+
import type { LoadEvent } from "bosbun";
|
|
49
49
|
|
|
50
50
|
export async function load({ params }: LoadEvent) {
|
|
51
51
|
return { post: await getPost(params.slug) };
|
|
@@ -97,6 +97,6 @@ cn("px-4 py-2", isActive && "bg-primary")
|
|
|
97
97
|
|
|
98
98
|
## Learn More
|
|
99
99
|
|
|
100
|
-
- [
|
|
100
|
+
- [Bosbun documentation](https://github.com/bosapi/bosbun)
|
|
101
101
|
- [Svelte 5 docs](https://svelte.dev)
|
|
102
102
|
- [Tailwind CSS v4](https://tailwindcss.com)
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "
|
|
7
|
-
"build": "
|
|
8
|
-
"start": "
|
|
6
|
+
"dev": "bosbun dev",
|
|
7
|
+
"build": "bosbun build",
|
|
8
|
+
"start": "bosbun start",
|
|
9
9
|
"check": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"
|
|
12
|
+
"bosbun": "*",
|
|
13
13
|
"svelte": "^5.20.0",
|
|
14
14
|
"clsx": "^2.1.1",
|
|
15
15
|
"tailwind-merge": "^3.5.0"
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
<p class="text-6xl">🐰</p>
|
|
8
8
|
<h1 class="text-4xl font-bold tracking-tight">{name}</h1>
|
|
9
9
|
<p class="text-muted-foreground text-lg">
|
|
10
|
-
A
|
|
10
|
+
A Bosbun project — SSR + Svelte 5 + Bun + ElysiaJS
|
|
11
11
|
</p>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
<div class="mt-4 flex gap-3">
|
|
15
15
|
<a
|
|
16
|
-
href="https://github.com/bosapi/
|
|
16
|
+
href="https://github.com/bosapi/bosbun"
|
|
17
17
|
target="_blank"
|
|
18
18
|
rel="noopener noreferrer"
|
|
19
19
|
class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
Docs
|
|
22
22
|
</a>
|
|
23
23
|
<a
|
|
24
|
-
href="https://github.com/bosapi/
|
|
24
|
+
href="https://github.com/bosapi/bosbun/tree/main/registry"
|
|
25
25
|
target="_blank"
|
|
26
26
|
rel="noopener noreferrer"
|
|
27
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"
|