Программный API: webpack() и compiler

Программный API Webpack позволяет запускать сборку не через CLI, а напрямую из JavaScript-кода. Центральной точкой входа является функция webpack(), принимающая объект конфигурации и возвращающая экземпляр Compiler либо MultiCompiler.

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

  • кастомных инструментах сборки;
  • dev-server реализациях;
  • CLI-обёртках;
  • IDE-интеграциях;
  • системах непрерывной интеграции;
  • серверных сборщиках;
  • SSR-инфраструктуре;
  • Electron-приложениях;
  • собственных build-системах.

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

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

const compiler = webpack(config);

После вызова создаётся внутренний граф зависимостей, набор хуков, система плагинов и объект компилятора, управляющий жизненным циклом сборки.


Что возвращает webpack()

Тип возвращаемого значения зависит от конфигурации.

Один объект конфигурации

const compiler = webpack(config);

Возвращается экземпляр Compiler.


Массив конфигураций

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

Возвращается экземпляр MultiCompiler.

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


Структура Compiler

Compiler — главный объект Webpack Runtime API.

Он содержит:

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

Пример:

const webpack = require('webpack');

const compiler = webpack({
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js'
    }
});

Запуск единичной сборки: run()

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

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

    console.log(stats.toString());
});

Параметры callback в run()

err

Фатальная ошибка уровня компилятора:

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

Примеры:

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

stats

Объект статистики сборки.

Тип:

Stats

Через него доступны:

  • ошибки;
  • предупреждения;
  • список модулей;
  • assets;
  • чанки;
  • timing;
  • dependency graph.

Проверка ошибок сборки

Ошибки внутри модулей не всегда попадают в err.

Правильная схема:

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

    if (stats.hasErrors()) {
        console.error(stats.toJson().errors);
    }

    if (stats.hasWarnings()) {
        console.warn(stats.toJson().warnings);
    }
});

Это важная особенность API Webpack.


Завершение compiler после run()

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

compiler.run((err, stats) => {
    compiler.close((closeErr) => {
        console.log('Compiler closed');
    });
});

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

  • persistent cache;
  • watch infrastructure;
  • memory FS;
  • worker threads;
  • CI/CD процессах.

Метод close()

Webpack 5 использует внутренние ресурсы:

  • файловые дескрипторы;
  • кэш;
  • watchers;
  • worker pool;
  • snapshot system.

Без close() процесс Node.js может не завершаться.

Пример:

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

Получение JSON-статистики

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

    console.log(json.assets);
});

Настройка вывода статистики

const info = stats.toJson({
    assets: true,
    chunks: true,
    modules: true
});

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

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

Watch-режим через API

Метод watch() запускает постоянное отслеживание файлов.

compiler.watch({}, (err, stats) => {
    console.log('Rebuild completed');
});

Опции watch()

compiler.watch({
    aggregateTimeout: 300,
    poll: 1000
}, callback);

aggregateTimeout

Задержка перед повторной сборкой.


poll

Polling-режим для файловых систем без native watching.


Watching instance

watch() возвращает объект Watching.

const watching = compiler.watch({}, callback);

Через него можно остановить наблюдение:

watching.close(() => {
    console.log('Stopped');
});

Повторная компиляция invalidate()

Принудительный rebuild:

watching.invalidate();

Используется в:

  • HMR;
  • middleware;
  • dev servers;
  • IDE plugins.

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

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

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

const app = express();

const compiler = webpack(config);

compiler.watch({}, () => {
    console.log('Compiled');
});

app.listen(3000);

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

Часто программный API применяется вместе с виртуальной файловой системой.

Пример:

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

const compiler = webpack(config);

compiler.outputFileSystem = new MemoryFS();

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

    console.log(content);
});

outputFileSystem

Webpack абстрагирует файловую систему через интерфейс.

Можно подменять:

compiler.outputFileSystem = customFs;

Используется в:

  • webpack-dev-middleware;
  • SSR;
  • Electron;
  • тестировании;
  • in-memory build systems.

inputFileSystem

Можно переопределить и входную файловую систему:

compiler.inputFileSystem = fs;

