При компиляции современного JavaScript-кода исходные файлы редко попадают в браузер в первоначальном виде. Код проходит через несколько этапов обработки:
После такой цепочки исходный код существенно отличается от того, который исполняется браузером. Без специальных механизмов отладка становится крайне затруднительной: сообщения об ошибках указывают на строки и позиции внутри итогового бандла, а не исходных файлов проекта.
Для решения этой проблемы используются source maps — специальные карты соответствия между итоговым и исходным кодом.
Esbuild поддерживает генерацию и объединение source maps практически на всех этапах сборки, что особенно важно при многоэтапных пайплайнах.
Source map представляет собой JSON-документ, который хранит информацию о соответствии между:
Упрощённо карта сообщает браузеру:
данный участок минифицированного кода был получен из определённой строки определённого исходного файла.
Например, имеется исходный TypeScript:
function calculatePrice(price: number) {
return price * 1.2;
}
console.log(calculatePrice(100));
После минификации код может выглядеть так:
function e(o){return 1.2*o}console.log(e(100));
Без source map ошибка внутри функции будет указывать на позицию внутри строки минифицированного файла.
При наличии карты браузер восстановит информацию и покажет реальное расположение ошибки в TypeScript-файле.
Esbuild поддерживает несколько режимов генерации карт.
Создаётся отдельный файл карты.
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
sourcemap: true
});
или
sourcemap: 'linked'
Результат:
dist/
├─ app.js
└─ app.js.map
В конец файла добавляется ссылка:
//# sourceMappingURL=app.js.map
Это наиболее распространённый вариант для разработки.
Карта создаётся отдельно, но ссылка в файл не добавляется.
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
sourcemap: 'external'
});
Полезно при публикации карт на отдельном сервере.
Карта встраивается прямо в JavaScript-файл.
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
sourcemap: 'inline'
});
Файл содержит большой Base64-блок:
//# sourceMappingURL=dat a:application/json;base64,...
Преимущества:
Недостатки:
Комбинированный режим.
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
sourcemap: 'both'
});
Создаются:
Используется редко.
Рассмотрим типичную цепочку обработки:
TypeScript
↓
Babel
↓
Esbuild
↓
Minifier
Каждый этап меняет структуру программы.
Если хотя бы один инструмент не передаст информацию о предыдущих преобразованиях, итоговая карта окажется некорректной.
Например:
index.ts
↓
index.js + map
↓
bundle.js
Если второй этап проигнорирует существующую карту:
index.ts
↓
index.js
↓
bundle.js
браузер сможет отобразить только промежуточный JavaScript, но не оригинальный TypeScript.
Поэтому в многоступенчатых процессах сборки требуется цепочка source maps.
Одной из сильных сторон Esbuild является автоматическое объединение входящих карт.
Допустим, TypeScript-компилятор уже создал карту:
{
"version": 3,
...
}
После этого Esbuild получает:
input.js
input.js.map
Во время сборки Esbuild:
В результате браузер получает прямое соответствие:
bundle.js
↓
index.ts
без промежуточных уровней.
Частый сценарий выглядит следующим образом:
{
"compilerOptions": {
"sourceMap": true
}
}
Компиляция:
tsc
Получаем:
src/
├─ index.ts
├─ index.js
└─ index.js.map
Далее выполняется сборка через Esbuild:
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
bundle: true,
sourcemap: true
});
Esbuild автоматически прочитает:
index.js.map
и включит сведения в итоговую карту.
На практике чаще используется другой вариант.
Esbuild самостоятельно обрабатывает TypeScript:
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/app.js',
sourcemap: true
});
В этом случае цепочка преобразований полностью контролируется Esbuild.
Преимущества:
При преобразовании JSX возникают серьёзные изменения структуры кода.
Исходник:
export function App() {
return <h1>Hello</h1>;
}
После компиляции:
export function App() {
return React.createElement("h1", null, "Hello");
}
Количество строк, колонок и символов меняется.
Esbuild фиксирует эти преобразования в карте:
await esbuild.build({
entryPoints: ['src/App.tsx'],
bundle: true,
sourcemap: true
});
В результате React DevTools и браузерные инструменты продолжают показывать оригинальный TSX-код.
Минификация создаёт наиболее серьёзные различия между исходным и итоговым кодом.
Исходный код:
const calculateTax = (price) => {
return price * 0.2;
};
После минификации:
const c=e=>.2*e;
Включение карты:
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
minify: true,
sourcemap: true
});
позволяет браузеру восстанавливать оригинальные:
Обычно применяются разные настройки.
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
sourcemap: true
});
или
sourcemap: 'inline'
Цель:
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
sourcemap: 'external'
});
Такой вариант позволяет:
По умолчанию карта может содержать полный текст исходных файлов.
Это поле называется:
"sourcesContent"
Пример:
{
"sourcesContent": [
"const x = 1;"
]
}
Преимущество:
Недостаток:
Отключение:
await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/app.js',
sourcemap: true,
sourcesContent: false
});
Теперь карта будет хранить только ссылки на файлы.
В некоторых случаях исходные файлы располагаются отдельно от карты.
Например:
dist/
├─ app.js
└─ app.js.map
src/
└─ index.ts
Для корректного поиска файлов может использоваться поле:
{
"sourceRoot": "../src"
}
Esbuild обычно формирует пути автоматически, однако при интеграции с другими инструментами иногда требуется дополнительная настройка структуры каталогов.
Source maps полезны не только в браузере.
После минификации Node.js может выдавать ошибки вида:
TypeError: Cannot read properties of undefined
at app.js:1:24567
При наличии карты возможна обратная трансляция:
TypeError: Cannot read properties of undefined
at services/user.ts:42:15
Для этого используются специальные библиотеки:
npm install source-map-support
Подключение:
import 'source-map-support/register';
Теперь стек вызовов будет отображать реальные исходные файлы.
Метод transform() также поддерживает source maps.
const result = await esbuild.transform(code, {
loader: 'ts',
sourcemap: true
});
Результат:
result.code
result.map
Это особенно полезно при построении собственных сборочных пайплайнов.
Если код уже содержит карту:
//# sourceMappingURL=input.js.map
можно объединять преобразования последовательно.
Схема:
Tool A
↓
map A
Tool B
↓
map B
Tool C
↓
final map
Esbuild поддерживает такие сценарии, сохраняя связь между исходным кодом и конечным результатом.
После сложной многоэтапной сборки рекомендуется проверить:
Типичный признак ошибки:
bundle.js:1:32567
вместо:
src/components/Button.tsx:18:7
Подобная ситуация почти всегда означает потерю одной из промежуточных карт.
Например:
babel(input, {
sourceMaps: false
});
Цепочка обрывается.
В результате Esbuild не сможет восстановить исходное соответствие.
Некоторые инструменты удаляют:
//# sourceMappingURL=file.js.map
После этого следующая стадия сборки не находит карту.
Ошибка:
Failed to load source map
часто связана с неправильным расположением файлов:
app.js
app.js.map
и ссылкой:
//# sourceMappingURL=maps/app.js.map
которая указывает на несуществующий путь.
Пример проблемной схемы:
Tool A → inline map
Tool B → external map
Tool C → inline map
Хотя Esbuild способен работать с подобными комбинациями, единый подход обычно обеспечивает более предсказуемый результат.
Современный процесс может выглядеть следующим образом:
TypeScript
↓
Esbuild (TS → JS)
↓
Esbuild Bundle
↓
Esbuild Minify
↓
Source Map
Конфигурация:
await esbuild.build({
entryPoints: ['src/index.ts'],
outfile: 'dist/app.js',
bundle: true,
minify: true,
sourcemap: 'external',
sourcesContent: true
});
Результат:
dist/
├─ app.js
└─ app.js.map
При возникновении ошибки в минифицированном коде браузер или серверная среда смогут восстановить точное местоположение проблемы вплоть до исходного TypeScript-файла, несмотря на множество промежуточных этапов обработки.