Использование source maps для отладки в браузере

При разработке современных JavaScript-приложений исходный код редко попадает в браузер в первоначальном виде. Перед публикацией выполняются транспиляция, объединение модулей, минификация, удаление неиспользуемого кода и другие этапы обработки. В результате итоговый файл может значительно отличаться от исходных модулей проекта.

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

Для решения этой проблемы используются source maps — специальные файлы сопоставления, позволяющие браузеру связывать итоговый код с оригинальными исходниками.

Source map хранит информацию о том:

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

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


Как работают source maps

Предположим, существует файл:

// src/math.js

export function sum(a, b) {
    return a + b;
}

После сборки Rollup может создать следующий результат:

function sum(n,o){return n+o}
export{sum};

Если в коде возникнет ошибка, браузер увидит только минифицированную версию.

При наличии source map инструменты разработчика получают дополнительную информацию:

{
  "version": 3,
  "file": "bundle.js",
  "sources": [
    "src/math.js"
  ]
}

На основании этих данных браузер сможет показать:

export function sum(a, b) {
    return a + b;
}

даже несмотря на то, что фактически выполняется минифицированный код.


Включение source maps в Rollup

Поддержка source maps встроена в Rollup и активируется через параметр sourcemap.

Простейшая конфигурация:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/bundle.js',
        format: 'esm',
        sourcemap: true
    }
};

После сборки будут созданы файлы:

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

Файл .map содержит всю информацию о сопоставлении исходников.


Связь между бандлом и картой исходников

В конец итогового файла автоматически добавляется специальный комментарий:

//# sourceMappingURL=bundle.js.map

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

При открытии страницы DevTools обнаруживает директиву:

//# sourceMappingURL=bundle.js.map

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


Режим sourcemap: true

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

output: {
    file: 'dist/app.js',
    format: 'iife',
    sourcemap: true
}

Результат:

app.js
app.js.map

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

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

Такой вариант чаще всего используется в режиме разработки.


Встроенные source maps

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

Для этого используется режим:

output: {
    file: 'dist/app.js',
    format: 'esm',
    sourcemap: 'inline'
}

В конце файла появится конструкция вида:

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

Карта будет закодирована в Base64 и встроена непосредственно в бандл.

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

  • один файл вместо двух;
  • удобно для тестирования;
  • не требуется переносить отдельный .map файл.

Недостатки:

  • увеличение размера сборки;
  • замедление загрузки;
  • неудобство анализа крупных проектов.

Скрытые source maps

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

Для этого используется режим:

output: {
    file: 'dist/app.js',
    format: 'esm',
    sourcemap: 'hidden'
}

В этом случае:

app.js
app.js.map

будут созданы, но комментарий

//# sourceMappingURL=

в итоговый файл не попадёт.

Такой подход часто используется в production-среде.


Использование source maps с TypeScript

При сборке TypeScript-проектов обычно участвуют два источника карт:

  1. TypeScript Compiler.
  2. Rollup.

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

{
  "compilerOptions": {
    "sourceMap": true
  }
}

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

export default {
    input: 'src/main.ts',

    output: {
        file: 'dist/app.js',
        format: 'esm',
        sourcemap: true
    }
};

Rollup способен объединять карты, созданные TypeScript, с собственными картами сборки.

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

const message: string = 'Hello';

а не промежуточный JavaScript-код.


Использование source maps с Babel

Аналогичная ситуация возникает при использовании Babel.

Плагин:

babel({
    babelHelpers: 'bundled'
})

может генерировать собственные карты преобразований.

Пример цепочки обработки:

TypeScript
    ↓
Babel
    ↓
Rollup

Каждый этап вносит изменения в код.

Если source maps включены на всех этапах, Rollup сможет объединить их в единую карту, сохраняющую связь с первоначальными файлами.


Отладка модульной структуры проекта

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

src/
├── main.js
├── api.js
├── auth.js
└── utils.js

После сборки появляется:

dist/
└── bundle.js

Без source maps браузер покажет только:

bundle.js

С включёнными картами в DevTools можно увидеть:

