Управление зависимостями JavaScript

    Управление зависимостями JavaScript


    Всем привет! Меня зовут Слава Фомин, я ведущий разработчик в компании DomClick. За свою 16-ти летнюю практику я в первых рядах наблюдал за становлением и развитием JavaScript как стандарта и экосистемы. В нашей компании мы используем JavaScript, в первую очередь, для продвинутой front-end разработки и успели перепробовать достаточно большое количество различных технологий, инструментов и подходов, набить много шишек. Результатом этого кропотливого труда стал ценнейший опыт, которым я и хочу поделиться с вами.


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


    Вот список тем, которые я планирую затронуть в этом блоге:


    • Что такое пакет, манифест пакета и зависимости.
    • Как правильно описывать зависимости для различных типов проектов.
    • Как работает semver и как правильно использовать диапазоны версий в манифесте проекта.
    • Как установленные зависимости могут быть представлены в файловой системе, плюсы и минусы разных решений.
    • Как работает поиск зависимостей (resolving).
    • Какие существуют инструменты для работы с зависимостями.
    • Как правильно обновлять зависимости.
    • Как следить за безопасностью, отслеживать и предупреждать угрозы.
    • Для чего нужны lock-файлы и как правильно ими пользоваться.
    • Как можно эффективно работать над сотнями пакетов одновременно, используя монорепозитории и специальные инструменты.
    • Что такое фантомные пакеты, откуда берется проблема дублирующихся пакетов и как с этим можно бороться.
    • Как эффективно и безопасно использовать менеджер пакетов в контексте CI/CD.
    • и многое другое.

    Итак, не будем терять времени!


    …Мы подобны карликам, усевшимся на плечах великанов; мы видим больше и дальше, чем они, не потому, что обладаем лучшим зрением, и не потому, что выше их, но потому, что они нас подняли и увеличили наш рост собственным величием…

    — Бернар Шартрский


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


    Несмотря на огромный прогресс в фундаментальных веб-технологиях, создание даже мало-мальски сложного приложения потребовало бы достаточно высокой квалификации разработчиков и написания огромного количества базового кода с нуля. Но, слава богу, существуют такие решения, как Angular, React, Express, Lodash, Webpack и многие другие, и нам не нужно каждый раз изобретать колесо.


    Немного истории JavaScript


    Мы можем вспомнить «дикие времена», когда код популярных библиотек (таких как jQuery) и плагины к ним разработчику нужно было напрямую скачивать с официального сайта, а затем распаковывать из архивов в директорию проекта. Разумеется, обновление таких библиотек происходило точно так же: вручную. Сборка такого приложения тоже требовала ручного и достаточно творческого, уникального подхода. Про оптимизацию сборки я даже не стану упоминать.


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


    Node.js приходит на помощь


    Современную веб-разработку уже совершенно невозможно представить без Node.js, технологии, которая изначально разрабатывалась для сервера, но впоследствии стала платформой для любых JavaScript-проектов, как front-end приложений, так и всевозможных инструментов, а с популяризацией SSR граница между средами начала окончательно стираться. Таким образом, менеджер пакетов для Node.js (Node Package Manager, или npm), постепенно стал универсальным менеджером пакетов для всех библиотек и инструментов, написанных на JavaScript.


    Также стоит заметить, что до появления ESM стандарт языка JavaScript в принципе не имел концепции модулей и зависимостей: весь код просто загружался через тег script в браузере и выполнялся в одной большой глобальной области видимости. По этой причине разработчики Node внедрили собственный формат модулей. Он был основан на неофициальном стандарте CommonJS (от слов «распространенный/универсальный JavaScript», или CJS), который впоследствии стал де-факто стандартом в индустрии. Сам же алгоритм поиска зависимостей Node (Node.js module resolution algorithm) стал стандартом представления пакетов в файловой системе проекта, который сейчас используется всеми загрузчиками и инструментами сборки.


    Пакет всему голова


    Как было упомянуто выше, Node.js ввел свой формат представления и поиска зависимостей, который сейчас де-факто является общим стандартом для JavaScript-проектов.


    В основе системы лежит концепция пакета: npm-пакет — это минимальная единица распространения кода на JavaScript. Любая библиотека или фреймворк представляются как один или несколько связанных пакетов. Ваше приложение также является пакетом.


    Перед публикацией пакет, как правило, компилируется, а потом загружается в хранилище, которое называется npm registry. В основном используется централизованный официальный npm registry, который находится в публичном доступе на домене registry.npmjs.org. Однако использование частных закрытых npm registry также распространено (мы в ДомКлике активно используем такой для внутренних пакетов). Другие разработчики могут установить опубликованный пакет как зависимость в свой проект, загрузив его из registry. Это происходит автоматически при помощи менеджера пакетов (вроде npm).


    Найти нужный пакет или изучить их список можно на официальном сайте npm.


    Опубликованный пакет физически представляет собой версионированный архив с кодом, который готов к выполнению или внедрению в другой проект. Этот код может представлять собой библиотеку, которую вы можете включить в свое приложение (например, lodash), либо полноценную программу, которую можно напрямую вызвать у вас на компьютере (например, webpack).


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



    На изображении показан результат команды npm ls — дерево зависимостей проекта, в котором установлено всего два пакета: HTTP-сервер Express (с множеством дочерних зависимостей) и библиотека Lodash (без зависимостей). Обратите внимание, что одна и та же зависимость debug встречается 4 раза в разных частях дерева. Надпись deduped означает, что npm обнаружил дублирующиеся зависимости и установил пакет только один раз (подробнее о дубликации мы поговорим в следующих постах).


    Поскольку экосистема Node проповедует философию Unix, когда один пакет должен решать какую-то свою узкую задачу и делать это хорошо, то количество зависимостей в среднестатистическом проекте может быть очень велико и легко переваливает за несколько сотен. Это приводит к тому, что дерево зависимостей сильно разрастается как в ширину, так и в глубину. Наверное, только ленивый не шутил про размеры директории node_modules, в которой устанавливаются все эти зависимости. Нередко, люди со стороны критикуют JavaScript за это:



    Манифест пакета


    Что же является пакетом и как мы можем его создать? По сути, пакетом может являться любая директория, содержащая специальный файл-манифест: package.json. Он может содержать множество полезной информации о пакете, такой как:


    • название, версия и описание,
    • тип лицензии,
    • URL домашней страницы, URL git-репозитория, URL страницы для баг-репортинга,
    • имена и контакты авторов и мейнтейнеров,
    • ключевые слова, чтобы пакет можно было найти,
    • файловые пути к коду библиотеки или выполняемым файлам,
    • список зависимостей,
    • вспомогательные локальные команды (scripts) для работы над пакетом,
    • и др. (см. полный список).

    Пример манифеста package.json.


    Описание зависимостей пакета


    Манифест пакета содержит ряд опциональных полей, которые позволяют задавать список зависимостей:


    • dependencies,
    • devDependencies,
    • peerDependencies,
    • optionalDependencies.

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


    Пример:


    {
      …
      "dependencies": {
        "lodash": "^4.17.15",
        "chalk": "~2.3",
        "debug": ">2 <4",
      },
      …
    }

    Давайте рассмотрим назначение каждого поля в отдельности.


    dependencies


    Поле dependencies определяет список зависимостей, без которых код вашего проекта не сможет корректно работать. Это главный и основной список зависимостей для библиотек и программ на Node.js. Если в вашем коде есть импорты каких-то сторонних зависимостей, например import { get } from 'lodash', то эта зависимость должна быть прописана в поле dependencies. Ее отсутствие приведет к тому, что при выполнении ваша программа упадет с ошибкой, потому что нужная зависимость не будет найдена.


    devDependencies


    Поле devDependencies позволяет задать список зависимостей, которые необходимы только на этапе разработки пакета, но не для выполнения его кода в рантайме. Сюда можно отнести всевозможные инструменты разработки и сборки, такие как typescript, webpack, eslint и прочие. Если ваш пакет будет устанавливаться как зависимость для другого пакета, то зависимости из этого списка установлены не будут.


    peerDependencies


    Поле peerDependencies играет особую роль при разработке вспомогательных пакетов для некоторых инструментов и фреймворков. К примеру, если вы пишете плагин для Webpack, то в поле peerDependencies вы можете указать версию webpack, которую ваш плагин поддерживает.


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


    При установке вашего пакета зависимости из поля peerDependencies автоматически не устанавливаются, разработчик должен сам установить их в свой родительский проект, прописав в манифесте своего пакета. Это также гарантирует, что в проекте будет использоваться только одна версия зависимости без дубликатов. Ведь если в проекте будет установлено несколько разных версий, скажем, Webpack, то это может привести к серьезным конфликтам.


    optionalDependencies


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


    Важно понимать, что ваш код должен корректно реагировать на отсутствие таких зависимостей, например, используя связку try… require… catch.


    Зависимости во front-end проектах


    Выше мы рассмотрели четыре способа задания различных зависимостей для вашего проекта. Однако не стоит забывать, что эта система была изначально придумана для приложений и библиотек на Node.js, которые выполняются напрямую на машине пользователя, а не в особой песочнице, коей является браузер. Таким образом, стандарт никак не учитывает особенности разработки front-end приложений.


    Если кто-то вам скажет, что один способ определения npm-зависимостей для front-end приложений является правильным, а другой нет, то не верьте: «правильного» способа не существует, потому что такой вариант использования просто не учтен в node и npm.


    Однако для удобства работы над front-end приложениями я могу предложить вам проверенный временем и опытом формат определения зависимостей. Но для начала давайте попробуем разобраться, чем отличается front-end проект от проекта на Node.js.


    Обычно конечная цель Node.js-разработчика заключается в том, чтобы опубликовать созданный им пакет в npm registry, а уже оттуда этот пакет скачивается, устанавливается и используется пользователем как готовое ПО или как библиотека в составе более сложного продукта. При этом зависимости из поля dependencies в манифесте пакета устанавливаются в проект конечного пользователя.


    В случае же с front-end приложением оно не публикуется в npm registry, а собирается как самостоятельный артефакт (статика) и выгружается, например, на CDN. По-сути, npm во front-end проектах используется только для того, чтобы устанавливать сторонние зависимости. По этой причине в манифесте подобного проекта рекомендуется использовать опцию private: true, которая гарантирует, что файлы приложения не будут случайно отправлены в публичный npm-registry. Название же и версия самого пакета приложения не имеют смысла, т. к. «снаружи» нигде не используются.


    Эта особенность front-end приложений позволяет нам использовать поле dependencies не совсем по его прямому назначению, а как категорию для того, чтобы разделить список зависимостей на две части: в поле dependencies вы пишете список прямых зависимостей, которые используются в коде приложения, например, lodash, react, date-fns и т. д., а в поле devDependencies — зависимости, которые нужны для разработки и сборки приложения: webpack, eslint, декларации из пакетов @types и т. д.


    Постойте, но это ведь ничем не отличается от того, как прописываются зависимости для пакетов на Node.js! Да, однако некоторые особо педантичные разработчики могут заявить, что раз сторонние зависимости объединяются в бандл приложения при сборке и фактически не импортируются в рантайме, то они должны находиться в поле devDependencies. Теперь вы можете аргументировано защитить более практичный подход.


    Семантическое версионирование


    В экосистеме npm принят стандарт версионирования пакетов semver (от слов Semantic Versioning (семантическое версионирование)).


    Суть стандарта заключается в том, что версия пакета состоит из трех чисел: основной (major) версии, младшей (minor) версии и patch-версии:



    Например: 3.12.1.


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


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


    Увеличение minor-версии означает, что в пакет была добавлена новая функциональность, но совместимость сохранилась.


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


    Нестабильные версии


    Версии пакетов до 1.0.0, например, 0.0.3 или 0.1.2, в системе semver также имеют определенный смысл: такие версии считаются нестабильными и повышение первого ненулевого числа версии должно расцениваться как изменение с потенциальным нарушением обратной совместимости.


    Продолжение следует


    Мы рассмотрели самые основы управления зависимостями в JavaScript: узнали, что такое пакет, как он определяется и как задаются зависимости. В следующем посте мы подробнее рассматриваем, как на практике работает semver, как правильно прописывать диапазоны версий и обновлять зависимости.


    Читать продолжение →

    ДомКлик
    Место силы

    Comments 19

      +1

      Есть мнение, что зависимости которые требуются для сборки пакета необходимо ставить в секцию dependencies. Тогда при npm ci --production мы будем ставить зависимости, необходимые для того, чтобы проект заработал. Например на CI это просто сэкономит время сборки вашего приложения, особенно если у вас в dev-dependencies находятся какие-нибудь жирные библиотеки, качающие бинарники.

        +3
        Такой подход неизбежно приведет к более сложной категоризации зависимостей и нарушит общепринятую семантику. Соответственно другим разработчикам будет сложно понять что происходит в проекте и по какому принципу добавляются зависимости. В итоге dev-зависимости перемешаются с зависимостями исходного кода и управлять ими будет гораздо сложнее. Кроме того, довольно сложно отделить зависимости, которые нужны только для сборки и зависимости, которые нужны для процесса разработки. Скорее всего выиграть на этом много не получится и игра просто не будет стоить свеч. Я бы не рекомендовал такой подход.

        Оптимизировать сборку проекта в контексте CI/CD имеет смысл при помощи других инструментов, например, кэширования. Я постараюсь раскрыть этот вопрос в следующих постах.
          +2

          Посыл в том, что ваш код не будет работать без тех самых сборщиков, которые установлены в devDependencies. Когда стоит придерживаться той семантики, которую вы описали? Когда вы разрабатываете библиотеку или backend под nodejs.
          В той же документации на npmjs.org назначение devDependencies поля однозначно:


          Packages that are only needed for local development and testing.

          webpack => чтобы приложение работало
          webpack-dev-server => для локальной разработки
          Не холивара ради, а ради того, что есть и другие способы организации зависимостей.
          Есть статья, которая возможно даст пищу для размышлений :) https://incubator.flaks.dev/devdependencies-mistake

            +1
            Одна из главных мыслей, которую я хотел донести в статье, заключается в том, что данная система была разработана для Node.js проектов и идеально на front-end проекты она не может лечь. Не существует единственно верного канонического решения. Не стоит пытаться читать документацию к npm и интерпретировать ее буквально, т. к. впринципе этот инструмент не создавался для фронтенда. Здесь скорее вопрос в том, как адаптировать инструмент для эффективной работы конкретно в твоем проекте. Схема, предложенная мной, кажется наиболее понятной и удобной. А мой опыт говорит о том, что в разработке простота, читабельность и понятность кода или подходов намного важнее, чем оптимизация, которая в данном случае, кажется довольно сомнительной.
              +1
              Посыл в том, что ваш код не будет работать без тех самых сборщиков, которые установлены в devDependencies.

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


              Даже, если мы говорим о программе на Node.js, которая должна собираться перед своей работой (например через tsc), то распределение зависимостей может быть точно таким же. Разница лишь в том, что перед публикацией пакета в registry он будет автоматически скомпилирован (скрипт prepublishOnly) и уже готовый для выполнения код окажется в архиве пакета. Таким образом, при установке в проект всё будет работать, несмотря на отсутствие dev-зависимостей, которые нужны для сборки.

                +1
                А он и не должен, т. к. пакет фронтенд приложения не является полноценным npm-пакетом. npm используется исключительно для того, чтобы скачать зависимости и сложить их в node_modules.

                Зачем вы пишите код, который не работает без devDependencies ?) Мой комментарий касается только процесса написания конечного web клиента (не npm пакета) и уменьшения кол-ва скачиваемых зависимостей, необходимых для того чтобы ваше приложение собралось и запустилось в браузере

                  +1
                  Зачем вы пишите код, который не работает без devDependencies ?)

                  Не совсем уловил суть вопроса.


                  Мой комментарий касается только процесса написания конечного web клиента (не npm пакета) и уменьшения кол-ва скачиваемых зависимостей, необходимых для того чтобы ваше приложение собралось и запустилось в браузере

                  Это я понял и все мои ответы в данной ветке как раз соответствуют данному контексту.

              +1

              Если только для фронтенда, то действительно, разделение на прод-зависимости и дев-зависимости не несет значимого смысла, т.к. в ci после сборки не нужны node_modules, и в финальную папку можно копировать только папку build.
              В изоморфной же схеме либо при сборке для ноды, когда зависимости не включаются в билд, а подтягиваются в рантайме (например, с помощью опции externals в Webpack), разделение на разного рода зависимости становится актуальным. После сборки в финальном образе с файлами ci в идеале должен выполнить npm i --production, чтобы очистить node_modules от лишнего и уменьшить размер. Эту оговорочку надо бы включить в статью

                +1

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

              +1
              Есть мнение, что зависимости которые требуются для сборки пакета необходимо ставить в секцию dependencies

              Потом вы решите использовать Docker. Поиграетесь и придете к multistage сборке. И для оптимизации придется опять переносить зависимости сборки в devDependencies. Хотя не спорю, для отдельных сценариев этот способ может иметь право на жизнь.

              0
              Существует ли возможность забандлить express js приложение в один толстый js файл со всеми зависимостями? И почему так никто не делает?
                +2
                В среде Node.js (и других похожих технологиях) впринципе не принято бандлить зависимости. Идея бандлинга пришла из front-end проектов, потому что передавать огромное количество маленьких файлов по старому протоколу HTTP было неэффективно (новые версии протокола, начиная с версии 2 лишены этого недостатка). Кроме того, если ты забандлишь какую-то зависимость внутрь своего пакета, то в рамках родительского приложения она уже не сможет обновляться и переиспользоваться. Соответственно, чтобы обновить зависимости Express (как прямые, так и транзитивные) нужно будет постоянно выпускать новые версии Express, хотя в самом пакете ничего может не меняться при этом.

                К примеру, Express, как и многие другие библиотеки, использует пакет «debug». Если каждая библиотека будет бандлить «debug» внутри себя, то в конечном приложении код «debug» разных версий будет встречаться десятки раз, что будет раздувать количество кода. А так npm при установке зависимостей автоматически выполняет дедубликацию и пакет «debug» устанавливается только один раз (об этом упомянуто в моём посте).

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

                В будущих постах я подробнее расскажу о проблеме дублирующихся пакетов и почему это важно.
                  0
                  Меня интересует бандлинг своего приложения, а не общедоступного пакета. Проблема безопасности решается пересборкой с обновленным пакетом, ведь пакет все равно надо будет вручную обновить. Дедубликация выполняется бандлером (вебпаком например). Деплоить один js скрипт намного проще и безопаснее чем выполнять npm install на прод сервере.
                    +1

                    Прошу прощения. Видимо я не совсем уловил суть вопроса. Однако, часть моего ответа все еще верна — бандлинг изобретение для фронтенда, чтобы повысить производительность при работе со старыми версиями протокола HTTP, т. к. передать один файл по сети проще, чем, скажем, тысячу.


                    При использовании Node.js такой проблемы впринципе не стоит, по этой причине ее и нет смысла решать, это только усложнило бы сборку приложения. Кроме того, это лишило бы приложение ряда интересных возможностей, например, загрузки модулей динамически: представьте, что у вас в приложении есть директория в которую вы можете класть JS-файлы, а приложение само их находит, загружает и выполняет по мере необходимости. Забандлить такую реализацию было бы очень проблематично.


                    Ну и делать npm install на продакшене явно не самая лучшая идея. Если вкратце, то лучше собирать артефакт приложения в среде CI/CD (на сборочной машине), а потом уже разворачивать готовый к выполнению код в продуктовой среде передавая нужные данные через окружение. Я постараюсь подробнее об этом написать в будущих постах.

                      +2

                      Почему не делают? Делают. Погуглите “use webpack bundling for server side code”. Только вот смысла в этом не очень много: вебпак довольно-таки тормозной, а многие современные проекты пишут на TypeScript, который очень хорошо создает директорию типа dist из src и кладет в нее те же файлы, что были в src, но js, d.ts и map. И работает он гораздо быстрее костыльного ts-loader’а или awesome-ts-loader-а (причем все быстрее и быстрее, особенно с -b), плюс все фичи из tsconfig.json поддерживает. Зачем там еще webpack какой-то...

                        +1

                        К слову, именно таким образом я лично и собираю проекты на Node.js, при помощи чистого tsc и ни какой магии.

                          0

                          Да… Магия возникает, когда хочется задействовать разные нетрадиционные вещи типа ttypescript или typescript-is. Или когда хочется билдить при помощи tsc -b —watch не только сервер, но и клиент (с реактом и tsx!), я потом на результат (js+map) натравливать уже webpack —watch (ибо так работает гораздо быстрее, чем ts-loader, когда монорепа с многими подпроектами). Или когда хочется jest гонять не по ts-файлам, а по все тем же сгенерированным js (ибо так тесты в 2-3 раза быстрее запускаются, чем чере ts-jest, но приходится доставать бубен для поддержки снапшотов в правильном месте). Или когда хочется нормальный watch или eslint сделать в монорепе (ужас-ужас). А уж про es6-модули для TS и node и говорить не приходится. Все это в 2020 году находится в страшно сыром и разрозненном состоянии, к сожалению, но процесс медленно идет.

                        –3
                        лучше собирать артефакт приложения в среде CI/CD (на сборочной машине), а потом уже разворачивать готовый к выполнению код в продуктовой среде

                        Такой подход превращает «отладку на продакшене» в кромешный ад. Например, если у меня есть в продакшене невидимая для юзеров веб-морда, либо какой-то фоновый сервис, в случае аварийной ситуации (или для эксперимента) я смогу всегда там файлик руками подправить, перебилдить-перезапустить и посмотреть, что будет. А если мне артефакты собирает CI, то такой фокус уже ее пройдет, да и депллй начинает длиться долговато (возникает зависимость от CI, аварийные фиксы не прочеррипикать также быстро).


                        В общем, много удобств в том, чтобы собирать там, где ранаешь. Например, в WhatsApp весь сервер так собирался (там erlang), и это суперудобно в аварийных ситуациях или для экспериментов.

                  0
                  (удалено)

                  Only users with full accounts can post comments. Log in, please.