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

в 15:49, , рубрики: $mol, build system, cyclic references, dependencies, javascript, mam, modular system, TypeScript, versioning, Разработка веб-сайтов, системы сборки

Здравствуйте, меня зовут Дмитрий Карловский, и я… обожаю 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 изменения происходят между мажорными версиями. А у другого — между минорными. А так как поставить две версии одной зависимости на одном уровне невозможно, то ни о каком плавном переходе, когда в приложении какое-то время сосуществуют одновременно две версии библиотеки, не может быть и речи.

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

Красивая идея 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

Автор: Дмитрий Карловский

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js