Отладка плагина

Плагин в Vite представляет собой прослойку между системой сборки, dev-сервером, трансформацией модулей и механизмом Rollup. Ошибки внутри плагинов редко проявляются как обычные исключения. Чаще возникают более сложные проблемы:

  • неправильная трансформация кода;
  • бесконечные циклы HMR;
  • дублирование модулей;
  • конфликт между несколькими плагинами;
  • потеря sourcemap;
  • нарушение порядка хуков;
  • различия между serve и build;
  • утечки памяти в dev-сервере;
  • повторные вызовы хуков;
  • неправильная обработка виртуальных модулей.

Отладка плагинов требует понимания внутреннего жизненного цикла Vite и Rollup, а также особенностей dev-сервера.


Основные источники ошибок

Ошибки трансформации

Наиболее распространённый тип проблем связан с хуком transform.

Пример:

export default function myPlugin() {
    return {
        name: 'my-plugin',

        transform(code, id) {
            if (id.endsWith('.js')) {
                return code.replace('foo', 'bar');
            }
        }
    };
}

Проблемы:

  • трансформация применяется к node_modules;
  • код ломает sourcemap;
  • регулярное выражение изменяет лишние участки;
  • код модифицируется повторно;
  • трансформация применяется к уже обработанному модулю.

Ошибки resolve-механизма

Проблемы в resolveId особенно трудно диагностировать.

Пример:

resolveId(source) {
    if (source === 'virtual:config') {
        return '\0virtual:config';
    }
}

Типичные ошибки:

  • циклический resolve;
  • возврат некорректного пути;
  • потеря расширения файла;
  • несовместимость Windows-путей;
  • отсутствие \0 для виртуального модуля;
  • конфликт alias и resolveId.

Ошибки загрузки модулей

Хук load может приводить к:

  • повторной генерации контента;
  • конфликту с кешем;
  • проблемам HMR;
  • неправильной сериализации данных;
  • потере производительности.

Использование console.log

Базовая трассировка

Самый простой способ диагностики — вывод информации в консоль.

transform(code, id) {
    console.log('[transform]', id);

    return code;
}

Полезно выводить:

  • id;
  • содержимое кода;
  • mode;
  • command;
  • результаты resolve;
  • время выполнения.

Форматирование логов

Без структурирования логи быстро становятся нечитаемыми.

Хороший вариант:

console.log(
    '[my-plugin:transform]',
    {
        id,
        length: code.length
    }
);

Группировка логов

console.group('[my-plugin]');
console.log('id:', id);
console.log('code:', code);
console.groupEnd();

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

Для крупных плагинов console.log становится неудобным. В экосистеме Vite широко используется библиотека debug.

Установка:

npm install debug

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

import debug from 'debug';

const log = debug('vite:my-plugin');

export default function myPlugin() {
    return {
        name: 'my-plugin',

        transform(code, id) {
            log('transform %s', id);

            return code;
        }
    };
}

Запуск:

DEBUG=vite:my-plugin vite

Пространства имён

Хорошая практика:

vite:plugin-name:resolve
vite:plugin-name:transform
vite:plugin-name:hmr
vite:plugin-name:load

Отладка жизненного цикла плагина

Проверка порядка вызовов

Для диагностики важно понимать последовательность хуков.

Пример:

export default function myPlugin() {
    return {
        name: 'my-plugin',

        options() {
            console.log('options');
        },

        buildStart() {
            console.log('buildStart');
        },

        resolveId(id) {
            console.log('resolveId', id);
        },

        load(id) {
            console.log('load', id);
        },

        transform(code, id) {
            console.log('transform', id);
        }
    };
}

Это позволяет увидеть:

  • какие хуки вообще вызываются;
  • порядок выполнения;
  • повторные вызовы;
  • различия между serve и build.

Отладка dev-сервера

configureServer

Хук configureServer даёт доступ к экземпляру Vite Dev Server.

configureServer(server) {
    console.log(server.config);
}

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

  • middleware;
  • websocket;
  • модульный граф;
  • watcher;
  • HMR;
  • запросы.

Логирование HTTP-запросов

configureServer(server) {
    server.middlewares.use((req, res, next) => {
        console.log(req.url);

        next();
    });
}

Отладка websocket HMR

configureServer(server) {
    server.ws.on('connection', () => {
        console.log('ws connected');
    });
}

Отладка HMR

handleHotUpdate

