CLI-флаги Node.js: NODE_OPTIONS, preloads и диагностика¶
Источник: theNodeBook — Node.js CLI Flags
CLI-флаги Node.js — опции, которые исполняемый файл node разбирает до запуска entrypoint приложения. В механике — NODE_OPTIONS, preloads, флаги V8, source maps, conditions, предупреждения, диагностика, лимиты памяти и node --run. Конфигурация runtime складывается из argv, переменных окружения, флагов V8 и отдельных экспериментальных файлов конфигурации. process.execArgv отражает флаги исполнения Node; process.argv — путь к исполняемому файлу, entrypoint и аргументы программы.
CLI-флаги и конфигурация runtime в Node.js¶
Node сначала потребляет свои опции, инициализирует runtime, выполняет preloads, затем передаёт аргументы скрипта пользовательскому коду. process.execArgv показывает флаги исполнения Node, дошедшие до текущего процесса. process.argv — исполняемый файл, entrypoint и аргументы приложения.
Первое решение при старте принимается ещё до того, как существует entrypoint.
Node получает окружение родителя и вектор аргументов, затем начинает разбирать то, что относится к runtime: флаги, preloads, опции V8, политику source map и разделитель -- — всё это обрабатывается до разбора вашего файла.
1 2 | |
Оболочка запускает процесс и передаёт Node два блока: окружение и argv. Node читает оба. Из NODE_OPTIONS берётся --trace-warnings, затем потребляются --enable-source-maps и --require ./boot.cjs, app.mjs выбирается как entrypoint программы, а --port=3000 остаётся приложению, потому что стоит после разделителя --.
Ваш код видит результат:
1 2 | |
В process.execArgv — опции runtime Node. В process.argv — путь к исполняемому файлу, entrypoint и аргументы скрипта. Интересное происходит раньше: Node решает, какие байты относятся к runtime, какие — к программе, и какие настройки уже зафиксированы до запуска entrypoint.
Эта граница важна. CLI-флаг Node — опция, которую потребляет сам исполняемый файл node. Он меняет способ старта, разбора исходников, загрузки модулей, вывода предупреждений, размера кучи V8, диагностики или запуска скриптов из package.json. Конфигурация runtime — полное состояние старта, которое Node выводит из этих флагов, переменных окружения и нескольких экспериментальных поверхностей конфигурации до того, как пользовательский код получит управление.
После старта entrypoint часть этого состояния уже неизменна.
Source maps либо включены, либо нет. Preload-модули уже выполнены. V8 уже получил флаги кучи. Набор conditions для разрешения модулей задан. Политика вывода предупреждений установлена. Приложение по-прежнему может читать process.env, подгружать модули и вешать обработчики, но стартует в процессе, чья форма runtime была выбрана раньше.
Путь старта компактен:
1 2 3 4 5 6 7 | |
Это практическая модель. Когда production-команда выглядит странно, идите слева направо. Остановитесь там, где Node перестаёт потреблять опции. Всё после этого принадлежит программе.
Состояние при старте до JavaScript¶
Старт Node начинается в нативном коде. JavaScript-entrypoint появляется поздно в цепочке.
Исполняемый файл получает argc, argv и окружение процесса от ОС. К этому моменту оболочка уже сделала свою работу: сняла кавычки, раскрыла переменные, при необходимости glob'ы, применила перенаправления вне argv. Node получает строки. Исходный текст команды в shell уже исчез.
Отсюда многие «странные» баги команд. Node может разобрать --require ./boot.cjs, потому что это две записи argv. Может разобрать --require=./boot.cjs, потому что это одна запись со значением inline. Кавычка, съеденная shell, уже не существует. Литерал, переменная shell и wrapper-скрипт могут дать один и тот же итоговый argv, если wrapper не оставил следов в argv или окружении.
Затем Node читает входы старта слоями.
Первый слой — окружение. Часть переменных — общие данные runtime (PATH, TZ). Часть специфична для Node. NODE_OPTIONS особенна: она подаётся в парсер опций Node. Другие переменные Node затрагивают подсистемы напрямую: NODE_NO_WARNINGS, NODE_PENDING_DEPRECATION, NODE_REDIRECT_WARNINGS, NODE_V8_COVERAGE, UV_THREADPOOL_SIZE — примеры. Свойство одно: Node читает значения при инициализации процесса или при ранней инициализации подсистем, до того как приложение успеет переписать process.env.
Второй слой — область опций командной строки. Node разбирает флаги, пока не встретит форму entrypoint или разделитель аргументов. Во время разбора строится внутреннее состояние опций Node и собираются поддерживаемые настройки V8 для инициализации движка. Плохие флаги падают здесь. Плохие комбинации — здесь. Некорректное значение опции старта — до того, как начнётся граф модулей приложения.
Третий слой — создание движка и окружения. V8 нужны опции до создания isolate. Node нужны process-wide опции до создания JavaScript-окружения с globalThis, process, встроенными модулями и загрузчиками. Event loop появляется как часть этого окружения, но эта глава про конфигурацию, которая формирует его до начала работы.
После этого Node может отдать разобранный результат обратно в JavaScript. process.execArgv — видимый список аргументов исполнения Node. process.argv — видимый вектор аргументов программы. Это снимки границы старта. Поздняя мутация process.execArgv меняет массив, но source maps, политика предупреждений, размер кучи V8 и preloads для текущего процесса уже настроены.
Следующий крупный шаг — preload-модули. У Node уже достаточно JavaScript-runtime, чтобы загружать модули. Entrypoint приложения ещё впереди. Узкая фаза с process-wide охватом. Preloads могут ставить обработчики, патчить модули, инициализировать инструментирование и создавать глобальное состояние до того, как выполнится граф зависимостей entrypoint. Они также могут бросить исключение. Ошибка в preload прерывает старт до того, как приложение «успеет» стартовать.
Только после этих шагов Node оценивает entrypoint. Для файла — передача пути в путь загрузки CJS или ESM. Для --eval и stdin — компиляция переданной строки с выбранным типом ввода. Для --check — разбор и выход. Для --run — встроенный runner скриптов из package.json вместо загрузки файла приложения.
Нативная→JavaScript последовательность объясняет, почему флаги старта ощущаются иначе, чем обычная конфигурация. Код приложения может их инспектировать, но приходит уже после того, как многие из них потреблены.
Граница аргументов¶
Документированная форма команды Node:
1 | |
Первая область принадлежит Node. Вторая — выбор источника. Последняя — ваша.
1 | |
--trace-warnings и --max-old-space-size=2048 влияют на runtime. server.js — entrypoint. --verbose передаётся в server.js, потому что Node уже нашёл entrypoint программы. Приложение разбирает его как угодно.
Явный маркер -- делает границу однозначной:
1 | |
Первый --trace-warnings меняет вывод предупреждений Node. Второй — просто строка в process.argv. Node его не трогает.
Разделение видно на крошечном скрипте:
1 2 | |
Запуск с флагом runtime и флагом приложения:
1 | |
В process.execArgv попадёт --trace-warnings. В process.argv.slice(2) — --name=api. Этого часто хватает, чтобы отладить сломанную команду старта. Если флаг должен менять Node, но оказался в process.argv, он стоит после entrypoint или после --. Если флаг должен принадлежать приложению, но Node его отклоняет, он стоит до границы.
Node также принимает флаги V8 в области runtime. Проброс флагов V8 означает: Node распознаёт поддерживаемый набор флагов движка и передаёт их при создании isolate и состояния runtime. Типичный пример — флаги памяти:
1 | |
--max-old-space-size принадлежит V8. Node принимает его, проверяет форму достаточно для старта, и V8 использует значение при размере old generation. Скрипт видит флаг в process.execArgv. Размер кучи задан до первой строки вашего кода.
Часть флагов Node повторяема, часть — однозначна. Это снова проявится с NODE_OPTIONS, так что держите в голове: повторяемый preload накапливается, повторяемый порт inspector даёт одно победившее значение.
Есть ещё одна граница на виду: путь к исполняемому файлу. process.argv[0] — путь, которым запустили Node, или значение из механизмов запуска на некоторых платформах. process.argv[1] обычно путь к entrypoint, когда участвует файл. При --eval, --print, stdin и --run второй слот может отличаться, потому что обычного файла скрипта нет. Инструменты, слепо считающие process.argv[1] путём к файлу на диске, ломаются в этих режимах.
Process manager добавляет ещё слой. Unit-файл, shell-скрипт, Docker entrypoint или npm-скрипт могут обернуть итоговую команду Node. Wrapper может вставить флаги до видимой команды приложения, дописать аргументы после или задать NODE_OPTIONS. При неожиданном поведении смотрите эффективный процесс. В Linux /proc/<pid>/cmdline показывает argv, разделённый нулевыми байтами, а /proc/<pid>/environ — окружение, если хватает прав. Это вне Node, но часто самый быстрый способ доказать, какие входы старта процесс реально получил.
Для дочерних процессов Node снова полезен process.execArgv. Многие API при spawn новых экземпляров Node по умолчанию наследуют или переиспользуют exec-аргументы родителя. Это может быть именно то, что нужно для preloads и отладки. Это же может разнести флаг памяти, подавление предупреждений или inspector в helper-процессы, которые должны были остаться маленькими. Наследуемые exec-аргументы — часть контракта дочернего процесса, даже если вызов spawn далеко от команды старта сервиса.
Режимы entrypoint¶
Режим entrypoint — способ, которым Node получает исходный текст для выполнения. Путь к файлу — обычный случай. Есть и другие; они меняют разбор так, что это всплывает при отладке.
Файловый entrypoint — норма:
1 | |
Node разрешает путь от текущего рабочего каталога и передаёт файл в путь загрузки CJS или ESM. Путь entrypoint попадает в process.argv. Флаги до него — Node. Аргументы после — программа.
--eval выполняет исходник из командной строки:
1 | |
Обычно пишут -e. Node выполняет следующую строку как исходник программы. Файлового entrypoint нет. Текст приходит из значения argv, поэтому детали вроде import.meta.filename ведут себя иначе, чем для файла на диске.
Текущий рабочий каталог в eval-режиме заметнее. Относительные require() и import() всё равно нуждаются в базе. Для CommonJS eval Node оценивает строку в синтетическом main-контексте. Для ESM eval с --input-type=module у исходника семантика модуля, включая top-level await. Удобно для разовых команд инспекции. Скрипты, предполагающие file-local метаданные, в этом режиме могут ломаться.
--print — тот же источник с дополнительным выводом:
1 | |
-p вычисляет строку и печатает результат. Удобно для быстрой инспекции runtime: вывод идёт от самого Node в той же конфигурации старта, что и у любой другой команды. Если NODE_OPTIONS задаёт --trace-warnings, node -p это наследует.
--check разбирает файл и останавливается после проверки синтаксиса:
1 | |
Node читает файл, парсит, сообщает синтаксические ошибки и выходит. Тело программы не выполняется. Preloads всё равно важны: --check про исходник entrypoint, а конфигурация старта может изменить процесс ещё до этого шага. Считайте это проверкой синтаксиса с обычной конфигурацией старта.
--check остаётся на уровне синтаксиса. Он ловит ошибки разбора. Разрешение импортов, top-level выполнение и доступность runtime-фич — на других путях. Файл может пройти node --check и упасть при линковке ESM или выполнении CommonJS. Используйте его для узкой задачи.
Stdin — ещё один режим entrypoint:
1 | |
Entrypoint - говорит Node читать исходник из стандартного ввода. Без расширения файла или границы пакета Node нужен другой сигнал формы модуля, если ввод использует синтаксис ESM или режимы stripping TypeScript.
Этот сигнал — --input-type.
1 | |
--input-type задаёт, как Node интерпретирует строковый ввод из --eval, --print или stdin. В Node v24 значения включают commonjs, module, commonjs-typescript и module-typescript. Узкий runtime-факт: --input-type применяется к строковому вводу через --eval, --print или stdin.
Поведение по умолчанию для строкового ввода менялось между релизами по мере развития детекции синтаксиса. В v24 Node может обнаруживать синтаксис ESM в неоднозначном вводе при настройках по умолчанию. Команда старта, которой нужна стабильность между shell, CI и минорными версиями Node, всё равно должна явно говорить, что имеется в виду. --input-type=module яснее, чем полагаться на детекцию для eval с await или import.
Ловушку легко воспроизвести:
1 | |
Для файла на диске Node отклоняет такую форму при старте с ERR_INPUT_TYPE_NOT_ALLOWED. --input-type — для --eval, --print и stdin. Для файлового ES module entrypoint используйте .mjs, "type": "module" или метаданные границы пакета.
Интерактивный режим — ещё одна форма старта. У REPL свой путь. Сейчас важен выбор источника: файл, eval-строка, print-строка, stdin, проверка синтаксиса. Режим определяет, что Node разберёт после потребления runtime-флагов.
NODE_OPTIONS обрабатывается раньше командной строки¶
NODE_OPTIONS — переменная окружения, которая внедряет CLI-опции Node при старте. Node читает её до разбора аргументов командной строки. Практический результат — унаследованное состояние runtime.
1 | |
Команда меняет старт, хотя видимый argv называет только node и app.js. Внутри скрипта смотрите оба поля:
1 2 | |
NODE_OPTIONS — след в окружении. process.execArgv фиксирует аргументы исполнения Node с командной строки, поэтому в примере выше execArgv пуст, а строка NODE_OPTIONS видна в process.env. Глубже инспекции: NODE_OPTIONS меняет процесс до того, как пользовательский код успеет его «очистить».
Отсюда и польза, и опасность.
Полезно: платформа может навязать source maps, preloads, диагностику или лимиты памяти каждому процессу Node под service wrapper.
Рискованно: профиль shell, CI, test harness, process manager или родительский процесс могут протечь NODE_OPTIONS в команду, которая должна была стартовать «чистой».
Типичный сбой скучен и част: export NODE_OPTIONS="--require ./instrumentation.cjs" и затем node tools/migrate.js. Миграция стартует после того, как instrumentation.cjs уже выполнился. Если preload меняет глобалы, ставит хуки, меняет политику предупреждений или читает конфигурацию, команда миграции ведёт себя иначе, чем видимый текст node tools/migrate.js.
Баг обычно выглядит как расхождение двух способов запустить один файл. Разработчик в чистом shell: node app.js — один результат. CI под окружением job — другой. Файл тот же. Граф модулей на диске тот же. Состояние старта разное.
Держите временный print рано в отладочной ветке, до полной загрузки графа зависимостей. Он покажет, какие прямые флаги Node дошли до process.execArgv и какие унаследованные пришли из окружения. Для preloads print в entrypoint идёт после preloads; если подозреваете порядок, добавьте второй print в сам preload.
Node ограничивает, какие флаги допустимы в NODE_OPTIONS. Allowlist включает многие runtime-флаги, preload-флаги, source map и warning, диагностику, conditions и выбранные флаги V8. Парсер отклоняет флаги, которые сделали бы инъекцию через окружение слишком неоднозначной или структурно небезопасной. Имена скриптов и режим entrypoint идут из реальной команды. Процессу всё равно нужна настоящая граница команды.
Allowlist — граница безопасности и предсказуемости. Унаследованное окружение может включить --require, --import, предупреждения, conditions, диагностику, выбранные permission-флаги, inspector и часть флагов V8. Исходник программы и аргументы приложения остаются у реальной командной строки. Node всё равно разбирает реальную команду и находит там режим программы.
Приоритет опций — правило Node, когда одна настройка встречается в нескольких источниках старта. Для однозначных флагов командная строка побеждает NODE_OPTIONS.
1 2 | |
Inspector слушает порт 5555. Командная строка даёт более позднее, более конкретное значение. Окружение задало умолчание. Прямая команда его переопределила.
Повторяемые флаги комбинируются по порядку. Сначала записи из NODE_OPTIONS, затем с командной строки.
1 2 | |
Это эквивалентно --require ./a.cjs, затем --require ./b.cjs. Порядок важен: preload-модули выполняются по порядку и могут менять состояние процесса.
Кавычки тоже важны. NODE_OPTIONS — строка, которую разбирает парсер опций Node. Пробелы делят аргументы, если shell и Node не согласовали кавычки. На практике держите строку простой: полные имена флагов, кавычки для путей, предпочитайте видимые флаги командной строки, когда настройка относится к одной команде, а не ко всему дереву процессов.
Кроссплатформенные кавычки — ещё причина держать NODE_OPTIONS короткой. POSIX shell, PowerShell, cmd.exe, service manager и контейнеры по-своему экранируют строку до Node. Парсер Node видит значение переменной после этого слоя. Флаг, работающий в интерактивном shell, может упасть под wrapper'ом service, потому что кавычки изменились до старта Node.
Для стабильной эксплуатации разделяйте умолчания и политику конкретной команды. --enable-source-maps может жить в launcher'е, если все сервисы отгружают трансформированный код. Разовый preload для миграции — на команде миграции. Флаг памяти для worker — на команде worker. Широкая инъекция через окружение удобна, пока helper-процесс не унаследует настройку, под которую не проектировался.
NODE_OPTIONS также пересекается с загрузкой env-файлов. Node v24 может грузить переменные из файлов, и в них может быть NODE_OPTIONS. Получается несколько слоёв конфигурации; правила приоритета решают, кто победит. Детали разбора — в главе про env-файлы; для обзора старта достаточно знать, что слои существуют.
Preloads выполняются до entrypoint¶
Preload-флаги запускают код до entrypoint программы. Это буквальные хуки старта: Node разрешает и оценивает модули во время bootstrap, затем оценивает entrypoint.
--require — традиционный путь preload для CommonJS.
1 | |
--require означает: Node загружает ./boot.cjs через CommonJS loader до app.js. Разрешение следует тем же правилам, что и поздний require(). Модуль выполняется один раз и попадает в Module._cache. Если app.js позже вызовет require('./boot.cjs'), вернётся кэшированный экспорт без повторного выполнения файла.
Современный Node может preload'ить синхронный ESM-граф и через --require. Практическое разделение: --require для CommonJS setup, --import для ESM setup, когда preload использует top-level await или должен идти по ESM-native пути старта.
Деталь кэша имеет последствия. Preload с изменяемым экспортом может стать тем же экземпляром, который приложение импортирует позже. Это может быть намеренно:
1 2 | |
Если entrypoint require'ит этот модуль, он читает экземпляр, инициализированный при preload. Та же идентичность объекта возвращается. Чистый способ централизовать крошечное состояние старта. Плохое место для большой конфигурации приложения: связывает порядок старта с состоянием модуля и усложняет тесты.
Поэтому --require часто используют для инструментирования и process-wide setup:
1 2 3 | |
Положите это в boot.cjs, передайте через --require — обработчик предупреждений есть до загрузки графа зависимостей entrypoint. Обработчик внутри app.js может опоздать на предупреждения при загрузке модулей, которые app.js импортирует.
Пакеты инструментирования используют эту фазу, потому что им нужно обернуть API до импорта приложением. Если HTTP-библиотека загружена раньше модуля инструментирования, тот может не успеть обернуть экспортируемые функции или конструкторы. Порядок preload превращает гонку в правило старта: сначала инструментирование, потом приложение.
Патчинг имеет цену. Когда preload меняет экспорт встроенного модуля, глобалы или ставит async hooks, каждый следующий модуль работает в изменённом runtime. Исходник приложения выглядит чистым, а поведение приходит из кода старта. В production держите preload в развёртываемой команде и в процессе review, а не в скрытой локальной настройке shell.
Повторяемые --require выполняются в указанном порядке:
1 | |
Сначала env.cjs, затем tracing.cjs, потом app.js. Если tracing.cjs читает конфигурацию из env.cjs, порядок — часть контракта старта. Спрячьте порядок в длинном конфиге process manager — кто-нибудь его сломает.
ESM-native preload — --import.
1 | |
--import означает: Node разрешает и оценивает модуль через ES module loader до entrypoint. Действуют правила разрешения ESM, участие в module map ESM и возможность top-level await. Последнее влияет на тайминг старта: если preload ESM ждёт promise, Node ждёт завершения оценки модуля до entrypoint.
1 2 | |
Это работает в ESM preload. Это же задерживает entrypoint. Если awaited-операция зависнет, программа зависнет до старта app.mjs. Код entrypoint слишком поздно, чтобы поставить таймаут вокруг preload, который уже взял управление.
ESM preload проходит линковку ESM до оценки. Статические импорты разрешаются и линкуются первыми. Затем идёт оценка, включая top-level await. Сбой в этом графе отклоняет старт до графа entrypoint. Стек указывает на preload-граф — это верно, но команды часто ищут баг в entrypoint, потому что именно этот файл назван в команде сервиса.
CommonJS preloads выполняются раньше ESM preloads.
1 | |
Порядок в тексте команды обманчив. Node выполняет --require preloads раньше --import preloads. Документированное правило явное: у загрузчиков разные требования bootstrap. Если есть оба вида, порядок такой:
1 2 3 4 5 | |
Внутри каждого семейства повторяемые флаги сохраняют порядок источника: сначала NODE_OPTIONS, затем командная строка. Между семействами сначала CommonJS preloads.
Preloads также действуют за пределами main thread в нескольких путях старта Node. Worker threads, fork и cluster могут наследовать то же поведение preload при старте новых экземпляров Node с унаследованными exec-аргументами. Важно для инструментирования. Важно и для кода, который меняет глобалы, патчит built-ins или открывает ресурсы в preload. Preload, написанный для одного host-процесса, может выполняться один раз на каждый экземпляр Node.
Чистый preload мал и скучен. Поставить хук. Зарегистрировать инструментирование. Задать глобальный мост, если приложение уже владеет таким соглашением. И остановиться. Тяжёлый I/O, сетевые вызовы и process.exit в preload дают сбои старта, которые трудно приписать, потому что entrypoint приложения может отсутствовать в ожидаемом месте стека.
Полезный тест preload: убрать entrypoint приложения и запустить только preload.
1 | |
Для ESM:
1 | |
Так изолируется поведение старта. Если команда зависает, бросает исключение, открывает handles или печатает предупреждения — это preload. У приложения могут быть свои баги, но runtime уже пришёл к нему с изменённым состоянием.
Conditions меняют выбор пакета¶
--conditions добавляет пользовательские conditions в резолвер package.json exports.
1 | |
Условные exports позволяют пакету отдавать разные файлы для разных conditions. У Node уже есть встроенные вроде node, import и require. Пользовательская condition — ещё один селектор в команде старта.
При пакете с conditional exports:
1 2 3 4 5 6 7 8 | |
Запуск с --conditions=development может выбрать ./dev.js для этой точки входа. Команда с набором conditions по умолчанию выберет путь default.
Стартовый флаг может менять, какой файл загрузит пакет. Тот же исходник. Та же строка import. Другой граф модулей — резолвер получил другой набор conditions.
Используйте сдержанно. Пользовательская condition уместна, когда пакет намеренно публикует разные runtime entry. Становится грязно, когда команды используют её как скрытый «переключатель окружения». Две команды импортируют одно имя пакета с разными conditions — могут выполнять разный исходник с разными побочными эффектами и разным графом.
Флаг влияет на разрешение, тайминг раньше оценки. Когда спецификатор разрешился в конкретный URL файла или имя файла CommonJS, загрузчик идёт с этим файлом. Ключ кэша следует разрешённой цели. Два процесса с разными наборами conditions строят разные графы из одних строк import. Внутри одного процесса набор conditions — состояние старта. Половинчатая мутация process.env не меняет conditions, заданные при старте.
--conditions уместен для осознанных вариантов на уровне пакета и неудобен для поведения «на запрос». У сервисного процесса один набор conditions. Все запросы делят его. Поведение на запрос — в коде приложения. Поведение разрешения пакетов — в команде старта, как идентичность процесса.
Повторяемые conditions накапливаются:
1 | |
Обе пользовательские conditions доступны резолверу. Объект "exports" пакета всё равно контролирует итоговое совпадение, включая порядок ключей и fallback. Флаг только добавляет имена conditions. Метаданные пакета решают, какая цель победит.
Имена conditions — обычные строки. Выбирайте имена из контракта пакета, а не с одного ноутбука или ветки. development и production распространены, потому что многие инструменты их понимают. Командное имя работает, если пакет публикует его намеренно. Неожиданное имя в команде старта должно отправить вас в поле "exports" пакета смотреть, какой файл выбирается.
Source maps, предупреждения и политика deprecation¶
Часть флагов меняет то, что видят разработчики и операторы при сообщении о проблеме. Бизнес-логика та же. Меняется путь вывода runtime.
Поддержка source maps сопоставляет сгенерированные стеки с исходными местами, когда карты доступны.
1 | |
С включёнными source maps Node использует данные карт при форматировании stack trace. Полезно для bundled или трансформированного кода, в том числе из TypeScript. Механизм относится к подготовке stack trace. Node всё равно выполняет dist/server.js; карта меняет напечатанные в ошибках позиции.
Цена — на error path и при работе с метаданными. Для backend-сервисов source maps часто стоят включения, когда развёрнутый код отличается от написанного. Но настройка должна быть видимой. Отчёт о падении с mapped paths читается проще; битая или устаревшая карта может указывать на строки, которых нет в артефакте.
Source maps также затрагивают метаданные coverage через вывод V8 coverage. Node потребляет комментарии source map и данные, которые компилятор, bundler или путь stripping TypeScript уже создали. Если карты нет, она устарела или указывает на пути, отсутствующие в runtime-образе, Node располагает ограниченной информацией.
У предупреждений свои флаги политики.
1 2 3 | |
--trace-warnings печатает стеки для process warnings, включая deprecation. --no-warnings подавляет вывод предупреждений в stderr по умолчанию. --redirect-warnings пишет предупреждения в файл, при сбое записи — fallback в stderr.
Контроль предупреждений — политика вывода runtime. Он влияет на то, как предупреждения показываются. Вызов устаревшего API всё равно произошёл. Предупреждение max-listeners всё равно означает, что emitter превысил лимит слушателей. Подавление вывода меняет только видимость.
--no-warnings и узкие отключения по коду скрывают сигналы миграции и нестабильных API. Подавление вывода не убирает саму проблему — только то, что вы её увидите в логах.
Есть и точечный контроль:
1 | |
Отключает предупреждения по коду или типу. Может приглушить известный шумный путь при миграции. Может скрыть единственный сигнал старта, что флаг, API или зависимость идут по нестабильному пути. Используйте самый узкий контроль под задачу; при расследовании предпочитайте trace output.
Режим deprecation — политика старта для deprecation warnings.
1 2 3 4 | |
--pending-deprecation включает pending deprecations, тихие по умолчанию. --trace-deprecation печатает стеки для deprecations. --throw-deprecation превращает deprecations в thrown errors. --no-deprecation подавляет deprecation warnings.
Режимы полезны на разных этапах жизни кодовой базы. CI может работать строже, чтобы поймать использование раньше. Production на короткое окно расследования может предпочесть trace. Пустое подавление убирает один из немногих встроенных сигналов миграции Node — у такой политики должны быть владелец и срок.
У предупреждений есть и формы через переменные окружения. NODE_NO_WARNINGS=1 подавляет предупреждения. NODE_PENDING_DEPRECATION=1 включает pending deprecations. NODE_REDIRECT_WARNINGS=file перенаправляет вывод. Это тоже конфигурация старта, хотя не записана как CLI-флаг. Если вывод предупреждений различается между окружениями, смотрите и команду, и окружение.
Отдельно — политика необработанных rejection:
1 | |
Узкий runtime-факт для промисов: флаг меняет реакцию Node, когда rejection остаётся необработанным. В Node v24 режим по умолчанию — throw. Команда старта, меняющая его, может изменить, будет ли процесс предупреждать, бросать, выходить с кодом или молчать для этого класса сбоев.
V8, память и диагностика¶
CLI принадлежит Node. JavaScript-движок — V8. Стартовые флаги пересекают эту границу.
1 | |
Команда печатает опции V8, принимаемые встроенным движком. Полный каталог редко нужен в документации приложения или манифестах сервисов. Навык обзора — узнавать, когда флаг относится к движку, а не к JavaScript API Node.
Флаги лимита памяти — частый случай:
1 | |
--max-old-space-size задаёт максимальный размер old generation кучи V8 в MiB. Old generation хранит долгоживущие объекты JavaScript. При приближении к лимиту GC работает агрессивнее; если V8 не освобождает достаточно памяти, процесс может завершиться с out-of-memory.
Флаг читается до построения кучи для isolate. Отсюда другая семантика, чем у конфигурации приложения. Файл конфигурации, который грузит приложение, может решать число job'ов, соединений с БД или размер in-memory кэша. Размер кучи V8 к тому моменту уже выбран. Если лимит кучи — часть контракта сервиса, положите его в команду старта или конфиг process manager.
Флаг ограничивает одну область кучи V8. Буферы могут использовать external memory. Нативный код тоже аллоцирует. C++ сторона Node, OpenSSL, zlib, mmap-данные, стеки потоков и фрагментация аллокатора добавляют память вне old generation. Узкий вывод: --max-old-space-size размерит одну крупную область кучи V8 и должен быть задан до инициализации isolate.
Отсюда частое расхождение: сервис с --max-old-space-size=512 может показывать RSS выше 512 MiB. У JavaScript old generation есть лимит. У процесса — больше регионов. Лимиты контейнера, учёт ОС, нативные аллокации и external buffers тоже важны. Флаг V8 — давление на кучу V8. Лимиты платформы — политика памяти всего процесса.
Есть --max-semi-space-size для young generation semi-space. Большинство backend-сервисов трогает его реже. Настройка «наугад» меняет компромисс пропускной способности аллокаций и частоты GC в зависимости от нагрузки. Назовите при встрече; меняйте после профилирования, а не по догадке.
В Node v24 есть --max-old-space-size-percentage — размер old space как процент доступной памяти. Полезно в ограниченных средах: runtime выводит размер кучи из лимита, который видит Node. Точная польза зависит от того, как ОС и контейнер отдают ограничения памяти. Считайте это политикой старта и проверяйте поведение в той же среде, где будет работать процесс.
Флаги памяти меняют и форму сбоя. Меньший лимит кучи может раньше и предсказуемее валить утечки. Больший — откладывает падение, увеличивая паузы GC и давление на хост. Ни один не чинит удержание ссылок — только конверт, в котором утечка становится видимой.
Диагностические флаги старта включают сбор данных или меняют вывод при сбое.
1 2 3 | |
Это диагностика. --report-uncaught-exception пишет diagnostic report при необработанном исключении наверху. --report-on-fatalerror — при фатальных ошибках внутри runtime. --trace-uncaught печатает стек для uncaught exceptions с дополнительным контекстом места throw.
Глубокий workflow отчётов — в главе про диагностику. Факт старта здесь: флаг отчёта меняет, какой артефакт останется, когда процесс уже мёртв.
Диагностике часто нужны companion-флаги путей:
1 | |
Пути оцениваются в окружении процесса. Относительные пути — от текущего рабочего каталога, который у service manager может отличаться. Если отчёты — часть production-контракта, используйте каталог, который существует, доступен на запись пользователю сервиса и собирается вашей системой логов или артефактов.
Флаги heap и CPU profiling в той же категории старта:
1 2 | |
При обзоре команды старта спросите, куда лягут файлы, как они ротируются и уместен ли overhead в этом окружении. Интерпретация профилей — в отдельных главах.
Часть диагностических флагов работает непрерывно, часть — при сбое, часть — по сигналу. --heap-prof просит процесс собирать heap profile. --heapsnapshot-signal=SIGUSR2 — реагировать при сигнале. Классифицируйте флаг по моменту активации.
Inspector-флаги тоже видны здесь:
1 | |
Inspector открывает endpoint отладки при старте. Привязка к публичному интерфейсу меняет экспозицию процесса.
Не публикуйте --inspect на внешний интерфейс без осознанной сетевой границы. Endpoint отладки — полноценная поверхность атаки на runtime.
Тот же привычный обзор — для permission-, TLS-, test- и OpenSSL-флагов. В реальных командах они встречаются. Назовите категорию, механизм — в своей главе. Одна команда может смешивать слои:
1 | |
Здесь test runner, source maps и политика предупреждений одновременно. При разборе разделяйте слои.
Часть флагов платформенно ограничена. Некоторые опции V8 есть только на Linux или Windows. Некоторые флаги Node в v24 экспериментальны. Парсер может отклонить неподдерживаемые комбинации до entrypoint. Раннее падение хорошо: процесс умер до полуинициализированного кода приложения.
node --run запускает скрипты из package.json¶
node --run выполняет скрипт из package.json через встроенный package-script runner Node.
1 | |
Node поднимается вверх от текущего каталога, пока не найдёт package.json, затем ищет test в объекте "scripts". Команда из скрипта выполняется из каталога с этим package.json. Node также prepends node_modules/.bin с текущего пути вверх, чтобы находились локальные бинарники пакетов.
Обход каталогов важен в монорепозиториях. node --run test из packages/api/src может выбрать ближайший родительский package.json со скриптом. Команда выполняется из каталога этого пакета. Скрипт, предполагающий process.cwd() равным каталогу shell при старте, под --run может вести себя иначе.
Аргументы после -- передаются скрипту:
1 | |
Строка --watch принадлежит команде package script. У watch mode Node свой регион флагов. Идея границы та же: Node потребляет --run test, затем -- отделяет аргументы runner'а от аргументов дочерней команды.
node --run намеренно меньше, чем script runner'ы package manager'ов. Он выполняет названный скрипт напрямую. npm lifecycle вроде pretest и posttest остаются у npm. npm-специфичные переменные окружения тоже. Node задаёт свои метаданные скрипта — имя и путь пакета — и запускает настроенную команду.
Меньший контракт — и фича. Node может стартовать скрипт с меньшей обвязкой. Совместимость зависит от допущений скрипта. Если npm run build работает, потому что prebuild генерирует файлы, node --run build пропустит lifecycle — сборка может упасть. Если скрипт читает npm-специфичные переменные, прямой запуск Node меняет входы.
Разница полезна для простых скриптов:
1 2 3 4 5 6 | |
1 | |
Если проект опирается на npm lifecycle, node --run меняет контракт: прямое выполнение скрипта, метаданные Node, локальные бинарники в PATH. Используйте там, где вся нужная подготовка уже внутри самой команды скрипта.
--run пересекается с другими режимами старта. Это режим команды. Watch, eval и syntax-check имеют свои ожидания. При разборе node --run name читайте как выполнение package script; всё после -- — аргументы этого скрипта.
Экспериментальные файлы конфигурации¶
Node v24 включает экспериментальную поверхность configuration file.
1 | |
Файл конфигурации Node — JSON, централизующий поддерживаемые runtime-опции. Может содержать nodeOptions и настройки по namespace для подсистем вроде test, watch и permission. В CLI docs есть schema URL, привязанный к версии Node.
Файл всё равно вход старта. Node читает его до entrypoint, валидирует ключи, применяет поддерживаемые значения и падает рано на неизвестные или не на своём месте ключи. Значения в nodeOptions ограничены опциями, допустимыми в NODE_OPTIONS. Блоки namespace могут включать соответствующую подсистему через конфигурацию. Файл структурированнее длинной переменной окружения, но точная схема имеет значение.
Приоритет часть дизайна: командная строка и NODE_OPTIONS выше файла конфигурации; файл конфигурации выше NODE_OPTIONS, загруженного из dotenv-файлов. Неизвестные ключи и неподдержанное использование namespace падают при разборе.
Правило приоритета даёт путь аудита. Флаг командной строки — самый сильный видимый вход. NODE_OPTIONS из реального окружения рядом с ним. Файл конфигурации заполняет структурированные умолчания ниже. NODE_OPTIONS из dotenv — ниже файла. Одна опция может встретиться в нескольких слоях; побеждает слой с более высоким приоритетом.
В Node v24 поверхность всё ещё экспериментальная. Для production-контракта старта сегодня проще аудировать короткую shell-команду или команду process manager с явными флагами, чем JSON-файл, чья стабильность и поведение namespace ещё могут меняться.
Проход по отладке старта¶
Баги старта обычно попадают в несколько механических групп.
Первая — не на месте флаги. Node-флаг после entrypoint становится аргументом приложения. Флаг скрипта до entrypoint разбирает Node и может уронить процесс. Лечение — разделитель -- или перестановка команды.
1 | |
Здесь --trace-warnings достаётся app.js. Политика предупреждений Node не меняется.
1 | |
Здесь политика меняется. Та же строка. Другая область.
Вторая группа — унаследованное состояние runtime. NODE_OPTIONS, NODE_NO_WARNINGS, NODE_PENDING_DEPRECATION и родственные переменные могут прийти от родителя. Видимая команда чистая, а эффективный процесс стартует с лишней конфигурацией. Печатайте process.execArgv, смотрите окружение и wrapper, который запускает Node.
Третья — побочные эффекты preload. Preload может бросить, зависнуть, пропатчить built-in, открыть handle или поставить обработчик до entrypoint. Воспроизведите с --eval "0" и теми же preload-флагами. Если сбой есть на урезанной команде, виноват хук старта.
Четвёртая — выбор загрузчика. --conditions, --input-type, "type" в package, расширения файлов и вид preload влияют на путь загрузки. --input-type формирует eval и stdin. Файловый entrypoint — расширение и метаданные пакета. --require — CommonJS. --import — ESM. --conditions — разрешение package exports.
Пятая — политика вывода. --no-warnings, --redirect-warnings, флаги deprecation и source maps меняют то, что видят операторы, пока условие в runtime остаётся. Чистый лог может означать здоровый процесс. Может означать, что вывод предупреждений перенесли или подавили.
Шестая — несовпадение ресурсного конверта. Флаги кучи V8 влияют на регионы кучи V8. Память процесса шире. Диагностические флаги создают файлы только там, где процесс может писать. Inspector открывает endpoint только там, где интерфейс и порт реально привязаны. На этих границах конфигурация старта упирается в ОС.
Угадывание добавляет шум. Классифицируйте флаг, найдите границу, определите слой, который его подал, и проверьте, есть ли этот слой в окружении, где воспроизводится баг.
Проход по разбору команды¶
Плотная команда становится управляемой, когда каждую часть классифицируют.
1 2 3 | |
NODE_OPTIONS внедряет CommonJS preload и trace предупреждений. Командная строка задаёт лимит old-space V8, condition prod для exports, включает source maps и выбирает ./dist/server.mjs как entrypoint. --port=8080 — приложению.
Порядок выполнения следует из классификации:
1 2 3 4 5 6 | |
Мелкие изменения меняют поведение. Перенесите --enable-source-maps после ./dist/server.mjs — Node перестанет считать его runtime-флагом. Добавьте -- перед --conditions=prod — резолвер пакетов не увидит condition. Ещё один --require на командной строке выполнится после того, что из NODE_OPTIONS. Замените --require на --import — поведение кэша CommonJS сменится на module map ESM.
Привычка такая: читайте команду как состояние старта. Runtime начинается до вашего файла, и флаги, которые чаще всего решают исход, приложение часто даже не разбирает.
Связанное чтение¶
- Предыдущая: stdin, stdout и stderr в Node.js: TTY, pipe и backpressure
- Далее: Файлы .env в Node.js: --env-file, process.env, NODE_OPTIONS и разбор DotEnv