Webpack 5.107

صدر Webpack 5.107. العنوان الأبرز في هذا الإصدار هو أول خطوة نحو التعامل مع ملفات .html بشكل أصلي داخل webpack core: أصبح استيراد ملف HTML من JavaScript يحل مراجع <img src> و<link href> ووسوم <style> الداخلية و<script src> عبر مسار webpack المعتاد، وهذا يستبدل الدور الذي كان يؤديه html-loader لسنوات. أما HTML entry points، أي الجزء الذي يغطيه html-webpack-plugin، فليست موجودة في 5.107 بعد، وهي مخطط لها في الإصدار minor التالي.

يضيف هذا الإصدار أيضًا دعمًا تجريبيًا لـ TypeScript يعتمد على ميزة Node.js المدمجة لإزالة الأنواع، بحيث يمكن للمشاريع البسيطة المكتوبة بـ TypeScript أن تُبنى بدون ts-loader أو swc-loader.

الميّزتان تجريبيتان وتحتاجان إلى تفعيل صريح، لكن الاتجاه واضح: الهدف أن تتمكن لاحقًا من بناء تطبيق ويب كامل بدون loaders أو plugins إضافية لـ HTML وCSS وTypeScript الأساسي.

إلى جانب العمل التجريبي، يواصل هذا الإصدار تحسين مسار CSS المدمج، ويضيف عدة تحسينات لـ tree shaking وdeferred imports وmodule resolution.

استكشف الجديد:

HTML Modules (Experimental)

تاريخيًا، أي إعداد webpack حقيقي يحتاج HTML entry point كان يتطلب اعتمادين إضافيين على الأقل. الأول هو html-webpack-plugin، وهو الذي يولّد أو يصدر ملف HTML ويحقن فيه روابط bundles الصحيحة. والثاني هو html-loader، وهو الذي يسمح لـ webpack بالمرور على <img src> و<link href> و<script src> وما شابه، حتى تمر هذه المراجع عبر resolver ومسار assets المعتادين.

خدم هذان الاعتمادان المجتمع لفترة طويلة، لكنهما خارج core. يبدأ Webpack 5.107 بإدخال جانب html-loader من هذا العمل إلى الداخل.

experiments.html Flag

كل ما في هذا القسم خلف flag واحد يحتاج إلى تفعيل صريح. الخيار الجديد experiments.html يسجل نوع module باسم html على NormalModuleFactory ويفعل سلوكيات HTML الموضحة أدناه.

// webpack.config.js
module.exports = {
  experiments: {
    html: true,
  },
};

بعد ذلك تستورد ملف HTML من JavaScript. يكون default export هو HTML المعالج كنص، مع حل كل مراجع assets عبر webpack:

// src/index.js
import page from "./page.html";

document.documentElement.innerHTML = page;

هذه هي الصيغة نفسها التي ينتجها html-loader، لذلك يفترض أن يستمر الكود الموجود الذي يستورد HTML بالعمل. يقوم المسار الجديد بالعمل الذي كان loader يقوم به: تُحل مراجع الوسوم، وتُعاد كتابة أسماء الملفات التي تحتوي على hash داخل نص HTML، ثم تصل النتيجة إلى JS bundle. استخدام ملف .html مباشرة كـ webpack entry غير مدعوم في 5.107.

Inline <style> Tags

عندما يجد webpack كتلة <style> داخل HTML module، يمرر جسم CSS عبر مسار CSS نفسه الذي تستخدمه لملف .css. تُعامل الكتلة كـ virtual CSS module مع exportType: "text"، لذلك تُحل مراجع url() وعبارات @import داخل style نسبة إلى ملف HTML. وبعد انتهاء المعالجة، يُكتب CSS المحوّل مرة أخرى داخل وسم <style> الأصلي في نص HTML الصادر.

<!-- src/page.html -->
<!doctype html>
<html>
  <head>
    <style>
      @import "./reset.css";

      body {
        background: url("./bg.png");
      }
    </style>
  </head>
  <body>
    ...
  </body>
</html>

تتم معالجة <style type="text/css"> و<style> بدون خاصية type. أما أي وسم يملك type غير CSS فيمر كما هو بدون تعديل. هذا يغطي سلوك inline-style الخاص بـ html-loader بدون الحاجة إليه في pipeline.

