Запуск Webpack из скрипта Node.js

Webpack предоставляет не только CLI-интерфейс, но и полноценный программный API для запуска сборки непосредственно из Node.js-кода. Такой подход используется при разработке собственных build-систем, интеграции Webpack в серверные приложения, создании CLI-инструментов, автоматизации CI/CD и построении сложных сценариев компиляции.

В основе API находится модуль webpack, экспортирующий функцию создания компилятора.

Базовый пример:

const webpack = require('webpack');

const config = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  }
};

const compiler = webpack(config);

После вызова webpack(config) создаётся объект Compiler, содержащий всю внутреннюю инфраструктуру сборки:

  • файловую систему;
  • кэш;
  • граф модулей;
  • плагины;
  • хуки;
  • настройки компиляции;
  • watcher;
  • систему логирования.

Установка Webpack для программного запуска

Для использования API необходимы пакеты:

npm install webpack webpack-cli --save-dev

Даже если CLI не используется напрямую, webpack-cli часто устанавливается для совместимости со сторонними инструментами.


Создание минимального скрипта сборки

Структура проекта:

project/
├── build.js
├── src/
│   └── index.js
└── dist/

Файл build.js:

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

const config = {
  mode: 'development',

  entry: path.resolve(__dirname, 'src/index.js'),

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

const compiler = webpack(config);

compiler.run((err, stats) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(
    stats.toString({
      colors: true
    })
  );
});

Запуск:

node build.js

Метод compiler.run()

Метод run() запускает одиночную компиляцию.

compiler.run((err, stats) => {

});

Аргументы callback:

Аргумент Описание
err Критическая ошибка инфраструктуры
stats Объект статистики сборки

Важно разделять:

  • ошибки инфраструктуры Webpack;
  • ошибки компиляции модулей.

Инфраструктурные ошибки

Инфраструктурные ошибки появляются при:

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

Пример:

compiler.run((err, stats) => {
  if (err) {
    console.error('Fatal webpack error');
    console.error(err);
    return;
  }
});

Ошибки компиляции

Ошибки модулей находятся внутри stats.

compiler.run((err, stats) => {
  if (stats.hasErrors()) {
    console.error(
      stats.toJson().errors
    );
  }
});

Пример ошибки:

Module not found: Error: Can't resolve './app'

Объект Stats

Stats содержит огромный объём информации о сборке.

Получение JSON-представления:

const info = stats.toJson();

Основные поля:

Поле Назначение
errors Ошибки
warnings Предупреждения
assets Список файлов
modules Информация о модулях
chunks Информация о чанках
hash Хэш сборки
time Время компиляции

Форматированный вывод статистики

Webpack умеет красиво форматировать статистику:

console.log(
  stats.toString({
    colors: true,
    modules: false,
    chunks: false
  })
);

Популярные параметры:

Параметр Описание
colors ANSI-цвета
modules Вывод модулей
chunks Вывод чанков
assets Вывод ресурсов
entrypoints Entrypoints
children Child compilations

Закрытие compiler

После завершения сборки рекомендуется закрывать compiler.

compiler.run((err, stats) => {
  compiler.close(closeErr => {
    if (closeErr) {
      console.error(closeErr);
    }
  });
});

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

  • persistent cache;
  • watcher;
  • работе с файловыми дескрипторами;
  • многократных сборках.

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

Webpack API построен на callback-модели, однако легко оборачивается в Promise.

function runWebpack(config) {
  return new Promise((resolve, reject) => {
    const compiler = webpack(config);

    compiler.run((err, stats) => {
      if (err) {
        reject(err);
        return;
      }

      compiler.close(closeErr => {
        if (closeErr) {
          reject(closeErr);
          return;
        }

        resolve(stats);
      });
    });
  });
}

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

(async () => {
  try {
    const stats = await runWebpack(config);

    console.log(
      stats.toString({
        colors: true
      })
    );
  } catch (e) {
    console.error(e);
  }
})();

Множественные конфигурации

Webpack поддерживает массив конфигураций.

const compiler = webpack([
  clientConfig,
  serverConfig
]);

В этом случае создаётся MultiCompiler.

Пример:

const clientConfig = {
  name: 'client',
  target: 'web'
};

const serverConfig = {
  name: 'server',
  target: 'node'
};

const compiler = webpack([
  clientConfig,
  serverConfig
]);

