@webority/mobile-tools 0.111.0 → 0.112.3
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 +44 -31
- package/lib/build.js +1 -1
- package/lib/cache.js +1 -1
- package/lib/cli.js +1 -1
- package/lib/config.js +1 -1
- package/lib/doctor.js +1 -1
- package/lib/ios.js +1 -1
- package/lib/otaNative.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/lib/xos.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,25 +20,36 @@ npx mobile-tools prebuild --same-env [--platform android|ios]
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- Merges `appsettings.json` with `appsettings.<Env>.json` into `appsettings.Compiled.json`.
|
|
23
|
-
- Stamps `COMMIT` (the short git sha, with `-dirty` when the app folder has changes
|
|
23
|
+
- Stamps `COMMIT` (the short git sha, with `-dirty` when the app folder has changes) and `BUILD_TIME` (UTC, ISO 8601).
|
|
24
24
|
- Fails, naming the key, when the merged settings lack `APP_NAME`, `APP_ID`, `ENVIRONMENT`, `BASE_URL`, `VERSION_DISPLAY` or `VERSION_INTERNAL`.
|
|
25
25
|
- Fails, naming every key, when a key listed in `requiredSettings` (`mobile-tools.config.json`) is absent or null in the merged settings. An empty string passes.
|
|
26
26
|
- Fails when `ENVIRONMENT` differs from the `--env` given, when `VERSION_INTERNAL` is not a positive whole number, or when an `APP_ID_SUFFIX` disagrees with the environment.
|
|
27
27
|
- Checks the app settings rules and warns about each break: a Staging or Production `BASE_URL` that is not https or points at this machine; `API_TIMEOUT`, `UPLOAD_TIMEOUT`, `MAX_RETRY_ATTEMPTS`, `LOGGING_ENABLED`, `PUSH_ENABLED`, `LOG_LEVEL` and `MIN_SUPPORTED_INTERNAL_VERSION` with the wrong type; a `MIN_SUPPORTED_INTERNAL_VERSION` above `VERSION_INTERNAL`; and a missing `PUSH_ENABLED` in an app that depends on `@react-native-firebase/messaging`. Set `MOBILE_TOOLS_STRICT_SETTINGS=1` to stop the prebuild on any of them.
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
- Writes only git-ignored files, and refuses a tracked file that still carries a managed value or an output that is not git-ignored, naming the file.
|
|
29
|
+
- **Android**: writes no tracked file. `android/app/build.gradle` reads the application id and the version from `appsettings.Compiled.json` with this loader, placed above `android {`:
|
|
30
|
+
|
|
31
|
+
```groovy
|
|
32
|
+
def compiledSettingsFile = rootProject.file("../appsettings.Compiled.json")
|
|
33
|
+
if (!compiledSettingsFile.isFile()) {
|
|
34
|
+
throw new GradleException("appsettings.Compiled.json is missing: run npx mobile-tools prebuild --env <Development|Staging|Production> in the app folder first")
|
|
35
|
+
}
|
|
36
|
+
def compiledSettings = new groovy.json.JsonSlurper().parse(compiledSettingsFile)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In `defaultConfig`: `applicationId compiledSettings.APP_ID`, `versionCode Integer.parseInt(String.valueOf(compiledSettings.VERSION_INTERNAL))`, `versionName String.valueOf(compiledSettings.VERSION_DISPLAY)`.
|
|
40
|
+
- `APP_ID` is the base id; each flavor's `applicationIdSuffix` adds the suffix.
|
|
31
41
|
- **iOS**:
|
|
32
|
-
-
|
|
33
|
-
- In `project.pbxproj`, sets `PRODUCT_BUNDLE_IDENTIFIER` (suffixed), `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`. It also sets `ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-<Env>` when that icon set exists.
|
|
42
|
+
- Writes `ios/appsettings.Compiled.xcconfig`, the project-level base configuration that Xcode and xOS Tools both read: `APP_NAME`, `PRODUCT_BUNDLE_IDENTIFIER` (suffixed), `MARKETING_VERSION`, `CURRENT_PROJECT_VERSION`, and `ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-<Env>` when that icon set exists. `project.pbxproj` holds none of these, and `Info.plist` reads the name as `$(APP_NAME)`.
|
|
34
43
|
- When another environment's icon set exists but this one's does not, prebuild fails, so a build never ships the wrong icon.
|
|
35
44
|
- Copies `<firebasePlistDir>/GoogleService-Info.<Env>.plist` to `ios/<name>/GoogleService-Info.plist`. Without that file for the environment it fails when the app depends on `@react-native-firebase/app` (the Crashlytics build phase needs it), and otherwise removes the copy.
|
|
36
45
|
- **Firebase on Android**: an app that depends on `@react-native-firebase/app` fails without `android/app/google-services.json`, whose absence would otherwise stop the Gradle build later with a less direct error.
|
|
37
|
-
- **Over-the-air updates
|
|
38
|
-
- Writes
|
|
46
|
+
- **Over-the-air updates**: OTA is on when the settings carry `OTA_UPDATE_BASE_URL` and the platform's component id (`OTA_ANDROID_COMPONENT_ID`, `OTA_IOS_COMPONENT_ID`). Otherwise OTA is off for the environment.
|
|
47
|
+
- Writes `android/app/src/<flavor>/AndroidManifest.xml` for the environment's flavor, with the `expo.modules.updates.*` meta-data, and `ios/<name>/Supporting/Expo.plist` as a whole plist document. Both are git-ignored, so neither is tracked.
|
|
48
|
+
- With OTA on, the manifest URL `<OTA_UPDATE_BASE_URL>/ota/manifest/<OTA_PROJECT_ID>/<component id>/<environment>`, the runtime version `VERSION_INTERNAL`, check on launch `ALWAYS`, launch wait 0, and the code-signing certificate with the key id read from it and `alg` `rsa-v1_5-sha256`.
|
|
49
|
+
- With OTA off, the Android manifest holds only `expo.modules.updates.ENABLED` `false`, and Expo.plist holds only `EXUpdatesEnabled` `<false/>`.
|
|
39
50
|
- `OTA_CODE_SIGNING_CERT` names the update server's PEM certificate, relative to the app folder. It is required, because the server signs every manifest and an app without the certificate cannot verify one. A missing `OTA_PROJECT_ID` also fails.
|
|
40
|
-
-
|
|
41
|
-
- Without `--platform`, it
|
|
51
|
+
- An app without environment flavors is refused: the flavor manifest is the only place these settings go.
|
|
52
|
+
- Without `--platform`, it writes the files of every platform folder the app has.
|
|
42
53
|
- `--same-env` recompiles the environment last compiled, which refreshes the build stamp.
|
|
43
54
|
- Running it twice gives the same native files as running it once. A file whose content did not change is not rewritten.
|
|
44
55
|
|
|
@@ -71,7 +82,7 @@ npx mobile-tools hooks install
|
|
|
71
82
|
### `run`
|
|
72
83
|
|
|
73
84
|
```bash
|
|
74
|
-
npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild] [--profiling]
|
|
85
|
+
npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild] [--profiling] [--app <path.app>]
|
|
75
86
|
```
|
|
76
87
|
|
|
77
88
|
`--profiling` installs the same debug build but serves production JavaScript with React's profiling renderer (Metro runs `--no-dev --minify` with `WEBORITY_PROFILING=1`). A Metro serving development JavaScript must be stopped first; mobile-tools refuses to reuse it.
|
|
@@ -91,15 +102,15 @@ npx mobile-tools run --env <Environment> [--platform android|ios] [--device <ser
|
|
|
91
102
|
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.
|
|
92
103
|
9. Sets `adb reverse tcp:8081` (and, for Development, the local API port). It then launches `<applicationId>/<namespace>.MainActivity`.
|
|
93
104
|
|
|
94
|
-
**iOS** (macOS only):
|
|
105
|
+
**iOS** (macOS only, for the Simulator):
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
2. Runs `prebuild`.
|
|
98
|
-
3. Builds with `xcodebuild -workspace ios/<name>.xcworkspace -scheme <name> -configuration Debug -destination id=<udid> -derivedDataPath ios/build ONLY_ACTIVE_ARCH=YES`.
|
|
99
|
-
4. Installs and launches the app.
|
|
100
|
-
5. Metro must be on 8081, where the iOS app looks.
|
|
107
|
+
`--app <path.app>` is required. The app is built with `build --artifact ios-sim` on a host with xOS Tools, then copied to the Mac. `run` never builds.
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
1. Installs the `--app` app on the booted simulator with `xcrun simctl`.
|
|
110
|
+
2. Starts Metro on 8081, where the iOS app looks, or reuses one already serving this app.
|
|
111
|
+
3. Launches the app.
|
|
112
|
+
|
|
113
|
+
A physical iPhone installs through xOS Tools (`xos install`, `xos launch`), not through `run`.
|
|
103
114
|
|
|
104
115
|
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.
|
|
105
116
|
|
|
@@ -107,14 +118,14 @@ Every program is started by absolute path with an argument list, never through a
|
|
|
107
118
|
|
|
108
119
|
- `android/gradlew` on macOS. `android\gradlew.bat` on Windows, through `cmd.exe` named by path, because Node will not start a `.bat` file any other way.
|
|
109
120
|
- `adb` from `ANDROID_HOME/platform-tools`, falling back to `PATH`.
|
|
110
|
-
- `/usr/bin/xcrun
|
|
121
|
+
- `/usr/bin/xcrun`.
|
|
111
122
|
|
|
112
123
|
`NoDefaultCurrentDirectoryInExePath` is removed from every child's environment. On any other OS an iOS command stops with one line.
|
|
113
124
|
|
|
114
125
|
### `build`
|
|
115
126
|
|
|
116
127
|
```bash
|
|
117
|
-
npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--ota-test] [--json] [-- <gradle args>]
|
|
128
|
+
npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--ota-test] [--arch x86_64|arm64] [--json] [-- <gradle args>]
|
|
118
129
|
```
|
|
119
130
|
|
|
120
131
|
`--ota-test` appends `-otatest` to the OTA runtime version, so only builds of this artifact receive an update published for that version. A later plain build restamps the native files.
|
|
@@ -122,14 +133,15 @@ npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug]
|
|
|
122
133
|
1. Stops on a stale tree, as `run` does.
|
|
123
134
|
2. Runs `prebuild` for the environment and platform.
|
|
124
135
|
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.
|
|
125
|
-
4. iOS (`ios-sim`): runs `
|
|
136
|
+
4. iOS (`ios-sim`): runs `xos build --pods` then `xos app` for the simulator, into `ios/build/xos/<Configuration>-iphonesimulator`. `<Configuration>` is `Debug` with `--debug` and `Release` otherwise.
|
|
126
137
|
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.
|
|
127
138
|
|
|
128
139
|
Notes:
|
|
129
140
|
|
|
130
141
|
- `apk` and `aab` are Android release builds of every ABI unless `--debug` is given.
|
|
131
142
|
- `aab` is release only.
|
|
132
|
-
- `ios-sim`
|
|
143
|
+
- `ios-sim` runs on any host with `xos` on `PATH`, and is unsigned.
|
|
144
|
+
- `--arch` applies to `ios-sim` only, and defaults to `x86_64`. Use `arm64` for an Apple-silicon Mac. A wrong architecture fails at `simctl install` with Apple's own message.
|
|
133
145
|
- Store `ipa` builds are not this command's job: iOS store builds run on the build server through xOS Tools.
|
|
134
146
|
- Arguments after `--` go to Gradle unchanged for apk and aab (for example `-- --no-daemon --max-workers=3`); each must be a plain token, and ios-sim refuses them.
|
|
135
147
|
- With CI=true, a failed build also prints the whole log to stderr, so a CI job log carries it.
|
|
@@ -157,7 +169,7 @@ npx mobile-tools devices [--platform android|ios] [--json]
|
|
|
157
169
|
|
|
158
170
|
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.
|
|
159
171
|
2. Android: lists every ready device once, however many serials reach it.
|
|
160
|
-
3. iOS (macOS only): lists the booted
|
|
172
|
+
3. iOS (macOS only): lists the booted simulators from `xcrun simctl`. Physical iPhones are not listed, because `run` installs only simulator apps.
|
|
161
173
|
|
|
162
174
|
Human output, one line per device:
|
|
163
175
|
|
|
@@ -174,7 +186,7 @@ The `(also ...)` part appears only when the device has more than one serial. Eac
|
|
|
174
186
|
{
|
|
175
187
|
"devices": [
|
|
176
188
|
{ "platform": "android", "serial": "<serial>", "serials": ["<serial>"], "model": "<model>", "abi": "<abi>" },
|
|
177
|
-
{ "platform": "ios", "udid": "<udid>", "name": "<name>", "kind": "simulator
|
|
189
|
+
{ "platform": "ios", "udid": "<udid>", "name": "<name>", "kind": "simulator", "serials": [], "model": "<model>" }
|
|
178
190
|
]
|
|
179
191
|
}
|
|
180
192
|
```
|
|
@@ -202,11 +214,11 @@ Checks the toolchain and the app tree, one check per code:
|
|
|
202
214
|
- `autolinkingCache`: the Android autolinking cache was made for this tree.
|
|
203
215
|
- `ndkHook`: `android/build.gradle` gives library modules the app's NDK.
|
|
204
216
|
- `firebaseConfig`: `android/app/google-services.json` exists, when the app uses `@react-native-firebase`.
|
|
205
|
-
- `xcode`: `xcode-select` has an Xcode (macOS).
|
|
206
|
-
- `
|
|
207
|
-
- `
|
|
217
|
+
- `xcode`: `xcode-select` has an Xcode (macOS, for the Simulator).
|
|
218
|
+
- `xos`: `xos` is on `PATH` and `xos doctor --json` exits 0 (a host that is not macOS).
|
|
219
|
+
- `cocoapods`: CocoaPods (`bundle exec pod` or `pod`) is on `PATH` (macOS and a host with xos).
|
|
208
220
|
|
|
209
|
-
`--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
|
|
221
|
+
`--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 when `ios/` exists on macOS or on a host with `xos` on `PATH`.
|
|
210
222
|
|
|
211
223
|
Human output, one line per check, then a summary:
|
|
212
224
|
|
|
@@ -321,11 +333,11 @@ With `--json`, the failure is one document on stdout instead:
|
|
|
321
333
|
| `appConfigInvalid` | A file of the app is missing or wrong. |
|
|
322
334
|
| `toolMissing` | A program or package is not installed or not on `PATH`. |
|
|
323
335
|
| `toolFailed` | A program ran and failed, outside a build. |
|
|
324
|
-
| `buildFailed` | Gradle or `
|
|
336
|
+
| `buildFailed` | Gradle or `xos` failed; the message gives the root error and the log path. |
|
|
325
337
|
| `artifactMissing` | A build succeeded but produced no expected file. |
|
|
326
338
|
| `deviceNotFound` | No ready device, or none matches `--device`. |
|
|
327
339
|
| `deviceAmbiguous` | Several devices and no terminal to ask. |
|
|
328
|
-
| `platformUnavailable` |
|
|
340
|
+
| `platformUnavailable` | The Simulator run off macOS. |
|
|
329
341
|
| `installStale` | `node_modules` or `ios/Pods` do not match the lockfiles. |
|
|
330
342
|
| `metroUnavailable` | Metro could not be reused, started or restarted. |
|
|
331
343
|
|
|
@@ -372,7 +384,6 @@ It returns Expo's `getDefaultConfig` for the app, with `console` calls dropped f
|
|
|
372
384
|
"android:staging": "mobile-tools run --env Staging",
|
|
373
385
|
"android:stagingApk": "mobile-tools build --env Staging --artifact apk",
|
|
374
386
|
"android:productionAab": "mobile-tools build --env Production --artifact aab",
|
|
375
|
-
"ios": "mobile-tools run --env Development --platform ios",
|
|
376
387
|
"prebuild": "mobile-tools prebuild --same-env",
|
|
377
388
|
"sync": "mobile-tools sync",
|
|
378
389
|
"postinstall": "mobile-tools sync --stamp"
|
|
@@ -380,4 +391,6 @@ It returns Expo's `getDefaultConfig` for the app, with `console` calls dropped f
|
|
|
380
391
|
}
|
|
381
392
|
```
|
|
382
393
|
|
|
394
|
+
There is no `ios` script: an iOS run takes two steps on two hosts. On a host with `xos`, run `npx mobile-tools build --env <Env> --artifact ios-sim --debug`; then on the Mac, run `npx mobile-tools run --env <Env> --platform ios --app <path.app>`.
|
|
395
|
+
|
|
383
396
|
`postinstall` keeps the stamp accurate after any install. After a pull, `npx mobile-tools sync` is still the one command to run: it also resets native codegen when a native package moved, which a plain install does not.
|
package/lib/build.js
CHANGED
|
@@ -1 +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
|
|
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}from"./platform.js";import{prebuild}from"./prebuild.js";import{ensurePods,requireSynced}from"./sync.js";import{prependXosShims,xosPath,xosSimulatorBuild}from"./xos.js";const buildAndroid=async(o,a,i)=>{const{appRoot:r,env:s}=o;requireSynced(r);const t=loadToolsConfig(r),e=path.join(r,"android");existsSync(path.join(e,"app","build.gradle"))||fail("appConfigInvalid",`android/app/build.gradle not found in ${r}`),prebuild({appRoot:r,env:s,platform:"android",otaTest:o.otaTest});const l=gradleTask(s,t.flavors,a,i),n=path.join(logDir(r),`gradle-${s}-${logStamp()}.log`);await buildOrFail(gradleInvocation(e,[`:app:${l}`,...o.gradleArgs],process.platform,process.env),e,n,`gradle ${l}`);const p=gradleOutputDir(r,s,t.flavors,a,i),d=existsSync(p)?readdirSync(p,{withFileTypes:!0}).filter(o=>o.isFile()&&o.name.endsWith(`.${a}`)).map(o=>path.resolve(p,o.name)).sort():[];0===d.length&&fail("artifactMissing",`the build produced no ${a} in ${p}`);const g=mappingFile(r,s,t.flavors);return{artifacts:d,mapping:"release"===i&&existsSync(g)?g:null,log:n}},buildIos=async(o,a)=>{o.gradleArgs.length>0&&fail("badInput","Gradle arguments after -- apply to apk and aab builds only");const{appRoot:i,env:r}=o,s=xosPath(process.env,process.platform);requireSynced(i);const t=loadToolsConfig(i),e=resolveIosProjectName(i,t)??fail("appConfigInvalid",`no ios/ folder in ${i}`);prebuild({appRoot:i,env:r,platform:"ios",otaTest:o.otaTest}),prependXosShims(s,i),ensurePods(i,process.platform);const l="release"===a?"Release":"Debug",n=path.join(logDir(i),`xos-${r}-${logStamp()}.log`);return{artifacts:[await xosSimulatorBuild(s,{appRoot:i,projectName:e,configuration:l,arch:o.arch??"x86_64"},n)],mapping:null,log:n}};export const build=async o=>{const a=o.debug?"debug":"release",i="ios-sim"===o.artifact?await buildIos(o,a):await buildAndroid(o,o.artifact,a);if(isJsonOutput())sayJson({artifacts:i.artifacts,mapping:i.mapping,log:i.log});else{for(const o of i.artifacts)say(`build: ${o}`);null!==i.mapping&&say(`build: mapping ${i.mapping}`)}};
|
package/lib/cache.js
CHANGED
|
@@ -1 +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}`)};
|
|
1
|
+
import{createHash}from"node:crypto";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";import{nativeBuildInputs}from"./prebuild.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)?createHash("sha256").update(s).update("\n").update(nativeBuildInputs(t,e)).digest("hex"):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{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] [--profiling]\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 --profiling serves production JavaScript with React's profiling renderer; a Metro serving development JavaScript must be stopped first.\n\n build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--ota-test] [--json] [-- <gradle args>]\n Prebuild, then build a release APK or App Bundle (Android, every ABI) or an iOS simulator app (
|
|
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";import{SIMULATOR_ARCHS}from"./xos.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] [--profiling] [--app <path.app>]\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 iOS (macOS): installs the simulator app --app names on a booted simulator, starts Metro and launches it; it never builds.\n --profiling serves production JavaScript with React's profiling renderer; a Metro serving development JavaScript must be stopped first.\n\n build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--ota-test] [--arch x86_64|arm64] [--json] [-- <gradle args>]\n Prebuild, then build a release APK or App Bundle (Android, every ABI) or an iOS simulator app through xOS Tools (xos on PATH).\n --debug builds the debug variant (apk or ios-sim only).\n --ota-test suffixes the OTA runtime version with -otatest, so only this build receives an update published for it.\n Arguments after -- go to Gradle unchanged (apk and aab only), for example -- --no-daemon --max-workers=3.\n\n devices [--platform android|ios] [--json]\n Connect local MuMu emulators, then list every ready device once (simulators for iOS).\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(),a=t.indexOf("--"),s=a<0?t:t.slice(0,a);switch(setJsonOutput(JSON_COMMANDS.has(o??"")&&s.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"},profiling:{type:"boolean"},app:{type:"string"}}),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,profiling:!0===e.profiling,app:"string"==typeof e.app?e.app:void 0})}case"build":{const e=t.indexOf("--"),o=e<0?t:t.slice(0,e),a=e<0?[]:t.slice(e+1),{values:s}=parse(o,{env:{type:"string"},artifact:{type:"string"},debug:{type:"boolean"},"ota-test":{type:"boolean"},json:{type:"boolean"},arch:{type:"string"}}),i=envOption(s.env)??fail("badInput","build needs --env <Development|Staging|Production>"),r=artifactOption(s.artifact);!0===s.debug&&"aab"===r&&fail("badInput","--debug builds an apk or an ios-sim app; an App Bundle is release only"),"ios-sim"===r&&a.length>0&&fail("badInput","Gradle arguments after -- apply to apk and aab builds only");const p="string"==typeof s.arch?s.arch:void 0;"ios-sim"!==r&&void 0!==p&&fail("badInput","--arch applies to ios-sim only");const l="ios-sim"===r?SIMULATOR_ARCHS.find(e=>e===(p??"x86_64"))??fail("badInput","--arch must be x86_64 or arm64"):void 0;return void await build({appRoot:n,env:i,artifact:r,debug:!0===s.debug,otaTest:!0===s["ota-test"],gradleArgs:a,arch:l})}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=t=>ENVIRONMENTS.find(e=>e.toLowerCase()===t.toLowerCase())??fail("badInput",`unknown environment "${t}": use ${ENVIRONMENTS.join(", ")}`);export const envSuffix=t=>SUFFIX[t];export const flavorOf=t=>t.toLowerCase();export const applicationId=(t,e)=>`${t}${envSuffix(e)}`;
|
|
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=t=>ENVIRONMENTS.find(e=>e.toLowerCase()===t.toLowerCase())??fail("badInput",`unknown environment "${t}": use ${ENVIRONMENTS.join(", ")}`);export const envSuffix=t=>SUFFIX[t];export const flavorOf=t=>t.toLowerCase();export const applicationId=(t,e)=>`${t}${envSuffix(e)}`;const capitalize=t=>`${t[0].toUpperCase()}${t.slice(1)}`;export const variantName=(t,e,n)=>e?`${flavorOf(t)}${capitalize(n)}`:n;export const gradleTask=(t,e,n,i)=>`${"apk"===n?"assemble":"bundle"}${capitalize(variantName(t,e,i))}`;export const gradleOutputDir=(t,e,n,i,o)=>{const s=path.join(t,"android","app","build","outputs");return"aab"===i?path.join(s,"bundle",variantName(e,n,o)):n?path.join(s,"apk",flavorOf(e),o):path.join(s,"apk",o)};export const mappingFile=(t,e,n)=>path.join(t,"android","app","build","outputs","mapping",variantName(e,n,"release"),"mapping.txt");const readJson=t=>{try{return JSON.parse(readFileSync(t,"utf8"))}catch(e){return fail("appConfigInvalid",`cannot read ${t}: ${e.message}`)}},isRecord=t=>"object"==typeof t&&null!==t&&!Array.isArray(t),CONFIG_KEYS={iosProjectName:"string",flavors:"boolean",localApiPort:"port",iconSource:"string",firebasePlistDir:"string",requiredSettings:"settingNames"},SETTING_NAME_PATTERN=/^[A-Z][A-Z0-9_]*$/,checkSettingNames=(t,e)=>{Array.isArray(t)&&!t.some(t=>"string"!=typeof t)||fail("appConfigInvalid",`${e}: "requiredSettings" must be an array of setting names such as ["LOG_LEVEL"]`);const n=new Set;for(const i of t)SETTING_NAME_PATTERN.test(i)||fail("appConfigInvalid",`${e}: "requiredSettings" holds ${JSON.stringify(i)}; a setting name is upper case letters, digits and _`),n.has(i)&&fail("appConfigInvalid",`${e}: "requiredSettings" lists ${i} twice`),REQUIRED_SETTINGS.includes(i)&&fail("appConfigInvalid",`${e}: "requiredSettings" lists ${i}, which prebuild always requires; remove it`),n.add(i)};export const parseToolsConfig=(t,e=CONFIG_FILE)=>{if(!isRecord(t))return fail("appConfigInvalid",`${e} must hold a JSON object`);for(const[n,i]of Object.entries(t)){const t=Object.hasOwn(CONFIG_KEYS,n)?CONFIG_KEYS[n]:void 0;if(!t)return fail("appConfigInvalid",`${e}: unknown key "${n}" (known: ${Object.keys(CONFIG_KEYS).join(", ")})`);if("settingNames"===t){checkSettingNames(i,e);continue}if(!("port"===t?Number.isInteger(i)&&i>0&&i<65536:"string"===t?"string"==typeof i&&""!==i.trim():typeof i===t)){fail("appConfigInvalid",`${e}: "${n}" must be ${{port:"a port number",string:"a non-empty string",boolean:"true or false"}[t]}`)}}return{iosProjectName:t.iosProjectName,flavors:t.flavors??!0,localApiPort:t.localApiPort,iconSource:t.iconSource,firebasePlistDir:t.firebasePlistDir??path.join("ios","firebase"),requiredSettings:t.requiredSettings??[]}};export const loadToolsConfig=t=>{const e=path.join(t,CONFIG_FILE);return parseToolsConfig(existsSync(e)?readJson(e):{})};export const resolveIosProjectName=(t,e)=>{if(e.iosProjectName)return e.iosProjectName;const n=path.join(t,"ios");if(!existsSync(n))return;const i=readdirSync(n).filter(t=>t.endsWith(".xcodeproj"));return 1!==i.length?fail("appConfigInvalid",`found ${i.length} .xcodeproj folders in ios/: set "iosProjectName" in ${CONFIG_FILE}`):i[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=(t,e,n,i)=>{if(!isRecord(t))return fail("appConfigInvalid","appsettings.json must hold a JSON object");if(!isRecord(e))return fail("appConfigInvalid",`appsettings.${n}.json must hold a JSON object`);const o={...t,...e},s=REQUIRED_SETTINGS.filter(t=>void 0===o[t]||null===o[t]||""===o[t]);s.length>0&&fail("appConfigInvalid",`appsettings (${n}) is missing required key(s): ${s.join(", ")}`);const a=i.filter(t=>void 0===o[t]||null===o[t]);return a.length>0&&fail("appConfigInvalid",`appsettings (${n}) is missing setting(s) mobile-tools.config.json requires: ${a.join(", ")}`),o.ENVIRONMENT!==n&&fail("appConfigInvalid",`appsettings.${n}.json sets ENVIRONMENT to ${JSON.stringify(o.ENVIRONMENT)}; it must be "${n}"`),void 0!==o.APP_ID_SUFFIX&&o.APP_ID_SUFFIX!==envSuffix(n)&&fail("appConfigInvalid",`appsettings (${n}) sets APP_ID_SUFFIX to ${JSON.stringify(o.APP_ID_SUFFIX)}; ${n} builds always use ${JSON.stringify(envSuffix(n))}. Remove the key or correct it.`),/^[1-9][0-9]*$/.test(String(o.VERSION_INTERNAL))||fail("appConfigInvalid",`VERSION_INTERNAL must be a positive whole number, not ${JSON.stringify(o.VERSION_INTERNAL)}`),APP_ID_PATTERN.test(String(o.APP_ID))||fail("appConfigInvalid",`APP_ID ${JSON.stringify(o.APP_ID)} is not a reverse-domain id such as com.example.app`),o};export const loadAppSettings=(t,e,n)=>{const i=path.join(t,"appsettings.json"),o=path.join(t,`appsettings.${e}.json`);return existsSync(i)||fail("appConfigInvalid",`no appsettings.json in ${t}: run mobile-tools from the app's root folder`),existsSync(o)||fail("appConfigInvalid",`appsettings.${e}.json not found: every buildable environment declares its own file`),mergeAppSettings(readJson(i),readJson(o),e,n)};export const baseAppId=t=>{const e=path.join(t,"appsettings.json");if(!existsSync(e))return fail("appConfigInvalid",`no appsettings.json in ${t}: run mobile-tools from the app's root folder`);const n=readJson(e),i=isRecord(n)?n.APP_ID:void 0;return"string"==typeof i&&APP_ID_PATTERN.test(i)?i:fail("appConfigInvalid",`APP_ID ${JSON.stringify(i)} in appsettings.json is not a reverse-domain id such as com.example.app`)};export const lastCompiledEnvironment=t=>{const e=path.join(t,COMPILED_FILE);if(!existsSync(e))return fail("appConfigInvalid",`--same-env: no ${COMPILED_FILE} yet; pass --env <Environment> once`);const n=readJson(e);return isRecord(n)&&"string"==typeof n.ENVIRONMENT?parseEnvironment(n.ENVIRONMENT):fail("appConfigInvalid",`--same-env: ${COMPILED_FILE} has no ENVIRONMENT`)};export const localApiPort=(t,e)=>{if(void 0!==e.localApiPort)return e.localApiPort;let n;try{n=new URL(t)}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/doctor.js
CHANGED
|
@@ -1 +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
|
|
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}from"./platform.js";import{checkNdkHook,usesFirebase}from"./prebuild.js";import{checkSync}from"./sync.js";import{xosPath}from"./xos.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",XOS_FIX="install xOS Tools (xos) and put it on PATH",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 s=e[1].split(/[._+-]/),t=Number(s[0]),n=1===t?Number(s[1]):t;return Number.isInteger(n)?n:void 0};export const gradleSdkVersions=o=>{const e={},s=/ndkVersion\s*=\s*["']([^"']+)["']/.exec(o),t=/buildToolsVersion\s*=\s*["']([^"']+)["']/.exec(o),n=/compileSdkVersion\s*=\s*(\d+)/.exec(o);return s&&(e.ndk=s[1]),t&&(e.buildTools=t[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,s)=>{let t;try{t=JSON.parse(o)}catch{return"android/build/generated/autolinking/autolinking.json does not parse"}if("string"==typeof t?.root&&!samePath(t.root,e))return`the autolinking cache was made for ${t.root}, not ${e}`;for(const[o,e]of Object.entries(t?.dependencies??{}))if("string"==typeof e?.root&&!s(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,s)=>({code:o,status:"problem",message:e,fix:s}),probe=(o,e,s)=>{try{return s()}catch(s){return problem(o,s instanceof Error?s.message:String(s),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 s=path.join(o,"android","build.gradle");if(!existsSync(s))return skipped(e.code,"no android/build.gradle");const t=process.env.ANDROID_HOME;if(!t)return skipped(e.code,"ANDROID_HOME is not set");const n=gradleSdkVersions(readFileSync(s,"utf8"))[e.field];if(void 0===n)return skipped(e.code,`${e.key} is not declared in android/build.gradle`);const r=e.folder(t,n);return existsSync(r)?ok(e.code,r):problem(e.code,`${r} is missing`,e.install(n))}),androidChecks=o=>{const{appRoot:e}=o,s=[];s.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")})),s.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 t={};s.push(probe("adb",ADB_FIX,()=>{const o=adbPath(process.env,process.platform),s=capture({file:o,args:["version"]},e);if(0!==s.status)return problem("adb",outputOf(s),ADB_FIX);t.path=o;const n=s.stdout.split(/\r?\n/).find(o=>o.startsWith("Version"));return ok("adb",n?`${o}, ${n.trim()}`:o)}));const n=t.path;s.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),s=`${o.stdout}\n${o.stderr}`.split(/\r?\n/).find(o=>o.includes("doesn't match this client"));return void 0===s?ok("adbServer","one adb server version"):problem("adbServer",`another adb restarted the adb server at a different version: ${s.trim()}`,"use one adb, the SDK platform-tools one: MuMu ships its own adb")})),s.push(probe("jdk",JDK_FIX,()=>{const o=process.env.JAVA_HOME,s="win32"===process.platform?"java.exe":"java",t=o?path.join(o,"bin",s):void 0,n=void 0!==t&&existsSync(t)?t: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)s.push(sdkCheck(e,o));s.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 s=capture({file:o,args:["--version"]},e).stdout.trim(),t=/^(\d+)\.(\d+)/.exec(s);if(!t)return problem("ninja",`ninja --version printed no version at ${o}`,NINJA_FIX);return Number(t[1])>1||1===Number(t[1])&&Number(t[2])>=11?ok("ninja",`ninja ${s} at ${o}`):problem("ninja",`ninja ${s} at ${o} is older than 1.11`,NINJA_FIX)})),s.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 s.push(probe("autolinkingCache",r,()=>{const s=path.join(e,AUTOLINKING_FILE);if(!existsSync(s))return ok("autolinkingCache","no autolinking cache yet");const t=autolinkingProblem(readFileSync(s,"utf8"),e,existsSync);if(void 0===t)return ok("autolinkingCache","matches this tree");if(!o.fix)return problem("autolinkingCache",t,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"}})),s.push(probe("ndkHook","add the subprojects block the message names to android/build.gradle",()=>(checkNdkHook(e),ok("ndkHook","library modules get the app NDK")))),s.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")})),s},xcodeCheck=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)}),xosCheck=o=>probe("xos",XOS_FIX,()=>{const e=xosPath(process.env,process.platform),s=capture({file:e,args:["doctor","--json"]},o);return 0===s.status?ok("xos",e):problem("xos",s.stderr.trim().split(/\r?\n/)[0]||`xos doctor exited with status ${s.status}`,"run xos doctor and install what it names")});export const iosChecks=(o,e)=>["darwin"===e?xcodeCheck(o):xosCheck(o),probe("cocoapods","install CocoaPods",()=>{const s=existsSync(path.join(o,"Gemfile"))?"bundle":"pod",t=findOnPath(s,process.env,e);return void 0===t?problem("cocoapods",`${s} not found on PATH`,"install CocoaPods"):ok("cocoapods",t)})];export const doctor=async o=>{const e=path.join(o.appRoot,"android"),s=path.join(o.appRoot,"ios");"android"!==o.platform||existsSync(e)||fail("appConfigInvalid",`no android/ folder in ${o.appRoot}`);const t=[];("android"===o.platform||void 0===o.platform&&existsSync(e))&&t.push(...androidChecks(o));const n="darwin"===process.platform||void 0!==findOnPath("xos",process.env,process.platform);("ios"===o.platform||void 0===o.platform&&n&&existsSync(s))&&t.push(...iosChecks(o.appRoot,process.platform));const r=t.filter(o=>"problem"===o.status).length;if(isJsonOutput())sayJson({checks:t,problems:r});else{for(const o of t){const e="problem"===o.status&&o.fix?` Fix: ${o.fix}`:"";say(`doctor: ${o.status} ${o.code}: ${o.message}${e}`)}say(0===r?"doctor: ready":`doctor: ${r} problem(s)`)}r>0&&(process.exitCode=1)};
|
package/lib/ios.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from"node:path";import{capture}from"./exec.js";import{fail}from"./output.js";import{XCRUN}from"./platform.js";export const PLUTIL="/usr/bin/plutil";export const parseSimctlBooted=t=>{const e=JSON.parse(t);return Object.values(e.devices??{}).flat().filter(t=>"Booted"===t.state).map(t=>({udid:t.udid,name:t.name,kind:"simulator",serials:[t.udid],model:t.name}))};export const listAppleDevices=t=>{const e=capture({file:XCRUN,args:["simctl","list","-j","devices","booted"]},t);if(0!==e.status)return fail("toolFailed",`xcrun simctl failed: ${e.stderr.trim()}`);const o=parseSimctlBooted(e.stdout);return 0===o.length?fail("deviceNotFound","no booted simulator: boot one with xcrun simctl boot <name>; a physical iPhone installs through xOS Tools (xos install, xos launch)"):o};export const describeAppleDevice=t=>`${t.name} (${t.kind}, ${t.udid})`;export const appBundleId=(t,e)=>{const o=capture({file:PLUTIL,args:["-extract","CFBundleIdentifier","raw","-o","-",path.join(t,"Info.plist")]},e),i=o.stdout.trim();return 0!==o.status||""===i?fail("appConfigInvalid",`${t} has no readable CFBundleIdentifier in Info.plist: rebuild it with mobile-tools build --env <Env> --artifact ios-sim --debug`):i};
|
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("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=")),
|
|
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 a.endsWith(" OTA Updates")?fail("appConfigInvalid",`${t} is the shared OTA certificate (CN=${a}), 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 the repository that owns the OTA signing keys`):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,a)=>{const o=t.OTA_UPDATE_BASE_URL;if(!isSet(o)||!isSet(t[COMPONENT_KEY[n]]))return;const s=componentIdFor(t,n),r=t.OTA_PROJECT_ID;if(!isSet(r)||!WHOLE_NUMBER.test(String(r)))return fail("appConfigInvalid",`OTA_PROJECT_ID must be the update server's project id (a positive whole number), not ${JSON.stringify(r)}`);const p=t.OTA_CODE_SIGNING_CERT;if("string"!=typeof p||""===p.trim())return fail("appConfigInvalid","OTA_CODE_SIGNING_CERT must name the PEM certificate file, relative to the app folder");const d=path.join(e,p);if(!existsSync(d))return fail("appConfigInvalid",`OTA_CODE_SIGNING_CERT ${p} not found in ${e}`);const c=readFileSync(d,"utf8").replace(/\r\n/g,"\n").trim();return c.startsWith("-----BEGIN CERTIFICATE-----")?{updateUrl:`${String(o).replace(/\/+$/,"")}/ota/manifest/${r}/${s}/${i}`,runtimeVersion:a?`${String(t.VERSION_INTERNAL)}-otatest`:String(t.VERSION_INTERNAL),certificate:c,keyId:codeSigningKeyIdOf(c,p)}:fail("appConfigInvalid",`${p} is not a PEM certificate`)};const escapeXml=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");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})]];export const androidOtaManifest=e=>'<?xml version="1.0" encoding="utf-8"?>\n\x3c!-- Written by mobile-tools prebuild for this flavor\'s environment. Not tracked: run prebuild to change it. --\x3e\n<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n <application>\n'+(e?androidOtaMetaData(e):[["expo.modules.updates.ENABLED","false"]]).map(([e,t])=>` <meta-data android:name="${e}" android:value="${escapeXml(t).replace(/\n/g," ")}" />\n`).join("")+" </application>\n</manifest>\n";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>`};export const expoPlist=e=>'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n'+(e?iosOtaPlistKeys(e):[["EXUpdatesEnabled",!1]]).map(([e,t])=>`\t<key>${escapeXml(e)}</key>\n\t${plistValue(t,"\t","\n")}\n`).join("")+"</dict>\n</plist>\n";
|
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,t)=>{const o=Object.keys(n).find(n=>n.toLowerCase()===t.toLowerCase());return void 0===o?void 0:n[o]};export const childEnv=n=>{const t={};for(const[o,e]of Object.entries(n))"nodefaultcurrentdirectoryinexepath"!==o.toLowerCase()&&(t[o]=e);return t};export const findOnPath=(n,t,o,e=existsSync)=>{const i=pathFor(o),r=readEnv(t,"PATH")??"",s="win32"===o?`${n}.exe`:n;for(const n of r.split("win32"===o?";":":")){if(""===n)continue;const t=i.join(n,s);if(e(t))return t}};export const adbPath=(n,t,o=existsSync)=>{const e=pathFor(t),i=readEnv(n,"ANDROID_HOME");if(i){const n=e.join(i,"platform-tools","win32"===t?"adb.exe":"adb");if(o(n))return n}return findOnPath("adb",n,t,o)??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,t,o,e)=>{for(const n of t)SAFE_GRADLE_ARG.test(n)||fail("badInput",`refusing Gradle argument ${JSON.stringify(n)}: only plain task names and -Pkey=value`);const i=pathFor(o);if("win32"!==o)return{file:i.join(n,"gradlew"),args:t};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}" ${t.join(" ")}"`],windowsVerbatimArguments:!0}};export const requireDarwin=(n,t)=>{"darwin"!==n&&fail("platformUnavailable",`${t} needs macOS: the iOS Simulator and xcrun exist only there.`)};export const XCRUN="/usr/bin/xcrun";export const npmInvocation=(n,t,o,e,i=existsSync)=>{const r=pathFor(o),s=readEnv(t,"npm_execpath"),a=[s&&/npm-cli\.c?js$/.test(s)?s:void 0,"win32"===o?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,t,o,e,i=existsSync)=>{const r=readEnv(t,"npm_execpath");if(r&&/pnpm\.c?js$/.test(r)&&i(r))return{file:e,args:[r,...n]};const s=findOnPath("pnpm",t,o,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{checkAppSettings}from"./appSettingsRules.js";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 n=eolOf(e),o=e.split(n),i=new Map(Object.entries(t)),s=o.map(e=>{const t=/^\s*([A-Za-z0-9_.-]+)\s*[=:]/.exec(e);if(t&&i.has(t[1])){const e=t[1],n=i.get(e);return i.delete(e),`${e}=${n}`}return e});if(0===i.size)return s.join(n);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(n)};const pbxValue=e=>/^[A-Za-z0-9._/-]+$/.test(e)?e:`"${e.replace(/"/g,'\\"')}"`;export const rewritePbxproj=(e,t,n={})=>{let o=e;for(const[e,i]of Object.entries(t)){const t=new RegExp(`(\\s)${e} = [^;\\n]*;`,"g");t.test(o)||fail("appConfigInvalid",`${e} not found in project.pbxproj`);const s=n.quote?.includes(e)?`"${i.replace(/"/g,'\\"')}"`:pbxValue(i);o=o.replace(t,(t,n)=>`${n}${e} = ${s};`)}return o};const escapeXml=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");export const rewriteInfoPlistDisplayName=(e,t)=>{const n=/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/;return n.test(e)||fail("appConfigInvalid","CFBundleDisplayName not found in Info.plist"),e.replace(n,(e,n,o)=>`${n}${escapeXml(t)}${o}`)};const pbxObject=(e,t)=>{const n=new RegExp(`\\n\\s*${t}(?: /\\*[^\\n]*?\\*/)? = \\{`).exec(e);if(!n)return;let o=0;for(let t=n.index+n[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])o++;else if("}"===e[t]&&0===--o)return e.slice(n.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,n=/\n\s*(\w+)(?: \/\*[^\n]*?\*\/)? = \{\s*isa = PBXNativeTarget;/g;for(const[,o]of e.matchAll(n)){const n=pbxObject(e,o)??"",i=unquote(/\sproductType = ([^;\n]*);/.exec(n)?.[1]??"");if(!shipsInfoPlist(i))continue;const s=unquote(/\sname = ([^;\n]*);/.exec(n)?.[1]??o),r=/\sbuildConfigurationList = (\w+)/.exec(n)?.[1],a=r&&pbxObject(e,r)||"",p=/buildConfigurations = \(([^)]*)\)/.exec(a)?.[1]??"";for(const[n]of p.matchAll(/\w+(?= \/\*|,)/g)){const o=/\sINFOPLIST_FILE = ([^;\n]*);/.exec(pbxObject(e,n)??"")?.[1];o&&!t.has(unquote(o))&&t.set(unquote(o),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,n])=>{const o=new RegExp(`<key>${t}</key>\\s*(<[^>]*>)?([^<]*)`).exec(e);if(!o)return[];const i="<string>"===o[1]?o[2].trim():o[1]??o[2].trim();return i===`$(${n})`||i===`\${${n}}`?[]:[{key:t,value:i,setting:n}]});export const checkInfoPlistReferences=(e,t)=>{const n=path.join(e,"ios"),o=readFileSync(path.join(n,`${t}.xcodeproj`,"project.pbxproj"),"utf8"),i=[{target:t,file:`${t}/Info.plist`},...shippedInfoPlists(o)],s=[],r=new Set;for(const{target:t,file:o}of i){const i=o.replace(/^(\$\(SRCROOT\)|\$\{SRCROOT\}|\$\(PROJECT_DIR\))\//,"");if(/\$[({]/.test(i)){s.push(`target ${t}: INFOPLIST_FILE ${o} uses a build setting prebuild cannot resolve`);continue}const a=path.resolve(n,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:n}of fixedInfoPlistValues(readFileSync(a,"utf8")))s.push(`${p}: ${e} is "${t}", must be $(${n})`);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 n=[COMPILED_FILE,"android/gradle.properties"];return t&&n.push(ANDROID_MANIFEST),e&&(n.push(`ios/${e}/Info.plist`,`ios/${e}.xcodeproj/project.pbxproj`,`ios/${e}/GoogleService-Info.plist`),t&&n.push(expoPlistPath(e))),n};export const buildCommit=(e,t)=>{const n=findOnPath("git",process.env,process.platform);if(!n)return"nogit";const o=capture({file:n,args:["rev-parse","--short","HEAD"]},e);if(0!==o.status)return"nogit";const i=capture({file:n,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()?o.stdout.trim():`${o.stdout.trim()}-dirty`};const writeIfChanged=(e,t)=>(!existsSync(e)||readFileSync(e,"utf8")!==t)&&(writeFileSync(e,t,"utf8"),!0),report=(e,t,n)=>{say(`prebuild: ${n?"wrote":"unchanged"} ${path.relative(e,t)}`)};export const usesFirebase=e=>{const t=path.join(e,"package.json");if(!existsSync(t))return!1;const n=JSON.parse(readFileSync(t,"utf8"));return Boolean(n.dependencies?.["@react-native-firebase/app"])};export const checkNdkHook=e=>{const t=path.join(e,"android","build.gradle");if(!existsSync(t))return;const n=readFileSync(t,"utf8");/withId\(\s*["']com\.android\.library["']\s*\)/.test(n)&&/\.ndkVersion\s*=\s*rootProject\.ext\.ndkVersion/.test(n)||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,n,o,i)=>{const s=path.join(e,"android","gradle.properties");existsSync(s)||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 r=rewriteGradleProperties(readFileSync(s,"utf8"),{APP_NAME:String(t.APP_NAME),APP_ID:gradleAppId(t.APP_ID,n,o.flavors),VERSION_DISPLAY:String(t.VERSION_DISPLAY),VERSION_INTERNAL:String(t.VERSION_INTERNAL)});report(e,s,writeIfChanged(s,r));const a=otaNativeSettings(e,t,n,"android",i);if(a){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"),a)))}};export const stampIos=(e,t,n,o,i,s)=>{const r=path.join(e,"ios",i),a=path.join(r,"Info.plist"),p=path.join(e,"ios",`${i}.xcodeproj`,"project.pbxproj");for(const t of[a,p])existsSync(t)||fail("appConfigInvalid",`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,i);const c=`AppIcon-${n}`,l=path.join(r,"Images.xcassets"),d=existsSync(path.join(l,`${c}.appiconset`)),f=ENVIRONMENTS.filter(e=>e!==n&&existsSync(path.join(l,`AppIcon-${e}.appiconset`)));!d&&f.length>0&&fail("appConfigInvalid",`ios/${i}/Images.xcassets has AppIcon-${f[0]} but no ${c}: run mobile-tools icons`),report(e,a,writeIfChanged(a,rewriteInfoPlistDisplayName(readFileSync(a,"utf8"),String(t.APP_NAME))));const u={PRODUCT_BUNDLE_IDENTIFIER:applicationId(t.APP_ID,n),MARKETING_VERSION:String(t.VERSION_DISPLAY),CURRENT_PROJECT_VERSION:String(t.VERSION_INTERNAL)};d&&(u.ASSETCATALOG_COMPILER_APPICON_NAME=c),report(e,p,writeIfChanged(p,rewritePbxproj(readFileSync(p,"utf8"),u,{quote:["PRODUCT_BUNDLE_IDENTIFIER"]}))),d||say("prebuild: no AppIcon-<Env> sets, app icon left as the project sets it");const I=path.join(r,"GoogleService-Info.plist"),g=path.join(e,o.firebasePlistDir,`GoogleService-Info.${n}.plist`);if(!existsSync(g)&&usesFirebase(e)&&fail("appConfigInvalid",`${path.relative(e,g)} is missing; the app uses @react-native-firebase, and an iOS build without it reports no crashes`),existsSync(g)){const t=!existsSync(I)||!readFileSync(g).equals(readFileSync(I));t&©FileSync(g,I),report(e,I,t)}else existsSync(I)&&(rmSync(I),say(`prebuild: removed ${path.relative(e,I)}, no ${path.relative(e,g)}`));const S=otaNativeSettings(e,t,n,"ios",s);if(S){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"),S)))}};export const prebuild=e=>{const{appRoot:t,sameEnv:n,platform:o}=e,i=!0===e.otaTest;n&&e.env&&fail("badInput","pass --env or --same-env, not both");const s=n?lastCompiledEnvironment(t):e.env;if(!s)return fail("badInput","prebuild needs --env <Development|Staging|Production> or --same-env");const r=loadToolsConfig(t),a=resolveIosProjectName(t,r),p=loadAppSettings(t,s,r.requiredSettings);checkAppSettings(t,p,s);const c={...p,COMMIT:buildCommit(t,prebuildOutputs(a,Boolean(p.OTA_UPDATE_BASE_URL))),BUILD_TIME:(new Date).toISOString()};writeFileSync(path.join(t,COMPILED_FILE),`${JSON.stringify(c,null,2)}\n`,"utf8"),say(`prebuild: wrote ${COMPILED_FILE} (${s}, ${c.COMMIT})`);const l=existsSync(path.join(t,"android"));if(("android"===o||!o&&l)&&stampAndroid(t,c,s,r,i),"ios"===o||!o&&a){if(!a)return fail("appConfigInvalid",`no ios/ folder in ${t}`);stampIos(t,c,s,r,a,i)}return"Production"!==s&&say(`prebuild: tracked native files now carry ${s} values; run prebuild --env Production before committing them`),c};
|
|
1
|
+
import{copyFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{checkAppSettings}from"./appSettingsRules.js";import{applicationId,COMPILED_FILE,ENVIRONMENTS,flavorOf,lastCompiledEnvironment,loadAppSettings,loadToolsConfig,resolveIosProjectName}from"./config.js";import{capture}from"./exec.js";import{androidOtaManifest,expoPlist,otaNativeSettings}from"./otaNative.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";export const IOS_COMPILED_XCCONFIG="ios/appsettings.Compiled.xcconfig";export const androidFlavorManifest=e=>`android/app/src/${flavorOf(e)}/AndroidManifest.xml`;export const expoPlistPath=e=>`ios/${e}/Supporting/Expo.plist`;export const usesExpoUpdates=e=>dependsOn(e,"expo-updates");export const generatedNativeFiles=(e,t,i,o)=>{if("android"===o)return usesExpoUpdates(e)?[androidFlavorManifest(i)]:[];const n=[IOS_COMPILED_XCCONFIG];if(void 0!==t){const o=path.join(e,loadToolsConfig(e).firebasePlistDir,`GoogleService-Info.${i}.plist`);existsSync(o)&&n.push(`ios/${t}/GoogleService-Info.plist`),usesExpoUpdates(e)&&n.push(expoPlistPath(t))}return n};export const nativeBuildInputs=(e,t)=>{const i=path.join(e,COMPILED_FILE);if(!existsSync(i))return fail("appConfigInvalid",`${COMPILED_FILE} is missing: run npx mobile-tools prebuild --env <Development|Staging|Production> first`);const o=JSON.parse(readFileSync(i,"utf8")),n=resolveIosProjectName(e,loadToolsConfig(e)),s=generatedNativeFiles(e,n,o.ENVIRONMENT,t).filter(t=>existsSync(path.join(e,t)));return[...["APP_ID","APP_NAME","VERSION_DISPLAY","VERSION_INTERNAL"].map(e=>`${e}=${JSON.stringify(o[e])}`),...s.map(t=>`${t}=${JSON.stringify(readFileSync(path.join(e,t),"utf8"))}`)].join("\n")};const objectsOf=(e,t)=>[...e.matchAll(new RegExp(`\\n\\s*(\\w+)(?: /\\*([^\\n]*?)\\*/)? = \\{\\s*isa = ${t};`,"g"))].map(([,e,t=""])=>({id:e,name:t.trim()})),pbxObject=(e,t)=>{const i=new RegExp(`\\n\\s*${t}(?: /\\*[^\\n]*?\\*/)? = \\{`).exec(e);if(!i)return;let o=0;for(let t=i.index+i[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])o++;else if("}"===e[t]&&0===--o)return e.slice(i.index,t+1)},configurationsOf=(e,t)=>[...(/buildConfigurations = \(([^)]*)\)/.exec(pbxObject(e,t)??"")?.[1]??"").matchAll(/(\w+)(?: \/\* ([^*]*?) \*\/)?,/g)].map(([,e,t=""])=>({id:e,name:t})),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,i=/\n\s*(\w+)(?: \/\*[^\n]*?\*\/)? = \{\s*isa = PBXNativeTarget;/g;for(const[,o]of e.matchAll(i)){const i=pbxObject(e,o)??"",n=unquote(/\sproductType = ([^;\n]*);/.exec(i)?.[1]??"");if(!shipsInfoPlist(n))continue;const s=unquote(/\sname = ([^;\n]*);/.exec(i)?.[1]??o),a=/\sbuildConfigurationList = (\w+)/.exec(i)?.[1],r=a&&pbxObject(e,a)||"",p=/buildConfigurations = \(([^)]*)\)/.exec(r)?.[1]??"";for(const[i]of p.matchAll(/\w+(?= \/\*|,)/g)){const o=/\sINFOPLIST_FILE = ([^;\n]*);/.exec(pbxObject(e,i)??"")?.[1];o&&!t.has(unquote(o))&&t.set(unquote(o),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,i])=>{const o=new RegExp(`<key>${t}</key>\\s*(<[^>]*>)?([^<]*)`).exec(e);if(!o)return[];const n="<string>"===o[1]?o[2].trim():o[1]??o[2].trim();return n===`$(${i})`||n===`\${${i}}`?[]:[{key:t,value:n,setting:i}]});export const checkInfoPlistReferences=(e,t)=>{const i=path.join(e,"ios"),o=readFileSync(path.join(i,`${t}.xcodeproj`,"project.pbxproj"),"utf8"),n=[{target:t,file:`${t}/Info.plist`},...shippedInfoPlists(o)],s=path.resolve(i,`${t}/Info.plist`),a=[],r=new Set;for(const{target:t,file:o}of n){const n=o.replace(/^(\$\(SRCROOT\)|\$\{SRCROOT\}|\$\(PROJECT_DIR\))\//,"");if(/\$[({]/.test(n)){a.push(`target ${t}: INFOPLIST_FILE ${o} uses a build setting prebuild cannot resolve`);continue}const p=path.resolve(i,n);if(r.has(p))continue;r.add(p);const c=path.relative(e,p).replace(/\\/g,"/");if(!existsSync(p)){a.push(`${c} not found (INFOPLIST_FILE of target ${t})`);continue}const d=readFileSync(p,"utf8");for(const{key:e,value:t,setting:i}of fixedInfoPlistValues(d))a.push(`${c}: ${e} is "${t}", must be $(${i})`);p!==s||/<key>CFBundleDisplayName<\/key>\s*<string>\$\(APP_NAME\)<\/string>/.test(d)||a.push(`${c}: CFBundleDisplayName must be $(APP_NAME), which ios/appsettings.Compiled.xcconfig sets`)}a.length>0&&fail("appConfigInvalid",`Info.plist values that ignore what prebuild writes into appsettings.Compiled.xcconfig:\n ${a.join("\n ")}`)};const XCCONFIG_BASE=/baseConfigurationReference = \w+ \/\* appsettings\.Compiled\.xcconfig \*\//,MANAGED_BUILD_SETTINGS=["PRODUCT_BUNDLE_IDENTIFIER","MARKETING_VERSION","CURRENT_PROJECT_VERSION"];export const checkPbxprojSettings=(e,t)=>{const i=[],o=objectsOf(e,"PBXProject")[0];void 0===o&&i.push("project.pbxproj has no PBXProject object to carry the base configuration");const n=o?/\sbuildConfigurationList = (\w+)/.exec(pbxObject(e,o.id)??"")?.[1]:void 0;for(const{id:t,name:o}of n?configurationsOf(e,n):[])XCCONFIG_BASE.test(pbxObject(e,t)??"")||i.push(`project configuration ${t} /* ${o} */ does not use appsettings.Compiled.xcconfig as its base configuration: set the project-level configurations' base configuration to appsettings.Compiled.xcconfig`);const s=t?[...MANAGED_BUILD_SETTINGS,"ASSETCATALOG_COMPILER_APPICON_NAME"]:MANAGED_BUILD_SETTINGS;for(const{id:t,name:o}of objectsOf(e,"XCBuildConfiguration")){const n=pbxObject(e,t)??"";for(const e of s)new RegExp(`\\s${e} = `).test(n)&&i.push(`configuration ${t} /* ${o} */ sets ${e}: remove it; ios/appsettings.Compiled.xcconfig sets it`);/\sINFOPLIST_KEY_CFBundleDisplayName = /.test(n)&&i.push(`configuration ${t} /* ${o} */ sets INFOPLIST_KEY_CFBundleDisplayName: remove it; Info.plist reads $(APP_NAME), which ios/appsettings.Compiled.xcconfig sets`)}i.length>0&&fail("appConfigInvalid",`project.pbxproj sets values that ios/appsettings.Compiled.xcconfig owns:\n ${i.join("\n ")}`)};const XCCONFIG_UNSAFE=/\/\/|[$";]/,xcconfigText=(e,t,i)=>{const o=[["APP_NAME",String(e.APP_NAME)],["PRODUCT_BUNDLE_IDENTIFIER",applicationId(String(e.APP_ID),t)],["MARKETING_VERSION",String(e.VERSION_DISPLAY)],["CURRENT_PROJECT_VERSION",String(e.VERSION_INTERNAL)],...i?[["ASSETCATALOG_COMPILER_APPICON_NAME",`AppIcon-${t}`]]:[]];for(const[e,t]of o)XCCONFIG_UNSAFE.test(t)&&fail("appConfigInvalid",`${e} must not contain // $ " or ;: it is written unquoted into ios/appsettings.Compiled.xcconfig`);return["// Written by mobile-tools prebuild from appsettings.Compiled.json for one environment. Not tracked: run prebuild to change it.",...o.map(([e,t])=>`${e} = ${t}`)].map(e=>`${e}\n`).join("")};export const buildCommit=e=>{const t=findOnPath("git",process.env,process.platform);if(!t)return"nogit";const i=capture({file:t,args:["rev-parse","--short","HEAD"]},e);if(0!==i.status)return"nogit";const o=capture({file:t,args:["status","--porcelain","--","."]},e);return 0!==o.status?fail("toolFailed",`git status failed in ${e}: ${o.stderr.trim()}`):""===o.stdout.trim()?i.stdout.trim():`${i.stdout.trim()}-dirty`};export const requireIgnored=(e,t)=>{const i=findOnPath("git",process.env,process.platform);if(!i||0!==capture({file:i,args:["rev-parse","--is-inside-work-tree"]},e).status)return;const o=t.filter(t=>{const o=capture({file:i,args:["check-ignore","-q","--",t]},e);return 0!==o.status&&1!==o.status&&fail("toolFailed",`git check-ignore failed for ${t}: ${o.stderr.trim()}`),1===o.status});o.length>0&&fail("appConfigInvalid",`add these lines to ${path.basename(e)}/.gitignore; prebuild writes them per environment:\n${o.join("\n")}`)};const writeIfChanged=(e,t)=>(!existsSync(e)||readFileSync(e,"utf8")!==t)&&(writeFileSync(e,t,"utf8"),!0),report=(e,t,i)=>{say(`prebuild: ${i?"wrote":"unchanged"} ${path.relative(e,t)}`)},dependsOn=(e,t)=>{const i=path.join(e,"package.json");if(!existsSync(i))return!1;const o=JSON.parse(readFileSync(i,"utf8"));return Boolean(o.dependencies?.[t])};export const usesFirebase=e=>dependsOn(e,"@react-native-firebase/app");export const checkNdkHook=e=>{const t=path.join(e,"android","build.gradle");if(!existsSync(t))return;const i=readFileSync(t,"utf8");/withId\(\s*["']com\.android\.library["']\s*\)/.test(i)&&/\.ndkVersion\s*=\s*rootProject\.ext\.ndkVersion/.test(i)||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 } }')};const MANAGED_GRADLE_KEYS=/^\s*(APP_NAME|APP_ID|VERSION_DISPLAY|VERSION_INTERNAL)\s*[=:]/gm;export const stampAndroid=(e,t,i,o,n)=>{const s=path.join(e,"android","gradle.properties");existsSync(s)||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 a=[...new Set([...readFileSync(s,"utf8").matchAll(MANAGED_GRADLE_KEYS)].map(([,e])=>e))];a.length>0&&fail("appConfigInvalid",`android/gradle.properties sets ${a.join(", ")}: remove them; android/app/build.gradle reads them from appsettings.Compiled.json`);const r=path.join(e,"android","app","build.gradle");if(existsSync(r)&&readFileSync(r,"utf8").includes("appsettings.Compiled.json")||fail("appConfigInvalid","android/app/build.gradle does not read appsettings.Compiled.json; add the compiledSettings loader (mobile-tools README, prebuild)"),usesExpoUpdates(e)){o.flavors||fail("appConfigInvalid",'expo-updates settings go into the flavor\'s manifest; set "flavors": true');const s=path.join(e,"android","app","src","main","AndroidManifest.xml");existsSync(s)||fail("appConfigInvalid","android/app/src/main/AndroidManifest.xml not found");const a=[...readFileSync(s,"utf8").matchAll(/android:name="(expo\.modules\.updates\.[^"]*)"/g)].map(([,e])=>e);a.length>0&&fail("appConfigInvalid",`android/app/src/main/AndroidManifest.xml sets ${a.join(", ")}: remove them from android/app/src/main/AndroidManifest.xml; prebuild writes them into ${androidFlavorManifest(i)}`);const r=otaNativeSettings(e,t,i,"android",n),p=path.join(e,androidFlavorManifest(i));mkdirSync(path.dirname(p),{recursive:!0}),report(e,p,writeIfChanged(p,androidOtaManifest(r)))}};export const stampIos=(e,t,i,o,n,s)=>{const a=path.join(e,"ios",n),r=path.join(a,"Info.plist"),p=path.join(e,"ios",`${n}.xcodeproj`,"project.pbxproj");for(const t of[r,p])existsSync(t)||fail("appConfigInvalid",`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,n);const c=`AppIcon-${i}`,d=path.join(a,"Images.xcassets"),l=existsSync(path.join(d,`${c}.appiconset`)),f=ENVIRONMENTS.filter(e=>e!==i&&existsSync(path.join(d,`AppIcon-${e}.appiconset`)));!l&&f.length>0&&fail("appConfigInvalid",`ios/${n}/Images.xcassets has AppIcon-${f[0]} but no ${c}: run mobile-tools icons`),checkPbxprojSettings(readFileSync(p,"utf8"),l);const u=path.join(e,IOS_COMPILED_XCCONFIG);report(e,u,writeIfChanged(u,xcconfigText(t,i,l))),l||say(`prebuild: no AppIcon-${i} set, so ios/appsettings.Compiled.xcconfig sets no app icon name`);const g=path.join(a,"GoogleService-Info.plist"),I=path.join(e,o.firebasePlistDir,`GoogleService-Info.${i}.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(g)||!readFileSync(I).equals(readFileSync(g));t&©FileSync(I,g),report(e,g,t)}else existsSync(g)&&(rmSync(g),say(`prebuild: removed ${path.relative(e,g)}, no ${path.relative(e,I)}`));if(usesExpoUpdates(e)){readFileSync(p,"utf8").includes("Expo.plist in Resources")||fail("appConfigInvalid","add Supporting/Expo.plist to the Xcode project's Copy Bundle Resources; prebuild writes its contents");const o=otaNativeSettings(e,t,i,"ios",s),a=path.join(e,expoPlistPath(n));mkdirSync(path.dirname(a),{recursive:!0}),report(e,a,writeIfChanged(a,expoPlist(o)))}};export const prebuild=e=>{const{appRoot:t,sameEnv:i,platform:o}=e,n=!0===e.otaTest;i&&e.env&&fail("badInput","pass --env or --same-env, not both");const s=i?lastCompiledEnvironment(t):e.env;if(!s)return fail("badInput","prebuild needs --env <Development|Staging|Production> or --same-env");const a=loadToolsConfig(t),r=resolveIosProjectName(t,a),p=loadAppSettings(t,s,a.requiredSettings);checkAppSettings(t,p,s);const c="android"===o||!o&&existsSync(path.join(t,"android")),d="android"===o?void 0:r;if("ios"===o&&void 0===d)return fail("appConfigInvalid",`no ios/ folder in ${t}`);const l=[...c?generatedNativeFiles(t,d,s,"android"):[],...void 0!==d?generatedNativeFiles(t,d,s,"ios"):[]];requireIgnored(t,[COMPILED_FILE,...l]);const f={...p,COMMIT:buildCommit(t),BUILD_TIME:(new Date).toISOString()};return writeFileSync(path.join(t,COMPILED_FILE),`${JSON.stringify(f,null,2)}\n`,"utf8"),say(`prebuild: wrote ${COMPILED_FILE} (${s}, ${f.COMMIT})`),c&&stampAndroid(t,f,s,a,n),void 0!==d&&stampIos(t,f,s,a,d,n),f};
|
package/lib/run.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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,loadToolsConfig,localApiPort,
|
|
1
|
+
import{existsSync,readdirSync,readFileSync,statSync}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,loadToolsConfig,localApiPort,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{appBundleId,describeAppleDevice,listAppleDevices}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";export const readNamespace=e=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(e)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(e,a)=>{const i=e.filter(e=>e.endsWith(".apk")).sort();return i.find(e=>e.includes(a))??i.find(e=>e.includes("universal"))??i[0]};const adbOrFail=(e,a,i)=>{const t=capture({file:e,args:a},i);return 0!==t.status?fail("toolFailed",`adb ${a.join(" ")} failed: ${(t.stderr||t.stdout).trim()}`):t.stdout};export const installFailure=e=>/Failure \[[^\]]*\]/.exec(e)?.[0];const UNINSTALL_FIRST=/INSTALL_FAILED_(UPDATE_INCOMPATIBLE|VERSION_DOWNGRADE)/;export const installFailureReason=(e,a,i,t)=>{const r=installFailure(`${e}\n${a}`);if(void 0!==r)return UNINSTALL_FIRST.test(r)?`${r}; run \`adb -s ${i} uninstall ${t}\` (this deletes the app's data), then run again`:r;return[e.trim(),a.trim()].filter(Boolean).join("\n")||"adb gave no reason; check the device is still listed by `adb devices`, then run again"};const installOrFail=(e,a,i,t,r)=>{const s=capture({file:e,args:["-s",a,"install","-r","-t",i]},r);0===s.status&&void 0===installFailure(`${s.stderr}\n${s.stdout}`)||fail("toolFailed",`adb install on ${a} failed: ${installFailureReason(s.stderr,s.stdout,a,t)}`)},elapsedSeconds=e=>((Date.now()-e)/1e3).toFixed(1),cachedBuild=(e,a,i)=>{const t=e.fingerprint.slice(0,12),r=a?void 0:findCachedBuild(e);if(r)return say(`cache: hit ${t} (fingerprint ${i} s), installing the cached build`),r;say(a?"cache: skipped (--rebuild), building":`cache: miss ${t} (fingerprint ${i} s), building`)},DEVICE_METRO_PORT=8081,DEV_HOST_ENTRY=/^\s*<string name="debug_http_host">[^<]*<\/string>\s*$\n?/m;export const withDevHost=(e,a)=>{const i=e.includes("</map>")?e.replace(DEV_HOST_ENTRY,""):"<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map>\n</map>\n";return void 0===a?i:i.replace("</map>",` <string name="debug_http_host">${a}</string>\n</map>`)};const pointAppAtMetro=(e,a,i,t,r)=>{const s=`shared_prefs/${i}_preferences.xml`;adbOrFail(e,["-s",a,"shell","am","force-stop",i],r);const o=capture({file:e,args:["-s",a,"shell","run-as",i,"cat",s]},r),n=8081===t?void 0:`localhost:${t}`,l=withDevHost(0===o.status?o.stdout:"",n),p=Buffer.from(l,"utf8").toString("base64"),d=capture({file:e,args:["-s",a,"shell",`run-as ${i} sh -c 'mkdir -p shared_prefs && echo ${p} | base64 -d > ${s}'`]},r);0!==d.status&&void 0!==n&&fail("toolFailed",`could not point ${i} at Metro port ${t}: ${(d.stderr||d.stdout).trim()}`),adbOrFail(e,["-s",a,"reverse",`tcp:${t}`,`tcp:${t}`],r)},runAndroid=async e=>{const{appRoot:a,env:i}=e,t=loadToolsConfig(a),r=path.join(a,"android"),s=path.join(r,"app","build.gradle");existsSync(s)||fail("appConfigInvalid",`android/app/build.gradle not found in ${a}`);const o=readNamespace(readFileSync(s,"utf8"))??fail("appConfigInvalid","android/app/build.gradle declares no namespace"),n=adbPath(process.env,process.platform);await connectLocalEmulators(n,a);const l=listAndroidDevices(n,a),p=await pickDevice(l,e.device,describeDevice,!0===process.stdin.isTTY);ABI_PATTERN.test(p.abi)||fail("deviceNotFound",`${describeDevice(p)} reports an ABI this build cannot target: "${p.abi}"`),say(`run: ${describeDevice(p)}`);const d=prebuild({appRoot:a,env:i,platform:"android"}),c=Date.now(),u=await nativeFingerprint(a,"android"),f={appId:baseAppId(a),platform:"android",variant:variantName(i,t.flavors,"debug"),abi:p.abi,fingerprint:u};let m=cachedBuild(f,e.rebuild,elapsedSeconds(c));if(void 0===m){const e=gradleTask(i,t.flavors,"apk","debug"),s=logDir(a);await buildOrFail(gradleInvocation(r,[`:app:${e}`,`-PreactNativeArchitectures=${p.abi}`],process.platform,process.env),r,path.join(s,`gradle-${i}-${logStamp()}.log`),`gradle ${e}`);const o=gradleOutputDir(a,i,t.flavors,"apk","debug"),n=existsSync(o)?chooseApk(readdirSync(o),p.abi):void 0;n||fail("artifactMissing",`the build produced no APK in ${o}`),m=path.join(o,n),storeCachedBuild(f,m),say(`cache: stored ${u.slice(0,12)}`)}const h=applicationId(d.APP_ID,i);say(`run: installing ${path.basename(m)} on ${p.serial}`),installOrFail(n,p.serial,m,h,a);const b=await ensureMetro(a,path.join(logDir(a),"metro.log"),{fixedPort:!1,profiling:e.profiling});if(pointAppAtMetro(n,p.serial,h,b,a),8081!==b&&say(`run: ${h} loads from Metro on port ${b}; port 8081 belongs to another app`),"Development"===i){const e=localApiPort(String(d.BASE_URL),t);void 0!==e&&(adbOrFail(n,["-s",p.serial,"reverse",`tcp:${e}`,`tcp:${e}`],a),say(`run: device localhost:${e} reaches this machine's API`))}adbOrFail(n,["-s",p.serial,"shell","am","start","-n",`${h}/${o}.MainActivity`],a),say(`run: launched ${h} on ${p.model||p.serial}`)},runIos=async e=>{requireDarwin(process.platform,"run --platform ios");const{appRoot:a,env:i}=e;e.rebuild&&fail("badInput","--rebuild applies to Android; an iOS run installs the app --app names");const t=e.app??fail("badInput","run --platform ios installs a simulator app built with xOS Tools: on a host with xos run npx mobile-tools build --env <Env> --artifact ios-sim --debug, bring the .app here, then pass --app <path>"),r=path.resolve(a,t);r.endsWith(".app")&&existsSync(r)&&statSync(r).isDirectory()||fail("badInput",`--app must be a simulator .app folder: ${t}`);const s=await pickDevice(listAppleDevices(a),e.device,describeAppleDevice,!0===process.stdin.isTTY);say(`run: ${describeAppleDevice(s)}`);const o=prebuild({appRoot:a,env:i,platform:"ios"}),n=applicationId(o.APP_ID,i),l=appBundleId(r,a);l!==n&&fail("appConfigInvalid",`${r} is ${l}, but --env ${i} runs ${n}: build the simulator app for ${i}`),await ensureMetro(a,path.join(logDir(a),"metro.log"),{fixedPort:!0,profiling:e.profiling});const p=[["simctl","install",s.udid,r],["simctl","launch",s.udid,n]];for(const e of p){const i=capture({file:XCRUN,args:e},a);0!==i.status&&fail("toolFailed",`xcrun ${e.slice(0,3).join(" ")} failed: ${(i.stderr||i.stdout).trim()}`)}say(`run: launched ${n} on ${s.name}`)};export const run=async e=>{"android"===e.platform&&void 0!==e.app&&fail("badInput","--app installs a simulator app; an Android run builds its own"),requireSynced(e.appRoot),"ios"===e.platform?await runIos(e):await runAndroid(e)};
|
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");export const PODS_RECORD=path.join("ios","Pods",".webority-native.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"),s=path.join(e,"package-lock.json");return existsSync(n)&&existsSync(s)?fail("appConfigInvalid","both pnpm-lock.yaml and package-lock.json exist: delete the one not in use"):existsSync(n)?{manager:"pnpm",file:n}:existsSync(s)?{manager:"npm",file:s}:fail("appConfigInvalid",`no package-lock.json or pnpm-lock.yaml in ${e}`)};const filesUnder=(e,n="")=>readdirSync(e,{withFileTypes:!0}).flatMap(s=>{const
|
|
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";import{prependXosShims,xosPath}from"./xos.js";export const STAMP_FILE=path.join("node_modules",".webority-install.json");export const PODS_RECORD=path.join("ios","Pods",".webority-native.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"),s=path.join(e,"package-lock.json");return existsSync(n)&&existsSync(s)?fail("appConfigInvalid","both pnpm-lock.yaml and package-lock.json exist: delete the one not in use"):existsSync(n)?{manager:"pnpm",file:n}:existsSync(s)?{manager:"npm",file:s}:fail("appConfigInvalid",`no package-lock.json or pnpm-lock.yaml in ${e}`)};const filesUnder=(e,n="")=>readdirSync(e,{withFileTypes:!0}).flatMap(s=>{const o=""===n?s.name:`${n}/${s.name}`;return s.isDirectory()?filesUnder(path.join(e,s.name),o):[o]}).sort();export const installHash=e=>{const{file:n}=lockfileOf(e),s=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,s[e]??null])));const t=path.join(e,"patches");if(existsSync(t))for(const e of filesUnder(t))o.update(`\0patches/${e}\0`),o.update(readFileSync(path.join(t,...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"),s=path.join(e,"ios","Pods","Manifest.lock");return!(!existsSync(n)||!existsSync(s))&&!readFileSync(n).equals(readFileSync(s))};const nodeModulesNewerThanPods=e=>{const n=[path.join(e,"node_modules",".modules.yaml"),path.join(e,"node_modules",".pnpm","lock.yaml")].find(e=>existsSync(e)),s=path.join(e,"ios","Pods","Manifest.lock");return!(!n||!existsSync(s))&&statSync(n).mtimeMs>statSync(s).mtimeMs};export const checkSync=(e,n)=>{const s=[],o=readStamp(e),{file:t}=lockfileOf(e);if(o?o.hash!==installHash(e)&&s.push(`node_modules is stale: ${path.basename(t)}, package.json or patches/ changed since the last install. Fix: npx mobile-tools sync`):s.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&&s.push(`node_modules native packages changed since the last install: ${n.join(", ")}. Fix: npx mobile-tools sync`)}return void 0!==findOnPath("xos",process.env,n)&&podsStale(e)&&s.push(`ios/Pods is stale: ios/Podfile.lock differs from ios/Pods/Manifest.lock. Fix: ${podFix(e)}`),void 0!==findOnPath("xos",process.env,n)&&existsSync(path.join(e,"ios","Podfile.lock"))&&nodeModulesNewerThanPods(e)&&s.push("node_modules changed after the last pod install; run pod install in ios/ (or mobile-tools sync)"),s};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 s=[];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()&&s.push(path.join(n,e.name,o.name));else s.push(path.join(n,e.name));const o={};for(const e of s){const n=path.join(e,"android"),s=path.join(e,"package.json");if(existsSync(n)&&statSync(n).isDirectory()&&existsSync(s))try{const e=JSON.parse(readFileSync(s,"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(s=>e[s]!==n[s]).sort();export const writeStamp=e=>{const{manager:n}=lockfileOf(e),s={hash:installHash(e),manager:n,nativePackages:nativePackages(e)};return writeFileSync(path.join(e,STAMP_FILE),`${JSON.stringify(s,null,2)}\n`,"utf8"),s};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 ensurePods=(e,n)=>{if(!existsSync(path.join(e,"ios","Podfile")))return;const s=path.join(e,"ios","Pods"),o=path.join(e,PODS_RECORD);let t;if(existsSync(s)?existsSync(path.join(e,"ios","Podfile.lock"))?existsSync(path.join(s,"Manifest.lock"))?podsStale(e)?t="ios/Podfile.lock differs from ios/Pods/Manifest.lock":nodeModulesNewerThanPods(e)?t="node_modules changed after the last pod install":existsSync(o)?readFileSync(o,"utf8")!==JSON.stringify(nativePackages(e))&&(t="native packages changed since the last pod install"):t="no pod install record":t="ios/Pods/Manifest.lock is missing":t="ios/Podfile.lock is missing":t="ios/Pods is missing",void 0===t)return;say(`pods: ${t}; running pod install`);const i=existsSync(path.join(e,"Gemfile")),a=findOnPath(i?"bundle":"pod",process.env,n);a||fail("toolMissing",`${i?"bundle":"pod"} not found on PATH; install CocoaPods, then ${podFix(e)}`),runInherit({file:a,args:i?["exec","pod","install"]:["install"]},path.join(e,"ios"),"pod install");const c=path.join(e,"ios","build","ModuleCache.noindex");existsSync(c)&&(rmSync(c,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,c)}`)),writeFileSync(o,JSON.stringify(nativePackages(e)),"utf8")};export const sync=(e,n)=>{const{manager:s}=lockfileOf(e),o=readStamp(e)?.nativePackages??nativePackages(e);say("sync: "+("pnpm"===s?"pnpm install --frozen-lockfile":"npm ci")),runInherit(installInvocation(s,n),e,`${s} install`);const t=writeStamp(e).nativePackages,i=changedNativePackages(o,t),a=path.join(e,"android"),c=existsSync(path.join(a,"app",".cxx"))||existsSync(path.join(a,"app","build"));if(i.length>0&&c){say(`sync: native packages changed (${i.join(", ")}): regenerating codegen`),runInherit(gradleInvocation(a,["generateCodegenArtifactsFromSchema"],n,process.env),a,"gradlew generateCodegenArtifactsFromSchema");for(const n of[path.join(a,"app",".cxx"),path.join(a,"app","build","generated","autolinking")])rmSync(n,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,n)}`)}void 0!==findOnPath("xos",process.env,n)&&existsSync(path.join(e,"ios","Podfile"))&&(prependXosShims(xosPath(process.env,n),e),ensurePods(e,n)),say("sync: done")};
|
package/lib/xos.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync,readdirSync}from"node:fs";import path from"node:path";import{buildOrFail}from"./buildLog.js";import{capture}from"./exec.js";import{fail}from"./output.js";import{findOnPath}from"./platform.js";export const SIMULATOR_ARCHS=["x86_64","arm64"];export const xosPath=(o,i)=>findOnPath("xos",o,i)??fail("toolMissing","xos not found on PATH: iOS apps build with xOS Tools (xos). Build on a host with xos installed with mobile-tools build --env <Env> --artifact ios-sim --debug, then install the .app on a Mac with mobile-tools run --platform ios --app <path>");export const prependXosShims=(o,i)=>{const t=capture({file:o,args:["doctor","--json"]},i);let s;0!==t.status&&fail("toolMissing",`xos doctor failed: ${t.stderr.trim()}`);try{s=JSON.parse(t.stdout).shims}catch{s=void 0}"string"!=typeof s&&fail("toolFailed","xos doctor --json printed no string field shims: reinstall xOS Tools, then run xos doctor --json and check it prints shims"),process.env.PATH=s+path.delimiter+(process.env.PATH??"")};export const simulatorBuildDir=(o,i)=>path.join(o,"ios","build","xos",`${i}-iphonesimulator`);export const xosSimulatorBuild=async(o,i,t)=>{const{appRoot:s,projectName:a,configuration:r,arch:e}=i,n=simulatorBuildDir(s,r);await buildOrFail({file:o,args:["build","--pods",path.join("ios","Pods"),"--build-dir",n,"--configuration",r,"--platform","iossimulator","--arch",e,"--jobs","0"]},s,t,"xos build --pods"),await buildOrFail({file:o,args:["app","--project",path.join("ios",`${a}.xcodeproj`),"--target",a,"--configuration",r,"--platform","iossimulator","--arch",e,"--build-dir",n,"--assets",path.join("ios",a,"Images.xcassets"),"--jobs","0"]},s,t,"xos app");const subfolders=o=>existsSync(o)?readdirSync(o,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(i=>path.join(o,i.name)):[],l=`${a}.app`,p=subfolders(n),d=[...p,...p.flatMap(subfolders)].find(o=>path.basename(o)===l)??fail("artifactMissing",`the build produced no ${l} in ${n}: read the xos log at ${t}`);return path.resolve(d)};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.3",
|
|
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",
|