REPL, Inspector, watch mode и SEA в Node.js¶
Источник: theNodeBook — REPL, Inspector, Watch & SEA
Node.js помещает в один исполняемый файл оценщик выражений, конечную точку отладчика, цикл перезапуска и хуки упаковки SEA. Это инструменты рантайма, поэтому риски связаны с состоянием процесса: кто может вводить команды, кто подключается к порту, что перезапускается и какие файлы попадают в старт.
Инструменты рантайма: REPL, Inspector, watch mode и SEA¶
Запустите node без файла — получите интерактивную строку:
1 2 3 4 5 | |
Эта строка — REPL: read, evaluate, print, loop. Он читает одну единицу ввода, оценивает её в постоянном JavaScript-окружении, печатает результат и ждёт следующий ввод. «Loop» буквально: процесс жив, потому что REPL владеет активными потоками ввода-вывода — обычно process.stdin и process.stdout — и снова просит оценщик выполнить следующий фрагмент текста.
REPL — живое состояние рантайма за приглашением. Привязки сохраняются. Встроенные модули доступны. Важна текущая рабочая директория. Тот же объект process. Если в приглашении вы меняете process.env.DEBUG, вы меняете текущий процесс. Если делаете require(), модуль попадает в обычный кэш CommonJS. Если бросаете исключение, REPL ловит его на этом интерактивном ходе и возвращает приглашение.
Маленький инструмент. Большой охват.
Остальные инструменты так же близки к процессу. Inspector открывает конечную точку протокола в состояние отладки. Watch mode владеет перезапуском при изменении файлов. SEA упаковывает стартовую нагрузку в артефакт исполняемого файла. Остальная глава называет эти границы через механику рантайма.
Контекст REPL¶
Контекст REPL — объект, используемый как локальное окружение для оцениваемого ввода. Автономный REPL Node использует глобальный контекст JavaScript. Встроенный REPL, созданный кодом приложения, обычно получает отдельный контекст, если вы не настроили global.
Разница видна сразу:
1 2 3 4 | |
Привязка живёт, потому что REPL держит контекст между оценками. Файл скрипта оценивается один раз и завершается, когда работы не осталось. REPL продолжает принимать исходный текст и оценивает каждый ход против того же состояния сессии.
У оценщика по умолчанию есть поведение, специфичное для Node. Имена core-модулей могут разрешаться по требованию в приглашении:
1 2 3 4 | |
fs и path становятся доступны через правила оценки REPL по умолчанию. Это удобство REPL. Обычному файлу .js по-прежнему нужны require('node:fs') или import.
Текущая рабочая директория — база для многих экспериментов в приглашении. process.cwd() — каталог процесса на старте REPL, если вы не вызвали process.chdir(). Относительный fs.readFileSync('x') использует этот каталог. Относительный CommonJS require('./x') из автономного REPL тоже разрешается от текущей рабочей директории синтетического main-контекста REPL. Обычный модуль разрешает относительные импорты от своего файла. У REPL нет реального исходного файла для только что набранной строки — для экспериментов с путями это важно помнить.
Результаты выводятся через функцию writer. По умолчанию используется util.inspect(), поэтому объекты печатаются в формате инспекции, а не JSON. Предыдущий результат попадает в _, пока вы сами не присвоите _. Предыдущая ошибка — в _error по тому же правилу.
1 2 3 4 | |
Состояние подчёркивания удобно при инспекции. Это также состояние сессии. Относитесь к нему как к служебной переменной, а не к проектированию программы.
Путь оценщика сложнее, чем кажется по приглашению. REPL получает байты из потока ввода. readline превращает ввод терминала в отправленные строки, обрабатывает редактирование и буферизует незавершённый многострочный ввод. Сервер REPL решает: специальная команда, неполный JavaScript или готовый к оценке исходник. Специальные команды выполняются на REPLServer. Неполный JavaScript остаётся в буфере. Полный уходит в оценщик с четырьмя данными: отправленный код, объект контекста, имя ресурса для диагностики и колбэк с ошибкой или значением.
Колбэк важен. Оценка может быть синхронной на уровне JavaScript и всё равно вернуться через контракт колбэка. Пользовательский оценщик может завершиться позже. Приглашение возвращается, когда оценщик вызвал колбэк и writer закончил форматирование значения.
Оценщик по умолчанию компилирует отправленный исходник как интерактивный скрипт. Это не то же самое, что выполнение файла через загрузчик CommonJS. У REPL уже есть контекст. Вокруг оценки достаточно обёртки, чтобы привязки жили, core-модули подтягивались по имени, работал top-level await, присваивался _ и восстанавливалась синтаксическая ошибка, требующая ещё строк. Синтаксическая ошибка, похожая на незавершённую, внутри может стать repl.Recoverable — сервер продолжает собирать строки вместо немедленного вывода сбоя.
1 2 3 4 5 6 | |
Приглашение | значит, что REPL держит буферизованный ввод. В Node v24 индикатор многострочности — эта вертикальная черта, добавлена поддержка многострочной истории. Оценщик не выполнял объявление функции после первой строки — ждёт, пока буфер станет полной единицей ввода.
Состояние контекста и состояние модулей — разные части процесса. Привязка const, созданная в приглашении, принадлежит оцененному окружению REPL. Модуль, загруженный через require(), — кэшу CommonJS. В REPL с отдельным контекстом .clear сбрасывает контекст и прерывает незавершённый многострочный ввод. В автономном REPL в режиме global объявления и мутации глобала могут остаться. Загруженные модули, открытые дескрипторы, изменённые переменные окружения, подменённые built-in, активные таймеры и изменённые объекты могут сохраниться — они живут вне объекта контекста.
Первый урок REPL: приглашение кажется одноразовым. Процесс хранит состояние.
История REPL — сохранённый список предыдущих вводов. Автономный REPL Node хранит историю между сессиями, если не настроено иначе. В Node v24 многострочная история сохраняется с индикатором |, прежние многострочные записи можно редактировать при возврате из истории. Программным REPL для файловой истории нужен явный replServer.setupHistory().
Специальные команды — dot-команды REPL, обрабатываемые до обычной оценки JavaScript. .help перечисляет их. .exit закрывает сессию. .clear сбрасывает отдельный контекст REPL и прерывает незавершённый многострочный ввод; в автономном global-режиме существующие объявления могут остаться. .save пишет сессию в файл. .load подаёт файл в текущую сессию. .editor переводит приглашение в режим многострочного редактора.
1 2 3 | |
Это управляющий ввод REPL. Его разбирает сервер REPL. Это не синтаксис JavaScript, и он не проходит тот же оценщик, что 1 + 1.
Сигналы и TTY важны: приглашение интерактивно. Ctrl+C один раз прерывает текущий ввод или путь оценки. Дважды на пустой строке — выход. Ctrl+D — выход. Tab запрашивает кандидатов автодополнения. Это взаимодействия терминала поверх потока ввода; REPL на не-TTY потоках получает меньше возможностей.
Await в приглашении¶
Top-level await в REPL — приглашение принимает await на верхнем уровне одного интерактивного хода.
1 2 3 4 | |
Top-level await уже есть в ES-модулях (глава о ES-модулях) и в асинхронном выполнении (async/await). Версия для REPL — удобство вокруг интерактивной оценки: один ход приглашения ждёт settlement Promise, затем печатает fulfilled-значение или сообщает об отклонении.
Приглашение по-прежнему сессия REPL, а не внезапно ES-модуль на диске. Удобства CommonJS остаются. Core-модули по-прежнему можно тянуть по имени в оценщике по умолчанию. import() работает, когда нужна семантика загрузки ESM из приглашения.
1 2 3 4 | |
Число 1361 — пример вывода. У вашего проекта будет свой размер локального package.json.
Один край REPL await касается лексической области const. Документация Node отмечает, что в некоторых интерактивных случаях REPL await может нарушить обычное лексическое поведение const. Практический совет прост: REPL — для инспекции и коротких экспериментов. Повторяемое поведение загрузки модулей выносите в файл, когда сами границы модуля под тестом.
Await меняет и тайминг приглашения. REPL получает полную единицу ввода, начинает оценку, видит асинхронный путь результата и ждёт перед печатью финального значения. Пока Promise pending, процесс может выполнять другую работу из очереди — event loop движется. Приглашение просто не даёт следующий ход ввода, пока выражение с await не урегулируется.
1 2 3 4 5 | |
Таймер срабатывает, пока Promise в await pending. REPL интерактивен, но это тот же процесс Node с тем же event loop и поведением микрозадач. Приглашение ждёт результат оценки; рантайм продолжает обрабатывать работу.
Top-level await в модуле участвует в связывании и оценке модулей. Top-level await в REPL — в одном интерактивном ходе оценки. Разница важна при проверке порядка импортов, циклов или сбоев старта. У графа модулей — состояние линкера. У сессии REPL — контекст и явно загруженные модули.
То же предупреждение про границы пакетов. Файл tool.mjs идёт через загрузчик ES-модулей. "type": "module" в package.json меняет загрузку .js. REPL сохраняет идентичность интерактивного оценщика через эти границы: может импортировать модули, require() built-in и CommonJS, await — но свои имена ресурсов, правила контекста и служебные привязки.
--no-experimental-repl-await отключает top-level await в REPL. Имя флага историческое; в Node v24 поведение включено по умолчанию.
Встраивание REPL¶
node:repl — встроенный модуль для создания серверов REPL из кода. Встроенный REPL запускается внутри другой программы Node. node без точки входа — автономный REPL; node:repl даёт приложению ту же серверную машинерию.
1 2 3 4 | |
repl.start() возвращает REPLServer. Сервер читает поток ввода, пишет в поток вывода, оценивает каждую отправленную команду и экспонирует объект context. Присвоение pid выше делает pid видимым в приглашении как локальное значение.
Потоки по умолчанию — process.stdin и process.stdout. Можно передать другие — так строят Unix-сокеты, тестовые оболочки или контролируемые админ-консоли. Потоки определяют, насколько сессия «терминальная»: TTY даёт цвета, редактирование строк, превью автодополнения и обработку клавиш; обычный поток — текст в обе стороны.
Пользовательские значения контекста — главная причина встраивать REPL:
1 2 3 4 5 6 | |
В приглашении state.requests читает тот же объект, который мутирует программа. В этом смысл. В этом же риск.
Встроенный REPL может открыть живые внутренности процесса. Клиент БД в контексте — запросы из приглашения. Кэш в контексте — мутации из приглашения. При global в контексте REPL видит и меняет глобальное состояние процесса. Свойства объектов записываемы, пока вы не определите иначе.
Для read-only значений контекста используйте Object.defineProperty():
1 2 3 4 5 6 | |
Замораживается переданный объект, свойство контекста стабильно. Вложенные объекты нуждаются в своей политике. REPL делает ровно то, что говорят правила объектов JavaScript.
Есть и пользовательские оценщики: текст ввода, контекст, имя ресурса, колбэк. Полезно, когда приглашение — интерфейс команд, а не JavaScript. Для этой главы достаточно встроенного оценщика. Важнее операционная граница: встраивание REPL даёт интерактивный путь выполнения кода внутри процесса.
Опция useGlobal решает, сколько REPL делит с хост-программой:
1 2 3 4 5 6 | |
При useGlobal: true оцениваемый код использует глобальный объект JavaScript как контекст — так работает автономный CLI REPL Node. По умолчанию у repl.start() — false, Node создаёт отдельный контекст. «Отдельный» значит свой global-подобный объект для привязок оценки; значения на r.context по-прежнему указывают на объекты, которые вы туда положили.
Граница полезна для узкой инспекции и тонка, если прикрепить живые объекты. Отдельный контекст с server, db или cache всё равно открывает методы этих объектов. Идентичность объектов JavaScript пересекает свойство контекста. Если у объекта есть метод, мутирующий состояние, REPL может его вызвать.
Потоки ввода-вывода — ещё одна граница. REPL на process.stdin конкурирует с приложением за ввод терминала. REPL на Unix-сокете, TCP или произвольном потоке переносит приглашение в другое место. Рантайм не добавляет контроль доступа из-за смены потока. Поток — транспорт. Политику «кто может подключиться» задаёт ваша программа.
Встроенный REPL влияет и на завершение процесса. Живой REPL держит ввод открытым. Если остальная программа закончилась, а у REPL активен поток ввода, процесс может остаться жив. r.close() закрывает сервер REPL. Закрытие базового потока тоже может завершить сессию. Для локальной диагностики встройте REPL в тот же путь shutdown, что и остальной процесс, чтобы он не держал event loop после полезной работы.
Держите границу локальной, пока нет отдельного дизайна безопасности. Аутентификация, авторизация, аудит и сетевая экспозиция — темы эксплуатации позже. Механизм рантайма прост: сервер REPL принимает ввод и оценивает его с доступом к тому контексту и состоянию процесса, которые вы ему дали.
Конечная точка Inspector¶
Inspector — интерфейс отладки Node к бэкенду V8 inspector. Он открывает состояние рантайма через Inspector Protocol — JSON-протокол для фронтендов отладчика и программных клиентов.
Запуск с конечной точкой inspector:
1 | |
Node открывает WebSocket-конечную точку, обычно на 127.0.0.1:9229, и печатает URL с ws://. Конечная точка inspector — хост, порт и сгенерированный путь. Клиент отладчика подключается и шлёт команды протокола. Node маршрутизирует их в V8 и интеграцию inspector Node, ответы и события идут обратно по тому же соединению.
Варианты старта меняют момент запуска пользовательского кода:
1 2 3 | |
--inspect — конечная точка и сразу выполнение программы. --inspect-brk — конечная точка и останов до пользовательского кода. --inspect-wait — конечная точка и ожидание подключения отладчика перед запуском пользовательского кода. Все три принимают необязательные хост и порт. Порт 0 просит ОС выдать свободный порт.
Короткая цепочка:
1 2 3 4 5 6 | |
Протокол организует команды по доменам. Runtime.evaluate оценивает выражение. Debugger.enable включает события отладчика. Есть Profiler.* и HeapProfiler.*, но CPU-профили и heap snapshot — темы observability и производительности. Здесь важна граница протокола: инструменты общаются сообщениями; процесс владеет JavaScript-объектами и отвечает данными протокола.
Конечная точка публикует target. Клиент узнаёт или получает WebSocket URL, открывает соединение, обменивается сообщениями с числовыми ID. Запросы несут имя метода вроде Runtime.evaluate или Debugger.setBreakpointByUrl. Ответы — тот же ID для сопоставления. События (notifications в API Node) приходят без привязки к конкретному запросу. Debugger.paused может прийти из-за breakpoint, debugger в коде или по просьбе клиента приостановить выполнение.
Форма «запрос-ответ плюс события» объясняет дизайн node:inspector. Отправленная команда может завершиться результатом. Сессия одновременно эмитит события. Оба пути активны.
1 2 3 | |
Домены протокола сопоставляются агентам внутри V8 и интеграции Node. Runtime может оценивать выражения и отдавать превью объектов. Debugger — события разбора скриптов и breakpoints. Console пересылает активность консоли. В Node v24 есть экспериментальные точки для сетевых событий inspector за флагами — это интеграция инструментов, а не ядро модели отладки здесь.
Инспекция объектов по протоколу использует handles, previews и явные команды release. Фронтенд может запросить удалённый объект, показать превью, затем запросить свойства. В инспектируемом процессе остаются исходные объекты рантайма. Фронтенд получает описания протокола и ID handles. Поэтому приостановленный процесс может показывать состояние объекта инструменту где угодно.
Пауза меняет выполнение. Когда отладчик останавливает главный поток, JavaScript останавливается в известной точке. Таймеры и готовность I/O могут накапливаться за паузой. Resume возвращает управление. Для локальной отладки нормально. Для production на неверном процессе — инцидент.
Привязка inspector к публичному адресу открывает интерфейс отладки с возможностью выполнения кода. Безопасный дефолт разработки — loopback. 127.0.0.1 — локальные клиенты. 0.0.0.0 — любой, кто маршрутизируется на порт, может попытаться подключиться. Файрволы, туннели, контейнеры и удалённые хосты меняют состав «кто». Привязка хоста — решение безопасности, а не удобства.
--inspect-publish-uid управляет, куда Node публикует URL inspector. По умолчанию — stderr и HTTP discovery. Инструментам проще найти target. URL может попасть в логи. Сгенерированный идентификатор в URL есть, но реальная граница экспозиции — хост и порт.
--inspect-brk и --inspect-wait — управление стартом; затрагивают и preload из --require или --import. Когда отладчик останавливается до пользовательского кода, точная остановка зависит от режима и пути загрузчика, но цель одна: дать отладчику наблюдать код до того, как entrypoint приложения уйдёт далеко. Важно для багов в bootstrap конфигурации, preload-инструментации или top-level оценке модулей.
Активация inspector по Unix SIGUSR1 имеет свой переключатель старта: --disable-sigusr1. Флаги permission model тоже могут ограничивать inspector; модель прав runtime в этом курсе отложена. Локальный вывод: inspector — возможность процесса. Решайте при старте, может ли процесс её открывать, и согласуйте программные вызовы с этим решением.
Сессии Inspector из кода¶
node:inspector — встроенный модуль для работы с inspector из JavaScript: открыть конечную точку, получить URL, ждать отладчик, создавать сессии.
1 2 3 4 5 | |
inspector.open() активирует inspector после старта процесса. Первый аргумент — порт; 0 — случайный свободный. Второй — хост. В Node v24 вызов возвращает Disposable; dispose закрывает inspector через inspector.close().
inspector.waitForDebugger() блокирует, пока подключённый клиент не отправит Runtime.runIfWaitingForDebugger. Бросает, если inspector не активен.
1 2 3 4 5 | |
Код намеренно останавливает текущий поток: отладчик успевает подключиться до следующего кода. Используйте, где важно состояние старта. Уберите из обычного старта сервиса.
Сессия inspector — клиентский объект, подключённый к бэкенду inspector. Callback API — node:inspector. Promise API — node:inspector/promises. В Node v24 node:inspector/promises помечен Stability 1, experimental, хотя основной node:inspector стабилен.
1 2 3 4 5 6 7 8 9 | |
session.connect() подключает сессию к бэкенду. session.post() шлёт команду Inspector Protocol. Форма результата — от протокола: Runtime.evaluate возвращает объект с полями вроде type, value, description.
Сессии расширяют EventEmitter — уведомления по имени события:
1 2 3 | |
Отладка в том же потоке имеет пределы. Сессия внутри потока, который она же отлаживает, может отправлять команды в то, что сейчас выполняет код сессии. Breakpoint на том же пути может остановить и клиент отладчика, и отлаживаемое выполнение вместе. Инспекция worker — отдельные правила, позже. Здесь узкая модель: программные сессии — для контролируемых команд протокола; пошаговая интерактивная отладка — внешнее подключение отладчика.
Важна очистка. session.disconnect() сбрасывает состояние протокола — включённые агенты, настроенные breakpoints. inspector.close() блокирует, пока активные соединения inspector не закроются, затем деактивирует конечную точку. Это состояние уровня процесса — библиотечный код не должен открывать/закрывать inspector без явного контракта с вызывающим.
Promise API и callback API делят один бэкенд. Выберите один на путь вызова. Promise API experimental в Node v24, но читается лучше в startup-пробах и одноразовых скриптах:
1 2 3 4 5 6 | |
Callback API уместен в старом коде или там, где уже есть контракт колбэка. В любом случае session.post() шлёт строки методов Inspector Protocol. Опечатка в имени метода — ошибка протокола. Неверная форма параметров — решение бэкенда.
Программный код inspector должен иметь очевидный жизненный цикл. Открыть конечную точку, если просили. Подключить сессию. Отправить узкий набор команд. Отключить. Закрыть конечную точку, если ваш код её открывал. Оставленная конечная точка после диагностики меняет экспонированное состояние процесса.
Ещё одна граница — между inspector и observability. Сессия inspector может запросить у V8 профили и heap snapshot. Модуль inspector всё равно клиент протокола. Хранение, политика сэмплирования, фильтрация приватности, пути загрузки, retention и production runbook — в других главах. Полезный навык здесь — назвать момент, когда вы открыли конечную точку отладчика или отправили команду протокола, и когда перешли к эксплуатационной теме.
Watch mode¶
Watch mode перезапускает процесс Node при изменении отслеживаемых файлов.
1 | |
Флаг --watch запускает entrypoint в встроенном цикле перезапуска Node. В Node v24 watch mode по умолчанию следит за entrypoint и за всеми require/import модулями. При изменении одного из них Node останавливает текущий запуск и стартует снова.
Знакомый сбой: dev-сервер крутит старый код, потому что никто не перезапустил, или перезапускается слишком часто из-за сгенерированных файлов в наборе наблюдения. Встроенный watch mode закрывает первый случай. Выбор путей — второй.
1 | |
--watch-path задаёт пути для наблюдения и включает watch mode. С ним Node следит за этими путями вместо автообнаруженных require/import модулей. В Node v24 поддержка --watch-path ограничена macOS и Windows. На платформах без поддержки Node бросает ERR_FEATURE_UNAVAILABLE_ON_PLATFORM.
Watch mode привязан к файлу entrypoint:
1 2 | |
Исключённые комбинации: --check, --eval, --interactive и REPL. --watch-path также исключает --test. --run имеет приоритет и игнорирует watch mode. --watch без пути к файлу — выход с кодом 9.
Сигнал перезапуска watch — сигнал, которым Node останавливает текущий процесс перед следующим запуском. В Node v24.4.0 добавлен --watch-kill-signal:
1 | |
Сигнал меняет shutdown, потому что процесс по-разному обрабатывает SIGINT, SIGTERM и другие поддерживаемые сигналы (сигналы и коды выхода). Узкий watch-пункт: перезапуск — это завершение процесса плюс свежий старт. Любое состояние в памяти исчезает. Путь cleanup зависит от сигнала и обработчиков приложения.
По умолчанию watch mode очищает консоль между перезапусками. Сохранение вывода:
1 | |
Полезно, когда строка с причиной сбоя перезапуска ушла при прокрутке или очистке. Сохранённый вывод шумит при быстрых правках — выбирайте режим под текущую отладку.
Watch mode опирается на наблюдение за файлами; глава о наблюдении за файлами уже объясняла, почему поведение различается по платформам. Редакторы сохраняют через временный файл и rename. Сборки переписывают каталоги. Сетевые ФС задерживают или сливают события. Watch mode Node сидит поверх этих сигналов ОС и превращает обнаруженное изменение в решение о перезапуске.
У цикла перезапуска две стороны. Одна отслеживает файлы. Другая владеет запущенным процессом. При изменении watcher шлёт настроенный сигнал перезапуска, ждёт shutdown и запускает новый прогон с тем же entrypoint и аргументами выполнения. Новый прогон — свежая куча JavaScript, свежие кэши модулей, новая последовательность старта. Внешнее состояние остаётся внешним: порты, файлы, БД, сокеты и дочерние процессы следуют своему cleanup.
Отсюда сбои перезапуска. Сервер, закрывающий listening socket на SIGINT, обычно перезапускается чисто. Сервер, игнорирующий сигнал перезапуска, может держать порт занятым, и watch mode ждёт завершения. Дочерний процесс, если приложение его не отслеживает и не завершает, может пережить родителя. Watch mode стартует следующий прогон; ваш shutdown-код решает, что оставляет предыдущий.
Сгенерированные файлы заслуживают внимания. src/server.js импортирует src/routes.js, сборка дважды пишет src/routes.generated.js за сохранение — дефолтный --watch видит импортированные модули и перезапускается. --watch-path=src видит и исходники, и generated. Частота перезапусков — от событий ФС, которые получил Node. Держите generated вне watched-путей или сужайте каталог.
Есть граница entrypoint. Watch mode перезапускает программу Node. Свежий старт процесса — механизм. Код, рассчитывающий на сохранение состояния в памяти между правками, — другой дизайн. Для обычной backend-разработки перезапуск с нуля практичен: снова проходят баги старта, побочные эффекты модулей и загрузка конфигурации.
NODE_OPTIONS и флаги CLI применяются на каждом перезапуске:
1 2 | |
Каждый новый прогон получает source maps и preload. Если preload меняется и попадает в watched-граф модулей или путь --watch-path, процесс перезапускается с новым preload. Если preload вне наблюдаемого набора — зависит от выбранных флагов.
Watch mode и терминал дают мелкие сбои. Очистка вывода может спрятать первый stack trace после быстрого перезапуска. Сохранение вывода оставляет три неудачных старта на экране — текущий сложнее найти. Синтаксическая ошибка может сразу завершить новый прогон, пока watcher жив и ждёт следующую правку. Для локальной разработки полезно, что команда watch остаётся после битого сохранения.
Список файлов выводится из рантайма, если вы не задали watch paths. Импортированные модули попадают в набор после загрузки. Ленивые импорты — позже. Файл, прочитанный через fs.readFile(), — данные, не модуль; дефолтный watch может его не отслеживать. --watch-path, когда данные должны триггерить перезапуск — с лимитом платформы в Node v24.
Тонкое взаимодействие с тестами: у test runner свой watch, --watch-path не сочетается с --test. Разделение владения перезапуском: watch приложения перезапускает entrypoint; watch тестов — перезапускает тесты. Отладка сервера и тестов одновременно — две команды рантайма, а не один процесс на всё.
Watch mode не даёт транзакционной гарантии вокруг сохранения. Редактор может записать половину файла, переименовать, затем дописать связанное через миллисекунды — watcher перезапускает между записями. TypeScript stripper, bundler или генератор делают то же с выходными файлами. Исправление часто вне Node: атомарная запись generated, partial-файлы вне watched-путей, наблюдение за финальным каталогом сборки.
Watch mode — инструмент разработки. Process manager владеет supervision, backoff, политикой логов, health checks и порядком boot. Встроенный watch mode владеет «правка → перезапуск» локально.
Single Executable Applications (SEA)¶
Single executable application (SEA) — исполняемый файл Node с внедрённым blob подготовки приложения. В Node v24 поток SEA: bundled CommonJS-скрипт → blob подготовки через Node → внедрение в копию бинарника Node → при запуске выполняется встроенный скрипт.
Минимальная конфигурация:
1 2 3 4 | |
Это конфигурация SEA. main — bundled-скрипт. output — имя blob подготовки. Запуск с флагом v24:
1 | |
Выходной файл — blob подготовки SEA: бинарная нагрузка от Node по конфигурации. Может содержать bundled-скрипт, встроенные assets, необязательные execution arguments, необязательный V8 code cache и необязательные данные startup snapshot.
Генерацию blob нужно делать той же версией Node, что будет потреблять внедрение. Данные подготовки привязаны к формату бинарника и рантайму. Blob от одной версии — неверный вход для другого бинарника Node. Держите process.version шага сборки и скопированного исполняемого файла согласованными.
Blob всё равно нужно внедрить в исполняемый файл. В Node v24 шаг внедрения отдельный. Инструмент вроде postject пишет blob в копию бинарника Node под именем ресурса NODE_SEA_BLOB, затем переключает fuse-строку SEA Node, чтобы исполняемый файл знал о blob.
Важнее последовательности команд трассировка рантайма:
1 2 3 4 5 6 | |
В Windows blob внедряется как PE resource. В macOS — как секция Mach-O в сегменте NODE_SEA. В Linux — как ELF note. Детали формата файла — для инструментов упаковки. Факт рантайма проще: при старте проверяется NODE_SEA_BLOB; если есть — Node идёт по встроенному приложению.
Старт как у любого Node: ОС запускает бинарник, передаёт argv и окружение, Node инициализирует состояние. Путь SEA ответвляется при проверке маркера и blob. Без blob бинарник ведёт себя как обычный node. С blob — метаданные встроенного приложения и main вместо entrypoint-файла из командной строки.
Пользовательские аргументы остаются. В примерах execution arguments process.argv[0] и process.argv[1] указывают на путь исполняемого файла, пользовательские аргументы с индекса 2. process.execArgv содержит execution arguments из конфигурации SEA и разрешённых механизмов расширения. Форма старта близка к обычному Node, но код, ожидающий process.argv[1] как путь к исходнику, получит путь к исполняемому файлу.
SEA — упаковка. Bundling — отдельный шаг сборки. В Node v24 SEA выполняет один встроенный CommonJS-скрипт. Граф зависимостей нужно собрать в один файл до шага SEA, если он тянет файлы из node_modules или дерева исходников. У встроенного main require() с узким поведением: built-in модули и урезанные свойства file-based require, кроме require.main. Для file-based загрузки явно:
1 2 3 | |
Даёт file-based resolver с корнем в __filename. В main SEA __filename и module.filename равны process.execPath, __dirname — каталогу с исполняемым файлом. Исходный путь — у входа сборки, породившего blob.
Смена идентичности файла ломает тихие предположения. Шаблоны через path.join(__dirname, 'templates') — рядом с исполняемым файлом. Отчёт имени файла — исполняемый файл. Поиск корня пакета вверх от __dirname — от места установки. Некоторым CLI это нужно. Многим серверам — явные каталоги config/cache/data через обычную конфигурацию рантайма.
Assets — файлы в blob подготовки по ключам конфигурации:
1 2 3 4 5 | |
Во встроенном скрипте API SEA — node:sea:
1 2 3 4 5 6 | |
node:sea — встроенный модуль для кода внутри SEA. sea.isSea() — запуск из внедрённого приложения. sea.getAsset() — копия как строка или ArrayBuffer. sea.getAssetAsBlob() — Blob. sea.getRawAsset() — сырой ArrayBuffer из исполняемого файла без копирования. sea.getAssetKeys() — ключи assets с Node v24.8.0.
Сырые assets требуют осторожности. Буфер указывает на память секции внедрённого исполняемого файла. Документация Node предупреждает не писать в него — выравнивание и записываемость зависят от внедрения. getAsset() — когда копия приемлема. getRawAsset() — когда отказ от копии часть измеренного дизайна.
Ключи assets — имена уровня приложения. Можно использовать path-подобные строки, Node трактует их как ключи. Ключ "schema" может указывать на schema.json при генерации blob; исполняемый файл достаёт байты через sea.getAsset('schema'). Перемещение schema.json после сборки не меняет уже собранный исполняемый файл. Обновление — пересборка blob.
Полезно для небольших read-only данных: схемы, шаблоны, текст миграций, конфиг по умолчанию, сертификаты для локальной разработки, help. Плохо для изменяемого состояния — пишите в реальный путь ФС, выбранный в рантайме. Asset API читает данные, внедрённые в исполняемый файл.
Execution arguments можно запечь в конфигурацию SEA:
1 2 3 4 5 | |
При старте они применяются и попадают в process.execArgv. execArgvExtension управляет, могут ли дополнительные execution arguments прийти из NODE_OPTIONS, из CLI --node-options или ниоткуда. Дефолт "env" оставляет NODE_OPTIONS активным — может удивить, кто ожидал, что упакованный исполняемый файл игнорирует окружение родителя.
Три режима расширения. "none" — только execArgv из конфигурации SEA, NODE_OPTIONS игнорируется. "env" — NODE_OPTIONS как расширение, как при обычном старте Node. "cli" — аргумент --node-options="..." к исполняемому файлу парсится как execution arguments Node, а не пользовательские. "none" — фиксированный контракт рантайма. "env" — платформенная обёртка всё ещё владеет настройками. "cli" — явный escape hatch для продвинутых пользователей.
SEA code cache — кэш компиляции V8 в blob подготовки. При "useCodeCache": true Node компилирует встроенный main при генерации blob и сохраняет данные кэша V8. При запуске Node может использовать кэш при компиляции встроенного скрипта, снижая работу компиляции на старте. Кэш привязан к платформе и движку. Для кросс-платформенной генерации SEA ставьте useCodeCache в false.
Отдельный край: для import() в этом пути SEA code cache должен быть выключен. Если встроенный скрипт использует import(), не включайте code cache.
Поддержка startup snapshot идёт иначе. При "useSnapshot": true main выполняется при генерации blob на машине сборки. Результирующий blob содержит сериализованное состояние кучи V8. При запуске Node десериализует состояние и выполняет функцию, зарегистрированную через v8.startupSnapshot.setDeserializeMainFunction(). Ограничения snapshot — тема V8 и производительности старта позже; локальной модели достаточно: код на этапе генерации blob, куча захвачена, запуск начинается с захваченного состояния.
Генерация snapshot меняет момент побочных эффектов. По умолчанию встроенный main выполняется при запуске пользователем. Со snapshot main выполняется при генерации blob. Чтения файлов, окружения, случайных значений, дат и инициализация могут произойти на машине сборки, если не изолировать их за deserialize main function. Snapshot-friendly entrypoint разделяет подготовку кучи на сборке и пользовательскую работу при запуске.
Code cache и snapshot делят лимит переносимости. Данные соответствуют платформе и сборке Node/V8, которая их создала. Blob с одной платформы может упасть на другой с code cache или snapshot. Простые script/asset blob проще.
Привычка проверяемой сборки SEA — четыре шага. Собрать приложение в один CommonJS-файл. Сгенерировать blob той же версией Node, что скопированный бинарник. Внедрить blob в свежую копию бинарника. Запустить исполняемый файл в «пустом» каталоге без дерева исходников. Последний тест ловит случайные зависимости от ФС: если исполняемый файл всё ещё лезет в src/, node_modules/ или забытый локальный .env, контракт рантайма не тот, что вы ожидали.
Типичные ошибки конкретны. Нет ключа asset — исключение в node:sea. File-based require() — если зависимость не попала в bundle. Blob от одного бинарника Node на другом — сбой. Подписанный бинарник на macOS/Windows может потерять подпись при модификации и не пройти политику платформы до повторной подписи. Это сбои сборки и запуска вокруг исполняемого файла.
SEA меняет и механику обновлений. Замена schema.json рядом с проектом больше не обновляет встроенный asset. Нужны пересборка blob и повторное внедрение. Файл конфигурации рядом с исполняемым файлом по-прежнему обновляется, если программа читает его из ФС при запуске. Простое разделение: данные в blob — build-time; данные в ФС — launch-time.
SEA меняет предположения о файлах при деплое. Обычное Node-приложение читает относительные файлы из каталога проекта. SEA стартует от пути исполняемого файла. Assets в blob — через node:sea; файлы рядом с исполняемым — обычные файлы ФС; зависимости в исходном дереве — только через настроенный file-based resolver. Эту границу проверяйте до того, как считать SEA релизным артефактом.
Нативные аддоны можно включить как assets, но загрузка всё равно требует реального пути файла — process.dlopen() грузит нативный бинарник с ФС. Обычный паттерн: записать asset во временный файл, затем process.dlopen(). Это работа упаковки платформы; стратегия релиза — отдельная глава курса. В рантайме assets дают байты; API нативного загрузчика решают, что можно выполнить.
В Node v24 функция помечена active development. Метка важна для совместимости инструментов. Высокоуровневый путь достаточно документирован для рассуждений, но точные команды генерации, платформы и хелперы могут меняться между релизами Node. Для baseline v24 — --experimental-sea-config и внедрение.
Подпись, нотаризация, layout установщика, Docker-образы и production-каналы — темы деплоя. Платформа рантайма заканчивается поведением исполняемого файла: форма argv, встроенный main, assets, необязательные execution arguments, необязательный code cache, необязательный snapshot и изменённые семантики путей.
Границы production¶
Все эти инструменты пересекают обычные границы приложения.
REPL оценивает ввод в живом процессе. Встроенный REPL открывает всё, что вы положили в контекст. Держите локально или оборачивайте отдельным эксплуатационным дизайном.
Inspector открывает конечную точку отладки. Может оценивать выражения, ставить паузы, инспектировать память и отдавать профилирование через домены протокола. Привязка к loopback — дефолт разработки. Публичная привязка — решение об экспозиции.
Watch mode владеет перезапуском при изменениях для локальной итерации. Перезапуск с нуля и сигналы наблюдения за файлами, зависящие от платформы. Политику supervision владеет process manager.
SEA упаковывает рантайм Node и встроенную нагрузку приложения в один исполняемый файл. Меняются предположения о путях, загрузке модулей и доступе к assets. Всё равно проверяйте флаги старта, взаимодействие с окружением, ожидания ФС и платформенные шаги релиза.
Инструменты платформы рантайма важны, потому что работают вплотную к процессу. Та же близость — и граница. Приглашение, порт отладчика, цикл перезапуска и внедрённый исполняемый файл становятся частью того, как процесс стартует, останавливается, принимает ввод и экспонирует состояние.
Связанное чтение¶
- Предыдущая: TypeScript в Node.js, compile cache и снятие типов
- Далее: TCP/IP в Node.js: сокеты ОС и порты