Inline <script> Tags

تُمرر أجسام وسوم <script> الداخلية عبر مسار entry نفسه الذي يتعامل مسبقًا مع <script src>. يصبح كل جسم <script> entry مستقلًا في webpack: السكربتات الداخلية الكلاسيكية تُحزم كـ CommonJS، ووسوم <script type="module"> الداخلية تُحزم كـ ESM. يعاد كتابة الوسم في HTML الناتج إلى <script src="…"> يشير إلى chunk المولد، مع إفراغ الجسم.

<!-- src/page.html -->
<!doctype html>
<html>
  <body>
    <script type="module">
      import { greet } from "./lib.js";
      greet("world");
    </script>

    <script>
      console.log("classic inline script");
    </script>
  </body>
</html>

السلوكيات نفسها التي تنطبق على <script src> الخارجية تنطبق هنا أيضًا:

  • عند تفعيل output.module، تتم ترقية وسوم <script> الكلاسيكية تلقائيًا إلى type="module"، مثل الترقية التلقائية لـ <script src>.
  • يعمل webpackIgnore على وسوم <script> الداخلية أيضًا، فيترك الجسم الأصلي كما هو.
  • قيم type غير الخاصة بـ JS مثل application/ld+json وimportmap تمر بدون تغيير.

<script src> and <link rel="modulepreload">

تتحول مراجع <script src> و<link rel="modulepreload"> داخل HTML module إلى webpack entries حقيقية، ثم يعاد كتابة URL الخاص بالـ chunk الصادر داخل نص HTML حتى تبقى أسماء الملفات التي تحتوي على hash صحيحة.

<!-- src/page.html -->
<!doctype html>
<html>
  <head>
    <link rel="modulepreload" href="./preloaded.js" />
  </head>
  <body>
    <script src="./entry.js"></script>
    <script src="./second.js"></script>
  </body>
</html>

هناك عدة سلوكيات مهمة:

  • عدة وسوم <script src> في الصفحة نفسها تشترك في runtime واحد. داخل كل مجموعة، سواء كانت classic أو type="module"، يحمل الأول runtime والبقية تعلن dependOn عليه.
  • تبقى entries الخاصة بـ <link rel="modulepreload"> مستقلة ولا يتم استيرادها من scripts مجاورة، لذلك تعمل كـ preload بدون تنفيذ، تمامًا كما تتطلب المواصفة.
  • عند تفعيل output.module، تتم ترقية وسوم <script src> الكلاسيكية تلقائيًا إلى <script type="module" src> حتى تُحمّل ES-module chunks الناتجة بالوضع الصحيح.
  • أنواع script غير JS مثل application/ld+json أو importmap، وكذلك data URIs، تمر بدون تغيير ولا تُحزم كـ JS.

webpackIgnore Magic Comment

أصبح magic comment المعروف webpackIgnore: true يعمل الآن داخل HTML modules. ضع تعليق HTML يحتوي على التوجيه قبل الوسم مباشرة، وسيترك webpack روابط ذلك الوسم كما هي في output. وهذا هو بالضبط سلوك html-loader للحالة نفسها.

<!-- webpackIgnore: true -->
<img src="https://cdn.example.com/logo.png" />

<!-- webpackIgnore: true -->
<script src="/legacy/external.js"></script>

تُقرأ قيمة التعليق بالسياق نفسه الذي تستخدمه parsers الخاصة بـ JS وCSS، لذلك القيم غير boolean ستصدر UnsupportedFeatureWarning.

TypeScript Support (Experimental)

يضيف Webpack 5.107 دعمًا مباشرًا لـ TypeScript خلف flag جديد باسم experiments.typescript. عند تفعيله، يجمّع webpack ملفات .ts و.cts و.mts مباشرة عبر module.stripTypeScriptTypes المدمجة في Node.js، بدون loader خارجي. كما يتم تفعيل flag تلقائيًا بواسطة experiments.futureDefaults.

// webpack.config.js
export default {
  experiments: {
    typescript: true,
  },
  entry: "./src/index.ts",
};

تفعيل flag يربط أيضًا defaults مناسبة: rules لملفات .ts / .cts / .mts، وإضافة .ts إلى extension resolution قبل .js، وextensionAlias حتى يجرب import "./foo.js" الملف ./foo.ts أيضًا، وحل tsconfig.json، ومفتاح conditional exports باسم "typescript" للـ monorepos التي تنشر مصادر .ts.