src/
├── main.js
├── api.js
├── auth.js
└── utils.js

Это значительно упрощает поиск ошибок.


Просмотр исходников в Chrome DevTools

После открытия вкладки Sources отображаются:

Page
 └─ localhost
     └─ dist
         └─ bundle.js

При наличии source maps появляется дополнительная структура:

webpack://
rollup://
src/

или аналогичное дерево исходных файлов.

Разработчик получает возможность:

  • ставить точки останова в оригинальном коде;
  • пошагово выполнять исходные модули;
  • просматривать реальные переменные;
  • анализировать стек вызовов.

Точки останова в исходном коде

Предположим, существует модуль:

export function divide(a, b) {
    return a / b;
}

Точка останова устанавливается непосредственно на строку:

return a / b;

Несмотря на то что браузер выполняет уже объединённый бандл, выполнение остановится именно в исходном модуле.

Это одно из главных преимуществ source maps.


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

Без карт исходников стек может выглядеть так:

app.min.js:1:5421
app.min.js:1:5518
app.min.js:1:5572

Подобный стек практически бесполезен.

С включёнными source maps браузер покажет:

src/api.js:14
src/auth.js:27
src/main.js:53

Поиск проблемы становится значительно проще.


Работа с минифицированным кодом

Часто production-сборка использует плагин минификации.

Например:

import terser from '@rollup/plugin-terser';

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

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/app.min.js',
        format: 'esm',
        sourcemap: true
    },

    plugins: [
        terser()
    ]
};

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

function n(n,t){return n+t}

Однако source map сохранит соответствие исходному варианту:

function add(first, second) {
    return first + second;
}

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

После сборки необходимо убедиться, что карта действительно работает.

Признаки корректной настройки:

  • в каталоге сборки присутствует файл .map;
  • в DevTools отображаются исходные файлы;
  • стек вызовов содержит реальные пути к исходникам;
  • точки останова работают в оригинальном коде;
  • отображаются настоящие номера строк.

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


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

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

Пример:

output: {
    file: 'dist/app.js',
    sourcemap: 'app.debug.map'
}

Результат:

app.js
app.debug.map

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


Параметр sourcemapExcludeSources

По умолчанию карта может содержать содержимое исходных файлов.

Иногда это нежелательно.

Настройка:

output: {
    file: 'dist/app.js',
    format: 'esm',
    sourcemap: true,
    sourcemapExcludeSources: true
}

В результате:

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

Source maps в режиме разработки

Типичная конфигурация разработки:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/bundle.js',
        format: 'esm',
        sourcemap: true
    }
};

Особенности:

  • максимальное удобство отладки;
  • подробные сообщения об ошибках;
  • корректные точки останова;
  • полноценная работа DevTools.

Source maps в production-среде

Для production используются различные стратегии.

Полностью отключить:

sourcemap: false

Создавать отдельный файл:

sourcemap: true

Создавать скрытую карту:

sourcemap: 'hidden'

Наиболее распространённым компромиссом считается вариант:

sourcemap: 'hidden'

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


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

Отсутствует файл .map

Причина:

sourcemap: false

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

Решение:

sourcemap: true

Браузер не видит исходники

Возможные причины:

  • карта не была загружена;
  • неправильный путь к файлу;
  • сервер не отдаёт .map;
  • файл карты удалён после сборки.

Неверные номера строк

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

Например:

TypeScript
  ↓
Babel
  ↓
Rollup

Если Babel не создаёт source maps, итоговая карта может содержать некорректные позиции.


Ошибки после минификации

Некоторые плагины могут нарушать цепочку преобразований.

Признаки:

  • точки останова смещены;
  • стек вызовов содержит неверные строки;
  • DevTools показывает неправильные участки кода.

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


Практическая схема работы

Для современных проектов на Rollup обычно применяется следующая конфигурация:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/app.js',
        format: 'esm',
        sourcemap: true
    }
};

Для production-сборки:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/app.min.js',
        format: 'esm',
        sourcemap: 'hidden'
    }
};

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