Хабрахабр

Node.js без node_modules

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

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

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

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

Любой вызов require() маппится на файловую систему. Изначально модульная система NodeJS была полностью основана на файловой системе. Таким образом, каждый проект получал свой отдельный набор зависимостей, нерационально расходуя дисковое пространство. Для организации 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": .

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

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

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

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

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

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

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

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

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

Пробуем NPM tink

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

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

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

Заключение

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

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

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

Ссылки

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть