Итак, мы знаем, что есть спецификация, а есть ее реализация. Знаем, что реализация зачастую отстает от спецификации. Более того, разные реализации по-разному отстают от спецификации. Написав код, мы не можем гарантировать, где он будет запускаться, а где — нет.

Исходя из этого можно сделать вывод, что нужно писать код, придерживаясь старых стандартов. К счастью, есть другой путь: мы можем писать код с использованием всех возможных фич, но перед публикацией автоматически транслировать его (то есть переводить из одного вида в другой) в старую версию. Звучит сложно, но на практике все просто.

Сама природа JS и его способы использования готовят нас к тому, что никогда не настанет светлых времен с современными рантаймами. Люди использовали и продолжат использовать разные браузеры и разные версии браузеров, разные версии Node.js и так далее. Использование новых синтаксических конструкций в такой ситуации практически невозможно. Запуск кода на платформе, не поддерживающей новый синтаксис приведет к синтаксической ошибке. Закономерным решением этой проблемы стало появление Babel — программы, которая берет указанный код и возвращает тот же код, но транслированный в старую версию JS. Фактически, в современном мире Babel стал неотъемлемой частью JS. Его не используют только в старых проектах, также называемых легаси-проектами. Все новые проекты так или иначе делают с его использованием.

У Babel есть собственный онлайн REPL. Попробуйте вставить туда любой код, который вы писали на Хекслете, и посмотрите, во что он превратится. Такая трансляция называется транспайлингом, а сам Babel называют транспайлером, от transpiler.

// Before
const factorial = (n) => {
  if (n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}
// after
"use strict";

var factorial = function factorial(n) {
  if (n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
};

Babel состоит из многих частей. Пакет @babel/core содержит код, который выполняет всю работу по трансляции, но не содержит внутри себя правил преобразования. Правила описаны в отдельных пакетах, называемых плагинами (например, babel-plugin-transform-constant-string). Этих плагинов настолько много, что их принято объединять в группы, называемые пресетами (preset), которые затем подключаются к Babel наравне с плагинами. Пакет @babel/cli обеспечивает возможность работы с бабелем через терминал.

Установка

$ npm install --save-dev @babel/core @babel/cli @babel/node @babel/preset-env

Настройка

Babel полагается на наличие файла babel.config.js в корне проекта. Именно через него он узнает, как нужно транслировать код. Если вы забудете добавить туда плагин или пресет, то на выходе Babel отдаст тот же код, что был и на входе.

module.exports = {
  presets: [
    ['@babel/env', {
      targets: {
        node: 'current',
        firefox: '60',
        chrome: '67',
        safari: '11.1',
      },
    }],
  ],
};

Разные среды исполнения поддерживают (или не поддерживают) разные возможности и синтаксические конструкции языка. В свойстве targets перечисляются конкретные окружения (и их версии), для которых пишете код. Если код предназначен для выполнения на nodejs, то достаточно указать только его. В таком случае babel будет транслировать конструкции, поддерживаемые на nodejs, и ничего лишнего:

module.exports = {
  presets: [
    ['@babel/env', {
      targets: {
        node: 'current',
      },
    }],
  ],
};

Минимально достаточно подключить пресет @babel/preset-env. Он добавляет возможности JS, которые входят в стандарт.

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

$ npx babel src --out-dir dist

Эта команда берет весь код из файлов в папке src и создает его транслированную версию в папке dist. Запускается он точно так же, как и любой другой код, и фактически именно этот код нужно доставить в NPM-репозиторий. Другими словами, пользователи вашего пакета запускают код из папки dist, а не src, хотя сами об этом не знают. Сама папка dist добавляется в .gitignore, так как сгенерированный код нужен только в момент публикации пакета для упаковки в архив, который уходит в NPM-репозиторий. В процессе разработки пакета, запуск сборки не требуется.

Есть только один маленький нюанс. Изначально я сказал, что NPM никак не интегрирован с Git, но это не совсем правда. По умолчанию NPM смотрит в файл .gitignore. Все, что там перечислено, не попадет в NPM-репозиторий при публикации пакета. В нашем случае такой папкой является dist, но именно ее мы и хотим опубликовать. Выходов из этой ситуации несколько. Один связан с файлом .npmignore и описан в документации, про другой я скажу подробнее. NPM позволяет указать список файлов и папок, которые нужно опубликовать. Достаточно добавить секцию files в package.json. Содержимое files — массив папок и файлов:

"files": [
  "dist"
]

Существует два способа подготовки пакета к публикации. Первый подход заключается в том, чтобы перед выполнением npm publish вручную сгенерировать каталог dist, используя скрипты: npx babel src --out-dir dist. Подход рабочий, но сопряжён с постоянными ошибками в стиле "ой, забыл собрать новый код". К тому же, это действие может быть автоматизировано — именно эту идею и реализует второй подход. NPM содержит множество предопределённых скриптов, которые выполняются автоматически в определённые этапы работы. Например, prepublishOnly запускается перед непосредственным выполнением публикации. То, что нам и требуется.

"scripts": {
  "build": "NODE_ENV=production babel src --out-dir dist",
  "prepublishOnly": "npm run build"
}

Если у вас Windows, вам понадобится утилита cross-env.

В примере выше используется небольшой трюк. В prepublishOnly вызывается другой скрипт — build. Этот приём используется широко, и он действительно удобен. Бывают ситуации, когда все же нужно запускать сборку руками. Поэтому удобно иметь отдельную команду только для генерации. Скрипт build как раз и призван решить эту задачу.

Подчеркну еще раз: каталог dist не должен храниться в git-репозитории, и вы не найдете его на Гитхабе. Посмотрите lodash. Она генерируется только в момент публикации пакета и заливается в npm-репозиторий. Каждая новая публикация должна генерировать этот каталог заново. Только в этом случае обновится код в пакете.

Подведём итог. В git-репозитории хранится исходный код, ещё не обработанный babel. Это значит, что вы всегда можете найти библиотеку и изучить её содержимое на github. А вот пакет, установленный к вам в систему содержит обработанный код, предназначенный для запуска, а не для чтения. Этот код не хранится в git-репозитории. Он попадает в NPM-репозиторий в момент публикации новой версии пакета за счет выполнения команды prepublishOnly (в которую вы сами должны прописать вызов трансляции).

Babel-node

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

  1. Делаем изменение.
  2. Транслируем код с помощью Babel.
  3. Запускаем на выполнение.

Разработчики Babel предусмотрели эту ситуацию. В этом случае можно установить пакет @babel-node. Теперь код можно вызывать так: npx babel-node src/index.js. Команда babel-node делает одновременно две вещи. Транслирует код и сразу же запускает его на выполнение. В отличие от команды babel, babel-node не сохраняет результат трансляции. Все происходит во время работы в памяти. Обратите внимание на то, что вам все равно понадобится правильно настроенный файл babel.config.js в корне проекта иначе babel-node не сможет произвести трансляцию и так же завершится с ошибкой синтаксиса на момент запуска.

Самостоятельная работа

Попробуйте выполнить скрипт build в пакете nodejs-package. Изучите результаты его работы в папке dist. Вы должны увидеть, что содержимое файлов внутри dist отличается от содержимого тех же файлов внутри src. Вместо const использован var, вместо import - require. В целом код остается читаем, хотя и выглядит странновато.


Дополнительные материалы

  1. Babel Node - описание способов запуска из командной строки