Создание сборщика

Как работают сборщики Nuxt и как написать свой.

Сборщик (builder) — часть Nuxt, которая собирает приложение в бандлы. Nuxt поставляет три официальных сборщика: Vite (по умолчанию), webpack и Rspack. Выбор — через опцию builder или свой сборщик.

Здесь — как сборщик встроен в сборку Nuxt, какой контракт он должен выполнять и как написать свой.

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

Что делает сборщик

Nuxt разделяет что собирать и как это собирать.

Ядро Nuxt (контекст nuxt из Как устроен Nuxt) и ваши модули формируют виртуальное приложение: точки входа, таблицу маршрутов, плагины, реестр компонентов и остальную виртуальную файловую систему под #build. Сборщик берёт это виртуальное приложение и превращает его в реальные JavaScript- и CSS-бандлы; в разработке поднимает dev-сервер, отдаёт их и делает hot reload.

Конкретно сборщик отвечает за:

  • Сборку клиентского бандла (браузер) и при включённом SSR — серверного (точка входа SSR-приложения).
  • Артефакты, которые нужны серверному рантайму для рендера и гидратации: client manifest, карта стилей по компонентам и т.д. (см. Контракт выходных данных сборки).
  • В разработке — dev-сервер и перезагрузку при изменении сборки.

Сам сервер для деплоя собирает Nitro через @nuxt/nitro-server, а не сборщик. Сборщик передаёт выходные данные Nitro по типизированному контракту; Nitro упаковывает их в финальный .output.

Интерфейс сборщика

Сборщик — объект, реализующий интерфейс NuxtBuilder. Обязателен только метод bundle:

import type { Nuxt } from '@nuxt/schema'

export interface NuxtBuilder {
  bundle: (nuxt: Nuxt) => Promise<void>
  /**
   * Optional. When the user opts in via `experimental.watcher: 'builder'`,
   * Nuxt calls this instead of starting its own dev file watcher, letting
   * the builder reuse its own watcher. The builder should register a
   * `nuxt.hook('close', ...)` to clean up.
   */
  setupWatcher?: (nuxt: Nuxt) => Promise<void> | void
}

Nuxt выбирает активный сборщик из опции builder. Можно указать модуль с default-экспортом NuxtBuilder или inline-объект:

nuxt.config.ts
export default defineNuxtConfig({
  // a package that exports `{ bundle }`
  builder: '@nuxt/vite-builder',
})
nuxt.config.ts
import type { NuxtBuilder } from '@nuxt/schema'

const myBuilder: NuxtBuilder = {
  async bundle (nuxt) {
    // ...
  },
}

export default defineNuxtConfig({
  builder: myBuilder,
})

Nuxt вызывает bundle(nuxt) один раз при nuxt build и nuxt dev, после генерации виртуального приложения и хука build:before. Nuxt оборачивает ваш bundle: любая ошибка автоматически вызывает хук build:error.

По имени для специфичного поведения сборщика в других частях Nuxt распознаются только три официальных идентификатора (@nuxt/vite-builder, @nuxt/webpack-builder, @nuxt/rspack-builder). Свой сборщик работает через общий контракт ниже, но считается legacy-путём (не Vite Environment).

Жизненный цикл сборки

При nuxt build или nuxt dev Nuxt:

  1. Создаёт контекст nuxt, запускает модули, заполняет nuxt.options и хуки сборки.
  2. Генерирует виртуальное приложение (шаблоны, таблицу маршрутов, плагины) в виртуальную ФС #build.
  3. Вызывает build:before.
  4. Разрешает сборщик и вызывает builder.bundle(nuxt). Здесь работает ваш сборщик.
  5. Вызывает build:done; в production закрывает экземпляр nuxt.

Реализация bundle обычно ветвится по nuxt.options.dev:

  • В production — доводит клиентскую и (если nuxt.options.ssr) серверную сборку до конца, пишет артефакты в nuxt.options.buildDir и регистрирует их как build outputs.
  • В development — поднимает dev-сервер, запускает watching-сборку, назначает nuxt.server и продолжает работу.

Сборщик почти целиком общается с остальным Nuxt через хуки. Конфигурацию сборки он читает из nuxt.options и при необходимости даёт модулям расширять конфиг bundler'а.

Расширение bundler'а модулями

Модули влияют на бандл через хелперы Nuxt Kit. Сборщик должен их учитывать:

Официальные сборщики также эмитят свои хуки (например, vite:extendConfig, vite:serverCreated, webpack:config), чтобы модули и Nitro участвовали в сборке. Свой сборщик может эмитить свои хуки, но совместимость с серверным рантаймом Nuxt обеспечивает контракт выходных данных ниже.

Контракт выходных данных сборки

Серверный рантайм (@nuxt/nitro-server) не знает, какой сборщик собрал приложение. Он импортирует каждый артефакт через стабильный subpath nuxt/*; активный сборщик заполняет их через nuxt.buildOutputs. Это контракт выходных данных сборки.

Контракт описан интерфейсом NuxtBuildOutputs в @nuxt/schema:

export interface NuxtBuildOutputs {
  /** Module body re-exporting the SSR app entry. */
  serverEntry: () => string | Promise<string>
  /** Path to the emitted per-component SSR styles map, or `undefined` when no inline styles are produced. */
  ssrStyles: string | undefined
  /** Serialized client manifest for `vue-bundle-renderer`. */
  clientManifest: () => string | Promise<string>
  /** Serialized precomputed client dependency data for `vue-bundle-renderer`. */
  clientPrecomputed: () => string | Promise<string>
  /** Module body exporting the hashed entry chunk filename for import maps. */
  entryChunkName: () => string | Promise<string>
  /** Module body exporting the entry module IDs used for inline style extraction. */
  entryIds: () => string | Promise<string>
}