Статистика MultiCompiler

При использовании массива конфигураций stats содержит дочерние сборки.

compiler.run((err, stats) => {
  const children = stats.stats;

  for (const child of children) {
    console.log(child.compilation.name);
  }
});

Запуск в watch-режиме

Программный API поддерживает watcher.

compiler.watch({}, (err, stats) => {

});

Пример:

const watching = compiler.watch(
  {
    aggregateTimeout: 300,
    poll: undefined
  },
  (err, stats) => {
    console.log('Rebuild completed');
  }
);

Остановка watcher

watching.close(err => {
  if (err) {
    console.error(err);
  }
});

Параметры watch

Параметр Назначение
aggregateTimeout Задержка перед rebuild
poll Polling mode
ignored Игнорируемые пути
stdin Закрытие по stdin

Пример:

compiler.watch(
  {
    ignored: /node_modules/,
    aggregateTimeout: 500
  },
  callback
);

Использование хуков Compiler

Compiler предоставляет систему hooks через Tapable.

Подписка на завершение сборки:

compiler.hooks.done.tap(
  'DonePlugin',
  stats => {
    console.log('Build complete');
  }
);

Другие важные хуки:

Hook Назначение
run Перед запуском
watchRun Перед rebuild
compile Начало компиляции
emit Перед записью файлов
afterEmit После записи
done После завершения
failed При ошибке

Хук emit

Позволяет изменять assets перед записью.

compiler.hooks.emit.tapAsync(
  'CustomPlugin',
  (compilation, callback) => {

    compilation.assets['meta.txt'] = {
      source() {
        return 'Build metadata';
      },

      size() {
        return 14;
      }
    };

    callback();
  }
);

Доступ к Compilation

Compilation содержит состояние конкретной сборки.

Получение через hook:

compiler.hooks.compilation.tap(
  'InspectCompilation',
  compilation => {

  }
);

Внутри доступны:

  • modules;
  • chunks;
  • assets;
  • dependency graph;
  • build info.

Использование виртуальной файловой системы

Webpack позволяет заменить файловую систему.

const MemoryFS = require('memory-fs');

compiler.outputFileSystem = new MemoryFS();

Теперь bundle не записывается на диск.

Чтение результата:

compiler.run((err, stats) => {
  const content =
    compiler.outputFileSystem.readFileSync(
      '/dist/bundle.js',
      'utf8'
    );

  console.log(content);
});

Такой подход активно используется:

  • webpack-dev-middleware;
  • webpack-dev-server;
  • SSR;
  • тестовыми системами.

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

Webpack также позволяет переопределять входную файловую систему.

compiler.inputFileSystem = customFs;

Это используется при:

  • работе с in-memory файлами;
  • виртуальными проектами;
  • IDE;
  • браузерными sandbox-средами.

Интеграция с Express

Пример запуска сборки из HTTP-сервера:

const express = require('express');
const webpack = require('webpack');

const app = express();

const compiler = webpack(config);

app.get('/build', (req, res) => {
  compiler.run((err, stats) => {
    if (err) {
      res.status(500).send(err.message);
      return;
    }

    res.send(
      stats.toString({
        colors: false
      })
    );
  });
});

app.listen(3000);

Использование webpack-dev-middleware

Программный запуск особенно важен для middleware.

const express = require('express');
const webpack = require('webpack');
const middleware = require('webpack-dev-middleware');

const compiler = webpack(config);

const app = express();

app.use(
  middleware(compiler)
);

В этом случае:

  • Webpack работает в памяти;
  • rebuild происходит автоматически;
  • bundle не сохраняется на диск.

Сборка внутри собственного CLI

Пример минимального CLI:

#!/usr/bin/env node

const webpack = require('webpack');
const config = require('./webpack.config');

const compiler = webpack(config);

compiler.run((err, stats) => {
  if (err) {
    process.exit(1);
  }

  console.log(
    stats.toString({
      colors: true
    })
  );
});

Управление режимами сборки

Конфигурацию можно генерировать динамически.

function createConfig(mode) {
  return {
    mode,

    devtool:
      mode === 'development'
        ? 'eval-source-map'
        : false
  };
}

const config =
  createConfig(process.env.NODE_ENV);

Динамическая модификация конфигурации

Программный API особенно полезен для runtime-конфигурации.

