Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

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.

#См. также