Файлы с расширением .node представляют собой нативные
бинарные дополнения для среды Node.js. По сути это динамические
библиотеки, скомпилированные из C, C++ или Rust и подключаемые через
механизм require().
Пример подключения:
const nativeAddon = require('./build/Release/addon.node');
Webpack по умолчанию ориентирован на работу с JavaScript, JSON, WASM и ассетами. Нативные бинарники не анализируются как обычные модули и требуют отдельной настройки.
На практике .node файлы используются:
Популярные пакеты с нативными модулями:
bcryptsharpsqlite3better-sqlite3node-sassffi-napicanvas.node автоматическиWebpack анализирует граф зависимостей на этапе сборки. Для JavaScript это возможно благодаря AST-анализу. Нативный бинарный файл не содержит JS-кода, поэтому Webpack не может:
При попытке импортировать .node напрямую обычно
возникает ошибка:
Module parse failed: Unexpected character
или:
You may need an appropriate loader to handle this file type
.node файл собирается под конкретную ОС и
архитектуру:
Один бинарник нельзя использовать на всех платформах.
Node.js использует ABI-интерфейс. Бинарник, собранный под одну версию Node.js, может не работать в другой версии.
Пример ошибки:
Module version mismatch
Нативный модуль может требовать:
Webpack не управляет такими зависимостями.
.node работает только внутри Node.js или Electron.
В браузере такие файлы не исполняются.
node-loaderНаиболее распространённый способ работы с .node —
загрузчик node-loader.
Установка:
npm install node-loader --save-dev
Конфигурация:
module.exports = {
target: 'node',
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader'
}
]
}
};
node-loadernode-loader:
process.dlopen().Итоговый код примерно эквивалентен:
process.dlopen(module, binaryPath);
project/
├── src/
│ └── index.js
├── native/
│ └── addon.node
├── webpack.config.js
└── package.json
import addon from '../native/addon.node';
console.log(addon.sum(2, 3));
const path = require('path');
module.exports = {
mode: 'production',
target: 'node',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'server.js'
},
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader'
}
]
}
};
target: 'node'Webpack может собирать проекты для:
Нативные модули требуют серверной среды.
Без target: 'node' Webpack попытается:
Это приведёт к неработоспособному bundle.
externalsВо многих случаях .node вообще не нужно включать в
bundle.
Гораздо безопаснее оставить пакет внешней зависимостью.
Пример:
module.exports = {
target: 'node',
externals: {
sharp: 'commonjs sharp'
}
};
Тогда:
externalsexternals предпочтительнее, если:
node_modules;node-loadernode-loader полезен:
Electron активно использует нативные модули:
Конфигурация:
module.exports = {
target: 'electron-main',
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader'
}
]
}
};
.nodeВ renderer-процессе ситуация сложнее.
Если включён:
contextIsolation: true
или:
sandbox: true
нативные модули могут быть недоступны напрямую.
Часто используется схема:
Renderer
↓ IPC
Main Process
↓
Native Module
@vercel/webpack-asset-relocator-loaderНекоторые нативные библиотеки динамически загружают бинарники.
Например:
require(path.join(__dirname, 'binding.node'));
Webpack не способен корректно обработать такие вызовы статически.
Для решения используется:
npm install @vercel/webpack-asset-relocator-loader --save-dev
Конфигурация:
module.exports = {
module: {
rules: [
{
test: /\.(m?js|node)$/,
parser: {
amd: false
},
use: {
loader: '@vercel/webpack-asset-relocator-loader',
options: {
outputAssetBase: 'native_modules'
}
}
}
]
}
};
Лоадер:
require;.node;Особенно полезен для Electron Builder.
Иногда используется связка:
rules: [
{
test: /\.node$/,
use: 'node-loader'
},
{
test: /\.(m?js|node)$/,
parser: {
amd: false
},
use: {
loader: '@vercel/webpack-asset-relocator-loader'
}
}
]
Очень распространённая проблема — потеря путей после сборки.
Исходный код:
const addon = require('./build/Release/addon.node');
После bundling структура директорий меняется.
В результате:
Cannot find module addon.node
__dirnameДля Node.js:
const path = require('path');
const addon = require(
path.join(__dirname, 'addon.node')
);
__dirnameWebpack может заменить __dirname.
Для Node.js рекомендуется:
module.exports = {
node: {
__dirname: false
}
};
В Webpack 5 поведение изменилось, и настройка применяется реже, но в legacy-проектах проблема остаётся актуальной.
Плохо:
require('./addons/' + name + '.node');
Webpack не может определить зависимости во время сборки.
Лучше:
const addons = {
sqlite: require('./addons/sqlite.node'),
crypto: require('./addons/crypto.node')
};
resolve.extensionsИногда добавляют:
resolve: {
extensions: ['.js', '.json', '.node']
}
Это позволяет:
require('./addon');
вместо:
require('./addon.node');
Многие библиотеки поставляют готовые бинарники:
prebuilds/
├── win32-x64/
├── linux-x64/
└── darwin-arm64/
Webpack может случайно включить лишние платформы.
Это увеличивает размер bundle в десятки раз.
Полезно использовать:
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/linux-arm64$/,
});
или:
new webpack.ContextReplacementPlugin(
/prebuilds/,
path.resolve(__dirname, 'prebuilds/win32-x64')
);
.nodeTree shaking не работает с бинарными модулями.
Webpack не знает:
Любой .node рассматривается как opaque binary blob.
Нативные модули могут участвовать в lazy loading.
Пример:
async function loadAddon() {
return import('./addon.node');
}
Но фактическая выгода ограничена:
Source maps для .node отсутствуют.
Отладка обычно ведётся:
Современные нативные модули всё чаще используют Node-API.
Преимущества:
Старый подход:
#include <nan.h>
Минусы:
Современный подход:
#include <node_api.h>
Плюсы:
node-gypWebpack не компилирует .node.
Сборка выполняется отдельно:
node-gyp rebuild
или:
npm install
Webpack работает уже с готовым бинарником.
C++ Source
↓
node-gyp
↓
addon.node
↓
Webpack
↓
Bundle
Нативные модули часто ломают CI:
Многие prebuild-бинарники рассчитаны на glibc.
В Alpine используется musl.
Частая ошибка:
Error loading shared library
Решения:
.nodeРекомендуется multi-stage build:
FROM node:22 AS build
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app .
CMD ["node", "dist/server.js"]
AWS Lambda и другие serverless-платформы требуют:
Сборка под Windows и запуск в Lambda почти всегда приводят к ошибкам.
Некоторые пакеты содержат optional native bindings.
Пример:
try {
module.exports = require('./native.node');
} catch {
module.exports = require('./fallback.js');
}
Webpack может выдавать предупреждения.
Подавление:
ignoreWarnings: [
{
module: /native\.node/
}
]
Многие библиотеки имеют два режима:
Пример:
let binding;
try {
binding = require('./native.node');
} catch {
binding = require('./fallback.js');
}
module.exports = binding;
Это улучшает переносимость.
Иногда выбирается бинарник вручную:
const platform = process.platform;
const arch = process.arch;
Пример:
const addon = require(
`./prebuilds/${platform}-${arch}/addon.node`
);
Такие конструкции плохо анализируются Webpack.
Настройка:
{
test: /\.node$/,
loader: 'node-loader',
options: {
name: '[name].[ext]'
}
}
или:
options: {
name: 'native/[contenthash].[ext]'
}
.nodeWebpack 5 поддерживает Asset Modules, но .node не
является обычным asset.
Использование:
{
test: /\.node$/,
type: 'asset/resource'
}
обычно недостаточно.
Файл скопируется, но корректная загрузка через dlopen не
произойдёт автоматически.
Нативные модули могут значительно ускорять:
Но присутствуют издержки:
node -p process.versions.modules
Linux:
file addon.node
Windows:
dumpbin /headers addon.node
Linux:
ldd addon.node
macOS:
otool -L addon.node
Причина:
Причина:
Причина:
Причина:
Наиболее стабильный вариант:
externals: {
sqlite3: 'commonjs sqlite3'
}
Обычно используются:
node-loaderasset-relocator-loaderЛучше выбирать библиотеки:
Желательно:
Полезно фиксировать:
Node.js version
npm version
architecture
platform
ABI