1. Предисловие

    1. MAM

    2. Реактивность

    3. View-компоненты

  2. Часть 1. Модульная система MAM

    1. Идеи и концепции

      1. Модуль

      2. Соглашения вместо конфигурации

      3. Отделение прикладного кода от инфраструктурного

      4. Фрактальные моно-поли-репозитории

      5. Версионирование

      6. Сборка

      7. Понятные имена

      8. Автоматический импорт/экспорт

      9. Гранулированность

      10. Оптимизация размера бандла

      11. Независимость от языков

      12. Разные типы файлов

      13. Тестовые бандлы

      14. Одинаковый код на dev и prod

    2. Погружение

      1. Установка MAM-окружения и настройка VSCode

      2. Где находятся исходники MAM?

      3. Как создать модуль?

      4. Какие языки и форматы может содержать модуль?

      5. Как называть файлы модуля?

      6. Начальная реализация модуля $my_counter

      7. Как собрать модуль вручную?

      8. Где искать результаты сборки?

      9. Какие файлы создает сборщик?

      10. Запуск дев-сервера

      11. Как импортировать и экспортировать модули?

      12. Что если модуль используется много раз и его имя слишком длинное?

      13. Какие модули включаются в дистрибутив?

      14. Зависимость между css и ts

      15. Как перенести модуль в другое место?

      16. Монорепозитории и полирепозитории

      17. Разделение кода на платформы

      18. Как использовать NPM-пакеты в node?

      19. Как добавить статический файл в дистрибутив?

      20. Как включить модуль в проект, на который никто не ссылается?

      21. Развертывание модуля в NPM

      22. Как использовать NPM в web?

      23. Развертывание приложения на Github Pages

      24. Циклические зависимости

    3. Рекомендации по использованию MAM

  3. В следующей части

  4. Контакты

Предисловие

Эта статья открывает серию публикаций по обучению фреймворку $mol. Сегодня мы разберемся в модульной системе MAM. Речь пойдет об организации кода, отделении его от инфраструктуры, сборке, версионировании, нейминге, минимизации размера бандла, автоматическом выкачивании зависимостей, фрактальных моно-поли-репозиториях, разделении кода на платформы, альтернативе импортам/экспортам, автоматическом разруливании циклических зависимостей.

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

Для кого-то богатство экосистемы фреймворка является определяющим фактором. В $mol много модулей, они закрывают большинство потребностей. Недостающие можно собрать из существующих или использовать npm.

Как это не парадоксально звучит, но чем больше у вас опыта, тем сложнее освоить $mol. В нем используется много непривычных подходов, которые при первом знакомстве вызывают отторжение. Но стоит их переварить и становится непонятно, как жилось без этого раньше.

MAM

MAM - это модульная система, в которой живут модули $mol. Абстрактный модуль - это директория с файлами, которые его реализуют. MAM - это набор правил, ограничений и принципов, которые превращают код в кубики LEGO.

Изучение $mol, лучше начинать с MAM. Разберитесь как использовать модули на практике, а затем приступите к знакомству с системой реактивности.

Реактивность

Если вы знаете, как работает Mobx, то уже представляете как работает система реактивности в $mol. Тут она используется на всех уровнях, а не только на уровне представления.

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

При вызове, реактивное свойство запоминают к каким свойствам оно обращалось во время выполнения, и кто к нему обращался.

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

Всё приложение на этапе выполнения представляет собой большое дерево зависимостей. Каждое свойство знает свои зависимости, это дает простой и надежный механизм управления жизненным циклом объектов - они создаются, когда появляется зависимость, и уничтожаются, когда от них ничего не зависит. Это решает две фундаментальные проблемы: утечки памяти и инвалидацию кэша.

View-компоненты

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

В $mol компонент состоит из нескольких частей, а каждая часть, находится в отдельном файле, можно выделить 5 частей:

  • Декларативное описание интерфейса компонента и потоков данных(обязательное, остальное опционально);

  • Императивное поведение компонента;

  • Стили;

  • Локализация;

  • Тесты.

Есть базовый класс $mol_view с набором свойств, такими как события, атрибуты, дети и т.д. Он является оберткой над одним DOM-элементом. Когда любое из его свойств становится неактуальным, реактивная система автоматически вызывает его актуализацию, что точечно обновляет элемент в DOM-дереве.

Пользовательские компоненты, наследуются от базового класса, и настраивают его компоновку и поведение.

Для описания компонент используется язык view.tree. С виду он выглядит сложными и нечитаемым, но это только с первого взгляда, точно такое же впечатление производит html, когда видишь его впервые. Но view.tree не html, там всего порядка 10 операторов, на изучение которых необходимо несколько часов. Он не является шаблоном в общепринятом понимании, это ближе к интерфейсам в typescript.

С помощью view.tree мы говорим:

  • как называется наш компонент;

  • от какого компонента он наследуется;

  • какими компонентами он владеет;

  • как эти компоненты компонуются в его children;

  • какие состояния имеет компонент;

  • какими потоками данных связаны компоненты и состояния.

Сборщик из view.tree описания генерирует класс, от которого можно отнаследоваться и добавить поведение.

Часть 1. Модульная система MAM

MAM - модульная система, в которой переосмыслена работа с кодом и его организация. Она проектировалась для единообразной работы с произвольными объемами кода, облегчая переиспользование и минимизируя рутину.

MAM расшифровывается как Mam owns Abstract Modules.

Идеи и концепции

Модуль

Модуль - директория, внутри которой находятся файлы реализующие его. Разные части модуля находятся в разных файлах. Абстрактный - значит, что модуль не привязан к какому-то конкретному языку, а может быть реализован на нескольких. Модуль первичен, а технологии на которых он реализован - вторичны.

Особенности:

  • Один модуль - одна директория.

  • Модули произвольно вкладываются друг в друга, все есть модуль.

  • Имя модуля - путь до него в файловой системе.

  • Модуль может выступать пространством имен - содержать только директории.

  • Зависимости между модулями отслеживаются автоматически.

  • Разные типы исходников модуля, попадают в разные бандлы.

Можно провести аналогию с БЭМ методологией, там используется термин "блок", первичен блок, его реализация вторична.

Соглашения вместо конфигурации

В MAM нет пользовательского конфига, вместо него используются соглашения. Соглашение можно рассматривать как некое условие, которое нужно выполнить, чтобы получить требуемый результат.

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

Отделение прикладного кода от инфраструктурного

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

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

Фрактальные моно-поли-репозитории

В начале у нас один репозиторий с проектом. Когда он разрастется, часть можно вынести в отдельный репозиторий. Репозитории могут образовывать дерево, вкладываясь друг в друга. При разделении на несколько репозиториев, код остается неизменным, добавляется только ссылка на удаленный репозиторий. MAM автоматически клонирует нужные для проекта репозитории. Локально код всех приложений выглядит как один моно-репозиторий.

Версионирование

Подход к версионированию в MAM называется "verless" - безверсионность. Он работает по принципу открытости/закрытости.

  • Модуль всегда имеет одну версию - последнюю.

  • Версии которые сохраняют обратную совместимость API, публикуются под одним именем - рефакторинг, фиксы, расширение.

  • Не совместимые, под разными именами - $mol_atom -> $mol_atom2.

  • Реализация старого интерфейса, может использовать новую реализацию (или наоборот), что предотвращает дублирование.