handleHotUpdate(ctx) {
    console.log(ctx.file);

    return ctx.modules;
}

Контекст содержит:

{
    file,
    modules,
    server,
    timestamp,
    read
}

Анализ HMR-циклов

Проблемы:

  • бесконечный reload;
  • повторное обновление;
  • множественные invalidation;
  • постоянная перестройка графа.

Полезно логировать:

handleHotUpdate(ctx) {
    console.log({
        file: ctx.file,
        modules: ctx.modules.map(m => m.url)
    });
}

Отладка виртуальных модулей

Проверка resolve/load

const virtualId = 'virtual:my-module';
const resolved = '\0' + virtualId;

export default function myPlugin() {
    return {
        name: 'my-plugin',

        resolveId(id) {
            console.log('resolve', id);

            if (id === virtualId) {
                return resolved;
            }
        },

        load(id) {
            console.log('load', id);

            if (id === resolved) {
                return 'export const value = 42';
            }
        }
    };
}

Частые проблемы виртуальных модулей

Отсутствие префикса \0

Без него Rollup и Vite могут пытаться искать реальный файл.

Несогласованность id

Ошибка:

return 'virtual-module';

и:

if (id === '\0virtual-module')

Такие идентификаторы никогда не совпадут.


Отладка sourcemap

Потеря корректных sourcemap

Неправильная трансформация ломает:

  • breakpoint;
  • stack trace;
  • devtools;
  • HMR.

Проверка sourcemap

transform(code, id) {
    return {
        code: transformed,
        map: null
    };
}

Если указать map: null, Vite считает sourcemap отсутствующим.


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

Наиболее безопасный способ модификации кода:

import MagicString from 'magic-string';

transform(code, id) {
    const s = new MagicString(code);

    s.replace('foo', 'bar');

    return {
        code: s.toString(),
        map: s.generateMap({
            hires: true
        })
    };
}

Проверка условий выполнения

command и mode

Многие ошибки связаны с неправильным окружением.

config(config, { command, mode }) {
    console.log(command);
    console.log(mode);
}

Возможные значения:

serve
build

Различия между serve и build

Некоторые хуки работают по-разному:

Хук Serve Build
configureServer Да Нет
handleHotUpdate Да Нет
generateBundle Нет Да
writeBundle Нет Да

Использование Node.js Inspector

Запуск Vite в режиме инспектора

node --inspect-brk ./node_modules/vite/bin/vite.js

После запуска можно подключиться через Chrome DevTools.


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

Breakpoint особенно полезен внутри:

  • transform;
  • resolveId;
  • load;
  • generateBundle;
  • handleHotUpdate.

Отладка асинхронных хуков

async transform(code, id) {
    debugger;

    const result = await processCode(code);

    return result;
}

Отладка Rollup-части плагина

Vite использует Rollup при production-сборке, поэтому необходимо проверять:

  • generateBundle;
  • renderChunk;
  • outputOptions;
  • buildEnd.

Проверка bundle

generateBundle(options, bundle) {
    console.log(Object.keys(bundle));
}

Анализ чанков

generateBundle(options, bundle) {
    for (const [fileName, chunk] of Object.entries(bundle)) {
        console.log(fileName);

        if (chunk.type === 'chunk') {
            console.log(chunk.modules);
        }
    }
}

Анализ графа модулей

Доступ к moduleGraph

configureServer(server) {
    console.log(server.moduleGraph);
}

Получение информации о модуле

const mod = server.moduleGraph.getModuleById(id);

console.log(mod);

Полезные поля:

  • url;
  • importers;
  • importedModules;
  • transformResult;
  • lastHMRTimestamp.

Изоляция ошибок

Временное отключение плагинов

Очень часто проблема вызвана конфликтом.

plugins: [
    pluginA(),
    // pluginB(),
    pluginC()
]

Минимальный reproduction

Для сложных проблем создаётся минимальный проект:

src/
vite.config.js
package.json

Без:

  • UI-библиотек;
  • TypeScript;
  • CSS-фреймворков;
  • SSR;
  • лишних плагинов.

Проверка порядка плагинов

enforce

export default function myPlugin() {
    return {
        name: 'my-plugin',
        enforce: 'pre'
    };
}

Варианты:

pre
post

Диагностика конфликтов

Если другой плагин изменяет код раньше:

transform(code) {
    console.log(code);
}

можно увидеть уже модифицированный результат.


