tamer-dev-client

Native dev client module for Tamer4Lynx — QR scan, discovery, URL persistence, reload bridge, embedded dev launcher UI.

Overview

The dev client provides:

  • Discovery — Find dev servers on the local network (mDNS)
  • Connect — Enter URL or scan QR code to connect
  • Recent — List of recently used dev server URLs
  • Reload — Reload the Lynx bundle from the dev server
  • Compatibility check — Validates native modules between app and project; shows modal if incompatible

iOS: how the host’s native modules are known

The dev server’s meta.json lists required native modules using the same JVM-style moduleClassName strings as Android (from each package’s lynx.ext.json / tamer.json).

On iOS, Lynx does not enumerate registered modules at runtime. The host normally passes that list via DevClientModule.attachSupportedModuleClassNames(...) in generated LynxInitProcessor.swift (Tamer autolink).

Additionally, t4l link ios / t4l bundle ios / t4l build ios (via autolink) writes tamer-host-native-modules.json into ios/<AppName>/ and adds it to Copy Bundle Resources. It contains { "moduleClassNames": [ ... ] } using the same discovery as dev server meta.json (nativeModules[].moduleClassName). DevClientModule reads this file only when the set from attachSupportedModuleClassNames is still empty (e.g. custom LynxInit without the GENERATED DEV_CLIENT_SUPPORTED block). Re-run t4l link ios after adding or removing @tamer4lynx/* native packages.

Recent tab: Rows can show a cached icon (data: URL + localStorage when available) and a status dot: green when the server responds with Tamer meta.json and tamerAppKey matches the saved entry (if present); amber/gray/red for mismatch, stale meta, or offline.

Rspeedy dev server compatibility: The dev client can scan a QR code or manually enter a URL from a plain rspeedy dev server and load the bundle directly — no t4l start required. When connecting, the dev client probes in order: (1) GET /meta.json — full Tamer server; (2) GET /status — rspeedy-compatible server (shows "not serving meta.json" if hit); (3) HEAD /main.lynx.bundle — bundle directly accessible, loaded via openProjectDirect. Since rspeedy does not serve /status, probe (3) succeeds and the bundle loads directly. HMR and native module compatibility checks require t4l start.

When you build with debug (t4l build ios -d / t4l build android -d), the dev client UI is embedded and the Lynx bundle is loaded from the connected dev server. Build with release (t4l build ios -r / t4l build android -r) for an unsigned app without the dev-client shell. Production (t4l build ios -p / t4l build android -p) is signed (store-oriented) and also omits the dev client; configure signing first (t4l signing). See Getting Started and Commands.

Installation

t4l add tamer-dev-client
# or the full dev stack (dev-client + dependencies):
t4l add-dev

The CLI resolves npm’s default installable version for the package. To add the dependency by hand, use an explicit version from the registry instead of assuming latest matches the line you want. After install, run t4l link unless autolink runs it from postinstall.

Dependencies

Peer: @lynx-js/react. Bundled dependencies: @tamer4lynx/tamer-app-shell, tamer-insets, tamer-system-ui, tamer-plugin, tamer-router (routing hooks like useNavigate come from tamer-router, which re-exports react-router—you do not need a direct react-router dependency for typical usage).

Setup

Wrap your app with DevLauncherProvider:

import { root } from '@lynx-js/react'
import { FileRouter } from '@tamer4lynx/tamer-router'
import { DevLauncherProvider } from '@tamer4lynx/tamer-dev-client'
import routes from '@tamer4lynx/tamer-router/generated-routes'

root.render(
  <DevLauncherProvider>
    <FileRouter routes={routes} />
  </DevLauncherProvider>
)

API

DevLauncherProvider

Context provider for the dev launcher. Pass children (your app tree).

useDevLauncher()

Returns the dev launcher context. Throws if used outside DevLauncherProvider.

PropertyTypeDescription
urlstringCurrent dev server URL
setUrl(u: string) => voidSet URL
navigateToConnectRefMutableRefObject<(() => void) | null>Ref for navigating to Connect tab (e.g. after QR scan)
themeDevLauncherTheme | nullTheme from native (or null)
setTheme(t: DevLauncherTheme | null) => voidSet theme
recentUrlsstring[]Recently used URLs (derived from recentEntries)
recentEntriesRecentEntry[]Recent rows: url, optional label, iconUrl, tamerAppKey
recentReachabilityRecord<string, RecentReachability>Per-URL: checking | matched | mismatch | stale | offline
recentRowIconSrcRecord<string, string>Cached data: icon URL per recent row url
discoveredServersDiscoveredServer[]mDNS-discovered servers
removeRecentItem(url: string) => voidRemove a recent URL (native + UI state)
showIncompatibleModalForUrl(parsed: string) => voidRun compatibility check and open modal if missing modules
connectErrorstringLast connect error (e.g. unreachable URL)
clearConnectError() => voidClear connectError
incompatibleModalVisiblebooleanWhether compatibility modal is shown
setIncompatibleModalVisible(v: boolean) => voidShow/hide modal
incompatibleModulesRequiredModule[]Modules required by project but missing in app
refreshRecent() => voidRefresh recent URLs from native
connectToUrl(parsed: string) => voidConnect to a parsed URL, reload bundle
openProject(rawUrl: string) => voidParse URL and connect
onSelectRecent(u: string) => voidSet URL when selecting from recent list
onScanQR() => voidTrigger QR scan
parseUrl(input: string) => stringNormalize URL (tamer://http://, strip /main.lynx.bundle)

DevLauncherTheme

interface DevLauncherTheme {
  primary?: string
  primaryDark?: string
  background?: string
  surface?: string
  surfaceContainer?: string
  onSurface?: string
  onSurfaceVariant?: string
  isDark?: boolean
}

resolveTheme(theme)

Merges partial theme with FALLBACK_THEME. Use when theme may be null.

DiscoveredServer

type DiscoveredServer = {
  url: string
  name: string
  compatible?: boolean
  iconUrl?: string
  tamerAppKey?: string
}

RecentEntry

type RecentEntry = { url: string; tamerAppKey?: string; label?: string; iconUrl?: string }

RecentReachability

'checking' | 'matched' | 'mismatch' | 'stale' | 'offline' — see Recent tab behavior above.

RequiredModule

type RequiredModule = { packageName: string; moduleClassName: string }

devCall(method, data?, callback?)

Low-level native bridge. Methods:

MethodDataCallbackDescription
setDevServerUrl{ url: string }Save dev server URL
getDevServerUrl(url: string) => voidGet saved URL
getRecentUrls(urls: string[]) => voidGet recent URLs
getRecentEntries(rows: RecentEntry[]) => voidGet recent rows (when native supports it)
removeRecentUrl{ url: string }Remove one recent URL
getDiscoveredServers(servers: DiscoveredServer[]) => voidGet mDNS servers
startDiscoveryStart mDNS discovery
stopDiscoveryStop discovery
reloadWithProjectBundleReload bundle from dev server
checkServerCompatibility{ url: string }(compatible: boolean, requiredModules: unknown) => voidCheck app vs project meta.json nativeModules. On iOS, native delivers one NSArray argument; devCall normalizes to the same two-arg shape as Android. Use toRequiredModules on the second value.
scanQROpen QR scanner

toRequiredModules(raw)

Parses checkServerCompatibility callback result into RequiredModule[].

Built-in pages

The dev client ships with Connect, Recent, and Discover pages. The layout uses Tabs with titleForPath for dynamic titles. Events:

  • devclient:scanResult — QR scan result (payload: { url })
  • devclient:discoveredServers — mDNS servers (payload: { servers })

Standard module pattern

tamer-dev-client is a standard tamer module. When installed, t4l sync android (or t4l sync) reads host templates from the package (android/templates/, ios/templates/) and applies them to wire the dev client into your host app. The templates use {{PACKAGE_NAME}} and {{APP_NAME}} placeholders. (Sync is supported for Android; iOS dev client is synced via the iOS create/sync flow.)

Build behavior

  • Debug build (t4l build ios -d / t4l build android -d): If @tamer4lynx/tamer-dev-client is in your app's dependencies, the dev client is embedded. Your app becomes a dev app (QR scan, HMR).
  • Release build (t4l build ios -r / t4l build android -r): The dev client is not included. Use for production or store builds.

tamer-dev-app

tamer-dev-app is a monorepo workspace app that bundles tamer-dev-client for a reference dev launcher. It is not part of the installable npm package set for app projects. In practice you still build that app (or your own app with tamer-dev-client) with t4l build … -d after t4l link, so the native side matches your linked modules—the same reason there is not yet a single prebuilt “install everywhere” HMR app on the store.

A generalized dev launcher (one binary for typical workflows without per-project builds) is planned. Until then, treat your debug build as the HMR shell tailored to your node_modules and autolink output.