Что это дает:

  • Мейнтейнер и пользователи модуля фокусируются на одной "версии", вместо распыления внимания на несколько.

  • Несколько "версий" одного модуля могут сосуществовать рядом. Возможна плавная миграция.

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

  • Важную функциональность необходимо покрывать тестами.

В случае, если обновление что-то ломает, фиксация ревизии обеспечивается системой контроля версий.

Сборка

Любой модуль можно собрать независимо, без предварительной подготовки. Сборщик автоматически установит недостающие зависимости и скачает удаленные репозитории, от которых зависит собираемый модуль. Артефакты помещаются в директорию - (минус) , которая создается в папке с модулем.

Далее будем называть директорию, в которую помещаются артефакты сборки - дистрибутив. А отдельный артефакт в ней - бандл.

Понятные имена

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

  • Через знак подчеркивания, повторять путь до этого модуля в файловой системе.

  • Начинаться с $ (не во всех языках такое возможно, в ts/js - да, в css - нет).

Примеры: $my_alert, $mol_data_record, $hyoo_crowd_doc

Такое именование называется Fully Qualified Name - оно позволяют однозначно идентифицировать сущность, независимо от контекста ее использования.

Это ограничение позволяет:

  • Лучше продумывать имена модулей и структуру приложения.

  • Разработчик всегда знает, что где лежит.

  • Делает имена глобально-уникальными.

  • Упрощает анализ кода.

Автоматический импорт/экспорт

IDE умеют генерировать импорты автоматически, что мешает делать это сборщику? В MAM не нужно использовать импорты/экспорты, чтобы воспользоваться сущностью из другого модуля, достаточно просто написать ее имя.

$mol_assert_ok( true )

Сборщик по FQN-именам понимает где и какой модуль используется, и автоматически их подключает при сборке.

Гранулированность

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

Для этого нужно, простое создание и использование модулей. В MAM для создания модуля, достаточно создать директорию с файлом, а для использования обратится к FQN-имени.

Оптимизация размера бандла

Так как модули имеют высокую гранулированность, а в сборке участвуют только зависимые модули, то бандлы имеют минимальный размер.

Независимость от языков

Для разных вещей используются разные языки: js, css, html, svg, ts, и т.д. Например в webpack, точкой входа является скрипт, в котором подключаются файлы на остальных языках. А что если модуль состоит только из CSS?

В MAM модульная система отделена от языков, т.е. зависимости могут быть кросс-языковыми. css может зависеть от js, который зависит от ts. В исходниках обнаруживаются зависимости от модулей, а модули подключаются целиком(все файлы) и могут содержать исходники на любых языках - точнее на тех, которые сейчас поддерживает MAM, но есть возможность расширить их список.

Разные типы файлов

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

Например:

  • *.node.ts - код из этого файла попадет только в бандл node.js;

  • *.web.ts - код попадет только в бандл web.js;

  • *.ts - попадет в оба бандла web.js и node.js;

  • *.test.ts - попадет в бандл с тестами web.test.js и node.test.js;

  • *.node.test.ts - также можно у платформу;

  • *.locale=ru.json, *.locale=en.json - файлы локализации для русского и английского.

Подробный разбор того какие файлы модуля поддерживаются MAM и какие бандлы создаются в дистрибутиве производится ниже.

Тестовые бандлы

MAM создает дополнительные бандлы с тестами web.test.js и node.test.js. В них добавляется код приложения и код тестов(для web это не совсем так, объясняется ниже), тесты создаются в файлах *.test.ts*. При запуске тестового бандла, исполняется код приложения, после него запускаются тесты.

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

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

Одинаковый код на dev и prod

В NPM-пакетах можно встретить ситуацию, что код который запускается во время разработки отличается от кода, который публикуется. Ситуация когда ошибка воспроизводится только на production не исключительна. MAM специально не преобразует код в production бандлах, при разработке запускается тот же код. Отличие только в том, что в тестовые бандлы добавляется код тестов.

Погружение

Далее, на примере небольшого веб-приложения - счетчик, мы попробуем MAM на практике и разберем подробности его работы. В следующих частях цикла к этому приложению добавим реактивность и переведем его на $mol-компоненты.

Установка MAM-окружения и настройка VSCode

  1. Обновите NodeJS до LTS версии.

  2. Загрузите репозиторий MAM.

git clone https://github.com/hyoo-ru/mam.git ./mam && cd mam
  1. Установите зависимости, вашим пакетным менеджером.

npm install
  1. Установите плагины для VSCode

Можно использовать gitpod, окружение установится автоматически, согласитесь установить плагины.

MAM-окружение достаточно установить один раз и использовать для всех проектов

Где находятся исходники MAM?

Репозиторий с MAM-окружением зависит от NPM-пакета mam. Этот пакет является модулем $mol_build, который опубликован в NPM, исходники модуля тут. Вся логика MAM сейчас реализована в этом модуле. Сам модуль создан одним из первых и нуждается в рефакторинге. Есть прототип новой версии, но пока нет ресурсов для его завершения.

Как создать модуль?

  1. Подумать над именем.

  2. Создать директорию с файлом.

Условно, модули можно разделить на три типа:

  • Пространство имен/namespace - модуль, который содержит только другие модули.

  • Модуль - директория с файлами и другими модулями.

  • Подмодуль - модуль внутри другого модуля Один и тот же модуль в разных контекстах можно назвать и тем и другим.

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

Создадим неймспейс и модуль:

  1. Перейдите в директорию с MAM - cd mam

  2. Создайте директорию для неймспейса - mkdir my && cd my

  3. Создайте директорию для модуля приложения - mkdir counter

Какие языки и форматы может содержать модуль?

MAM возник вместе с $mol и часть файлов заточена под него. Но в целом, ограничений нет, при необходимости можно добавить поддержку других, например dockerfile.

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

  • index.html

    Это обычный html, который может содержать произвольную разметку. Он является точкой входа для бандла web.js, в нем определяется корневой DOM-элемент, к которому будет монтироваться приложение.

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

  • package.json

    Сборщик автоматически генерирует package.json. Он поможет при публикации модуля в NPM и при разработке под NodeJS - информация о зависимостях и некоторая другая генерируется автоматически.

    В директории модуля можно разместить файл package.json с необходимым содержимым, тогда сборщик смержит его со сгенерированным package.json.

  • readme.md

    Используется для документации модулей. Если модуль еще разрабатывается, добавьте строку unstable. При сборке модуля, этот файл копируется в дистрибутив. Если он отсутствует, то сборщик ищет файл readme.md в родительском модуле и так рекурсивно до корня. Пример.

  • .ts

    Код на typesctipt

  • .jam.js

    Код на javascript тоже поддерживается, но необходимо перед расширением добавлять jam (javascript abstract module). Пример.

  • .web.ts, .node.ts

    Для разделения кода по платформам используются теги web и node. Если тег указан, то код попадет в указанный бандл, web.js или node.js. Если тег не указан, то код попадет в оба бандла.

  • .test.ts

    Код в файле с тегом test попадет в тестовый бандл, их тоже два web.test.js и node.test.js. В месте с тегом test, можно указывать тег платформы - *.web.test.ts.

  • .css

    Произвольный css код. В FQN-именах у css - знак $ не ставится в начале.

  • .css.ts

    Статически типизированный, каскадный css in ts, можно использовать только с компонентами $mol.

  • .view.tree

    Декларативное описания view-компонент, используется в $mol. Можно использовать для описания любых классов.

  • .locale=*.json*

    Локализованные тексты на разных языках, используется в $mol. Тег locale принимает параметр - язык текстов, например *.locale=ru.json.

  • .meta.tree

    Файл с инструкциями для сборщика, поддерживает несколько команд:

    • deploy - копирует указанный файл в дистрибутив

    • require и include - включает указанный модуль в зависимости, даже если в коде он не используется.

    • pack - указывает адрес удаленного репозитория для подмодуля

  • Тег view

    В $mol принято файлам реализующим view-компонент, добавлять тег view - counter.view.tree, counter.view.ts, counter.view.css. У формата .view.tree, view - это не тег, а часть расширения.

    Все теги - это часть составного расширения. Если читать расширение справа налево, получится конкретизация от общего к частному.

