Автоматическое определение JSX-файлов

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

При обработке модуля пайплайн esbuild проходит этап определения загрузчика (loader resolution). На этом этапе каждому файлу сопоставляется конкретный парсер и трансформер: JavaScript, JSX, TypeScript, TSX, JSON, текст и другие форматы.


Механизм выбора loader для JSX

JSX-обработка в esbuild активируется через загрузчики jsx и tsx. Их выбор зависит от расширения файла:

  • .jsx → автоматически используется loader jsx
  • .tsx → автоматически используется loader tsx
  • .js → loader js (JSX не включён)
  • .ts → loader ts (без JSX)

Ключевая особенность: наличие JSX-синтаксиса в .js файле не приводит к автоматическому переключению режима парсинга.


Таблица соответствия расширений и поведения

Расширение Loader Поддержка JSX
.js js нет
.jsx jsx да
.ts ts нет
.tsx tsx да
.mjs js нет
.cjs js нет

Поведение при наличии JSX в .js файлах

Если файл .js содержит JSX-разметку:

const app = <div>Hello</div>

esbuild выдаст ошибку синтаксиса, поскольку стандартный parser для js не ожидает JSX-узлы.

Это одно из наиболее частых расхождений между ожиданиями разработчиков и реальным поведением сборщика: JSX не определяется эвристически.


Явное включение JSX для .js файлов

Чтобы заставить esbuild обрабатывать .js как JSX, необходимо переопределить loader:

CLI вариант

esbuild src/index.js --loader:.js=jsx

JavaScript API

import * as esbuild from 'esbuild'

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  loader: {
    '.js': 'jsx'
  }
})

Такой подход изменяет глобальное правило интерпретации расширения и может повлиять на весь проект, включая сторонние зависимости, если они резолвятся через .js.


Влияние расширения на трансформацию JSX

Loader jsx и tsx отличаются не только поддержкой синтаксиса, но и стратегией обработки TypeScript-аннотаций.

  • jsx — только JavaScript + JSX
  • tsx — TypeScript + JSX

Внутренне esbuild использует разные грамматики:

  • JSX grammar extension подключается поверх JavaScript parser
  • TSX включает дополнительно type annotation parsing

JSX runtime и автоматическая вставка импорта

При использовании JSX esbuild может автоматически добавлять runtime-импорт в зависимости от настроек:

esbuild.build({
  jsx: 'automatic',
  jsxImportSource: 'react'
})

При этом автоматическое определение файлов никак не влияет на выбор runtime — оно определяется отдельно от loader-механизма.


Особенности работы с TypeScript и TSX

Для .tsx файлов JSX включён всегда, но TypeScript-парсер требует строгого соблюдения синтаксиса типов.

const element: JSX.Element = <div />

Если файл имеет расширение .ts, даже при наличии JSX он не будет интерпретирован корректно без переопределения loader:

loader: {
  '.ts': 'tsx'
}

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


CLI и API: различия в определении JSX-файлов

CLI esbuild опирается исключительно на расширения и флаги:

esbuild app.js --bundle --jsx=automatic

Но ключевым параметром остаётся --loader, а не --jsx. Флаг --jsx влияет только на трансформацию, но не на определение возможности парсинга JSX в .js.

JavaScript API предоставляет более точный контроль:

  • loader
  • jsx
  • jsxFactory
  • jsxFragment
  • jsxImportSource

Переопределение поведения loader на уровне проекта

В монорепозиториях часто возникает необходимость унифицировать обработку JSX:

loader: {
  '.js': 'jsx',
  '.mjs': 'jsx'
}

Однако такое решение влияет на:

  • сторонние пакеты
  • node_modules
  • ESM/CJS интерпретацию

Поэтому практикуется ограниченное применение через include/exclude плагины.


Плагины и вмешательство в определение JSX

Хотя esbuild не анализирует содержимое файла для определения JSX, плагины могут перехватывать загрузку и подменять loader:

const jsxPlugin = {
  name: 'jsx-detection',
  setup(build) {
    build.onLoad({ filter: /\.js$/ }, async (args) => {
      const source = await fs.promises.readFile(args.path, 'utf8')

      if (source.includes('<') && source.includes('>')) {
        return {
          loader: 'jsx',
          contents: source
        }
      }
    })
  }
}

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

  • увеличивает время сборки
  • не гарантирует корректность (ложные срабатывания)
  • усложняет кеширование

Кэширование и влияние loader на производительность

Loader напрямую влияет на ключ кеша модуля. Файл a.js с loader js и тот же файл с loader jsx рассматриваются как разные сущности в графе зависимостей.

Это означает:

  • изменение loader приводит к полной инвалидации кеша
  • смешивание JSX и JS loader в одном расширении ухудшает стабильность инкрементальной сборки

Типичные ошибки при работе с JSX-определением

  1. Использование JSX в .js без loader:

    • приводит к синтаксической ошибке
  2. Попытка полагаться на авто-распознавание:

    • esbuild не анализирует AST до выбора loader
  3. Конфликт .ts и tsx:

    • JSX в .ts игнорируется без переопределения
  4. Глобальное переопределение .js=jsx:

    • ломает зависимости в node_modules

Поведение в связке с React и современными runtime

При использовании React 17+ с automatic runtime:

jsx: 'automatic'

и файлов .jsx/.tsx esbuild автоматически вставляет импорты из источника, указанного в jsxImportSource.

Loader при этом остаётся единственным механизмом, определяющим, будет ли JSX вообще обрабатываться.


Итоговая модель определения JSX-файлов внутри esbuild

Процесс можно свести к последовательности:

  1. Определение расширения файла
  2. Выбор loader на основе таблицы или пользовательской конфигурации
  3. Парсинг кода соответствующим синтаксическим анализатором
  4. Применение JSX-трансформации только для jsx или tsx loader
  5. Генерация итогового JS-кода без повторного анализа содержимого файла