Отладка в DevTools с source maps от esbuild

После сборки проекта исходный код обычно подвергается различным преобразованиям:

  • объединяется в один или несколько файлов;
  • минифицируется;
  • транспилируется из TypeScript в JavaScript;
  • преобразуется из JSX или TSX;
  • проходит через дополнительные плагины и инструменты.

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

Source map — специальный файл сопоставления, позволяющий браузерным инструментам разработки связать сгенерированный код с оригинальными исходниками. Благодаря этому DevTools отображает настоящий исходный код вместо результата сборки.

Без source maps ошибка может выглядеть следующим образом:

bundle.js:1 Uncaught TypeError: Cannot read properties of undefined

При этом весь код находится в одной минифицированной строке.

С source maps браузер показывает:

src/components/UserProfile.tsx:27

и позволяет сразу перейти к проблемному месту.


Поддержка source maps в esbuild

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

Основные режимы работы:

Режим Описание
linked Создается отдельный файл карты исходников
external Создается внешний файл карты без ссылки в JS
inline Карта встраивается непосредственно в итоговый файл
both Генерируются встроенная и внешняя карты одновременно

Простейшая настройка:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true,
  outfile: 'dist/app.js'
});

Эквивалентно:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: 'linked',
  outfile: 'dist/app.js'
});

После сборки появятся файлы:

dist/
 ├─ app.js
 └─ app.js.map

В конец JavaScript-файла автоматически добавляется ссылка:

//# sourceMappingURL=app.js.map

Браузер обнаруживает эту директиву и загружает карту исходников.


Виды source maps в esbuild

linked

Наиболее распространённый вариант.

Конфигурация:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: 'linked',
  outfile: 'dist/app.js'
});

Результат:

app.js
app.js.map

Внутри app.js:

//# sourceMappingURL=app.js.map

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

  • удобная работа в браузере;
  • небольшой размер JS-файла;
  • стандартный вариант для разработки.

Недостатки:

  • требуется дополнительный запрос к файлу карты.

inline

Source map кодируется в формате Base64 и помещается непосредственно в итоговый файл.

Конфигурация:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: 'inline',
  outfile: 'dist/app.js'
});

Фрагмент результата:

//# sourceMappingURL=dat a:application/json;base64,...

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

  • отсутствуют дополнительные файлы;
  • удобно для локальной разработки.

Недостатки:

  • существенно увеличивается размер бандла.

external

Карта создаётся отдельно, но ссылка в JavaScript не добавляется.

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: 'external',
  outfile: 'dist/app.js'
});

Результат:

app.js
app.js.map

Однако в файле app.js не будет строки:

//# sourceMappingURL=app.js.map

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


both

Одновременно создаются две версии карты.

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: 'both',
  outfile: 'dist/app.js'
});

Создаются:

app.js
app.js.map

При этом карта присутствует и внутри JS-файла.


Настройка source maps через CLI

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

Включение source maps:

esbuild src/index.ts \
  --bundle \
  --sourcemap \
  --outfile=dist/app.js

Явное указание режима:

esbuild src/index.ts \
  --bundle \
  --sourcemap=inline \
  --outfile=dist/app.js

Другие варианты:

--sourcemap=linked
--sourcemap=external
--sourcemap=both

Работа source maps с TypeScript

Одним из наиболее важных сценариев является отладка TypeScript-приложений.

Исходный код:

interface User {
  name: string;
}

function printUser(user: User) {
  console.log(user.name.toUpperCase());
}

printUser(undefined as any);

Сборка:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true,
  outfile: 'dist/app.js'
});

При возникновении ошибки DevTools покажет:

src/index.ts

а не:

dist/app.js

Стек вызовов также будет ссылаться на TypeScript-файлы.


Отладка React-приложений

Source maps особенно полезны при работе с JSX и TSX.

Компонент:

export function UserCard({ user }) {
  return (
    <div>
      {user.name.toUpperCase()}
    </div>
  );
}

После сборки JSX превращается в вызовы React API:

jsx("div", {
  children: user.name.toUpperCase()
});

Без карты исходников DevTools показывает именно этот код.

С включёнными source maps отображается исходный TSX-файл:

{user.name.toUpperCase()}

Точки останова ставятся непосредственно в компоненте.


Использование точек останова

После генерации source maps DevTools позволяет устанавливать breakpoints в оригинальных файлах.

