Если вы хотите пропустить церемонию и сразу перейти к коду, ознакомьтесь с прилагаемым репозиторием GitHub.

Это мой отчет об использовании make для поддержки и автоматизации монорепозитория «современного» JavaScript, включая то, почему я это сделал, как и, возможно, некоторые причины, по которым вам следует или не следует делать то же самое.

Мой первый опыт работы с make был много лет назад, когда мне пришлось компилировать драйверы для моей карты Wi-Fi (драйверы atheros и linux… тьфу).
С тех пор долгое время make был просто шагом, после которого вы бежали
./configure и до make install. До недавнего времени я искренне думал, что
make — это инструмент сборки, специально предназначенный для запуска gcc и компиляции программ на C.

Пару месяцев назад, когда я работал над проектом monorepo. Нынешний модный инструмент для управления монорепозиториями называется lerna, и он супер крутой. На момент использования lerna была еще довольно незрелой, и я лично изо всех сил пытался заставить все работать так, как я хотел.

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

Использование make было интригующим из-за его минималистичности. Он имел
нулевые зависимости и, как правило, уже был установлен на большинстве компьютеров разработчиков. make также представлял собой очень низкий барьер между
задачей, которую вы выполняли, и тем, что на самом деле происходило. Не найти репозиторий GitHub для какого-то эзотерического плагина, просто иметь возможность точно видеть, какие команды оболочки были запущены.

Цели

По пути я узнал о make больше, чем мог себе представить. Например, я никогда не понимал всю прелесть и простоту правил и целей make.

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

lib/index.js: lib/index.jsx
babel — out-file lib/index.jsx lib/index.js

В приведенном выше примере определяется правило для преобразования файла JSX (n ES6+) в файл Javascript ES5. lib/index.js — цель, lib/index.jsx — компонент, а babel … — команда, используемая для создания цели из компонента.

Это означает, что babel будет работать _только_ на lib/index.jsx если и только если этот файл каким-то образом изменился. Это огромная победа для больших проектов, потому что повторная компиляция всех вещей каждый раз, когда изменяется файл, может очень дорого и очень быстро раздражать.

Динамические правила

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

Динамические правила — одна из самых крутых частей make, о которой я узнал, и
они фактически позволили make управлять _n_ числом общих пакетов в одном репозитории. Мне пришлось задать вопрос на StackOverlow, и я так любезен, что получил два замечательных ответа, в том числе один, который действительно объяснил мне это.

В монорепозитории структура может быть примерно такой (в моем случае она была такой):

- Makefile
- packages
 | foo
 | lib
 — src
 — bar
 | lib
 — src

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

Следующий фрагмент кода является его сутью (мне пришлось бы написать еще (пару) сообщений в блоге, чтобы точно объяснить, что здесь происходит). По любым вопросам я бы посоветовал ознакомиться с моим вопросом StackOverflow и последующими ответами или ознакомиться с документацией GNU Make.

# Directories
PKGS_ROOT := packages
PKGS_SRCDIR := src
PKGS_OUTDIR := lib
# Expands to the source directory for the specified package
pkg-srcdir = $(PKGS_ROOT)/$1/$(PKGS_SRCDIR)
# Expands to the output directory for the specified package
pkg-libdir = $(PKGS_ROOT)/$1/$(PKGS_OUTDIR)
# Expands to all output targets for the specified package
pkg-libs-js = $(addprefix $(call pkg-libdir,$1)/,$(patsubst %.jsx,%.js,$(notdir $(wildcard $(call pkg-srcdir,$1)/*.js*))))
# Defines the following rules for the specified package:
## PER-PACKAGE RULES START HERE
define pkg-rules
# build rule for .js(x) files
$(call pkg-libdir,$1)/%.js: $(call pkg-srcdir,$1)/%.js* | $(call pkg-libdir,$1)
 $(TRANSPILER) $(TRANSPILER_OPTS) — out-file $$@ $$^
endef
## PER-PACKAGE RULES END HERE
# Creates rules for the specified package
add-pkg = $(eval $(call pkg-rules,$1))
# Create rules for all packages
PKGS := $(notdir $(wildcard $(PKGS_ROOT)/*))
$(foreach p,$(PKGS),$(call add-pkg,$p))

Начнем с каталога пакетов: PKGS_ROOT. Затем мы определяем PKGS как все подкаталоги PKGS_ROOT (PKGS := $(notdir $(wildcard $(PKGS_ROOT)/*))). Наконец, мы выполняем команду add-pkg для каждого каталога, найденного в PKGS:

add-pkg = $(eval $(call pkg-rules,$1))
PKGS := $(notdir $(wildcard $(PKGS_ROOT)/*))
$(foreach p,$(PKGS),$(call add-pkg,$p))

Как видите, к сожалению, make имеет довольно эзотерический синтаксис, который не совсем интуитивно понятен с первого раза. Большая часть документации действительно хороша (несмотря на то, что почти все примеры относятся к коду на C и C++), и в Интернете имеется множество примеров и других ресурсов.

Также у make толком нет сообщества или плагин-экосистемы, насколько мне известно, так что простых plug-n-go решений вы не найдете (хотя можно включить другие Makefiles). Одна интересная идея состоит в том, чтобы взять файл Makefile монорепозитория и убрать
команды управления монорепозиторием, чтобы сделать универсальную и общедоступную автоматизацию сборки, которая будет работать в различных средах. Было бы здорово, если бы было больше модульных Makefile сообществ, которые делились бы общими сниппетами.

Как программист-полиглот, я использовал множество инструментов сборки. От grunt до gulp, от pip до setup.py, от maven до gradle и т. д. Честно говоря, я люблю make. Он близок к «металлу» в том смысле, что я могу видеть и редактировать точные выполняемые команды.

На данный момент я собираюсь использовать make в качестве основного источника автоматизации для фронтенд-разработки. Особенно когда проект перерос npm-scripts. Он эффективен, ясен, и я думаю, что мой Makefiles будет легко расширять по мере того, как инструменты разработки будут появляться и исчезать.

На самом деле, на моей основной работе мы сейчас используем make для фронтенд-проектов. На самом деле у нас есть монорепозиторий — полностью управляемый make — интерфейсных пакетов, которые мы можем использовать для создания общих пользовательских интерфейсов. До сих пор make был относительно успешным. Есть некоторая непривычность, но это работает, и определенно есть возможность еще больше упростить Makefile, чтобы обеспечить лучшую интерпретацию.

Make нужен еще один выстрел. Я бы предложил дать ему один.