Releases: phantom/vitest-mobile
Release list
vitest-mobile@0.4.3
Patch Changes
- 706afc9: Fix
bootstrapfailing on iOS for projects that depend onreact-native-reanimated@4. The React Native community CLI's--force-podspath callspod installwithRCT_NEW_ARCH_ENABLED='0'even when the template defaults the New Architecture to on, which trips Reanimated's podspec assertion. The harness builder now prependsENV['RCT_NEW_ARCH_ENABLED'] = '1'to the scaffolded Podfile so the env is corrected inside the same pod-install process, before any podspecs are evaluated.
vitest-mobile@0.4.2
Patch Changes
- 60e4368: Fix
bootstrapfailing on iOS for projects that depend onreact-native-reanimated@4. The React Native community CLI'spod installstep was passingRCT_NEW_ARCH_ENABLED='0'on a virgin scaffold because its New Architecture detector relies on a Pods xcodeproj that doesn't exist yet, which Reanimated's podspec asserts against. The harness builder now pre-seeds the file the detector reads, so pod install runs with the New Architecture enabled like the template expects.
vitest-mobile@0.4.1
Patch Changes
-
0124380: ### fix(harness): prevent bootstrap hang from npx install prompts
Add
--yesto everynpxcall in the harness builder so npx
auto-confirms package downloads instead of prompting. Under the
spinner, stdin is'ignore'— any prompt would hang indefinitely:-
npx --yes @react-native-community/cli init(scaffold step, runs
before the project'snode_modulesexist, so npx always needs to
resolve the package over the network) -
npx --yes react-native build-iosandnpx --yes react-native build-android(build steps; defensive — these usually resolve
locally afternpm install)While here, also switch
buildAndroidfrom a rawgradlew assembleDebugtonpx react-native build-androidfor symmetry with
iOS and to let the RN CLI handle codegen + Gradle wrapper setup.
Pass--tasks assembleDebugexplicitly: the CLI defaults its task
prefix tobundle(producing an.aab), notassemble(producing
the.apkwe need). The--helptext is misleading on this point.
-
vitest-mobile@0.4.0
Minor Changes
-
be23517: Fix component hot reload during
pause()and improve build robustness.HMR fix: Editing a component file while paused previously triggered a full
app reload instead of a live Fast Refresh update. The root cause was twofold:- Components loaded with
__ReactRefreshdisabled had no family registrations,
soperformReactRefresh()couldn't map old→new component types. - Restoring the real Refresh runtime during pause caused Metro to detect a
boundary status change ("invalidated boundary") and call
performFullRefresh().
The fix installs a registration-only shim that builds component families from
initial load (forwardingregister()) while preventing implicit self-accept
(isLikelyComponentType()→ false). During pause,performFullRefresh()is
suppressed andperformReactRefresh()is triggered manually.Build improvements:
- Use
npx react-native build-ios/build-androidinstead of raw
xcodebuild/gradlew — simplifies the build step and lets RN CLI handle
pod install, gem setup, and gradle wrapper automatically. - Detect and recover from stale build locks (killed/interrupted builds no
longer block subsequent runs indefinitely). - Clean incomplete binaries and DerivedData before rebuilding.
- Add
--verboseflag to all CLI commands (streams child-process output
instead of using a spinner).
- Components loaded with
Patch Changes
-
2035ecc: Anchor the generated Metro config's
vitest-stubs/lookup at the active
workspace'snode_modules/vitest-mobile/instead of the cached harness's.The cached harness's
node_modules/vitest-mobileis installed viafile:,
so npm creates a symlink to whichever workspace first built the cache.
Two workspaces with the same RN version + native modules + vitest-mobile
version share a cache key, and the second one would hit Metro errors likeFailed to get the SHA-1 for: <other-workspace>/node_modules/vitest-mobile/src/metro/vitest-stubs/empty.js. Potential causes: 1) The file is not watched. Ensure it is under the configured `projectRoot` or `watchFolders`.— because the symlink target lives outside the second workspace's
projectRootandwatchFolders, so Metro's file map doesn't track it.Resolving the stubs from
projectRootinstead is safe:computeCacheKey
already includes the vitest-mobile package version, so the workspace's
stubs are guaranteed to match the harness's on every run. The fix is
template-only — existing cached harness binaries continue to work
unchanged, no rebuild required.Unblocks running tests from multiple checkouts of the same repo (and
fixes CI tarball-restore scenarios where the originating workspace
isn't present on the runner). -
a3eeb10: Default the device picker to an existing simulator/AVD instead of "Create new"
The interactive device picker (used by
bootstrapandboot-device) now
defaults to an existing device rather than prompting to create a dedicated
one. On iOS the pre-selected device is the most recently booted simulator,
matching Expo CLI's heuristic; on Android it's the first available AVD."Create new dedicated simulator/AVD" is still available at the bottom of
the list for users who want isolation. The non-interactive (CI) fallback
is unchanged — it still auto-creates a project-scoped device.This prevents vitest-mobile from stealing Expo CLI's default simulator:
previously, creating and booting aVitestMobile-*sim made it macOS
Simulator.app's "most recently used" device, so Expo would target it
on the nextexpo start→ipress.
vitest-mobile@0.3.1
Patch Changes
-
846d725: Pin
install-expo-modulesto0.14.21so the Expo autolinking step
succeeds on Linux runners (Android-only CI).install-expo-modules@0.14.18(the version currently published behind
upstream'slatesttag) ships without theprocess.platform === 'darwin'
gate around its finalpod install --repo-updatestep, so on Linux it
crashes withENOENT spawn podand bootstrap aborts before
patchAppDelegateForExpocan run.0.14.21(newest stable) has the gate
restored. Switch from@latestto a pinned version so we pick up the
fix regardless of whichinstall-expo-modulesversion upstream tags as
latestnext.
vitest-mobile@0.3.0
Minor Changes
-
d348a55: The harness builder now auto-wires Expo modules autolinking whenever a user
declares anyexpo-*(orexpo, or@expo/*) entry in
nativePlugin({ harness: { nativeModules } }). This unblocks testing
components from libraries that pull Expo modules under the hood — e.g.
expo-blur,expo-haptics,expo-image, etc.Previously the harness was a vanilla React Native template (
use_native_modules!
only — nouse_expo_modules!), so listingexpo-blurinnativeModulesgot
the JS dep installed but no native pod, and JS-side renders crashed with
Cannot read property 'BlurView' of undefinedbecause
expo-modules-autolinkinghad never run.The builder now detects Expo-shaped names in
nativeModulesand runs
npx install-expo-modules@latest --non-interactiveagainst the scaffolded
project to wire up the Podfile (use_expo_modules!),settings.gradle
(useExpoModules()),MainApplication, andAppDelegate. Two
post-processing patches keep the result compatible with vitest-mobile's
own pipeline:- The CLI integration's bundle-root rename (
index→
.expo/.virtual-metro-entry) is reverted, because vitest-mobile
rewrites/index.bundlerequests onto its prebuilt bundle directly
and never consults Expo CLI's resolver. - The missing
bindReactNativeFactory(factory)call is inserted into
AppDelegate.swift. SDK 54+'sExpoAppDelegate.recreateRootView
reads its ownfactoryproperty andfatalErrors if it's unset;
install-expo-modules's Swift transform doesn't add the bind call,
but the from-scratch Expo bare template does.
Cache key bumps to
fmt6so users with v5 binaries that listed Expo modules
as deps (no autolinking pipeline) get a fresh build the next time they
bootstrap.Heuristic: a
nativeModulesentry triggers the Expo wiring when its name
matchesexpo,expo-*, or@expo/*. Modules outside that pattern still
go through the React Native community CLI's autolinking, which the
scaffolded RN template already supports. - The CLI integration's bundle-root rename (
vitest-mobile@0.2.2
Patch Changes
-
320c60b: Fix
vitest-mobile bundle(and any other CLI command that statically reads the
metro customizer from a vitest config) to look atmetro.customizeon the
plugin options.readMetroCustomizerFromConfigwas inspectingstored.metroas if it were the
customizer function itself, butnativePlugin({ metro })stashes a
MetroOptionsobject ({ bundle, customize, babelPlugins }) on the plugin
instance — so the customizer was never picked up and the bundle was built with
only the harness-anchored base resolver. Any user resolver hook (e.g. monorepo
#src/*rewrites orreact-nativecondition pinning) silently dropped on the
floor in pre-built bundles, while the in-process Vitest pool path was unaffected
because it readsoptions.metro.customizedirectly.While there: collapse the three plugin-options readers
(readNativeModulesFromConfig,readMetroCustomizerFromConfig,
readBabelPluginsFromConfig) onto a single typed extractor
(readVitestMobilePluginOptions) that returnsNativePluginOptions[]for the
matching projects. Each per-field reader is now a tiny pluck function over the
shared extractor — so a future change to the plugin-options shape fails the
type-checker in one place instead of silently dropping options in three. The
extractor also normalizes the legacy top-levelnativeModulesfield into
harness.nativeModulesso consumers stay strictly typed.Internal: also extract the readers from
cli/index.tsinto a new internal
cli/config-readers.tsmodule (no public API change) so they can be
unit-tested without dragging the cac dispatcher in. Adds regression tests
covering all readers, including the metro-customizer bug fix and the legacy
nativeModulescompat path.
vitest-mobile@0.2.1
Patch Changes
-
66f4239: Add
metro.babelPluginsoption to inject extra Babel plugins into Metro's transform pipeline.Native modules like
react-native-reanimatedrequire compile-time Babel transforms (e.g. worklet directives) that Metro won't apply unless the plugin is explicitly wired in. Previously, users had no way to add these — worklet transforms were silently skipped in both watch mode and pre-built bundles.New option:
metro.babelPlugins.nativePlugin({ harness: { nativeModules: ['react-native-reanimated'] }, metro: { babelPlugins: ['react-native-reanimated/plugin'] }, });
Plugins are resolved from the harness project's
node_modulesand injected into the generated Metro transformer shim. They run before vitest-mobile's own plugins so worklet transforms etc. are applied before the test wrapper inspects the output. Works in both live Metro (watch mode) andbundlepre-builds.Auto-injection for known modules. When a native module listed in
harness.nativeModuleshas a well-known companion Babel plugin (currently justreact-native-reanimated→react-native-reanimated/plugin), the harness builder automatically adds it tobabel.config.jsduringbootstrap.CLI plumbing. The
bundlecommand now readsmetro.babelPluginsfrom the vitest config and passes them through to the bundler, so pre-built bundles match live-Metro output.
vitest-mobile@0.2.0
Minor Changes
-
fd223b8: Restructure plugin options and rewrite tye device-side runtime.
Breaking: plugin options
nativePlugin(...)options are now nested intoharness/device/metrogroups:// Before nativePlugin({ platform: 'ios', headless: true, bundle: true, metro: customizerFn }); // After nativePlugin({ platform: 'ios', device: { headless: true }, metro: { bundle: true, customize: customizerFn } });
Removed
promptForNewDeviceandskipIfUnavailable. Missing environments now throw instead of soft-skipping.Runtime rewrite
setup.ts,ControlBridge,TestRegistry,TestRunnerService,run.ts,state.tscollapsed into a singleHarnessRuntimeroot that owns connection, state, registry diff, and RPC forwarding.- Generated test-registry module replaced by
require.contextintest-context.ts(backed by newinline-app-root-plugin). - Explorer UI walks Vitest's native task tree directly; old
TestTreeNodeparallel tree deleted. ReactNativeRunnersimplified to(config, runtime)— delegatesonCollected/onTaskUpdateto the runtime.- Task state (
collectedFiles,taskState) lives on the runtime instance; entries are append-only across reruns.
Other
- New
vitest-compat-plugin.tsrewrites Vitest dist for Hermes/Metro. Hermes-safenode:*stubs added. - Pool-side:
PauseController, Metro log tailing, bundle server, and pool message types extracted into dedicated modules.
-
1434ccd: Per-project device ownership, interactive device picker, and a big CLI UX polish pass.
Project-scoped devices (iOS + Android).
- Each project now owns a specific simulator / AVD, stored in
~/.cache/vitest-mobile/devices.jsonkeyed by project path. Running Expo, Android Studio, or another vitest-mobile project on the same machine no longer collides with your tests. vitest-mobile bootstrapalways shows an interactive picker: your existing simulators/AVDs plus a "Create new dedicated device" option. Current mapping is pre-selected so hitting Enter keeps your choice.--device <name>skips the picker (for CI and scripts). Non-TTY bootstrap auto-creates the project-scoped device.- On Android, "Create new" is annotated as unavailable and refused unless the Android cmdline-tools (
sdkmanager+avdmanager) are installed — with a pointer to install them or pick an existing AVD instead. reset-devicenow respects whether vitest-mobile created the device: deletes + clears the mapping if we created it, only clears the mapping if the user picked their own device.- iOS: existing
VitestMobile-<hash>simulators are auto-registered into the mapping on first run (no re-prompt on upgrade). - Concurrent runs on the same project get a per-instance secondary simulator (iOS) / emulator instance (Android).
CLI UX polish.
- Consistent
--platformflag (breaking). Every command takes--platform <ios|android>(previously some were positional, some--platform). The oldvitest-mobile build iosform prints a clear migration hint and exits non-zero. Most commands prompt (TTY) or error (non-TTY) when--platformis omitted;trim-cache/clean-devices/bundledefault to both platforms;cache-keystill requires an explicit platform. - Spinners with live step messages for
build,install,bootstrap,bundle,boot-device— instead of a wall of xcodebuild / gradle / pod-install output. Spinner now animates during long builds (the underlying spawn is async; previously a syncexecSyncblocked the event loop). - Child-process output is tee'd to
~/.cache/vitest-mobile/logs/<timestamp>-<command>-<platform>.log. On failure the log path is printed; nothing is silently swallowed. - Ctrl+C now works during long builds (same async-spawn fix — SIGINT previously went to the blocked child instead of Node).
- Unknown commands exit 1 with a help dump (previously exited 0 silently).
Metro + native modules config.
- New
nativePlugin({ metro })customizer and exportedMetroConfigCustomizertype. Layers on top of the auto-generated harness-anchored base Metro config (runs before internal test transforms, so the vitest shim and test-registry stay authoritative). Composes across multiple plugin instances. build,install,bootstrap, andbundleCLIs readnativeModulesfrom the vitest config automatically;--native-modulesoverrides when passed.
Setup diagnostics.
- Simulator creation failures now report the real cause — unaccepted Xcode license,
xcode-selectpointing at Command Line Tools, missing iOS runtime. Preflight checks beforexcodebuildcatch SDK/runtime mismatch upfront.
Build system.
- Xcode 26.4 compat: bundled
fmtpod pinned to C++17 so RCT-Folly compiles under Apple Clang 21. Harness build format bumped to v5 — existing cached binaries rebuild once.
Fixes.
promptConfirmno longer leaks a stdin resume that keptbootstrapalive after accepting the prompt.
- Each project now owns a specific simulator / AVD, stored in
Patch Changes
-
01ce6fe: Collapse the pool worker lifecycle to fire once per user-initiated run instead of once per file, by setting
test.isolate = falsein the plugin's config hook.Why. With
isolate: true(the previous default), Vitest's scheduler created onePoolRunnerper test file, meaningworker.start()andworker.stop()— and the 60s / 90s handshake timeouts guarding them — fired N times per run. The React Native harness shares a single JS VM across files anyway, so the per-file isolation was a fiction maintained by singleton idempotency flags. Underisolate: false+maxWorkers: 1, Vitest bundles every file into one task withcontext.files = [all], and the handshake timers fire exactly once per user-initiated run (initial or HMR-driven rerun). Timer scope now matches reality.Changes.
test.isolate = falseis applied by the plugin, guarded so user-level overrides win.canReuse: () => trueadded to the pool worker.- Device-side
handleRunrefactored into a per-file loop so explorer file-start/file-done UI events still fire per file; test execution insidestartTestsis unchanged. - Removed dead code: the fallback
__rerunreplay path (Vitest ^4'srerunFilesis guaranteed), the_lastRunMessagesmap,_sessionCount,countTestsInSpecs, and the triple-keying of file identifiers. Reporter shrunk to a pass-through for__native_run_start/__native_run_endplus therun-modeteardown()await.
No user-visible behavior change. Custom Vitest pools, test configs that explicitly set
test.isolate, and the device-side test execution path are all unaffected.
vitest-mobile@0.1.1
Patch Changes
- 5c06893: Includes missing files in publish