Как называть файлы модуля?

Обычно файлам дают имя модуля, например counter.view.tree, counter.view.ts, counter.view.css для файлов в модуле my/counter. Но сборщику не важны их имена, он читает все файлы модуля, которые поддерживает. Важно как называются сущности внутри них, например класс компонента my/counter в коде должен называться class $my_counter {}.

index.html, package.json, readme.md - называются всегда одинаково.

Начальная реализация модуля $my_counter

Создайте файл mam/my/counter/index.html с таким содержимым:

<!doctype html>
<html style="height: 100%">

	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
	</head>

	<body style="width: 100%; height: 100%; margin: 0">
		<div id="root"></div>
		<script src="web.js"></script>
	</body>

</html>

Создайте mam/my/counter/counter.ts, весь код приведенный ниже поместите в него. Позже мы его разделим на модули.

class View {

	// Тут и ниже, такие поля используются для кеширования.
	// Удалятся при добавлении реактивности, в следующей главе
	_dom_node = null as unknown as Element
	// Создание DOM-ноды и регистрация событий на ней
	dom_node() {
		if ( this._dom_node ) return this._dom_node

		const node = document.createElement( this.dom_name() )

		for ( const [name, fn] of Object.entries(this.event()) ) {
			node.addEventListener(name ,fn)
		}

		// Атрибут с именем класса, для матчинга из css
		node.setAttribute('view', this.constructor.name)

		return this._dom_node = node
	}

	// Актуализация атрибутов и полей
	dom_node_actual() {
		const node = this.dom_node()

		for ( const [name, val] of Object.entries(this.attr()) ) {
			node.setAttribute(name, String(val))
		}

		for ( const [name, val] of Object.entries(this.field()) ) {
			node[name] = val
		}

		return node
	}

	// Подготовка и рендеринг дочерних компонентов
	dom_tree() {
		const node = this.dom_node_actual()

		const node_list = this.sub().map( node => {
			if ( node === null ) return null
			return node instanceof View ? node.dom_tree() : String(node)
		} )

		// Воспользуемся рендером из $mol
		$.$mol_dom_render_children( node , node_list )

		return node
	}

	// Методы ниже будут переопредялятся в компонентах-наследниках
	
	// Имя DOM-элемента
	dom_name() {
		return 'div'
	}
	// Объект с атрибутами
	attr(): { [key: string]: string|number|boolean|null } {
		return {}
	}
	// Объект с событиями
	event(): { [key: string]: (e: Event) => any } {
		return {}
	}
	// Объекст с полями
	field(): { [key: string]: any } {
		return {}
	}
	// Дочерние компоненты
	sub(): Array<View | Node | string | number | boolean> {
  	return []
  }

}

Класс View - обертка для DOM элемента, предоставляющая интерфейс для упрощения работы с ним.

Функция $mol_dom_render_children рендерит дочерние элементы, без лишних вставок и удалений в DOM-дереве. Сейчас нам нет смысла ее реализовывать, поэтому воспользуемся готовой из $mol.

Теперь создадим несколько компонентов на базе класса View. Мы наследуемся от него, переопределяем нужные методы.

class Button extends View {

	dom_name() { return 'button' }

	title() { return '' }

	click( e: Event ) {}

	sub() {
		return [ this.title() ]
	}

	event() {
		return {
			click: (e: Event) => this.click(e)
		}
	}
}

class Input extends View {
	dom_name() { return 'input' }

	type() { return 'text' }

	_value = ''
	value( next = this._value ) {
		return this._value = next
	}

	change( e: Event ) {
		this.value( (e.target as HTMLInputElement).value )
	}

	field() {
		return {
			value: this.value(),
		}
	}

	attr() {
		return {
			type: this.type(),
		}
	}

	event() {
		return {
			input: (e: Event)=> this.change(e),
		}
	}
}

И добавляем класс с логикой приложения.

class Counter extends View {
	// Синхронизайия с localStorage,
	// все вкладки приложения будут синхронизироваться
	storage<Value>( key: string, next?: Value ) {
		if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )

		if ( next === null ) localStorage.removeItem( key )
		else localStorage.setItem( key, JSON.stringify( next ) )

		return next
	}

	count( next?: number ) {
		return this.storage( 'count' , next ) ?? 0
	}

	count_str( next?: string ) {
		return this.count( next?.valueOf && Number(next) ).toString()
	}

	inc() {
		this.count( this.count() + 1 )
	}

	dec() {
		this.count( this.count() - 1 )
	}

	// Создаем инстанс Button
	// Переопределяем title
	// click биндим на this.inc
	_Inc = null as unknown as View
	Inc() {
		if (this._Inc) return this._Inc
		
		const obj = new Button
		obj.title = ()=> '+'
		obj.click = ()=> this.inc()

		return this._Inc = obj
	}

	_Dec = null as unknown as View
	Dec() {
		if (this._Dec) return this._Dec

		const obj = new Button
		obj.title = ()=> '-'
		obj.click = ()=> this.dec()

		return this._Dec = obj
	}

	_Count = null as unknown as View
	Count() {
		if (this._Count) return this._Count

		const obj = new Input
		obj.value = (next?: string)=> this.count_str( next )

		return this._Count = obj
	}

	sub() {
		return [
			this.Dec(),
			this.Count(),
			this.Inc(),
		]
	}

	static mount() {
		const node = document.querySelector( '#root' )
		const obj = new Counter()

		node?.replaceWith( obj.dom_tree() )

		// Реактивность добавится в следующей главе, сейчас воспользуемся костылем
		setInterval( ()=> obj.dom_tree() , 100 )
	}
}

// Вызываем для монтирования приложения в DOM-дерево
Counter.mount()

Как собрать модуль вручную?

Сборка запускается командой npm start путь/до/модуля. При ручном запуске, сборщик собирает все бандлы, которые поддерживает.

Соберите приложение

cd mam
npm start my/counter

После запуска, сборщик вернет ошибку ReferenceError: document is not defined для строки const node = document.querySelector('#root'). Обратите внимание на имя файла node.test.js, он node.test.js запускается автоматически сразу после сборки.

У нас в коде, после объявления класса Counter, запускается статический метод Counter.mount(). Внутри него есть обращение к window.document, т.к. node.test.js запускается под NodeJS, мы получаем ошибку.

Сейчас мы добавим костыль, позже исправим. Добавьте строку в начало метода Counter.mount

static mount() {
	if ( typeof document === 'undefined' ) return // +

	const node = document.querySelector( '#root' )
	const obj = new Counter()

	node?.replaceWith( obj.dom_tree() )

	setInterval( ()=> obj.dom_tree() , 100 )
} 