Пример:

function calculatePrice(price: number) {
  return price * 1.2;
}

Вкладка Sources отображает:

src/
 └─ utils/
     └─ price.ts

Точка останова ставится на строку:

return price * 1.2;

Хотя в браузере исполняется уже собранный код.


Анализ стека вызовов

Рассмотрим пример:

function stepOne() {
  stepTwo();
}

function stepTwo() {
  stepThree();
}

function stepThree() {
  throw new Error('Failure');
}

stepOne();

Без source maps стек может выглядеть так:

app.js:1
app.js:1
app.js:1

С source maps:

stepThree (src/main.ts:9)
stepTwo   (src/main.ts:5)
stepOne   (src/main.ts:1)

Диагностика становится значительно проще.


Отладка минифицированного кода

В production-сборках часто используется минификация.

Настройка:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  minify: true,
  sourcemap: true,
  outfile: 'dist/app.js'
});

Исходный код:

function calculateDiscount(price, discount) {
  return price - price * discount;
}

После минификации:

function n(t,e){return t-t*e}

Без source maps анализ подобного кода крайне неудобен.

При наличии карты DevTools восстанавливает исходное представление файла.


Опция sourcesContent

По умолчанию esbuild помещает содержимое исходников непосредственно в source map.

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

Настройка:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true,
  sourcesContent: true,
  outfile: 'dist/app.js'
});

Фрагмент карты:

{
  "sources": [
    "../src/index.ts"
  ],
  "sourcesContent": [
    "const message = 'Hello';"
  ]
}

Отключение sourcesContent

Иногда необходимо уменьшить размер карты.

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true,
  sourcesContent: false,
  outfile: 'dist/app.js'
});

Теперь карта содержит только ссылки на исходники:

{
  "sources": [
    "../src/index.ts"
  ]
}

Браузеру потребуется доступ к самим файлам.


Source maps и код-сплиттинг

Esbuild поддерживает создание отдельных чанков.

Конфигурация:

await esbuild.build({
  entryPoints: [
    'src/home.ts',
    'src/admin.ts'
  ],
  bundle: true,
  splitting: true,
  format: 'esm',
  sourcemap: true,
  outdir: 'dist'
});

Результат:

dist/
 ├─ home.js
 ├─ home.js.map
 ├─ admin.js
 ├─ admin.js.map
 ├─ chunk-A.js
 └─ chunk-A.js.map

Каждый чанк получает собственную карту исходников.

При переходе между модулями DevTools автоматически связывает их с соответствующими исходными файлами.


Проверка корректности загрузки source maps

В Chrome DevTools открыть вкладку:

Sources

В дереве файлов должны отображаться исходники проекта:

webpack://
src/

или

localhost
 └─ src

В зависимости от структуры сборки.

Дополнительно можно проверить вкладку:

Network

Файл карты должен успешно загружаться:

app.js.map
Status: 200

Если карта отсутствует:

Status: 404

браузер не сможет восстановить исходный код.


Типичные проблемы при работе с source maps

Карты не генерируются

Причина:

sourcemap: false

или отсутствие параметра.

Исправление:

sourcemap: true

Файл карты не публикуется

Сборка создаёт:

app.js.map

но сервер отдаёт только:

app.js

В результате DevTools сообщает:

Failed to load source map

Необходимо публиковать файл карты вместе со сборкой.


Неверные пути к исходникам

Пример структуры:

dist/app.js.map
src/index.ts

Если пути внутри карты повреждены или изменены сторонними инструментами, DevTools не сможет открыть оригинальные файлы.

Проверка содержимого:

{
  "sources": [
    "../src/index.ts"
  ]
}

Проблемы после дополнительной обработки

Иногда после esbuild запускаются:

  • обфускаторы;
  • постпроцессоры;
  • сторонние минификаторы.

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

Симптомы:

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

Рекомендации для разработки

Для локальной разработки наиболее удобной считается конфигурация:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true,
  minify: false,
  outfile: 'dist/app.js'
});

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

  • корректные стеки вызовов;
  • полноценная работа breakpoints;
  • отображение TypeScript и JSX исходников;
  • удобная навигация по проекту через DevTools.

Для production-сборок распространён следующий подход:

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  minify: true,
  sourcemap: 'external',
  outfile: 'dist/app.js'
});

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