حل الوحدات النمطية في JavaScript هو موضوع معقد.
النظام البيئي حاليًا في خضم انتقال طويل الأمد من وحدات CommonJS إلى وحدات ES الأصلية. يفرض TypeScript مجموعة خاصة به من القواعد حول امتدادات الاستيراد التي لا تتوافق مع ESM. تدعم أدوات البناء المختلفة إعادة تعيين المسارات عبر آليات غير متوافقة متباينة.
يهدف Bun إلى توفير نظام حل وحدات نمطية متسق ويمكن التنبؤ به يعمل فقط. لسوء الحظ لا يزال معقدًا إلى حد ما.
الصيغة
ضع في اعتبارك الملفات التالية.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}عند تشغيل index.ts، يطبع "Hello world!".
bun index.ts
Hello world!في هذه الحالة، نستورد من ./hello، مسار نسبي بدون امتداد. الاستيرادات ذات الامتدادات اختيارية ولكنها مدعومة. لحل هذا الاستيراد، سيتحقق Bun من الملفات التالية بالترتيب:
./hello.tsx./hello.jsx./hello.ts./hello.mjs./hello.js./hello.cjs./hello.json./hello/index.tsx./hello/index.jsx./hello/index.ts./hello/index.mjs./hello/index.js./hello/index.cjs./hello/index.json
يمكن أن تتضمن مسارات الاستيراد امتدادات بشكل اختياري. إذا كان الامتداد موجودًا، فسيبحث Bun فقط عن ملف بهذا الامتداد الدقيق.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // هذا يعملإذا قمت بالاستيراد from "*.js{x}"، فسيبحث Bun أيضًا عن ملف *.ts{x} مطابق، ليكون متوافقًا مع دعم وحدة ES الخاص بـ TypeScript.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // هذا يعمل
import { hello } from "./hello.js"; // هذا يعمل أيضًايدعم Bun كلاً من وحدات ES (صيغة import/export) ووحدات CommonJS (require()/module.exports). سيعمل إصدار CommonJS التالي أيضًا في Bun.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;مع ذلك، يُنصح بعدم استخدام CommonJS في المشاريع الجديدة.
أنظمة الوحدات النمطية
يدعم Bun وحدات CommonJS و ESModules بشكل أصلي. وحدات ES هي تنسيق الوحدة الموصى به للمشاريع الجديدة، لكن وحدات CommonJS لا تزال مستخدمة على نطاق واسع في نظام Node.js البيئي.
في وقت تشغيل JavaScript الخاص بـ Bun، يمكن استخدام require بواسطة كل من وحدات ESModules ووحدات CommonJS. إذا كانت الوحدة المستهدفة هي وحدة ES، ترجع require كائن مساحة اسم الوحدة (ما يعادل import * as). إذا كانت الوحدة المستهدفة هي وحدة CommonJS، ترجع require كائن module.exports (كما في Node.js).
| نوع الوحدة | require() | import * as |
|---|---|---|
| وحدة ES | مساحة اسم الوحدة | مساحة اسم الوحدة |
| CommonJS | module.exports | default هو module.exports، مفاتيح module.exports هي تصديرات مسماة |
استخدام require()
يمكنك require() أي ملف أو حزمة، حتى ملفات .ts أو .mjs.
const { foo } = require("./foo"); // الامتدادات اختيارية
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");ما هي وحدة CommonJS؟">
في عام 2016، أضاف ECMAScript دعم وحدات ES. وحدات ES هي المعيار لوحدات JavaScript. ومع ذلك، لا تزال ملايين حزم npm تستخدم وحدات CommonJS.
وحدات CommonJS هي وحدات تستخدم module.exports لتصدير القيم. عادةً، يتم استخدام require لاستيراد وحدات CommonJS.
const stuff = require("./stuff");
module.exports = { stuff };أكبر فرق بين CommonJS ووحدات ES هو أن وحدات CommonJS متزامنة، بينما وحدات ES غير متزامنة. هناك فروق أخرى أيضًا.
- وحدات ES تدعم
awaitعلى المستوى العلوي ووحدات CommonJS لا تدعمه. - وحدات ES دائمًا في الوضع الصارم، بينما وحدات CommonJS ليست كذلك.
- المتصفحات لا تدعم وحدات CommonJS بشكل أصلي، لكنها تدعم وحدات ES بشكل أصلي عبر
<script type="module">. - وحدات CommonJS لا يمكن تحليلها ثابتًا، بينما وحدات ES تسمح فقط بالاستيرادات والتصدير الثابت.
وحدات CommonJS: هي نوع من نظام الوحدات المستخدم في JavaScript. إحدى الميزات الرئيسية لوحدات CommonJS هي أنها تُحمّل وتُنفذ بشكل متزامن. هذا يعني أنه عند استيراد وحدة CommonJS، يتم تشغيل الكود في تلك الوحدة فورًا، وينتظر برنامجك حتى ينتهي قبل الانتقال إلى المهمة التالية. يشبه ذلك قراءة كتاب من البداية إلى النهاية دون تخطي الصفحات.
وحدات ES (ESM): هي نوع آخر من نظام الوحدات تم تقديمه في JavaScript. لديها سلوك مختلف قليلاً مقارنة بـ CommonJS. في ESM، الاستيرادات الثابتة (الاستيرادات التي تتم باستخدام عبارات import) متزامنة، تمامًا مثل CommonJS. هذا يعني أنه عند استيراد ESM باستخدام عبارة import عادية، يتم تشغيل الكود في تلك الوحدة فورًا، ويستمر برنامجك خطوة بخطوة. فكر في الأمر مثل قراءة كتاب صفحة تلو الأخرى.
الاستيرادات الديناميكية: الآن، هنا يأتي الجزء الذي قد يكون مربكًا. تدعم وحدات ES أيضًا استيراد الوحدات في اللحظة عبر الدالة import(). هذا يسمى "استيراد ديناميكي" وهو غير متزامن، لذا لا يمنع تنفيذ البرنامج الرئيسي. بدلاً من ذلك، يجلب ويحمّل الوحدة في الخلفية بينما يستمر برنامجك في التشغيل. بمجرد أن تصبح الوحدة جاهزة، يمكنك استخدامها. هذا مثل الحصول على معلومات إضافية من كتاب بينما لا تزال تقرأه، دون الحاجة إلى إيقاف قراءتك.
باختصار:
- وحدات CommonJS ووحدات ES الثابتة (عبارات
import) تعملان بطريقة متزامنة متشابهة، مثل قراءة كتاب من البداية إلى النهاية. - توفر وحدات ES أيضًا خيار استيراد الوحدات بشكل غير متزامن باستخدام الدالة
import(). هذا مثل البحث عن معلومات إضافية في منتصف قراءة الكتاب دون التوقف.
استخدام import
يمكنك import أي ملف أو حزمة، حتى ملفات .cjs.
import { foo } from "./foo"; // الامتدادات اختيارية
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";استخدام import و require() معًا
في Bun، يمكنك استخدام import أو require في نفس الملف - كلاهما يعمل طوال الوقت.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");await على المستوى العلوي
الاستثناء الوحيد لهذه القاعدة هو await على المستوى العلوي. لا يمكنك require() ملف يستخدم await على المستوى العلوي، لأن دالة require() متزامنة بطبيعتها.
لحسن الحظ، عدد قليل جدًا من المكتبات تستخدم await على المستوى العلوي، لذا نادرًا ما تكون هذه مشكلة. لكن إذا كنت تستخدم await على المستوى العلوي في كود التطبيق الخاص بك، فتأكد من عدم require() هذا الملف من مكان آخر في تطبيقك. بدلاً من ذلك، يجب استخدام import أو import() الديناميكي.
استيراد الحزم
يطبق Bun خوارزمية حل الوحدات النمطية الخاصة بـ Node.js، لذا يمكنك استيراد الحزم من node_modules باستخدام محدد مجرد.
import { stuff } from "foo";تم توثيق المواصفات الكاملة لهذه الخوارزمية رسميًا في توثيق Node.js؛ لن نكررها هنا. باختصار: إذا قمت بالاستيراد from "foo"، يمسح Bun نظام الملفات لأعلى للبحث عن دليل node_modules يحتوي على الحزمة foo.
NODE_PATH
يدعم Bun NODE_PATH لأدلة حل الوحدات النمطية الإضافية:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";تستخدم المسارات المتعددة فاصل المنصة (: في Unix، ; في Windows):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # Windowsبمجرد العثور على حزمة foo، يقرأ Bun package.json لتحديد كيفية استيراد الحزمة. لتحديد نقطة دخول الحزمة، يقرأ Bun أولاً حقل exports ويتحقق من الشروط التالية.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // إذا كان المستورد CommonJS
"import": "./index.mjs", // إذا كان المستورد وحدة ES
"default": "./index.js"
}
}أيًا من هذه الشروط يحدث أولاً في package.json يُستخدم لتحديد نقطة دخول الحزمة.
يحترم Bun المسارات الفرعية "exports" و "imports".
{
"name": "foo",
"exports": {
".": "./index.js"
}
}تعمل استيرادات المسارات الفرعية والاستيرادات الشرطية معًا.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}كما في Node.js، تحديد أي مسار فرعي في خريطة "exports" سيمنع استيراد المسارات الفرعية الأخرى؛ يمكنك فقط استيراد الملفات المصدرة صراحةً. بالنظر إلى package.json أعلاه:
import stuff from "foo"; // هذا يعمل
import stuff from "foo/index.mjs"; // هذا لا يعملNOTE
**نشر TypeScript** — لاحظ أن Bun يدعم شرط التصدير `"bun"` الخاص. إذا كانت مكتبتك مكتوبة بـ TypeScript، يمكنك نشر ملفات TypeScript الخاصة بك (غير المحولة!) إلى `npm` مباشرةً. إذا حددت نقطة دخول `*.ts` لحزمتك في شرط `"bun"`، فسيقوم Bun باستيراد وتنفيذ ملفات مصدر TypeScript الخاصة بك مباشرةً.إذا لم يتم تعريف exports، يعود Bun إلى "module" (استيرادات ESM فقط) ثم "main".
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}شروط مخصصة
يسمح لك العلم --conditions بتحديد قائمة من الشروط لاستخدامها عند حل الحزم من package.json "exports".
هذا العلم مدعوم في كل من bun build ووقت تشغيل Bun.
# استخدمه مع bun build:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# استخدمه مع وقت تشغيل bun:
bun --conditions="react-server" ./app/foo/route.jsيمكنك أيضًا استخدام conditions برمجيًا مع Bun.build:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});إعادة تعيين المسارات
يدعم Bun إعادة تعيين مسارات الاستيراد من خلال compilerOptions.paths الخاص بـ TypeScript في tsconfig.json، والذي يعمل بشكل جيد مع المحررين. إذا لم تكن مستخدم TypeScript، يمكنك تحقيق نفس السلوك باستخدام jsconfig.json في جذر مشروعك.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // تعيين المحدد إلى الملف
"components/*": ["components/*"] // مطابقة البدل
}
}
}يدعم Bun أيضًا استيرادات المسارات الفرعية بنمط Node.js في package.json، حيث يجب أن تبدأ المسارات المعينة بـ #. هذا النهج لا يعمل بشكل جيد مع المحررين، ولكن يمكن استخدام كلا الخيارين معًا.
{
"imports": {
"#config": "./config.ts", // تعيين المحدد إلى الملف
"#components/*": "./components/*" // مطابقة البدل
}
}تفاصيل منخفضة المستوى لـ CommonJS interop في Bun">
يدعم وقت تشغيل JavaScript الخاص بـ Bun وحدات CommonJS بشكل أصلي. عندما يكتشف المحول البرمجي لـ JavaScript استخدامات module.exports، يعامل الملف كـ CommonJS. سيقوم محمّل الوحدة بعد ذلك بتغليف الوحدة المحولة في دالة بهذا الشكل:
(function (module, exports, require) {
// الوحدة المحولة
})(module, exports, require);module و exports و require تشبه إلى حد كبير module و exports و require في Node.js. يتم تعيين هذه عبر with scope في C++. تخزن Map داخلية كائن exports للتعامل مع استدعاءات require الدائرية قبل تحميل الوحدة بالكامل.
بمجرد تقييم وحدة CommonJS بنجاح، يتم إنشاء سجل وحدة اصطناعي مع تصدير وحدة ES default مضبوط على module.exports ويتم إعادة تصدير مفاتيح كائن module.exports كتصدير مسماة (إذا كان كائن module.exports كائنًا).
عند استخدام أداة تجميع Bun، يعمل هذا بشكل مختلف. ستقوم أداة التجميع بتغليف وحدة CommonJS في دالة require_${moduleName} ترجع كائن module.exports.
import.meta
كائن import.meta هو طريقة للوحدة للوصول إلى معلومات حول نفسها. هو جزء من لغة JavaScript، لكن محتوياته ليست موحدة. كل "مضيف" (متصفح، وقت تشغيل، إلخ) حر في تنفيذ أي خصائص يرغب فيها على كائن import.meta.
يطبق Bun الخصائص التالية.
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"
import.meta.main; // `true` إذا تم تنفيذ هذا الملف مباشرة بواسطة `bun run`
// `false` خلاف ذلك
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| الخاصية | الوصف |
|---|---|
import.meta.dir | المسار المطلق للدليل الذي يحتوي على الملف الحالي، مثل /path/to/project. يعادل __dirname في وحدات CommonJS (و Node.js) |
import.meta.dirname | اسم مستعار لـ import.meta.dir، لتوافق Node.js |
import.meta.env | اسم مستعار لـ process.env. |
import.meta.file | اسم الملف الحالي، مثل index.tsx |
import.meta.path | المسار المطلق للملف الحالي، مثل /path/to/project/index.ts. يعادل __filename في وحدات CommonJS (و Node.js) |
import.meta.filename | اسم مستعار لـ import.meta.path، لتوافق Node.js |
import.meta.main | يشير إلى ما إذا كان الملف الحالي هو نقطة الدخول لعملية bun الحالية. هل يتم تنفيذ الملف مباشرة بواسطة bun run أم يتم استيراده؟ |
import.meta.resolve | حل محدد وحدة (مثل "zod" أو "./file.tsx") إلى عنوان url. يعادل import.meta.resolve في المتصفحات. مثال: import.meta.resolve("zod") ترجع "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | عنوان url string للملف الحالي، مثل file:///path/to/project/index.ts. يعادل import.meta.url في المتصفحات |