التحويل يزيل الأنواع فقط: لا يوجد type checking، ولا JSX / .tsx، ولا syntax غير قابلة للإزالة مثل enum وnamespace وparameter-property constructors وdecorator metadata. هذه هي القيود نفسها التي يفرضها TypeScript مع erasableSyntaxOnly. لفحص الأنواع، استخدم flag مع tsc --noEmit أو fork-ts-checker-webpack-plugin. ولـ JSX أو syntax غير قابلة للإزالة، استمر باستخدام ts-loader أو swc-loader.

راجع examples/typescript للإعداد المدمج، وexamples/typescript-non-erasable لاستخدام ts-loader كخيار fallback.

CSS Improvements

Scope Hoisting for CSS Modules

كانت module concatenation، والمعروفة أيضًا باسم scope hoisting، تحسينًا خاصًا بـ JavaScript فقط. وحتى مع تفعيل experiments.css، كانت CSS Modules الداخلة في bundle مدمج تنتج runtime instances منفصلة. ابتداءً من 5.107، ينطبق التحسين نفسه على CSS Modules التي يكون export type الخاص بها text أو css-style-sheet أو style أو link. النتيجة runtime overhead أقل وoutput أصغر في bundles التي تحتوي CSS بكثافة.

module.exports = {
  experiments: { css: true },
  optimization: {
    concatenateModules: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        type: "css/module",
        parser: {
          exportType: "css-style-sheet",
        },
      },
    ],
  },
};

Pure Mode for CSS Modules

خيار parser جديد باسم pure لـ css/module وcss/auto يحاكي strict pure mode في postcss-modules-local-by-default. عند تفعيله، يجب أن يحتوي كل selector على class أو id محلي واحد على الأقل، وإلا سيرفع webpack خطأ build. الهدف هو اكتشاف selectors العالمية غير المقصودة داخل CSS Modules قبل وصولها إلى production.

module.exports = {
  experiments: { css: true },
  module: {
    parser: {
      "css/module": {
        pure: true,
      },
    },
  },
};

يوجد تعليقان للخروج من هذا الفحص عند الحاجة. الأول يعطّل الفحص لقاعدة واحدة:

/* cssmodules-pure-ignore */
a {
  /* معطّل لهذه القاعدة فقط */
  color: blue;
}

والثاني، عند وضعه ضمن التعليقات الأولى في الملف قبل أي قاعدة، يعطّل الفحص للملف كاملًا:

/* cssmodules-pure-no-check */
/* يعطل pure mode لهذا الملف */

a {
  /* كان سيفشل عادةً تحت pure mode */
  color: red;
}

القواعد المتداخلة داخل ancestor يحتوي على local class تُعد متوافقة مع pure mode، و& يُحل إلى purity الخاصة بالـ parent rule، كما تُستثنى أجسام @keyframes و@counter-style.

@value in URLs and @import

يمكن الآن استخدام identifiers الخاصة بـ @value في CSS Modules كمسار داخل @import وداخل مراجع url(). هذا يسهل تعريف المسارات وassets المشتركة مرة واحدة وإعادة استخدامها بين stylesheets.

@value path: "./other.module.css";
@import path;

@value bg: "./image.png";

.a {
  background: url(bg);
}

تعمل الصيغ المقتبسة ("./x" و'./x') والصيغ غير المقتبسة (./x) للقيمة. أي صيغة تكتبها تُفك وتُحل كـ module request، لذلك يمر asset عبر resolver ومسار assets المعتادين بدل أن يبقى identifier حرفيًا.

Multiple Aliases via exportsConvention

أصبح شكل الدالة في generator.exportsConvention الخاص بـ CSS Modules يقبل string[] إلى جانب string. إرجاع array يصدر local class بكل اسم داخل array، بما يطابق سلوك css-loader. هذا مفيد عندما تريد توفير عدة aliases لـ class واحد، مثل الاسم الأصلي ونسخة بحروف كبيرة.

module.exports = {
  experiments: { css: true },
  module: {
    generator: {
      "css/module": {
        exportsConvention: (name) => [name, name.toUpperCase()],
      },
    },
  },
};
// الاستخدام في JS
import styles from "./button.module.css";

