Какой сборщик нужен: его выбирает платформа

сборщики≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Три статьи главы разбирали, как сборка устроена изнутри: свой сборщик, компилятор файла, Turbopack внутри Next. Остался вопрос, с которого обычно начинают: какой сборщик взять. Ответ короче, чем кажется: почти всегда его выбирает платформа, и сегодня это будет видно по скриптам трёх проектов. Самому решать приходится, когда платформы нет или она из прошлого: страница без сборки, старый проект на webpack, скрипт, который собирает один файл. А в конце — как самому проверять таблицу с версиями, которая стареет быстрее учебника.

Разделы темы:·

«Какой сборщик взять?» — вопрос почти всегда задают не в том порядке. Сначала выбирают платформу: фреймворк, мобильное приложение, сервер или страницу вовсе без фреймворка. Нужен ли фреймворк, решает статья про фреймворки, а здесь — какой сборщик приходит вместе с ответом. Его называет скрипт в package.json, и этот скрипт положил туда шаблон проекта.

Ниже — скрипты трёх проектов учебника: приложения Next из статьи про Turbopack, шаблона Vite оттуда же и мобильного приложения автора. Прогноз: встретится ли в них слово webpack, Turbopack или Metro?

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал; next-app — приложение из статьи про Turbopack (next 16.3.5), vite-app — шаблон react-ts из create-vite 9.2.1, mobile — мобильное приложение автора (expo 57.0.16); macOS arm64, 15.09.2026
next-app/package.json: "dev": "next dev",
next-app/package.json: "build": "next build",
vite-app/package.json: "dev": "vite",
vite-app/package.json: "build": "tsc -b && vite build",
mobile/package.json: "dev": "expo start --clear",
mobile/package.json: "typecheck": "tsc --noEmit",
$ grep -v "//" mobile/babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

