CLI-приложения отличаются от браузерных проектов архитектурой, способом запуска и требованиями к выходному коду. Основная цель сборки — сформировать единый исполняемый файл или компактный набор файлов, пригодных для запуска через Node.js.
Типичные особенности CLI-инструментов:
nodeWebpack способен эффективно собирать подобные приложения благодаря
режиму target: 'node'.
Для серверных приложений и CLI используется специальная цель сборки:
module.exports = {
target: 'node'
};
Этот режим изменяет поведение Webpack:
requireБез target: 'node' Webpack может попытаться внедрить
браузерные зависимости, что приведёт к ошибкам исполнения.
Пример структуры:
project/
├── src/
│ ├── cli.js
│ ├── commands/
│ └── utils/
├── dist/
├── package.json
└── webpack.config.js
Точка входа:
// src/cli.js
console.log('CLI started');
Конфигурация:
const path = require('path');
module.exports = {
mode: 'production',
target: 'node',
entry: './src/cli.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cli.js'
}
};
CLI-инструменты обычно запускаются напрямую:
mycli build
Для этого в начале файла требуется shebang:
#!/usr/bin/env node
Webpack не всегда сохраняет его автоматически. Чаще всего
используется BannerPlugin.
const webpack = require('webpack');
plugins: [
new webpack.BannerPlugin({
banner: '#!/usr/bin/env node',
raw: true
})
]
После сборки файл становится исполняемым:
chmod +x dist/cli.js
Для публикации CLI используется поле bin.
{
"name": "mycli",
"bin": {
"mycli": "./dist/cli.js"
}
}
После установки пакет автоматически создаёт системную команду.
Глобальная установка:
npm install -g
Локальный запуск:
npx mycli
Webpack понимает встроенные модули Node.js:
const fs = require('fs');
const path = require('path');
const os = require('os');
При target: 'node' они не включаются в bundle, а
остаются внешними зависимостями runtime.
Серверные приложения часто не бандлят зависимости из
node_modules.
Причины:
Пример externals:
module.exports = {
externals: {
express: 'commonjs express'
}
};
Теперь Express не попадёт в bundle.
Ручное перечисление зависимостей неудобно. Для Node.js используется
пакет webpack-node-externals.
Установка:
npm install webpack-node-externals --save-dev
Конфигурация:
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
externals: [nodeExternals()]
};
Webpack автоматически исключит содержимое
node_modules.
CLI-инструменты нередко распространяются единым файлом.
Преимущества:
В этом случае externals обычно отключают:
module.exports = {
target: 'node',
externals: []
};
Все зависимости попадают внутрь bundle.
Node.js способен выполнять минифицированный код, поэтому production-сборка остаётся актуальной.
module.exports = {
mode: 'production'
};
Webpack включает:
Tree shaking работает не только в браузере.
Пример:
// math.js
export function sum(a, b) {
return a + b;
}
export function unused() {
return 'unused';
}
import { sum } from './math';
Функция unused будет удалена из bundle.
Наиболее эффективно tree shaking работает с ES-модулями.
Node.js поддерживает оба формата:
Webpack умеет собирать оба варианта.
const fs = require('fs');
module.exports = function() {};
import fs from 'fs';
export default function() {}
Если собирается серверная библиотека, а не приложение, используется:
output: {
libraryTarget: 'commonjs2'
}
Пример:
module.exports = {
output: {
filename: 'index.js',
libraryTarget: 'commonjs2'
}
};
Webpack способен собирать полноценные серверы.
Пример Express-сервера:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello');
});
app.listen(3000);
Конфигурация:
const path = require('path');
module.exports = {
target: 'node',
mode: 'production',
entry: './src/server.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'server.js'
}
};
Запуск:
node dist/server.js
Webpack изменяет поведение Node.js-переменных.
Для сохранения оригинального поведения:
module.exports = {
node: {
__dirname: false,
__filename: false
}
};
Это особенно важно при работе:
Webpack плохо анализирует динамические пути.
Проблемный код:
require('./commands/' + name);
Компилятор не может заранее определить зависимости.
Лучший вариант:
const commands = {
build: require('./commands/build'),
serve: require('./commands/serve')
};
commands[name]();
Webpack создаёт context modules для анализа директорий.
Пример:
const context = require.context('./commands', false, /\.js$/);
Это позволяет динамически подключать файлы, сохраняя возможность статического анализа.
Code splitting работает и в серверной среде.
Пример:
async function start() {
const module = await import('./heavy-module');
module.run();
}
Webpack создаст отдельный chunk.
На сервере разделение кода используется осторожно.
Недостатки:
Для CLI-инструментов чаще используется единый bundle.
Source maps особенно важны для серверной отладки.
module.exports = {
devtool: 'source-map'
};
Ошибки будут указывать на исходный код TypeScript или ESNext.
devtool: 'eval-cheap-module-source-map'
devtool: 'source-map'
devtool: false
Webpack-dev-server ориентирован на браузер.
Для Node.js используются:
Пример:
{
"scripts": {
"dev": "webpack --watch"
}
}
Дополнительно:
nodemon dist/server.js
Webpack отслеживает изменения файлов:
webpack --watch
Преимущества:
Webpack 5 поддерживает filesystem cache.
module.exports = {
cache: {
type: 'filesystem'
}
};
Это существенно ускоряет крупные серверные проекты.
Webpack часто используется совместно с Babel.
Пример loader:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
Вместо браузеров указывается версия Node.js:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "18"
}
}
]
]
}
Это уменьшает объём трансформаций.
Webpack эффективно собирает TypeScript-серверы.
Установка:
npm install typescript ts-loader --save-dev
Конфигурация:
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader'
}
]
},
resolve: {
extensions: ['.ts', '.js']
}
};
Преимущества:
Недостатки:
Преимущества:
Недостатки:
Некоторые пакеты содержат нативный код:
Webpack может работать с ними нестабильно.
Обычно такие зависимости исключают через externals.
externals: {
sharp: 'commonjs sharp'
}
Webpack умеет импортировать JSON без loader.
import config from './config.json';
JSON включается в bundle автоматически.
Серверные приложения активно используют окружение.
Пример:
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
Частая связка:
npm install dotenv
require('dotenv').config();
Webpack подставляет значения на этапе компиляции.
Встраивание секретов в bundle опасно.
Плохой вариант:
DefinePlugin({
API_KEY: JSON.stringify(process.env.API_KEY)
})
После сборки ключ окажется внутри кода.
Для серверных приложений лучше читать переменные runtime.
CLI-инструменты чувствительны к startup latency.
Методы оптимизации:
Используется webpack-bundle-analyzer.
Установка:
npm install webpack-bundle-analyzer --save-dev
Конфигурация:
const { BundleAnalyzerPlugin } =
require('webpack-bundle-analyzer');
plugins: [
new BundleAnalyzerPlugin()
]
Инструмент показывает:
Webpack может собирать несколько targets одновременно.
module.exports = [
{
target: 'node',
entry: './src/server.js'
},
{
target: 'node',
entry: './src/cli.js'
}
];
Типичная структура:
webpack/
├── common.js
├── dev.js
└── prod.js
Объединение:
const { merge } = require('webpack-merge');
Серверные bundle удобно использовать внутри контейнеров.
Преимущества:
Webpack особенно полезен для:
Причины:
В serverless-среде стратегия зависит от платформы.
Иногда зависимости оставляют внешними:
externals: [nodeExternals()]
Иногда включают внутрь bundle:
externals: []
Подход определяется способом deployment.
В monorepo Webpack помогает:
Пример alias:
resolve: {
alias: {
'@core': path.resolve(__dirname, '../core')
}
}
Node.js постепенно переходит на ESM.
Типичные проблемы:
require is not definedWebpack сглаживает различия между системами модулей, но полностью проблему не устраняет.
Ключевые практики:
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
target: 'node',
entry: './src/cli.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cli.js'
},
optimization: {
minimize: true
},
cache: {
type: 'filesystem'
},
plugins: [
new webpack.BannerPlugin({
banner: '#!/usr/bin/env node',
raw: true
})
]
};