Обработка ошибок и предупреждений в плагинах

В плагинах Esbuild ошибки формируются не только через выбрасывание исключений, но и через специализированные механизмы API, которые позволяют точно привязывать проблему к этапу сборки и исходному коду. Основные точки вмешательства — хуки onResolve и onLoad, где происходит разрешение путей и загрузка модулей.

Внутри этих хуков доступен контекст плагина (PluginBuild), предоставляющий методы:

  • this.error(...) — фиксация ошибки сборки
  • this.warn(...) — формирование предупреждения

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

Пример логики обработки в onResolve:

onResolve({ filter: /\.txt$/ }, (args) => {
  if (args.path.includes("forbidden")) {
    this.error({
      text: "Использование запрещённого пути",
      location: {
        file: args.importer,
        line: 1,
        column: 0
      }
    })
  }

  return {
    path: args.path,
    namespace: "text"
  }
})

Здесь ошибка не прерывает выполнение немедленно, а регистрируется в системе диагностики сборки.


Генерация ошибок в onLoad

Хук onLoad отвечает за загрузку содержимого модуля. Это наиболее частое место для генерации ошибок, связанных с чтением файлов, парсингом или трансформацией содержимого.

Типовой сценарий — невозможность прочитать файл:

onLoad({ filter: /.*/, namespace: "file" }, async (args) => {
  let source

  try {
    source = await fs.promises.readFile(args.path, "utf8")
  } catch (err) {
    this.error({
      text: `Не удалось прочитать файл: ${err.message}`,
      location: null
    })
    return
  }

  return {
    contents: source,
    loader: "text"
  }
})

Ключевой момент: возврат undefined после this.error не обязательно прерывает сборку полностью, но сигнализирует о проблеме в конкретном модуле.


Отличие throw от this.error

Использование throw в плагинах Esbuild приводит к немедленному прерыванию выполнения текущего хука. Это поведение удобно для критических ошибок, когда дальнейшая сборка теряет смысл.

Однако в большинстве случаев предпочтительнее this.error, потому что:

  • ошибки собираются в едином отчёте Esbuild
  • сборка может продолжаться для остальных модулей
  • сохраняется контекст файла и позиции
  • улучшается диагностика в CLI и API

throw целесообразен в ситуациях, когда невозможно продолжить обработку даже текущего файла, например при повреждении внутреннего состояния плагина.


Формирование предупреждений

Предупреждения (this.warn) используются для сигнализации о некритичных проблемах: устаревшие API, подозрительные импорты, потенциальные ошибки оптимизации.

onResolve({ filter: /\.legacy\.js$/ }, (args) => {
  this.warn({
    text: "Используется устаревший модуль",
    location: {
      file: args.importer,
      line: 10,
      column: 5
    }
  })

  return {
    path: args.path,
    namespace: "file"
  }
})

В отличие от ошибок, предупреждения не влияют на статус завершения сборки, но могут быть преобразованы в ошибки через настройки CLI или API.


Структура диагностической информации

Объекты ошибок и предупреждений в Esbuild поддерживают унифицированный формат:

  • text — сообщение
  • location — позиция в исходном коде
  • notes — дополнительные пояснения (в некоторых сценариях)
  • detail — расширенные данные (не всегда отображаются в CLI)

Пример расширенной ошибки:

this.error({
  text: "Неверный формат конфигурации",
  location: {
    file: args.path,
    line: 3,
    column: 15
  },
  notes: [
    {
      text: "Ожидается JSON-объект"
    }
  ]
})

Такая структура позволяет формировать многоуровневую диагностику, полезную при разработке сложных трансформационных плагинов.


Ошибки в цепочке плагинов

Esbuild выполняет плагины последовательно в рамках цепочки onResolveonLoad. Ошибка может возникнуть на любом этапе, и её обработка зависит от точки возникновения.

Если ошибка происходит в onResolve, модуль может быть исключён из дальнейшей обработки. Если в onLoad, модуль считается частично или полностью не загруженным.

При этом важно учитывать:

  • один модуль может быть обработан несколькими плагинами
  • каждый плагин может добавлять собственные ошибки и предупреждения
  • итоговый отчёт агрегируется на уровне всей сборки

Это означает, что плагины не изолированы и ошибки могут накапливаться от разных источников.


Контроль потока выполнения через ошибки

Плагины Esbuild часто используют ошибки как механизм управления потоком выполнения. Например, можно остановить дальнейшую обработку конкретного namespace:

