Добро пожаловать, любители техники! В сегодняшней статье мы начнем увлекательное исследование исходных карт JavaScript. Мы разберем их обратно в исходные кодовые формы, используя Node.js вместе с парой полезных библиотек npm.

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

Вы также можете найти этот сценарий CLI,готовый к использованию, на моем GitHub:
https://github.com /puntorigen/recover-source

Что такое исходная карта и для чего она используется?

Карты исходного кода являются важнейшим компонентом современной веб-разработки, обеспечивая мост между минимизированным/упрощенным производственным кодом и оригинальным исходным кодом. По сути, исходная карта — это карта, которая связывает преобразованную версию вашего кода с исходной версией, что очень полезно при отладке.

JavaScript часто минимизируют или транспилируют, чтобы сделать код более эффективным для использования в продакшене. Однако этот процесс делает код почти нечитаемым, что затрудняет отладку. Здесь нам на помощь приходят Source Maps. Они сопоставляют уменьшенный код обратно с исходным кодом, что значительно упрощает поиск точного местоположения любых ошибок или проблем.

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

Шаг 1. Установите необходимые библиотеки

Нам понадобятся две библиотеки для анализа исходной карты: source-map и fs. Библиотека source-map позволит нам проанализировать исходную карту, а библиотека fs, собственный пакет Node.js, поможет нам прочитать файл.

Создайте новый проект Node.js и установите source-map через npm, выполнив в терминале следующую команду:

npm install source-map

Шаг 2: Чтение исходного файла карты

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

const fs = require('fs');
const rawSourceMap = fs.readFileSync('./path_to_your_sourcemap.map');

Здесь замените «./path_to_your_sourcemap.map» на путь к файлу исходной карты. Функция readFileSync читает файл и возвращает его содержимое.

Шаг 3: Разберите исходную карту

Теперь мы проанализируем исходную карту, используя библиотеку source-map. Сначала импортируйте класс SourceMapConsumer из библиотеки, затем создайте новый его экземпляр с необработанной исходной картой в качестве параметра:

const { SourceMapConsumer } = require('source-map');

let rawSourceMapJson = JSON.parse(rawSourceMap);

new SourceMapConsumer(rawSourceMapJson).then((sourceMapConsumer) => {
  console.log(sourceMapConsumer);
});

Объект SourceMapConsumer предоставляет несколько методов для извлечения информации из исходной карты. В качестве входных данных используется исходная карта (в формате JSON), поэтому мы используем JSON.parse для преобразования нашей исходной карты в объект JavaScript.

Шаг 4: Получите исходный код

Теперь вы можете получить исходный код с помощью объекта SourceMapConsumer. Вот пример кода для печати исходного кода определенной позиции в минимизированном коде:

new SourceMapConsumer(rawSourceMapJson).then((sourceMapConsumer) => {
  const pos = {
    line: 10, // The line number in the minified code
    column: 30 // The column number in the minified code
  };
  console.log(sourceMapConsumer.originalPositionFor(pos));
});

Метод originalPositionFor получает позицию в преобразованном коде и возвращает соответствующую позицию в исходном коде.
Это выведет что-то вроде:

{ source: 'original.js',
  line: 50,
  column: 10,
  name: 'myFunction'
}

Это означает, что код в строке 10, столбце 30 в минимизированном коде соответствует коду в строке 50, столбце 10 в исходном исходном файле original.js. Поле имени указывает исходное имя переменной или функции.

Шаг 5: Получите весь исходный код

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

В большинстве случаев, если поле sourcesContent присутствует в исходной карте, вы можете реконструировать весь исходный код. Вот как это сделать:

new SourceMapConsumer(rawSourceMapJson).then((sourceMapConsumer) => {
  const sources = sourceMapConsumer.sources;

  sources.forEach(source => {
    console.log('Source:', source);
    console.log('Content:', sourceMapConsumer.sourceContentFor(source));
  });
});

Метод sourceContentFor принимает путь к исходному файлу и возвращает исходный код этого файла.

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

Шаг 6: Реконструируйте исходный код

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

const fs = require('fs');
const prettier = require('prettier');
const { SourceMapConsumer } = require('source-map');

const minifiedCode = fs.readFileSync('./path_to_your_minified_file.js', 'utf8');
const rawSourceMap = fs.readFileSync('./path_to_your_sourcemap_file.map', 'utf8');
const rawSourceMapJson = JSON.parse(rawSourceMap);

const lines = minifiedCode.split('\n');

new SourceMapConsumer(rawSourceMapJson).then((sourceMapConsumer) => {
  const reconstructedSource = {};

  lines.forEach((line, lineIndex) => {
    const lineNum = lineIndex + 1;
    const segments = line.split(';');

    segments.forEach((segment, segmentIndex) => {
      const column = segment.length;
      const pos = {line: lineNum, column: column};
      const originalPosition = sourceMapConsumer.originalPositionFor(pos);

      if (originalPosition.source === null || originalPosition.name === null) {
        return;
      }

      if (!reconstructedSource[originalPosition.source]) {
        reconstructedSource[originalPosition.source] = [];
      }

      // Replace the obfuscated name with the original name
      segments[segmentIndex] = segment.replace(/[_$][\w\d]+/g, originalPosition.name);
    });

    lines[lineIndex] = segments.join(';');
  });

  for (const source in reconstructedSource) {
    let prettyCode = lines.join('\n');

    // Pass the code through Prettier
    const formattedCode = prettier.format(prettyCode, { parser: 'babel' });

    // Log the reconstructed source code
    console.log(`Source: ${source}`);
    console.log('Content:', formattedCode);
  }
});

