Какой сборщик нужен: его выбирает платформа
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Три статьи главы разбирали, как сборка устроена изнутри: свой сборщик, компилятор файла, Turbopack внутри Next. Остался вопрос, с которого обычно начинают: какой сборщик взять. Ответ короче, чем кажется: почти всегда его выбирает платформа, и сегодня это будет видно по скриптам трёх проектов. Самому решать приходится, когда платформы нет или она из прошлого: страница без сборки, старый проект на webpack, скрипт, который собирает один файл. А в конце — как самому проверять таблицу с версиями, которая стареет быстрее учебника.
«Какой сборщик взять?» — вопрос почти всегда задают не в том порядке. Сначала выбирают платформу: фреймворк, мобильное приложение, сервер или страницу вовсе без фреймворка. Нужен ли фреймворк, решает статья про фреймворки, а здесь — какой сборщик приходит вместе с ответом. Его называет скрипт в package.json, и этот скрипт положил туда шаблон проекта.
Ниже — скрипты трёх проектов учебника: приложения Next из статьи про Turbopack, шаблона Vite оттуда же и мобильного приложения автора. Прогноз: встретится ли в них слово webpack, Turbopack или Metro?
Ни разу. Сборщик назван командой платформы: за next dev и next build стоит Turbopack, за vite — Vite с Rolldown внутри, за expo start — Metro, который собирает бандл для телефона. Компилятор файла мобильное приложение тоже не выбирало: в babel.config.js один пресет, babel-preset-expo, и нужные плагины он подключает сам. Выбрали шаблон — выбрали и сборщик.
Заметьте и то, чего сборщик не делает ни в одном из трёх проектов, — проверку типов. У Vite перед сборкой стоит tsc -b, у мобильного приложения — отдельный скрипт typecheck, а next build проверяет типы своим шагом Running TypeScript. При любом сборщике типы проверяет не он.
Те же ответы одной таблицей. Версии в ней — на 15.09.2026, и у вас они могут быть уже другими: как это проверить, покажет последний раздел.
| Платформа | Сборщик | Компилятор файла | Где в учебнике |
|---|---|---|---|
| Next.js 16.3.5 | Turbopack; webpack — флагом --webpack | SWC | вся часть IV, устройство — эта глава |
| React без фреймворка и React Router в режиме фреймворка: Vite 8.3.0 | Rolldown внутри Vite | Oxc | сравнение с Next — в статье про Turbopack |
| Expo SDK 57 и React Native | Metro | Babel 7 с babel-preset-expo | часть V |
| Nest, сервер части VI | по умолчанию бандла нет: nest build переводит файлы | tsc, с флагом -b swc — SWC | часть VI |
| скрипт сборки, библиотека одним файлом | esbuild 0.28.2 | esbuild | раздел «Скрипт, библиотека, сервер» |
| старый проект на webpack | webpack 5.111.0; переезд — Rspack или Rsbuild | тот, что вписан лоадером, например babel-loader | раздел «Старый проект на webpack» |
| страница без npm-пакетов, TypeScript и JSX | не нужен | не нужен | раздел «Когда сборка не нужна» |
На том же Vite стоят SvelteKit и проекты, которые создаёт create-vue: выбрав их, вы выбрали и сборщик. Где какой компилятор файла и когда в проекте появляется Babel, подробнее — в таблице статьи про компилятор файла. Самому выбирать приходится в трёх нижних строках, где платформы нет или она из прошлого, — им и отданы следующие разделы.
Нижняя строка таблицы — самая короткая. Страница из своих модулей работает вовсе без сборки: точку входа подключают с type="module", остальные файлы браузер подтянет сам. Голому имени пакета адрес даёт карта импортов — тег <script type="importmap">. В статье про npm карта дала адрес имени dayjs, а Vite лечил ею каскад хешей в статье про свой сборщик. Карта ничего не склеивает и хеш в имя не ставит: она только говорит браузеру, где лежит файл с таким именем.
Сборка появляется вместе с первым пунктом из списка ниже. Все пункты в главе и раньше уже разобраны, поэтому здесь — только где:
| Что появилось в проекте | Почему карты импортов мало | Где разобрано |
|---|---|---|
| JSX или TypeScript | движок не разберёт ни разметку, ни аннотации типов: нужен компилятор файла | статья про компилятор файла |
| пакет на CommonJS | в браузере нет require и module, а на CommonJS написан и сам react | статья про свой сборщик, раздел про реестр |
| пакет из сотен файлов | каждый модуль — отдельный запрос | статья про свой сборщик, раздел про граф |
| долгий кеш после выката | без хеша в имени посетитель рискует получить вчерашний файл | статья про свой сборщик, раздел про хеш |
| вес бандла | минификация и tree shaking — работа сборки | статья про свой сборщик и статичность импортов |
| сервер и клиент в одном проекте | нужен общий граф, разрезанный по "use client" | статья про Turbopack |
Третья задача прикладывает этот список к странице из четырёх файлов: какие из них браузер не выполнит как есть.
webpack никуда не делся: пятая версия выходит до сих пор, даты последних выпусков — в листинге последнего раздела. Но для нового проекта его уже не советуют: документация React для сборки приложения с нуля называет Vite, Parcel и Rsbuild. У проекта, который уже живёт на webpack, три дороги — переехать на Turbopack, если это Next, переехать на Rspack или остаться.
Начнём с Next: Turbopack в нём — сборщик по умолчанию, и старый webpack.config приходится разбирать. Правило вы видели в статье про Turbopack: лоадеры переезжают в turbopack.rules, алиасы — в resolveAlias, плагины — нет, и кому без плагина не обойтись, тот собирает с флагом --webpack. migrate раскладывает типичный конфиг по этому правилу с одним уточнением: у DefinePlugin замена в next.config есть. Прогноз: какие строки получат «не переедет»?
Две: BundleAnalyzerPlugin и CopyWebpackPlugin. «Не переедет» говорит о плагине, а не о задаче, которую он решал: у задачи бывает замена другим механизмом. Для разбора бандла у Next есть экспериментальная команда next experimental-analyze — по её справке, только для Turbopack. Замена DefinePlugin — настройка compiler.define, и на ней переезд легко сломать: значение там понимается иначе. Вот одни и те же пять значений в compiler.define у Next и в DefinePlugin у webpack:
Одни и те же значения — два разных файла. У DefinePlugin значение — код: строка "/api" встала в бандл голыми буквами /api, и файл не разбирается, хотя сборка прошла без единой ошибки. Поэтому строки в webpack пишут через JSON.stringify("/api"). А "Date.now()" стало вызовом, который выполнится у посетителя, а не на сборке. У compiler.define значение — данные: в исходниках Next, в next/dist/build/define-env.js, каждое проходит через JSON.stringify. "/api" стало строкой, false и 3 — значениями, и ветку "dev" оба сборщика выбросили: Next свернул условие до "prod", webpack без минификации оставил на её месте 0. А JSON.stringify("/api") из старого конфига принёс кавычки внутрь текста.
Отсюда правило переноса: JSON.stringify снимают, а выражение данными не станет — его или вычисляют в самом next.config, или оставляют в коде. Перенос целиком — первая задача.
Вторая дорога — Rspack: сборщик с API webpack, написанный на Rust, и Rsbuild — готовая сборка над ним, та самая, что названа в документации React. Полной совместимости Rspack не обещает, и что переедет, проверяют по его же руководству по переезду:
Везде стоит слово most — «большинство». Формат конфига тот же, лоадеры, как правило, берутся без изменений, у встроенных плагинов те же имена и параметры: DefinePlugin меняет только пространство имён, rspack.DefinePlugin вместо webpack.DefinePlugin. У copy-webpack-plugin есть встроенная замена, webpack-bundle-analyzer в таблице совместимости отмечен как работающий. Остальные плагины сверяют с той же таблицей по одному. Отсюда и место Rspack: проект держится за конфиг и плагины webpack, но хочет уйти с самого webpack.
Третья дорога — остаться. webpack выпускается, и для проекта, где всё работает, переезд — отдельная работа, а не обязанность. В Next остаться — значит собирать с --webpack.
Бывает, что платформы нет вовсе: нужен один файл для чужой страницы, расширения браузера или своей оснастки. Dev-сервер, горячая замена и манифесты такому файлу ни к чему — хватает одной функции build, и её даёт esbuild. Так собрана оснастка песочницы этого учебника. ts — настоящий компилятор TypeScript из примеров статьи про компилятор файла — приезжает в песочницу файлом, который собирает вот этот вызов:
format: 'iife' оборачивает весь бандл в функцию, которая тут же выполняется, — тот самый приём IIFE. globalName: 'ts' кладёт её результат в глобальную переменную, отсюда первые байты файла: var ts=(()=>{. Песочница подключает этот файл тегом script раньше кода примера, поэтому ts в примерах и доступен без всякого import. Тем же скриптом и тем же esbuild для песочницы собран React — его подстановку режима вы видели в статье про компилятор файла.
Серверу на Node бандл нужен реже: пакеты из node_modules и файлы с диска Node находит сам. Nest из части VI собирает команда nest build: по умолчанию она зовёт tsc, и тот выпускает по файлу на каждый исходник, а с флагом -b swc — SWC. Чем этот выбор оборачивается для Nest, разберёт часть VI.
Всё версионное в этой главе подписано датой: листинги, таблица компиляторов и таблица из первого раздела. Дата там не для порядка. Проверить, что сдвинулось, можно двумя командами npm: dist-tags показывает, что ставится по умолчанию и что готовится следом, time — даты выпусков.
Читается это просто. У Next рядом с latest стоит canary следующей версии, так что строка таблицы про Next поменяет номер. webpack и Rspack выпускаются по нескольку раз в месяц — номер webpack в таблице отстанет скоро. А тег next у babel-preset-expo уже смотрит на следующую ветку пресета, то есть на следующий SDK Expo, и вместе с ним сдвинется строка про Metro и Babel.
Кое-что уже известно как движущееся, и в главе оно помечено экспериментальным: порты React Compiler на Rust в Next и в Vite, режим разработки с бандлом и карта импортов для кусков в Vite. Babel 7, на котором работает Metro, получает только исправления безопасности, и срок у них назначен — он в таблице статьи про компилятор файла. А TypeScript уже сменился: npm install -D typescript ставит нативный компилятор седьмой версии.
Команда tsc на месте, только за ней теперь исполняемый файл под вашу платформу из отдельного пакета. А прежнего JS API в главном экспорте пакета нет: require("typescript") отдаёт только номер версии, и transpileModule там не найти. Новый API лежит в экспортах typescript/unstable/* и сам ничего не компилирует: он запускает тот же исполняемый файл отдельной программой и обменивается с ним сообщениями. Запускать программы Node умеет встроенным модулем node:child_process — его имя и нашёл grep в файлах API. Странице в браузере запускать программы на компьютере нельзя, поэтому песочница учебника, которой компилятор нужен прямо на странице, осталась на шестой версии.
Номера из этой главы устареют, а то, что вы собрали руками, — нет. Граф импортов, обёртка модуля и реестр с кешем, кусок по import() и хеш в имени, дерево и посетитель компилятора, ключ работы из входа — из этих деталей в разных сочетаниях собраны все сборщики главы, и от смены названий они не устареют. Появится новый — задайте ему те же вопросы: как он строит граф, где держит реестр, как называет куски, кто переводит файл и из чего собран ключ кеша.
- Сборщик почти всегда приходит с платформой: за
nextстоит Turbopack, за шаблоном Vite — Vite с Rolldown внутри, за Expo — Metro с Babel. Его называет скрипт вpackage.json, а выбирали шаблон. - Страница из своих модулей обходится
type="module"и картой импортов. Сборка появляется с первым из списка: JSX или TypeScript, пакет на CommonJS или из сотен файлов, хеш для долгого кеша, минификация и tree shaking, общий граф сервера и клиента. - «Не переедет» говорит о плагине, а не о его задаче: у
DefinePluginв Next есть замена — настройкаcompiler.define, а бандл разбирает экспериментальная командаnext experimental-analyze. - У
DefinePluginзначение — код, уcompiler.define— данные, которые Next сам проводит черезJSON.stringify. При переносе обёртку снимают, а выражение данными не станет. - Rspack — API webpack на Rust: формат конфига, большинство лоадеров и встроенных плагинов переезжают, остальные плагины сверяют с таблицей совместимости. Скрипт или файл для чужой страницы собирают esbuild одной функцией
build. - Всё версионное в главе подписано датой и проверяется
npm view. Механизмы — граф, реестр с кешем, кусок, хеш, дерево и посетитель, ключ из входа — переживут смену инструментов.
1/4Проект создан шаблоном create-vite, скрипты dev и build зовут vite. Кто выбрал этому проекту сборщик?
2/4Страница из трёх своих модулей подключена через type="module" и работает без сборщика. Что первым потребует сборки?
3/4Из DefinePlugin в compiler.define как есть перенесли API: JSON.stringify("/api"). Что окажется в бандле на месте API?
4/4Проект на webpack переезжает на Rspack. Что сверять в первую очередь?
Перенос DefinePlugin
В старом webpack.config пять констант DefinePlugin. Перенос в compiler.define как есть повторяет ошибку из листинга раздела — строки приезжают с кавычками внутри, — а записанные кодом "false" и "3" становятся строками. Перепишите toCompilerDefine так, чтобы код стал данными: строка — строкой, false — логическим значением, 3 — числом. Выражение, которое данными не записать, не переносите, а напечатайте о нём строку. Литерал из текста кода разбирает JSON.parse.
JSON.parse разбирает ровно литералы: строку в двойных кавычках, число, true и false. На выражении он бросает исключение — по нему и видно, что значение данными не записать.
Вывод:
Без правки хуже всех пришлось бы __DEV__: строка "false" в условии истинна, и отладочная ветка осталась бы в выпускной сборке. А Date.now() в DefinePlugin и раньше выполнялся у посетителя при каждом обращении, так что времени сборки не хранил. Если нужно именно оно, его вычисляют в самом next.config: __BUILT_AT__: Date.now() — число, которое конфиг получит при запуске сборки.
Опции лоадеров
В правилах появились опции, а в resolve — список расширений. migrate из раздела их не видит: запустите и найдите строку, которая обещает переезд зря. Допишите разбор: расширения уходят в turbopack.resolveExtensions, а лоадер, у которого среди опций есть функция, получает «не переедет: функция в опциях». Опции лоадера Turbopack принимает только данными — примитивами, объектами и массивами.
Вывод:
Зря обещала строка ./loaders/markdown.js → turbopack.rules: функцию transform в turbopack.rules не передать, документация Next запрещает там даже модуль плагина через require(). Своему лоадеру это не приговор: функцию он может импортировать сам, а в опциях оставить имя или флаг. Проверка смотрит только верхний уровень опций — функция во вложенном объекте её обманет. И тонкость с расширениями из той же документации: resolveExtensions заменяет список по умолчанию, а не дополняет, поэтому обычные расширения в нём перечисляют явно.
Где кончается страница без сборки
Страница живёт без сборщика: карта импортов, свои модули и один пакет в папке vendor. Допишите blockers — пусть найдёт, что из этого браузер не выполнит как есть. Причины трёх видов: расширение .ts или .tsx, CommonJS в тексте файла и голое имя, которого нет в карте. Регулярок хватит — с теми же оговорками, что в первой статье главы.
Вывод:
chart-lite в карте есть, но карта дала имени только адрес. Файл по нему — CommonJS, ни одного export в нём нет: импорт { draw } браузер отклонит с SyntaxError, и из main.js не выполнится ни одна строка, даже стоящая выше импорта. Загруженный через import(), тот же файл упадёт с ReferenceError — module в браузере не определён. dayjs карта не знает вовсе, и такой импорт кончается TypeError. Имена ошибок — из Chrome, в котором это проверялось. Двух причин из списка раздела так не найти: сотни файлов в пакете и хеш для долгого кеша — свойства не текста, а сети и выката.