Запустите сборку снова.

Где искать результаты сборки?

Выше мы уже говорили, что бандлы помещаются в директорию -, она создается в директории модуля. Также вы можете увидеть еще несколько директорий название которых начинается со знака минус -css, -view.tree, -node - это промежуточные результаты, они создаются по необходимости.

Когда модуль выносится в отдельный репозиторий в .gitignore достаточно добавить строку -*

Какие файлы создает сборщик?

Теперь заглянем в директорию дистрибутива mam/my/counter/-.

index.html, test.html
Это точка входа, для запуска модуля в браузере. Если файл index.html создан в модуле, то он будет просто скопирован. Автоматически сборщик не создает его.

Файл test.html создается всегда, не зависимо от наличия index.html. Он нужен для того чтобы запустить тесты в браузере. Если index.html отсутствует, то test.html генерируется автоматически, с таким контентом:

<!doctype html><meta charset="utf-8" /><body><script src="web.js" charset="utf-8"></script>
	
	<script src="/mol/build/client/client.js" charset="utf-8"></script>
	<script>
	addEventListener( 'load', ()=> {
		const test = document.createElement( 'script' )
		test.src = 'web.test.js'
		const audit = document.createElement( 'script' )
		audit.src = 'web.audit.js'
		test.onload = ()=> document.head.appendChild( audit )
		document.head.appendChild( test )
	} )
	</script>

Тут подключается web.js файл, он содержит код модуля и зависимостей. Файл /mol/build/client/client.js - небольшой скрипт, открывает соединение по веб-сокетам с дев-сервером и по его команде перезагружает страницу. По событию load загружается web.test.js - тесты для браузера и web.audit.js - выводит в лог ошибки типов typescript.

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

Если есть index.html, его содержимое копируется в test.html и часть начиная с загрузки client.js добавляется в конец.

Браузерные бандлы

  • web.js содержит код собираемого модуля и код модулей от которых зависит;

  • web.*.map source map;

  • web.esm.js тоже самое, только в формате esm модуля;

  • web.d.ts файл с декларациями typescript типов;

  • web.test.js содержит тесты модуля и тесты его зависимостей, самого кода модуля и его зависимостей в нем нет, т.к. в html файл web.js подгружается отдельно;

  • web.view.tree сюда складываются деклорации из всех view.tree файлов;

  • web.locale=en.json локализация на английском, генерируется автоматически путем анализа view.tree, для других языков этот файл копируется, переводится и помещается в директорию с модулем;

  • web.view=*.json локализация для других языков, просто копируется из директории модуля в дистрибутив;

  • web.deps.json информация о графе зависимостей модулей;

  • web.audit.js в случае ошибок в проверке типов, тут будет console.log с информацией о них. Если ошибок нет, то console.log("Audit passed").

Серверные бандлы
Все файлы с префиксом node предназначены для запуска под NodeJS. Список файлов, точно такой же как и для браузера. Отличие только в коде, т.е. исходный код файлов с тегом node попадает только в серверные бандлы, а с тегом web только в браузерные.

node.test.js содержит и код модуля с зависимостями и тесты к ним, в отличие от web.test.js.

readme.md
Копируется из директории с модулем, если в модуле его нет, то ищется в родительском модуле и так до корня.

package.json
Сборщик автоматически генерирует файл package.json, используется для публикации пакетов в NPM и для серверных приложений. Если приложение использует NPM-пакеты, то они будут указаны в зависимостях. Если этот файл присутствует в модуле, то он буде объединен со сгенерированным файлом.

Запуск дев-сервера

Давайте запустим дев-сервер, он перезапускает сборку при изменении зависимостей модуля и перезагружает страницу в браузере.

Выполните команду:

cd mam
npm start

Ссылка http://127.0.0.1:9080 появится в терминале. Откройте ее, вы увидите в файловом менеджере директории находящиеся в mam. Откройте модуль приложения mam/my/counter. Обнаружив файл index.html, дев-сервер начнет сборку этого модуля.

На текущий момент, поддерживается пересборка только для модулей содержащих файл index.html.

Когда вы откроете модуль c файлом index.html, в адресной строке браузера будет путь http://127.0.0.1:9081/my/counter/-/test.html. Он состоит из:

  • путь до модуля, который вы собираете /my/counter,

  • директория дистрибутива /-/

  • запрашиваемый бандл test.html

После того как браузер загрузит html документ, начнется загрузка js файла, ссылка на который находится в теге script - <script src="web.js"></script>. Браузер сделает запрос по такому адресу http://127.0.0.1:9081/my/counter/-/web.js.

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

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

Сейчас дев-сервер поддерживает сборку только веб-приложений. Если вы разрабатываете NodeJS проект, то вы можете запустить дев-сервер и вручную отправлять запрос за файлом node.test.js, для его пересборки.

Как импортировать и экспортировать модули?

MAM автоматически отслеживает зависимости между модулями, анализируя FQN-имена в исходниках. Если мы хотим "экспортировать", какую-то сущность из модуля, чтобы она была доступна в других модулях, необходимо дать ей имя в формате FQN. Чтобы использовать метод, функцию или любую сущность из другого модуля, нужно просто написать ее имя.

Например, в модуле $my_csv объявлено две функции

// mam/my/csv/cvs.ts
function $my_csv_decode( text = 'a;b;c\n1;2;3' ) {
	return $mol_csv_parse( text )
}

function $my_csv_encode( list = [['a','b','c'], [1,2,3]] ) {
	return list.map(
		line => line.map( cell => `"${cell.replace( /"/g, '""' )}"` ).join(';')
	).join('\n')
}

Ими можно воспользоваться в любом другом модуле, просто написав имя $my_csv_decode( 'q;w;\n1;2' ), будто она объявлена выше в этом же файле.

Обратите внимание, что совпадать должен только префикс имени $my_csv_ с путем до файла mam/my/csv. В этом же файле мы можем объявить функцию с таким именем $my_csv_decode_stream, это не значит что мы обязаны класть эту функцию в mam/my/csv/decode/stream.

Теперь давайте воспользуемся FQN-именами в нашем приложении и заодно разобьем его на несколько модулей.

Получится такая структура:

mam /
	my /
		counter /
			view /
			button /
			input /	
  1. Переименуйте класс View в $my_counter_view, создайте файл mam/my/counter/view/view.ts и перенесите туда код этого класса.

  2. Тоже самое делаем с классом Button

// mam/my/counter/button/button.ts

class $my_counter_button extends $my_counter_view {
	dom_name() { return 'button' }

	title() { return '' }

	click( e: Event ) {}

	sub() {
		return [ this.title() ]
	}

	event() {
		return {
			click: (e: Event) => this.click(e)
		}
	}
}

И с классом Input

// mam/my/counter/input/input.ts

class $my_counter_input extends $my_counter_view {
	dom_name() { return 'input' }

	type() { return 'text' }

	_value = ''
	value( next = this._value ) {
		return this._value = next
	}

	event_change( e: Event ) {
		this.value( (e.target as HTMLInputElement).value )
	}

	field() {
		return {
			value: this.value(),
		}
	}

	attr() {
		return {
			type: this.type(),
		}
	}

	event() {
		return {
			input: (e: Event)=> this.event_change(e),
		}
	}
}

В файле mam/my/counter/counter.ts остался класс Counter. Измените его имя на $my_counter и имена переименованных классов.