const config = require('./webpack.config');

config.plugins.push(
  new MyPlugin()
);

config.mode = 'production';

Измерение времени сборки

const start = Date.now();

compiler.run((err, stats) => {
  console.log(
    `Build time: ${Date.now() - start}ms`
  );
});

Либо:

console.log(stats.endTime - stats.startTime);

Запуск нескольких сборок последовательно

async function buildAll() {
  await runWebpack(clientConfig);
  await runWebpack(serverConfig);
}

Параллельный запуск

await Promise.all([
  runWebpack(clientConfig),
  runWebpack(serverConfig)
]);

Использование Node API для SSR

Программный запуск Webpack особенно распространён в SSR-системах.

Схема работы:

  1. Node-сервер запускает Webpack.
  2. Серверный bundle собирается в memory-fs.
  3. Код загружается через require-from-string.
  4. Выполняется SSR-render.

Генерация bundle в памяти

const fs = compiler.outputFileSystem;

const bundle = fs.readFileSync(
  '/dist/server.js',
  'utf8'
);

Инвалидация watcher

Watcher поддерживает ручной rebuild.

watching.invalidate();

Это принудительно запускает повторную компиляцию.


Работа с logging API

Webpack имеет инфраструктурный логгер.

const logger =
  compiler.getInfrastructureLogger(
    'custom'
  );

logger.info('Build started');

Уровни логирования

infrastructureLogging: {
  level: 'verbose'
}

Варианты:

  • none
  • error
  • warn
  • info
  • log
  • verbose

Получение assets после сборки

compiler.run((err, stats) => {
  const assets =
    stats.compilation.assets;

  for (const name in assets) {
    console.log(name);
  }
});

Получение содержимого asset

const source =
  assets['bundle.js'].source();

Работа с chunk graph

compiler.hooks.done.tap(
  'ChunksInfo',
  stats => {
    for (const chunk of stats.compilation.chunks) {
      console.log(chunk.name);
    }
  }
);

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

Webpack позволяет создавать дочерние компиляторы.

const childCompiler =
  compilation.createChildCompiler(
    'child',
    outputOptions
  );

Child compiler используется:

  • HtmlWebpackPlugin;
  • MiniCssExtractPlugin;
  • Module Federation;
  • SSR-инструментами.

Очистка ресурсов

При многократных сборках важно:

  • закрывать compiler;
  • останавливать watcher;
  • очищать таймеры;
  • удалять подписки;
  • освобождать memory-fs.

Иначе возможны:

  • утечки памяти;
  • зависшие файловые дескрипторы;
  • рост CPU usage;
  • накопление watchers.

Типичная архитектура build-системы

Программный API часто используется следующим образом:

CLI
  ↓
Build Orchestrator
  ↓
Webpack Compiler
  ↓
Plugins
  ↓
Assets

Оркестратор управляет:

  • конфигурациями;
  • rebuild;
  • watch-режимом;
  • кэшем;
  • сервером разработки;
  • SSR;
  • логированием;
  • параллельными задачами.

Отличия CLI от Node API

Возможность CLI Node API
Простая сборка Да Да
Гибкая логика Ограничено Полностью
Runtime-конфигурация Частично Да
Интеграция в сервер Нет Да
Контроль watcher Ограничено Да
Кастомные пайплайны Нет Да
Управление памятью Нет Да

Типичные сценарии использования Node API

Сценарий Причина
webpack-dev-server Управление rebuild
SSR Сборка в памяти
Electron Генерация main/renderer bundle
Monorepo Координация сборок
IDE Виртуальные FS
Тестовые системы In-memory compilation
CI/CD Кастомная автоматизация
Low-code платформы Runtime build

Пример полноценного build orchestration

const webpack = require('webpack');

async function compile(config) {
  return new Promise((resolve, reject) => {
    const compiler = webpack(config);

    compiler.run((err, stats) => {
      compiler.close(() => {});

      if (err) {
        reject(err);
        return;
      }

      if (stats.hasErrors()) {
        reject(stats.toJson().errors);
        return;
      }

      resolve(stats);
    });
  });
}

(async () => {
  try {
    const stats = await compile({
      mode: 'production',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      }
    });

    console.log(
      stats.toString({
        colors: true
      })
    );
  } catch (e) {
    console.error(e);
  }
})();