hyoo-ru / HabHub

Peering social blog
The Unlicense
0 stars 0 forks source link

Введение в $mol. Часть 1. Модульная система MAM #4

Open PavelZubkov opened 2 years ago

PavelZubkov commented 2 years ago

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

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

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

Модуль

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

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

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

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

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

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

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

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

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

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

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

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

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

Что это дает:

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

Сборка

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

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

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

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

Примеры: $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, но есть возможность расширить их список.

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

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

Например:

Подробный разбор того какие файлы модуля поддерживаются 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 Online Dev Workspace , окружение установится автоматически, согласитесь установить плагины.

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

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

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

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

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

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

В руководстве будет использоваться неймспейс 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 Файл с инструкциями для сборщика, поддерживает несколько команд:

Тег 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 добавляется в конец.

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

Серверные бандлы Все файлы с префиксом 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. Он состоит из:

После того как браузер загрузит 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 и tss

Создайте модуль $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

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

  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-пакета выглядит так:

Простой пример подключения 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

Далее

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

mendrew commented 5 months ago

@PavelZubkov привет! Спасибо за доку!

Нашел очепятку в названии метода -> mound.