Документация
Dev-сервер и отладка
Во время разработки NSH собирает ваше приложение при каждом сохранении. Rust-приложение (nsh.json задаёт его "root") собирается в собственный раннер, который запускает nsh run; изменение его крейта приводит к новой сборке и перезапуску приложения. TSX-приложение отправляется на каждый подключённый хост, который обновляется на месте. Это делают две команды:
nsh runсобирает приложение и запускает его: окно Mac Catalyst или AppKit, окно GTK в Linux, окно Win32 в Windows, симулятор iOS, устройство или эмулятор Android либо страницу в браузере. Для TSX она также запускает dev-сервер, к которому подключается хост разработки.nsh dev --server-onlyзапускает dev-сервер для TSX, для хостов, которые вы запускаете сами: ваше собственное приложение с development SDK, устройство в сети или несколько хостов одновременно.
# A Rust app, from its directory
nsh run # AppKit window (macOS); GTK window (Linux); Win32 window (Windows)
nsh run --target ios # iOS simulator
nsh run --target android # Android device or emulator
# A TSX app
nsh run App.tsx # AppKit window (macOS); GTK window (Linux); Win32 window (Windows)
nsh run App.tsx --target macos # AppKit window (macOS)
nsh run App.tsx --target ios # iOS simulator
nsh run App.tsx --target android # Android device or emulator
nsh dev App.tsx --server-only # server only
nsh dev также по умолчанию открывает нативный хост; мобильные хосты используют --target ios или --target android.
#nsh run
| Флаг | Описание |
|---|---|
--target <catalyst | ios | android | macos | linux | windows | gpui | web> | Где запускать. По умолчанию macos в macOS (AppKit), linux в Linux, windows в Windows. Rust-приложение запускается на всех из них, кроме gpui, который встраивает свой корень в приложение GPUI (GPUI). |
--device <name or UDID> | Используемый симулятор iOS. По умолчанию: уже запущенный, иначе новейший iPhone (запускается за вас). |
--port <port> | Порт dev-сервера. По умолчанию 8790; если он занят, используется следующий свободный порт. |
--release | Отдавать релизные сборки TSX (минифицированные, без отладочной информации, без горячих обновлений). |
--trace | Выводить в лог время каждого коммита на хосте (TSX-приложения). |
Что делает каждая цель:
catalyst(macOS) запускает хост разработки как приложение для Mac. Остановкаnsh run(Ctrl-C илиkill) закрывает его, а закрытие окна завершаетnsh run.macos(macOS) так же запускает хост разработки AppKit, десктопное приложение в окне 1200×800; Cmd+R перезагружает последнюю сборку. Требуется macOS 14 или новее (macOS).linux(Linux) так же запускает хост разработки GTK в окне 390×844: остановкаnsh runзакрывает его, закрытие окна завершаетnsh run. Требуется GTK 4.14 или новее (Linux).windows(Windows) так же запускает хост разработки Win32 в окне 1200×800; хост никогда не переживётnsh run, даже если консоль закрыта. Требуется Windows 10 1809 или новее (Windows).iosустанавливает хост разработки в симулятор, запускает его с указанием на dev-сервер и продолжает следить за изменениями. Симулятор использует сеть Mac, поэтому хост обращается к серверу по адресу127.0.0.1. Остановкаnsh run(Ctrl-C) закрывает хост, который иначе присоединился бы к следующему сеансу. Требуется Xcode.androidустанавливает APK хоста разработки (повторно только если изменился рантайм), выполняетadb reverse tcp:<port> tcp:<port>, чтобы собственный127.0.0.1устройства достучался до сервера, и запускает его. Работает с эмулятором или USB-устройством. Требуется Android SDK (ANDROID_HOME). Когдаnsh runзавершается (в том числе по Ctrl-C), он останавливает хост и снимает это перенаправление порта, чтобы хост не присоединился к следующему сеансу.
Когда nsh run завершается, как бы это ни произошло, он останавливает запущенный им хост: закрывает приложение Catalyst или хост macOS, Linux или Windows, завершает хост разработки в симуляторе или принудительно останавливает его на устройстве.
Для Rust-приложения каждая цель запускает собственный раннер приложения, собранный вместе с его крейтом: на десктопах это отладочная сборка на месте, с выводом в терминал; на iOS и Android это пакет раннера, который устанавливается. Для TSX хосты универсальные: они запускают любое приложение NSH, которое присылает сервер, поэтому для каждого приложения ничего собирать не нужно (приложению с собственным крейтом достаются хосты, собранные вместе с ним).
#nsh dev
nsh dev App.tsx
✓ built App.tsx (3.2 KB js, 9.8 KB bytecode, 21ms)
→ dev server ws://127.0.0.1:8790/ws
devices on this network: ws://192.168.1.20:8790/ws
| Флаг | Описание |
|---|---|
--port <port> | Порт dev-сервера. По умолчанию 8790 (следующий свободный, если он занят). |
--release | Отдавать релизные сборки. |
Подключайте хосты к выведенному URL:
- Ваше собственное iOS-приложение, скомпонованное с
NativeShellDev.xcframework:try app.connectDevServer(URL(string: "ws://192.168.1.20:8790/ws")!). См. Embedding in iOS apps. - Ваше собственное Android-приложение с
nsh-dev.aar:nsh.connectDevServer("ws://192.168.1.20:8790/ws")илиadb reverse tcp:8790 tcp:8790иws://127.0.0.1:8790/ws. См. Embedding in Android apps.
Одновременно можно подключить любое число хостов; каждая сборка отправляется всем. Хост, подключившийся позже, сразу получает текущую сборку. Хост, который не может запустить приложение, потому что в нём нет набора UI, который импортирует приложение (хост разработки, собранный для другого приложения), не получает сборку: он показывает причину, а терминал выводит строку warn с командой, которая устанавливает хост приложения.
#Изменения в Rust
Rust компилируется: изменение крейта приложения или крейта, от которого оно зависит по пути, приводит к новой сборке приложения, а nsh run перезапускает его с новым кодом. Состояние приложения начинается заново; то, что должно пережить перезапуск, храните в storage. Ошибка сборки выводится в терминал, а следующее сохранение, которое компилируется, запускается снова. В приложении с Rust и TSX правки TSX остаются горячими обновлениями (см. ниже), а правки Rust перезапускают приложение.
#Горячая замена модулей
Development-сборки TSX хранят по одному модулю на каждый исходный файл. При сохранении NSH пересобирает проект, сравнивает модули с последней успешной сборкой и отправляет патч только с изменившимися модулями. Хост применяет его на месте существенно быстрее миллисекунды; от сохранения файла до появления изменения проходит примерно 120 мс.
Что происходит с вашим приложением:
- Заново отрисовываются только отредактированные компоненты. Каждый компонент верхнего уровня (функция с заглавной буквы, возвращающая JSX) обёрнут в стабильный прокси. Когда его код меняется (или меняется остальной код его модуля, который он может использовать), его экземпляры отрисовываются заново с новым кодом; компоненты, код которых не менялся, сохраняют свои экземпляры, а вместе с ними сигналы и состояние. Измените стиль кнопки, и экран останется на месте, с тем же введённым текстом и той же позицией прокрутки.
- Изменившиеся модули выполняются заново, как и модули, которые импортируют изменившийся модуль и не могут принять обновление сами, вплоть до входной точки. Поэтому код уровня модуля выполняется заново, кроме того, что сохраняет своё состояние:
- Сигналы и стеки навигации верхнего уровня сохраняют состояние:
const, созданный черезsignal(…)илиcreateNavStack(…)в начале модуля, сохраняет своё значение (стек сохраняет свои страницы) между правками, пока его собственный инициализатор тот же; чтобы начать заново, измените инициализатор (signal(0)наsignal(10)). - Объекты
context()верхнего уровня сохраняют идентичность, получая новое значение по умолчанию, поэтому провайдеры и потребители в разных модулях продолжают соответствовать друг другу. - Редактирование входной точки не размонтирует приложение. Повторный запуск
mount(App)(илиmount(() => <App />)) с тем же компонентом сохраняет то, что было смонтировано: заново отрисовываются только отредактированные компоненты. Если отредактировать самApp, он отрисуется заново вместе с тем, что он отрисовывает (их собственное состояние начинается заново), но навигатор, созданный в начале модуля, сохраняет свои маршруты, поэтому открытый экран остаётся открытым. Изменившийся вызовmount(другой компонент, другие props) монтирует приложение заново. - Компонент, который выбрасывает исключение, сообщает об ошибке и ничего не отрисовывает, пока вы её не исправите; остальное приложение продолжает работать. Следующее удачное сохранение восстанавливает его.
Хост перезагружает всё приложение вместо применения патча, когда:
- изменение импортирует модуль NSH, который запущенное приложение ещё не импортировало (например, первый
import … from "nsh"); - изменился
capabilitiesприложения вnsh.json; - хост не запускает сборку, на которой основан патч (он только что подключился или предыдущая загрузка не удалась);
- патч невозможно применить.
--release отдаёт релизные сборки без горячих обновлений: каждое сохранение перезагружает приложение. Используйте его, чтобы проверить минифицированную сборку. Всё, что загружается с dev-сервера, --release или нет, обходит усиленную защиту рантайма (eval остаётся доступным); см. Sandbox and capabilities.
#Логи
Rust-приложение пишет логи через крейт log (log::info!, log::warn!, log::error!): nsh run показывает эти строки в своём терминале, вместе с выводом приложения, на десктопах и в симуляторе iOS.
В TSX console.log, console.info, console.debug, console.warn и console.error попадают в нативный лог с целью js. Пока хост подключён, его строки лога пересылаются в терминал, где запущен nsh run / nsh dev: ваши строки console.log как есть (с префиксом в виде имени устройства, если подключено несколько устройств), предупреждения и ошибки после имени устройства:
• iPhone 16 Pro connected, build 1
loading more after 1000
warn iPhone 16 Pro: deprecated option
error iPhone 16 Pro: uncaught exception: TypeError: cannot read property 'id' of undefined
at Row (demos/Photos.tsx:18:22)
console.log форматирует значения как консоль браузера: строки как есть, ошибки со стеком, функции по имени, объекты и массивы как JSON. Когда хост не подключён, логи попадают в лог платформы (консоль Xcode / unified log в iOS, adb logcat в Android).
Promise, который отклонён и всё ещё не имеет обработчика после завершения работы, вызвавшей отклонение (его микрозадачи выполнены), тоже записывается в лог как ошибка, так же как это делает браузер: функция async, которая выбрасывает исключение, пока её никто не ожидает через await, Promise.reject(…), который никто не перехватывает, вызов в Rust, который завершается ошибкой, когда его никто не ждёт. Строка попадает туда же, куда и console.error (в терминал, лог платформы, консоль браузера в вебе), а её стек указывает на ваши исходники:
error iPhone 16 Pro: unhandled rejection: Error: HTTP 404
at loadProfile (App.tsx:12:11)
Обработчик, добавленный позже в том же такте (const p = load(); …; p.catch(…)), считается обработкой. Ожидайте promise через await или добавляйте .catch там, где важен сбой.
Остальной вывод терминала — одна строка на шаг (✓ готово, → куда подключаться, • что сделало устройство); вывод инструментов, которые запускает NSH (cargo, сами хосты), печатается только при их сбое. nsh run -v (или NSH_VERBOSE=1) печатает всё: каждую строку лога с уровнем и источником, вывод cargo и собственный вывод хостов.
--trace добавляет стоимость каждого коммита:
perf commit: 10 ops, 151µs (list js 2µs), 8 mount items, 27 nodes
О том, что означают эти числа, см. Testing.
#Оверлей ошибок
Эти ошибки показываются красным оверлеем поверх приложения на хосте, с сообщением и стеком:
- сбой сборки (синтаксическая ошибка, неизвестный элемент, отсутствующий импорт) с фрагментом кода, указывающим на строку;
- ошибка при выполнении бандла;
- необработанное исключение в обработчике события, таймере или эффекте.
Нажмите на оверлей, чтобы скрыть его. Он также исчезает при следующей успешной перезагрузке. Сбои сборки выводятся и в терминал:
error building App.tsx failed
App.tsx
x <Buton> names nothing in scope
,-[3:11]
2 | function App() {
3 | return <Buton />;
: ^^^^^
4 | }
Бандлер удаляет типы TypeScript без их проверки; запускайте nsh check (нативный компилятор TypeScript с записями tsconfig.json nsh init; Node не нужен) или полагайтесь на редактор для поиска ошибок типов.
В релизных сборках (встраивающее приложение с релизным SDK, упакованное приложение) ошибки записываются в лог и передаются обработчику ошибок хост-приложения (NshAppView.onError, NshView.setErrorListener), но поверх приложения не показываются.
#Source maps и стек-трейсы
При бандлинге создаётся source map, объединяющая все шаги (TypeScript → JSX → бандл), и она отправляется вместе с каждой сборкой. Перед тем как стек-трейс будет выведен или показан, рантайм переписывает позиции в бандле на позиции в ваших исходниках, поэтому стеки указывают на App.tsx:2:28, а не на бандл:
error Mac Catalyst: uncaught exception: Error: tap boom
at <anonymous> (App.tsx:2:58)
nsh build записывает карту как bundle.js.map; встраивающее приложение, которое загружает бандл из папки, читает её рядом с бандлом, а подписанные пакеты тоже содержат её, если только она не хранится отдельно от пакета и не применяется через nsh symbolicate (Errors from published packages).
#Советы по отладке
- После правки ничего не обновляется: проверьте терминал на ошибку сборки; оверлей показывает её и на хосте.
- Приложение хранит старое состояние, которое вы ожидали сбросить: горячая замена намеренно сохраняет состояние неизменившихся компонентов, сигналов верхнего уровня и навигаторов. Перезапустите через Ctrl-C и
nsh runили измените инициализатор состояния. - Запрос завершается ошибкой только в упакованной или встроенной сборке: проверьте capabilities приложения; отказы записываются в лог с ключом, который нужно добавить.
- Что-то работает медленно: воспроизведите это в headless-режиме с помощью
nsh test --trace, который печатает каждую нативную операцию над представлением, или запустите TSX-приложение с--trace. - Ошибки типов в редакторе:
nsh runиnsh devподдерживают.nsh/typesв актуальном состоянии; см. Project structure.