console.log(styles.btn); // hashed class
console.log(styles.BTN); // hashed class نفسه مع alias بحروف كبيرة

linkInsert Hook

إذا أردت يومًا التحكم في المكان الذي يضيف فيه webpack وسم <link> الخاص بـ stylesheet داخل document، أصبح لديك hook لذلك. يعرض CssLoadingRuntimeModule.getCompilationHooks(compilation) hook جديدًا باسم linkInsert. يستقبل مصدر الإدراج الافتراضي، وهو document.head.appendChild(link);، وكذلك chunk، ويرجع JS المستخدم لإرفاق link.

const webpack = require("webpack");

class MyLinkInsertPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("MyLinkInsertPlugin", (compilation) => {
      const hooks =
        webpack.web.CssLoadingRuntimeModule.getCompilationHooks(compilation);

      // استبدال `document.head.appendChild(link);` الافتراضي
      hooks.linkInsert.tap(
        "MyLinkInsertPlugin",
        (source, chunk) =>
          'link.setAttribute("data-injected", "true"); document.body.appendChild(link);',
      );
    });
  }
}

module.exports = {
  experiments: { css: true },
  plugins: [new MyLinkInsertPlugin()],
};

الـ hook من نوع SyncWaterfallHook<[string, Chunk]>. أرجع source الافتراضي للحفاظ على السلوك الأصلي، أو أرجع JS خاصًا بك لتغيير مكان إرفاق link وطريقته.

orderModules Hook

عندما يسحب chunk ملفات CSS من عدة ملفات، يكون ترتيب webpack الافتراضي topological sort لـ import graph. هذا مناسب غالبًا، لكن في مشاريع حقيقية قد يكون import graph غامضًا بما يكفي لإظهار تحذير "Conflicting order between CSS …"، بدون حل نظيف سوى إعادة هيكلة imports.

يوفر hook جديد باسم orderModules على CssModulesPlugin.getCompilationHooks(compilation) مخرجًا deterministic لمؤلفي plugins. يعمل مرة لكل نوع CSS source، أي CSS_IMPORT_TYPE وCSS_TYPE، مع modules الخاصة بالـ chunk مرتبة مسبقًا حسب full module name. أرجع Module[] مرتبة لتجاوز الترتيب الافتراضي، أو أرجع undefined للرجوع إلى topological sort الحالي الخاص بـ webpack.

const webpack = require("webpack");

class CssOrderByPathPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap(
      "CssOrderByPathPlugin",
      (compilation) => {
        const hooks =
          webpack.css.CssModulesPlugin.getCompilationHooks(compilation);

        // تصل modules مرتبة مسبقًا حسب full module name. إرجاعها كما هي
        // يعطي ترتيبًا deterministic حسب مسار الملف ويتجنب تحذير
        // conflicting order.
        hooks.orderModules.tap(
          "CssOrderByPathPlugin",
          (_chunk, modules) => modules,
        );
      },
    );
  }
}

module.exports = {
  experiments: { css: true },
  plugins: [new CssOrderByPathPlugin()],
};

الـ hook من نوع SyncBailHook<[Chunk, Module[], Compilation], Module[] | undefined>. أول tap يرجع قيمة غير undefined هو الذي يفوز.

JavaScript and ESM

Anonymous Default Export Naming

أضاف Webpack 5.106 إصلاحًا يضبط .name على "default" للـ anonymous default exports، بما يطابق مواصفة ES. يعمل هذا بشكل صحيح، لكنه يحقن استدعاءات Object.defineProperty لا يمكن تصغير أسمائها، مما يزيد حجم bundle. بالنسبة لمستهلكي المكتبات، الذين نادرًا ما يعتمدون على .name === "default"، يكون هذا runtime helper الإضافي تكلفة بلا فائدة كبيرة.

يضيف 5.107 خيارًا جديدًا باسم module.parser.javascript.anonymousDefaultExportName للتحكم في هذا السلوك. تكون قيمته الافتراضية true للتطبيقات وfalse للمكتبات عند ضبط output.library. تبقى التطبيقات مطابقة للمواصفة افتراضيًا، ويتوقف مؤلفو المكتبات عن دفع تكلفة runtime helper الإضافي بدون الحاجة لمعرفة التفاصيل.

// input
export default function () {
  /* ... */
}