Запустите дев-сервер, если еще не сделали этого, и убедитесь что приложение работает.

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

Положите ссылку на него в переменную с более коротким именем.

const Response = $mol_data_record({
	status: $mol_data_number,
	data: $mol_data_record({
		name: $mol_data_string,
		surname: $mol_data_string,
		age: $mol_data_number,
		birth_date: $mol_data_pipe( $mol_data_string, $mol_time_moment ),
	}),
})

Станет:

const Rec = $mol_data_record
const Str = $mol_data_string
const Num = $mol_data_number

const Response = Rec({
	status: Num,
	data: Rec({
		name: Str,
		surname: Str,
		age: Num,
		birth_data: $mol_data_pipe( Str, $mol_time_moment ),
	}),
})

Какие модули включаются в дистрибутив?

Сборка работает по нескольким правилам:

  1. В бандлы включаются все модули от которых зависит собираемый модуль. Анализируется по FQN-именам.

  2. Включаются модули, подключенные командами include и require, а также копируются статические файлы командой deploy. Подробнее рассматривается ниже.

  3. Включается родительский модуль, для каждого включенного модуля. Например, при сборке модуля /a/b/c, в бандлы будут включены модули c, b, a, /. Код родительского модуля, будет включен раньше кода модуля. Как пример можно рассмотреть модуль mam, он находится непосредственно в репозитории с дев-окружением, и будет включен в дистрибутив при сборке любого модуля.

  4. Модуль включается целиком. Если модуль включается в дистрибутив, то все его файлы, с которыми умеет работать MAM, будут включены в соответствующие бандлы. Подмодули не включаются автоматически, только если срабатывают правила выше.

Добавим модуль для работы с localStorage. Создайте директорию для модуля $my_counter_storage и ts файл.

// mam/my/counter/storage/storage.ts

class $my_counter_storage {

	static value<Value>( key: string, next?: Value ) {
		if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )

		if ( next === null ) localStorage.removeItem( key )
		else localStorage.setItem( key, JSON.stringify( next ) )

		return next
	}

}

Сейчас не нужно его использовать в $my_counter.

Добавьте в файл mam/my/counter/button/button.ts еще одну кнопку - $my_counter_button_minor. Она не будет использоваться, нужна только для демонстрации.

// mam/my/counter/button/button.ts

class $my_counter_button extends $my_counter_view {

	dom_name() { return 'button' }

	title() { return '' }

	click( e: Event ) {}

	sub() {
		return [ this.title() ]
	}

	event() {
		return {
			click: (e: Event) => this.click(e)
		}
	}

}

class $my_counter_button_minor extends $my_counter_button {

	attr() {
		return {
			'my_counter_button_minor': true,
		}
	}

}

После сборки откройте бандл web.js. Найдите класс $my_counter_button_minor, он включен в бандл, потому что модуль $my_counter_button используется в приложении, а класс минорной кнопки объявлен именно в нем. Если вынести объявление кнопки в отдельный модуль mam/my/counter/button/minor, тогда она не добавится в бандл.

Класс $my_counter_storage вы не найдете в бандле, потому что он не используется в приложении.

Теперь используем модуль $my_counter_storage в коде.

// mam/my/counter/counter.ts
class $my_counter extends $my_counter_view {

// delete
// -	storage<Value>( key: string, next?: Value ) {
// -		if ( next === undefined ) return JSON.parse( localStorage.getItem( key ) ?? 'null' )
// -
// -		if ( next === null ) localStorage.removeItem( key )
// -		else localStorage.setItem( key, JSON.stringify( next ) )
// -
// -		return next
// -	}

	count( next?: number ) {
// -		return this.storage( 'count' , next ) ?? 0
		return $my_counter_storage.value( 'count' , next ) ?? 0 // +
	}

После сборки вы найдете модуль $my_counter_storage в бандле.

Зависимость между css и ts

Создайте модуль $my_theme с файлом theme.ts

// mam/my/theme/theme.ts
setInterval( ()=> {
	document?.documentElement.setAttribute(
		'my_theme' ,
		new Date().getSeconds() < 30 ? 'light' : 'dark' ,
	)
} , 1_000 )

Код выше, раз в 30 секунд меняет значение атрибута my_theme, на элементе html.

Создайте css файл в модуле $my_counter

/* mam/my/counter/counter.css */
[my_theme="light"] {
	background-color: white;
}

[my_theme="dark"] {
	background-color: black;
}

В css знак $ в FQN-именах не используется.

Откройте приложение, вы увидите что цвет фона страницы, меняется каждые 30 секунд.

Сборщик находит имя модуля $my_theme в файле counter.css и добавляет все файлы модуля $my_theme в бандлы. Найдите его код в бандле web.js.

После добавления css файла, вы найдете несколько новых модулей в js бандле. Сейчас стили добавляются в web.js бандл с помощью функции $mol_style_attach. Она и ее зависимости добавлены в бандл.

Как перенести модуль в другое место?

Во время рефакторинга, может понадобится перенести модуль в другое место. Поскольку имена зависят от месторасположения, их тоже нужно менять. Вам нужно выполнить команду редактора find and replace. Например, ищем все имена с именем my_module_name и заменяем на my_module_new_name, после перемещения файлов.

В meta.tree вместо FQN используются пути через /, их придется менять отдельно

Вынесем в отдельный модуль, модули с библиотечным кодом $my_counter_view, $my_counter_button и т.д., а в модуле $my_counter останется только код приложения.

Создайте директорию для модуля $my_lom

cd mam/my
mkdir lom

Переместите директорию $my_counter_view в $my_lom_view, и используя find and replace переименуйте все строки my_counter_view в my_lom_view. Поиск и замена производится на уровне корневой директории mam.

Повторите проделанное выше с остальными модулями: $my_counter_button, $my_counter_input, $my_counter_storage, $my_theme.

Сейчас весь библиотечный код находится в модуле $my_lom, а в модуле $my_counter остался только прикладной код. Убедитесь что приложение работает.

Монорепозитории и полирепозитории

MAM поддерживает работу с обоими типами репозиториев одновременно. Их можно вкладывать друг в друга, также как и модули. Сборщик автоматически клонирует отсутствующие репозитории. Можно произвольный модуль вынести в отдельный репозиторий, без изменения его исходного кода. Разработчик использует одно дев-окружение, в котором находятся все проекты рядом, каждый в своем неймспейсе/модуле, в том числе неймспейс mol. Т.е. при разработке это выглядит как один большой монорепозиторий, можно внести изменения в любой модуль, но при этом этот код может хранится во множестве удаленных репозиториях на github, gitlab, bitbucket и т.д.

Для того чтобы сборщик мог загрузить репозиторий, он должен знать его адрес. Ссылки на удаленные репозитории находятся в файле *.meta.tree, который размещается в родительском модуле, по отношению к модулю который выносится в удаленный репозиторий. Пример файла meta.tree

Команда выглядит так:
pack apps git \https://github.com/hyoo-ru/apps.hyoo.ru.git

  • pack - инструкция, говорит сборщику что перед нами удаленный репозиторий;

  • apps - имя директории подмодуля, в которую будет загружен репозиторий;

  • git - система контроля версия, сейчас поддерживается только одна;

  • после \ размещается ссылка на репозиторий.

Разберем на примере как это работает:

  1. Запускаем команду yarn start hyoo/draw.

