MAM: сборка фронтенда без боли

    Здравствуйте, меня зовут Дмитрий Карловский, и я… обожаю MAM. MАМ управляет Агностик Модулями, избавляя меня от львиной доли рутины.


    Типичный Агностик Модуль


    Агностик Модуль, в отличие от традиционного, это не файл с исходником, а директория, внутри которой могут быть исходники на самых разных языках: программная логика на JS/TS, тесты к ней на TS/JS, композиция компонент на view.tree, стили на CSS, локализация в locale=*.json, картинки и тд, и тп. При желании не сложно прикрутить поддержку любого другого языка. Например, Stylus для написания стилей, или HTML для описания шаблонов.


    Зависимости между модулями трекаются автоматически путём анализа исходников. Если модуль включается, то включается целиком — каждый исходник модуля транспилируется и попадает в соответствующий бандл: скрипты — отдельно, стили — отдельно, тесты — отдельно. Для разных платформ — свои бандлы: для ноды — свои, для браузера — свои.


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


    Философия


    МАМ — это смелый эксперимент по радикальному изменению способа организации кода и процесса работы с ним. Вот основные принципы:


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


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


    Не платить за то, что не используешь. Используешь какой-то модуль — он включается в бандл со всеми своими зависимостями. Не используешь — не включается. Чем меньше модули, тем больше гранулярность и меньше лишнего кода в бандле.


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


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


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


    Самый простой путь — самый верный. Если правильный путь требует дополнительных усилий, то будьте уверены, что никто им не пойдёт.


    Импорты/экспорты


    Открываем первый попавшийся проект с использованием современной системы модулей: Модуль меньше чем на 300 строк, 30 из них — импорты.


    Но это ещё цветочки: Для функции из 9 строк требуется 8 импортов.


    И моё любимое: Ни одной строчки полезного кода. 20 строчек перекладывания значений из кучи модулей в один, чтобы потом импортировать из одного модуля, а не из двадцати.


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


    Всё это приводит к низкой гранулярности кода и раздуванию размеров бандлов неиспользуемым кодом, которому повезло оказаться рядом с тем, который используется. Эту проблему для JS худо-бедно пытаются решить усложнением сборочного пайплайна, путём добавления так называемого "tree-shaking", вырезающего лишнее из того, что вы наимпортировали. Это замедляет сборку, но вырезает далеко не всё.


    Идея: Что если мы не будем импортировать, а будем просто брать и использовать, а сборщик уже сам разберётся что нужно заимпортировать?


    Современные IDE умеют автоматически генерировать импорты для использованных вами сущностей. Если это может сделать IDE, то что мешает сделать это сборщику? Достаточно иметь простое соглашение об именовании и расположении файлов, которое было бы удобно для пользователя и понятно для машины. В PHP давно есть такое стандартное соглашение: PSR-4. MAM вводит аналогичное для .ts и .jam.js файлов: имена, начинающиеся с $ являются Fully Qualified Name какой-либо глобальной сущности, код которой подгружается по пути, получаемому из FQN путём замены разделителей на слеши. Простой пример из двух модулей:


    my/alert/alert.ts


    const $my_alert = alert // FQN предотвращает конфликты имён

    my/app/app.ts


    $my_alert( 'Hello!' ) // Ага, зависимость от /my/alert/

    Целый модуль из одной строки — что может быть проще? Результат не заставляет себя долго ждать: простота создания и использования модулей приводит к минимизации их размеров. Как следствие — к максимизации гранулярности. И как вишенка — минимизации размеров бандлов без каких-либо tree-shaking.


    Наглядный пример — семейство модулей валидации JSON /mol/data. Если вы воспользуетесь где-либо в своём коде функцией $mol_data_integer, то в бандл будут включены модули /mol/data/integer и /mol/data/number, от которого зависит $mol_data_integer. А вот, например, /mol/data/email сборщик даже не прочитает с диска, так как от него никто не зависит.


    Разгребая бардак


    Раз уж мы начали пинать Angular, то не будем останавливаться. Как вы думаете, где искать объявление функции applyStyles? Ни за что не догадаетесь, в /packages/core/src/render3/styling_next/bindings.ts. Возможность помещать что угодно куда угодно приводит к тому, что в каждом проекте мы наблюдаем уникальную систему расположения файлов, часто не поддающуюся никакой логике. И если в IDE зачастую спасает "прыжок к определению", то просмотр кода на гитхабе или обзор пулреквеста лишены такой возможности.


    Идея: Что если имена сущностей будут строго соответствовать их расположению?


    Чтобы расположить код в файле /angular/packages/core/src/render3/stylingNext/bindings.ts, в МАМ архитектуре придётся назвать сущность $angular_packages_core_src_render3_stylingNext_applyStyles, но так, конечно, никто не поступит, ведь тут столько всего лишнего в имени. А ведь имена в коде хочется видеть короткими и лаконичными, поэтому из названия разработчик постарается исключить всё лишнее, оставив лишь важное: $angular_render3_applyStyles. А расположится это соответственно в /angular/render3/applyStyles/applyStyles.ts.


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


    73ebc45e517ffcc3dcce53f5b39b6d06fc95cae1 $mol_vector: range expanding support
    3a843b2cb77be19688324eeb72bd090d350a6cc3 $mol_data: allowed transformations
    24576f087133a18e0c9f31e0d61052265fd8a31a $mol_data_record: support recursion

    Или, допустим, вы хотите найти все упоминания модуля $mol_fiber в интернете — сделать это проще простого благодаря FQN.


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


    Напишем в одном файле 7 строк простого кода:


    export class Foo {
        get bar() {
            return new Bar();
        }
    }
    
    export class Bar extends Foo {}
    
    console.log(new Foo().bar);

    Не смотря на циклическую зависимость он работает корректно. Разобьём его на 3 файла:


    my/foo.js


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

    my/bar.js


    import { Foo } from './foo.js';
    
    export class Bar extends Foo {}

    my/app.js


    import { Foo } from './foo.js';
    
    console.log(new Foo().bar);

    Опа, ReferenceError: Cannot access 'Foo' before initialization. Что за бред? Чтобы это починить, наш app.js должен знать, что foo.js зависит от bar.js. Поэтому нам надо сначала заимпортировать bar.js, который заимпортирует foo.js. После чего мы уже можем заимпортировать foo.js без ошибки:


    my/app.js


    import './bar.js';
    import { Foo } from './foo.js';
    
    console.log(new Foo().bar);

    Что браузеры, что NodeJS, что Webpack, что Parcel — все они криво работают с циклическими зависимостями. И ладно бы они их просто запрещали — можно было бы сразу усложнить код так, чтобы циклов не было. Но они могут работать нормально, а потом бац, и выдать непонятную ошибку.


    Идея: Что если при сборке мы будем просто склеивать файлы в правильном порядке, как если бы весь код был изначально написан в одном файле?


    Давайте разделим код, используя принципы МАМ:


    my/foo/foo.ts


    class $my_foo {
        get bar() {
            return new $my_bar();
        }
    }

    my/bar/bar.ts


    class $my_bar extends $my_foo {}

    my/app/app.ts


    console.log(new $my_foo().bar);

    Всё те же 7 строчек кода, что были изначально. И они просто работают без дополнительных шаманств. Всё дело в том, что сборщик понимает, что зависимость my/bar от my/foo более жёсткая, чем my/foo от my/bar. А значит включать в бандл эти модули следует именно в таком порядке: my/foo, my/bar, my/app.


    Как сборщик это понимает? Сейчас эвристика простая — по числу отступов в строке, в которой обнаружена зависимость. Обратите внимание, что более сильная зависимость в нашем примере имеет нулевой отступ, а слабая — двойной.


    Разные языки


    Так уж получилось, что для разных вещей у нас есть разные языки под эти разные вещи заточенные. Из наиболее распространённых это: JS, TS, CSS, HTML, SVG, SCSS, Less, Stylus. У каждого своя система модулей, никак не взаимодействующая с другими языками. Что и говорить про 100500 видов более специфичных языков. В результате, чтобы подключить компонент, приходится отдельно подключать его скрипты, отдельно стили, отдельно регистрировать шаблоны, отдельно настраивать деплой необходимых ему статических файлов и тд, и тп.


    Webpack благодаря лоадерам пытается решить эту проблему. Но у него точкой входа является скрипт, который подключает уже файлы на остальных языках. А если нам не нужен скрипт? Например, у нас есть модуль с красивыми стилями для табличек и мы хотим, чтобы в светлой теме они имели одни цвета, а в тёмной другие:


    .dark-theme table {
        background: black;
    }
    .light-theme table {
        background: white;
    }

    При этом, если мы зависим от темы, то должен быть подгружен скрипт, который установит нужную тему в зависимости от времени суток. То есть CSS фактически зависит от JS.


    Идея: Что если модульная система не будет зависеть от языков?


    Так как в MAM модульная система отделена от языков, то зависимости могут быть кроссязыковыми. CSS может зависеть от JS, который может зависеть от TS, который может зависеть от другого JS. Достигается это за счёт того, что в исходниках обнаруживаются зависимости от модулей, а модули подключаются целиком и могут содержать исходники на любых языках. В случае примера с темами это выглядит так:


    /my/table/table.css


    /* Ага, зависимость от /my/theme */
    [my_theme="dark"] table {
        background: black;
    }
    [my_theme="light"] table {
        background: white;
    }

    /my/theme/theme.js


    document.documentElement.setAttribute(
        'my_theme' ,
        ( new Date().getHours() + 15 ) % 24 < 12 ? 'light' : 'dark' ,
    )

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


    Много библиотек


    Обычно точкой входа для сборки бандла является какой-то файл. В случае Webpack это JS. Если вы разрабатываете множество отчуждаемых библиотек и приложений, то вам нужно множество же и бандлов. И для каждого бандла нужно создавать отдельную точку входа. В случае Parcel точкой входа является HTML, который для приложений в любом случае придётся создавать. Но для библиотек это как-то не очень подходит.


    Идея: Что если любой модуль можно будет собрать в независимый бандл без предварительной подготовки?


    Давайте соберём последнюю версию сборщика MAM проектов $mol_build:


    mam mol/build

    А теперь запустим этот сборщик и пусть он соберёт сам себя ещё раз чтобы убедиться, что он всё ещё способен сам себя собрать:


    node mol/build/-/node.js mol/build

    Хотя, нет, давайте вместе со сборкой попросим его ещё и тесты прогнать:


    node mol/build/-/node.test.js mol/build

    И если всё прошло успешно, опубликуем результат в NPM:


    npm publish mol/build/-

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


    • web.dep.json — вся информация о графе зависимостей
    • web.js — бандл скриптов для браузеров
    • web.js.map — сорсмапы для него
    • web.esm.js — он же в виде es-модуля
    • web.esm.js.map — и для него сорсмапы
    • web.test.js — бандл с тестами
    • web.test.js.map — и для тестов сорсмапы
    • web.d.ts — бандл с типами всего, что есть в бандле скриптов
    • web.css — бандл со стилями
    • web.css.map — и сорсмапы для него
    • web.test.html — точка входа, чтобы запустить тесты на исполнение в браузере
    • web.view.tree — декларации всех включённых в бандл view.tree компонент
    • web.locale=*.json — бандлы с локализованными текстами, для каждого обнаруженного языка свой бандл
    • package.json — позволяет тут же опубликовать собранный модуль в NPM
    • node.dep.json — вся информация о графе зависимостей
    • node.js — бандл скриптов для ноды
    • node.js.map — сорсмапы для него
    • node.esm.js — он же в виде es-модуля
    • node.esm.js.map — и для него сорсмапы
    • node.test.js — тот же бандл, но ещё и с тестами
    • node.test.js.map — и для него сорсмапы
    • node.d.ts — бандл с типами всего, что есть в бандле скриптов
    • node.view.tree — декларации всех включённых в бандл view.tree компонент
    • node.locale=*.json — бандлы с локализованными текстами, для каждого обнаруженного языка свой бандл

    Статика просто копируется вместе с путями. В качестве примера, возьмём приложение, которое выводит собственные исходные коды. Его исходники лежат тут:


    • /mol/app/quine/quine.view.tree
    • /mol/app/quine/quine.view.ts
    • /mol/app/quine/index.html
    • /mol/app/quine/quine.locale=ru.json

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


    /mol/app/quine/quine.meta.tree


    deploy \/mol/app/quine/quine.view.tree
    deploy \/mol/app/quine/quine.view.ts
    deploy \/mol/app/quine/index.html
    deploy \/mol/app/quine/quine.locale=ru.json

    В результате сборки /mol/app/quine, они будут скопированы по следующим путям:


    • /mol/app/quine/-/mol/app/quine/quine.view.tree
    • /mol/app/quine/-/mol/app/quine/quine.view.ts
    • /mol/app/quine/-/mol/app/quine/index.html
    • /mol/app/quine/-/mol/app/quine/quine.locale=ru.json

    Теперь директорию /mol/app/quine/- можно выложить на любой статический хостинг и приложение будет полностью работоспособно.


    Целевые платформы


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


    Идея: Что если предназначение файла будет отражено в его имени?


    В MAM используется система тегов в именах файлов. Например, модуль $mol_state_arg предоставляет доступ к задаваемым пользователем параметрам приложения. В браузере эти параметры задаютя через строку адреса. А в ноде — через аргументы командной строки. $mol_sate_arg абстрагирует всё остальное приложение от этих нюансов путём реализации обоих вариантов с единым интерфейсом, располагая их в файлах:


    • /mol/state/arg/arg.web.ts — реализация для браузеров
    • /mol/state/arg/arg.node.ts — реализация для ноды

    Исходники не помеченные этими тегами включаются независимо от целевой платформы.


    Аналогичная ситуация наблюдается и с тестами — их хочется хранить рядом с остальными исходниками, но не хочется включать в тот бандл, что пойдёт конечному пользователю. Поэтому тесты тоже помечаются отдельным тегом:


    • /mol/state/arg/arg.test.ts — тесты модуля, они попадут в бандл с тестами

    Теги могут быть и параметрическими. Например, с каждым модулем могут идти тексты на самых разных языках и они должны быть включены в соответствующие языковые бандлы. Файл с текстами — это обычный JSON-словарь, именованный с указанием локали в имени:


    • /mol/app/life/life.locale=ru.json — тексты для русского языка
    • /mol/app/life/life.locale=jp.json — тексты для японского языка

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


    • /hyoo/toys/.git — начинается с точки, поэтому сборщик эту директорию проигнорирует

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


    Сперва Гугл выпустил AngularJS и опубликовал его в NPM как angular. Потом он создал совершенно новый фреймворк с похожим названием — Angular и опубликовал его под тем же самым именем, но уже версии 2. Теперь эти два феймворка развиваются независимо. Только у одного ломающие API изменения происходят между мажорными версиями. А у другого — между минорными. А так как поставить две версии одной зависимости на одном уровне невозможно, то ни о каком плавном переходе, когда в приложении какое-то время сосуществуют одновременно две версии библиотеки, не может быть и речи.


    Кажется команда Ангуляра наступила уже на все возможные грабли. И вот ещё одни: код фреймворка разбит на несколько крупных модулей. Сначала они версионировали их независимо, но очень быстро даже сами начали путаться какие версии модулей совместимы между собой, что уж говорить о рядовых разработчиках. Поэтому Angular перешёл на сквозное версионирование, где мажорная версия модуля может меняться даже без каких-либо изменений в коде. Поддержка множества версий множества модулей — это большая проблема как для самих мейнтейнеров, так и для экосистемы в целом. Ведь куча ресурсов всех участников сообщества тратится на обеспечение совместимости с уже устаревшими модулями.


    Красивая идея Semantic Versioning разбивается о суровую реальность — вы никогда не знаете, сломается ли у вас что-то, при изменении минорной версии или даже версии патча. Поэтому во многих проектах фиксируют конкретную версию зависимости. Однако, такая фиксация не действует на транзитивные зависимости, которые могут притянуться последней версии при установке с нуля, а могут остаться прежней, если уже стоят. Эта неразбериха приводит к тому, что вы никогда не можете положиться на зафиксированную версию и вам регулярно необходимо проверять совместимость с актуальными версиями (как минимум транзитивных) зависимостей.


    А как же лок-файлы? Если вы разрабатываете библиотеку, устанавливаемую через зависимости, лок-файл вам не поможет, ибо будет проигнорирован менеджером пакетов. Для конечного же приложения лок-файл даст вам так называемую "воспроизводимость сборок". Но давайте будем честными. Сколько раз вам нужно собирать конечное приложение из одних и тех же исходников? Ровно один раз. Получая на выходе, не зависящий ни от каких NPM, артефакт сборки: исполнимый бинарник, докер-контейнер или просто архив со всем необходимым для запуска кодом. Надеюсь вы не делаете npm install на проде?


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


    С фиксацией версий же зависимости очень быстро протухают, создавая вам даже больше проблем, чем решают. Например, однажды в одной компании стартанули проект на актуальном на тот момент Angular@4 (или даже 3). Фреймворк развивался, но никто его не обновлял, ибо "это не входит в скоуп задачи" и "мы не брали это в спринт". Была написана куча кода под Angular@4 и никто даже не знал, что он не совместим с Angular@5. Когда же на горизонте замаячил Angular@6, команда решила таки взять обновление этой зависимости в спринт. Новый Angular потребовал новый TypeScript и кучу других зависимостей. Потребовалось переписать кучу собственного кода. В итоге, по истечении 2 недель спринта, было решено… отложить обновление фреймворка до лучших времён, так как business value сам себя не создаст, пока команда возвращает технический долг, взятый с, как выяснилось, адскими процентами.


    А вишенкой на торте из версионных граблей является спонтанное появление в бандле нескольких версий одной и той же зависимости, о чём вы узнаёте, лишь когда замечаете аномально долгую загрузку приложения, и лезете разбираться, почему размер вашего бандла вырос в 2 раза. А всё оказывается просто: одна зависимость требует одну версию Реакта, другая — другую, третья — третью. В результате на страницу грузится аж 3 React, 5 jQuery, 7 lodash.


    Идея: Что если у всех модулей будет только одна версия — последняя?


    Мы фундаментально не можем решить проблему несовместимости при обновлениях. Но мы можем научиться как-то с этим жить. Признав попытки фиксации версий несостоятельными, мы можем отказаться от указания версий вовсе. При каждой установке любой зависимости, будет скачан самый актуальный код. Тот код, который сейчас поддерживается мейнтейнером. Тот код, который сейчас видят все остальные потребители библиотеки. И все вместе решают проблемы с этой библиотекой, если они вдруг возникают. А не так, что одни уже обновились и бьются над проблемой, а у других хата с краю и они ни чем не помогают. А помощь может быть самая разная: завести issue, объяснить мейнтейнерам важность проблемы, найти workaround, сделать pull request, форкнуть в конце концов, если мейнтейнеры совсем забили на поддержку. Чем больше людей одновременно испытывают одну и ту же боль, тем скорее найдётся тот, кто эту боль устранит. Это объединяет людей для улучшения единой кодовой базы. В то же время версионирование фрагментирует сообщество по куче разных используемых версий.


    Без версионирования мейнтейнер гораздо быстрее получит обратную связь от своих потребителей и либо выпустит хотфикс, либо попросту откатит изменения для лучшей их проработки. Зная, что неосторожный комит может сломать сборку всем потребителям, мейнтейнер будет более ответственен ко внесению изменений. Ну либо его библиотеками никто не будет пользоваться. И тут появится запрос на более продвинутый тулинг. Например такой: репозиторий зависимости, рассылает уведомления всем зависимым проектам о том, что появился комит в фиче-ветке. Те проверяют с этой фиче-веткой интеграцию и если обнаруживаются проблемы — присылают подробности о них в репозиторий зависимости. Таким образом, мейнтейнер библиотеки мог бы получать обратную связь от потребителей ещё до того, как влить свою фиче-ветку в мастер. Такой пайплайн был бы очень полезен и при версионировании, но, как видим, в экосистеме NPM ничего такого до сих пор не распространено. Всё потому, что нет острой потребности в этом. Отказ от версий же форсирует развитие экосистемы.


    Но что если всё же надо сломать обратную совместимость, но не хочется ломать всем сборку? Всё просто — создаём новый модуль. Был mobx, стал mobx2 и меняй в нём API как захочешь. Казалось бы — это то же версионирование, но есть фундаментальная разница: раз это два разных модуля, то они могут быть оба поставлены одновременно. При этом последняя реализация mobx может быть реализована как легковесный адаптер к mobx2, реализующий на его основе старый API. Таким образом можно осуществлять плавный переход между несовместимыми API, не раздувая бандл дублирующимся кодом.


    Отсутствие версионирования даёт и ещё один неожиданный эффект. Обнаружив зависимость сборщик всегда знает какую именно версию надо установить — последнюю. То есть, чтобы воспользоваться снипетом из интернета вида:


    var pages_count = $mol_atom2_sync( ()=> $lib_pdfjs.getDocument( uri ).promise ).document().numPages

    Вам не нужно ставить модули mol_atom2_sync и lib_pdfjs, подбирая подходящие для этого снипета версии:


    npm install mol_atom2_sync@2.1 lib_pdfjs@5.6

    Всё, что вам нужно, — это написать код, а все зависимости установятся автоматически при сборке. Но как сборщик узнаёт откуда брать какие модули? Всё очень просто — не найдя ожидаемой директории, он смотрит файлы *.meta.tree, где может быть указано какие директории из каких репозиториев брать:


    /.meta.tree


    pack node git \https://github.com/nin-jin/pms-node.git
    pack mol git \https://github.com/eigenmethod/mol.git
    pack lib git \https://github.com/eigenmethod/mam-lib.git

    Это фрагмент корневого мапинга. Таким же образом вы можете выносить любые подмодули вашего модуля в отдельные репозитории.


    Интеграция с NPM


    MAM — совершенно иная нежели NPM экосистема. Однако, пытаться перекладывать код из одной системы в другую — контрпродуктивно. Поэтому, мы работаем над тем, чтобы использовать опубликованные в NPM модули было бы не слишком болезненно.


    Если вам нужно на сервере обратиться к уже установленному NPM модулю, то можете воспользоваться модулем $node. Например, давайте найдём какой-нибудь свободный порт и поднимем на нём статический веб-сервер:


    /my/app/app.ts


    $node.portastic.find({
        min : 8080 ,
        max : 8100 ,
        retrieve : 1
    }).then( ( ports : number[] ) => {
        $node.express().listen( ports[0] )
    })

    Если же надо именно включить в бандл, то тут всё немного сложнее. Поэтому-то и появился пакет lib содержащий адаптеры к некоторым популярным NPM библиотекам. Например, вот как выглядит подключение NPM-модуля pdfjs-dist:


    /lib/pdfjs/pdfjs.ts


    namespace $ {
        export let $lib_pdfjs : typeof import( 'pdfjs-dist' ) = require( 'pdfjs-dist/build/pdf.min.js' )
        $lib_pdfjs.disableRange = true
        $lib_pdfjs.GlobalWorkerOptions.workerSrc = '-/node_modules/pdfjs-dist/build/pdf.worker.min.js'
    }

    /lib/pdfjs/pdfjs.meta.tree


    deploy \/node_modules/pdfjs-dist/build/pdf.worker.min.js

    Надеюсь в будущем нам удастся упростить эту интеграцию, но пока что так.


    Окружение разработчика


    Для старта нового проекта часто приходится настраивать очень много вещей. Именно поэтому появились всякие create-react-app и angular-cli, но ни прячут от вас свои конфиги. Вы, конечно, можете сделать eject и эти конфиги переедут в ваш проект. Но тогда он станет намертво привязан к этой эджектнутой инфраструктуре. Если вы разрабатываете множество библиотек и приложений, то хотели бы единообразно работать с каждым из них, и вносить свои кастомизации сразу для всех.


    Идея: Что если инфраструктура будет отделена от кода?


    Инфраструктура в случае MAM живёт в отдельном от кода репозитории. У вас может быть множество проектов в рамках одной инфраструктуры.


    Проще всего начать работать с MAM форкнув репозиторий с базовой MAM инфраструктурой, где уже всё настроено:


    git clone https://github.com/eigenmethod/mam.git ./mam && cd mam
    npm install
    npm start

    На порту 8080 поднимется сервер разработчика. Всё, что вам останется — лишь писать код в соответствии с принципами MAM.


    Заведите себе собственный неймспейс (для примера — acme) и пропишите в нём ссылки на ваши проекты (для примера — hello и home ):


    /acme/acme.meta.tree


    pack hello git \https://github.com/acme/hello.git
    pack home git \https://github.com/acme/home.git

    Для сборки конкретных модулей достаточно приписать пути до них после npm start:


    npm start acme/hello acme/home

    Переводить уже существующий проект на эти рельсы довольно затруднительно. А вот начать новый — самое то. Попробуйте, будет сложно, но вам понравится. А если столкнётесь трудностями — пишите нам телеграммы: https://t.me/mam_mol

    Поддержать автора
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 37

      0

      Здравствуйте! Давно не видели историй о вашем интересном проекте.


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

        0

        Если кто-то воспользовался вашим внутренним классном вместо задокументированного API, то:
        1) Оно ему действительно надо.
        2) Он берёт на себя всю ответственность за возможные тяжёлые последствия.


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

          0

          Как документировать API в MAM структуре? Желательно, не руками, а генерацией из исходников

            0

            Что именно генерировать? Страничку с сигнатурами функций? Гораздо лучше и быстрее эту информацию предоставит IDE.

              0

              есть два класса, $my_class_one и $my_class_two. Как понять, какой из них публичный, а какой – нет?

                0

                Можете добавить doc-коментарий к этим классам, можете назвать их так, чтобы было очевидно, что есть что.

                  0

                  Значит, ответ на изначальный вопрос такой и будет: приватное API определяется doc-комментариями. Ясно, спасибо за ответ.

                    0

                    Тут неплохо описано, почему в MAM всё публично: https://habr.com/ru/post/457034/

                      0

                      В Node.js так не считают, потому что регулярно огребают (раз, два) от monkey-patching нативных модулей.

                        0

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

        +5
        И все вместе решают проблемы с этой библиотекой, если они вдруг возникают. А не так, что одни уже обновились и бьются над проблемой, а у других хата с краю и они ни чем не помогают.
        Здравствуйте, меня зовут Дмитрий Карловский, и я… люблю писать фантастику. В жанре «магический реализм».

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

          Когда я слышу такие рассуждения, мне всегда представляется бедный студент, перебивающийся от халявы к халяве и жалующийся, что на дорогу до неё приходится тратиться.

            +3
            Похоже это не магический реализм, а отрицание реальности.

            Окэй, возьмём «одну компанию» из текущей статьи:
            В итоге, по истечении 2 недель спринта, было решено… отложить обновление фреймворка до лучших времён, так как business value сам себя не создаст, пока команда возвращает технический долг, взятый с, как выяснилось, адскими процентами.

            Эта компания вместо каких-то технических обновлений решила делать business value. А теперь представим, что эта компания выкинула ExtJS, Angular, Dart и так далее, у них теперь $jin $mol, MAM и розовые единороги с бабочками. Теперь у них тот же спринт выглядит так:

            1. Посреди спринта внезапно подкралось обновление библиотеки (библиотека тоже использует философию MAM, но она движется вперёд). Она теперь требует новую версию TS (MicroSoft слишком большие, чтобы смотреть на какой-то хайповый МАМ и делают всё по-старому, с богопротивными версиями) и кучу всего. Мы не можем не обновить: наш проект просто перестал собираться, ибо это философия МАМ! Обновляем свой старый код.
            2. Параллельно выяснилось, что новая версия библиотеки плохо работает на тех юзкейзах, которые характерны для нашего продукта. Сообщество помогает нам исправлять.
            3. Параллельно нам пришлось выделить 3 команды на помощь сообществу с другими проблемами, которые у нас решены.
            4. Мы не можем в этом спринте увеличить ценность продукта, ибо занимались техническим долгом (или правильней это назвать «техническим прерыванием?»). И посему весь скоуп спринта переехал на следующий спринт (разбить и перенести, ага).


            Что мы имеем в итоге? Бизнес получает те же самые проблемы (обновление версий из окружающего мира), только теперь это стало вынужденным. И если стейкхолдерам сейчас очень важно сделать бизнес-ценность, например выкатить важную фичу перед переговорами о новом раунде инвестиций (стейкхолдеры наши — бедные студенты, перебивающиеся от халявы к халяве, поэтому пусть речь идёт об очередных $15 млн инвестиций), то они получат фигу. И, учитывая, что эта компания за две недели так и не перешла с Ангуляр@3 на Ангуляр@5, то нет никаких гарантий, что это «техническое прерывание» не займёт и весь следующий спринт. И да, сообщество по-прежнему помогает исправлять те просадки, которые выявились в новой версии — с этим всё по-прежнему не ясно.
              0

              Не могли бы вы вести себя более культурно? Заранее брагодарен.


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


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


              А если уж фантазировать про переход с Ангуляра на $mol, то могу вас заверить, что высвободившихся в результате такого перехода ресурсов хватит, чтобы пилить той же командой в 3 раза больше фичей, не пренебрегая обновлениями и рефакторингом. Достаточно взглянуть на эту статистику, чтобы понять размер трагедии:



              И это при том, что в $mol куда больше функциональности, чем в Angular.

                0
                но по моему опыту держать проект постоянно в тонусе обходится в конечном счёте дешевле, чем делать это скачкообразно, когда припекает.

                А по моему — наоборот.
                  0

                  Это как так получается?


                  В какой вселенной проще обновлять пакеты всем скопом, а не поштучно?

                    0
                    Эм… Об этом речи не было. Я о том, что каждый пакет проще обновлять не каждый раз.
                  +1

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

                    –1

                    Чинить баг.

                      +2

                      Ага, тратим целый день (как минимум) на анализ чужой библиотеки, отправляем пулреквест, а автор говорит: «Ок, спасибо, посмотрю как буду свободен».

                        0

                        Жизненно, бывало такое.


                        Вот тут предлагается интересный воркэраунд для этой проблемы: https://mobile.twitter.com/ryanflorence/status/1139552733029998592

                          0

                          Тоже бывало такое, в некоторых случаях будеь удобней форкнуть и положить в корпоративный репозиторий (или просто в npm), тогда и другие коллеги смогут использовать/поддерживать просто как зависимость. А через несколько месяцев, когда автор проснётся и примет PR, вернуться на оригинальный пакет.

                          –3

                          Чинить баг в своём коде. Не путайтесь в показаниях. Если же автор библиотеки закоммитил баг, а не фикс, то пинаете автора. Если автор тормозит, откатываетесь на предыдущую ревизию и продолжаете работу. Если автор совсем в спячке — форкаете и откатываете. Ну а если чувствуете в себе силы — чините самостоятельно. Вариантов много, и вы прекрасно сами все их знаете. В том числе и временная фиксация ревизии. Экстраординарные обстоятельства требуют экстраординарных решений. Ну а если один и тот же автор постоянно косячит — это хороший повод переключиться на альтернативу.

                            +2

                            Я не путаюсь в показаниях. Речь именно о баге во внешней библиотеке, повлекшей баг в собственном проекте.


                            Значит предлагается зафиксировать ревизию, если нет возможности быстро обновиться? Чем же это отличается от фиксации версии или использования ^x.0.0 в семвере? По сути обязанность автора библиотеки — в случае внесения обратно несовместимых изменений создать новый проект с увеличенным номером, но если ему трудно соблюсти семантическое версионирование, то вряд ли он будет соблюдать МАМ-версионирование, которое выглядит более трудоёмким. Гарантий опять же никаких нет. К тому же «новую» версию популярной библиотеки могут засквоттить недоброжелатели.

                              0

                              В статье есть ответы на все ваши вопросы:


                              Мы фундаментально не можем решить проблему несовместимости при обновлениях. Но мы можем научиться как-то с этим жить.

                              ...


                              Заведите себе собственный неймспейс (для примера — acme) и пропишите в нём ссылки на ваши проекты (для примера — hello и home ):
                      +1
                      Не могли бы вы вести себя более культурно? Заранее брагодарен.
                      Я себя где-то не культурно веду? Например считаю, что вы отрицаете реальность? А как это сказать более культурно? «Сударь, не будете ли вы любезны вернуться в нашу реальность»? Да и с каких пор вы, ведя себя всегда в комментариях резко и вызывающе (прямо скажем — некультурно), стали таким неженкой?

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

                      … то могу вас заверить, что высвободившихся в результате такого перехода ресурсов хватит, чтобы пилить той же командой в 3 раза больше фичей
                      Не надо заверять, я уже видел вживую совершенно обратный результат в «одной компании».
                        –1

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

                          0

                          Ой ли? Мой первый рабочий день был на следующий день после вашего увольнения. Или переименовав $jin в $mol тут же родилась новая библиотека?


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

                            +3

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

                0
                Например, однажды в одной компании стартанули проект на актуальном на тот момент Angular@4 (или даже 3)


                Это такой подвох для внимательного читателя? Angular 3 никогда не существовал, сразу перескочив на 4, как раз из-за перехода на semver и решения проблем зоопарка версий ключевых модулей.
                      0
                      На Хабре столько раз обсуждалось, что в ТС-варианте 3-я версия пропущена, а для Дарт-варианта не пропущена… Технически вы правы, если под словом Angular подразумевать только TS-вариант. Но, как мне кажется, автор подразумевал именно Дарт-вариант (несмотря на эпатажность и заскоки автора, он всё-таки разбирается в предметной области). И вообще в контексте статьи этот вопрос не важен :)
                        +1

                        Это не так. Третьей версией я, разумеется, назвал Angular2 с модулями, добравшимися, до 3+ версии. Впрочем, конкретные цифры не важны в повествовании.

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

                    Буквально только что пофиксил багу с прода привнесённую минификатором.


                    Идея: Что если у девелопера будет крутиться абсолютно тот же код, что и на проде?

                    0
                    Некоторые пункты применимы и без внедрения собственно МАМ:

                    — Соглашения вместо конфигурирования
                    — Инфраструктура отдельно, код отдельно
                    — Не платить за то, что не используешь
                    — Минимум лишнего кода

                    А собственно версионирование — необходимое зло для поддержания порядка, но которое можно обуздать.

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

                    Самое читаемое