Отладка transformInclude

Некоторые плагины используют фильтрацию файлов.

transform(code, id) {
    if (!id.endsWith('.js')) {
        return;
    }
}

Ошибки:

  • обработка query-параметров;
  • неправильное сравнение расширения;
  • игнорирование .vue;
  • потеря virtual-модулей.

Проблема query-параметров

Vite добавляет параметры:

Component.vue?vue&type=script

Проверка:

id.endsWith('.vue')

не сработает.


Корректный вариант

const cleanId = id.split('?')[0];

Отладка производительности

Измерение времени

transform(code, id) {
    const start = performance.now();

    const result = heavyTransform(code);

    console.log(
        id,
        performance.now() - start
    );

    return result;
}

Поиск медленных хуков

Особенно важны:

  • transform;
  • resolveId;
  • load.

Они вызываются очень часто.


Использование try/catch

Локализация ошибок

async transform(code, id) {
    try {
        return await compile(code);
    } catch (error) {
        console.error(id);
        throw error;
    }
}

Добавление контекста

catch (error) {
    error.message =
        `[my-plugin] ${id}\n` +
        error.message;

    throw error;
}

Использование this.error

В Rollup/Vite рекомендуется использовать встроенный API ошибок.

transform(code, id) {
    if (!isValid(code)) {
        this.error(
            `Invalid syntax in ${id}`
        );
    }
}

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

  • красивый вывод;
  • корректный stack trace;
  • интеграция с Rollup;
  • остановка сборки.

Использование this.warn

Для неблокирующих проблем:

this.warn(
    `Deprecated API in ${id}`
);

Проверка кэша

Проблемы кеширования

Vite активно кеширует:

  • transform;
  • prebundle;
  • module graph;
  • optimized deps.

Очистка кеша

rm -rf node_modules/.vite

или:

vite --force

Отладка optimizeDeps

Проблемы pre-bundling часто маскируются под ошибки плагина.

Диагностика:

configResolved(config) {
    console.log(config.optimizeDeps);
}

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

Один из наиболее полезных хуков для диагностики.

configResolved(config) {
    console.log(config.plugins);
}

Позволяет:

  • проверить итоговую конфигурацию;
  • увидеть список плагинов;
  • определить порядок;
  • изучить alias;
  • проверить env;
  • анализировать optimizeDeps.

Отладка SSR

Проверка SSR-режима

transform(code, id, options) {
    console.log(options?.ssr);
}

Отличия SSR

В SSR:

  • другой pipeline;
  • иная обработка import;
  • отсутствие browser API;
  • другой HMR;
  • отдельный module graph.

Диагностика ошибок Windows-путей

Нормализация путей

Проблема:

C:\project\src\main.js

vs

C:/project/src/main.js

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

import { normalizePath } from 'vite';

const normalized = normalizePath(id);

Использование vite-plugin-inspect

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

Установка:

npm install vite-plugin-inspect -D

Подключение:

import Inspect from 'vite-plugin-inspect';

export default {
    plugins: [
        Inspect()
    ]
};

Возможности vite-plugin-inspect

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

  • какие плагины изменили модуль;
  • результат каждой трансформации;
  • порядок обработки;
  • итоговый код;
  • sourcemap;
  • virtual modules.

Интерфейс inspect

После запуска доступен адрес:

/__inspect/

Там можно анализировать:

  • transform pipeline;
  • resolve pipeline;
  • virtual files;
  • HMR graph.

Типичная стратегия отладки

Этап 1 — проверка вызова хука

console.log('transform', id);

Этап 2 — проверка условий

console.log(id);
console.log(command);
console.log(ssr);

Этап 3 — проверка результата

return {
    code,
    map
};

Этап 4 — проверка других плагинов

configResolved(config) {
    console.log(
        config.plugins.map(
            p => p.name
        )
    );
}

Этап 5 — анализ transform pipeline

Используется vite-plugin-inspect.


Частые симптомы и причины

Симптом Возможная причина
transform вызывается дважды SSR + client
Бесконечный HMR invalidate внутри update
Не работает virtual module отсутствует \0
Не срабатывает transform неверный id
Ломаются breakpoint некорректный sourcemap
Ошибка только в build Rollup hook
Ошибка только в serve Vite dev pipeline
Дублирование кода повторная трансформация
Не обновляется модуль кеш moduleGraph
Плагин игнорируется неправильный enforce