Externals для Node.js: externalsPresets

При сборке серверных приложений на Node.js возникает важная задача: исключить встроенные модули Node.js и некоторые зависимости из итогового бандла. В браузерной среде Webpack обычно стремится включить все зависимости внутрь сборки, однако для серверного кода подобное поведение часто оказывается нежелательным.

Webpack 5 ввёл механизм externalsPresets, позволяющий быстро активировать предустановленные наборы поведения для различных платформ выполнения. Для Node.js этот механизм особенно важен, поскольку серверная среда уже содержит собственные встроенные API:

  • fs
  • path
  • http
  • crypto
  • stream
  • os
  • url
  • events

Без правильной конфигурации Webpack может пытаться обрабатывать эти модули как обычные зависимости, что приводит к ошибкам, полифилам или некорректной сборке.


Проблема без externalsPresets

Типичный серверный код:

const fs = require('fs');
const path = require('path');

console.log(fs.readFileSync(path.resolve(__dirname, 'file.txt')));

Если собрать такой проект без специальных настроек:

module.exports = {
    target: 'node'
};

Webpack может:

  • попытаться встроить некоторые модули;
  • подключить ненужные полифилы;
  • изменить поведение импортов;
  • увеличить размер бандла;
  • выдать предупреждения о невозможности разрешить зависимости.

Особенно заметны проблемы после миграции с Webpack 4 на Webpack 5, где автоматические полифилы Node.js были удалены.


Базовая конфигурация externalsPresets

Минимальная настройка для Node.js:

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    }
};

Значение:

node: true

активирует специальный режим, в котором Webpack:

  • считает встроенные модули Node.js внешними;
  • не пытается их бандлить;
  • сохраняет вызовы require;
  • корректно работает с серверной средой.

Что именно делает node: true

Конфигурация:

externalsPresets: {
    node: true
}

автоматически исключает из сборки:

require('fs')
require('path')
require('http')
require('https')
require('stream')
require('crypto')

В результирующем бандле они остаются в исходном виде:

const fs = require('fs');

Webpack не заменяет их внутренними реализациями.


Разница между target: 'node' и externalsPresets.node

Многие разработчики ошибочно считают, что target: 'node' полностью решает задачу серверной сборки.

На практике эти параметры выполняют разные функции.

target: 'node'

Определяет:

  • формат генерации кода;
  • поведение runtime;
  • особенности загрузки чанков;
  • способ обработки __dirname;
  • поддержку CommonJS.

Пример:

module.exports = {
    target: 'node'
};

externalsPresets.node

Определяет:

  • какие модули считать внешними;
  • какие встроенные API не нужно включать в бандл.

Пример:

module.exports = {
    externalsPresets: {
        node: true
    }
};

Совместное использование

Практически всегда для Node.js используется комбинация:

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    }
};

Работа с externals

externalsPresets часто используется совместно с externals.

Пример:

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    },

    externals: {
        express: 'commonjs express'
    }
};

Здесь:

  • встроенные модули Node.js исключаются автоматически;
  • express также исключается вручную.

В итоговом коде:

const express = require('express');

останется без изменений.


Использование webpack-node-externals

В серверных проектах часто применяется пакет:

npm install webpack-node-externals

Пример конфигурации:

const nodeExternals = require('webpack-node-externals');

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    },

    externals: [nodeExternals()]
};

Такой подход:

  • исключает все зависимости из node_modules;
  • оставляет только пользовательский код;
  • уменьшает размер бандла;
  • ускоряет сборку.

Как работает webpack-node-externals

Допустим, проект использует:

npm install express lodash mongoose

При конфигурации:

externals: [nodeExternals()]

Webpack не будет включать:

  • express
  • lodash
  • mongoose

в итоговый файл.

Вместо этого сохранится:

require('express')
require('lodash')
require('mongoose')

Когда исключение зависимостей полезно

Серверные приложения

На сервере зависимости уже присутствуют в node_modules.

Поэтому нет смысла:

  • упаковывать их в бандл;
  • увеличивать размер файла;
  • замедлять сборку.

CLI-приложения

Для утилит командной строки:

my-cli build

обычно важны:

  • быстрый запуск;
  • минимальный размер;
  • корректная работа с файловой системой.

Electron Main Process

Главный процесс Electron работает в Node.js-среде.

Поэтому конфигурация:

externalsPresets: {
    node: true
}

часто обязательна.


Когда externals нежелателен

Иногда зависимости необходимо встроить внутрь бандла.

Например:

  • serverless deployment;
  • AWS Lambda;
  • Cloudflare Workers;
  • single-file deployment;
  • Docker minimal image.

В подобных случаях:

externals: []

может быть предпочтительнее.


Поддержка ESM

Webpack 5 поддерживает Node.js ESM-сборки.

Пример:

module.exports = {
    target: 'node',

    experiments: {
        outputModule: true
    },

    externalsPresets: {
        node: true
    },

    output: {
        module: true
    }
};

CommonJS и ESM externals

Webpack умеет генерировать разные типы внешних импортов.

CommonJS

externals: {
    lodash: 'commonjs lodash'
}