  2. Сборщик не найдя директорию mam/hyoo, ищет meta.tree в директории mam.

  3. В meta.tree находит строку в которой директория подмодуля называется hyoo - pack hyoo git \https://github.com/hyoo-ru/mam_hyoo.git.

  4. Клонирует этот репозиторий в mam/hyoo.

  5. Ищет директорию mam/hyoo/draw, не найдя заглядывает в mam/hyoo/hyoo.meta.tree, теперь его интересует строка начинающаяся с pack draw. Найдя ее, клонирует репозиторий в mam/hyoo/draw.

  6. Приступает к сборке $hyoo_draw.

Для того, чтобы VSCode корректно работал с несколькими репозиториями, создайте файл mam/.gitmodules с подобным содержимым:

[submodule "mam_mol"]
    path = mol
    url = git@github.com:hyoo-ru/mam_mol.git

[submodule "hyoo"]
    path = hyoo
    url = git@github.com:hyoo-ru/hyoo.git

На вкладке контроля версий, у вас появится несколько репозиториев.

VSCode не всегда сразу отображает репозитории перечисленные в .gitmodules на вкладке Source Control. Чаще всего это решается перезапуском VSCode. Если перезапуск не помогает, то нужно изменить порядок модулей в .gitmodules

Сейчас мы вынесем неймспейс $my и модули $my_lom, $my_counter в отдельные репозитории.

  1. Создайте репозиторий с именем mam_my на github.com.

  2. Создайте git-репозиторий в модуле $my и свяжите его с репозиторием созданным на github. В листингах ниже **YOUR_NAME** замените на свое значение.

cd mam/my
echo "# mam_my - namespace for MAM-based projects" > readme.md
echo "-*" > .gitignore
git init
git add readme.md
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/mam_my.git
git push -u origin main
  1. Повторяем шаги 1 и 2 для модулей $my_lom и $my_counter. Репозитории назовите my_lom и my_counter соответственно.

cd mam/my/lom
echo "# mam_lom" > readme.md
echo "-*" > .gitignore
git init
git add --all
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/my_lom.git
git push -u origin main

cd mam/my/counter
echo "# mam_counter" > readme.md
echo "-*" > .gitignore
git init
git add --all
git commit -m "Init"
git branch -M main
git remote add origin https://github.com/**YOUR_NAME**/my_counter.git
git push -u origin main
  1. Создайте файл mam/my/my.meta.tree с таким содержимым:

pack lom git \https://github.com/**YOUR_NAME**/my_lom.git
pack counter git \https://github.com/**YOUR_NAME**/my_counter.git
  1. Отправьте коммит с ним в удаленный репозиторий

cd mam/my
git add my.meta.tree
git commit -m "Add meta.tree"
git push

Удалите директории модулей $my_lom и $my_counter, и запустите сборку модуля $my_counter - npm start my/counter. Сборщик загрузит репозитории и соберет модуль. В логе вы увидите следующее:

come
	time \2022-03-25T19:42:27.723Z
	place \$mol_exec
	dir \my/lom
	message \Run
	command \git init

come
	time \2022-03-25T19:42:27.738Z
	place \$mol_exec
	dir \my/lom
	message \Run
	command \git remote show https://github.com/**YOUR_NAME**/my_lom.git

come
	time \2022-03-25T19:42:28.475Z
	place \$mol_exec
	dir \my/lom
	message \Run
	command \git remote add --track main origin https://github.com/**YOUR_NAME**/my_lom.git

come
	time \2022-03-25T19:42:28.488Z
	place \$mol_exec
	dir \my/lom
	message \Run
	command \git pull

Разделение кода на платформы

MAM поддерживает две платформы - браузер и NodeJS. Файлы кода с тегом web будут включены только в web.js бандл, а с тегом node только в node.js. Код без тега платформы попадет в оба бандла.

Ранее, мы добавили костыль if (typeof document === undefined) return, для того чтобы не падали тесты под NodeJS. Теперь исправим его.

Но сначала займемся небольшим рефакторингом, добавьте два статических метода в класс $my_lom_view.

// mam/my/lom/view/view.ts
static root: ()=> typeof $my_lom_view
static mount() {
	const node = document.querySelector( '#root' )
	if ( !node ) return

	const View = this.root()
	const obj = new View

	node.replaceWith( obj.dom_tree() )
	setInterval( ()=> obj.dom_tree() , 100 )
}

Мы переместили метод mound и добавили метод root, который поможет указать какой компонент является корневым.

Удалите метод mount и его вызов из класса $my_counter и добавьте присвоение компонента методу root.

// mam/my/counter/counter.ts

// ...
// -	static mount() {
// -		if ( typeof document === 'undefined' ) return
// -
// -		const node = document.querySelector( '#root' )
// -		const obj = new $my_counter()
// -
// -		node?.replaceWith( obj.dom_tree() )
// -
// -		setInterval( ()=> obj.dom_tree() , 100 )
// -	}
}

// - $my_counter.mount()
$my_lom_view.root = ()=> $my_counter // +

Создайте файла view.web.ts в модуле $my_lom_view.

// mam/my/lom/view/view.web.ts

Promise.resolve( ()=> $my_lom_view.mount() )

Этот код будет добавлен только в web бандл. Метод mount вызывается асинхронно, потому что в бандле $my_lom_view находится выше, чем $my_counter и установка значения происходит после запуска этого кода. Соберите модуль $my_counter вручную и проверьте что этот код присутствует в web.js бандле и отсутcтвует в бандле node.js.

Как использовать NPM-пакеты в node?

Для использования NPM пакетов и модулей с которыми поставляется NodeJS создан специальный модуль $node. Вы просто пишите $node['is-odd'] или $node.express. Сборщик MAM автоматически установит эти пакеты их typescript типы, добавит их в бандл package.json.

Сейчас мы добавим изоморфности в наше приложение. Создайте директорию для модуля $my_lom_dom_context и файл context.ts.

// mam/my/lom/dom/context/context.ts
let $my_lom_dom_context: typeof globalThis

В общем коде для node и web, мы просто объявили переменную, которой будет присвоено различное значение в зависимости от платформы. Создайте файл context.web.ts.

// mam/my/lom/dom/context/context.web.ts
$my_lom_dom_context = self

В web мы присваиваем этой переменной объект window. Создайте файл context.node.ts.

// mam/my/lom/context/context.node.ts
$my_lom_dom_context = new $node.jsdom.JSDOM( '' , { url : 'https://localhost/' } ).window as any

В node мы присваиваем этой переменной экземпляр класса JSDOM, с помощью него наш код сможет работать под NodeJS. Для ssr такого решения не достаточно, но для тестов вполне.

as any потому что jsdom не реализует все браузерное API и его типы с typeof globalThis не сходятся.

Замените прямые обращения к document, на использование $my_lom_dom_context.document:

// $my_lom_view.mount
const node = $my_lom_dom_context.document.querySelector( '#root' )

// $my_lom_view.dom_node
const node = $my_lom_dom_context.document.createElement( this.dom_name() )

// $my_lom_theme
$my_lom_dom_context.document.documentElement.setAttribute( /*...*/ )

Запустите сборку вручную npm start my/counter. Если директория mam/node_modules не содержит jsdom, вы увидите в терминале подобные сообщения:

> start
> node ./mol/build/-/node.js "my/counter"

