Разработка собственных плагинов для Webpack почти всегда сопровождается сложной отладкой. В отличие от обычного JavaScript-кода, плагины работают внутри жизненного цикла сборщика, взаимодействуют с внутренними хуами, файловой системой, компиляцией модулей и асинхронными процессами. Ошибки могут проявляться неявно:
Грамотная отладка позволяет контролировать состояние компиляции, анализировать внутренние структуры Webpack и локализовать проблемы на раннем этапе.
Типичная структура плагина выглядит следующим образом:
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', compilation => {
console.log('emit');
});
}
}
module.exports = MyPlugin;
Главная точка входа — метод apply. Именно внутри него
происходит подключение к хукам compiler и
compilation.
При отладке необходимо понимать:
apply;Первый этап отладки — убедиться, что плагин действительно зарегистрирован.
class DebugPlugin {
apply(compiler) {
console.log('Plugin initialized');
}
}
Если сообщение не появляется:
webpack.config.js;Пример подключения:
const DebugPlugin = require('./plugins/DebugPlugin');
module.exports = {
plugins: [
new DebugPlugin()
]
};
Webpack содержит большое количество compiler hooks.
Для анализа порядка работы удобно логировать каждый этап:
class LifecyclePlugin {
apply(compiler) {
compiler.hooks.initialize.tap('LifecyclePlugin', () => {
console.log('initialize');
});
compiler.hooks.beforeRun.tap('LifecyclePlugin', () => {
console.log('beforeRun');
});
compiler.hooks.run.tap('LifecyclePlugin', () => {
console.log('run');
});
compiler.hooks.emit.tap('LifecyclePlugin', () => {
console.log('emit');
});
compiler.hooks.done.tap('LifecyclePlugin', () => {
console.log('done');
});
}
}
Такой подход помогает определить:
Наиболее эффективный способ отладки — использование
debugger.
class DebugPlugin {
apply(compiler) {
compiler.hooks.emit.tap('DebugPlugin', compilation => {
debugger;
});
}
}
Webpack можно запускать через Node Inspector:
node --inspect-brk ./node_modules/webpack/bin/webpack.js
Либо:
node --inspect ./node_modules/webpack/bin/webpack.js
После запуска инспектор подключается через:
Во время остановки можно анализировать:
compiler;compilation;Пример launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Webpack Debug",
"program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js",
"args": [
"--config",
"webpack.config.js"
]
}
]
}
Теперь breakpoints внутри плагина будут работать напрямую.
compiler — глобальный объект процесса сборки.
Полезные свойства:
console.log(compiler.options);
console.log(compiler.context);
console.log(compiler.outputPath);
console.log(compiler.name);
Через compiler.options можно проверить:
Пример:
compiler.hooks.beforeRun.tap('DebugPlugin', () => {
console.log(compiler.options.mode);
});
compilation содержит состояние конкретной сборки.
Пример исследования:
compiler.hooks.emit.tap('DebugPlugin', compilation => {
console.log(compilation.assets);
});
Полезные свойства:
compilation.modules
compilation.chunks
compilation.assets
compilation.errors
compilation.warnings
compilation.fileDependencies
Одной из самых частых проблем является отсутствие изменений в итоговых файлах.
Проверка списка ассетов:
compiler.hooks.emit.tap('DebugPlugin', compilation => {
Object.keys(compilation.assets).forEach(name => {
console.log(name);
});
});
Проверка содержимого:
const asset = compilation.assets['main.js'];
console.log(asset.source());
Анализ модулей помогает понять:
Пример:
compiler.hooks.compilation.tap('DebugPlugin', compilation => {
compilation.hooks.buildModule.tap('DebugPlugin', module => {
console.log(module.resource);
});
});
В сложных сборках важен анализ чанков.
compiler.hooks.emit.tap('DebugPlugin', compilation => {
for (const chunk of compilation.chunks) {
console.log(chunk.name);
for (const file of chunk.files) {
console.log(file);
}
}
});
Это помогает понять:
Webpack построен на библиотеке Tapable.
Типы хуков:
Ошибки часто связаны с неправильным использованием async hooks.
Неверный код:
compiler.hooks.emit.tap('Plugin', async compilation => {
await saveFile();
});
tap не ожидает Promise.
Правильно:
compiler.hooks.emit.tapPromise('Plugin', async compilation => {
await saveFile();
});
Либо:
compiler.hooks.emit.tapAsync('Plugin', (compilation, callback) => {
saveFile().then(() => callback());
});
Если выбрать неправильный тип tap:
Полезно измерять производительность плагина.
compiler.hooks.emit.tapAsync('Plugin', async (compilation, callback) => {
console.time('plugin');
await heavyOperation();
console.timeEnd('plugin');
callback();
});
Либо:
const start = Date.now();
await task();
console.log(Date.now() - start);
Webpack поддерживает встроенную систему логирования.
class LoggerPlugin {
apply(compiler) {
const logger = compiler.getInfrastructureLogger('LoggerPlugin');
logger.info('info');
logger.warn('warn');
logger.error('error');
}
}
В конфигурации:
module.exports = {
infrastructureLogging: {
level: 'log'
}
};
Доступные уровни:
Преимущества:
Плагин может добавлять ошибки вручную.
compilation.errors.push(
new Error('Plugin error')
);
Для предупреждений:
compilation.warnings.push(
new Error('Plugin warning')
);
Проверка ошибок:
compiler.hooks.done.tap('Plugin', stats => {
console.log(stats.hasErrors());
console.log(stats.compilation.errors);
});
Webpack предоставляет объект stats.
compiler.hooks.done.tap('Plugin', stats => {
console.log(
stats.toJson({
assets: true,
chunks: true,
modules: true
})
);
});
Через stats можно анализировать:
CLI-команда:
webpack --profile --json > stats.json
Далее файл можно анализировать через:
Webpack использует абстракцию файловой системы.
Проверка output FS:
compiler.hooks.emit.tap('Plugin', compilation => {
console.log(compiler.outputFileSystem);
});
При использовании dev-server запись может происходить в память, а не на диск.
Из-за этого часто возникает ошибка:
Во время разработки сборка работает в watch-режиме.
Плагин может вызывать:
Полезные хуки:
compiler.hooks.watchRun.tap('Plugin', () => {
console.log('watchRun');
});
compiler.hooks.invalid.tap('Plugin', file => {
console.log(file);
});
Частая проблема:
fs.writeFileSync('output.txt', 'data');
Если файл попадает в зависимости Webpack, начинается цикл:
Решение:
Ошибки утечек памяти часто появляются в watch-режиме.
Неверный код:
compiler.hooks.emit.tap('Plugin', () => {
process.on('exit', () => {});
});
Listener добавляется на каждой сборке.
Для хранения состояния между compilation:
const cache = new WeakMap();
class Plugin {
apply(compiler) {
compiler.hooks.compilation.tap('Plugin', compilation => {
cache.set(compilation, {
started: Date.now()
});
});
}
}
Unhandled rejection внутри плагинов особенно опасны.
Полезно подключать:
process.on('unhandledRejection', error => {
console.error(error);
});
process.on('uncaughtException', error => {
console.error(error);
});
Некоторые плагины конфликтуют между собой.
Порядок регистрации важен:
plugins: [
new FirstPlugin(),
new SecondPlugin()
]
Для анализа:
console.log(
compiler.options.plugins.map(
plugin => plugin.constructor.name
)
);
Webpack 5 позволяет задавать приоритет.
compiler.hooks.emit.tap(
{
name: 'Plugin',
stage: 100
},
compilation => {
console.log('emit');
}
);
Это помогает диагностировать:
Tapable поддерживает interceptors.
compiler.hooks.emit.intercept({
register(tap) {
console.log(tap.name);
return tap;
},
call() {
console.log('emit called');
}
});
Это мощный инструмент анализа внутренних вызовов.
В Webpack 5 рекомендуется использовать
processAssets.
compiler.hooks.thisCompilation.tap('Plugin', compilation => {
compilation.hooks.processAssets.tap(
{
name: 'Plugin',
stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE
},
assets => {
console.log(Object.keys(assets));
}
);
});
Если Webpack не завершает процесс:
Пример проблемы:
compiler.hooks.emit.tapAsync('Plugin', (compilation, callback) => {
});
callback() никогда не вызывается.
Полезный приём:
setInterval(() => {
console.log(process._getActiveHandles());
}, 5000);
Позволяет обнаружить:
Для глубокой диагностики:
NODE_OPTIONS="--trace-warnings --trace-deprecation"
Либо:
NODE_OPTIONS="--inspect"
При проблемах памяти:
node --trace-gc ./node_modules/webpack/bin/webpack.js
Для поиска утечек:
node --inspect
После этого через Chrome DevTools:
Можно выявить:
Обычный console.log плохо отображает глубокие объекты
Webpack.
Лучше:
const util = require('util');
console.log(
util.inspect(compilation, {
depth: 2,
colors: true
})
);
При модификации кода важно проверять source maps.
const { SourceMapSource } = require('webpack-sources');
Ошибки обычно связаны с:
Многие плагины ломаются после миграции.
Типичные проблемы:
Проверка версии:
const webpack = require('webpack');
console.log(webpack.version);
Встроенный профайлинг:
const { debug } = require('webpack');
console.log(debug.ProfilingPlugin);
Пример:
plugins: [
new webpack.debug.ProfilingPlugin({
outputPath: 'events.json'
})
]
Файл можно открыть в Chrome tracing tools.
Webpack 5 активно использует filesystem cache.
Иногда изменения плагина не видны из-за кэширования.
Отключение:
module.exports = {
cache: false
};
Либо:
rm -rf node_modules/.cache
Loader и plugin часто взаимодействуют.
Полезно логировать:
module.exports = function(source) {
console.log(this.resourcePath);
return source;
};
И сравнивать с хуками compilation.
Эффективный подход:
Это резко сокращает количество возможных причин ошибки.
Для сложных плагинов полезно иметь отдельный sandbox-проект.
Минимальная структура:
test-project/
├── src/
├── dist/
├── webpack.config.js
└── plugins/
Такой стенд позволяет:
Для проверки поведения удобно запускать Webpack программно.
const webpack = require('webpack');
const config = require('./webpack.config');
webpack(config, (err, stats) => {
if (err) {
console.error(err);
}
console.log(stats.toString());
});
Это особенно полезно в unit и integration tests.
Иногда hook подключается несколько раз.
Диагностика:
console.log(
compiler.hooks.emit.taps
);
Можно увидеть:
Типичные симптомы:
Каждый из этих симптомов обычно связан с конкретным этапом жизненного цикла Webpack и требует точечной диагностики через hooks, logging, profiling и инспекцию внутренних структур сборщика.