الوحدات الماكرو هي آلية لتشغيل دوال JavaScript في وقت الحزمة. القيمة التي تُرجعها هذه الدوال يتم تضمينها مباشرة في الحزمة الخاصة بك.
كمثال توضيحي، ضع في اعتبارك هذه الدالة البسيطة التي تُرجع رقمًا عشوائيًا.
export function random() {
return Math.random();
}هذه مجرد دالة عادية في ملف عادي، لكن يمكننا استخدامها كوحدة ماكرو هكذا:
import { random } from "./random.ts" with { type: "macro" };
console.log(`رقمك العشوائي هو ${random()}`);NOTE
يتم الإشارة إلى وحدات الماكرو باستخدام صيغة سمة الاستيراد. إذا لم تكن قد رأيت هذه الصيغة من قبل، فهي مقترح TC39 من المرحلة 3 يسمح لك بإرفاق بيانات وصفية إضافية لبيانات الاستيراد.الآن سنحزم هذا الملف باستخدام bun build. سيتم طباعة الملف المحزم إلى stdout.
bun build ./cli.tsxconsole.log(`رقمك العشوائي هو ${0.6805550949689833}`);كما ترى، الكود المصدري لدالة random لا يظهر في أي مكان في الحزمة. بدلاً من ذلك، يتم تنفيذها أثناء الحزمة واستدعاء الدالة (random()) يتم استبداله بنتيجة الدالة. نظرًا لأن الكود المصدري لن يتم تضمينه أبدًا في الحزمة، يمكن لوحدات الماكرو تنفيذ عمليات مميزة بأمان مثل القراءة من قاعدة البيانات.
متى تستخدم وحدات الماكرو
إذا كان لديك عدة نصوص بناء لأشياء صغيرة حيث يكون لديك نص بناء لمرة واحدة، يمكن أن يكون تنفيذ الكود في وقت الحزمة أسهل في الصيانة. إنه يعيش مع بقية الكود الخاص بك، ويتم تشغيله مع بقية البناء، ويتم توازيه تلقائيًا، وإذا فشل، يفشل البناء أيضًا.
إذا وجدت نفسك تقوم بتشغيل الكثير من الكود في وقت الحزمة، ففكر في تشغيل خادم بدلاً من ذلك.
سمات الاستيراد
وحدات الماكرو في Bun هي بيانات استيراد تم شرحها باستخدام إما:
with { type: 'macro' }— سمة استيراد، مقترح ECMA Script من المرحلة 3assert { type: 'macro' }— تأكيد استيراد، تجسيد سابق لسمات الاستيراد تم التخلي عنه الآن (لكنه مدعوم بالفعل من قبل عدد من المتصفحات وبيئات التشغيل)
اعتبارات الأمان
يجب استيراد وحدات الماكرو صراحةً باستخدام { type: "macro" } لتنفيذها في وقت الحزمة. هذه الواردات ليس لها تأثير إذا لم يتم استدعاؤها، على عكس واردات JavaScript العادية التي قد يكون لها آثار جانبية.
يمكنك تعطيل وحدات الماكرو تمامًا عن طريق تمرير العلم --no-macros إلى Bun. ينتج خطأ بناء مثل هذا:
error: Macros are disabled
foo();
^
./hello.js:3:1 53لتقليل سطح الهجوم المحتمل للحزم الضارة، لا يمكن استدعاء وحدات الماكرو من داخل node_modules/**/*. إذا حاولت حزمة استدعاء وحدة ماكرو، سترى خطأ مثل هذا:
error: For security reasons, macros cannot be run from node_modules.
beEvil();
^
node_modules/evil/index.js:3:1 50لا يزال كود التطبيق الخاص بك يمكنه استيراد وحدات الماكرو من node_modules واستدعائها.
import { macro } from "some-package" with { type: "macro" };
macro();شرط التصدير "macro"
عند شحن مكتبة تحتوي على وحدة ماكرو إلى npm أو سجل حزمة آخر، استخدم شرط التصدير "macro" لتقديم إصدار خاص من الحزمة الخاصة بك حصريًا لبيئة الماكرو.
{
"name": "my-package",
"exports": {
"import": "./index.js",
"require": "./index.js",
"default": "./index.js",
"macro": "./index.macro.js"
}
}مع هذا التكوين، يمكن للمستخدمين استهلاك الحزمة الخاصة بك في وقت التشغيل أو في وقت الحزمة باستخدام محدد الاستيراد نفسه:
import pkg from "my-package"; // استيراد وقت التشغيل
import { macro } from "my-package" with { type: "macro" }; // استيراد الماكروسيتم حل الاستيراد الأول إلى ./node_modules/my-package/index.js، بينما سيتم حل الثاني بواسطة حزمة Bun إلى ./node_modules/my-package/index.macro.js.
التنفيذ
عندما يرى محول Bun البرمجي استيراد ماكرو، يستدعي الدالة داخل المحول باستخدام وقت تشغيل JavaScript في Bun ويحول قيمة الإرجاع من JavaScript إلى عقدة AST. يتم استدعاء دوال JavaScript هذه في وقت الحزمة، وليس وقت التشغيل.
يتم تنفيذ وحدات الماكرو بشكل متزامن في المحول البرمجي أثناء مرحلة الزيارة - قبل المكونات الإضافية وقبل أن يولد المحول البرمجي AST. يتم تنفيذها بالترتيب الذي يتم استيرادها به. سينتظر المحول البرمجي حتى ينتهي تنفيذ الماكرو قبل المتابعة. سيقوم المحول البرمجي أيضًا بانتظار أي Promise يتم إرجاعه بواسطة ماكرو.
حزمة Bun متعددة الخيوط. على هذا النحو، يتم تنفيذ وحدات الماكرو بشكل متوازٍ داخل عدة "عمال" JavaScript تم إنشاؤها.
إزالة الكود الميت
تقوم الحزمة بإزالة الكود الميت بعد تشغيل وتضمين وحدات الماكرو. لذا مع وحدة الماكرو التالية:
export function returnFalse() {
return false;
}...فإن حزمة الملف التالي ستنتج حزمة فارغة، بشرط تمكين خيار تصغير الصيغة.
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };
if (returnFalse()) {
console.log("تم إزالة هذا الكود");
}إمكانية التسلسل
يحتاج محول Bun البرمجي إلى أن يكون قادرًا على تسلسل نتيجة الماكرو حتى يمكن تضمينها في AST. جميع هياكل البيانات المتوافقة مع JSON مدعومة:
export function getObject() {
return {
foo: "bar",
baz: 123,
array: [1, 2, { nested: "value" }],
};
}يمكن أن تكون وحدات الماكرو غير متزامنة، أو تُرجع مثيلات Promise. سيقوم محول Bun البرمجي تلقائيًا بانتظار Promise وتضمين النتيجة.
export async function getText() {
return "قيمة غير متزامنة";
}ينفذ المحول البرمجي منطقًا خاصًا لتسلسل تنسيقات البيانات الشائعة مثل Response و Blob و TypedArray.
- TypedArray: يتم حلها إلى سلسلة مشفرة base64.
- Response: ستقرأ Bun
Content-Typeوتقوم بالتسلسل وفقًا لذلك؛ على سبيل المثال، سيتم تحليل Response من نوعapplication/jsonتلقائيًا إلى كائن وسيتم تضمينtext/plainكسلسلة. سيتم ترميز Responses ذات النوع غير المعترف به أو غير المحدد باستخدام base-64. - Blob: كما هو الحال مع Response، يعتمد التسلسل على خاصية
type.
نتيجة fetch هي Promise<Response>، لذا يمكن إرجاعها مباشرة.
export function getObject() {
return fetch("https://bun.com");
}الدوال ومثيلات معظم الفئات (باستثناء تلك المذكورة أعلاه) غير قابلة للتسلسل.
export function getText(url: string) {
// هذا لا يعمل!
return () => {};
}الوسائط
يمكن لوحدات الماكرو قبول المدخلات، ولكن فقط في حالات محدودة. يجب أن تكون القيمة معروفة ثابتًا. على سبيل المثال، التالي غير مسموح به:
import { getText } from "./getText.ts" with { type: "macro" };
export function howLong() {
// لا يمكن معرفة قيمة `foo` بشكل ثابت
const foo = Math.random() ? "foo" : "bar";
const text = getText(`https://example.com/${foo}`);
console.log("الصفحة طولها ", text.length, " حرف");
}ومع ذلك، إذا كانت قيمة foo معروفة في وقت الحزمة (على سبيل المثال، إذا كانت ثابتة أو نتيجة ماكرو آخر) فهذا مسموح به:
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };
export function howLong() {
// هذا يعمل لأن getFoo() معروفة ثابتًا
const foo = getFoo();
const text = getText(`https://example.com/${foo}`);
console.log("الصفحة طولها", text.length, "حرف");
}هذا ينتج:
function howLong() {
console.log("الصفحة طولها", 1322, "حرف");
}
export { howLong };أمثلة
تضمين أحدث تجزئة التزام git
export function getGitCommitHash() {
const { stdout } = Bun.spawnSync({
cmd: ["git", "rev-parse", "HEAD"],
stdout: "pipe",
});
return stdout.toString();
}عندما نبنيه، يتم استبدال getGitCommitHash بنتيجة استدعاء الدالة:
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };
console.log(`تجزئة التزام git الحالية هي ${getGitCommitHash()}`);console.log(`تجزئة التزام git الحالية هي 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);إجراء طلبات fetch() في وقت الحزمة
في هذا المثال، نجري طلب HTTP صادر باستخدام fetch()، ونحلل استجابة HTML باستخدام HTMLRewriter، ونُرجع كائنًا يحتوي على العنوان ووسوم meta - كل ذلك في وقت الحزمة.
export async function extractMetaTags(url: string) {
const response = await fetch(url);
const meta = {
title: "",
};
new HTMLRewriter()
.on("title", {
text(element) {
meta.title += element.text;
},
})
.on("meta", {
element(element) {
const name =
element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");
if (name) meta[name] = element.getAttribute("content");
},
})
.transform(response);
return meta;
}يتم مسح دالة extractMetaTags في وقت الحزمة واستبدالها بنتيجة استدعاء الدالة. هذا يعني أن طلب fetch يحدث في وقت الحزمة، والنتيجة مضمنة في الحزمة. أيضًا، يتم إزالة الفرع الذي يرمي الخطأ لأنه لا يمكن الوصول إليه.
import { extractMetaTags } from "./meta.ts" with { type: "macro" };
export const Head = () => {
const headTags = extractMetaTags("https://example.com");
if (headTags.title !== "Example Domain") {
throw new Error("من المتوقع أن يكون العنوان 'Example Domain'");
}
return (
<head>
<title>{headTags.title}</title>
<meta name="viewport" content={headTags.viewport} />
</head>
);
};export const Head = () => {
const headTags = {
title: "Example Domain",
viewport: "width=device-width, initial-scale=1",
};
return (
<head>
<title>{headTags.title}</title>
<meta name="viewport" content={headTags.viewport} />
</head>
);
};