يوفر Bun واجهة برمجة تطبيقات إضافية عالمية يمكن استخدامها لتوسيع كل من وقت التشغيل و_أداة الحزمة_.
تعتراض الإضافات عمليات الاستيراد وتنفذ منطق تحميل مخصص: قراءة الملفات، تحويل الكود، إلخ. يمكن استخدامها لإضافة دعم لأنواع ملفات إضافية، مثل .scss أو .yaml. في سياق أداة حزمة Bun، يمكن استخدام الإضافات لتنفيذ ميزات على مستوى الإطار مثل استخراج CSS، والماكرو، وتحديد موقع كود العميل والخادم معًا.
خطافات دورة الحياة
يمكن للإضافات تسجيل دوال استدعاء ليتم تشغيلها في نقاط مختلفة خلال دورة حياة الحزمة:
onStart(): يتم تشغيلها بمجرد أن تبدأ أداة الحزمة حزمةonResolve(): يتم تشغيلها قبل حل الوحدة النمطيةonLoad(): يتم تشغيلها قبل تحميل الوحدة النمطية.onBeforeParse(): تشغيل إضافات أصلية بدون نسخ في خيط المحلل قبل تحليل الملف.
مرجع
نظرة عامة تقريبية على الأنواع (يرجى الرجوع إلى bun.d.ts الخاص بـ Bun للحصول على تعريفات الأنواع الكاملة):
type PluginBuilder = {
onStart(callback: () => void): void;
onResolve: (
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string; importer: string }) => {
path: string;
namespace?: string;
} | void,
) => void;
onLoad: (
args: { filter: RegExp; namespace?: string },
defer: () => Promise<void>,
callback: (args: { path: string }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
) => void;
config: BuildConfig;
};
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "file"
| "napi"
| "wasm"
| "text"
| "css"
| "html";الاستخدام
يتم تعريف الإضافة ككائن JavaScript بسيط يحتوي على خاصية name ودالة setup.
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "محمل مخصص",
setup(build) {
// التنفيذ
},
};يمكن تمرير هذه الإضافة إلى مصفوفة plugins عند استدعاء Bun.build.
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
plugins: [myPlugin],
});دورة حياة الإضافة
مساحات الأسماء
تقبل onLoad و onResolve سلسلة namespace اختيارية. ما هي مساحة الاسم؟
كل وحدة نمطية لها مساحة اسم. تُستخدم مساحات الأسماء كبادئة للاستيراد في الكود المحول؛ على سبيل المثال، محمل مع filter: /\.yaml$/ و namespace: "yaml:" سيحول استيراد من ./myfile.yaml إلى yaml:./myfile.yaml.
مساحة الاسم الافتراضية هي "file" وليس من الضروري تحديدها، على سبيل المثال: import myModule from "./my-module.ts" هو نفسه import myModule from "file:./my-module.ts".
مساحات الأسماء الشائعة الأخرى هي:
"bun": لوحدات Bun المحددة (مثل"bun:test"،"bun:sqlite")"node": لوحدات Node.js (مثل"node:fs"،"node:path")
onStart
onStart(callback: () => void): Promise<void> | void;تسجل دالة استدعاء ليتم تشغيلها عندما تبدأ أداة الحزمة حزمة جديدة.
import { plugin } from "bun";
plugin({
name: "مثال onStart",
setup(build) {
build.onStart(() => {
console.log("بدأت الحزمة!");
});
},
});يمكن لدالة الاستدعاء إرجاع Promise. بعد تهيئة عملية الحزمة، تنتظر أداة الحزمة حتى تكتمل جميع دوال استدعاء onStart() قبل المتابعة.
على سبيل المثال:
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "النوم لمدة 10 ثوانٍ",
setup(build) {
build.onStart(async () => {
await Bun.sleep(10_000);
});
},
},
{
name: "تسجيل وقت الحزمة إلى ملف",
setup(build) {
build.onStart(async () => {
const now = Date.now();
await Bun.$`echo ${now} > bundle-time.txt`;
});
},
},
],
});في المثال أعلاه، سينتظر Bun حتى يكتمل أول onStart() (النوم لمدة 10 ثوانٍ)، وكذلك ثاني onStart() (كتابة وقت الحزمة إلى ملف).
لاحظ أن دوال استدعاء onStart() (مثل كل دالة استدعاء أخرى لدورة الحياة) لا تملك القدرة على تعديل كائن build.config. إذا كنت تريد تغيير build.config، يجب عليك القيام بذلك مباشرة في دالة setup().
onResolve
onResolve(
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string; importer: string }) => {
path: string;
namespace?: string;
} | void,
): void;لحزمة مشروعك، ينزل Bun في شجرة الاعتماديات لجميع الوحدات النمطية في مشروعك. لكل وحدة نمطية مستوردة، يجب على Bun في الواقع العثور على وقراءة تلك الوحدة النمطية. يُعرف جزء "العثور" باسم "حل" الوحدة النمطية.
تسمح لك دالة استدعاء دورة حياة الإضافة onResolve() بتكوين كيفية حل الوحدة النمطية.
الوسيطة الأولى لـ onResolve() هي كائن يحتوي على خاصية filter و namespace. الفلتر هو تعبير نمطي يتم تشغيله على سلسلة الاستيراد. في الواقع، تسمح لك هذه بتصفية الوحدات النمطية التي سيتم تطبيق منطق الحل المخصص عليها.
الوسيطة الثانية لـ onResolve() هي دالة استدعاء يتم تشغيلها لكل استيراد وحدة نمطية يجدها Bun يتطابق مع filter و namespace المحددين في الوسيطة الأولى.
تستقبل دالة الاستدعاء كمدخل مسار الوحدة النمطية المطابقة. يمكن لدالة الاستدعاء إرجاع مسار جديد للوحدة النمطية. سيقرأ Bun محتويات المسار الجديد ويحللها كوحدة نمطية.
على سبيل المثال، إعادة توجيه جميع الاستيرادات إلى images/ إلى ./public/images/:
import { plugin } from "bun";
plugin({
name: "مثال onResolve",
setup(build) {
build.onResolve({ filter: /.*/, namespace: "file" }, args => {
if (args.path.startsWith("images/")) {
return {
path: args.path.replace("images/", "./public/images/"),
};
}
});
},
});onLoad
onLoad(
args: { filter: RegExp; namespace?: string },
defer: () => Promise<void>,
callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;بعد أن يحل أداة حزمة Bun وحدة نمطية، تحتاج إلى قراءة محتويات الوحدة النمطية وتحليلها.
تسمح لك دالة استدعاء دورة حياة الإضافة onLoad() بتعديل محتويات وحدة نمطية قبل أن يقرأها Bun ويحللها.
مثل onResolve()، تسمح لك الوسيطة الأولى لـ onLoad() بتصفية الوحدات النمطية التي سيتم تطبيق هذه الاستدعاء لـ onLoad() عليها.
الوسيطة الثانية لـ onLoad() هي دالة استدعاء يتم تشغيلها لكل وحدة نمطية مطابقة قبل أن يحمل Bun محتويات الوحدة النمطية في الذاكرة.
تستقبل دالة الاستدعاء كمدخل مسار الوحدة النمطية المطابقة، و مستورد الوحدة النمطية (الوحدة النمطية التي استوردت الوحدة النمطية)، و مساحة الاسم للوحدة النمطية، و نوع الوحدة النمطية.
يمكن لدالة الاستدعاء إرجاع سلسلة contents جديدة للوحدة النمطية بالإضافة إلى loader جديد.
على سبيل المثال:
import { plugin } from "bun";
const envPlugin: BunPlugin = {
name: "إضافة env",
setup(build) {
build.onLoad({ filter: /env/, namespace: "file" }, args => {
return {
contents: `export default ${JSON.stringify(process.env)}`,
loader: "js",
};
});
},
});
Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [envPlugin],
});
// import env from "env"
// env.FOO === "bar"ستحول هذه الإضافة جميع الاستيرادات من الشكل import env from "env" إلى وحدة نمطية JavaScript تُصدّر متغيرات البيئة الحالية.
.defer()
أحد الوسائط الممررة إلى دالة استدعاء onLoad هي دالة defer. ترجع هذه الدالة Promise يتم حلها عند تحميل جميع الوحدات النمطية الأخرى.
يسمح لك هذا بتأخير تنفيذ دالة استدعاء onLoad حتى يتم تحميل جميع الوحدات النمطية الأخرى.
هذا مفيد لإرجاع محتويات وحدة نمطية تعتمد على وحدات نمطية أخرى.
مثال: تتبع والإبلاغ عن الصادرات غير المستخدمة
import { plugin } from "bun";
plugin({
name: "تتبع الاستيرادات",
setup(build) {
const transpiler = new Bun.Transpiler();
let trackedImports: Record<string, number> = {};
// كل وحدة نمطية تمر عبر دالة استدعاء onLoad هذه
// ستسجل استيراداتها في `trackedImports`
build.onLoad({ filter: /\.ts/ }, async ({ path }) => {
const contents = await Bun.file(path).arrayBuffer();
const imports = transpiler.scanImports(contents);
for (const i of imports) {
trackedImports[i.path] = (trackedImports[i.path] || 0) + 1;
}
return undefined;
});
build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => {
// انتظر حتى يتم تحميل جميع الملفات، مما يضمن
// أن كل ملف يمر عبر دالة `onLoad()` أعلاه
// ويتم تتبع استيراداتها
await defer();
// إصدار JSON يحتوي على إحصائيات كل استيراد
return {
contents: `export default ${JSON.stringify(trackedImports)}`,
loader: "json",
};
});
},
});لاحظ أن دالة .defer() لديها حاليًا قيد أنها يمكن أن تُستدعى مرة واحدة فقط لكل دالة استدعاء onLoad.
الإضافات الأصلية
أحد الأسباب التي تجعل أداة حزمة Bun سريعة جدًا هي أنها مكتوبة بكود أصلي وتستفيد من تعدد الخيوط لتحميل وتحليل الوحدات النمطية بشكل متوازٍ.
ومع ذلك، أحد قيود الإضافات المكتوبة بـ JavaScript هو أن JavaScript نفسها أحادية الخيط.
الإضافات الأصلية مكتوبة كوحدات NAPI ويمكن تشغيلها على خيوط متعددة. هذا يسمح للإضافات الأصلية بالعمل بشكل أسرع بكثير من إضافات JavaScript.
بالإضافة إلى ذلك، يمكن للإضافات الأصلية تخطي العمل غير الضروري مثل تحويل UTF-8 -> UTF-16 المطلوب لتمرير السلاسل إلى JavaScript.
هذه هي خطافات دورة الحياة التالية المتاحة للإضافات الأصلية:
onBeforeParse(): تُستدعى على أي خيط قبل أن يحلل Bun الملف.
الإضافات الأصلية هي وحدات NAPI تعرض خطافات دورة الحياة كدوال C ABI.
لإنشاء إضافة أصلية، يجب تصدير دالة C ABI تطابق توقيع خطاف دورة الحياة الأصلي الذي تريد تنفيذه.
إنشاء إضافة أصلية في Rust
الإضافات الأصلية هي وحدات NAPI تعرض خطافات دورة الحياة كدوال C ABI.
لإنشاء إضافة أصلية، يجب تصدير دالة C ABI تطابق توقيع خطاف دورة الحياة الأصلي الذي تريد تنفيذه.
bun add -g @napi-rs/cli
napi newثم قم بتثبيت هذه الحزمة:
cargo add bun-native-pluginالآن، داخل ملف lib.rs، سنستخدم ماكرو bun_native_plugin::bun proc لتحديد دالة ستنفذ إضافتنا الأصلية.
إليك مثال ينفذ خطاف onBeforeParse:
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;
/// تحديد الإضافة واسمها
define_bun_plugin!("replace-foo-with-bar");
/// هنا سنفذ `onBeforeParse` بكود يستبدل جميع حدوث
/// `foo` بـ `bar`.
///
/// نستخدم ماكرو #[bun] لتوليد بعض الكود الجاهز.
///
/// وسيطة الدالة (`handle: &mut OnBeforeParse`) تخبر
/// الماكرو أن هذه الدالة تنفذ خطاف `onBeforeParse`.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
// جلب كود المصدر المدخل.
let input_source_code = handle.input_source_code()?;
// الحصول على Loader للملف
let loader = handle.output_loader();
let output_source_code = input_source_code.replace("foo", "bar");
handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX);
Ok(())
}ولاستخدامه في Bun.build():
import myNativeAddon from "./my-native-addon";
Bun.build({
entrypoints: ["./app.tsx"],
plugins: [
{
name: "my-plugin",
setup(build) {
build.onBeforeParse(
{
namespace: "file",
filter: "**/*.tsx",
},
{
napiModule: myNativeAddon,
symbol: "replace_foo_with_bar",
// external: myNativeAddon.getSharedState()
},
);
},
},
],
});onBeforeParse
onBeforeParse(
args: { filter: RegExp; namespace?: string },
callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;يتم تشغيل خطاف دورة الحياة هذا فورًا قبل أن يحلل Bun الملف.
كمدخل، يستقبل محتويات الملف ويمكنه اختياريًا إرجاع كود مصدر جديد.
يمكن استدعاء دالة الاستدعاء هذه من أي خيط وبالتالي يجب أن يكون تنفيذ وحدة napi آمنًا للخيوط.