للبدء، استورد ملفات HTML ومررها إلى خيار routes في Bun.serve().
import { serve } from "bun";
import dashboard from "./dashboard.html";
import homepage from "./index.html";
const server = serve({
routes: {
// ** واردات HTML **
// حزم وتوجيه index.html إلى "/". هذا يستخدم HTMLRewriter لمسح
// HTML بحثًا عن وسوم `<script>` و `<link>`، يشغل حزمة JavaScript
// و CSS في Bun، يترجم أي TypeScript و JSX و TSX،
// يخفض CSS بمحلل CSS في Bun ويقدم النتيجة.
"/": homepage,
// حزم وتوجيه dashboard.html إلى "/dashboard"
"/dashboard": dashboard,
// ** نقاط نهاية API ** (يتطلب Bun v1.2.3+)
"/api/users": {
async GET(req) {
const users = await sql`SELECT * FROM users`;
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
const [user] = await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;
return Response.json(user);
},
},
"/api/users/:id": async req => {
const { id } = req.params;
const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;
return Response.json(user);
},
},
// تفعيل وضع التطوير لـ
// - رسائل خطأ مفصلة
// - إعادة التحميل الساخنة (يتطلب Bun v1.2.3+)
development: true,
});
console.log(`Listening on ${server.url}`);bun run app.tsمسارات HTML
واردات HTML كمسارات
الويب يبدأ بـ HTML، وكذلك خادم التطوير كامل المكدس في Bun.
لتحديد نقاط الدخول للواجهة الأمامية، استورد ملفات HTML في ملفات JavaScript/TypeScript/TSX/JSX الخاصة بك.
import dashboard from "./dashboard.html";
import homepage from "./index.html";تُستخدم ملفات HTML هذه كمسارات في خادم تطوير Bun يمكنك تمريرها إلى Bun.serve().
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
fetch(req) {
// ... طلبات api
},
});عند إجراء طلب إلى /dashboard أو /، Bun يحزم تلقائيًا وسوم <script> و <link> في ملفات HTML، ويعرضها كمسارات ثابتة، ويقدم النتيجة.
مثال معالجة HTML
ملف index.html مثل هذا:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="./reset.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./sentry-and-preloads.ts"></script>
<script type="module" src="./my-app.tsx"></script>
</body>
</html>يصبح شيئًا مثل هذا:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="/index-[hash].css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/index-[hash].js"></script>
</body>
</html>تكامل React
لاستخدام React في كود جانب العميل، استورد react-dom/client وقدم تطبيقك.
import dashboard from "../public/dashboard.html";
import { serve } from "bun";
serve({
routes: {
"/": dashboard,
},
async fetch(req) {
// ...طلبات api
return new Response("hello world");
},
});import { createRoot } from 'react-dom/client';
import App from './app';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/frontend.tsx"></script>
</body>
</html>import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
);
}وضع التطوير
عند البناء محليًا، فعّل وضع التطوير عن طريق تعيين development: true في Bun.serve().
import homepage from "./index.html";
import dashboard from "./dashboard.html";
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
development: true,
fetch(req) {
// ... طلبات api
},
});ميزات وضع التطوير
عندما يكون development هو true، سيقوم Bun بـ:
- تضمين رأس SourceMap في الاستجابة حتى تتمكن devtools من عرض كود المصدر الأصلي
- تعطيل التصغير
- إعادة حزم الأصول في كل طلب لملف
.html - تفعيل إعادة تحميل الوحدة الساخنة (ما لم يتم تعيين
hmr: false) - صدى سجلات console من المتصفح إلى المحطة
تكوين التطوير المتقدم
Bun.serve() يدعم صدى سجلات console من المتصفح إلى المحطة.
لتفعيل هذا، مرر console: true في كائن development في Bun.serve().
import homepage from "./index.html";
Bun.serve({
// development يمكن أن يكون أيضًا كائن.
development: {
// تفعيل إعادة تحميل الوحدة الساخنة
hmr: true,
// صدى سجلات console من المتصفح إلى المحطة
console: true,
},
routes: {
"/": homepage,
},
});عند تعيين console: true، سيقوم Bun ببث سجلات console من المتصفح إلى المحطة. يعيد هذا استخدام اتصال WebSocket الموجود من HMR لإرسال السجلات.
التطوير مقابل الإنتاج
| الميزة | التطوير | الإنتاج |
|---|---|---|
| خرائط المصدر | ✅ مفعلة | ❌ معطلة |
| التصغير | ❌ معطل | ✅ مفعل |
| إعادة التحميل الساخنة | ✅ مفعلة | ❌ معطلة |
| حزم الأصول | 🔄 في كل طلب | 💾 مخزنة |
| سجلات Console | 🖥️ المتصفح → المحطة | ❌ معطل |
| تفاصيل الخطأ | 📝 مفصلة | 🔒 دنيا |
وضع الإنتاج
إعادة التحميل الساخنة و development: true تساعدك على التكرار بسرعة، لكن في الإنتاج، يجب أن يكون خادمك سريعًا قدر الإمكان وأن يكون لديه أقل عدد ممكن من التبعيات الخارجية.
الحزمة المسبقة (موصى به)
اعتبارًا من Bun v1.2.17، يمكنك استخدام Bun.build أو bun build لحزمة تطبيقك كامل المكدس مسبقًا.
bun build --target=bun --production --outdir=dist ./src/index.tsعندما يرى حزمة Bun استيراد HTML من كود جانب الخادم، سيحزم ملفات JavaScript/TypeScript/TSX/JSX و CSS المشار إليها في كائن بيان يمكن لـ Bun.serve() استخدامه لتقديم الأصول.
import { serve } from "bun";
import index from "./index.html";
serve({
routes: { "/": index },
});الحزمة وقت التشغيل
عند إضافة خطوة بناء معقدة للغاية، يمكنك تعيين development: false في Bun.serve().
هذا سيقوم بـ:
- تفعيل التخزين المؤقت في الذاكرة للأصول المحزمة. Bun سيحزم الأصول بشكل كسول عند أول طلب لملف
.html، ويخزن النتيجة في الذاكرة حتى إعادة تشغيل الخادم. - تفعيل رؤوس
Cache-ControlورؤوسETag - تصغير ملفات JavaScript/TypeScript/TSX/JSX
import { serve } from "bun";
import homepage from "./index.html";
serve({
routes: {
"/": homepage,
},
// وضع الإنتاج
development: false,
});مسارات API
معالجات طرق HTTP
حدد نقاط نهاية API مع معالجات طرق HTTP:
import { serve } from "bun";
serve({
routes: {
"/api/users": {
async GET(req) {
// معالجة طلبات GET
const users = await getUsers();
return Response.json(users);
},
async POST(req) {
// معالجة طلبات POST
const userData = await req.json();
const user = await createUser(userData);
return Response.json(user, { status: 201 });
},
async PUT(req) {
// معالجة طلبات PUT
const userData = await req.json();
const user = await updateUser(userData);
return Response.json(user);
},
async DELETE(req) {
// معالجة طلبات DELETE
await deleteUser(req.params.id);
return new Response(null, { status: 204 });
},
},
},
});مسارات ديناميكية
استخدم معلمات URL في مساراتك:
serve({
routes: {
// معلمة واحدة
"/api/users/:id": async req => {
const { id } = req.params;
const user = await getUserById(id);
return Response.json(user);
},
// معلمات متعددة
"/api/users/:userId/posts/:postId": async req => {
const { userId, postId } = req.params;
const post = await getPostByUser(userId, postId);
return Response.json(post);
},
// مسارات البدل
"/api/files/*": async req => {
const filePath = req.params["*"];
const file = await getFile(filePath);
return new Response(file);
},
},
});معالجة الطلبات
serve({
routes: {
"/api/data": {
async POST(req) {
// تحليل جسم JSON
const body = await req.json();
// الوصول إلى الرؤوس
const auth = req.headers.get("Authorization");
// الوصول إلى معلمات URL
const { id } = req.params;
// الوصول إلى معلمات الاستعلام
const url = new URL(req.url);
const page = url.searchParams.get("page") || "1";
// إرجاع الاستجابة
return Response.json({
message: "Data processed",
page: parseInt(page),
authenticated: !!auth,
});
},
},
},
});المكونات الإضافية
مكونات Bun الإضافية للحزمة مدعومة أيضًا عند حزم المسارات الثابتة.
لتكوين المكونات الإضافية لـ Bun.serve، أضف مصفوفة plugins في قسم [serve.static] من bunfig.toml.
مكون TailwindCSS الإضافي
يمكنك استخدام TailwindCSS عن طريق تثبيت وإضافة حزمة tailwindcss ومكون bun-plugin-tailwind الإضافي.
bun add tailwindcss bun-plugin-tailwind[serve.static]
plugins = ["bun-plugin-tailwind"]هذا سيسمح لك باستخدام فئات أدوات TailwindCSS في ملفات HTML و CSS. كل ما تحتاجه هو استيراد tailwindcss في مكان ما:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="tailwindcss" />
</head>
<!-- بقية HTML... -->
</html>بدلاً من ذلك، يمكنك استيراد TailwindCSS في ملف CSS:
@import "tailwindcss";
.custom-class {
@apply bg-red-500 text-white;
}<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./style.css" />
</head>
<!-- بقية HTML... -->
</html>مكونات إضافية مخصصة
أي ملف JS أو وحدة تصدر كائن مكون إضافي صالح للحزمة (في الأساس كائن يحتوي على حقلي name و setup) يمكن وضعه داخل مصفوفة plugins:
[serve.static]
plugins = ["./my-plugin-implementation.ts"]import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "my-custom-plugin",
setup(build) {
// تنفيذ المكون الإضافي
build.onLoad({ filter: /\.custom$/ }, async args => {
const text = await Bun.file(args.path).text();
return {
contents: `export default ${JSON.stringify(text)};`,
loader: "js",
};
});
},
};
export default myPlugin;Bun سيقوم بحل وتحميل كل مكون إضافي بشكل كسول ويستخدمها لحزم مساراتك.
NOTE
هذا حاليًا في `bunfig.toml` لجعل من الممكن معرفة ثابتًا أي المكونات الإضافية قيد الاستخدام عندما نقوم في النهاية بدمج هذا مع `bun build` CLI. هذه المكونات الإضافية تعمل في واجهة برمجة تطبيقات JS لـ `Bun.build()`، لكنها غير مدعومة بعد في CLI.متغيرات البيئة المضمنة
Bun يمكنه استبدال مراجع process.env.* في JavaScript و TypeScript للواجهة الأمامية بقيمها الفعلية في وقت البناء. اضبط خيار env في bunfig.toml:
[serve.static]
env = "PUBLIC_*" # فقط متغيرات البيئة التي تبدأ بـ PUBLIC_ (موصى به)
# env = "inline" # تضمين جميع متغيرات البيئة
# env = "disable" # تعطيل استبدال متغيرات البيئة (افتراضي)ملاحظة
هذا يعمل فقط مع مراجع process.env.FOO الحرفية، وليس import.meta.env أو الوصول غير المباشر مثل const env = process.env; env.FOO.
إذا لم يتم تعيين متغير بيئة، قد ترى أخطاء وقت التشغيل مثل ReferenceError: process is not defined في المتصفح.
راجع توثيق HTML والمواقع الثابتة للحصول على مزيد من التفاصيل حول تكوين وقت البناء والأمثلة.
كيف يعمل
يستخدم Bun HTMLRewriter لمسح وسوم <script> و <link> في ملفات HTML، ويستخدمها كنقاط دخول لحزمة Bun، وينشئ حزمة محسنة لملفات JavaScript/TypeScript/TSX/JSX و CSS، ويقدم النتيجة.
خط المعالجة
1. معالجة script
- يترجم TypeScript و JSX و TSX في وسوم
<script> - يحزم التبعيات المستوردة
- ينشئ خرائط مصدر للتنقيح
- يصغر عندما لا يكون
developmentهوtrueفيBun.serve()
<script type="module" src="./counter.tsx"></script>2. معالجة
- يعالج واردات CSS ووسوم
<link> - يدمج ملفات CSS
- يعيد كتابة مسارات url والأصول لتضمين تجزئات قابلة للعنوان بالمحتوى في URLs
<link rel="stylesheet" href="./styles.css" />3. معالجة
والأصول
- يتم إعادة كتابة الروابط للأصول لتضمين تجزئات قابلة للعنوان بالمحتوى في URLs
- الأصول الصغيرة في ملفات CSS يتم تضمينها في URLs
data:، مما يقلل العدد الإجمالي لطلبات HTTP المرسلة عبر السلك
4. إعادة كتابة HTML
- يدمج جميع وسوم
<script>في وسم<script>واحد مع تجزئة قابلة للعنوان بالمحتوى في URL - يدمج جميع وسوم
<link>في وسم<link>واحد مع تجزئة قابلة للعنوان بالمحتوى في URL - يخرج ملف HTML جديد
5. التقديم
- جميع ملفات الإخراج من الحزمة يتم عرضها كمسارات ثابتة، باستخدام نفس الآلية داخليًا كما عند تمرير كائن Response إلى
staticفيBun.serve(). - هذا يعمل بشكل مشابه لكيفية معالجة
Bun.buildلملفات HTML.
مثال كامل
إليك مثال تطبيق كامل المكدس كامل:
import { serve } from "bun";
import { Database } from "bun:sqlite";
import homepage from "./public/index.html";
import dashboard from "./public/dashboard.html";
// تهيئة قاعدة البيانات
const db = new Database("app.db");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const server = serve({
routes: {
// مسارات الواجهة الأمامية
"/": homepage,
"/dashboard": dashboard,
// مسارات API
"/api/users": {
async GET() {
const users = db.query("SELECT * FROM users").all();
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
try {
const result = db.query("INSERT INTO users (name, email) VALUES (?, ?) RETURNING *").get(name, email);
return Response.json(result, { status: 201 });
} catch (error) {
return Response.json({ error: "Email already exists" }, { status: 400 });
}
},
},
"/api/users/:id": {
async GET(req) {
const { id } = req.params;
const user = db.query("SELECT * FROM users WHERE id = ?").get(id);
if (!user) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return Response.json(user);
},
async DELETE(req) {
const { id } = req.params;
const result = db.query("DELETE FROM users WHERE id = ?").run(id);
if (result.changes === 0) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return new Response(null, { status: 204 });
},
},
// نقطة نهاية فحص الصحة
"/api/health": {
GET() {
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
});
},
},
},
// تفعيل وضع التطوير
development: {
hmr: true,
console: true,
},
// احتياط للمسارات غير المطابقة
fetch(req) {
return new Response("Not Found", { status: 404 });
},
});
console.log(`🚀 Server running on ${server.url}`);<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fullstack Bun App</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/main.tsx"></script>
</body>
</html>import { createRoot } from "react-dom/client";
import { App } from "./App";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(<App />);import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export function App() {
const [users, setUsers] = useState<User[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const fetchUsers = async () => {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
};
const createUser = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email }),
});
if (response.ok) {
setName("");
setEmail("");
await fetchUsers();
} else {
const error = await response.json();
alert(error.error);
}
} catch (error) {
alert("Failed to create user");
} finally {
setLoading(false);
}
};
const deleteUser = async (id: number) => {
if (!confirm("Are you sure?")) return;
try {
const response = await fetch(`/api/users/${id}`, {
method: "DELETE",
});
if (response.ok) {
await fetchUsers();
}
} catch (error) {
alert("Failed to delete user");
}
};
useEffect(() => {
fetchUsers();
}, []);
return (
<div className="container">
<h1>User Management</h1>
<form onSubmit={createUser} className="form">
<input type="text" placeholder="Name" value={name} onChange={e => setName(e.target.value)} required />
<input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required />
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create User"}
</button>
</form>
<div className="users">
<h2>Users ({users.length})</h2>
{users.map(user => (
<div key={user.id} className="user-card">
<div>
<strong>{user.name}</strong>
<br />
<span>{user.email}</span>
</div>
<button onClick={() => deleteUser(user.id)} className="delete-btn">
Delete
</button>
</div>
))}
</div>
</div>
);
}* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #2563eb;
margin-bottom: 2rem;
}
.form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.form input {
flex: 1;
min-width: 200px;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form button {
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.form button:hover {
background: #1d4ed8;
}
.form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.users {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.user-card:last-child {
border-bottom: none;
}
.delete-btn {
padding: 0.5rem 1rem;
background: #dc2626;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #b91c1c;
}أفضل الممارسات
هيكل المشروع
my-app/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ └── UserList.tsx
│ ├── styles/
│ │ ├── globals.css
│ │ └── components.css
│ ├── utils/
│ │ └── api.ts
│ ├── App.tsx
│ └── main.tsx
├── public/
│ ├── index.html
│ ├── dashboard.html
│ └── favicon.ico
├── server/
│ ├── routes/
│ │ ├── users.ts
│ │ └── auth.ts
│ ├── db/
│ │ └── schema.sql
│ └── index.ts
├── bunfig.toml
└── package.jsonتكوين قائم على البيئة
export const config = {
development: process.env.NODE_ENV !== "production",
port: process.env.PORT || 3000,
database: {
url: process.env.DATABASE_URL || "./dev.db",
},
cors: {
origin: process.env.CORS_ORIGIN || "*",
},
};معالجة الأخطاء
export function errorHandler(error: Error, req: Request) {
console.error("Server error:", error);
if (process.env.NODE_ENV === "production") {
return Response.json({ error: "Internal server error" }, { status: 500 });
}
return Response.json(
{
error: error.message,
stack: error.stack,
},
{ status: 500 },
);
}مساعدات استجابة API
export function json(data: any, status = 200) {
return Response.json(data, { status });
}
export function error(message: string, status = 400) {
return Response.json({ error: message }, { status });
}
export function notFound(message = "Not found") {
return error(message, 404);
}
export function unauthorized(message = "Unauthorized") {
return error(message, 401);
}أمان النوع
export interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export interface CreateUserRequest {
name: string;
email: string;
}
export interface ApiResponse<T> {
data?: T;
error?: string;
}النشر
بناء الإنتاج
# البناء للإنتاج
bun build --target=bun --production --outdir=dist ./server/index.ts
# تشغيل خادم الإنتاج
NODE_ENV=production bun dist/index.jsنشر Docker
FROM oven/bun:1 as base
WORKDIR /usr/src/app
# تثبيت التبعيات
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
# نسخ كود المصدر
COPY . .
# بناء التطبيق
RUN bun build --target=bun --production --outdir=dist ./server/index.ts
# مرحلة الإنتاج
FROM oven/bun:1-slim
WORKDIR /usr/src/app
COPY --from=base /usr/src/app/dist ./
COPY --from=base /usr/src/app/public ./public
EXPOSE 3000
CMD ["bun", "index.js"]متغيرات البيئة
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
CORS_ORIGIN=https://myapp.comالترحيل من الأطر الأخرى
من Express + Webpack
// قبل (Express + Webpack)
app.use(express.static("dist"));
app.get("/api/users", (req, res) => {
res.json(users);
});
// بعد (Bun كامل المكدس)
serve({
routes: {
"/": homepage, // يستبدل express.static
"/api/users": {
GET() {
return Response.json(users);
},
},
},
});من مسارات API في Next.js
// قبل (Next.js)
export default function handler(req, res) {
if (req.method === 'GET') {
res.json(users);
}
}
// بعد (Bun)
"/api/users": {
GET() { return Response.json(users); }
}القيود والخطط المستقبلية
القيود الحالية
- تكامل
bun buildCLI غير متاح بعد لتطبيقات المكدس الكامل - الاكتشاف التلقائي لمسارات API غير منفذ
- تقديم جانب الخادم (SSR) غير مدمج
الميزات المخططة
- التكامل مع
bun buildCLI - التوجيه القائم على الملفات لنقاط نهاية API
- دعم SSR مدمج
- نظام مكونات إضافية محسن
NOTE
هذا عمل قيد التقدم. الميزات وواجهات برمجة التطبيقات قد تتغير مع استمرار تطور Bun.