purgeInputFileSystem()

Очистка внутренних кэшей:

compiler.purgeInputFileSystem();

Полезно при:

  • виртуальных FS;
  • overlay filesystems;
  • динамической генерации файлов.

Хуки Compiler

Webpack построен вокруг Tapable.

Большая часть API реализована через хуки:

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

Основные хуки compiler

initialize

Инициализация compiler.

compiler.hooks.initialize.tap(
    'Plugin',
    () => {}
);

beforeRun

Перед запуском сборки.

compiler.hooks.beforeRun.tapAsync(
    'Plugin',
    (compiler, callback) => {
        callback();
    }
);

run

Начало обычной сборки.

compiler.hooks.run.tap(
    'Plugin',
    () => {}
);

watchRun

Начало rebuild в watch-режиме.

compiler.hooks.watchRun.tapAsync(
    'Plugin',
    (compiler, callback) => {
        callback();
    }
);

compile

Создание Compilation.

compiler.hooks.compile.tap(
    'Plugin',
    (params) => {}
);

thisCompilation

Создание новой compilation.

compiler.hooks.thisCompilation.tap(
    'Plugin',
    (compilation) => {}
);

compilation

Хук новой compilation.

compiler.hooks.compilation.tap(
    'Plugin',
    (compilation) => {}
);

emit

Перед записью assets.

compiler.hooks.emit.tapAsync(
    'Plugin',
    (compilation, callback) => {
        callback();
    }
);

afterEmit

После записи файлов.

compiler.hooks.afterEmit.tap(
    'Plugin',
    () => {}
);

done

После завершения сборки.

compiler.hooks.done.tap(
    'Plugin',
    (stats) => {}
);

failed

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

compiler.hooks.failed.tap(
    'Plugin',
    (error) => {}
);

Асинхронные хуки

Tapable поддерживает:

  • tap
  • tapAsync
  • tapPromise

Пример Promise API:

compiler.hooks.beforeRun.tapPromise(
    'Plugin',
    async () => {
        await doSomething();
    }
);

Создание собственного плагина

Простейший пример:

class BuildTimePlugin {
    apply(compiler) {
        compiler.hooks.done.tap(
            'BuildTimePlugin',
            (stats) => {
                console.log(
                    `Build time: ${stats.endTime - stats.startTime}`
                );
            }
        );
    }
}

Добавление плагина программно

const compiler = webpack(config);

new BuildTimePlugin().apply(compiler);

Compiler и Compilation

Важно различать два объекта.

Compiler

Глобальный объект процесса сборки.

Живёт между rebuild-ами.


Compilation

Конкретная единичная компиляция.

Создаётся заново при каждой сборке.


Доступ к compilation

compiler.hooks.compilation.tap(
    'Plugin',
    (compilation) => {
        console.log(compilation.hash);
    }
);

Child Compiler

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

Пример:

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

Используется:

  • HTMLWebpackPlugin;
  • Module Federation;
  • asset generation;
  • SSR tooling.

MultiCompiler

При передаче массива конфигураций:

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

Доступ к дочерним compiler

multiCompiler.compilers.forEach(
    (compiler) => {
        console.log(compiler.name);
    }
);

Запуск MultiCompiler

multiCompiler.run((err, stats) => {
    console.log(stats.stats.length);
});

stats.stats содержит массив статистик.


Параллельные сборки

MultiCompiler может выполнять:

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

Зависимости между compiler

module.exports = [
    {
        name: 'client'
    },
    {
        name: 'server',
        dependencies: ['client']
    }
];

Инфраструктурный логгер

Webpack 5 содержит Infrastructure Logging API.

Получение логгера:

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

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

logger.info('Build started');
logger.warn('Potential issue');
logger.error('Build failed');

infrastructureLogging

Настройка:

module.exports = {
    infrastructureLogging: {
        level: 'verbose'
    }
};

Программное изменение options

Конфигурацию можно модифицировать до запуска:

const compiler = webpack(config);

compiler.options.mode = 'production';

Изменение entry

compiler.options.entry = './src/new-entry.js';

Изменение output path

