История о том, почему мы не должны использовать инструменты для того, что можно сделать с помощью простого JavaScript.

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

Учитесь на моих ошибках

Когда-то давным-давно я настраивал сложный монорепорепозиторий. Он содержал несколько пакетов, а они содержали общую конфигурацию транспиляции, линтинга и тестирования. Некоторые из этих пакетов нуждались в небольшой настройке конфигурации. Я твердо верю, что меньше — это больше, поэтому использовал конфигурацию JSON внутри package.json. Общая конфигурация жила в пакете, другие пакеты использовали его через механизм расширения, предоставленный инструментом, на который мы нацеливались  —  Babylon:

{
  "name": "awesome-package",
  "version": "1.0.0",
  "babel": {
    "extends": "awesome-config",
    "plugins": ["lodash"]
  }
}

Так просто. К сожалению, не всякий инструмент реализует этот механизм “расширения”.

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

О боже! Я был так неправ… и они были так правы. Я был пьян обещаниями мощного package.json со всеми моими конфигурациями и совершенно упустил суть. Учитывая моё знание Ruby, я также должен был знать, что инструмент определения пакетов gemfile.rb всегда был гибче, чем package.json.

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

Почему конфигурация JavaScript

В основном потому, что JSON  —  это формат данных, а JavaScript  —  это просто код. Код поддаётся оценке (следовательно, динамический) и компонуемый. Нет необходимости в каком-то инструменте, чтобы делать что-то сверх JSON.

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

Переопределяемость

Если вам “требуется” (require) файл конфигурации, вы можете изменить возвращаемый объект и повторно экспортировать его. Пакету нужен дополнительный плагин Babel? Без проблем:

const base = require("../babel.config.js");

module.exports = {
   ...base,
   plugins: [...base.plugins, "lodash"],
};

Динамическое конфигурирование

Как я уже сказал, JSON  —  это формат данных, поэтому он не может быть динамическим. Если вам нужны разные версии конфигурации JSON, придётся использовать разные файлы, которые могут отличаться лишь незначительно. Применяйте JavaScript. Когда инструменту требуется файл конфигурации JavaScript, код вычисляет нужные значения. Плагин нужен только в производственном коде? Без проблем:

const base = require("../babel.config.js");

module.exports = {
  ...base,
  plugins: process.env.NODE_ENV === "production" 
    ? [...base.plugins, "lodash"]
    : base.plugins,
}

Совместное использование

Есть канонический способ совместного использования кода в JavaScript: пакет npm. Напишите соответствующий package.json и опубликуйте его:

{
  "name": "my-awesome-babel-config",
  "version": "1.0.0",
  "description": "My awesome babel config. Babelize all projects",
  "main": "babel.config.js",
  "files": ["babel.config.js"]
}

После публикации вы просто «требуете» пакет в каждом проекте, где он вам нужен:

const base = require("my-awesome-babel-config");

  module.exports = {
     ...base,
     plugins: [...base.plugins, "lodash"],
  };

Комментируемость

JSON не поддерживает комментарии. Или, по крайней мере, они не являются частью спецификации JSON. Вы можете использовать какой-то уродливый хак, например добавление дополнительных ключей с комментариями:

{
  "_plugins": "Cherry-pick Lodash modules",
  "plugins": ["lodash"]
}

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

module.exports = {
  // Cherry-pick Lodash modules
  "plugins": ["lodash"],
};

Тестируемость

Как я уже говорил, можно тестировать конфигурационные файлы. Это всего лишь код. Вы сами решаете, добавляет ли тестирование вашей инструментальной инфраструктуры ценность проекту. Предположим, так оно и есть. Давайте протестируем динамическую конфигурацию с помощью Jest:

describe("config", () => {
  beforeEach(() => {
    oldEnv = process.env.NODE_ENV;
  });
  afterEach(() => {
    // Мы требуем один и тот же файл несколько раз
    // Сбрасываем кэш require между тестами.
    jest.resetModules();
    process.env.NODE_ENV = oldEnv;
  });

  it("uses lodash plugin in production", () => {
    process.env.NODE_ENV = "production";
    const config = require("./babel.config.js");
    expect(config.plugins.includes("lodash")).toBe(true);
  });

  it("does not use lodash plugin in other envs", () => {
    process.env.NODE_ENV = "production";
    const config = require("./babel.config.js");
    expect(config.plugins.includes("lodash")).toBe(false);
  });
});

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

describe("config", () => {
  
  // ...beforeEach, afterEach...
  it("uses lodash plugin in production", () => {
    process.env.NODE_ENV = "production";
    expect(require("./babel.config.js")).toMatchSnapshot();
  }); 
  
  it("does not uses lodash plugins in other envs", () => {
    process.env.NODE_ENV = "development";
    expect(require("./babel.config.js")).toMatchSnapshot();
  });
});

Подведение итогов

Ошибиться в отношении конфигурационных файлов JavaScript было одной из лучших вещей (с технической точки зрения), произошедших со мной. Это сделало мою жизнь как разработчика проще. В этой истории я показал несколько примеров того, что можно сделать с помощью конфигурации JavaScript. Это те вещи, которые нелегко осуществить с помощью JSON без дополнительных инструментов. Кажется очевидным? Но я не знаю, почему большинство инструментов JS всё еще поддерживают конфигурацию через JSON. Я понимаю важность обратной совместимости и не призываю ломать весь интернет, но, возможно, пришло время начать выдавать предупреждения об устаревании и постепенно отказываться от JSON.

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

Читайте также:


Перевод статьи David Barral: Stop Using JSON Config Files

Предыдущая статьяПрограммное обеспечение без конструкции if-else
Следующая статьяActix или Rocket? Сравнение двух мощных платформ для веб-приложений на Rust