При разработке фронтенд-приложений код почти всегда проходит через этапы трансформации:
После сборки исходный код перестаёт быть похожим на оригинал. В production вместо множества файлов появляются компактные bundle-файлы:
(()=>{"use strict";const e=document.querySelector("#app");e.innerHTML="Hello"})();
Без source maps отладка такого кода становится крайне сложной:
Source maps решают эту проблему. Они создают соответствие между:
Браузер использует source map для восстановления исходной структуры проекта во время отладки.
Source map представляет собой JSON-файл со служебной информацией:
{
"version": 3,
"file": "app.min.js",
"sources": ["src/app.js"],
"names": ["document", "querySelector"],
"mappings": "AAAA..."
}
Основные поля:
| Поле | Назначение |
|---|---|
version |
версия формата |
file |
итоговый bundle |
sources |
список исходных файлов |
names |
имена символов |
mappings |
таблица соответствий |
В конце bundle обычно добавляется комментарий:
//# sourceMappingURL=app.js.map
Браузер загружает .map файл и связывает минифицированный
код с оригинальными исходниками.
Библиотека Tom Sel ect активно используется в production-сборках:
import TomSelect fr om 'tom-select';
new TomSelect('#tags');
В процессе сборки:
Без source maps ошибки внутри Tom Sel ect и пользовательских расширений становятся трудночитаемыми.
Например:
Uncaught TypeError at app.min.js:1:82931
С source maps браузер покажет:
plugins/remove_button/plugin.js:52
или:
src/components/sel ect.js:141
Vite поддерживает source maps через параметр
build.sourcemap.
import { defineConfig } fr om 'vite';
export default defineConfig({
build: {
sourcemap: true
}
});
После сборки:
dist/
├── assets/
│ ├── index.js
│ ├── index.js.map
│ ├── index.css
│ └── index.css.map
Полноценные отдельные .map файлы.
build: {
sourcemap: true
}
Преимущества:
Недостатки:
Source map встраивается внутрь bundle.
build: {
sourcemap: 'inline'
}
Результат:
//# sourceMappingURL=dat a:application/json;base64,...
Преимущества:
.map;Недостатки:
Map создаётся, но ссылка в bundle отсутствует.
build: {
sourcemap: 'hidden'
}
Используется для:
Браузер не увидит source map автоматически.
Webpack предоставляет более гибкую систему.
Главный параметр:
module.exports = {
devtool: 'source-map'
};
Максимально точные source maps.
devtool: 'source-map'
Подходит для production.
Source map внутри bundle.
devtool: 'inline-source-map'
Подходит для разработки.
Очень быстрый режим.
devtool: 'eval-source-map'
Используется dev-сервером.
Упрощённые карты.
devtool: 'cheap-source-map'
Особенности:
Скрытые production-карты.
devtool: 'hidden-source-map'
Часто применяется вместе с Sentry.
Tom Sel ect часто используется вместе с Rollup.
export default {
output: {
sourcemap: true
}
};
esbuild поддерживает несколько режимов.
await esbuild.build({
sourcemap: true
});
Создаётся отдельный .map.
await esbuild.build({
sourcemap: 'inline'
});
await esbuild.build({
sourcemap: 'both'
});
Создаются:
.map.TypeScript умеет генерировать карты исходников самостоятельно.
{
"compilerOptions": {
"sourceMap": true
}
}
Результат:
app.js
app.js.map
TypeScript может встраивать оригинальный код прямо в map.
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true
}
}
Преимущество:
Babel умеет передавать source maps между этапами трансформации.
module.exports = {
sourceMaps: true
};
При цепочке:
TypeScript → Babel → Webpack
source maps объединяются.
Современная сборка может выглядеть так:
SCSS
↓
PostCSS
↓
CSS bundle
↓
Minifier
или:
TypeScript
↓
Babel
↓
Webpack
↓
Terser
Каждый этап обязан корректно передавать source maps дальше.
Иначе:
Минификаторы:
умеют сохранять mapping.
new TerserPlugin({
extractComments: false
});
Webpack автоматически связывает source maps с Terser.
Без source maps:
main.js:1
С source maps:
plugins/dropdown_input/plugin.ts:74
new TomSelect('#users', {
render: {
option(data, escape) {
return `<div>${data.name.toUpperCase()}</div>`;
}
}
});
При ошибке debugger покажет реальную строку:
data.name.toUpperCase()
а не минифицированный bundle.
DevTools браузера умеет:
Breakpoint:
bundle.js:1
Breakpoint:
src/select/renderers.ts:91
Tom Select содержит CSS-стили:
.ts-wrapper {
position: relative;
}
После минификации:
.ts-wrapper{position:relative}
Source maps позволяют:
export default defineConfig({
css: {
devSourcemap: true
}
});
module.exports = {
map: true
};
PostCSS сохранит информацию о:
sass.render({
file: 'style.scss',
sourceMap: true
});
Debugger сможет открыть:
style.scss
вместо:
style.css
Source maps могут раскрывать:
Поэтому production source maps требуют осторожности.
build: {
sourcemap: false
}
Максимальная безопасность.
Минусы:
build: {
sourcemap: 'hidden'
}
Оптимальный production-вариант.
Map-файлы:
Sentry умеет декодировать stack trace.
Без source maps:
main.91d2.js:1:82931
С source maps:
src/plugins/tags.ts:114
Обычно используется:
sentry-cli releases files upload-sourcemaps
После загрузки Sentry автоматически восстанавливает:
Source maps влияют на:
devtool: 'cheap-module-source-map'
Ускоряет rebuild.
Полные column mappings тяжёлые.
Cheap-режимы используют только строки.
const isProd = process.env.NODE_ENV === 'production';
export default {
build: {
sourcemap: !isProd
}
};
Вкладка:
Sources
Появляются:
Необходимо убедиться в наличии:
app.js.map
В конце bundle:
//# sourceMappingURL=app.js.map
Причины:
Ошибка:
DevTools failed to load source map
Причина:
404 app.js.map
Причины:
Возможны при:
CDN может:
.map.Важно синхронизировать:
bundle hash
и:
source map hash
Tom Select может подключаться динамически:
const { default: TomSelect } =
await import('tom-select');
Каждый chunk получает собственный source map:
vendor.js
vendor.js.map
После tree shaking:
Корректный bundler обязан пересчитать mappings.
При code splitting:
main.js
admin.js
vendors.js
каждый chunk имеет собственную карту:
main.js.map
admin.js.map
vendors.js.map
Во время Hot Module Replacement source maps:
Node.js тоже поддерживает source maps.
node --enable-source-maps app.js
Стек ошибок TypeScript станет читаемым:
src/server.ts:41
вместо:
dist/server.js:1
SWC поддерживает генерацию карт:
{
"sourceMaps": true
}
Используется в:
Vitest использует source maps для:
Инструменты покрытия:
используют source maps для сопоставления:
coverage → original source
Иначе coverage будет рассчитан по bundle.
import { defineConfig } fr om 'vite';
export default defineConfig({
build: {
sourcemap: 'hidden',
minify: 'esbuild'
},
css: {
devSourcemap: true
}
});
import { defineConfig } fr om 'vite';
export default defineConfig({
build: {
sourcemap: true
}
});
Лучшие варианты:
source-map;inline-source-map;eval-source-map.Предпочтительно:
hidden-source-map;Транспиляция сильно меняет структуру кода.
render: {
item(data) {
return complexTemplate(data);
}
}
TomSelect.define('my_plugin', function() {
// complex logic
});
Множество chunks усложняют диагностику.
Особенно с:
Source maps поддерживаются:
Поддержка реализована через DevTools и не влияет на выполнение JavaScript.
Современные bundler-ы используют спецификацию:
Source Map Revision 3
Она поддерживает:
Map может хранить исходники внутри себя:
{
"sourcesContent": [
"const app = new TomSelect(...);"
]
}
Преимущества:
Недостатки:
.map.Типичный процесс:
Без source maps такой workflow практически невозможен в крупных frontend-проектах.