Теперь у вас есть оба метода для извлечения всего исходного кода из исходной карты JavaScript. Сначала попробуйте более простой подход с использованием sourcesContent. Если это недоступно, не волнуйтесь! С помощью этого нового метода вы все еще можете расшифровать логику запутанного кода. Хотя получившийся код не будет точной копией оригинала, он существенно приблизит вас к нему, повысив вашу способность понимать и отлаживать!

Шаг 7: Создание интерфейса командной строки (CLI) для восстановления исходного кода

Узнав о различных способах восстановления исходного кода из минимизированного JavaScript и соответствующей карты исходного кода, давайте упакуем эти знания в удобный скрипт CLI. Этот скрипт будет:

  1. Примите в качестве аргумента путь к минифицированным файлам JavaScript.
  2. Проверьте, есть ли соответствующая исходная карта для каждого файла .js в данном каталоге.
  3. Попытайтесь восстановить исходный код, используя описанные выше подходы.
  4. Запишите восстановленный исходный код в новый файл, используя исходные имена файлов, указанные в исходной карте.

Вы также можете найти этот сценарий CLI,готовый к использованию, на моем GitHub:
https://github.com /puntorigen/recover-source

Давайте начнем !

Создайте новый файл JavaScript recover-source.js со следующим содержимым:

const fs = require('fs');
const { SourceMapConsumer } = require('source-map');
const path = require('path');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');

const argv = yargs(hideBin(process.argv))
    .usage('Usage: $0 [options]')
    .option('input', {
        description: 'The input path to the minified JavaScript file or a directory containing multiple files',
        alias: 'i',
        type: 'string',
        demandOption: true
    })
    .help()
    .alias('help', 'h')
    .argv;


const handleFile = async (minifiedFilePath) => {
    // Check if the Source Map exists
    const sourceMapPath = minifiedFilePath + '.map';
    if (!fs.existsSync(sourceMapPath)) {
        console.error(`No source map found at ${sourceMapPath}`);
        return;
    }

    // Read the minified file and the Source Map
    const minifiedCode = fs.readFileSync(minifiedFilePath, 'utf8');
    const rawSourceMap = fs.readFileSync(sourceMapPath);
    const rawSourceMapJson = JSON.parse(rawSourceMap);

    await SourceMapConsumer.with(rawSourceMapJson, null, (sourceMapConsumer) => {
        const sources = sourceMapConsumer.sources;

        const sourceContents = sources.reduce((contents, source) => {
            contents[source] = sourceMapConsumer.sourceContentFor(source, true);
            return contents;
        }, {});

        if (Object.values(sourceContents).some(content => content !== null)) {
            // If sourcesContent exists, write it to the file
            for (const source in sourceContents) {
                if (sourceContents[source] !== null) {
                    const originalFilePath = path.join(path.dirname(minifiedFilePath), path.basename(source, '.js') + '');
                    fs.mkdirSync(path.dirname(originalFilePath), { recursive: true });
                    fs.writeFileSync(originalFilePath, sourceContents[source]);
                    console.log(`Source code recovered to ${originalFilePath}`);
                }
            }
        } else {
            // If sourcesContent doesn't exist, reconstruct the source code
            const lines = minifiedCode.split('\n');
            let reconstructedSource = '';

            lines.forEach((line, lineIndex) => {
                const lineNum = lineIndex + 1;
                const columnCount = line.length;

                for (let column = 0; column < columnCount; column++) {
                    const pos = { line: lineNum, column: column };
                    const originalPosition = sourceMapConsumer.originalPositionFor(pos);

                    if (originalPosition.source === null) continue;


                    if (originalPosition.name) {
                        reconstructedSource += originalPosition.name;
                    } else {
                        reconstructedSource += minifiedCode.charAt(column);
                    }
                }

                reconstructedSource += '\n';
            });

            // prettify the code
            try {
                reconstructedSource = prettier.format(reconstructedSource, { semi: false, parser: "babel" });
            } catch (error) {
                console.error("An error occurred while prettifying the code:", error);
            }

            const originalFilePath = path.join(path.dirname(minifiedFilePath), path.basename(minifiedFilePath, '.js') + '-recovered.js');
            fs.mkdirSync(path.dirname(originalFilePath), { recursive: true });
            fs.writeFileSync(originalFilePath, reconstructedSource);
            console.log(`Source code recovered to ${originalFilePath}`);
        }
    });
};


const handlePath = (inputPath) => {
    const files = fs.readdirSync(inputPath);
    for (const file of files) {
        const absolutePath = path.join(inputPath, file);

        if (fs.statSync(absolutePath).isDirectory()) {
            handlePath(absolutePath);
        } else if (path.extname(absolutePath) === '.js') {
            handleFile(absolutePath);
        }
    }
};

if (fs.statSync(argv.input).isDirectory()) {
    handlePath(argv.input);
} else {
    handleFile(argv.input);
}

Чтобы использовать этот скрипт, выполните следующую команду:

node recover-source.js -i ./path_to_your_minified_files

Обязательно замените «./path_to_your_minified_files» на путь к вашим мини-файлам JavaScript/TS.

И вуаля! Теперь у вас есть инструмент CLI для восстановления исходного кода из уменьшенных файлов JavaScript и их исходных карт. Удивительно, как многого можно добиться, немного поработав с Node.js и немного разобравшись в Source Maps.

Я надеюсь, что вы нашли этот урок полезным. Если у вас есть какие-либо вопросы или отзывы, сообщите нам об этом в комментариях. Удачного программирования!