Сборка CLI-инструментов и серверных приложений

CLI-приложения отличаются от браузерных проектов архитектурой, способом запуска и требованиями к выходному коду. Основная цель сборки — сформировать единый исполняемый файл или компактный набор файлов, пригодных для запуска через Node.js.

Типичные особенности CLI-инструментов:

  • запуск через node
  • использование файловой системы
  • работа с потоками stdin/stdout
  • отсутствие DOM и браузерных API
  • необходимость поддержки shebang-заголовков
  • минимизация времени запуска
  • совместимость с Linux, macOS и Windows

Webpack способен эффективно собирать подобные приложения благодаря режиму target: 'node'.


Настройка target для Node.js

Для серверных приложений и CLI используется специальная цель сборки:

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

Этот режим изменяет поведение Webpack:

  • не подключаются браузерные полифилы
  • сохраняется работа require
  • корректно обрабатываются встроенные модули Node.js
  • отключается генерация browser runtime

Без target: 'node' Webpack может попытаться внедрить браузерные зависимости, что приведёт к ошибкам исполнения.


Базовая структура CLI-проекта

Пример структуры:

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'
    }
};

Shebang и исполняемые файлы

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

Настройка bin в package.json

Для публикации CLI используется поле bin.

{
  "name": "mycli",
  "bin": {
    "mycli": "./dist/cli.js"
  }
}

После установки пакет автоматически создаёт системную команду.

Глобальная установка:

npm install -g

Локальный запуск:

npx mycli

Работа со встроенными модулями Node.js

Webpack понимает встроенные модули Node.js:

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

При target: 'node' они не включаются в bundle, а остаются внешними зависимостями runtime.


Externals в серверной сборке

Серверные приложения часто не бандлят зависимости из node_modules.

Причины:

  • уменьшение размера bundle
  • ускорение сборки
  • корректная работа native-модулей
  • снижение потребления памяти

Пример externals:

module.exports = {
    externals: {
        express: 'commonjs express'
    }
};

Теперь Express не попадёт в bundle.


webpack-node-externals

Ручное перечисление зависимостей неудобно. Для 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-инструменты нередко распространяются единым файлом.

Преимущества:

  • простая доставка
  • отсутствие дополнительных зависимостей
  • удобство CI/CD
  • быстрый запуск

В этом случае externals обычно отключают:

module.exports = {
    target: 'node',

    externals: []
};

Все зависимости попадают внутрь bundle.


Минификация серверного кода

Node.js способен выполнять минифицированный код, поэтому production-сборка остаётся актуальной.

module.exports = {
    mode: 'production'
};

Webpack включает:

  • Terser
  • tree shaking
  • dead code elimination
  • scope hoisting

Tree Shaking в Node.js

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-модулями.


CommonJS и ES Modules

Node.js поддерживает оба формата:

  • CommonJS
  • ES Modules

Webpack умеет собирать оба варианта.

CommonJS

const fs = require('fs');

module.exports = function() {};

ES Modules

import fs from 'fs';

export default function() {}

output.libraryTarget для серверных библиотек

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

output: {
    libraryTarget: 'commonjs2'
}

Пример:

module.exports = {
    output: {
        filename: 'index.js',
        libraryTarget: 'commonjs2'
    }
};

Сборка Express-приложений

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

__dirname и __filename

Webpack изменяет поведение Node.js-переменных.

Для сохранения оригинального поведения:

module.exports = {
    node: {
        __dirname: false,
        __filename: false
    }
};

Это особенно важно при работе:

  • с шаблонами
  • с файлами конфигурации
  • со статическими ресурсами
  • с SQLite
  • с логированием

Работа с динамическими require

Webpack плохо анализирует динамические пути.

Проблемный код:

require('./commands/' + name);

Компилятор не может заранее определить зависимости.

Лучший вариант:

const commands = {
    build: require('./commands/build'),
    serve: require('./commands/serve')
};

commands[name]();

ContextModule и динамические зависимости

Webpack создаёт context modules для анализа директорий.

Пример:

const context = require.context('./commands', false, /\.js$/);

Это позволяет динамически подключать файлы, сохраняя возможность статического анализа.


Lazy loading в Node.js

Code splitting работает и в серверной среде.

Пример:

async function start() {
    const module = await import('./heavy-module');

    module.run();
}

Webpack создаст отдельный chunk.


Ограничения code splitting на сервере

На сервере разделение кода используется осторожно.

Недостатки:

  • усложнение deployment
  • необходимость доступа к chunk-файлам
  • увеличение latency при первом импорте
  • сложность Docker-образов

Для CLI-инструментов чаще используется единый bundle.


Source Maps для Node.js

Source maps особенно важны для серверной отладки.

module.exports = {
    devtool: 'source-map'
};

Ошибки будут указывать на исходный код TypeScript или ESNext.


Оптимальные devtool для сервера

Development

devtool: 'eval-cheap-module-source-map'

Production

devtool: 'source-map'

Полное отключение

devtool: false

Hot Reload серверных приложений

Webpack-dev-server ориентирован на браузер.

