@webority/mobile-tools
v0.16.6
Published
Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run, per-environment launcher icons and over-the-air deploys, the same on Windows and macOS.
Maintainers
Readme
@webority/mobile-tools
The dev-time CLI for Webority React Native apps. It covers five jobs: compiling the environment config into the native projects, catching a stale node_modules before a 20-minute build, building and launching on one device, rendering per-environment launcher icons, and publishing over-the-air updates. It behaves the same on Windows (Git Bash, PowerShell, cmd) and macOS (zsh).
It is a devDependency. App code never imports it, and it adds nothing to the app bundle.
npm install --save-dev --save-exact @webority/mobile-toolsNode 24. Run every command from the app's root folder, where appsettings.json and package.json live.
Commands
prebuild
npx mobile-tools prebuild --env <Development|Staging|Production> [--platform android|ios]
npx mobile-tools prebuild --same-env [--platform android|ios]- Merges
appsettings.jsonwithappsettings.<Env>.jsonintoappsettings.Compiled.json. - Stamps
COMMIT(the short git sha, with-dirtywhen the app folder has changes other than the files prebuild writes) andBUILD_TIME(UTC, ISO 8601). - Fails, naming the key, when the merged settings lack
APP_NAME,APP_ID,ENVIRONMENT,BASE_URL,VERSION_DISPLAYorVERSION_INTERNAL. - Fails when
ENVIRONMENTdiffers from the--envgiven, whenVERSION_INTERNALis not a positive whole number, or when anAPP_ID_SUFFIXdisagrees with the environment. - Android (
android/gradle.properties): setsAPP_NAME,APP_ID,VERSION_DISPLAYandVERSION_INTERNAL. Every other line, comment and line ending stays as it was.- With flavors,
APP_IDis the base id. Each flavor'sapplicationIdSuffixadds the suffix. - Without flavors,
APP_IDis the suffixed id.
- With flavors,
- iOS:
- Sets
CFBundleDisplayNameinios/<name>/Info.plist. - In
project.pbxproj, setsPRODUCT_BUNDLE_IDENTIFIER(suffixed),MARKETING_VERSIONandCURRENT_PROJECT_VERSION. It also setsASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-<Env>when that icon set exists. - When another environment's icon set exists but this one's does not, prebuild fails, so a build never ships the wrong icon.
- Copies
<firebasePlistDir>/GoogleService-Info.<Env>.plisttoios/<name>/GoogleService-Info.plist. Without that file for the environment it fails when the app depends on@react-native-firebase/app(the Crashlytics build phase needs it), and otherwise removes the copy.
- Sets
- Firebase on Android: an app that depends on
@react-native-firebase/appfails withoutandroid/app/google-services.json, whose absence would otherwise stop the Gradle build later with a less direct error. - Over-the-air updates, only when the settings carry
OTA_UPDATE_BASE_URLand the platform's component id (OTA_ANDROID_COMPONENT_ID,OTA_IOS_COMPONENT_ID). An app without them is untouched.- Writes the expo-updates settings into
android/app/src/main/AndroidManifest.xml(expo.modules.updates.*meta-data) andios/<name>/Supporting/Expo.plist(EXUpdates*keys): enabled, the manifest URL<OTA_UPDATE_BASE_URL>/ota/manifest/<OTA_PROJECT_ID>/<component id>/<environment>, the runtime versionVERSION_INTERNAL, check on launchALWAYS, launch wait 0, and the code-signing certificate with metadatakeyidmain,algrsa-v1_5-sha256. OTA_CODE_SIGNING_CERTnames the update server's PEM certificate, relative to the app folder. It is required, because the server signs every manifest and an app without the certificate cannot verify one. A missingOTA_PROJECT_IDalso fails.- A missing
AndroidManifest.xmlorExpo.plistfails rather than being created: Xcode bundles only a plist the project lists.
- Writes the expo-updates settings into
- Without
--platform, it stamps every platform folder the app has. --same-envrecompiles the environment last compiled, which refreshes the build stamp.- Running it twice gives the same native files as running it once. A file whose content did not change is not rewritten.
| Environment | App id suffix | Android flavor |
|---|---|---|
| Development | .development | development |
| Staging | .staging | staging |
| Production | none | production |
sync
npx mobile-tools sync --check
npx mobile-tools sync
npx mobile-tools sync --stamp
npx mobile-tools hooks installsync --checkhashes the lockfile (package-lock.jsonorpnpm-lock.yaml), the dependency fields ofpackage.jsonand every file underpatches/. It compares that hash with the stamp innode_modules/.webority-install.json.- On macOS it also compares
ios/Podfile.lockwithios/Pods/Manifest.lockwhen both exist. - On a mismatch it prints the fix command and exits 1, well inside a second.
- On macOS it also compares
syncinstalls from the lockfile and writes the stamp:npm ci, orpnpm install --frozen-lockfilewhen the app haspnpm-lock.yaml.- When a package with an
android/folder was added, removed or changed version, and the app has been built before, it runsgradlew generateCodegenArtifactsFromSchema. It then deletesandroid/app/.cxxandandroid/app/build/generated/autolinking. - On macOS it runs
pod install(throughbundle execwhen the app has aGemfile) when the Pods are missing or stale.
- When a package with an
sync --stamponly records thatnode_modulesmatches the lockfile now. Run it from the app'spostinstall, so a plainnpm ciorpnpm installalso leaves an accurate stamp.hooks installwrites gitpost-mergeandpost-checkouthooks that runsync --checkafter every pull and branch switch.- It honours
core.hooksPath, and an app in a subfolder of the repo gets its own block. - A hook it did not write is kept as
<hook>.chainedand still runs first, with its exit status passed through.
- It honours
run
npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>]Android (the default):
- Runs
sync --checkand stops on a stale tree. - Lists
adbdevices. A device that answers on several serials (MuMu shows one instance as127.0.0.1:16416,127.0.0.1:7555andemulator-5556) is shown once. - Picks the device:
- the one named by
--device(any of its serials, or its model name); - otherwise the only device;
- otherwise asks, on a terminal. Without a terminal it fails with the list.
- the one named by
- Reads the device's ABI and runs
prebuild. - Builds
:app:assemble<Flavor>Debug(or:app:assembleDebugwithout flavors) with-PreactNativeArchitectures=<abi>, so only one ABI compiles. - Installs with
adb -s <serial> install -ron that device only. - Reuses a Metro already serving this app. Otherwise it starts one detached and prints the path of its log; when another project holds 8081, the new Metro takes the next free port.
- Sets
adb reverse tcp:8081(and, for Development, the local API port). It then launches<applicationId>/<namespace>.MainActivity.
iOS (macOS only):
- Picks a booted simulator or a connected iPhone (
xcrun simctl,xcrun devicectl). - Runs
prebuild. - Builds with
xcodebuild -workspace ios/<name>.xcworkspace -scheme <name> -configuration Debug -destination id=<udid> -derivedDataPath ios/build ONLY_ACTIVE_ARCH=YES. - Installs and launches the app.
- Metro must be on 8081, where the iOS app looks.
The build log goes to node_modules/.cache/mobile-tools/. On a failure only the root error is printed, with the log path. That root error is Metro's Unable to resolve module when present, otherwise Gradle's first "What went wrong" block or the compiler's error: lines.
Every program is started by absolute path with an argument list, never through a shell string:
android/gradlewon macOS.android\gradlew.baton Windows, throughcmd.exenamed by path, because Node will not start a.batfile any other way.adbfromANDROID_HOME/platform-tools, falling back toPATH./usr/bin/xcrunand/usr/bin/xcodebuild.
NoDefaultCurrentDirectoryInExePath is removed from every child's environment. On any other OS an iOS command stops with one line.
icons
npx mobile-tools icons [--source <png|svg>]Renders every environment's launcher icons from one square source: an SVG, or a PNG of at least 1024 px.
- Android, per flavor, into
android/app/src/<flavor>/res:ic_launcherandic_launcher_roundinmipmap-mdpithroughmipmap-xxxhdpi;- the adaptive
ic_launcher_foreground, withmipmap-anydpi-v26XML; ic_launcher_background, taken from the source's corner colour.- Launcher icons of the same name in another format (
.webp) are removed, because Android rejects the duplicate. - Skipped when
flavorsisfalse.
- iOS:
ios/<name>/Images.xcassets/AppIcon-<Env>.appiconset, a single 1024 px universal icon with no alpha channel. Xcode derives the other sizes. Prebuild selects the set. - Production is clean. Staging carries an orange
STAGINGribbon across the bottom-right corner, and Development a greenDEVone. The ribbon is SVG text in a generic sans-serif, with no font file. - The same source gives the same bytes on the same machine.
ota deploy
npx mobile-tools ota deploy --env <Staging|Production> [--platform android|ios] [--rollout <0-100>] [--notes <text>] [--critical]
npx mobile-tools ota deploy --completePublishes a JavaScript-only change over the air to the phones already on the current store build. A native change, a permission or an SDK bump goes through the stores instead.
- Reads every id from the app's settings for the environment, never from the command line:
OTA_ANDROID_COMPONENT_IDandOTA_IOS_COMPONENT_ID, the update server's component per platform;APP_ID, the identity the server and the app check the update against;VERSION_INTERNAL, the runtime version. An update reaches only phones whose build is exactly this number.
- Production only: asks the stores for the live build, through
webority-play release tracks getandwebority-appstore status. A platform whose live build is notVERSION_INTERNALis skipped, because the update would reach nobody (for example, while the new build is still in review).--forcepublishes anyway. A lookup that fails is reported and the platform kept.--no-store-checkskips the lookup, and--store-profile <name>picks the store CLIs' credential profile. - Per platform: runs
prebuild(which also writes the expo-updates settings, above), thenexpo export --platform <platform>intonode_modules/.cache/mobile-tools/ota/, thenweborityos ota deploywith--app-identifierand--yes. - The first deploy uploads nothing. It raises an approval card per platform and the ids are recorded in
node_modules/.cache/mobile-tools/ota/approvals.json. Once a person approves,ota deploy --completeuploads every recorded bundle.--approval-id <id>with--platformdoes the same for one platform in one run. - Prebuilds the environment that was compiled before, so a Development checkout is left as it was.
--dry-run prints the commands after the store check, and changes nothing. weborityos must be on PATH and signed in (weborityos login). Development is refused: it runs from Metro.
mobile-tools.config.json
Optional, at the app root. Every key is optional. An unknown key or a wrong type fails, naming the key.
{
"iosProjectName": "FieldApp",
"flavors": true,
"localApiPort": 62430,
"iconSource": "assets/app-icon.svg",
"firebasePlistDir": "ios/firebase"
}| Key | Default | Meaning |
|---|---|---|
| iosProjectName | the single ios/*.xcodeproj | Xcode project, scheme and workspace name |
| flavors | true | Android product flavors development, staging, production in dimension environment |
| localApiPort | the port of appsettings.Development.json's BASE_URL when it is localhost | port run reverses for a Development build |
| iconSource | none | source image for icons when --source is not given |
| firebasePlistDir | ios/firebase | folder holding GoogleService-Info.<Env>.plist |
Typical package.json scripts
{
"scripts": {
"android": "mobile-tools run --env Development",
"android:staging": "mobile-tools run --env Staging",
"ios": "mobile-tools run --env Development --platform ios",
"prebuild": "mobile-tools prebuild --same-env",
"sync": "mobile-tools sync",
"postinstall": "mobile-tools sync --stamp"
}
}postinstall keeps the stamp accurate after any install. After a pull, npx mobile-tools sync is still the one command to run: it also resets native codegen when a native package moved, which a plain install does not.