come
	time \2022-03-25T19:21:46.435Z
	place \$mol_exec
	dir \
	message \Run
	command \npm install jsdom

come
	time \2022-03-25T19:21:50.155Z
	place \$mol_exec
	dir \
	message \Run
	command \npm install @types/jsdom

В файл mam/my/counter/-/package.json добавятся зависимости:

{
	"jsdom": "*",
	"colorette": "*"
}

Модуль $node имеет зависимость от модуля, который использует NPM-пакет colorlette, поэтому он тоже добавился в package.json.

* используется, потому что MAM использует модель verless с NPM тоже. В экстренном случае, можно в директории модуля создать package.json и зафиксировать версию пакета в нем.

Файл mam/my/counter/-node/deps.d.ts содержит typescript типы для всех модулей NPM и NodeJS.

interface $node {
 	"jsdom" : typeof import( "jsdom" )
	"colorette" : typeof import( "colorette" )
	"path" : typeof import( "path" )
	"child_process" : typeof import( "child_process" )
}

Как добавить статический файл в дистрибутив?

Для этого в *.meta.tree предусмотрена команда deploy. Нужно его создать и добавить строку deploy \path/to/file/image.png. При сборке файл будет скопирован в mam/my/module/-/path/to/file/image.png. Пример meta.tree файла.

Давайте добавим fav-иконку к нашему приложению. Создайте модуль $my_counter_logo и скачайте туда эту иконку.

В модуле $my_counter, создайте файл counter.meta.tree:

deploy \/my/counter/logo/logo.svg

Соберите модуль npm start my/counter, после сборки, вы найдете иконку по пути mam/my/counter/-/my/counter/logo/logo.svg.

В index.html добавьте строку <link href="my/counter/logo/logo.svg" rel="icon"> внутри тега head.

Как включить модуль в проект, на который никто не ссылается?

Сборщик автоматически включает в дистрибутив модули от которых зависит собираемый модуль. Иногда вам нужно добавить модули, от которых ваш код не зависит. Например, когда вы создаете приложение с каталогом компонентов. Для этого используются две команды require и include, которые добавляются в файл meta.tree. Если необходимо подключить код указанного в команде модуля, раньше кода модуля в котором находится meta.tree, то используйте require иначе используйте include.

Создайте директорию для модуля $my_lom_lib и внутри файл lib.meta.tree, с таким содержимым:

include \/my/lom/button
include \/my/lom/dom
include \/my/lom/input
include \/my/lom/lib
include \/my/lom/storage
include \/my/lom/theme
include \/my/lom/view

Соберите модуль $my_lom_lib и проверьте js бандл. Все подключенные модули будут добавлены в него.

Развертывание модуля в NPM

Теперь настроим автопубликацию модуля $my_lom_lib в NPM. Для начала создайте аккаунт или войдите.

npm adduser
# or
npm login

Откройте бандл mam/my/lom/lib/-/package.json и проверьте поле name. Имя генерируется автоматически, несколько человек не смогут опубликовать модуль, если проходя это руководство используют имя my для неймспейса. Создайте файл package.json в модуле $my_lom_lib с другим именем.

// mam/my/lom/lib/package.json
{
    name: "my_lom_lib_test"
}

Поля из добавленного package.json будут объедены с полями сгенерированного package.json. Если вы сейчас соберете модуль, то увидите что бандле имя изменилось.

Убедимся что имя свободно.

npm search my_lom_lib_test

Если занято, измените его.

Создайте персональный токен доступа для NPM.

npm token create

Создайте секрет в репозитории my_lom с именем NPM_AUTH_TOKEN и созданным токеном в качестве значения.

Создайте файл mam/my/lom/.github/workflows/my_lom_lib.yml:

name: my_lom_lib

on:
  workflow_dispatch:
  push:
    branches: 
      - main

jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    
    - name: Environment Printer
      uses: managedkaos/print-env@v1.0
    
    - name: Build apps
      uses: hyoo-ru/mam_build@master2
      with:
        package: my/lom
        modules: lib

    - uses: JS-DevTools/npm-publish@v1
      with:
        token: ${{ secrets.NPM_AUTH_TOKEN }}
        package: ./my/lom/lib/-/package.json

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

При использовании NPM-пакетов, мы просто экспортируем их имена из нужного бандла web или node.

import {
  $my_lom_view as View,
} from "my_lom_lib_test/web";

Как использовать NPM в web?

Чтобы NPM-пакет работал в браузере, его код необходимо добавить в бандл web.js.

На данный момент MAM сборщик самостоятельно обрабатывает код NPM-пакетов, т.к. он не заточен под NPM, это приводит к некоторым проблемам. Например не умеет удалять неиспользуемые ветки кода - if (process.env.NODE_ENV === 'development') {}. Так же у MAM нет необходимости в treeshaking, т.к. он сразу подключает только используемые модули, но NPM-пакетам он нужен. В будущем код NPM-пакетов, будет обрабатываться отдельным бандлером, таким как webpack и т.п.

В общем виде процесс подключения NPM-пакета выглядит так:

  • создать модуль для этого пакета.

  • в модуле импортировать пакеты и типы через require.

  • положить все сущности которые надо экспортировать из пакета в переменные с FQN-именами.

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

Для NPM-пакетов создан отдельный модуль $lib, ссылка на репозиторий. При добавлении какого-то пакета, лучше добавлять его сразу туда.

При сборки модуля, NPM-пакеты установятся автоматически. При необходимости версию можно зафиксировать в корневом package.json.

Сейчас мы не будем будем использовать написанное ранее приложение, а добавим модуль подключающий ReactJS в MAM. Он уже добавлен сюда. Для практики, вы можете самостоятельно добавить другой пакет по примеру ниже.

Для начала нужно загрузить репозиторий модуля $lib, запустите npm start lib. Загрузить его можно и через git clone, но ссылка на репозиторий уже находится в mam/.meta.tree, сборщик сам загрузит его по ней.

Создайте директорию для модуля $lib_react и ts файл в ней.

namespace $ {

	const React = require('react') as typeof import('react')

	export const $lib_react = React
	export const $lib_react_jsx = React.createElement

}

Тут используется namespace $ {} - они используются в системе инверсии контроля $mol, как средство авторегистрации сущностей в контейнере, об этом мы поговорим в другой главе.

Мы просто импортировали ReactJS и положили его в $lib_react. В $lib_react_jsx вынесена функция createElement, она будет использоваться как короткая ссылка для указания какую функцию использовать при транспиляции tsx.

/** @jsx $lib_react_jsx */

Создадим модуль для react-dom - $lib_react_dom.

// mam/lib/react/dom/dom.ts
namespace $ {

	const ReactDOM = require('react-dom') as typeof import('react-dom')
	const Client = require('react-dom/client.js') as typeof import('react-dom/client')

	export const $lib_react_dom = ReactDOM
	export const $lib_react_dom_client = Client
}

В модуль $lib_react_demo положим приложение счетчик, для иллюстрации работы с модулем $lib_react.

<!-- mam/lib/react/demo/index.html -->

<!doctype html>
<html style="height: 100%">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">

<div id="root"></div>

<script src="web.js"></script>
// mam/lib/react/demo/demo.tsx

/** @jsx $lib_react_jsx */
namespace $ {