Результат:

require('lodash')

ESM

externalsType: 'module',

externals: {
    lodash: 'lodash'
}

Результат:

import lodash from 'lodash';

Использование externalsType

Пример:

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    },

    externalsType: 'commonjs'
};

Возможные значения:

  • commonjs
  • module
  • var
  • script
  • umd
  • this
  • window

Для Node.js обычно используется:

commonjs

или

module`

Исключение встроенных модулей вручную

Без externalsPresets пришлось бы писать:

externals: {
    fs: 'commonjs fs',
    path: 'commonjs path',
    crypto: 'commonjs crypto',
    stream: 'commonjs stream',
    os: 'commonjs os'
}

При большом количестве модулей это неудобно.

externalsPresets.node автоматизирует процесс.


Поддержка node:-префиксов

Современный Node.js поддерживает синтаксис:

import fs from 'node:fs';

Webpack 5 корректно работает с такими импортами при:

externalsPresets: {
    node: true
}

Поведение с динамическими require

Пример:

const moduleName = 'fs';
const lib = require(moduleName);

Webpack анализирует такие конструкции хуже, чем статические импорты.

Использование externalsPresets.node снижает вероятность ошибочной обработки встроенных модулей.


Влияние на размер бандла

Без externals:

bundle.js → 12 MB

С externals:

bundle.js → 400 KB

Особенно заметна разница при использовании:

  • Express;
  • Prisma;
  • Mongoose;
  • AWS SDK;
  • Puppeteer.

Влияние на скорость сборки

Исключение зависимостей:

  • уменьшает граф модулей;
  • сокращает время анализа;
  • снижает потребление памяти;
  • ускоряет incremental rebuild.

Для крупных backend-проектов разница может быть существенной.


Пример production-конфигурации

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
    mode: 'production',

    target: 'node',

    entry: './src/server.js',

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'server.js'
    },

    externalsPresets: {
        node: true
    },

    externals: [nodeExternals()]
};

Использование с TypeScript

Конфигурация:

const nodeExternals = require('webpack-node-externals');

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    },

    externals: [nodeExternals()],

    module: {
        rules: [
            {
                test: /\.ts$/,
                loader: 'ts-loader'
            }
        ]
    },

    resolve: {
        extensions: ['.ts', '.js']
    }
};

Использование с Babel

module.exports = {
    target: 'node',

    externalsPresets: {
        node: true
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: 'babel-loader'
            }
        ]
    }
};

Типичные ошибки

Отсутствует target: 'node'

Ошибка:

externalsPresets: {
    node: true
}

без:

target: 'node'

может привести к браузерному runtime.


Исключение необходимых модулей

Некоторые библиотеки должны попадать в бандл.

Например:

externals: [nodeExternals({
    allowlist: ['webpack/hot/poll?100']
})]

Конфликт с bundless deployment

Если приложение запускается без node_modules, externals приведут к ошибке:

Cannot find module 'express'

Проверка результата сборки

После сборки полезно проверить:

  • остались ли require('fs');
  • не встроились ли лишние зависимости;
  • нет ли полифилов браузера;
  • корректно ли работают динамические импорты.

Анализ generated bundle

Полезно использовать:

webpack --json > stats.json

и анализаторы:

webpack-bundle-analyzer

или:

speed-measure-webpack-plugin

Отличие от node configuration section

В старых версиях Webpack использовался раздел:

node: {
    __dirname: false
}

Webpack 5 делает акцент именно на:

externalsPresets

Эти механизмы решают разные задачи.


Доступные presets

Webpack поддерживает несколько preset-наборов:

externalsPresets: {
    node: true,
    electron: true,
    electronMain: true,
    electronRenderer: true,
    web: true
}

electronMain

Для Electron main process:

externalsPresets: {
    electronMain: true
}

Webpack автоматически учитывает:

  • Node.js API;
  • Electron built-in modules.

electronRenderer

Для renderer process:

externalsPresets: {
    electronRenderer: true
}

Поведение отличается от обычного браузера, поскольку renderer может иметь доступ к Node.js API.


web

Preset:

externalsPresets: {
    web: true
}

ориентирован на браузерную среду.

Для backend-приложений он обычно не используется.


Внутренняя логика Webpack

При активации:

externalsPresets: {
    node: true
}

Webpack подключает внутренний список built-in модулей Node.js.

Среди них:

assert
buffer
child_process
cluster
crypto
dgram
dns
events
fs
http
https
net
os
path
stream
tls
url
util
worker_threads
zlib

Все они рассматриваются как external dependency.


Практическая архитектура backend-сборок

Типичная современная серверная конфигурация:

const nodeExternals = require('webpack-node-externals');

module.exports = {
    mode: 'production',

    target: 'node',

    externalsPresets: {
        node: true
    },

    externals: [nodeExternals()],

    optimization: {
        minimize: false
    }
};

Особенности:

  • код приложения собирается;
  • зависимости остаются внешними;
  • built-in API Node.js не бандлятся;
  • итоговый файл остаётся компактным;
  • сохраняется высокая скорость запуска.