Ни разу. Сборщик назван командой платформы: за 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.5Turbopack; webpack — флагом --webpackSWCвся часть IV, устройство — эта глава
React без фреймворка и React Router в режиме фреймворка: Vite 8.3.0Rolldown внутри ViteOxcсравнение с Next — в статье про Turbopack
Expo SDK 57 и React NativeMetroBabel 7 с babel-preset-expoчасть V
Nest, сервер части VIпо умолчанию бандла нет: nest build переводит файлыtsc, с флагом -b swc — SWCчасть VI
скрипт сборки, библиотека одним файломesbuild 0.28.2esbuildраздел «Скрипт, библиотека, сервер»
старый проект на webpackwebpack 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 есть. Прогноз: какие строки получат «не переедет»?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Две: BundleAnalyzerPlugin и CopyWebpackPlugin. «Не переедет» говорит о плагине, а не о задаче, которую он решал: у задачи бывает замена другим механизмом. Для разбора бандла у Next есть экспериментальная команда next experimental-analyze — по её справке, только для Turbopack. Замена DefinePlugin — настройка compiler.define, и на ней переезд легко сломать: значение там понимается иначе. Вот одни и те же пять значений в compiler.define у Next и в DefinePlugin у webpack:

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал; next 16.3.5 (Turbopack) — копия приложения из статьи про Turbopack, NEXT_TELEMETRY_DISABLED=1, лог сборки после второй строки опущен; webpack 5.98.0 — копия внутри next 16.3.5, скрипт build.cjs зовёт её с mode: "production" и optimization.minimize: false; macOS arm64, Node 26.5.0, 15.09.2026
const config = {
compiler: {
define: {
API_URL: "/api",
API_URL_STRINGIFIED: JSON.stringify("/api"),
IS_DEV: false,
RETRIES: 3,
BUILT_AT: "Date.now()",
},
},
};
export default config;
$ grep "{count}" next-define/ui/price.tsx
{count} × {rub(base)} {API_URL} {API_URL_STRINGIFIED} {IS_DEV ? "dev" : "prod"} {RETRIES + 1} {BUILT_AT}
$ (cd next-define && npx next build)
▲ Next.js 16.3.5 (Turbopack)
✓ Running next.config.ts took 56ms
…
$ grep -o 'children:\[[^]]*\]' next-define/.next/static/chunks/*.js | grep /api
next-define/.next/static/chunks/2mi4sr6fc3lzo.js:children:[o," × ",t+" ₽"," ","/api"," ",'"/api"'," ","prod"," ",4," ","Date.now()"]
$ grep -A7 "new webpack.DefinePlugin" wp-define/build.cjs
new webpack.DefinePlugin({
API_URL: "/api",
API_URL_STRINGIFIED: JSON.stringify("/api"),
IS_DEV: false,
RETRIES: 3,
BUILT_AT: "Date.now()",
}),
],
$ cat wp-define/src/app.js
console.log(API_URL, API_URL_STRINGIFIED, IS_DEV ? "dev" : "prod", RETRIES + 1, BUILT_AT);
$ node wp-define/build.cjs
webpack 5.98.0 ошибок: 0
console.log(/api, "/api", false ? 0 : "prod", 3 + 1, Date.now());
$ node wp-define/dist/app.js 2>&1 | grep Error
SyntaxError: Invalid regular expression flags

Одни и те же значения — два разных файла. У 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 не обещает, и что переедет, проверяют по его же руководству по переезду:

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал; документация Rspack из репозитория web-infra-dev/rspack, ветка main (коммит 267a650); 15.09.2026
Rspack supports most webpack configuration options. See [Configure Rspack](/config/) for the complete list of supported options.
Rspack has implemented most of webpack's built-in plugins, with the same names and configuration parameters, allowing for easy replacement.
new rspack.DefinePlugin({ // [!code ++]
Rspack supports most of the webpack community plugins and also offers alternative solutions for some currently unsupported plugins.
Use [rspack.CopyRspackPlugin](/plugins/copy-rspack-plugin) instead of [copy-webpack-plugin](https://github.com/webpack/copy-webpack-plugin):
Rspack is compatible with most webpack loaders, so existing loaders can typically be reused without changes.
$ curl -s $D/components/CommunityCompatibleTable.tsx | grep -A2 "name: 'webpack-bundle-analyzer'"
name: 'webpack-bundle-analyzer',
url: 'https://www.npmjs.com/package/webpack-bundle-analyzer',
status: CompatibleStatus.Compatible,

Везде стоит слово most — «большинство». Формат конфига тот же, лоадеры, как правило, берутся без изменений, у встроенных плагинов те же имена и параметры: DefinePlugin меняет только пространство имён, rspack.DefinePlugin вместо webpack.DefinePlugin. У copy-webpack-plugin есть встроенная замена, webpack-bundle-analyzer в таблице совместимости отмечен как работающий. Остальные плагины сверяют с той же таблицей по одному. Отсюда и место Rspack: проект держится за конфиг и плагины webpack, но хочет уйти с самого webpack.

Третья дорога — остаться. webpack выпускается, и для проекта, где всё работает, переезд — отдельная работа, а не обязанность. В Next остаться — значит собирать с --webpack.

Бывает, что платформы нет вовсе: нужен один файл для чужой страницы, расширения браузера или своей оснастки. Dev-сервер, горячая замена и манифесты такому файлу ни к чему — хватает одной функции build, и её даёт esbuild. Так собрана оснастка песочницы этого учебника. ts — настоящий компилятор TypeScript из примеров статьи про компилятор файла — приезжает в песочницу файлом, который собирает вот этот вызов:

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал, репозиторий учебника: скрипт apps/web/scripts/build-ts-runtime.mjs и собранный им файл; esbuild 0.28.2, typescript 6.0.3; 15.09.2026
100- */
101-await build({
102- entryPoints: [join(ЛИБЫ, 'typescript.js')],
103- outfile: join(КУДА, `typescript-${ВЕРСИЯ}.js`),
104- minify: true,
105- format: 'iife',
106: globalName: 'ts',
107- bundle: true,
108- platform: 'browser',
109- logLevel: 'error',
$ head -c 13 apps/web/public/ts/typescript-6.0.3.js
var ts=(()=>{

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 — даты выпусков.

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал, npm 11.17.0; 15.09.2026, 08:43 UTC
dist-tags.latest = '16.3.5'
dist-tags.canary = '16.4.0-canary.31'
$ npm view webpack time --json | tail -4
"5.110.2": "2026-08-30T14:59:17.943Z",
"5.110.3": "2026-09-01T20:02:13.900Z",
"5.111.0": "2026-09-14T06:18:27.582Z"
}
$ npm view @rspack/core time --json | tail -4
"2.2.2": "2026-09-01T11:44:08.568Z",
"2.2.3": "2026-09-08T08:48:34.734Z",
"2.2.4": "2026-09-14T12:15:17.262Z"
}
$ npm view babel-preset-expo dist-tags.latest dist-tags.next
dist-tags.latest = '57.0.11'
dist-tags.next = '58.0.1'

Читается это просто. У 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 ставит нативный компилятор седьмой версии.

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал, каталог с одним npm install -D typescript (typescript 7.0.2) и tsconfig.json из tsc --init; путь сокращён; macOS arm64, Node 26.5.0, 15.09.2026
ts7@ …/ts7
`-- typescript@7.0.2
$ npx tsc --version
Version 7.0.2
$ file node_modules/@typescript/typescript-darwin-arm64/lib/tsc
node_modules/@typescript/typescript-darwin-arm64/lib/tsc: Mach-O 64-bit executable arm64
$ node -p 'const ts = require("typescript"); [Object.keys(ts), typeof ts.transpileModule]'
[ [ 'version', 'versionMajorMinor' ], 'undefined' ]
$ node -p 'Object.keys(require("typescript/package.json").exports)'
[
'./package.json',
'.',
'./unstable/sync',
'./unstable/async',
'./unstable/fs',
'./unstable/proto',
'./unstable/ast',
'./unstable/ast/is',
'./unstable/ast/factory',
'./unstable/ast/utils',
'./unstable/ast/scanner',
'./unstable/ast/visitor',
'./unstable/ast/clone'
]
$ grep -rl "node:child_process" node_modules/typescript/dist/api
node_modules/typescript/dist/api/syncChannel.js
node_modules/typescript/dist/api/async/client.js

Команда 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.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

JSON.parse разбирает ровно литералы: строку в двойных кавычках, число, true и false. На выражении он бросает исключение — по нему и видно, что значение данными не записать.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Без правки хуже всех пришлось бы __DEV__: строка "false" в условии истинна, и отладочная ветка осталась бы в выпускной сборке. А Date.now() в DefinePlugin и раньше выполнялся у посетителя при каждом обращении, так что времени сборки не хранил. Если нужно именно оно, его вычисляют в самом next.config: __BUILT_AT__: Date.now() — число, которое конфиг получит при запуске сборки.

Опции лоадеров

В правилах появились опции, а в resolve — список расширений. migrate из раздела их не видит: запустите и найдите строку, которая обещает переезд зря. Допишите разбор: расширения уходят в turbopack.resolveExtensions, а лоадер, у которого среди опций есть функция, получает «не переедет: функция в опциях». Опции лоадера Turbopack принимает только данными — примитивами, объектами и массивами.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Зря обещала строка ./loaders/markdown.js → turbopack.rules: функцию transform в turbopack.rules не передать, документация Next запрещает там даже модуль плагина через require(). Своему лоадеру это не приговор: функцию он может импортировать сам, а в опциях оставить имя или флаг. Проверка смотрит только верхний уровень опций — функция во вложенном объекте её обманет. И тонкость с расширениями из той же документации: resolveExtensions заменяет список по умолчанию, а не дополняет, поэтому обычные расширения в нём перечисляют явно.

Где кончается страница без сборки

Страница живёт без сборщика: карта импортов, свои модули и один пакет в папке vendor. Допишите blockers — пусть найдёт, что из этого браузер не выполнит как есть. Причины трёх видов: расширение .ts или .tsx, CommonJS в тексте файла и голое имя, которого нет в карте. Регулярок хватит — с теми же оговорками, что в первой статье главы.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

chart-lite в карте есть, но карта дала имени только адрес. Файл по нему — CommonJS, ни одного export в нём нет: импорт { draw } браузер отклонит с SyntaxError, и из main.js не выполнится ни одна строка, даже стоящая выше импорта. Загруженный через import(), тот же файл упадёт с ReferenceError — module в браузере не определён. dayjs карта не знает вовсе, и такой импорт кончается TypeError. Имена ошибок — из Chrome, в котором это проверялось. Двух причин из списка раздела так не найти: сотни файлов в пакете и хеш для долгого кеша — свойства не текста, а сети и выката.