Сейчас многие фреймворки имеют в своем арсенале CLI (Command Line Interface) и ангуляр – не исключение. Впервые с CLI-утилитами я столкнулся, когда пробовал EmberJS и тогда мне это показалось очень удобным инструментом, который довольно сильно экономил мое время. Но, к сожалению, из коробки CLI-утилиты подходят только для несложных, домашних проектов.
Использование их же в крупных production-проектах без допилки напильником почти невозможно. Поэтому на старте нашего проекта мы вынуждены были отказаться от его использования из-за особенностей нашей архитектуры. Почти год я периодически смотрел в сторону Angular CLI в плане его развития и огорчался, что мы не можем его использовать, так как это сделало бы вход в проект гораздо проще. Но в один прекрасный момент команда Angular выпустила @angular-devkit, который содержит в себе набор пакетов, используемых cli утилитой, и это дало нам возможность допилить Angular CLI под наши нужды. Репозиторий содержит на сегодняшний день с десяток пакетов, но в этой статье мы рассмотрим только те, которые относятся к schematics.
Schematics используется CLI для генерации стартового проекта или его частей, а также для добавления внешних пакетов в ваш проект или для апгрейда вашего проекта при выходе новых версий. Каждый раз, когда вы запускаете команду ‘ng new’, чтобы создать проект, или ‘ng generate’ чтобы добавить что-то в проект, на заднем плане всю работу выполняет именно schematics. Команда Angular не единственная, кто сейчас использует schematics. Пакеты для него также есть и у команды nrwl.io, которая предоставляет набор утилит для построения больших приложений, а также у библиотеки ngrx, которая позволяет реализовать redux-паттерн в проекте. Зачем schematics нужен CLI мы поняли, но какую пользу он может принести остальным? Вот приблизительный список задач, для которых можно его использовать:
- Вас не устраивают генераторы, встроенные в Angular CLI. С помощью schematics можно создать свои генераторы или расширить существующие.
- Вы внесли несовместимые изменения (breaking changes) в проект и не хотите вручную править код ваших модулей.
- Вы можете заменить макросы для вашей IDE на пакет скриптов для schematics. Генераторы, сделанные на основе schematics, можно версионировать и распространять, что дает возможность использовать их как в вашей команде, так и делиться наработками с другими командами.
- Создав генераторы для проекта, вы можете сократить время вхождения нового сотрудника в ваш проект, а также быть уверенным, что ваши соглашения по структуре проекта реализуются.
Это далеко не весь список возможностей, думаю, у каждого найдутся свои применения sсhematics. Итак, давайте рассмотрим, что мы можем сделать с помощью schematics.
Что же представляет из себя пакет schematics?
Schematics – это набор пакетов из @angular-devkit, который позволяет изменять структуру проекта: добавлять новые файлы к проекту, изменять существующие, удалять файлы из проекта, проводить рефакторинг кода.
Казалось бы, ничего особенного, все это можно реализовать и вручную, но у schematics есть два важных момента: модульная архитектура и изолированность рабочей среды (герметичность). Когда вы запускаете schematics-генератор, он не вносит весь набор изменений сразу в проект, а производит изменения сначала в собственной in-memory файловой системе, и если не возникло ошибок при исполнении ваших модификаций, то переносит изменения в реальный проект. Это похоже на систему версионирования git – создается копия вашего проекта (staging area), вносятся изменения в проект и если не возникло ошибок, то изменения переносятся в реальный проект (commit). Вот как это выглядит:
Когда генератор делает какие-то изменения, schematics сохраняет набор изменений в виде списка действий (Action List), которые могут быть провалидированы и применены к внутреннему состоянию вашего проекта. И если в результате валидации действий и применении их к состоянию проекта не возникло ошибок, то вызывается действие Sink, которое является конечным и переносит изменения в файловую систему.
Установка и создание шаблонного проекта
Итак, давайте разберемся, как же использовать schematics и начнем с его установки и создания базового проекта. Чтобы поставить schematics и начать с ним работать, необходимо установить пакет schematics-cli:
npm install -g @angular-devkit/schematics-cli
Разрабатывая свои расширения для schematics или Angular CLI, вы создаете собственные npm пакеты и добавляете их в свой проект. Давайте создадим шаблонный проект для schematics. Для этого мы можем использовать сам schematics:
schematics blank --name=sample
Выполнив данную команду, CLI создаст шаблонный проект. Давайте рассмотрим, из чего он состоит:
Как видите, помимо стандартных файлов любого npm модуля, наш пакет также содержит папку src со следующим содержимым — collection.json, index.ts и index_spec.ts.
Файл collection.json содержит метаданные коллекции наших schematics команд. Именно в нем вы описываете все возможности пакета. Вот как это выглядит в нашем случае:
А вот как это выглядит в наборе schematics для проекта Ангуляр:
Как видно из примеров, файл с коллекцией содержит раздел schematics, где перечисленные все поддерживаемые команды данного пакета. Каждая команда может содержать следующие параметры:
- schema – json схема, с описанием параметров нашей команды, которые мы можем передать в командной строке или при вызове команды из другой команды.
- factory — файл или папка с кодом, выполняющим всю основную работу по трансформации нашего проекта.
- description — описание нашей команды, которое увидит пользователь, если будет использовать angular-CLI.
- hidden — должна ли CLI-утилита выводить данную команду в листинге команд.
- aliases — сокращенные варианты названия нашей команды.
- extends — имя schematic команды, которую расширяет данная команда. Команда может быть как локальной для нашего пакета, так и внешней, из другой коллекции команд.
Только параметры factory и description являются обязательными, все остальные параметры вы можете использовать по необходимости.
Если заглянуть в package.json, то можно увидеть, что в нем появился новый параметр schematics, который ссылается на нашу коллекцию. Это необходимо, чтобы CLI- утилита знала, откуда ей взять список команд нашего пакета.
Теперь давайте рассмотрим содержимое файла index.ts, на который ссылается коллекция:
Файл содержит всего одну функцию, которая является фабрикой для нашей команды из коллекции. На вход фабрика получает параметр options, который содержит параметры, переданные команде из командой строки CLI, или при вызове нашей команды из другой schematics команды. Возвращает же фабрика объект Rule. Как я уже говорил в начале, выполняя трансформацию проекта, вы не работаете с файловой системой напрямую. Вместо этого вы проводите все изменения с объектом Tree, который представляет из себя виртуальную файловую систему в памяти. Объект Tree содержит в себе:
- base – набор всех файлов с файловой системы вашего проекта
- staging area – список всех изменений, сделанных вами относительно base
Когда вы выполняете изменения, вы не вносите их в base структуру, вместо этого они попадают в staging area. Этот подход должен быть знаком вам на примере того, как работает git. Но давайте вернемся к нашему типу Rule. Rule – это функция, которая принимает на вход состояние проекта (Tree), вносит необходимые изменения и возвращает новое состояние проекта. Rule – основной кирпичик schematic трансформаций, и единственное, что вносит изменения в проект. В нашем примере Rule, возвращаемое из нашей фабрики, не производит никаких изменений с проектом, а просто возвращает состояние без изменений.
Давайте добавим код и выполним его:
Мы добавили вывод приветствия пользователю с использованием переданного имени. Как вы видите, мы воспользовались методом логирования из контекста, переданного нам вторым параметром. Встроенное логирование стоит использовать для вывода на консоль, т.к. schematics не гарантирует, что использование стандартного console.log будет выводится в корректном порядке. Как вы могли заметить наша функция Rule синхронная, но это не является обязательным. Функция может возвращать не только измененное дерево проекта, но и, например, Observable, который вернет измененное дерево проекта. Это необходимо, например, если вам надо сначала выполнить установку внешнего пакета через npm, а затем уже добавить его в package.json.
Для того чтобы запустить код, нам нужно выполнить следующие команды:
npm run build
schematics .:sample --name=test
Для запуска команды мы должны передать CLI имя нашей коллекции и имя команды, а также необходимый набор параметров. Так как мы запускаем нашу команду из нашего проекта, то в качестве имени коллекции мы использовали текущий проект указав ‘.’.
Прежде чем пойти дальше, давайте еще немного улучшим проект. Как мы видели в коде нашей фабрики, параметр options, который в нее передается, имеет тип any. Это, конечно, удобно, но не позволяет CLI проверять параметры, необходимые для команды, а также не дает пользователю команды описания ее возможностей. Давайте исправим это и опишем структуру наших параметров. Для этого нам необходимо добавить два файла в проект – schema.json и schema.ts. Первый файл описывает параметры для angular-CLI, и именно их видит пользователь, когда запрашивает доку по нашему генератору. Второй нужен для проверки типов при вызове нашей команды из другой команды. Вот как будет выглядеть наш файл scheme.ts:
А вот что мы получим в schema.json:
Осталось только добавить нашу схему в нашу команду. Для этого мы должны передать ее в нашей коллекции, а также указать тип данных в нашей фабрике команды:
Мы добавили два параметра, используемые нашей командой. Один параметр описывает имя, передаваемое при вызове команды. Параметр quiet описывает, стоит ли выводить логирование на консоль. Как мы видим из нашей схемы, мы не указывали что какой-то из параметров обязательный, но такая возможность есть. Также вы могли заметить довольно странный синтаксис для дефолтного значения имени. Такая запись позволяет CLI взять параметр по его индексу из параметров, переданных в командной строке, что бывает удобно. Кроме этого, мы использовали возможность запросить имя у пользователя на случай, если его забыли указать в параметрах команды. Для этого мы указали параметр x-promt куда прописали вопрос, задаваемый пользователю. Это не единственный способ задать вопрос пользователю, библиотека содержит в себе довольно мощный механизм формирования пользовательского ввода, но мы не будем рассматривать его в данной статье.
Давайте запустим нашу команду и посмотрим, что мы получим:
Итак, теперь у нас есть наш проект, и мы знаем как запускать наши команды, давайте теперь рассмотрим возможности, которые дает нам библиотека schematics.
Основные возможности schematics
Пакет schematics содержит довольно много возможностей, начнем с самых простых и постепенно будем двигаться к более сложным. Самое простое, что вы можете делать, это работать с файлами вашего проекта – создавать, перемещать, перезаписывать и удалять их. Для этого у объекта Tree, который мы получаем на вход нашего Rule, есть следующие методы:
create(path: string, content: Buffer | string): void;
delete(path: string): void;
rename(from: string, to: string): void;
overwrite(path: string, content: Buffer | string): void;
read(path: string): Buffer | null;
exists(path: string): boolean;
Давайте изменим код генератора следующим образом:
Если мы теперь запустим генератор и посмотрим на содержимое нашего проекта, то не увидим созданного файла. Это происходит потому, что у schematics есть два режима работы – режим, когда изменения вносятся только в виртуальную файловую систему и полноценный режим, когда изменения применяются и в проекте. По умолчанию schematics работает в первом режиме и не вносит изменения в реальный проект. Для того, чтобы мы увидели изменения, надо запустить выполнение нашей команды с флагом ‘–dry-run=false’:
schematics .:sample --name=test –dry-run=false
Теперь мы увидим созданный файл на диске.
Все это, конечно, здорово, но неудобно в случае, если у нас есть готовый шаблон кода, и все, что нам надо сделать, — параметризировать его немного и переложить в исходники проекта. Если мы будем создавать все файлы с помощью рассмотренных выше методов, то поддержание нашего генератора будет очень трудоемким. Для таких задач у schematics есть возможность использовать шаблоны при генерации файлов с помощью следующих функций:
contentTemplate<T>(options: T)
pathTemplate<T>(options: T)
template<T>(options: T)
Функция contentTemplate применяет шаблонизацию для содержимого файла, pathTemplate применяет шаблонизацию к расположению файлов, последняя функция template является комбинацией первых двух. Что же делают данные функции? Они получают на вход файлы и добавляют действия (Action) в список изменений состояния проекта. Возможны следующие варианты:
- если функция template возвращает null, в результате добавляется действие DeleteAction
- если изменяется расположение или имя файла, то добавляется действие RenameAction
- если изменяется содержимое файла, то добавляется действие OverwriteAction
Функция template принимает на вход файл и выполняет с ним две трансформации – трансформацию пути к файлу и трансформацию его содержимого. Итак, давайте создадим генератор, который будет создавать сервис и файл тестов к нему. Для этого нам необходимо внести следующие изменения в состав файлов:
Как мы видим, мы добавили папку files с двумя файлами. Один файл содержит шаблон нашего файла с сервисом, а второй – файл тестов для нашего сервиса. Название наших файлов в папке files выглядит немного странным. Например, файл с нашим сервисом выглядит следующим образом:
__name@dasherize__.service.ts
Функция template позволяет использовать параметры в названии или в пути к нашим файлам шаблона, которые будут подставлены в момент обработки файла. Чтобы использовать параметр, его необходимо обрамить двумя символами ‘_’. В качестве параметров могут использоваться как обычные переменные, так и функции. В нашем случае мы будем подставлять параметр name. Но в имени файла мы еще видим странный кусочек после нашего параметра name – @dasherize. Это вызов функции, в которую будет передан наш параметр name и результат выполнения которой будет подставлен обратно.
Можно использовать сразу несколько функций. Используемая функция dasherize – это не встроенная возможность, нам придётся указать методу template данную функцию при его вызове. Как это сделать будет видно позднее, а сейчас давайте рассмотрим содержимое наших файлов:
__name@dasherize__.service.ts
__name@dasherize__.service.spec.ts
Как мы видим, функция template позволяет использовать шаблонную подстановку и в содержимом файла, и также использовать вызовы функций. Кроме подстановки мы можем использовать условный оператор if и цикл for:
<% if (a) { %>b<% } %>
<% for (let i = 0; i < value; i++) { %>1<% } %>
Давайте теперь рассмотрим код нашего генератора и то, как мы будем использовать функцию template:
Здесь довольно много новых функций, давайте разберем их по порядку. Для того, чтобы применить функцию template к нашим файлам, используем функцию apply. Эта функция принимает на вход объект типа Source и список Rule, которые она применяет к каждому элементу этого объекта.
Объект Source представляет из себя тоже самое, что и объект Tree, но у него нет base. Для того чтобы загрузить наши файлы в виде Source объекта, мы используем функцию url, передав ей путь к нашим файлам. Далее в нашей цепочке Rule мы видим функцию filter, которая позволяет отфильтровать наш файл с тестами, если пользователь не захотел его создавать. Если параметр spec задан, то используем функцию noop, которая просто возвращает полное состояние нашего Tree, в противном случае мы отфильтровываем из нашего списка файлов все spec файлы. После этого производится вызов функции template, которой мы передаем объект, содержащий все параметры, которые передал пользователь, а также содержимое объекта strings, который содержит различные функции, такие как dasherize и classify. Мы используем функции из пакета angular, но вы вправе написать свои. В конце мы вызываем функцию move, которая перемещает все обработанные файлы в другую директорию.
Функция apply возвращает Source объект, который нужно теперь добавить к состоянию проекта. Для этого воспользуемся тремя новыми функциями – chain, mergeWith, branchAndMerge. Функция chain принимает на вход цепочку Rule и возвращает новое Rule, который вернет результат выполнения всех переданных rule. Функция mergeWith объединит текущее состояние Tree с состоянием, которое вернет вызов нашей функции apply. Это очень похоже на то, как происходит rebase в git-репозитории. Все изменения просто применяются поверх текущего состояния кода. Функция branchAndMerge делает копию Tree и после внесения всех изменений накладывает его обратно на наше состояние дерева. Если теперь мы запустим команду, используя параметр dry-run=false, то увидим на диске два файла – один с классом, а второй с тестами для этого класса.
Вызов внешнего правила
Если бы возможности schematics ограничивались только созданием новых правил, это было бы неудобно и довольно скучно. Но, как я уже упоминал в начале, библиотека позволяет строить цепочки вызовов различных команд, что делает ее очень гибкой и избавляет нас от написания кода повторно. В API есть две команды, позволяющие запускать команды schematics из кода команды – externalSchematic и schematic. Первая позволяет запускать генераторы из других коллекций, вторая – из текущей.
Для вызова внешних команд необходимо всегда пользоваться данными функциями. Никогда не пытайтесь импортировать фабрику правил из стороннего пакета и вызвать ее напрямую, это может привести к ошибкам в работе вашего правила.
Для того, чтобы рассмотреть, как это работает, мы улучшим команду component из набора команд проекта Angular. Предположим, что политика разработки в компании требует наличие лицензии в каждом файле исходного кода. По умолчанию команды пакета Angular не дают возможности добавить лицензию при генерации компонентов. Давайте добавим эту возможность:
Как вы видите, в нашем примере мы воспользовались командой externalSchematic, которая вызвала команду из пакета Angular. После выполнения команды из пакета Angular, мы воспользуемся тем, что schematics сохраняет у себя всю цепочку изменений. Поэтому после выполнения генератора из пакета Angular, мы пройдемся по всем действиям в нашей staging area и для действий создания файлов с расширением ‘ts’ добавим наш лицензионный текст. Действия создания файлов будут иметь kind равный ‘c’.
Если мы запустим новую команду, как мы делали это раньше, то мы получим ошибку Invalid source: undefined. Дело в том, что для успешной работы команд ангуляр пакета их необходимо запускать через angular-CLI. Давайте рассмотрим, как нам это сделать.
Использование совместно с CLI
Для того чтобы использовать schematics пакет совместно с Angular CLI, создадим новый ангуляр проект:
ng new my-project
И добавим к нему наш пакет:
npm link $PATH_TO_SCHEMATIC_PROJECT
Здесь мы линкуем schematics проект, но в реальных условиях вы, скорее всего, будете ставить его как npm пакет. Теперь давайте выполним команду, используя уже CLI:
Как мы видим, после выполнения команды у нас появился новый компонент, и если мы проверим содержимое файлов, то увидим, что они содержат лицензию. Причем, мы добавили лицензию только в созданные файлы и не добавляли ее в измененный файл модуля.
Изменение существующего кода
Последнее, что я хотел бы рассмотреть, как менять уже существующий код. Мы, конечно, уже меняли файлы в примерах выше, но это были простые изменения на уровне файлов. А что нам делать, если мы хотим внести логические изменения в существующие файлы проекта?
Например, нам надо добавить везде импорт нового модуля или изменить сигнатуру функции. Для того, чтобы изменить существующий код, будь то typescript или json файл, осмысленно, вам необходимо будет парсить код, строить ast дерево и работать уже с ним. Я не буду в этой статье рассказывать, как работать с ast деревом в typescript, для внесения изменений в код мы воспользуемся функциями из пакета Angular. Так как команда Angular использует schematics для выполнения задач CLI, то она уже написала набор функций для работы с исходниками. В принципе, в нем есть почти всё, что может понадобиться на начальных этапах, более сложные трансформации кода вам придётся писать уже самим. Итак, у нас стоит задача добавить сервис в основной модуль программы. Для этого надо сделать следующее:
- Найти основной модуль программы
- Добавить импорт нашего модуля, используя относительный путь
- Добавить наш модуль в секцию providers
Для реализации задуманного нам понадобятся следующие функции из пакета schematics Angular:
findModuleFromOptions – ищет модуль Angular, начиная с переданной директории и постепенно поднимаясь вверх.
buildRelativePath – строит относительный путь к нашему файлу.
addProviderToModule – добавляет новый провайдер в секцию providers указанного модуля. Кроме этой функции пакет Angular содержит такие функции как addExportToModule, addImportToModule, addDeclarationToModule, addBootstrapToModule, etc.
Итак, давайте рассмотрим код нашей команды по частям. Первое, что нам нужно научиться делать, это загружать файл с исходниками и строить из него ast дерево:
Дальше нам надо добавить провайдер в код модуля:
Для этого мы формируем путь к сервису, относительно нашего модуля используя функцию buildRelativePath и вызываем функцию addProviderToModule из пакета ангуляр. Функция addProviderToModule не внесет изменения в модуль, а вернет список изменений с типом InsertChange. Все изменения в исходный код необходимо внести самостоятельно и для этого мы воспользуемся транзакционным изменением файла из набора API schematics – beginUpdate, insertLeft и commitUpdate. Первая функция начинает транзакцию, вторая вносит изменения начиная слева в указанную позицию, а последняя применяет наши изменения к файлу.
Последнее, что теперь осталось, это добавить вызов правила из генератора:
Как мы видим, используя наработки команды Angular, изменять структуру проекта становится довольно просто, но если вам понадобится сделать более сложное изменение, то это лишь немного сложнее.
Заключение
Как мы увидели в этой статье, schematics – довольно гибкое и функциональное средство, которое может упростить работу с проектом. Мы рассмотрели далеко не все, что может данный пакет, да и команда Angular постоянно развивает проект, добавляя новые возможности. Возможно, в следующих статья мы продолжим рассмотрение остальных пакетов из @angular-devkit.
Как обычно, полный код всех примеров доступен на github — https://github.com/KyKyPy3/schematics.
Автор статьи: Роман Ефременко KyKyPy3uK