// مع `anonymousDefaultExportName: true`، وهي القيمة الافتراضية للتطبيقات
// يضبط runtime قيمة .name إلى "default" لمطابقة سلوك ESM الأصلي

يمكنك تجاوز القيمة الافتراضية صراحة:

module.exports = {
  module: {
    parser: {
      javascript: {
        anonymousDefaultExportName: false,
      },
    },
  },
};

Preserving defer and source Phase on Externals

يحافظ webpack الآن على كلمات import phase defer وsource في external dependencies عند إخراج ESM، بالطريقة نفسها التي يحافظ بها على import attributes. سابقًا، كانت كلمة phase تُزال من العبارة الصادرة، لذلك كان import defer * as ns from "x" ضد external يفقد دلالة التأجيل في output.

بالنسبة لـ static module externals، تُصدر namespace defer imports وsingle-default source imports كصيغة phase أصلية في أعلى bundle:

// webpack.config.js
module.exports = {
  output: { module: true },
  externalsType: "module",
  externals: { "external-mod": "external-mod" },
};
// input
import defer * as ns from "external-mod";
import source v from "external-mod";

// emitted output
import defer * as ns from "external-mod";
import source v from "external-mod";

وبالنسبة لـ dynamic import externals، يتم إخراج import.defer("x") وimport.source("x") مباشرة:

// input
const ns = await import.defer("external-mod");
const src = await import.source("external-mod");

// emitted output
const ns = await import.defer("external-mod");
const src = await import.source("external-mod");

تحسين مرتبط: عندما يتم استيراد external نفسه بمرحلتين مختلفتين، أو بمجموعة attributes مختلفة، لم يعد يُدمج في ExternalModule واحد. كل تركيبة تنتج emit خاصًا بها، لذلك لا تضيع أي phase بصمت.

#__NO_SIDE_EFFECTS__ Annotation

يدعم webpack الآن annotation باسم #__NO_SIDE_EFFECTS__ لتعليم الدوال كـ pure من أجل tree shaking أفضل. يمكن حذف استدعاءات الدوال المعلّمة بهذه الطريقة من bundle عندما لا تُستخدم قيمة الإرجاع، حتى لو لم يكن جسم الدالة قابلًا للتحليل الثابت كـ pure.

// utils.js
/*#__NO_SIDE_EFFECTS__*/
export function createLogger(prefix) {
  return (msg) => console.log(`[${prefix}] ${msg}`);
}

export function realWork() {
  // ...
}
// app.js
import { createLogger, realWork } from "./utils";

// سيتم حذف هذا لأن `createLogger` معلّمة، ونتيجتها غير مستخدمة
const unused = createLogger("debug");

realWork();

Resolver Updates

يضيف webpack الآن "module-sync" إلى conditionNames الافتراضية في resolver defaults، بما يتوافق مع Node.js. يوفّر Node.js شرط المجتمع module-sync لـ ESM القابل للتحميل بشكل متزامن، وهذا التغيير يؤثر في resolvers الخاصة بـ ESM وCJS وAMD وworker وwasm وbuild-dependency.

بشكل عملي، أصبحت resolver defaults تتضمن module-sync مباشرة قبل module في سلسلة الشروط:

// Before (5.106)
conditionNames: ["require", "module", "..."]; // CJS deps
conditionNames: ["import", "module", "..."]; // ESM deps

// After (5.107)
conditionNames: ["require", "module-sync", "module", "..."];
conditionNames: ["import", "module-sync", "module", "..."];

هذا يعني أن الحزم التي تنشر شرط export باسم module-sync داخل package.json سيتم اختيارها تلقائيًا بدون أي إعداد إضافي:

{
  "name": "my-package",
  "exports": {
    ".": {
      "module-sync": "./esm/index.js",
      "default": "./cjs/index.js"
    }
  }
}

Bug Fixes

تم حل عدة bugs منذ الإصدار 5.106. راجع changelog لكل التفاصيل.

Thanks

شكر كبير لكل المساهمين والرعاة الذين جعلوا Webpack 5.107 ممكنًا. دعمكم، سواء عبر مساهمات الكود أو التوثيق أو الرعاية المالية، يساعد Webpack على التطور والتحسن للجميع.

Edit this page·
« Previous
المدونة

1 Contributor

RlxChap2