compiler.options.output.path =
    path.resolve(__dirname, 'build');

createCompiler внутри webpack()

Внутри Webpack функция webpack():

  1. нормализует конфигурацию;
  2. создаёт Compiler;
  3. применяет плагины;
  4. инициализирует environment;
  5. подготавливает resolver;
  6. создаёт filesystem cache;
  7. подключает built-in plugins.

NodeEnvironmentPlugin

Для Node.js Webpack автоматически подключает:

new NodeEnvironmentPlugin({
    infrastructureLogging
});

Он внедряет:

  • NodeFileSystem;
  • watching system;
  • cache infrastructure.

Compiler cache

В Webpack 5 compiler содержит встроенный cache layer.

Доступ:

compiler.cache

Idle cache

Webpack может переводить кэш в idle state:

compiler.cache.beginIdle();
compiler.cache.endIdle();

Shutdown cache

compiler.cache.shutdown(callback);

Resolver Factory

Compiler содержит фабрику резолверов:

compiler.resolverFactory

Создание resolver вручную

const resolver =
    compiler.resolverFactory.get('normal');

Reading records

Webpack поддерживает records-файлы.

compiler.readRecords(() => {
    console.log('Records loaded');
});

Writing records

compiler.emitRecords(() => {
    console.log('Records saved');
});

API purgeInputFileSystem

После rebuild можно очищать кэш путей:

compiler.purgeInputFileSystem();

isChild()

Проверка дочернего compiler:

if (compiler.isChild()) {
    console.log('Child compiler');
}

createCompilation()

Внутренний метод:

const compilation =
    compiler.createCompilation();

Используется ядром Webpack.


newCompilation()

Создание compilation с хуками:

const compilation =
    compiler.newCompilation(params);

emitAssets()

Запись assets:

compiler.emitAssets(
    compilation,
    callback
);

emitAsset()

Современный API добавления asset:

compilation.emitAsset(
    'file.txt',
    new RawSource('content')
);

removeAsset()

Удаление asset:

compilation.removeAsset('old.js');

updateAsset()

Изменение asset:

compilation.updateAsset(
    'bundle.js',
    old => transform(old)
);

Compiler lifecycle

Полный цикл compiler выглядит следующим образом:

  1. initialize
  2. beforeRun
  3. run
  4. beforeCompile
  5. compile
  6. make
  7. finishMake
  8. afterCompile
  9. emit
  10. afterEmit
  11. done

Отличие CLI от программного API

CLI внутри себя также использует webpack().

Разница лишь в:

  • обработке аргументов;
  • загрузке конфигурации;
  • выводе статистики;
  • watch management;
  • dev-server integration.

Когда используется программный API

Кастомные build systems

async function build() {
    const compiler = webpack(config);

    compiler.run(() => {});
}

Dev middleware

compiler.watch({}, callback);

Electron tooling

mainCompiler.run(callback);
rendererCompiler.watch({}, callback);

SSR

const fs = compiler.outputFileSystem;

IDE integration

compiler.hooks.failed.tap(
    'IDE',
    showDiagnostics
);

Типичные ошибки при работе с compiler

Отсутствие close()

Приводит к зависанию процесса.


Игнорирование stats.hasErrors()

Ошибки модулей могут остаться необработанными.


Повторное использование compiler после destroy

Compiler имеет внутреннее состояние.


Изменение compilation вне lifecycle hooks

Может ломать dependency graph.


Мутация options во время compilation

Опасно из-за cache invalidation.


Рекомендуемая схема build-процесса

const webpack = require('webpack');

async function build(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);
            });
        });
    });
}

Архитектурная роль Compiler в Webpack

Compiler фактически является координатором всей системы сборки.

Через него проходят:

  • создание dependency graph;
  • loader pipeline;
  • plugin lifecycle;
  • asset emission;
  • cache management;
  • incremental rebuild;
  • watch infrastructure;
  • filesystem abstraction;
  • module federation;
  • chunk graph generation;
  • optimization pipeline.

Именно вокруг Compiler построен весь внутренний runtime Webpack и большая часть экосистемы плагинов.