@webority/mobile-tools 0.95.0 → 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -7
- package/lib/build.js +1 -0
- package/lib/buildLog.js +1 -0
- package/lib/cache.js +1 -0
- package/lib/cli.js +1 -1
- package/lib/config.js +1 -1
- package/lib/devices.js +1 -1
- package/lib/doctor.js +1 -0
- package/lib/exec.js +1 -1
- package/lib/hooks.js +1 -1
- package/lib/icons.js +1 -1
- package/lib/ios.js +1 -1
- package/lib/metro.js +1 -1
- package/lib/otaNative.js +1 -1
- package/lib/output.js +1 -1
- package/lib/platform.js +1 -1
- package/lib/prebuild.js +1 -1
- package/lib/run.js +1 -1
- package/lib/sync.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @webority/mobile-tools
|
|
2
2
|
|
|
3
|
-
The dev-time CLI for Webority React Native apps. It covers
|
|
3
|
+
The dev-time CLI for Webority React Native apps. It covers these 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 (installing a cached build when nothing native changed), building release APKs, App Bundles and iOS simulator apps, listing devices, checking the machine and the app tree, restarting Metro, and rendering per-environment launcher icons. It behaves the same on Windows (Git Bash, PowerShell, cmd) and macOS (zsh).
|
|
4
4
|
|
|
5
5
|
It is a devDependency. App code never imports it, and it adds nothing to the app bundle.
|
|
6
6
|
|
|
@@ -69,22 +69,23 @@ npx mobile-tools hooks install
|
|
|
69
69
|
### `run`
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>]
|
|
72
|
+
npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild]
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
**Android** (the default):
|
|
76
76
|
|
|
77
77
|
1. Runs `sync --check` and stops on a stale tree.
|
|
78
|
-
2.
|
|
78
|
+
2. First runs `adb connect` for any open MuMu port (16384 + 32 n, and 7555) that `adb` does not list yet. Then lists `adb` devices. A device that answers on several serials (MuMu shows one instance as `127.0.0.1:16416`, `127.0.0.1:7555` and `emulator-5556`) is shown once.
|
|
79
79
|
3. Picks the device:
|
|
80
80
|
- the one named by `--device` (any of its serials, or its model name);
|
|
81
81
|
- otherwise the only device;
|
|
82
82
|
- otherwise asks, on a terminal. Without a terminal it fails with the list.
|
|
83
83
|
4. Reads the device's ABI and runs `prebuild`.
|
|
84
|
-
5.
|
|
85
|
-
6.
|
|
86
|
-
7.
|
|
87
|
-
8.
|
|
84
|
+
5. Computes the native fingerprint and looks for a cached build of this app, variant and ABI with that fingerprint. A hit installs the cached build and skips Gradle. A miss builds it (step 6) and stores the result. `--rebuild` skips the lookup and always builds.
|
|
85
|
+
6. On a miss or with `--rebuild`, builds `:app:assemble<Flavor>Debug` (or `:app:assembleDebug` without flavors) with `-PreactNativeArchitectures=<abi>`, so only one ABI compiles.
|
|
86
|
+
7. Installs with `adb -s <serial> install -r` on that device only.
|
|
87
|
+
8. 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. A Metro that mobile-tools started and that stops answering is restarted on the same port.
|
|
88
|
+
9. Sets `adb reverse tcp:8081` (and, for Development, the local API port). It then launches `<applicationId>/<namespace>.MainActivity`.
|
|
88
89
|
|
|
89
90
|
**iOS** (macOS only):
|
|
90
91
|
|
|
@@ -94,6 +95,8 @@ npx mobile-tools run --env <Environment> [--platform android|ios] [--device <ser
|
|
|
94
95
|
4. Installs and launches the app.
|
|
95
96
|
5. Metro must be on 8081, where the iOS app looks.
|
|
96
97
|
|
|
98
|
+
A simulator run uses the build cache the same way as Android: a hit skips `xcodebuild` and installs the cached app, a miss builds and stores it. A physical iPhone never uses the cache.
|
|
99
|
+
|
|
97
100
|
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.
|
|
98
101
|
|
|
99
102
|
Every program is started by absolute path with an argument list, never through a shell string:
|
|
@@ -104,6 +107,168 @@ Every program is started by absolute path with an argument list, never through a
|
|
|
104
107
|
|
|
105
108
|
`NoDefaultCurrentDirectoryInExePath` is removed from every child's environment. On any other OS an iOS command stops with one line.
|
|
106
109
|
|
|
110
|
+
### `build`
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--json]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
1. Stops on a stale tree, as `run` does.
|
|
117
|
+
2. Runs `prebuild` for the environment and platform.
|
|
118
|
+
3. Android: checks that `android/app/build.gradle` exists, then runs one Gradle task for the environment's flavor, the artifact and the build type, with every ABI.
|
|
119
|
+
4. iOS (`ios-sim`): runs `xcodebuild` for the iPhone simulator SDK with `CODE_SIGNING_ALLOWED=NO`, into `ios/build`.
|
|
120
|
+
5. Collects the artifact: every `.apk` or `.aab` in the Gradle output folder, or the simulator `.app`. Release builds also report the mapping file when it exists.
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
|
|
124
|
+
- `apk` and `aab` are Android release builds of every ABI unless `--debug` is given.
|
|
125
|
+
- `aab` is release only.
|
|
126
|
+
- `ios-sim` is macOS only and unsigned.
|
|
127
|
+
- Store `ipa` builds are not this command's job.
|
|
128
|
+
|
|
129
|
+
Human output, one line per artifact, then the mapping when there is one:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
build: <path to artifact>
|
|
133
|
+
build: mapping <path to mapping.txt>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`--json` prints one document:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{ "artifacts": ["<path>"], "mapping": "<path or null>", "log": "<build log path>" }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Exit codes: `0` when the artifacts exist; `1` when the build or the artifact check fails (see [Failures](#failures)); `2` for bad flags.
|
|
143
|
+
|
|
144
|
+
### `devices`
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx mobile-tools devices [--platform android|ios] [--json]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
1. Android: connects the local MuMu emulators. For each open port in 16384 + 32 n (n from 0 to 15) and 7555 that `adb` does not list as `127.0.0.1:<port>`, it runs `adb connect`. A failed connect is a warning, never an error.
|
|
151
|
+
2. Android: lists every ready device once, however many serials reach it.
|
|
152
|
+
3. iOS (macOS only): lists the booted and connected simulators and devices from `xcrun`.
|
|
153
|
+
|
|
154
|
+
Human output, one line per device:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
<serial> <model> <abi> (also <other serials, joined by ", ">)
|
|
158
|
+
<udid> <name> <kind>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The `(also ...)` part appears only when the device has more than one serial. Each port that `adb connect` reaches prints `devices: connected 127.0.0.1:<port>` before the listing.
|
|
162
|
+
|
|
163
|
+
`--json` prints one document:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"devices": [
|
|
168
|
+
{ "platform": "android", "serial": "<serial>", "serials": ["<serial>"], "model": "<model>", "abi": "<abi>" },
|
|
169
|
+
{ "platform": "ios", "udid": "<udid>", "name": "<name>", "kind": "simulator|device", "serials": [], "model": "<model>" }
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Exit codes: `0` when the listing succeeds; `1` when `adb` or `xcrun` fails (`toolFailed`); `2` for bad flags.
|
|
175
|
+
|
|
176
|
+
### `doctor`
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx mobile-tools doctor [--platform android|ios] [--fix] [--json]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Checks the toolchain and the app tree, one check per code:
|
|
183
|
+
|
|
184
|
+
- `nodeVersion`: Node is 24.
|
|
185
|
+
- `androidHome`: `ANDROID_HOME` is set and exists.
|
|
186
|
+
- `adb`: `adb` runs (from `ANDROID_HOME/platform-tools`, else `PATH`).
|
|
187
|
+
- `adbServer`: no other `adb` restarted the server at a different version.
|
|
188
|
+
- `jdk`: a Java 17 runs (from `JAVA_HOME`, else `PATH`).
|
|
189
|
+
- `ndk`: the NDK version in `android/build.gradle` is installed.
|
|
190
|
+
- `buildTools`: the build-tools version in `android/build.gradle` is installed.
|
|
191
|
+
- `androidPlatform`: the compile SDK platform in `android/build.gradle` is installed.
|
|
192
|
+
- `ninja`: on Windows, ninja 1.11 or newer is on `PATH`.
|
|
193
|
+
- `install`: `node_modules` matches the lockfile.
|
|
194
|
+
- `autolinkingCache`: the Android autolinking cache was made for this tree.
|
|
195
|
+
- `ndkHook`: `android/build.gradle` gives library modules the app's NDK.
|
|
196
|
+
- `firebaseConfig`: `android/app/google-services.json` exists, when the app uses `@react-native-firebase`.
|
|
197
|
+
- `xcode`: `xcode-select` has an Xcode (macOS).
|
|
198
|
+
- `cocoapods`: CocoaPods (`bundle exec pod` or `pod`) is on `PATH` (macOS).
|
|
199
|
+
- `rosetta`: the terminal does not run under Rosetta (macOS).
|
|
200
|
+
|
|
201
|
+
`--fix` removes the stale autolinking caches (`android/build/generated/autolinking` and `android/app/build/generated/autolinking`) and nothing else. Android checks run for `--platform android` or when `android/` exists; iOS checks run for `--platform ios`, or on macOS when `ios/` exists. `--platform ios` off macOS fails with `platformUnavailable`.
|
|
202
|
+
|
|
203
|
+
Human output, one line per check, then a summary:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
doctor: <status> <code>: <message>
|
|
207
|
+
doctor: <status> <code>: <message> Fix: <fix>
|
|
208
|
+
doctor: ready
|
|
209
|
+
doctor: <n> problem(s)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`<status>` is `ok`, `problem`, `skipped` or `fixed`. The `Fix:` part appears only on a problem.
|
|
213
|
+
|
|
214
|
+
`--json` prints one document:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{ "checks": [{ "code": "<code>", "status": "ok", "message": "<message>", "fix": "<fix, on a problem>" }], "problems": 0 }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Exit codes: `0` when no problem remains; `1` while a problem remains (no error line is printed); `2` for bad flags. A missing `android/` folder under `--platform android` fails with `appConfigInvalid`.
|
|
221
|
+
|
|
222
|
+
### `cache`
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx mobile-tools cache list|clear [--json]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Each `run` and each simulator `run` stores its build here, keyed by the native fingerprint, so a JS-only change still hits. The cache keeps the newest 5 builds per app, variant and ABI, in:
|
|
229
|
+
|
|
230
|
+
- Windows: `LOCALAPPDATA/Webority/mobile-tools/builds`
|
|
231
|
+
- macOS: `~/Library/Caches/Webority/mobile-tools/builds`
|
|
232
|
+
- Linux: `XDG_CACHE_HOME` or `~/.cache`, plus `/webority/mobile-tools/builds`
|
|
233
|
+
|
|
234
|
+
Each app's builds sit under its app id, then the platform, the variant and the ABI. An Android build is an `.apk` file; an iOS build is a `.app` folder.
|
|
235
|
+
|
|
236
|
+
`cache list` prints the cache folder, then one line per build:
|
|
237
|
+
|
|
238
|
+
```text
|
|
239
|
+
cache: <folder>
|
|
240
|
+
<platform> <variant> <abi> <first 12 of fingerprint> <size in MB, one decimal> MB <storedAtUtc>
|
|
241
|
+
cache: no cached builds for <appId>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`cache clear` removes every cached build of this app and prints `cache: removed <n> build(s) of <appId>`.
|
|
245
|
+
|
|
246
|
+
`--json` for `list` prints `{ "dir": "<folder>", "appId": "<appId>", "builds": [{ "appId", "platform", "variant", "abi", "fingerprint", "file", "bytes", "storedAtUtc" }] }`; `--json` for `clear` prints `{ "appId": "<appId>", "removed": <n> }`.
|
|
247
|
+
|
|
248
|
+
Exit codes: `0` on success; `1` when the fingerprint cannot be computed (`toolFailed`) or the app has no `@expo/fingerprint` (`toolMissing`); `2` for bad flags.
|
|
249
|
+
|
|
250
|
+
### `metro restart`
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npx mobile-tools metro restart
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
1. When the Metro mobile-tools started for this app (recorded in its log folder) is still a Node process, it stops that process tree and starts Metro again on the same port.
|
|
257
|
+
2. Otherwise, when the recorded port (or 8081) is free, it starts Metro there.
|
|
258
|
+
3. Otherwise it fails: a Metro this app did not start is left alone, and another server on the port is reported.
|
|
259
|
+
|
|
260
|
+
Human output, in order:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
metro: stopped pid <pid> on port <port>
|
|
264
|
+
metro: started on port <port>, log <log path>
|
|
265
|
+
metro: ready on port <port>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The first line appears only when a Metro was stopped. There is no `--json` for this command.
|
|
269
|
+
|
|
270
|
+
Exit codes: `0` when Metro answers; `1` when the port cannot be reused (`metroUnavailable`); `2` for bad flags.
|
|
271
|
+
|
|
107
272
|
### `icons`
|
|
108
273
|
|
|
109
274
|
```bash
|
|
@@ -122,6 +287,40 @@ Renders every environment's launcher icons from one square source: an SVG, or a
|
|
|
122
287
|
- Production is clean. Staging carries an orange `STAGING` ribbon across the bottom-right corner, and Development a green `DEV` one. The ribbon is SVG text in a generic sans-serif, with no font file.
|
|
123
288
|
- The same source gives the same bytes on the same machine.
|
|
124
289
|
|
|
290
|
+
## Failures
|
|
291
|
+
|
|
292
|
+
A command that fails exits with one of these codes:
|
|
293
|
+
|
|
294
|
+
- `0`: success.
|
|
295
|
+
- `1`: a failure. Every code below except `badInput` exits 1.
|
|
296
|
+
- `2`: `badInput`, a wrong flag or usage.
|
|
297
|
+
|
|
298
|
+
The failure prints one line on stderr:
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
mobile-tools: <message> [<code>]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
With `--json`, the failure is one document on stdout instead:
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{"error":{"code":"<code>","message":"<message>"}}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
| Code | Meaning |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `badInput` | A flag or usage is wrong. Exits 2. |
|
|
313
|
+
| `appConfigInvalid` | A file of the app is missing or wrong. |
|
|
314
|
+
| `toolMissing` | A program or package is not installed or not on `PATH`. |
|
|
315
|
+
| `toolFailed` | A program ran and failed, outside a build. |
|
|
316
|
+
| `buildFailed` | Gradle or `xcodebuild` failed; the message gives the root error and the log path. |
|
|
317
|
+
| `artifactMissing` | A build succeeded but produced no expected file. |
|
|
318
|
+
| `deviceNotFound` | No ready device, or none matches `--device`. |
|
|
319
|
+
| `deviceAmbiguous` | Several devices and no terminal to ask. |
|
|
320
|
+
| `platformUnavailable` | An iOS path run off macOS. |
|
|
321
|
+
| `installStale` | `node_modules` or `ios/Pods` do not match the lockfiles. |
|
|
322
|
+
| `metroUnavailable` | Metro could not be reused, started or restarted. |
|
|
323
|
+
|
|
125
324
|
## `mobile-tools.config.json`
|
|
126
325
|
|
|
127
326
|
Optional, at the app root. Every key is optional. An unknown key or a wrong type fails, naming the key.
|
|
@@ -151,6 +350,8 @@ Optional, at the app root. Every key is optional. An unknown key or a wrong type
|
|
|
151
350
|
"scripts": {
|
|
152
351
|
"android": "mobile-tools run --env Development",
|
|
153
352
|
"android:staging": "mobile-tools run --env Staging",
|
|
353
|
+
"android:stagingApk": "mobile-tools build --env Staging --artifact apk",
|
|
354
|
+
"android:productionAab": "mobile-tools build --env Production --artifact aab",
|
|
154
355
|
"ios": "mobile-tools run --env Development --platform ios",
|
|
155
356
|
"prebuild": "mobile-tools prebuild --same-env",
|
|
156
357
|
"sync": "mobile-tools sync",
|
package/lib/build.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync,readdirSync}from"node:fs";import path from"node:path";import{buildOrFail,logDir,logStamp}from"./buildLog.js";import{gradleOutputDir,gradleTask,loadToolsConfig,mappingFile,resolveIosProjectName}from"./config.js";import{fail,isJsonOutput,say,sayJson}from"./output.js";import{gradleInvocation,requireDarwin,XCODEBUILD}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";const buildAndroid=async(i,o,a)=>{const{appRoot:r,env:t}=i;requireSynced(r);const n=loadToolsConfig(r),s=path.join(r,"android");existsSync(path.join(s,"app","build.gradle"))||fail("appConfigInvalid",`android/app/build.gradle not found in ${r}`),prebuild({appRoot:r,env:t,platform:"android"});const e=gradleTask(t,n.flavors,o,a),l=path.join(logDir(r),`gradle-${t}-${logStamp()}.log`);await buildOrFail(gradleInvocation(s,[`:app:${e}`],process.platform,process.env),s,l,`gradle ${e}`);const p=gradleOutputDir(r,t,n.flavors,o,a),d=existsSync(p)?readdirSync(p,{withFileTypes:!0}).filter(i=>i.isFile()&&i.name.endsWith(`.${o}`)).map(i=>path.resolve(p,i.name)).sort():[];0===d.length&&fail("artifactMissing",`the build produced no ${o} in ${p}`);const c=mappingFile(r,t,n.flavors);return{artifacts:d,mapping:"release"===a&&existsSync(c)?c:null,log:l}},buildIos=async(i,o)=>{requireDarwin(process.platform,"build --artifact ios-sim");const{appRoot:a,env:r}=i;requireSynced(a);const t=loadToolsConfig(a),n=resolveIosProjectName(a,t)??fail("appConfigInvalid",`no ios/ folder in ${a}`),s=path.join(a,"ios",`${n}.xcworkspace`);existsSync(s)||fail("appConfigInvalid",`ios/${n}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`),prebuild({appRoot:a,env:r,platform:"ios"});const e="release"===o?"Release":"Debug",l=path.join(logDir(a),`xcodebuild-${r}-${logStamp()}.log`);await buildOrFail({file:XCODEBUILD,args:["-workspace",path.join("ios",`${n}.xcworkspace`),"-scheme",n,"-configuration",e,"-sdk","iphonesimulator","-destination","generic/platform=iOS Simulator","-derivedDataPath",path.join("ios","build"),"CODE_SIGNING_ALLOWED=NO","build"]},a,l,"xcodebuild");const p=path.join(a,"ios","build","Build","Products",`${e}-iphonesimulator`),d=path.join(p,`${n}.app`);return existsSync(d)||fail("artifactMissing",`the build produced no ${n}.app in ${p}`),{artifacts:[d],mapping:null,log:l}};export const build=async i=>{const o=i.debug?"debug":"release",a="ios-sim"===i.artifact?await buildIos(i,o):await buildAndroid(i,i.artifact,o);if(isJsonOutput())sayJson({artifacts:a.artifacts,mapping:a.mapping,log:a.log});else{for(const i of a.artifacts)say(`build: ${i}`);null!==a.mapping&&say(`build: mapping ${a.mapping}`)}};
|
package/lib/buildLog.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync,readFileSync}from"node:fs";import path from"node:path";import{runToLog}from"./exec.js";import{fail,say}from"./output.js";import{extractRootError}from"./rootError.js";export const logDir=o=>path.join(o,"node_modules",".cache","mobile-tools");export const logStamp=()=>(new Date).toISOString().replace(/[:.]/g,"-");export const buildOrFail=async(o,t,r,e)=>{say(`${e}: building, log ${r}`);const i=Date.now();if(0!==await runToLog(o,t,r)){const o=existsSync(r)?extractRootError(readFileSync(r,"utf8")):"";fail("buildFailed",`${e} failed:\n${o}\nFull log: ${r}`)}say(`${e}: built in ${Math.round((Date.now()-i)/1e3)} s`)};
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{copyFileSync,cpSync,existsSync,mkdirSync,readdirSync,renameSync,rmSync,statSync}from"node:fs";import{createRequire}from"node:module";import{homedir}from"node:os";import path from"node:path";import{pathToFileURL}from"node:url";import{baseAppId}from"./config.js";import{fail,isJsonOutput,say,sayJson}from"./output.js";import{pathFor}from"./platform.js";export const CACHE_KEEP=5;const ARTIFACT_SUFFIXES=[".apk",".app"],BYTES_PER_MB=1048576;export const buildCacheDir=(t,e,o)=>{const r=pathFor(e);return"win32"===e?t.LOCALAPPDATA?r.join(t.LOCALAPPDATA,"Webority","mobile-tools","builds"):fail("toolFailed","LOCALAPPDATA is not set; the build cache lives under it"):"darwin"===e?r.join(o,"Library","Caches","Webority","mobile-tools","builds"):r.join(t.XDG_CACHE_HOME||r.join(o,".cache"),"webority","mobile-tools","builds")};const defaultRoot=()=>buildCacheDir(process.env,process.platform,homedir()),resolveOrUndefined=t=>{try{return t()}catch{return}},fingerprintModuleFile=t=>{const e=createRequire(path.join(t,"package.json"));return resolveOrUndefined(()=>e.resolve("@expo/fingerprint"))??resolveOrUndefined(()=>createRequire(e.resolve("expo/package.json")).resolve("@expo/fingerprint"))};export const nativeFingerprint=async(t,e)=>{const o=fingerprintModuleFile(t)??fail("toolMissing","@expo/fingerprint not found in this app (it ships with expo); run npx mobile-tools sync"),r=await(import(pathToFileURL(o).href)),i=r.createProjectHashAsync??r.default?.createProjectHashAsync;let s;try{s=await(i?.(t,{platforms:[e],silent:!0}))}catch(t){return fail("toolFailed",`the native fingerprint could not be computed: ${t.message}`)}return"string"==typeof s&&/^[0-9a-f]{40,64}$/.test(s)?s:fail("toolFailed",`the native fingerprint could not be computed: ${String(s)}`)};const abiDir=(t,e)=>path.join(t,e.appId,e.platform,e.variant,e.abi),artifactName=t=>`${t.fingerprint}${"ios"===t.platform?".app":".apk"}`;export const findCachedBuild=(t,e=defaultRoot())=>{const o=path.join(abiDir(e,t),artifactName(t));return existsSync(o)?o:void 0};const names=t=>existsSync(t)&&statSync(t).isDirectory()?readdirSync(t):[],isArtifact=t=>ARTIFACT_SUFFIXES.some(e=>t.endsWith(e))&&!t.includes(".tmp-"),storedNewestFirst=t=>names(t).filter(isArtifact).map(e=>{const o=path.join(t,e);return{file:o,mtimeMs:statSync(o).mtimeMs}}).sort((t,e)=>e.mtimeMs-t.mtimeMs);export const storeCachedBuild=(t,e,o=defaultRoot())=>{const r=abiDir(o,t);mkdirSync(r,{recursive:!0});const i=path.join(r,artifactName(t)),s=`${i}.tmp-${process.pid}`;rmSync(s,{recursive:!0,force:!0}),statSync(e).isDirectory()?cpSync(e,s,{recursive:!0}):copyFileSync(e,s),rmSync(i,{recursive:!0,force:!0}),renameSync(s,i);for(const t of storedNewestFirst(r).slice(5))rmSync(t.file,{recursive:!0,force:!0});return i};const folderBytes=t=>readdirSync(t,{withFileTypes:!0}).reduce((e,o)=>{const r=path.join(t,o.name);return e+(o.isDirectory()?folderBytes(r):statSync(r).size)},0),listBuilds=(t,e)=>{const o=[];for(const r of names(e))for(const i of names(path.join(e,r)))for(const s of names(path.join(e,r,i))){const n=path.join(e,r,i,s);for(const e of names(n).filter(isArtifact)){const a=path.join(n,e),c=statSync(a);o.push({appId:t,platform:r,variant:i,abi:s,fingerprint:path.parse(e).name,file:a,bytes:c.isDirectory()?folderBytes(a):c.size,storedAtUtc:c.mtime.toISOString()})}}return o.sort((t,e)=>e.storedAtUtc.localeCompare(t.storedAtUtc))};export const cache=async t=>{const e=baseAppId(t.appRoot),o=defaultRoot(),r=path.join(o,e);if("clear"===t.action){const t=listBuilds(e,r).length;return rmSync(r,{recursive:!0,force:!0}),void(isJsonOutput()?sayJson({appId:e,removed:t}):say(`cache: removed ${t} build(s) of ${e}`))}const i=listBuilds(e,r);if(isJsonOutput())sayJson({dir:o,appId:e,builds:i});else if(say(`cache: ${o}`),0!==i.length)for(const t of i){const e=(t.bytes/1048576).toFixed(1);say(`${t.platform} ${t.variant} ${t.abi} ${t.fingerprint.slice(0,12)} ${e} MB ${t.storedAtUtc}`)}else say(`cache: no cached builds for ${e}`)};
|
package/lib/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{parseArgs}from"node:util";import{parseEnvironment}from"./config.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.js";import{CliError,fail,say,warn}from"./output.js";import{prebuild}from"./prebuild.js";import{run}from"./run.js";import{checkSync,sync,writeStamp}from"./sync.js";const USAGE="mobile-tools <command> [options] (run from the app's root folder)\n\n prebuild --env <Development|Staging|Production> [--platform android|ios]\n prebuild --same-env [--platform android|ios]\n Compile appsettings into appsettings.Compiled.json and stamp the native projects.\n\n sync [--check | --stamp]\n --check: exit 1 with the fix command when node_modules (or ios/Pods) is stale.\n --stamp: record that node_modules matches the lockfile now (run it from postinstall).\n Without either: install from the lockfile, stamp, and reset native codegen when needed.\n\n hooks install\n Add git post-merge and post-checkout hooks that run sync --check.\n\n run --env <Environment> [--platform android|ios] [--device <serial|name>]\n Build for one device's ABI, install it there, reuse or start Metro, and launch.\n\n icons [--source <png|svg>]\n Render per-environment launcher icons for Android flavors and iOS AppIcon-<Env> sets.",parse=(e,o,
|
|
2
|
+
import{parseArgs}from"node:util";import{build}from"./build.js";import{cache}from"./cache.js";import{parseEnvironment}from"./config.js";import{devices}from"./devices.js";import{doctor}from"./doctor.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.js";import{restartMetro}from"./metro.js";import{CliError,fail,reportFailure,say,setJsonOutput,warn}from"./output.js";import{prebuild}from"./prebuild.js";import{run}from"./run.js";import{checkSync,sync,writeStamp}from"./sync.js";const USAGE="mobile-tools <command> [options] (run from the app's root folder)\n\n prebuild --env <Development|Staging|Production> [--platform android|ios]\n prebuild --same-env [--platform android|ios]\n Compile appsettings into appsettings.Compiled.json and stamp the native projects.\n\n sync [--check | --stamp]\n --check: exit 1 with the fix command when node_modules (or ios/Pods) is stale.\n --stamp: record that node_modules matches the lockfile now (run it from postinstall).\n Without either: install from the lockfile, stamp, and reset native codegen when needed.\n\n hooks install\n Add git post-merge and post-checkout hooks that run sync --check.\n\n run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild]\n Build for one device's ABI (or install the cached build with the same native fingerprint), install it there, reuse or start Metro, and launch. --rebuild skips the cache.\n\n build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--json]\n Prebuild, then build a release APK or App Bundle (Android, every ABI) or an iOS simulator app (macOS only).\n --debug builds the debug variant (apk or ios-sim only).\n\n devices [--platform android|ios] [--json]\n Connect local MuMu emulators, then list every ready device once.\n\n doctor [--platform android|ios] [--fix] [--json]\n Check the toolchain and the app tree; exit 1 while a problem remains. --fix removes a stale autolinking cache.\n\n cache list|clear [--json]\n List or remove this app's cached builds.\n\n metro restart\n Stop the Metro mobile-tools started for this app and start it again on the same port.\n\n icons [--source <png|svg>]\n Render per-environment launcher icons for Android flavors and iOS AppIcon-<Env> sets.",parse=(e,o,t=!1)=>{try{const{values:n,positionals:a}=parseArgs({args:e,options:o,allowPositionals:t,strict:!0});return{values:n,positionals:a}}catch(e){return fail("badInput",`${e.message}\n\n${USAGE}`)}},envOption=e=>"string"==typeof e?parseEnvironment(e):void 0,platformOption=e=>{if(void 0!==e)return"android"!==e&&"ios"!==e?fail("badInput",`--platform must be android or ios, not ${String(e)}`):e},JSON_COMMANDS=new Set(["build","cache","devices","doctor"]),artifactOption=e=>"apk"===e||"aab"===e||"ios-sim"===e?e:fail("badInput","build needs --artifact apk, aab or ios-sim"),main=async e=>{const[o,...t]=e,n=process.cwd();switch(setJsonOutput(JSON_COMMANDS.has(o??"")&&t.includes("--json")),o){case"prebuild":{const{values:e}=parse(t,{env:{type:"string"},"same-env":{type:"boolean"},platform:{type:"string"}});return void prebuild({appRoot:n,env:envOption(e.env),sameEnv:!0===e["same-env"],platform:platformOption(e.platform)})}case"sync":{const{values:e}=parse(t,{check:{type:"boolean"},stamp:{type:"boolean"}});if(e.check&&e.stamp&&fail("badInput","use --check or --stamp, not both"),e.stamp)return writeStamp(n),void say("sync --stamp: recorded node_modules as matching the lockfile");if(e.check){const e=checkSync(n,process.platform);return e.length>0&&fail("installStale",e.join("\n")),void say("sync --check: node_modules matches the lockfile")}return void sync(n,process.platform)}case"hooks":{const{positionals:e}=parse(t,{},!0);return"install"===e[0]&&1===e.length||fail("badInput",`usage: mobile-tools hooks install\n\n${USAGE}`),void installHooks(n)}case"run":{const{values:e}=parse(t,{env:{type:"string"},platform:{type:"string"},device:{type:"string"},rebuild:{type:"boolean"}}),o=envOption(e.env)??fail("badInput","run needs --env <Development|Staging|Production>");return void await run({appRoot:n,env:o,platform:platformOption(e.platform)??"android",device:"string"==typeof e.device?e.device:void 0,rebuild:!0===e.rebuild})}case"build":{const{values:e}=parse(t,{env:{type:"string"},artifact:{type:"string"},debug:{type:"boolean"},json:{type:"boolean"}}),o=envOption(e.env)??fail("badInput","build needs --env <Development|Staging|Production>"),a=artifactOption(e.artifact);return!0===e.debug&&"aab"===a&&fail("badInput","--debug builds an apk or an ios-sim app; an App Bundle is release only"),void await build({appRoot:n,env:o,artifact:a,debug:!0===e.debug})}case"devices":{const{values:e}=parse(t,{platform:{type:"string"},json:{type:"boolean"}});return void await devices({appRoot:n,platform:platformOption(e.platform)??"android"})}case"doctor":{const{values:e}=parse(t,{platform:{type:"string"},fix:{type:"boolean"},json:{type:"boolean"}});return void await doctor({appRoot:n,platform:platformOption(e.platform),fix:!0===e.fix})}case"cache":{const{positionals:e}=parse(t,{json:{type:"boolean"}},!0),o=e[0];return(1!==e.length||"list"!==o&&"clear"!==o)&&fail("badInput",`usage: mobile-tools cache list|clear\n\n${USAGE}`),void await cache({appRoot:n,action:o})}case"metro":{const{positionals:e}=parse(t,{},!0);return"restart"===e[0]&&1===e.length||fail("badInput",`usage: mobile-tools metro restart\n\n${USAGE}`),void await restartMetro(n)}case"icons":{const{values:e}=parse(t,{source:{type:"string"}});return void await icons({appRoot:n,source:"string"==typeof e.source?e.source:void 0})}case void 0:case"help":case"--help":case"-h":return void say(USAGE);default:fail("badInput",`unknown command "${o}"\n\n${USAGE}`)}};main(process.argv.slice(2)).catch(e=>{e instanceof CliError&&(reportFailure(e),process.exit(e.exitCode)),warn(e instanceof Error?e.stack??e.message:String(e)),process.exit(1)});
|
package/lib/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const ENVIRONMENTS=["Development","Staging","Production"];const SUFFIX={Development:".development",Staging:".staging",Production:""};export const CONFIG_FILE="mobile-tools.config.json";export const COMPILED_FILE="appsettings.Compiled.json";export const REQUIRED_SETTINGS=["APP_NAME","APP_ID","ENVIRONMENT","BASE_URL","VERSION_DISPLAY","VERSION_INTERNAL"];export const parseEnvironment=o=>ENVIRONMENTS.find(t=>t.toLowerCase()===o.toLowerCase())??fail(
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const ENVIRONMENTS=["Development","Staging","Production"];const SUFFIX={Development:".development",Staging:".staging",Production:""};export const CONFIG_FILE="mobile-tools.config.json";export const COMPILED_FILE="appsettings.Compiled.json";export const REQUIRED_SETTINGS=["APP_NAME","APP_ID","ENVIRONMENT","BASE_URL","VERSION_DISPLAY","VERSION_INTERNAL"];export const parseEnvironment=o=>ENVIRONMENTS.find(t=>t.toLowerCase()===o.toLowerCase())??fail("badInput",`unknown environment "${o}": use ${ENVIRONMENTS.join(", ")}`);export const envSuffix=o=>SUFFIX[o];export const flavorOf=o=>o.toLowerCase();export const applicationId=(o,t)=>`${o}${envSuffix(t)}`;export const gradleAppId=(o,t,n)=>n?o:applicationId(o,t);const capitalize=o=>`${o[0].toUpperCase()}${o.slice(1)}`;export const variantName=(o,t,n)=>t?`${flavorOf(o)}${capitalize(n)}`:n;export const gradleTask=(o,t,n,e)=>`${"apk"===n?"assemble":"bundle"}${capitalize(variantName(o,t,e))}`;export const gradleOutputDir=(o,t,n,e,i)=>{const s=path.join(o,"android","app","build","outputs");return"aab"===e?path.join(s,"bundle",variantName(t,n,i)):n?path.join(s,"apk",flavorOf(t),i):path.join(s,"apk",i)};export const mappingFile=(o,t,n)=>path.join(o,"android","app","build","outputs","mapping",variantName(t,n,"release"),"mapping.txt");const readJson=o=>{try{return JSON.parse(readFileSync(o,"utf8"))}catch(t){return fail("appConfigInvalid",`cannot read ${o}: ${t.message}`)}},isRecord=o=>"object"==typeof o&&null!==o&&!Array.isArray(o),CONFIG_KEYS={iosProjectName:"string",flavors:"boolean",localApiPort:"port",iconSource:"string",firebasePlistDir:"string"};export const parseToolsConfig=(o,t=CONFIG_FILE)=>{if(!isRecord(o))return fail("appConfigInvalid",`${t} must hold a JSON object`);for(const[n,e]of Object.entries(o)){const o=Object.hasOwn(CONFIG_KEYS,n)?CONFIG_KEYS[n]:void 0;if(!o)return fail("appConfigInvalid",`${t}: unknown key "${n}" (known: ${Object.keys(CONFIG_KEYS).join(", ")})`);if(!("port"===o?Number.isInteger(e)&&e>0&&e<65536:"string"===o?"string"==typeof e&&""!==e.trim():typeof e===o)){fail("appConfigInvalid",`${t}: "${n}" must be ${{port:"a port number",string:"a non-empty string",boolean:"true or false"}[o]}`)}}return{iosProjectName:o.iosProjectName,flavors:o.flavors??!0,localApiPort:o.localApiPort,iconSource:o.iconSource,firebasePlistDir:o.firebasePlistDir??path.join("ios","firebase")}};export const loadToolsConfig=o=>{const t=path.join(o,CONFIG_FILE);return parseToolsConfig(existsSync(t)?readJson(t):{})};export const resolveIosProjectName=(o,t)=>{if(t.iosProjectName)return t.iosProjectName;const n=path.join(o,"ios");if(!existsSync(n))return;const e=readdirSync(n).filter(o=>o.endsWith(".xcodeproj"));return 1!==e.length?fail("appConfigInvalid",`found ${e.length} .xcodeproj folders in ios/: set "iosProjectName" in ${CONFIG_FILE}`):e[0].slice(0,-10)};const APP_ID_PATTERN=/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;export const mergeAppSettings=(o,t,n)=>{if(!isRecord(o))return fail("appConfigInvalid","appsettings.json must hold a JSON object");if(!isRecord(t))return fail("appConfigInvalid",`appsettings.${n}.json must hold a JSON object`);const e={...o,...t},i=REQUIRED_SETTINGS.filter(o=>void 0===e[o]||null===e[o]||""===e[o]);return i.length>0&&fail("appConfigInvalid",`appsettings (${n}) is missing required key(s): ${i.join(", ")}`),e.ENVIRONMENT!==n&&fail("appConfigInvalid",`appsettings.${n}.json sets ENVIRONMENT to ${JSON.stringify(e.ENVIRONMENT)}; it must be "${n}"`),void 0!==e.APP_ID_SUFFIX&&e.APP_ID_SUFFIX!==envSuffix(n)&&fail("appConfigInvalid",`appsettings (${n}) sets APP_ID_SUFFIX to ${JSON.stringify(e.APP_ID_SUFFIX)}; ${n} builds always use ${JSON.stringify(envSuffix(n))}. Remove the key or correct it.`),/^[1-9][0-9]*$/.test(String(e.VERSION_INTERNAL))||fail("appConfigInvalid",`VERSION_INTERNAL must be a positive whole number, not ${JSON.stringify(e.VERSION_INTERNAL)}`),APP_ID_PATTERN.test(String(e.APP_ID))||fail("appConfigInvalid",`APP_ID ${JSON.stringify(e.APP_ID)} is not a reverse-domain id such as com.example.app`),e};export const loadAppSettings=(o,t)=>{const n=path.join(o,"appsettings.json"),e=path.join(o,`appsettings.${t}.json`);return existsSync(n)||fail("appConfigInvalid",`no appsettings.json in ${o}: run mobile-tools from the app's root folder`),existsSync(e)||fail("appConfigInvalid",`appsettings.${t}.json not found: every buildable environment declares its own file`),mergeAppSettings(readJson(n),readJson(e),t)};export const baseAppId=o=>{const t=path.join(o,"appsettings.json");if(!existsSync(t))return fail("appConfigInvalid",`no appsettings.json in ${o}: run mobile-tools from the app's root folder`);const n=readJson(t),e=isRecord(n)?n.APP_ID:void 0;return"string"==typeof e&&APP_ID_PATTERN.test(e)?e:fail("appConfigInvalid",`APP_ID ${JSON.stringify(e)} in appsettings.json is not a reverse-domain id such as com.example.app`)};export const lastCompiledEnvironment=o=>{const t=path.join(o,COMPILED_FILE);if(!existsSync(t))return fail("appConfigInvalid",`--same-env: no ${COMPILED_FILE} yet; pass --env <Environment> once`);const n=readJson(t);return isRecord(n)&&"string"==typeof n.ENVIRONMENT?parseEnvironment(n.ENVIRONMENT):fail("appConfigInvalid",`--same-env: ${COMPILED_FILE} has no ENVIRONMENT`)};export const localApiPort=(o,t)=>{if(void 0!==t.localApiPort)return t.localApiPort;let n;try{n=new URL(o)}catch{return}return"localhost"===n.hostname||"127.0.0.1"===n.hostname?""!==n.port?Number(n.port):"https:"===n.protocol?443:80:void 0};
|
package/lib/devices.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createInterface}from"node:readline/promises";import{capture}from"./exec.js";import{fail}from"./output.js";export const parseAdbDevices=e=>e.split(/\r?\n/).map(e=>e.trim()).filter(e=>""!==e&&!e.startsWith("List of devices")&&!e.startsWith("*")).map(e=>{const[t,
|
|
1
|
+
import{Socket}from"node:net";import{createInterface}from"node:readline/promises";import{capture}from"./exec.js";import{listAppleDevices}from"./ios.js";import{fail,isJsonOutput,say,sayJson,warn}from"./output.js";import{adbPath,requireDarwin}from"./platform.js";export const parseAdbDevices=e=>e.split(/\r?\n/).map(e=>e.trim()).filter(e=>""!==e&&!e.startsWith("List of devices")&&!e.startsWith("*")).map(e=>{const[t,o=""]=e.split(/\s+/);return{serial:t,state:o}});const serialRank=e=>e.startsWith("emulator-")?1:e.includes(":")?2:0;export const dedupeDevices=e=>{const t=new Map;for(const o of e){const e=""===o.serialNo?`serial:${o.serial}`:`${o.serialNo}|${o.androidId}`;t.set(e,[...t.get(e)??[],o])}return[...t.values()].map(e=>{const t=e.map(e=>e.serial).sort((e,t)=>serialRank(e)-serialRank(t)||e.localeCompare(t)),o=e.find(e=>e.serial===t[0]);return{serial:t[0],serials:t,model:o.model,abi:o.abi}}).sort((e,t)=>e.serial.localeCompare(t.serial))};const PROBE_SCRIPT="getprop ro.serialno; getprop ro.product.model; getprop ro.product.cpu.abi; settings get secure android_id";export const probeDevice=(e,t,o)=>{const s=capture({file:e,args:["-s",t,"shell",PROBE_SCRIPT]},o);if(0!==s.status)return fail("toolFailed",`adb could not read ${t}: ${s.stderr.trim()||s.stdout.trim()}`);const[r="",i="",n="",a=""]=s.stdout.split(/\r?\n/).map(e=>e.trim());return{serial:t,serialNo:r,model:i,abi:n,androidId:a}};export const listAndroidDevices=(e,t)=>{const o=capture({file:e,args:["devices"]},t);if(0!==o.status)return fail("toolFailed",`adb devices failed: ${o.stderr.trim()}`);const s=parseAdbDevices(o.stdout),r=s.filter(e=>"device"!==e.state),i=s.filter(e=>"device"===e.state);if(0===i.length){const e=r.map(e=>`${e.serial} is ${e.state}`).join("; ");return fail("deviceNotFound",`no Android device ready${e?` (${e})`:""}. Connect a phone with USB debugging, start an emulator, or adb connect host:port.`)}return dedupeDevices(i.map(o=>probeDevice(e,o.serial,t)))};export const describeDevice=e=>`${e.model||"unknown model"} (${e.serial}, ${e.abi})`;export const findDevice=(e,t)=>e.find(e=>e.serials.includes(t))??e.find(e=>e.model.toLowerCase()===t.toLowerCase());export const pickDevice=async(e,t,o,s)=>{const r=e.map((e,t)=>` ${t+1}. ${o(e)}`).join("\n");if(t)return findDevice(e,t)??fail("deviceNotFound",`no device matches --device ${t}. Connected:\n${r}`);if(1===e.length)return e[0];if(!s)return fail("deviceAmbiguous",`several devices are connected; pass --device <serial|name>:\n${r}`);const i=createInterface({input:process.stdin,output:process.stdout});try{const t=await i.question(`Several devices are connected:\n${r}\nRun on which? [1-${e.length}] `);return e[Number(t.trim())-1]??fail("badInput",`no device numbered ${t.trim()}`)}finally{i.close()}};export const LOCAL_EMULATOR_PORTS=[...Array.from({length:16},(e,t)=>16384+32*t),7555];const isPortOpen=e=>new Promise(t=>{const o=new Socket;o.setTimeout(300),o.once("connect",()=>{o.destroy(),t(!0)}),o.once("timeout",()=>{o.destroy(),t(!1)}),o.once("error",()=>{o.destroy(),t(!1)}),o.connect(e,"127.0.0.1")});export const connectLocalEmulators=async(e,t)=>{const o=(await Promise.all(LOCAL_EMULATOR_PORTS.map(async e=>({port:e,open:await isPortOpen(e)})))).filter(e=>e.open).map(e=>e.port),s=capture({file:e,args:["devices"]},t),r=0===s.status?parseAdbDevices(s.stdout).map(e=>e.serial):[],i=[];for(const s of o){const o=`127.0.0.1:${s}`;if(r.includes(o))continue;const n=capture({file:e,args:["connect",o]},t),a=`${n.stdout}${n.stderr}`.trim();a.includes("connected to")?(say(`devices: connected ${o}`),i.push(o)):warn(`devices: ${o} is open but adb connect said: ${a}`)}return i};export const devices=async e=>{const{appRoot:t,platform:o}=e;let s;if("android"===o){const e=adbPath(process.env,process.platform);await connectLocalEmulators(e,t),s=listAndroidDevices(e,t).map(e=>({platform:"android",...e}))}else requireDarwin(process.platform,"devices --platform ios"),s=listAppleDevices(t).map(e=>({platform:"ios",...e}));if(isJsonOutput())sayJson({devices:s});else for(const e of s)if("android"===e.platform){const t=e.serials.length>1?` (also ${e.serials.slice(1).join(", ")})`:"";say(`${e.serial} ${e.model} ${e.abi}${t}`)}else say(`${e.udid} ${e.name} ${e.kind}`)};
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync,readFileSync,rmSync}from"node:fs";import path from"node:path";import{capture}from"./exec.js";import{fail,isJsonOutput,say,sayJson}from"./output.js";import{adbPath,findOnPath,requireDarwin}from"./platform.js";import{checkNdkHook,usesFirebase}from"./prebuild.js";import{checkSync}from"./sync.js";export const JDK_MAJOR=17;const JDK_FIX="install JDK 17 and set JAVA_HOME",ADB_FIX="set ANDROID_HOME to the Android SDK (adb lives in its platform-tools folder) or put adb on PATH",NINJA_FIX="install ninja 1.11 or newer (scoop install ninja) and put it first on PATH",XCODE_FIX="install Xcode, then xcode-select -s /Applications/Xcode.app",AUTOLINKING_FILE=path.join("android","build","generated","autolinking","autolinking.json"),AUTOLINKING_DIRS=[path.join("android","build","generated","autolinking"),path.join("android","app","build","generated","autolinking")];export const javaMajor=o=>{const e=/version\b[^"]*"([^"]+)"/.exec(o);if(!e)return;const t=e[1].split(/[._+-]/),s=Number(t[0]),n=1===s?Number(t[1]):s;return Number.isInteger(n)?n:void 0};export const gradleSdkVersions=o=>{const e={},t=/ndkVersion\s*=\s*["']([^"']+)["']/.exec(o),s=/buildToolsVersion\s*=\s*["']([^"']+)["']/.exec(o),n=/compileSdkVersion\s*=\s*(\d+)/.exec(o);return t&&(e.ndk=t[1]),s&&(e.buildTools=s[1]),n&&(e.compileSdk=n[1]),e};const samePath=(o,e)=>{const norm=o=>{const e=path.resolve(o).replace(/[\\/]+$/,"");return"win32"===process.platform?e.toLowerCase():e};return norm(o)===norm(e)};export const autolinkingProblem=(o,e,t)=>{let s;try{s=JSON.parse(o)}catch{return"android/build/generated/autolinking/autolinking.json does not parse"}if("string"==typeof s?.root&&!samePath(s.root,e))return`the autolinking cache was made for ${s.root}, not ${e}`;for(const[o,e]of Object.entries(s?.dependencies??{}))if("string"==typeof e?.root&&!t(e.root))return`the autolinking cache names ${o} at ${e.root}, which no longer exists`};const ok=(o,e)=>({code:o,status:"ok",message:e}),skipped=(o,e)=>({code:o,status:"skipped",message:e}),problem=(o,e,t)=>({code:o,status:"problem",message:e,fix:t}),probe=(o,e,t)=>{try{return t()}catch(t){return problem(o,t instanceof Error?t.message:String(t),e)}},outputOf=o=>[o.stdout,o.stderr].join("\n").trim()||`exited with status ${o.status}`,SDK_SPECS=[{code:"ndk",key:"ndkVersion",field:"ndk",folder:(o,e)=>path.join(o,"ndk",e),install:o=>`sdkmanager "ndk;${o}"`},{code:"buildTools",key:"buildToolsVersion",field:"buildTools",folder:(o,e)=>path.join(o,"build-tools",e),install:o=>`sdkmanager "build-tools;${o}"`},{code:"androidPlatform",key:"compileSdkVersion",field:"compileSdk",folder:(o,e)=>path.join(o,"platforms",`android-${e}`),install:o=>`sdkmanager "platforms;android-${o}"`}],sdkCheck=(o,e)=>probe(e.code,"fix the Android SDK under ANDROID_HOME",()=>{const t=path.join(o,"android","build.gradle");if(!existsSync(t))return skipped(e.code,"no android/build.gradle");const s=process.env.ANDROID_HOME;if(!s)return skipped(e.code,"ANDROID_HOME is not set");const n=gradleSdkVersions(readFileSync(t,"utf8"))[e.field];if(void 0===n)return skipped(e.code,`${e.key} is not declared in android/build.gradle`);const r=e.folder(s,n);return existsSync(r)?ok(e.code,r):problem(e.code,`${r} is missing`,e.install(n))}),androidChecks=o=>{const{appRoot:e}=o,t=[];t.push(probe("nodeVersion","install Node 24",()=>{const o=process.versions.node;return 24===Number(o.split(".")[0])?ok("nodeVersion",`Node ${o}`):problem("nodeVersion",`Node ${o} is not Node 24`,"install Node 24")})),t.push(probe("androidHome","set ANDROID_HOME to the Android SDK folder",()=>{const o=process.env.ANDROID_HOME;return o?existsSync(o)?ok("androidHome",o):problem("androidHome",`ANDROID_HOME ${o} does not exist`,"set ANDROID_HOME to the Android SDK folder"):problem("androidHome","ANDROID_HOME is not set","set ANDROID_HOME to the Android SDK folder")}));const s={};t.push(probe("adb",ADB_FIX,()=>{const o=adbPath(process.env,process.platform),t=capture({file:o,args:["version"]},e);if(0!==t.status)return problem("adb",outputOf(t),ADB_FIX);s.path=o;const n=t.stdout.split(/\r?\n/).find(o=>o.startsWith("Version"));return ok("adb",n?`${o}, ${n.trim()}`:o)}));const n=s.path;t.push(void 0===n?skipped("adbServer","adb is not available"):probe("adbServer","use one adb, the SDK platform-tools one: MuMu ships its own adb",()=>{const o=capture({file:n,args:["devices"]},e),t=`${o.stdout}\n${o.stderr}`.split(/\r?\n/).find(o=>o.includes("doesn't match this client"));return void 0===t?ok("adbServer","one adb server version"):problem("adbServer",`another adb restarted the adb server at a different version: ${t.trim()}`,"use one adb, the SDK platform-tools one: MuMu ships its own adb")})),t.push(probe("jdk",JDK_FIX,()=>{const o=process.env.JAVA_HOME,t="win32"===process.platform?"java.exe":"java",s=o?path.join(o,"bin",t):void 0,n=void 0!==s&&existsSync(s)?s:findOnPath("java",process.env,process.platform);if(void 0===n)return problem("jdk","java not found",JDK_FIX);const r=capture({file:n,args:["-version"]},e),i=javaMajor(`${r.stderr}${r.stdout}`);return 17===i?ok("jdk",`Java ${i} at ${n}`):problem("jdk",`Java ${i??"unknown"} at ${n}; React Native builds need Java 17`,JDK_FIX)}));for(const o of SDK_SPECS)t.push(sdkCheck(e,o));t.push("win32"!==process.platform?skipped("ninja","only Windows builds need ninja on PATH"):probe("ninja",NINJA_FIX,()=>{const o=findOnPath("ninja",process.env,process.platform);if(void 0===o)return problem("ninja","ninja not found on PATH",NINJA_FIX);const t=capture({file:o,args:["--version"]},e).stdout.trim(),s=/^(\d+)\.(\d+)/.exec(t);if(!s)return problem("ninja",`ninja --version printed no version at ${o}`,NINJA_FIX);return Number(s[1])>1||1===Number(s[1])&&Number(s[2])>=11?ok("ninja",`ninja ${t} at ${o}`):problem("ninja",`ninja ${t} at ${o} is older than 1.11`,NINJA_FIX)})),t.push(probe("install","npx mobile-tools sync",()=>{const o=checkSync(e,process.platform);return 0===o.length?ok("install","node_modules matches the lockfile"):problem("install",o.join("; "),"npx mobile-tools sync")}));const r="npx mobile-tools doctor --fix";return t.push(probe("autolinkingCache",r,()=>{const t=path.join(e,AUTOLINKING_FILE);if(!existsSync(t))return ok("autolinkingCache","no autolinking cache yet");const s=autolinkingProblem(readFileSync(t,"utf8"),e,existsSync);if(void 0===s)return ok("autolinkingCache","matches this tree");if(!o.fix)return problem("autolinkingCache",s,r);for(const o of AUTOLINKING_DIRS)rmSync(path.join(e,o),{recursive:!0,force:!0});return{code:"autolinkingCache",status:"fixed",message:"removed both autolinking caches; the next build regenerates them"}})),t.push(probe("ndkHook","add the subprojects block the message names to android/build.gradle",()=>(checkNdkHook(e),ok("ndkHook","library modules get the app NDK")))),t.push(probe("firebaseConfig","add the Firebase Android config for this app",()=>{if(!usesFirebase(e))return skipped("firebaseConfig","the app does not use @react-native-firebase");const o=path.join(e,"android","app","google-services.json");return existsSync(o)?ok("firebaseConfig","android/app/google-services.json"):problem("firebaseConfig","android/app/google-services.json is missing","add the Firebase Android config for this app")})),t},iosChecks=o=>[probe("xcode",XCODE_FIX,()=>{const e=capture({file:"/usr/bin/xcode-select",args:["-p"]},o);return 0===e.status?ok("xcode",e.stdout.trim()):problem("xcode","no Xcode developer directory is selected",XCODE_FIX)}),probe("cocoapods","install CocoaPods",()=>{const e=existsSync(path.join(o,"Gemfile"))?"bundle":"pod",t=findOnPath(e,process.env,process.platform);return void 0===t?problem("cocoapods",`${e} not found on PATH`,"install CocoaPods"):ok("cocoapods",t)}),probe("rosetta","open the terminal without Rosetta",()=>"1"===capture({file:"/usr/sbin/sysctl",args:["-n","sysctl.proc_translated"]},o).stdout.trim()?problem("rosetta","this terminal runs under Rosetta, so simulator builds come out x86_64","open the terminal without Rosetta"):ok("rosetta","native terminal"))];export const doctor=async o=>{"ios"===o.platform&&requireDarwin(process.platform,"doctor --platform ios");const e=path.join(o.appRoot,"android"),t=path.join(o.appRoot,"ios");"android"!==o.platform||existsSync(e)||fail("appConfigInvalid",`no android/ folder in ${o.appRoot}`);const s=[];("android"===o.platform||void 0===o.platform&&existsSync(e))&&s.push(...androidChecks(o)),("ios"===o.platform||void 0===o.platform&&"darwin"===process.platform&&existsSync(t))&&s.push(...iosChecks(o.appRoot));const n=s.filter(o=>"problem"===o.status).length;if(isJsonOutput())sayJson({checks:s,problems:n});else{for(const o of s){const e="problem"===o.status&&o.fix?` Fix: ${o.fix}`:"";say(`doctor: ${o.status} ${o.code}: ${o.message}${e}`)}say(0===n?"doctor: ready":`doctor: ${n} problem(s)`)}n>0&&(process.exitCode=1)};
|
package/lib/exec.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn,spawnSync}from"node:child_process";import{closeSync,mkdirSync,openSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";import{childEnv}from"./platform.js";export const capture=(e,r)=>{const n=spawnSync(e.file,e.args,{cwd:r,env:childEnv(process.env),encoding:"utf8",maxBuffer:67108864,windowsHide:!0,windowsVerbatimArguments:e.windowsVerbatimArguments});return{status:n.error?-1:n.status??-1,stdout:n.stdout??"",stderr:n.error?n.error.message:n.stderr??""}};export const runInherit=(e,r,n)=>{const
|
|
1
|
+
import{spawn,spawnSync}from"node:child_process";import{closeSync,mkdirSync,openSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";import{childEnv}from"./platform.js";export const capture=(e,r)=>{const n=spawnSync(e.file,e.args,{cwd:r,env:childEnv(process.env),encoding:"utf8",maxBuffer:67108864,windowsHide:!0,windowsVerbatimArguments:e.windowsVerbatimArguments});return{status:n.error?-1:n.status??-1,stdout:n.stdout??"",stderr:n.error?n.error.message:n.stderr??""}};export const runInherit=(e,r,n)=>{const o=spawnSync(e.file,e.args,{cwd:r,env:childEnv(process.env),stdio:"inherit",windowsHide:!0,windowsVerbatimArguments:e.windowsVerbatimArguments});o.error&&fail("toolFailed",`${n} could not start: ${o.error.message}`),0!==o.status&&fail("toolFailed",`${n} failed (exit ${o.status??"signal"})`)};export const runToLog=(e,r,n)=>{mkdirSync(path.dirname(n),{recursive:!0});const o=openSync(n,"w");return new Promise(n=>{const s=spawn(e.file,e.args,{cwd:r,env:childEnv(process.env),stdio:["ignore",o,o],windowsHide:!0,windowsVerbatimArguments:e.windowsVerbatimArguments});s.on("error",()=>{closeSync(o),n(-1)}),s.on("close",e=>{closeSync(o),n(e??-1)})})};export const startDetached=(e,r,n)=>{mkdirSync(path.dirname(n),{recursive:!0});const o=openSync(n,"a"),s=spawn(e.file,e.args,{cwd:r,env:childEnv(process.env),detached:!0,stdio:["ignore",o,o],windowsHide:!0});return s.unref(),closeSync(o),s.pid??fail("toolFailed",`${e.file} could not start; see ${n}`)};
|
package/lib/hooks.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{chmodSync,existsSync,mkdirSync,readFileSync,renameSync,writeFileSync}from"node:fs";import path from"node:path";import{capture}from"./exec.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";export const HOOK_NAMES=["post-merge","post-checkout"];const OWNER_LINE="# Written by @webority/mobile-tools (hooks install).",EXIT_LINE="exit $status",startMarker=o=>`# >>> mobile-tools ${o}`,endMarker=o=>`# <<< mobile-tools ${o}`;export const hookBlock=o=>{const t="node_modules/@webority/mobile-tools/lib/cli.js";return[startMarker(o),`if [ -f "${o}/${t}" ]; then`,` (cd "${o}" && node ${t} sync --check) || true`,"fi",endMarker(o)].join("\n")};export const chainedName=o=>`${o}.chained`;export const isOwnHook=o=>o.includes(OWNER_LINE);export const composeHook=(o,t,e)=>{const
|
|
1
|
+
import{chmodSync,existsSync,mkdirSync,readFileSync,renameSync,writeFileSync}from"node:fs";import path from"node:path";import{capture}from"./exec.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";export const HOOK_NAMES=["post-merge","post-checkout"];const OWNER_LINE="# Written by @webority/mobile-tools (hooks install).",EXIT_LINE="exit $status",startMarker=o=>`# >>> mobile-tools ${o}`,endMarker=o=>`# <<< mobile-tools ${o}`;export const hookBlock=o=>{const t="node_modules/@webority/mobile-tools/lib/cli.js";return[startMarker(o),`if [ -f "${o}/${t}" ]; then`,` (cd "${o}" && node ${t} sync --check) || true`,"fi",endMarker(o)].join("\n")};export const chainedName=o=>`${o}.chained`;export const isOwnHook=o=>o.includes(OWNER_LINE);export const composeHook=(o,t,e)=>{const i=hookBlock(e);if(void 0!==t&&isOwnHook(t)){const o=t.indexOf(startMarker(e)),n=t.indexOf(endMarker(e));if(o>=0&&n>o)return`${t.slice(0,o)}${i}${t.slice(n+endMarker(e).length)}`;const s=t.lastIndexOf(EXIT_LINE);return s>=0?`${t.slice(0,s)}${i}\n${t.slice(s)}`:`${t.trimEnd()}\n${i}\n`}return["#!/bin/sh",OWNER_LINE,"status=0",`chained="$(dirname "$0")/${chainedName(o)}"`,'if [ -x "$chained" ]; then "$chained" "$@" || status=$?; fi',i,EXIT_LINE,""].join("\n")};const git=(o,t)=>{const e=findOnPath("git",process.env,process.platform)??fail("toolMissing","git not found on PATH"),i=capture({file:e,args:t},o);return 0!==i.status?fail("toolFailed",`git ${t.join(" ")} failed: ${i.stderr.trim()}`):i.stdout.trim()};export const hooksDir=o=>{const t=git(o,["rev-parse","--show-toplevel"]),e=findOnPath("git",process.env,process.platform)??fail("toolMissing","git not found on PATH"),i=capture({file:e,args:["config","--get","core.hooksPath"]},o),n=0===i.status?i.stdout.trim():"";return""!==n?{dir:path.resolve(t,n),topLevel:t}:{dir:path.resolve(o,git(o,["rev-parse","--git-path","hooks"])),topLevel:t}};export const installHooks=o=>{const{dir:t,topLevel:e}=hooksDir(o),i=path.relative(e,o).split(path.sep).join("/")||".";mkdirSync(t,{recursive:!0});for(const o of HOOK_NAMES){const n=path.join(t,o);let s=existsSync(n)?readFileSync(n,"utf8"):void 0;if(void 0!==s&&!isOwnHook(s)){const e=path.join(t,chainedName(o));existsSync(e)&&fail("appConfigInvalid",`${n} is not ours and ${e} already exists; merge them by hand, then rerun`),renameSync(n,e),chmodSync(e,493),say(`hooks: kept your ${o} hook as ${chainedName(o)}; it still runs first`),s=void 0}writeFileSync(n,composeHook(o,s,i),"utf8"),chmodSync(n,493),say(`hooks: ${path.relative(e,n).split(path.sep).join("/")} runs sync --check for ${i}`)}};
|
package/lib/icons.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,mkdirSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{ENVIRONMENTS,flavorOf,loadToolsConfig,resolveIosProjectName}from"./config.js";import{ADAPTIVE_DP,ADAPTIVE_ICON_XML,ADAPTIVE_VISIBLE_DP,backgroundColorXml,circleMaskSvg,DENSITIES,IOS_ICON_FILE,iosContentsJson,LEGACY_DP,RIBBONS,ribbonSvg,toHex}from"./iconArt.js";import{fail,say}from"./output.js";const MASTER=1024,PNG={compressionLevel:9,adaptiveFiltering:!1,palette:!1,force:!0},ICON_NAMES=["ic_launcher","ic_launcher_round","ic_launcher_foreground"],removeOtherFormats=o=>{if(existsSync(o))for(const n of readdirSync(o)){const{name:e,ext:r}=path.parse(n);ICON_NAMES.includes(e)&&".png"!==r&&rmSync(path.join(o,n))}},write=(o,n)=>{mkdirSync(path.dirname(o),{recursive:!0}),writeFileSync(o,n)},assertNoColorClash=(o,n)=>{if(existsSync(o))for(const e of readdirSync(o))e!==n&&e.endsWith(".xml")&&readFileSync(path.join(o,e),"utf8").includes('name="ic_launcher_background"')&&fail(
|
|
1
|
+
import{existsSync,mkdirSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{ENVIRONMENTS,flavorOf,loadToolsConfig,resolveIosProjectName}from"./config.js";import{ADAPTIVE_DP,ADAPTIVE_ICON_XML,ADAPTIVE_VISIBLE_DP,backgroundColorXml,circleMaskSvg,DENSITIES,IOS_ICON_FILE,iosContentsJson,LEGACY_DP,RIBBONS,ribbonSvg,toHex}from"./iconArt.js";import{fail,say}from"./output.js";const MASTER=1024,PNG={compressionLevel:9,adaptiveFiltering:!1,palette:!1,force:!0},ICON_NAMES=["ic_launcher","ic_launcher_round","ic_launcher_foreground"],removeOtherFormats=o=>{if(existsSync(o))for(const n of readdirSync(o)){const{name:e,ext:r}=path.parse(n);ICON_NAMES.includes(e)&&".png"!==r&&rmSync(path.join(o,n))}},write=(o,n)=>{mkdirSync(path.dirname(o),{recursive:!0}),writeFileSync(o,n)},assertNoColorClash=(o,n)=>{if(existsSync(o))for(const e of readdirSync(o))e!==n&&e.endsWith(".xml")&&readFileSync(path.join(o,e),"utf8").includes('name="ic_launcher_background"')&&fail("appConfigInvalid",`${path.join(o,e)} already defines ic_launcher_background; remove it there`)},loadMaster=async(o,n)=>{const e=await o(n).metadata();if("png"!==e.format&&"svg"!==e.format)return fail("appConfigInvalid",`icon source must be a PNG or an SVG, not ${e.format??"unknown"}: ${n}`);const r=e.width??0,t=e.height??0;if(r!==t)return fail("appConfigInvalid",`icon source must be square, ${n} is ${r}x${t}`);if("png"===e.format&&r<1024)return fail("appConfigInvalid",`a PNG icon source must be at least 1024x1024, ${n} is ${r}x${t}`);return o(n,"svg"===e.format?{density:73728/r}:{}).resize(1024,1024,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).ensureAlpha().png(PNG).toBuffer()},cornerColor=async(o,n)=>{const{data:e}=await o(n).raw().toBuffer({resolveWithObject:!0});return 0===e[3]?"#FFFFFF":toHex(e[0],e[1],e[2])},withRibbon=async(o,n,e,r)=>{const t=RIBBONS[e];return t?o(n).composite([{input:Buffer.from(ribbonSvg(1024,r,t))}]).png(PNG).toBuffer():n},renderAndroid=async(o,n,e,r,t)=>{const a=path.join(n,"android","app","src",flavorOf(e),"res"),i=await withRibbon(o,r,e,{x:0,y:0,size:1024}),s=Math.round(1024*ADAPTIVE_VISIBLE_DP/ADAPTIVE_DP),c=Math.round((1024-s)/2),p=await o(r).resize(s,s).png(PNG).toBuffer(),f=await o({create:{width:1024,height:1024,channels:4,background:{r:0,g:0,b:0,alpha:0}}}).composite([{input:p,left:c,top:c}]).png(PNG).toBuffer(),l=await withRibbon(o,f,e,{x:c,y:c,size:s});for(const[n,e]of DENSITIES){const r=path.join(a,`mipmap-${n}`);removeOtherFormats(r);const t=Math.round(LEGACY_DP*e),s=Math.round(ADAPTIVE_DP*e);write(path.join(r,"ic_launcher.png"),await o(i).resize(t,t).png(PNG).toBuffer()),write(path.join(r,"ic_launcher_round.png"),await o(await o(i).resize(t,t).png(PNG).toBuffer()).composite([{input:Buffer.from(circleMaskSvg(t)),blend:"dest-in"}]).png(PNG).toBuffer()),write(path.join(r,"ic_launcher_foreground.png"),await o(l).resize(s,s).png(PNG).toBuffer())}const u=path.join(a,"mipmap-anydpi-v26");removeOtherFormats(u),write(path.join(u,"ic_launcher.xml"),ADAPTIVE_ICON_XML),write(path.join(u,"ic_launcher_round.xml"),ADAPTIVE_ICON_XML);const d=path.join(a,"values");assertNoColorClash(d,"ic_launcher_background.xml"),write(path.join(d,"ic_launcher_background.xml"),backgroundColorXml(t)),say(`icons: ${path.relative(n,a)}`)},renderIos=async(o,n,e,r,t,a)=>{const i=path.join(n,"ios",e,"Images.xcassets");existsSync(i)||fail("appConfigInvalid",`${path.relative(n,i)} not found: check "iosProjectName"`);const s=path.join(i,`AppIcon-${r}.appiconset`);rmSync(s,{recursive:!0,force:!0});const c=await withRibbon(o,t,r,{x:0,y:0,size:1024});write(path.join(s,IOS_ICON_FILE),await o(c).flatten({background:a}).png(PNG).toBuffer()),write(path.join(s,"Contents.json"),iosContentsJson()),say(`icons: ${path.relative(n,s)}`)},appEnvironments=o=>{const n=ENVIRONMENTS.filter(n=>existsSync(path.join(o,`appsettings.${n}.json`)));return 0===n.length&&fail("appConfigInvalid",`no appsettings.<Env>.json in ${o} (${ENVIRONMENTS.join(", ")}): run mobile-tools from the app's root folder`),n};export const icons=async o=>{const{appRoot:n}=o,e=appEnvironments(n),r=loadToolsConfig(n),t=o.source??r.iconSource??fail("badInput",'no icon source: pass --source <png|svg> or set "iconSource" in mobile-tools.config.json'),a=path.resolve(n,t);existsSync(a)||fail("appConfigInvalid",`icon source not found: ${a}`);const{default:i}=await(import("sharp")),s=await loadMaster(i,a),c=await cornerColor(i,s),p=resolveIosProjectName(n,r),f=existsSync(path.join(n,"android"));f&&!r.flavors&&say('icons: Android skipped, "flavors" is false: per-environment icons live in flavor source sets');for(const o of e)f&&r.flavors&&await renderAndroid(i,n,o,s,c),p&&await renderIos(i,n,p,o,s,c)};
|
package/lib/ios.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mkdtempSync,readFileSync,rmSync}from"node:fs";import{tmpdir}from"node:os";import path from"node:path";import{capture}from"./exec.js";import{fail}from"./output.js";import{XCRUN}from"./platform.js";export const parseSimctlBooted=e=>{const t=JSON.parse(e);return Object.values(t.devices??{}).flat().filter(e=>"Booted"===e.state).map(e=>({udid:e.udid,name:e.name,kind:"simulator",serials:[e.udid],model:e.name}))};export const parseDevicectl=e=>{const t=JSON.parse(e);return(t.result?.devices??[]).filter(e=>"iOS"===e.hardwareProperties?.platform&&"paired"===e.connectionProperties?.pairingState&&"unavailable"!==e.connectionProperties?.tunnelState&&e.hardwareProperties?.udid).map(e=>{const t=e.hardwareProperties?.udid,i=e.deviceProperties?.name??t;return{udid:t,name:i,kind:"device",serials:[t,e.identifier??t],model:i}})};export const listAppleDevices=e=>{const t=capture({file:XCRUN,args:["simctl","list","-j","devices","booted"]},e);if(0!==t.status)return fail(
|
|
1
|
+
import{mkdtempSync,readFileSync,rmSync}from"node:fs";import{tmpdir}from"node:os";import path from"node:path";import{capture}from"./exec.js";import{fail}from"./output.js";import{XCRUN}from"./platform.js";export const parseSimctlBooted=e=>{const t=JSON.parse(e);return Object.values(t.devices??{}).flat().filter(e=>"Booted"===e.state).map(e=>({udid:e.udid,name:e.name,kind:"simulator",serials:[e.udid],model:e.name}))};export const parseDevicectl=e=>{const t=JSON.parse(e);return(t.result?.devices??[]).filter(e=>"iOS"===e.hardwareProperties?.platform&&"paired"===e.connectionProperties?.pairingState&&"unavailable"!==e.connectionProperties?.tunnelState&&e.hardwareProperties?.udid).map(e=>{const t=e.hardwareProperties?.udid,i=e.deviceProperties?.name??t;return{udid:t,name:i,kind:"device",serials:[t,e.identifier??t],model:i}})};export const listAppleDevices=e=>{const t=capture({file:XCRUN,args:["simctl","list","-j","devices","booted"]},e);if(0!==t.status)return fail("toolFailed",`xcrun simctl failed: ${t.stderr.trim()}`);const i=parseSimctlBooted(t.stdout),o=mkdtempSync(path.join(tmpdir(),"mobile-tools-")),r=path.join(o,"devices.json");try{0===capture({file:XCRUN,args:["devicectl","list","devices","--json-output",r]},e).status&&i.push(...parseDevicectl(readFileSync(r,"utf8")))}finally{rmSync(o,{recursive:!0,force:!0})}return 0===i.length?fail("deviceNotFound","no booted simulator or connected iPhone: boot one with xcrun simctl boot <name>"):i};export const describeAppleDevice=e=>`${e.name} (${e.kind}, ${e.udid})`;export const xcodebuildArgs=(e,t)=>["-workspace",path.join("ios",`${e}.xcworkspace`),"-scheme",e,"-configuration","Debug","-destination",`id=${t}`,"-derivedDataPath",path.join("ios","build"),"ONLY_ACTIVE_ARCH=YES","build"];
|
package/lib/metro.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRequire}from"node:module";import{createServer}from"node:net";import path from"node:path";import{startDetached}from"./exec.js";import{fail,say}from"./output.js";export const DEFAULT_METRO_PORT=8081;const samePath=(e
|
|
1
|
+
import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{createServer}from"node:net";import path from"node:path";import{logDir}from"./buildLog.js";import{capture,startDetached}from"./exec.js";import{fail,say}from"./output.js";export const DEFAULT_METRO_PORT=8081;export const HUNG_PROBE_MS=5e3;const PORT_FREE_WAIT_MS=1e4,TERM_WAIT_MS=5e3,samePath=(t,e)=>{const norm=t=>{const e=path.resolve(t).replace(/[\\/]+$/,"");return"win32"===process.platform?e.toLowerCase():e};return norm(t)===norm(e)};export const classifyStatus=(t,e,r)=>{if(!t.includes("packager-status:running"))return"other";const o=e??/project-root:(.+)/.exec(t)?.[1]?.trim();return o&&samePath(o,r)?"ours":"other"};const portIsFree=t=>new Promise(e=>{const r=createServer();r.once("error",()=>e(!1)),r.once("listening",()=>r.close(()=>e(!0))),r.listen(t)});export const probeMetro=async(t,e)=>{try{const r=await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(1e3)});return classifyStatus(await r.text(),r.headers.get("X-React-Native-Project-Root"),e)}catch{return await portIsFree(t)?"free":"other"}};const sleep=t=>new Promise(e=>setTimeout(e,t)),waitForMetro=async(t,e,r)=>{const o=Date.now()+12e4;for(;Date.now()<o;){if("ours"===await probeMetro(t,e))return;await new Promise(t=>setTimeout(t,500))}fail("metroUnavailable",`Metro did not answer on port ${t} within 2 minutes; see ${r}`)};export const METRO_RECORD_FILE="metro.json";export const readMetroRecord=t=>{const e=path.join(logDir(t),"metro.json");if(existsSync(e))try{const t=JSON.parse(readFileSync(e,"utf8"));return"number"==typeof t.port&&"number"==typeof t.pid?t:void 0}catch{return}};const writeMetroRecord=(t,e,r)=>{mkdirSync(logDir(t),{recursive:!0});const o={port:e,pid:r,appRoot:t,startedAtUtc:(new Date).toISOString()};writeFileSync(path.join(logDir(t),"metro.json"),JSON.stringify(o,null,2))},startMetro=async(t,e,r)=>{let o;try{o=createRequire(path.join(t,"package.json")).resolve("expo/bin/cli")}catch{return fail("toolMissing","expo is not installed in this app; run npx mobile-tools sync")}const s=startDetached({file:process.execPath,args:[o,"start","--port",String(e)]},t,r);writeMetroRecord(t,e,s),say(`metro: started on port ${e}, log ${r}`),await waitForMetro(e,t,r)},answers=async(t,e)=>{try{return await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(e)}),!0}catch{return!1}},systemTool=t=>path.join(process.env.SystemRoot??"C:/Windows","System32",t),isNodeProcess=t=>{try{process.kill(t,0)}catch{return!1}if("win32"===process.platform){return capture({file:systemTool("tasklist.exe"),args:["/FI",`PID eq ${t}`,"/FO","CSV","/NH"]},process.cwd()).stdout.toLowerCase().includes("node.exe")}return capture({file:"/bin/ps",args:["-p",String(t),"-o","comm="]},process.cwd()).stdout.trim().endsWith("node")},signalGroup=(t,e)=>{try{process.kill(-t,e)}catch{}},stopTree=async t=>{if("win32"===process.platform){const e=capture({file:systemTool("taskkill.exe"),args:["/PID",String(t),"/T","/F"]},process.cwd());if(0!==e.status&&isNodeProcess(t)){const r=`${e.stdout}${e.stderr}`.trim();fail("toolFailed",`taskkill could not stop pid ${t}: ${r}`)}return}signalGroup(t,"SIGTERM");const e=Date.now()+5e3;for(;isNodeProcess(t)&&Date.now()<e;)await sleep(250);isNodeProcess(t)&&signalGroup(t,"SIGKILL")},waitPortFree=async(t,e)=>{const r=Date.now()+1e4;for(;!await portIsFree(t);)Date.now()>=r&&fail("metroUnavailable",`port ${t} is still held 10 s after stopping pid ${e}; stop the holder and rerun`),await sleep(250)};export const ensureMetro=async(t,e,{fixedPort:r})=>{const o=r?8081:8100;for(let r=8081;r<=o;r++){const o=await probeMetro(r,t);if("ours"===o)return say(`metro: reusing the server on port ${r}`),r;if("other"===o){const o=readMetroRecord(t);if(o?.port===r&&isNodeProcess(o.pid)&&!await answers(r,5e3))return say(`metro: the server on port ${r} (pid ${o.pid}) does not answer; restarting it, log ${e}`),await stopTree(o.pid),await waitPortFree(r,o.pid),await startMetro(t,r,e),r}if("free"===o)return await startMetro(t,r,e),r}return fail("metroUnavailable",r?"port 8081 is held by another server (the iOS app always looks there); stop it and rerun":"ports 8081-8100 are all held by other servers; stop one and rerun")};export const restartMetro=async t=>{const e=path.join(logDir(t),"metro.log"),r=readMetroRecord(t);if(r&&isNodeProcess(r.pid))return await stopTree(r.pid),say(`metro: stopped pid ${r.pid} on port ${r.port}`),await waitPortFree(r.port,r.pid),await startMetro(t,r.port,e),say(`metro: ready on port ${r.port}`),r.port;const o=r?.port??8081,s=await probeMetro(o,t);return"ours"===s?fail("metroUnavailable",`the Metro on port ${o} was not started by mobile-tools; stop it in its own terminal (Ctrl+C), then run mobile-tools metro restart`):"other"===s?fail("metroUnavailable",`port ${o} is held by another server; stop it and rerun`):(await startMetro(t,o,e),say(`metro: ready on port ${o}`),o)};
|
package/lib/otaNative.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{X509Certificate}from"node:crypto";import{existsSync,readFileSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const COMPONENT_KEY={android:"OTA_ANDROID_COMPONENT_ID",ios:"OTA_IOS_COMPONENT_ID"};const CODE_SIGNING_ALG="rsa-v1_5-sha256",KEY_ID=/^[a-z][a-z0-9]*$/,WHOLE_NUMBER=/^[1-9][0-9]*$/,isSet=e=>null!=e&&""!==e;export const componentIdFor=(e,t)=>{const i=COMPONENT_KEY[t],n=e[i];return isSet(n)?WHOLE_NUMBER.test(String(n))?String(n):fail(
|
|
1
|
+
import{X509Certificate}from"node:crypto";import{existsSync,readFileSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const COMPONENT_KEY={android:"OTA_ANDROID_COMPONENT_ID",ios:"OTA_IOS_COMPONENT_ID"};const CODE_SIGNING_ALG="rsa-v1_5-sha256",KEY_ID=/^[a-z][a-z0-9]*$/,WHOLE_NUMBER=/^[1-9][0-9]*$/,isSet=e=>null!=e&&""!==e;export const componentIdFor=(e,t)=>{const i=COMPONENT_KEY[t],n=e[i];return isSet(n)?WHOLE_NUMBER.test(String(n))?String(n):fail("appConfigInvalid",`${i} must be a positive whole number, not ${JSON.stringify(n)}`):fail("appConfigInvalid",`appsettings has no ${i}: the ${t} app has no OTA component to publish to`)};export const codeSigningKeyIdOf=(e,t)=>{let i;try{i=new X509Certificate(e).subject}catch{return fail("appConfigInvalid",`${t} is not a readable X.509 certificate`)}const n=i.split("\n").filter(e=>e.startsWith("CN=")),a=1===n.length?n[0].slice(3):"";return"FoodMe OTA Updates"===a?fail("appConfigInvalid",`${t} is the shared OTA certificate (CN=FoodMe OTA Updates), which is retired for this app: get the product's own certificate with \`bash scripts/ota-signing-key.sh cert <keyid> --out certs/ota-code-signing-cert.pem\` in webority-os`):KEY_ID.test(a)?a:fail("appConfigInvalid",`${t} has common name ${JSON.stringify(a)}, which is not a signing key id (a lowercase name, for example acme)`)};export const otaNativeSettings=(e,t,i,n)=>{const a=t.OTA_UPDATE_BASE_URL;if(!isSet(a)||!isSet(t[COMPONENT_KEY[n]]))return;const s=componentIdFor(t,n),o=t.OTA_PROJECT_ID;if(!isSet(o)||!WHOLE_NUMBER.test(String(o)))return fail("appConfigInvalid",`OTA_PROJECT_ID must be the update server's project id (a positive whole number), not ${JSON.stringify(o)}`);const r=t.OTA_CODE_SIGNING_CERT;if("string"!=typeof r||""===r.trim())return fail("appConfigInvalid","OTA_CODE_SIGNING_CERT must name the PEM certificate file, relative to the app folder");const p=path.join(e,r);if(!existsSync(p))return fail("appConfigInvalid",`OTA_CODE_SIGNING_CERT ${r} not found in ${e}`);const c=readFileSync(p,"utf8").replace(/\r\n/g,"\n").trim();return c.startsWith("-----BEGIN CERTIFICATE-----")?{updateUrl:`${String(a).replace(/\/+$/,"")}/ota/manifest/${o}/${s}/${i}`,runtimeVersion:String(t.VERSION_INTERNAL),certificate:c,keyId:codeSigningKeyIdOf(c,r)}:fail("appConfigInvalid",`${r} is not a PEM certificate`)};const escapeXml=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),escapeRegExp=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const androidOtaMetaData=e=>[["expo.modules.updates.ENABLED","true"],["expo.modules.updates.EXPO_UPDATE_URL",e.updateUrl],["expo.modules.updates.EXPO_RUNTIME_VERSION",e.runtimeVersion],["expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH","ALWAYS"],["expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS","0"],["expo.modules.updates.CODE_SIGNING_CERTIFICATE",e.certificate],["expo.modules.updates.CODE_SIGNING_METADATA",JSON.stringify({keyid:e.keyId,alg:CODE_SIGNING_ALG})]];const childIndent=(e,t)=>{const i=e.split(/\r?\n/).filter(e=>""!==e.trim()),n=/^[ \t]*/.exec(i[i.length-1]??"")?.[0]??"";return n.length>t.length?n:`${t} `};export const rewriteAndroidManifestOta=(e,t)=>{const i=eolOf(e);let n=e;const a=[];for(const[e,i]of androidOtaMetaData(t)){const t=escapeXml(i).replace(/\n/g," "),s=new RegExp(`(<meta-data\\s+android:name="${escapeRegExp(e)}"\\s+android:value=")[^"]*(")`);s.test(n)?n=n.replace(s,(e,i,n)=>`${i}${t}${n}`):a.push([e,t])}if(0===a.length)return n;const s=/^([ \t]*)<\/application>/m.exec(n);if(!s)return fail("appConfigInvalid","AndroidManifest.xml has no </application> to add the expo-updates settings to");const o=childIndent(n.slice(0,s.index),s[1]),r=a.map(([e,t])=>`${o}<meta-data android:name="${e}" android:value="${t}" />`).join(i);return`${n.slice(0,s.index)}${r}${i}${n.slice(s.index)}`};export const iosOtaPlistKeys=e=>[["EXUpdatesEnabled",!0],["EXUpdatesURL",e.updateUrl],["EXUpdatesRuntimeVersion",e.runtimeVersion],["EXUpdatesCheckOnLaunch","ALWAYS"],["EXUpdatesLaunchWaitMs",0],["EXUpdatesCodeSigningCertificate",e.certificate],["EXUpdatesCodeSigningMetadata",{keyid:e.keyId,alg:CODE_SIGNING_ALG}]];const plistValue=(e,t,i)=>{if("boolean"==typeof e)return e?"<true/>":"<false/>";if("number"==typeof e)return`<integer>${e}</integer>`;if("string"==typeof e)return`<string>${escapeXml(e).replace(/\n/g,i)}</string>`;const n=`${t}${t.includes("\t")?"\t":" "}`,a=Object.entries(e).map(([e,t])=>`${n}<key>${escapeXml(e)}</key>${i}${n}<string>${escapeXml(t)}</string>`).join(i);return`<dict>${i}${a}${i}${t}</dict>`},PLIST_VALUE=String.raw`(?:<(?:true|false)\/>|<string>[\s\S]*?<\/string>|<integer>[^<]*<\/integer>|<dict>[\s\S]*?<\/dict>|<dict\/>)`;export const rewriteExpoPlistOta=(e,t)=>{const i=eolOf(e);let n=e;const a=[];for(const[e,s]of iosOtaPlistKeys(t)){const t=new RegExp(`([ \\t]*)(<key>${escapeRegExp(e)}<\\/key>\\s*)${PLIST_VALUE}`).exec(n);if(t){const e=t[1];n=`${n.slice(0,t.index)}${e}${t[2]}${plistValue(s,e,i)}${n.slice(t.index+t[0].length)}`}else a.push([e,s])}if(0===a.length)return n;const s=/^([ \t]*)<\/dict>\s*<\/plist>/m.exec(n);if(!s)return fail("appConfigInvalid","Expo.plist has no root </dict> to add the expo-updates settings to");const o=childIndent(n.slice(0,s.index),s[1]),r=a.map(([e,t])=>`${o}<key>${escapeXml(e)}</key>${i}${o}${plistValue(t,o,i)}`).join(i);return`${n.slice(0,s.index)}${r}${i}${n.slice(s.index)}`};
|
package/lib/output.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class CliError extends Error{exitCode;constructor(
|
|
1
|
+
export class CliError extends Error{code;exitCode;constructor(t,o){super(o),this.name="CliError",this.code=t,this.exitCode="badInput"===t?2:1}}export const fail=(t,o)=>{throw new CliError(t,o)};let jsonOutput=!1;export const setJsonOutput=t=>{jsonOutput=t};export const isJsonOutput=()=>jsonOutput;export const say=t=>{(jsonOutput?process.stderr:process.stdout).write(`${t}\n`)};export const warn=t=>{process.stderr.write(`${t}\n`)};export const sayJson=t=>{process.stdout.write(`${JSON.stringify(t,null,2)}\n`)};export const reportFailure=t=>{jsonOutput?sayJson({error:{code:t.code,message:t.message}}):warn(`mobile-tools: ${t.message} [${t.code}]`)};
|
package/lib/platform.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const pathFor=n=>"win32"===n?path.win32:path.posix;const readEnv=(n,
|
|
1
|
+
import{existsSync}from"node:fs";import path from"node:path";import{fail}from"./output.js";export const pathFor=n=>"win32"===n?path.win32:path.posix;const readEnv=(n,o)=>{const t=Object.keys(n).find(n=>n.toLowerCase()===o.toLowerCase());return void 0===t?void 0:n[t]};export const childEnv=n=>{const o={};for(const[t,e]of Object.entries(n))"nodefaultcurrentdirectoryinexepath"!==t.toLowerCase()&&(o[t]=e);return o};export const findOnPath=(n,o,t,e=existsSync)=>{const i=pathFor(t),r=readEnv(o,"PATH")??"",s="win32"===t?`${n}.exe`:n;for(const n of r.split("win32"===t?";":":")){if(""===n)continue;const o=i.join(n,s);if(e(o))return o}};export const adbPath=(n,o,t=existsSync)=>{const e=pathFor(o),i=readEnv(n,"ANDROID_HOME");if(i){const n=e.join(i,"platform-tools","win32"===o?"adb.exe":"adb");if(t(n))return n}return findOnPath("adb",n,o,t)??fail("toolMissing","adb not found: set ANDROID_HOME to the Android SDK (adb lives in its platform-tools folder) or put adb on PATH.")};const SAFE_GRADLE_ARG=/^[A-Za-z0-9_:.=-]+$/;export const gradleInvocation=(n,o,t,e)=>{for(const n of o)SAFE_GRADLE_ARG.test(n)||fail("badInput",`refusing Gradle argument ${JSON.stringify(n)}: only plain task names and -Pkey=value`);const i=pathFor(t);if("win32"!==t)return{file:i.join(n,"gradlew"),args:o};const r=i.join(n,"gradlew.bat");/["%]/.test(r)&&fail("appConfigInvalid",`the app path cannot hold " or %: ${r}`);const s=readEnv(e,"SystemRoot")??"C:\\Windows";return{file:readEnv(e,"ComSpec")??i.join(s,"System32","cmd.exe"),args:["/d","/s","/c",`""${r}" ${o.join(" ")}"`],windowsVerbatimArguments:!0}};export const requireDarwin=(n,o)=>{"darwin"!==n&&fail("platformUnavailable",`${o} needs macOS: xcodebuild and xcrun exist only there.`)};export const XCRUN="/usr/bin/xcrun";export const XCODEBUILD="/usr/bin/xcodebuild";export const npmInvocation=(n,o,t,e,i=existsSync)=>{const r=pathFor(t),s=readEnv(o,"npm_execpath"),a=[s&&/npm-cli\.c?js$/.test(s)?s:void 0,"win32"===t?r.join(r.dirname(e),"node_modules","npm","bin","npm-cli.js"):r.join(r.dirname(e),"..","lib","node_modules","npm","bin","npm-cli.js")].find(n=>void 0!==n&&i(n));return a?{file:e,args:[a,...n]}:fail("toolMissing",`npm not found beside ${e}; install Node 24 with its bundled npm.`)};export const pnpmInvocation=(n,o,t,e,i=existsSync)=>{const r=readEnv(o,"npm_execpath");if(r&&/pnpm\.c?js$/.test(r)&&i(r))return{file:e,args:[r,...n]};const s=findOnPath("pnpm",o,t,i);return s?{file:s,args:n}:fail("toolMissing","pnpm not found on PATH (on Windows, pnpm.exe); install pnpm, or run through pnpm exec.")};
|
package/lib/prebuild.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{copyFileSync,existsSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{applicationId,COMPILED_FILE,ENVIRONMENTS,gradleAppId,lastCompiledEnvironment,loadAppSettings,loadToolsConfig,resolveIosProjectName}from"./config.js";import{capture}from"./exec.js";import{otaNativeSettings,rewriteAndroidManifestOta,rewriteExpoPlistOta}from"./otaNative.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";const eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const rewriteGradleProperties=(e,t)=>{const o=eolOf(e),n=e.split(o),i=new Map(Object.entries(t)),s=n.map(e=>{const t=/^\s*([A-Za-z0-9_.-]+)\s*[=:]/.exec(e);if(t&&i.has(t[1])){const e=t[1],o=i.get(e);return i.delete(e),`${e}=${o}`}return e});if(0===i.size)return s.join(o);const r=[...i].map(([e,t])=>`${e}=${t}`);return s.length>1&&""===s[s.length-1]?s.splice(s.length-1,0,...r):1===s.length&&""===s[0]?s.splice(0,1,...r):s.push(...r),s.join(o)};const pbxValue=e=>/^[A-Za-z0-9._/-]+$/.test(e)?e:`"${e.replace(/"/g,'\\"')}"`;export const rewritePbxproj=(e,t,o={})=>{let n=e;for(const[e,i]of Object.entries(t)){const t=new RegExp(`(\\s)${e} = [^;\\n]*;`,"g");t.test(n)||fail(
|
|
1
|
+
import{copyFileSync,existsSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{applicationId,COMPILED_FILE,ENVIRONMENTS,gradleAppId,lastCompiledEnvironment,loadAppSettings,loadToolsConfig,resolveIosProjectName}from"./config.js";import{capture}from"./exec.js";import{otaNativeSettings,rewriteAndroidManifestOta,rewriteExpoPlistOta}from"./otaNative.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";const eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const rewriteGradleProperties=(e,t)=>{const o=eolOf(e),n=e.split(o),i=new Map(Object.entries(t)),s=n.map(e=>{const t=/^\s*([A-Za-z0-9_.-]+)\s*[=:]/.exec(e);if(t&&i.has(t[1])){const e=t[1],o=i.get(e);return i.delete(e),`${e}=${o}`}return e});if(0===i.size)return s.join(o);const r=[...i].map(([e,t])=>`${e}=${t}`);return s.length>1&&""===s[s.length-1]?s.splice(s.length-1,0,...r):1===s.length&&""===s[0]?s.splice(0,1,...r):s.push(...r),s.join(o)};const pbxValue=e=>/^[A-Za-z0-9._/-]+$/.test(e)?e:`"${e.replace(/"/g,'\\"')}"`;export const rewritePbxproj=(e,t,o={})=>{let n=e;for(const[e,i]of Object.entries(t)){const t=new RegExp(`(\\s)${e} = [^;\\n]*;`,"g");t.test(n)||fail("appConfigInvalid",`${e} not found in project.pbxproj`);const s=o.quote?.includes(e)?`"${i.replace(/"/g,'\\"')}"`:pbxValue(i);n=n.replace(t,(t,o)=>`${o}${e} = ${s};`)}return n};const escapeXml=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");export const rewriteInfoPlistDisplayName=(e,t)=>{const o=/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/;return o.test(e)||fail("appConfigInvalid","CFBundleDisplayName not found in Info.plist"),e.replace(o,(e,o,n)=>`${o}${escapeXml(t)}${n}`)};const pbxObject=(e,t)=>{const o=new RegExp(`\\n\\s*${t}(?: /\\*[^\\n]*?\\*/)? = \\{`).exec(e);if(!o)return;let n=0;for(let t=o.index+o[0].length-1;t<e.length;t++)if(e.startsWith("/*",t)){if(t=e.indexOf("*/",t+2)+1,0===t)return}else if('"'===e[t])for(t++;t<e.length&&'"'!==e[t];t++)"\\"===e[t]&&t++;else if("{"===e[t])n++;else if("}"===e[t]&&0===--n)return e.slice(o.index,t+1)},unquote=e=>e.startsWith('"')&&e.endsWith('"')?e.slice(1,-1).replace(/\\"/g,'"'):e,shipsInfoPlist=e=>/^com\.apple\.product-type\.(application|.*extension)/.test(e);export const shippedInfoPlists=e=>{const t=new Map,o=/\n\s*(\w+)(?: \/\*[^\n]*?\*\/)? = \{\s*isa = PBXNativeTarget;/g;for(const[,n]of e.matchAll(o)){const o=pbxObject(e,n)??"",i=unquote(/\sproductType = ([^;\n]*);/.exec(o)?.[1]??"");if(!shipsInfoPlist(i))continue;const s=unquote(/\sname = ([^;\n]*);/.exec(o)?.[1]??n),r=/\sbuildConfigurationList = (\w+)/.exec(o)?.[1],a=r&&pbxObject(e,r)||"",p=/buildConfigurations = \(([^)]*)\)/.exec(a)?.[1]??"";for(const[o]of p.matchAll(/\w+(?= \/\*|,)/g)){const n=/\sINFOPLIST_FILE = ([^;\n]*);/.exec(pbxObject(e,o)??"")?.[1];n&&!t.has(unquote(n))&&t.set(unquote(n),s)}}return[...t].map(([e,t])=>({target:t,file:e}))};const BUILD_SETTING_KEYS=[["CFBundleIdentifier","PRODUCT_BUNDLE_IDENTIFIER"],["CFBundleShortVersionString","MARKETING_VERSION"],["CFBundleVersion","CURRENT_PROJECT_VERSION"]];export const fixedInfoPlistValues=e=>BUILD_SETTING_KEYS.flatMap(([t,o])=>{const n=new RegExp(`<key>${t}</key>\\s*(<[^>]*>)?([^<]*)`).exec(e);if(!n)return[];const i="<string>"===n[1]?n[2].trim():n[1]??n[2].trim();return i===`$(${o})`||i===`\${${o}}`?[]:[{key:t,value:i,setting:o}]});export const checkInfoPlistReferences=(e,t)=>{const o=path.join(e,"ios"),n=readFileSync(path.join(o,`${t}.xcodeproj`,"project.pbxproj"),"utf8"),i=[{target:t,file:`${t}/Info.plist`},...shippedInfoPlists(n)],s=[],r=new Set;for(const{target:t,file:n}of i){const i=n.replace(/^(\$\(SRCROOT\)|\$\{SRCROOT\}|\$\(PROJECT_DIR\))\//,"");if(/\$[({]/.test(i)){s.push(`target ${t}: INFOPLIST_FILE ${n} uses a build setting prebuild cannot resolve`);continue}const a=path.resolve(o,i);if(r.has(a))continue;r.add(a);const p=path.relative(e,a).replace(/\\/g,"/");if(existsSync(a))for(const{key:e,value:t,setting:o}of fixedInfoPlistValues(readFileSync(a,"utf8")))s.push(`${p}: ${e} is "${t}", must be $(${o})`);else s.push(`${p} not found (INFOPLIST_FILE of target ${t})`)}s.length>0&&fail("appConfigInvalid",`Info.plist values that ignore what prebuild writes into project.pbxproj:\n ${s.join("\n ")}`)};const ANDROID_MANIFEST="android/app/src/main/AndroidManifest.xml",expoPlistPath=e=>`ios/${e}/Supporting/Expo.plist`;export const prebuildOutputs=(e,t=!1)=>{const o=[COMPILED_FILE,"android/gradle.properties"];return t&&o.push(ANDROID_MANIFEST),e&&(o.push(`ios/${e}/Info.plist`,`ios/${e}.xcodeproj/project.pbxproj`,`ios/${e}/GoogleService-Info.plist`),t&&o.push(expoPlistPath(e))),o};export const buildCommit=(e,t)=>{const o=findOnPath("git",process.env,process.platform);if(!o)return"nogit";const n=capture({file:o,args:["rev-parse","--short","HEAD"]},e);if(0!==n.status)return"nogit";const i=capture({file:o,args:["status","--porcelain","--",".",...t.map(e=>`:(exclude)${e}`)]},e);return 0!==i.status?fail("toolFailed",`git status failed in ${e}: ${i.stderr.trim()}`):""===i.stdout.trim()?n.stdout.trim():`${n.stdout.trim()}-dirty`};const writeIfChanged=(e,t)=>(!existsSync(e)||readFileSync(e,"utf8")!==t)&&(writeFileSync(e,t,"utf8"),!0),report=(e,t,o)=>{say(`prebuild: ${o?"wrote":"unchanged"} ${path.relative(e,t)}`)};export const usesFirebase=e=>{const t=path.join(e,"package.json");if(!existsSync(t))return!1;const o=JSON.parse(readFileSync(t,"utf8"));return Boolean(o.dependencies?.["@react-native-firebase/app"])};export const checkNdkHook=e=>{const t=path.join(e,"android","build.gradle");if(!existsSync(t))return;const o=readFileSync(t,"utf8");/withId\(\s*["']com\.android\.library["']\s*\)/.test(o)&&/\.ndkVersion\s*=\s*rootProject\.ext\.ndkVersion/.test(o)||fail("appConfigInvalid",'android/build.gradle does not hand the app NDK to library modules; add after apply plugin "com.facebook.react.rootproject": subprojects { project -> project.plugins.withId("com.android.library") { project.android.ndkVersion = rootProject.ext.ndkVersion } }')};export const stampAndroid=(e,t,o,n)=>{const i=path.join(e,"android","gradle.properties");existsSync(i)||fail("appConfigInvalid",`android/gradle.properties not found in ${e}`),usesFirebase(e)&&!existsSync(path.join(e,"android","app","google-services.json"))&&fail("appConfigInvalid","android/app/google-services.json is missing; the app uses @react-native-firebase, whose Gradle plugin needs it"),checkNdkHook(e);const s=rewriteGradleProperties(readFileSync(i,"utf8"),{APP_NAME:String(t.APP_NAME),APP_ID:gradleAppId(t.APP_ID,o,n.flavors),VERSION_DISPLAY:String(t.VERSION_DISPLAY),VERSION_INTERNAL:String(t.VERSION_INTERNAL)});report(e,i,writeIfChanged(i,s));const r=otaNativeSettings(e,t,o,"android");if(r){const t=path.join(e,ANDROID_MANIFEST);existsSync(t)||fail("appConfigInvalid",`${ANDROID_MANIFEST} not found: expo-updates reads its settings from it`),report(e,t,writeIfChanged(t,rewriteAndroidManifestOta(readFileSync(t,"utf8"),r)))}};export const stampIos=(e,t,o,n,i)=>{const s=path.join(e,"ios",i),r=path.join(s,"Info.plist"),a=path.join(e,"ios",`${i}.xcodeproj`,"project.pbxproj");for(const t of[r,a])existsSync(t)||fail("appConfigInvalid",`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,i);const p=`AppIcon-${o}`,l=path.join(s,"Images.xcassets"),c=existsSync(path.join(l,`${p}.appiconset`)),d=ENVIRONMENTS.filter(e=>e!==o&&existsSync(path.join(l,`AppIcon-${e}.appiconset`)));!c&&d.length>0&&fail("appConfigInvalid",`ios/${i}/Images.xcassets has AppIcon-${d[0]} but no ${p}: run mobile-tools icons`),report(e,r,writeIfChanged(r,rewriteInfoPlistDisplayName(readFileSync(r,"utf8"),String(t.APP_NAME))));const f={PRODUCT_BUNDLE_IDENTIFIER:applicationId(t.APP_ID,o),MARKETING_VERSION:String(t.VERSION_DISPLAY),CURRENT_PROJECT_VERSION:String(t.VERSION_INTERNAL)};c&&(f.ASSETCATALOG_COMPILER_APPICON_NAME=p),report(e,a,writeIfChanged(a,rewritePbxproj(readFileSync(a,"utf8"),f,{quote:["PRODUCT_BUNDLE_IDENTIFIER"]}))),c||say("prebuild: no AppIcon-<Env> sets, app icon left as the project sets it");const u=path.join(s,"GoogleService-Info.plist"),I=path.join(e,n.firebasePlistDir,`GoogleService-Info.${o}.plist`);if(!existsSync(I)&&usesFirebase(e)&&fail("appConfigInvalid",`${path.relative(e,I)} is missing; the app uses @react-native-firebase, and an iOS build without it reports no crashes`),existsSync(I)){const t=!existsSync(u)||!readFileSync(I).equals(readFileSync(u));t&©FileSync(I,u),report(e,u,t)}else existsSync(u)&&(rmSync(u),say(`prebuild: removed ${path.relative(e,u)}, no ${path.relative(e,I)}`));const g=otaNativeSettings(e,t,o,"ios");if(g){const t=path.join(e,expoPlistPath(i));existsSync(t)||fail("appConfigInvalid",`${expoPlistPath(i)} not found: add it to the Xcode project for expo-updates`),report(e,t,writeIfChanged(t,rewriteExpoPlistOta(readFileSync(t,"utf8"),g)))}};export const prebuild=e=>{const{appRoot:t,sameEnv:o,platform:n}=e;o&&e.env&&fail("badInput","pass --env or --same-env, not both");const i=o?lastCompiledEnvironment(t):e.env;if(!i)return fail("badInput","prebuild needs --env <Development|Staging|Production> or --same-env");const s=loadToolsConfig(t),r=resolveIosProjectName(t,s),a=loadAppSettings(t,i),p={...a,COMMIT:buildCommit(t,prebuildOutputs(r,Boolean(a.OTA_UPDATE_BASE_URL))),BUILD_TIME:(new Date).toISOString()};writeFileSync(path.join(t,COMPILED_FILE),`${JSON.stringify(p,null,2)}\n`,"utf8"),say(`prebuild: wrote ${COMPILED_FILE} (${i}, ${p.COMMIT})`);const l=existsSync(path.join(t,"android"));if(("android"===n||!n&&l)&&stampAndroid(t,p,i,s),"ios"===n||!n&&r){if(!r)return fail("appConfigInvalid",`no ios/ folder in ${t}`);stampIos(t,p,i,s,r)}return p};
|
package/lib/run.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync}from"node:fs";import path from"node:path";import{
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import path from"node:path";import{buildOrFail,logDir,logStamp}from"./buildLog.js";import{findCachedBuild,nativeFingerprint,storeCachedBuild}from"./cache.js";import{applicationId,baseAppId,gradleOutputDir,gradleTask,loadAppSettings,loadToolsConfig,localApiPort,resolveIosProjectName,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{describeAppleDevice,listAppleDevices,xcodebuildArgs}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCODEBUILD,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";export const readNamespace=i=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(i)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(i,e)=>{const a=i.filter(i=>i.endsWith(".apk")).sort();return a.find(i=>i.includes(e))??a.find(i=>i.includes("universal"))??a[0]};const adbOrFail=(i,e,a)=>{const o=capture({file:i,args:e},a);return 0!==o.status?fail("toolFailed",`adb ${e.join(" ")} failed: ${(o.stderr||o.stdout).trim()}`):o.stdout},elapsedSeconds=i=>((Date.now()-i)/1e3).toFixed(1),cachedBuild=(i,e,a)=>{const o=i.fingerprint.slice(0,12),r=e?void 0:findCachedBuild(i);if(r)return say(`cache: hit ${o} (fingerprint ${a} s), installing the cached build`),r;say(e?"cache: skipped (--rebuild), building":`cache: miss ${o} (fingerprint ${a} s), building`)},runAndroid=async i=>{const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=path.join(e,"android"),t=path.join(r,"app","build.gradle");existsSync(t)||fail("appConfigInvalid",`android/app/build.gradle not found in ${e}`);const s=readNamespace(readFileSync(t,"utf8"))??fail("appConfigInvalid","android/app/build.gradle declares no namespace"),n=adbPath(process.env,process.platform);await connectLocalEmulators(n,e);const d=listAndroidDevices(n,e),l=await pickDevice(d,i.device,describeDevice,!0===process.stdin.isTTY);ABI_PATTERN.test(l.abi)||fail("deviceNotFound",`${describeDevice(l)} reports an ABI this build cannot target: "${l.abi}"`),say(`run: ${describeDevice(l)}`);const c=prebuild({appRoot:e,env:a,platform:"android"}),p=Date.now(),u=await nativeFingerprint(e,"android"),f={appId:baseAppId(e),platform:"android",variant:variantName(a,o.flavors,"debug"),abi:l.abi,fingerprint:u};let m=cachedBuild(f,i.rebuild,elapsedSeconds(p));if(void 0===m){const i=gradleTask(a,o.flavors,"apk","debug"),t=logDir(e);await buildOrFail(gradleInvocation(r,[`:app:${i}`,`-PreactNativeArchitectures=${l.abi}`],process.platform,process.env),r,path.join(t,`gradle-${a}-${logStamp()}.log`),`gradle ${i}`);const s=gradleOutputDir(e,a,o.flavors,"apk","debug"),n=existsSync(s)?chooseApk(readdirSync(s),l.abi):void 0;n||fail("artifactMissing",`the build produced no APK in ${s}`),m=path.join(s,n),storeCachedBuild(f,m),say(`cache: stored ${u.slice(0,12)}`)}say(`run: installing ${path.basename(m)} on ${l.serial}`),adbOrFail(n,["-s",l.serial,"install","-r",m],e);const v=await ensureMetro(e,path.join(logDir(e),"metro.log"),{fixedPort:!1});if(adbOrFail(n,["-s",l.serial,"reverse","tcp:8081",`tcp:${v}`],e),"Development"===a){const i=localApiPort(String(loadAppSettings(e,"Development").BASE_URL),o);void 0!==i&&(adbOrFail(n,["-s",l.serial,"reverse",`tcp:${i}`,`tcp:${i}`],e),say(`run: device localhost:${i} reaches this machine's API`))}const g=applicationId(c.APP_ID,a);adbOrFail(n,["-s",l.serial,"shell","am","start","-n",`${g}/${s}.MainActivity`],e),say(`run: launched ${g} on ${l.model||l.serial}`)},runIos=async i=>{requireDarwin(process.platform,"run --platform ios");const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=resolveIosProjectName(e,o)??fail("appConfigInvalid",`no ios/ folder in ${e}`),t=path.join(e,"ios",`${r}.xcworkspace`);existsSync(t)||fail("appConfigInvalid",`ios/${r}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`);const s=await pickDevice(listAppleDevices(e),i.device,describeAppleDevice,!0===process.stdin.isTTY);say(`run: ${describeAppleDevice(s)}`);const n=prebuild({appRoot:e,env:a,platform:"ios"}),d=logDir(e);let l,c;if("simulator"===s.kind){const a=Date.now(),o=await nativeFingerprint(e,"ios");l={appId:baseAppId(e),platform:"ios",variant:"Debug-iphonesimulator",abi:"simulator",fingerprint:o},c=cachedBuild(l,i.rebuild,elapsedSeconds(a))}else say("cache: not used for a physical iPhone");if(void 0===c){await buildOrFail({file:XCODEBUILD,args:xcodebuildArgs(r,s.udid)},e,path.join(d,`xcodebuild-${a}-${logStamp()}.log`),"xcodebuild");const i=path.join(e,"ios","build","Build","Products","simulator"===s.kind?"Debug-iphonesimulator":"Debug-iphoneos"),o=existsSync(i)?readdirSync(i).filter(i=>i.endsWith(".app")):[],t=o.find(i=>i===`${r}.app`)??o[0];t||fail("artifactMissing",`the build produced no .app in ${i}`),c=path.join(i,t),l&&(storeCachedBuild(l,c),say(`cache: stored ${l.fingerprint.slice(0,12)}`))}const p=applicationId(n.APP_ID,a);await ensureMetro(e,path.join(d,"metro.log"),{fixedPort:!0});const u="simulator"===s.kind?[["simctl","install",s.udid,c],["simctl","launch",s.udid,p]]:[["devicectl","device","install","app","--device",s.udid,c],["devicectl","device","process","launch","--device",s.udid,p]];for(const i of u){const a=capture({file:XCRUN,args:i},e);0!==a.status&&fail("toolFailed",`xcrun ${i.slice(0,3).join(" ")} failed: ${(a.stderr||a.stdout).trim()}`)}say(`run: launched ${p} on ${s.name}`)};export const run=async i=>{requireSynced(i.appRoot),"ios"===i.platform?await runIos(i):await runAndroid(i)};
|
package/lib/sync.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{existsSync,readdirSync,readFileSync,rmSync,statSync,writeFileSync}from"node:fs";import path from"node:path";import{runInherit}from"./exec.js";import{fail,say}from"./output.js";import{findOnPath,gradleInvocation,npmInvocation,pnpmInvocation}from"./platform.js";export const STAMP_FILE=path.join("node_modules",".webority-install.json");const INSTALL_FIELDS=["dependencies","devDependencies","optionalDependencies","peerDependencies","bundleDependencies","overrides","resolutions","pnpm","workspaces"];export const lockfileOf=e=>{const n=path.join(e,"pnpm-lock.yaml"),t=path.join(e,"package-lock.json");return existsSync(n)&&existsSync(t)?fail("both pnpm-lock.yaml and package-lock.json exist: delete the one not in use"):existsSync(n)?{manager:"pnpm",file:n}:existsSync(t)?{manager:"npm",file:t}:fail(
|
|
1
|
+
import{createHash}from"node:crypto";import{existsSync,readdirSync,readFileSync,rmSync,statSync,writeFileSync}from"node:fs";import path from"node:path";import{runInherit}from"./exec.js";import{fail,say}from"./output.js";import{findOnPath,gradleInvocation,npmInvocation,pnpmInvocation}from"./platform.js";export const STAMP_FILE=path.join("node_modules",".webority-install.json");const INSTALL_FIELDS=["dependencies","devDependencies","optionalDependencies","peerDependencies","bundleDependencies","overrides","resolutions","pnpm","workspaces"];export const lockfileOf=e=>{const n=path.join(e,"pnpm-lock.yaml"),t=path.join(e,"package-lock.json");return existsSync(n)&&existsSync(t)?fail("appConfigInvalid","both pnpm-lock.yaml and package-lock.json exist: delete the one not in use"):existsSync(n)?{manager:"pnpm",file:n}:existsSync(t)?{manager:"npm",file:t}:fail("appConfigInvalid",`no package-lock.json or pnpm-lock.yaml in ${e}`)};const filesUnder=(e,n="")=>readdirSync(e,{withFileTypes:!0}).flatMap(t=>{const o=""===n?t.name:`${n}/${t.name}`;return t.isDirectory()?filesUnder(path.join(e,t.name),o):[o]}).sort();export const installHash=e=>{const{file:n}=lockfileOf(e),t=JSON.parse(readFileSync(path.join(e,"package.json"),"utf8")),o=createHash("sha256");o.update(`lockfile:${path.basename(n)}\0`),o.update(readFileSync(n)),o.update("\0package.json\0"),o.update(JSON.stringify(INSTALL_FIELDS.map(e=>[e,t[e]??null])));const s=path.join(e,"patches");if(existsSync(s))for(const e of filesUnder(s))o.update(`\0patches/${e}\0`),o.update(readFileSync(path.join(s,...e.split("/"))));return o.digest("hex")};export const readStamp=e=>{const n=path.join(e,STAMP_FILE);if(existsSync(n))try{return JSON.parse(readFileSync(n,"utf8"))}catch{return}};const podFix=e=>existsSync(path.join(e,"Gemfile"))?"cd ios && bundle exec pod install":"cd ios && pod install";export const podsStale=e=>{const n=path.join(e,"ios","Podfile.lock"),t=path.join(e,"ios","Pods","Manifest.lock");return!(!existsSync(n)||!existsSync(t))&&!readFileSync(n).equals(readFileSync(t))};export const checkSync=(e,n)=>{const t=[],o=readStamp(e),{file:s}=lockfileOf(e);if(o?o.hash!==installHash(e)&&t.push(`node_modules is stale: ${path.basename(s)}, package.json or patches/ changed since the last install. Fix: npx mobile-tools sync`):t.push("node_modules has no install stamp (never installed through mobile-tools). Fix: npx mobile-tools sync"),o){const n=changedNativePackages(o.nativePackages??{},nativePackages(e));n.length>0&&t.push(`node_modules native packages changed since the last install: ${n.join(", ")}. Fix: npx mobile-tools sync`)}return"darwin"===n&&podsStale(e)&&t.push(`ios/Pods is stale: ios/Podfile.lock differs from ios/Pods/Manifest.lock. Fix: ${podFix(e)}`),t};export const requireSynced=e=>{const n=checkSync(e,process.platform);n.length>0&&fail("installStale",n.join("\n"))};export const nativePackages=e=>{const n=path.join(e,"node_modules");if(!existsSync(n))return{};const t=[];for(const e of readdirSync(n,{withFileTypes:!0}))if(!e.name.startsWith(".")&&e.isDirectory())if(e.name.startsWith("@"))for(const o of readdirSync(path.join(n,e.name),{withFileTypes:!0}))o.isDirectory()&&t.push(path.join(n,e.name,o.name));else t.push(path.join(n,e.name));const o={};for(const e of t){const n=path.join(e,"android"),t=path.join(e,"package.json");if(existsSync(n)&&statSync(n).isDirectory()&&existsSync(t))try{const e=JSON.parse(readFileSync(t,"utf8"));e.name&&e.version&&(o[e.name]=e.version)}catch{}}return o};export const changedNativePackages=(e,n)=>[...new Set([...Object.keys(e),...Object.keys(n)])].filter(t=>e[t]!==n[t]).sort();export const writeStamp=e=>{const{manager:n}=lockfileOf(e),t={hash:installHash(e),manager:n,nativePackages:nativePackages(e)};return writeFileSync(path.join(e,STAMP_FILE),`${JSON.stringify(t,null,2)}\n`,"utf8"),t};export const installInvocation=(e,n)=>"pnpm"===e?pnpmInvocation(["install","--frozen-lockfile"],process.env,n,process.execPath):npmInvocation(["ci"],process.env,n,process.execPath);export const sync=(e,n)=>{const{manager:t}=lockfileOf(e),o=readStamp(e)?.nativePackages??nativePackages(e);say("sync: "+("pnpm"===t?"pnpm install --frozen-lockfile":"npm ci")),runInherit(installInvocation(t,n),e,`${t} install`);const s=writeStamp(e).nativePackages,a=changedNativePackages(o,s),i=path.join(e,"android"),c=existsSync(path.join(i,"app",".cxx"))||existsSync(path.join(i,"app","build"));if(a.length>0&&c){say(`sync: native packages changed (${a.join(", ")}): regenerating codegen`),runInherit(gradleInvocation(i,["generateCodegenArtifactsFromSchema"],n,process.env),i,"gradlew generateCodegenArtifactsFromSchema");for(const n of[path.join(i,"app",".cxx"),path.join(i,"app","build","generated","autolinking")])rmSync(n,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,n)}`)}if("darwin"===n&&existsSync(path.join(e,"ios","Podfile"))){const t=path.join(e,"ios","Podfile.lock"),o=path.join(e,"ios","Pods","Manifest.lock");if(!existsSync(t)||!existsSync(o)||podsStale(e)){const t=existsSync(path.join(e,"Gemfile")),o=findOnPath(t?"bundle":"pod",process.env,n);o||fail("toolMissing",`${t?"bundle":"pod"} not found on PATH; install CocoaPods, then ${podFix(e)}`),runInherit({file:o,args:t?["exec","pod","install"]:["install"]},path.join(e,"ios"),"pod install");const s=path.join(e,"ios","build","ModuleCache.noindex");existsSync(s)&&(rmSync(s,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,s)}`))}}say("sync: done")};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.0",
|
|
4
4
|
"description": "Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run and per-environment launcher icons, the same on Windows and macOS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|