	export function $lib_react_demo() {

		const [count, count_set] = $lib_react.useState( 0 )

		return <div>
			<button onClick={()=> count_set(count - 1)}>-</button>
			<input
				type="text"
				value={count}
				onChange={e => count_set( Number( (e.target as HTMLInputElement).value ) )}
			/>
			<button onClick={()=> count_set(count + 1)}>+</button>
		</div>

	}

	const element = document.getElementById( 'root' )
	if ( !element ) throw new Error('Cannot find root element')

	const root = $lib_react_dom_client.createRoot( element )
	root.render( <$lib_react_demo /> )

}

Попробуйте открыть демо-приложение. Запустите дев-сервер и откройте в файловом менеджере lib/react/demo. Вы увидите ошибку.

// Uncaught ReferenceError: process is not defined
//    at Object.<anonymous> (react.development.js:12:1)
//    at -:2:1

if (process.env.NODE_ENV !== "production") {/*...*/}

Сборщик не умеет вырезать такие куски кода, пока что обойдемся заглушкой. Создадим для этого модуль $lib_react_env.

// mam/lib/react/env/env.ts
var process = process || { env: { NODE_ENV: 'development' } } as any

Используем var, чтобы без дополнительного кода объявить переменную на глобальном объекте. namespace тут не нужен, т.к. тогда typescript завернет этот код в вызов функции.

Нам нужно, чтобы этот код был включен в бандл и выполнен раньше, чем код ReactJS. Создадим файл mam/lib/react/react.meta.tree.

require \/lib/react/env

Код из модуля $lib_react_env, будет подключен в начало банла, перед кодом ReactJS.

Обновите страницу, приложение должно заработать. Этот адаптер нуждается в доработке, но для начала годится.

Развертывание приложения на Github Pages

Для этого нужно создайте файл .github/workflows/deploy.yml в модуле, который вынесен в отдельный репозиторий.

name: Deploy
on:
  workflow_dispatch:
  push:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: hyoo-ru/mam_build@master2
      with:
        token: ${{ secrets.GH_PAT }}
        package: 'my/wiki'
    - uses: alex-page/blazing-fast-gh-pages-deploy@v1.1.0
      if: github.ref == 'refs/heads/master'
      with:
        repo-token: ${{ secrets.GH_PAT }}
        site-directory: 'my/wiki/-'

После отправки коммита, приложение будет задеплоено. Исходники github action тут.

Сейчас настроим деплой нашего приложения на github. Сначала создайте персональный токен доступа тут. Создайте секрет в настройках репозитория my_counter, с созданным токеном, назовите его GH_PAT.

Создайте файл deploy.yml, не забудьте заменить \*\*YOUR_NAME\*\* в ссылке на репозиторий mam_my.

# mam/my/counter/.github/workflows/deploy.yml
name: Deploy

on:
  workflow_dispatch:
  push:

jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    
    - name: Build app
      uses: hyoo-ru/mam_build@master2
      with:
        token: ${{ secrets.GH_PAT }}
        package: 'my/counter'
        meta: |
          my https://github.com/**YOUR_NAME**/mam_my
    
    - name: Deploy on GitHub Pages
      if: github.ref == 'refs/heads/main'
      uses: alex-page/blazing-fast-gh-pages-deploy@v1.1.0
      with:
        repo-token: ${{ secrets.GH_PAT }}
        site-directory: 'my/counter/-'

Параметры, которые принимает github action описаны в readme. Параметр meta содержит ссылку на репозиторий неймспейса mam_my, т.к. у него в файле my.meta.tree указана ссылка на репозиторий модуля $my_lom, который нужен для сборки приложения. Если его не указывать, то сборка завершится ошибкой - не найден модуль ``$my_lom`.

После отправки изменений на github, начнется сборка, после ее окончания, вы найдете ссылку на приложение тут: https://github.com/**YOUR_NAME**/my_counter/settings/pages.
Скопируйте ее в описание репозитория.

Циклические зависимости

Сборщик оценивает жесткость зависимостей, чем она сильнее тем раньше модуль будет включен в бандл. Жесткость оценивается по простой эвристике - количеству отступов в строке, в которой находится зависимость.

Для демонстрации циклической зависимости, мы создадим несколько файлов с кодом вне директории MAM и запустим их с помощью NodeJS.

Создайте где-нибудь директорию cyclic и файл all.mjs

// cyclic/all.mjs
export class Foo {
    get bar() {
        return new Bar();
    }
}

export class Bar extends Foo {}

console.log(new Foo().bar);

Запустите его node app.mjs - ошибок нет. В нем есть циклическая зависимость, но код работает корректно. Разделим его на несколько файлов.

// cyclic/foo.mjs
import { Bar } from './bar.mjs';

export class Foo {
    get bar() {
        return new Bar();
    }
}
// cyclic/bar.mjs
import { Foo } from './foo.mjs';

export class Bar extends Foo {}
// cyclic/app.mjs
import { Foo } from './foo.mjs';

console.log(new Foo().bar);

Запустите node app.mjs, вы увидите ошибку ReferenceError: Cannot access 'Foo' before initialization. Для ее исправления, нужно добавить импорт bar.mjs перед foo.mjs.

// cyclic/app_fix.mjs
import './bar.mjs';
import { Foo } from './foo.mjs';

console.log(new Foo().bar);

Теперь перенесем этот код в MAM. Создайте модули $my_foo, $my_bar, $my_app с соответствующим содержимым.

// mam/my/foo/foo.ts
class $my_foo {
    get bar() {
        return new $my_bar();
    }
}
// mam/my/bar/bar.ts
class $my_bar extends $my_foo {}
// mam/my/app/app.ts
console.log(new $my_foo().bar);

Соберите модуль $my_app и посмотрите код в js бандле.

"use strict";
class $my_foo {
    get bar() {
        return new $my_bar();
    }
}
//my/foo/foo.ts
;
"use strict";
class $my_bar extends $my_foo {
}
//my/bar/bar.ts
;
"use strict";
console.log(new $my_foo().bar);
//my/app/app.ts

Сборщик склеивает файлы в таком порядке, как будто мы изначально писали код в одном файле. Зависимость $my_bar от $my_foo сильнее чем $my_foo от $my_bar, поэтому модули добавляются в бандл в таком порядке: $my_foo, $my_bar, $my_app.

Рекомендации по использованию MAM

  • Создайте корневой нейспейс - вам нужен модуль, который будет содержать все ваши наработки. Разместите его в отдельном репозиторий.

  • Если планируется распространять ваш код по открытой лицензии, то добавьте ссылку на него в корневой meta.tree через pull-request.

  • В рамках вашего неймспейса создаются неймспейсы для публичных и приватных модулей, которые разносятся по разным репозиториям.

  • Модули группируются по функциональности, а не по типу. Не должно быть директорий-складов actions, components, containers, helpers, utils и т.п. Модуль должен представлять из себя домен, который хорошо решает свою задачу. Например "хелпер" для сравнения объектов, можно отправить в модуль $my_object или сразу вынести в подмодуль $my_object_equal и использовать его на всех проектах.

  • Вся важная функциональность должна покрываться тестами.

  • При замораживании разработки репозиторий дев-окружения форкается и ревизии фиксируются через git-merge-tree, а также обновляется пайплайн на использование форка.

В следующей части

В следующей части мы научимся работать с системой реактивности $mol

Контакты

https://t.me/mam_mol - с вопросами, проблемами, предложеними пишите в наш чат. Вступайте, если следите за проектом.

https://t.me/mol_news - новости об экосистеме фреймворка $mol.