توفر 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() (كتابة وقت الحزمة إلى ملف).
NOTE
ردود اتصال `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. الـ filter هو تعبير منتظم يتم تشغيله على سلسلة الاستيراد. في الواقع، يسمح لك ذلك بتصفية الوحدات التي سيتم تطبيق منطق الحل المخصص عليها.
الوسيط الثاني لـ onResolve() هو رد اتصال يتم تشغيله لكل استيراد وحدة تجده Bun يتطابق مع المرشح ومساحة الاسم المحددة في الوسيط الأول.
يتلقى رد الاتصال كمدخل المسار إلى الوحدة المطابقة. يمكن لرد الاتصال إرجاع مسار جديد للوحدة. ستقرأ 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: "مكون إضافي للبيئة",
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",
};
});
},
});المكونات الإضافية الأصلية
أحد الأسباب التي تجعل حزمة 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.
كمدخل، يتلقى محتويات الملف ويمكنه اختياريًا إرجاع كود مصدر جديد.