onLoad({ namespace: "json" }, (args) => {
  const data = JSON.parse(fs.readFileSync(args.path, "utf8"))

  if (!data.version) {
    this.error({
      text: "Отсутствует обязательное поле version"
    })
    return
  }

  return {
    contents: JSON.stringify(data),
    loader: "json"
  }
})

Такая модель позволяет строить строгие схемы валидации входных данных прямо на этапе сборки.


Агрегация ошибок и поведение сборки

Esbuild не останавливается при первой ошибке (если не используется режим --log-level=error с жёсткими настройками). Вместо этого:

  • собирает все ошибки из всех плагинов
  • группирует их по файлам
  • выводит структурированный отчёт

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

При API-использовании результат сборки содержит массив errors, доступный для постобработки:

const result = await esbuild.build({ /* config */ })

console.log(result.errors)

Локации и точность диагностики

Корректное заполнение location критически важно для удобства отладки. Esbuild поддерживает:

  • файл (file)
  • строку (line)
  • колонку (column)
  • длину фрагмента (в некоторых случаях)

Отсутствие точной позиции приводит к менее информативным сообщениям, особенно при трансформации кода.

Пример улучшенной локализации:

this.error({
  text: "Синтаксическая ошибка в выражении",
  location: {
    file: args.path,
    line: error.line,
    column: error.column
  }
})

Обработка ошибок асинхронных операций

Плагины часто используют асинхронные операции: чтение файлов, запросы к API, вычисления.

В таких случаях ошибки должны обрабатываться внутри try/catch, иначе они могут быть потеряны или приведут к неконтролируемому падению процесса.

onLoad({ filter: /.*/ }, async (args) => {
  try {
    const res = await fetch(`https://api.example.com/meta?file=${args.path}`)
    const json = await res.json()

    return {
      contents: JSON.stringify(json),
      loader: "json"
    }
  } catch (e) {
    this.error({
      text: `Ошибка загрузки метаданных: ${e.message}`
    })
  }
})

Асинхронные ошибки особенно важны в плагинах, которые интегрируются с внешними системами.


Предсказуемость ошибок в пользовательских плагинах

При разработке сложных плагинов важно соблюдать предсказуемую модель ошибок:

  • одинаковый формат сообщений
  • единая стратегия обработки (не смешивать throw и this.error без необходимости)
  • минимизация побочных эффектов
  • явное указание источника проблемы

Это позволяет другим разработчикам интегрировать плагин без необходимости разбираться в нестандартном поведении сборки.


Ошибки трансформации содержимого

При возврате результата из onLoad можно столкнуться с ошибками преобразования:

  • некорректный синтаксис
  • повреждённые данные
  • несовместимые форматы

Такие ошибки обычно фиксируются до возврата результата:

onLoad({ filter: /\.data$/ }, (args) => {
  const raw = fs.readFileSync(args.path, "utf8")

  if (!raw.startsWith("{")) {
    this.error({
      text: "Ожидается JSON-подобный формат"
    })
  }

  const parsed = JSON.parse(raw)

  return {
    contents: JSON.stringify(parsed),
    loader: "json"
  }
})

Предупреждения как инструмент анализа кода

Предупреждения в Esbuild часто используются не для сигнализации о проблемах, а для анализа качества кода:

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

Такой подход позволяет использовать плагины как инструмент статического анализа, а не только сборки.

this.warn({
  text: "Импорт большого файла может замедлить сборку",
  location: {
    file: args.importer
  }
})

Ошибки в многопоточном контексте сборки

Esbuild выполняет сборку параллельно, поэтому ошибки из разных потоков могут приходить не в строгом порядке. Это влияет на:

  • порядок отображения ошибок
  • трассировку причин
  • интерпретацию цепочек зависимостей

Плагины должны быть независимыми и не полагаться на глобальное состояние для диагностики, иначе ошибки становятся недетерминированными.


Практика изоляции ошибок внутри плагина

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

function safeParse(input, ctx) {
  try {
    return JSON.parse(input)
  } catch (e) {
    ctx.error({
      text: "Ошибка парсинга JSON"
    })
    return null
  }
}

Это снижает вероятность распространения ошибки по цепочке обработки.


Поведение при множественных ошибках одного модуля

Один модуль может генерировать несколько ошибок в разных фазах обработки. Esbuild агрегирует их, не останавливаясь после первой.

Это поведение важно учитывать при:

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

Каждая ошибка должна быть самодостаточной и не зависеть от контекста предыдущих сообщений, чтобы сохранять читаемость отчёта диагностики.