Плагин CLI

Рекомендуемое решение для webpack: true. Работает автоматически, на каждой сборке, и о нём не нужно помнить как об отдельном шаге.

Тем же механизмом пользуется собственный плагин CLI из @nestjs/swagger, чтобы работать под webpack: true. Именно поэтому @ApiProperty() не нужен у каждого свойства DTO даже в сборке через webpack. Nest CLI одинаково передаёт хук плагина компилятора TypeScript (compilerOptions.plugins) и в сборщик tsc, и в webpack через ts-loader.

Регистрация плагина

Пропишите nestjs-docfy в nest-cli.json:

nest-cli.json
{
  "compilerOptions": {
    "webpack": true,
    "plugins": ["nestjs-docfy"]
  }
}
main.ts
import { applyDocfyMetadata } from 'nestjs-docfy';

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, applyDocfyMetadata(document));
OptionTypeDefaultDescription
metadataPathstringdocfy-metadata.json next to the running entry fileWhere to read the plugin-generated metadata from.
strictbooleanfalseThrow instead of warning when the metadata file is missing or invalid.

Автоматическое определение

Забыть зарегистрировать плагин легко, и обнаруживается это обычно уже после сборки без документации. Поэтому каждый запуск generate заглядывает в nest-cli.json и предупреждает, когда webpack: true стоит, а плагина нет. Передайте --register-plugin, чтобы команда поправила это сама, без ручной правки файла:

bash
npx nestjs-docfy generate --register-plugin

Флаг включается вручную намеренно: часть команд сознательно остаётся на patch-spec вместо плагина компилятора, поэтому generate никогда не трогает nest-cli.json без явной просьбы. Команда дописывает запись в уже существующий массив compilerOptions.plugins (распознавая как зарегистрированные и строковые записи, и записи-объекты) и учитывает --dry-run.

Как это работает

Плагин nestjs-docfy не переписывает синтаксис декораторов и вообще не трогает AST. На каждой компиляции он заново прогоняет ровно тот же статический анализ, который уже делают generate, check и patch-spec (через ts-morph, по дереву исходников, а не по бандлу), и записывает получившуюся правку в docfy-metadata.json рядом с результатом сборки.

Затем, сразу после SwaggerModule.createDocument(), applyDocfyMetadata() читает этот файл и вливает его содержимое. Никакого require.cache здесь нет, как нет и проблемы с идентичностью классов в бандле: чтобы всё посчитать, запущенное приложение вообще не нужно.

Сравнение с patch-spec

Manual, CI-driven alternative

Если добавлять плагин компилятора не хочется (скажем, сборка идёт не через Nest CLI или у вас строже правила насчёт того, что выполняется во время компиляции), patch-spec считает точно такую же правку вручную по уже собранному документу. До появления плагина это был единственный вариант, и для разовых правок он по-прежнему удобен.

Сборщик SWC

Со сборщиком SWC Nest CLI обходится иначе. Под "builder": "swc" он вообще не вызывает before(), только ReadonlyVisitor, и лишь при заданном "typeCheck": true (иначе SWC пропускает проверку типов сам по себе). Этот плагин экспортирует оба хука, так что достаточно включить упомянутую настройку:

nest-cli.json
{
  "compilerOptions": {
    "builder": "swc",
    "typeCheck": true,
    "plugins": ["nestjs-docfy"]
  }
}
Requires typeCheck: true

Стоит знать одну деталь: при такой регистрации на каждой сборке рядом с корнем исходников появляется файл metadata.ts. Это тот же артефакт, который уже создаёт там собственная поддержка SWC из @nestjs/swagger. Если тот плагин не зарегистрирован, файл останется пустым и никому не помешает.

Без typeCheck плагин просто замолкает. Сборка проходит, но docfy-metadata.json не записывается, и applyDocfyMetadata() нечего вливать. generate отслеживает именно это сочетание и предупреждает о нём.

Обнаружения DocfyModule в рантайме всё это не касается. Под SWC оно работает так же, как под tsc, потому что SWC по-прежнему компилирует по файлу на модуль и require.cache заполняется тем же порядком. Если включить typeCheck нельзя, связка @WithDocs() и DocfyModule.forRoot() решит задачу и вовсе без плагина.