Каждый ключ соответствует subpath nuxt/*, который импортирует серверный рантайм:

Выход сборкиSubpathИмпортируется как
serverEntrynuxt/entryфабрика SSR-приложения для createRenderer
clientManifestnuxt/manifestclient manifest для vue-bundle-renderer
clientPrecomputednuxt/precomputedпредвычисленные данные зависимостей
ssrStylesnuxt/stylesкарта inline-стилей по компонентам
entryChunkNamenuxt/entry-chunkимя entry-чанка с хешем (import map)
entryIdsnuxt/entry-idsID entry-модулей для извлечения стилей

У каждого subpath nuxt/* в пакете nuxt есть заглушка по умолчанию, поэтому серверный рантайм типизируется и собирается ещё до запуска сборщика. Сборщик переопределяет заглушку, задавая соответствующий build output; это значение подставляется на этапе сборки вместо заглушки.

Два вида build output

Ключи бывают двух форм:

  • Провайдеры значений (serverEntry, clientManifest, clientPrecomputed, entryChunkName, entryIds) — функции, возвращающие тело модуля строкой. Строка встраивается в серверный бандл дословно, поэтому не должна зависеть от расположения файла на диске. Например, serverEntry возвращает тело с re-export собранной SSR-точки входа по абсолютному specifier:
    setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)
    
  • Путь к эмитированному файлу (ssrStyles) — абсолютный путь (не код) к реальному модулю, который эмитировал сборщик. Импорт nuxt/styles в рантайме разрешается к этому файлу, чтобы bundler деплоя разрешал относительные sibling-импорты карты стилей относительно расположения файла. Если моделировать это строкой кода, контекст каталога теряется и относительные импорты ломаются, поэтому здесь — путь:
    setBuildOutput('ssrStyles', resolve(serverOutDir, 'styles.mjs'))
    

    Оставьте ssrStyles как undefined (значение по умолчанию), если сборка не даёт inline-стилей; рантайм использует пустую карту стилей.

Установка build outputs

Используйте хелпер setBuildOutput из @nuxt/kit:

import { setBuildOutput } from '@nuxt/kit'

setBuildOutput('clientManifest', () => 'export default ' + serializedManifest)

setBuildOutput пишет в nuxt.buildOutputs[key]. Внутри плагина bundler'а, где уже есть экземпляр nuxt, можно присвоить nuxt.buildOutputs[key] напрямую; setBuildOutput — удобство для кода, который получает nuxt через useNuxt().

Провайдер может быть асинхронным и читается лениво, когда серверная сборка разрешает соответствующий импорт nuxt/*. Сборщик может зарегистрировать провайдер заранее (например, до завершения клиентской сборки) и вернуть финальное значение, когда данные готовы.

Минимальный пример

Каркас сборщика, выполняющего контракт для production-сборки:

import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { setBuildOutput } from '@nuxt/kit'
import type { NuxtBuilder } from '@nuxt/schema'

export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
  const serverDir = resolve(nuxt.options.buildDir, 'dist/server')

  // ...run your client and server bundles here, writing artifacts to disk...
  const { serializedClientManifest } = await runBundles(nuxt, serverDir)

  if (nuxt.options.ssr) {
    // Point `nuxt/entry` at the built SSR app entry.
    const serverEntryURL = pathToFileURL(resolve(serverDir, 'server.mjs')).href
    setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)

    // Provide the client manifest produced by your client build.
    setBuildOutput('clientManifest', () => `export default ${serializedClientManifest}`)

    // If you emit a per-component styles map alongside its CSS chunks:
    setBuildOutput('ssrStyles', resolve(serverDir, 'styles.mjs'))
  }
}
Редко нужно задавать каждый ключ. Значения по умолчанию разумны (пустой manifest, без inline-стилей, undefined для entry chunk), поэтому указывайте только то, что реально производит сборка. При отключённом SSR serverEntry по умолчанию — no-op app, остальные выходы не используются.

Dev-сервер

В разработке сборщик также отвечает за отдачу приложения и hot reload. Важны две вещи:

  • nuxt.server — работающий dev-сервер. Его использует CLI Nuxt; официальные сборщики экспонируют handler (Node request listener), fetch (web fetch handler) и методы reload / close. Свой dev-сервер или делегирование Nitro (createDevServer из nitro/builder) — на усмотрение сборщика.
  • Перезагрузки — через nuxt.server.reload() по завершении компиляции. Официальные сборщики эмитят хук compiled (например, vite:compiled, webpack:compiled), на который подписывается серверная интеграция.

В dev build outputs обычно указывают на живые in-memory источники, а не на файлы на диске. SSR-точка входа может отдаваться из in-memory выхода bundler'а, client manifest — вычисляться из dev module graph, а не читаться из nuxt.options.buildDir.

Сборщики и Vite Environment API

Контракт выше намеренно не привязан к конкретному сборщику и работает и для legacy-пути (каждый сборщик гоняет свою сборку, Nitro отдельно собирает деплой через Rollup), и для Vite Environment API, где Nitro работает как Vite environment.

Для своего сборщика достаточно общего контракта. Интеграция Vite Environment API (experimental.nitroViteEnvironment) — только для @nuxt/vite-builder; остальные сборщики, включая кастомные, идут по legacy-пути Nitro Rollup.

Подробнее об интерфейсе Nuxt и разделении сборки и рантайма.
Утилиты Nuxt Kit для регистрации плагинов bundler'а и build outputs.