Pull to refresh

Node.js без node_modules

Reading time5 min
Views28K

На прошлой неделе разработчики Yarn (пакетного менеджера для Javascript) анонсировали новую фичу – Plug'n'Play установку. Эта возможность позволяет запускать Node.js проекты без использования папки node_modules, в которую обычно устанавливаются зависимости проекта перед запуском. Описание фичи декларирует, что node_modules больше не понадобится – модули будут загружаться из общего кеша пакетного менеджера.


Одновременно с ними разработчики NPM также анонсировали свое аналогичное решение проблемы.


Давайте посмотрим на эти решения повнимательнее и попробуем протестировать их в реальных проектах.


История проблемы


Изначально модульная система NodeJS была полностью основана на файловой системе. Любой вызов require() маппится на файловую систему. Для организации third-party модулей была придумана папка node_modules, в которую должны скачиваться и устанавливаться переиспользуемые модули и библиотеки. Таким образом, каждый проект получал свой отдельный набор зависимостей, нерационально расходуя дисковое пространство.


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


Упрощенно, установка модулей состоит из следующих шагов:


  1. Вычисляется конкретная версия модуля из допустимого интервала
  2. Все модули необходимых версий выкачиваются из репозитория и сохраняются в локальный кеш
  3. Модули из локального кеша копируются в папку node_modules проекта

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


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


Использование симлинков


Вместо реального копирования модулей, можно добавить симлинк на их местоположение в кеше. Такой подход реализован в PNPM, еще одном альтернативном пакетном менеджере. Подход вполне может работать, но с симлинками возникает множество проблем, связанных с двойственным местоположением файла, поиском смежных модулей и т.п. Кроме того, создание симлинков – это файловые операции, которых хотелось бы избежать в идеальном способе работы.


Пробуем Yarn PNP


Подробнее об этой фиче можно почитать в официальном описании. В этом параграфе содержится его краткий пересказ.


Версия Yarn с поддержкой PNP сейчас находится в feature-branch yarn-pnp.


Склонируем репозиторий локально с нужной веткой


git clone git@github.com:yarnpkg/yarn.git --branch yarn-pnp

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


После окончания сборки, добавляем себе алиас на кастомную версию yarn и можем начать c ней работать:


alias yarn-local="node $PWD/lib/cli/index.js"

Plug'n'play включается двумя способами: либо через флаг: yarn --pnp, либо дополнительной конфигурацией в package.json: "installConfig": {"pnp": true}.


В качестве примера разработчики Yarn уже подготовили демо-проект. В нем есть Webpack, Babel и другие типичные для современного фронтенда инструменты. Попробуем установить его зависимости разными способами и получаем следующие результаты:


  • Обычная установка yarn: 19s
  • Установка через yarn --pnp: 3s

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


Давайте теперь разберемся как это работает. После pnp-установки в корне проекта создается дополнительный файл .pnp.js который содержит переопределение нативной логики во встроенном в Node.js классе Module. Загружая этот файл в свой код, мы наделяем функцию require() возможностью доставать модули из глобального кеша и не смотреть в node_modules. Все встроенные yarn-команды, вроде yarn start или yarn test по умолчанию предзагружают этот файл, так что никаких изменений в вашем коде не потребуется, если вы уже использовали Yarn до этого.


В дополнение к маппингу модулей, pnp.js выполняет дополнительную валидацию зависимостей. Если вы попытаетесь вызвать require('test'), без задекларированной зависимости в package.json, вы получите следующую ошибку: Error: You cannot require a package ("test") that is not declared in your dependencies. Это улучшение должно повысить надежность и предсказуемость кода.


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


В демо-проекте есть наброски резолверов, для Eslint, Jest, Rollup и Webpack.


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


Также будут проблемы с postintall-скриптами. Поскольку модуль остаётся в кеше, postinstall-скрипты, меняющие его состояние (например, докачивающие дополнительные файлы) могут повредить кеш и сломать остальные проекты, зависящие от него. Разработчики Yarn рекомендуют отключать исполнение скриптов флагом --ignore-scripts. Они уже экспериментировали с включением этого флага по умолчанию для всех проектов внутри Facebook и не обнаружили серьезных проблем. В долгосрочной перспективе отказ от postinstall-скриптов кажется хорошим шагом в виду известных проблем с безопасностью.


Пробуем NPM tink


Команда NPM также анонсировала свое альтернативное решение. Их новый инструмент, tink поставляется отдельным, независимым от NPM, модулем. На вход tink принимает файл package-lock.json, который автоматически генерируется при запуске npm install. На основании lock-файла tink генерирует файл node_modules/.package-map.json, в котором хранится проекция локальных модулей на их реальное местоположение в кеше.


В отличие от Yarn, здесь нет хук-файла, который можно предзагрузить в свой проект, чтобы пропатчить require. Взамен предлагается использовать команду tink вместо node, чтобы получить правильное окружение. Такой подход менее эргономичный, поскольку потребует модификаций в вашем коде, чтобы заставить его работать. Однако в качестве proof-of-concept подойдет.


Я попробовал сравнить скорость установки модулей командами npm ci и tink, но tink оказался даже медленнее, поэтому результаты приводить не буду. Очевидно, что этот проект намного более сырой по сравнению с Yarn и совсем не оптимизирован. Что ж, будем ждать новых релизов.


Заключение


Отказ от директории node_modules – закономерный шаг, учитывая опыт других языков, где такого подхода не было изначально. Это благоприятно скажется на скорости сборки с CI-системах, где есть возможность сохранить кеш пакетов между билдами. Кроме того, если перенести кеш пакетов и файл .pnp.js с одного компьютера на другой, то можно воспроизвести окружение даже не запуская Yarn. Это может быть полезным в контейнерных системах сборки: монтируем директорию с кешем, кладем .pnp.js файл, и можно сразу запускать тесты.


Новый подход выглядит непривычно и ломает некоторые устоявшиеся практики, основанные на том, что все модули всегда в наличии в node_modules. Но .pnp.js файл предлагает API, которое позволит абстрагироваться от реального положения файлов и работать с виртуальным деревом. Кроме того, на крайний случай, есть команда yarn unplug --persist, которая извлечет модуль из кеша и разместит его локально в node_modules.


В любом случае, ещё ничего не финализировано, даже pull-request в Yarn еще не влит, стоит ожидать изменений. Но мне было интересно попробовать альфа-версию фичи в деле и протестировать их на паре своих личных проектов и убедиться, что этот подход действительно работает, делая установку быстрее.


Ссылки


Tags:
Hubs:
+35
Comments57

Articles

Change theme settings