Для Node.js используются:

  • nodemon
  • webpack –watch
  • webpack-node-dev
  • node-dev

Пример:

{
  "scripts": {
    "dev": "webpack --watch"
  }
}

Дополнительно:

nodemon dist/server.js

Watch Mode

Webpack отслеживает изменения файлов:

webpack --watch

Преимущества:

  • автоматическая пересборка
  • ускорение разработки
  • инкрементальная компиляция

Кэширование сборки

Webpack 5 поддерживает filesystem cache.

module.exports = {
    cache: {
        type: 'filesystem'
    }
};

Это существенно ускоряет крупные серверные проекты.


Babel в Node.js-проектах

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

Пример loader:

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

Настройка preset-env для Node.js

Вместо браузеров указывается версия Node.js:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "18"
        }
      }
    ]
  ]
}

Это уменьшает объём трансформаций.


TypeScript и серверные приложения

Webpack эффективно собирает TypeScript-серверы.

Установка:

npm install typescript ts-loader --save-dev

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

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

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

ts-node против Webpack

ts-node

Преимущества:

  • быстрый старт
  • простая настройка
  • удобство разработки

Недостатки:

  • медленный cold start
  • отсутствие bundle
  • более высокое потребление памяти

Webpack

Преимущества:

  • единый bundle
  • оптимизация зависимостей
  • production-ready deployment

Недостатки:

  • сложность конфигурации
  • увеличение времени сборки

Native-модули Node.js

Некоторые пакеты содержат нативный код:

  • sqlite3
  • sharp
  • bcrypt
  • canvas

Webpack может работать с ними нестабильно.

Обычно такие зависимости исключают через externals.

externals: {
    sharp: 'commonjs sharp'
}

Обработка JSON

Webpack умеет импортировать JSON без loader.

import config from './config.json';

JSON включается в bundle автоматически.


Работа с ENV-переменными

Серверные приложения активно используют окружение.

Пример:

plugins: [
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
    })
]

dotenv и Webpack

Частая связка:

npm install dotenv
require('dotenv').config();

Webpack подставляет значения на этапе компиляции.


Секреты и bundle

Встраивание секретов в bundle опасно.

Плохой вариант:

DefinePlugin({
    API_KEY: JSON.stringify(process.env.API_KEY)
})

После сборки ключ окажется внутри кода.

Для серверных приложений лучше читать переменные runtime.


Оптимизация времени запуска CLI

CLI-инструменты чувствительны к startup latency.

Методы оптимизации:

  • минимизация зависимостей
  • отказ от тяжёлых библиотек
  • lazy imports
  • tree shaking
  • уменьшение размера bundle

Анализ размера bundle

Используется webpack-bundle-analyzer.

Установка:

npm install webpack-bundle-analyzer --save-dev

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

const { BundleAnalyzerPlugin } =
    require('webpack-bundle-analyzer');

plugins: [
    new BundleAnalyzerPlugin()
]

Инструмент показывает:

  • размеры модулей
  • дублирование
  • тяжёлые зависимости
  • неэффективные импорты

Multi-compiler для серверов и CLI

Webpack может собирать несколько targets одновременно.

module.exports = [
    {
        target: 'node',
        entry: './src/server.js'
    },

    {
        target: 'node',
        entry: './src/cli.js'
    }
];

Разделение development и production-конфигураций

Типичная структура:

webpack/
├── common.js
├── dev.js
└── prod.js

Объединение:

const { merge } = require('webpack-merge');

Docker и Webpack

Серверные bundle удобно использовать внутри контейнеров.

Преимущества:

  • уменьшение размера образа
  • отсутствие devDependencies
  • ускорение запуска контейнера
  • упрощение deployment

Serverless и Webpack

Webpack особенно полезен для:

  • AWS Lambda
  • Google Cloud Functions
  • Azure Functions

Причины:

  • уменьшение cold start
  • сокращение размера deployment package
  • исключение неиспользуемого кода

webpack-node-externals и serverless

В serverless-среде стратегия зависит от платформы.

Иногда зависимости оставляют внешними:

externals: [nodeExternals()]

Иногда включают внутрь bundle:

externals: []

Подход определяется способом deployment.


Монорепозитории и Node.js-сборки

В monorepo Webpack помогает:

  • объединять внутренние пакеты
  • устранять дублирование
  • управлять alias
  • ускорять CI

Пример alias:

resolve: {
    alias: {
        '@core': path.resolve(__dirname, '../core')
    }
}

Проблемы совместимости ESM и CommonJS

Node.js постепенно переходит на ESM.

Типичные проблемы:

  • require is not defined
  • ошибки default export
  • различия import/export semantics
  • несовместимость старых библиотек

Webpack сглаживает различия между системами модулей, но полностью проблему не устраняет.


Оптимизация production-серверов

Ключевые практики:

  • отключение source maps при необходимости
  • минимизация runtime-зависимостей
  • externals для native-модулей
  • filesystem cache
  • tree shaking
  • production mode
  • анализ bundle size
  • lazy loading тяжёлых модулей
  • единый executable bundle для CLI

Типичная production-конфигурация для CLI

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
        })
    ]
};