@webority/mobile-tools 0.109.8 → 0.110.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,9 +71,11 @@ npx mobile-tools hooks install
71
71
  ### `run`
72
72
 
73
73
  ```bash
74
- npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild]
74
+ npx mobile-tools run --env <Environment> [--platform android|ios] [--device <serial|name>] [--rebuild] [--profiling]
75
75
  ```
76
76
 
77
+ `--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.
78
+
77
79
  **Android** (the default):
78
80
 
79
81
  1. Runs `sync --check` and stops on a stale tree.
@@ -112,9 +114,11 @@ Every program is started by absolute path with an argument list, never through a
112
114
  ### `build`
113
115
 
114
116
  ```bash
115
- npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--json] [-- <gradle args>]
117
+ npx mobile-tools build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--ota-test] [--json] [-- <gradle args>]
116
118
  ```
117
119
 
120
+ `--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.
121
+
118
122
  1. Stops on a stale tree, as `run` does.
119
123
  2. Runs `prebuild` for the environment and platform.
120
124
  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.
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,requireDarwin,XCODEBUILD}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";const buildAndroid=async(i,o,a)=>{const{appRoot:r,env:t}=i;requireSynced(r);const n=loadToolsConfig(r),s=path.join(r,"android");existsSync(path.join(s,"app","build.gradle"))||fail("appConfigInvalid",`android/app/build.gradle not found in ${r}`),prebuild({appRoot:r,env:t,platform:"android"});const e=gradleTask(t,n.flavors,o,a),l=path.join(logDir(r),`gradle-${t}-${logStamp()}.log`);await buildOrFail(gradleInvocation(s,[`:app:${e}`,...i.gradleArgs],process.platform,process.env),s,l,`gradle ${e}`);const p=gradleOutputDir(r,t,n.flavors,o,a),d=existsSync(p)?readdirSync(p,{withFileTypes:!0}).filter(i=>i.isFile()&&i.name.endsWith(`.${o}`)).map(i=>path.resolve(p,i.name)).sort():[];0===d.length&&fail("artifactMissing",`the build produced no ${o} in ${p}`);const u=mappingFile(r,t,n.flavors);return{artifacts:d,mapping:"release"===a&&existsSync(u)?u:null,log:l}},buildIos=async(i,o)=>{requireDarwin(process.platform,"build --artifact ios-sim"),i.gradleArgs.length>0&&fail("badInput","Gradle arguments after -- apply to apk and aab builds only");const{appRoot:a,env:r}=i;requireSynced(a);const t=loadToolsConfig(a),n=resolveIosProjectName(a,t)??fail("appConfigInvalid",`no ios/ folder in ${a}`),s=path.join(a,"ios",`${n}.xcworkspace`);existsSync(s)||fail("appConfigInvalid",`ios/${n}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`),prebuild({appRoot:a,env:r,platform:"ios"});const e="release"===o?"Release":"Debug",l=path.join(logDir(a),`xcodebuild-${r}-${logStamp()}.log`);await buildOrFail({file:XCODEBUILD,args:["-workspace",path.join("ios",`${n}.xcworkspace`),"-scheme",n,"-configuration",e,"-sdk","iphonesimulator","-destination","generic/platform=iOS Simulator","-derivedDataPath",path.join("ios","build"),"CODE_SIGNING_ALLOWED=NO","build"]},a,l,"xcodebuild");const p=path.join(a,"ios","build","Build","Products",`${e}-iphonesimulator`),d=path.join(p,`${n}.app`);return existsSync(d)||fail("artifactMissing",`the build produced no ${n}.app in ${p}`),{artifacts:[d],mapping:null,log:l}};export const build=async i=>{const o=i.debug?"debug":"release",a="ios-sim"===i.artifact?await buildIos(i,o):await buildAndroid(i,i.artifact,o);if(isJsonOutput())sayJson({artifacts:a.artifacts,mapping:a.mapping,log:a.log});else{for(const i of a.artifacts)say(`build: ${i}`);null!==a.mapping&&say(`build: mapping ${a.mapping}`)}};
1
+ import{existsSync,readdirSync}from"node:fs";import path from"node:path";import{buildOrFail,logDir,logStamp}from"./buildLog.js";import{gradleOutputDir,gradleTask,loadToolsConfig,mappingFile,resolveIosProjectName}from"./config.js";import{fail,isJsonOutput,say,sayJson}from"./output.js";import{gradleInvocation,requireDarwin,XCODEBUILD}from"./platform.js";import{prebuild}from"./prebuild.js";import{ensurePods,requireSynced}from"./sync.js";const buildAndroid=async(o,i,a)=>{const{appRoot:t,env:s}=o;requireSynced(t);const r=loadToolsConfig(t),e=path.join(t,"android");existsSync(path.join(e,"app","build.gradle"))||fail("appConfigInvalid",`android/app/build.gradle not found in ${t}`),prebuild({appRoot:t,env:s,platform:"android",otaTest:o.otaTest});const n=gradleTask(s,r.flavors,i,a),l=path.join(logDir(t),`gradle-${s}-${logStamp()}.log`);await buildOrFail(gradleInvocation(e,[`:app:${n}`,...o.gradleArgs],process.platform,process.env),e,l,`gradle ${n}`);const p=gradleOutputDir(t,s,r.flavors,i,a),d=existsSync(p)?readdirSync(p,{withFileTypes:!0}).filter(o=>o.isFile()&&o.name.endsWith(`.${i}`)).map(o=>path.resolve(p,o.name)).sort():[];0===d.length&&fail("artifactMissing",`the build produced no ${i} in ${p}`);const u=mappingFile(t,s,r.flavors);return{artifacts:d,mapping:"release"===a&&existsSync(u)?u:null,log:l}},buildIos=async(o,i)=>{requireDarwin(process.platform,"build --artifact ios-sim"),o.gradleArgs.length>0&&fail("badInput","Gradle arguments after -- apply to apk and aab builds only");const{appRoot:a,env:t}=o;ensurePods(a,process.platform),requireSynced(a);const s=loadToolsConfig(a),r=resolveIosProjectName(a,s)??fail("appConfigInvalid",`no ios/ folder in ${a}`),e=path.join(a,"ios",`${r}.xcworkspace`);existsSync(e)||fail("appConfigInvalid",`ios/${r}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`),prebuild({appRoot:a,env:t,platform:"ios",otaTest:o.otaTest});const n="release"===i?"Release":"Debug",l=path.join(logDir(a),`xcodebuild-${t}-${logStamp()}.log`);await buildOrFail({file:XCODEBUILD,args:["-workspace",path.join("ios",`${r}.xcworkspace`),"-scheme",r,"-configuration",n,"-sdk","iphonesimulator","-destination","generic/platform=iOS Simulator","-derivedDataPath",path.join("ios","build"),"CODE_SIGNING_ALLOWED=NO","build"]},a,l,"xcodebuild");const p=path.join(a,"ios","build","Build","Products",`${n}-iphonesimulator`),d=path.join(p,`${r}.app`);return existsSync(d)||fail("artifactMissing",`the build produced no ${r}.app in ${p}`),{artifacts:[d],mapping:null,log:l}};export const build=async o=>{const i=o.debug?"debug":"release",a="ios-sim"===o.artifact?await buildIos(o,i):await buildAndroid(o,o.artifact,i);if(isJsonOutput())sayJson({artifacts:a.artifacts,mapping:a.mapping,log:a.log});else{for(const o of a.artifacts)say(`build: ${o}`);null!==a.mapping&&say(`build: mapping ${a.mapping}`)}};
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]\n Build for one device's ABI (or install the cached build with the same native fingerprint), install it there, reuse or start Metro, and launch. --rebuild skips the cache.\n\n build --env <Environment> --artifact apk|aab|ios-sim [--debug] [--json] [-- <gradle args>]\n Prebuild, then build a release APK or App Bundle (Android, every ABI) or an iOS simulator app (macOS only).\n --debug builds the debug variant (apk or ios-sim only).\n 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.\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"}}),o=envOption(e.env)??fail("badInput","run needs --env <Development|Staging|Production>");return void await run({appRoot:n,env:o,platform:platformOption(e.platform)??"android",device:"string"==typeof e.device?e.device:void 0,rebuild:!0===e.rebuild})}case"build":{const 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"},json:{type:"boolean"}}),r=envOption(s.env)??fail("badInput","build needs --env <Development|Staging|Production>"),i=artifactOption(s.artifact);return!0===s.debug&&"aab"===i&&fail("badInput","--debug builds an apk or an ios-sim app; an App Bundle is release only"),"ios-sim"===i&&a.length>0&&fail("badInput","Gradle arguments after -- apply to apk and aab builds only"),void await build({appRoot:n,env:r,artifact:i,debug:!0===s.debug,gradleArgs:a})}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)});
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 (macOS only).\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.\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"}}),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})}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"}}),i=envOption(s.env)??fail("badInput","build needs --env <Development|Staging|Production>"),r=artifactOption(s.artifact);return!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"),void await build({appRoot:n,env:i,artifact:r,debug:!0===s.debug,otaTest:!0===s["ota-test"],gradleArgs:a})}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/metro.js CHANGED
@@ -1 +1 @@
1
- import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{createServer}from"node:net";import path from"node:path";import{logDir}from"./buildLog.js";import{capture,startDetached}from"./exec.js";import{fail,say}from"./output.js";export const DEFAULT_METRO_PORT=8081;export const HUNG_PROBE_MS=5e3;const PORT_FREE_WAIT_MS=1e4,TERM_WAIT_MS=5e3,samePath=(t,e)=>{const norm=t=>{const e=path.resolve(t).replace(/[\\/]+$/,"");return"win32"===process.platform?e.toLowerCase():e};return norm(t)===norm(e)};export const classifyStatus=(t,e,r)=>{if(!t.includes("packager-status:running"))return"other";const o=e??/project-root:(.+)/.exec(t)?.[1]?.trim();return o&&samePath(decodeHeaderPath(o),r)?"ours":"other"};const decodeHeaderPath=t=>{try{return decodeURIComponent(t)}catch{return t}},portIsFree=t=>new Promise(e=>{const r=createServer();r.once("error",()=>e(!1)),r.once("listening",()=>r.close(()=>e(!0))),r.listen(t)});export const probeMetro=async(t,e)=>{try{const r=await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(1e3)});return classifyStatus(await r.text(),r.headers.get("X-React-Native-Project-Root"),e)}catch{return await portIsFree(t)?"free":"other"}};const sleep=t=>new Promise(e=>setTimeout(e,t)),waitForMetro=async(t,e,r)=>{const o=Date.now()+12e4;for(;Date.now()<o;){if("ours"===await probeMetro(t,e))return;await new Promise(t=>setTimeout(t,500))}fail("metroUnavailable",`Metro did not answer on port ${t} within 2 minutes; see ${r}`)};export const METRO_RECORD_FILE="metro.json";export const readMetroRecord=t=>{const e=path.join(logDir(t),"metro.json");if(existsSync(e))try{const t=JSON.parse(readFileSync(e,"utf8"));return"number"==typeof t.port&&"number"==typeof t.pid?t:void 0}catch{return}};const writeMetroRecord=(t,e,r)=>{mkdirSync(logDir(t),{recursive:!0});const o={port:e,pid:r,appRoot:t,startedAtUtc:(new Date).toISOString()};writeFileSync(path.join(logDir(t),"metro.json"),JSON.stringify(o,null,2))},startMetro=async(t,e,r)=>{let o;try{o=createRequire(path.join(t,"package.json")).resolve("expo/bin/cli")}catch{return fail("toolMissing","expo is not installed in this app; run npx mobile-tools sync")}const s=startDetached({file:process.execPath,args:[o,"start","--port",String(e)]},t,r);writeMetroRecord(t,e,s),say(`metro: started on port ${e}, log ${r}`),await waitForMetro(e,t,r)},answers=async(t,e)=>{try{return await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(e)}),!0}catch{return!1}},systemTool=t=>path.join(process.env.SystemRoot??"C:/Windows","System32",t),isNodeProcess=t=>{try{process.kill(t,0)}catch{return!1}if("win32"===process.platform){return capture({file:systemTool("tasklist.exe"),args:["/FI",`PID eq ${t}`,"/FO","CSV","/NH"]},process.cwd()).stdout.toLowerCase().includes("node.exe")}return capture({file:"/bin/ps",args:["-p",String(t),"-o","comm="]},process.cwd()).stdout.trim().endsWith("node")},signalGroup=(t,e)=>{try{process.kill(-t,e)}catch{}},stopTree=async t=>{if("win32"===process.platform){const e=capture({file:systemTool("taskkill.exe"),args:["/PID",String(t),"/T","/F"]},process.cwd());if(0!==e.status&&isNodeProcess(t)){const r=`${e.stdout}${e.stderr}`.trim();fail("toolFailed",`taskkill could not stop pid ${t}: ${r}`)}return}signalGroup(t,"SIGTERM");const e=Date.now()+5e3;for(;isNodeProcess(t)&&Date.now()<e;)await sleep(250);isNodeProcess(t)&&signalGroup(t,"SIGKILL")},waitPortFree=async(t,e)=>{const r=Date.now()+1e4;for(;!await portIsFree(t);)Date.now()>=r&&fail("metroUnavailable",`port ${t} is still held 10 s after stopping pid ${e}; stop the holder and rerun`),await sleep(250)};export const ensureMetro=async(t,e,{fixedPort:r})=>{const o=r?8081:8100;for(let r=8081;r<=o;r++){const o=await probeMetro(r,t);if("ours"===o)return say(`metro: reusing the server on port ${r}`),r;if("other"===o){const o=readMetroRecord(t);if(o?.port===r&&isNodeProcess(o.pid)&&!await answers(r,5e3))return say(`metro: the server on port ${r} (pid ${o.pid}) does not answer; restarting it, log ${e}`),await stopTree(o.pid),await waitPortFree(r,o.pid),await startMetro(t,r,e),r}if("free"===o)return await startMetro(t,r,e),r}return fail("metroUnavailable",r?"port 8081 is held by another server (the iOS app always looks there); stop it and rerun":"ports 8081-8100 are all held by other servers; stop one and rerun")};export const restartMetro=async t=>{const e=path.join(logDir(t),"metro.log"),r=readMetroRecord(t);if(r&&isNodeProcess(r.pid))return await stopTree(r.pid),say(`metro: stopped pid ${r.pid} on port ${r.port}`),await waitPortFree(r.port,r.pid),await startMetro(t,r.port,e),say(`metro: ready on port ${r.port}`),r.port;const o=r?.port??8081,s=await probeMetro(o,t);return"ours"===s?fail("metroUnavailable",`the Metro on port ${o} was not started by mobile-tools; stop it in its own terminal (Ctrl+C), then run mobile-tools metro restart`):"other"===s?fail("metroUnavailable",`port ${o} is held by another server; stop it and rerun`):(await startMetro(t,o,e),say(`metro: ready on port ${o}`),o)};
1
+ import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{createServer}from"node:net";import path from"node:path";import{logDir}from"./buildLog.js";import{capture,startDetached}from"./exec.js";import{fail,say}from"./output.js";export const DEFAULT_METRO_PORT=8081;export const HUNG_PROBE_MS=5e3;const PORT_FREE_WAIT_MS=1e4,TERM_WAIT_MS=5e3,samePath=(e,t)=>{const norm=e=>{const t=path.resolve(e).replace(/[\\/]+$/,"");return"win32"===process.platform?t.toLowerCase():t};return norm(e)===norm(t)};export const classifyStatus=(e,t,r)=>{if(!e.includes("packager-status:running"))return"other";const o=t??/project-root:(.+)/.exec(e)?.[1]?.trim();return o&&samePath(decodeHeaderPath(o),r)?"ours":"other"};const decodeHeaderPath=e=>{try{return decodeURIComponent(e)}catch{return e}},portIsFree=e=>new Promise(t=>{const r=createServer();r.once("error",()=>t(!1)),r.once("listening",()=>r.close(()=>t(!0))),r.listen(e)});export const probeMetro=async(e,t)=>{try{const r=await fetch(`http://localhost:${e}/status`,{signal:AbortSignal.timeout(1e3)});return classifyStatus(await r.text(),r.headers.get("X-React-Native-Project-Root"),t)}catch{return await portIsFree(e)?"free":"other"}};const sleep=e=>new Promise(t=>setTimeout(t,e)),waitForMetro=async(e,t,r)=>{const o=Date.now()+12e4;for(;Date.now()<o;){if("ours"===await probeMetro(e,t))return;await new Promise(e=>setTimeout(e,500))}fail("metroUnavailable",`Metro did not answer on port ${e} within 2 minutes; see ${r}`)};export const METRO_RECORD_FILE="metro.json";export const readMetroRecord=e=>{const t=path.join(logDir(e),"metro.json");if(existsSync(t))try{const e=JSON.parse(readFileSync(t,"utf8"));return"number"==typeof e.port&&"number"==typeof e.pid?e:void 0}catch{return}};const writeMetroRecord=(e,t,r,o)=>{mkdirSync(logDir(e),{recursive:!0});const s={port:t,pid:r,appRoot:e,startedAtUtc:(new Date).toISOString(),mode:o};writeFileSync(path.join(logDir(e),"metro.json"),JSON.stringify(s,null,2))},servedMode=(e,t)=>{const r=readMetroRecord(e);return(r?.port===t?r.mode:void 0)??"development"},withProfilingEnv=(e,t)=>{const r=process.env.WEBORITY_PROFILING;e?process.env.WEBORITY_PROFILING="1":delete process.env.WEBORITY_PROFILING;try{return t()}finally{void 0===r?delete process.env.WEBORITY_PROFILING:process.env.WEBORITY_PROFILING=r}},startMetro=async(e,t,r,o)=>{let s;try{s=createRequire(path.join(e,"package.json")).resolve("expo/bin/cli")}catch{return fail("toolMissing","expo is not installed in this app; run npx mobile-tools sync")}const a=[s,"start","--port",String(t),...o?["--no-dev","--minify"]:[]],n=withProfilingEnv(o,()=>startDetached({file:process.execPath,args:a},e,r));writeMetroRecord(e,t,n,o?"profiling":"development"),say(`metro: started on port ${t}, log ${r}`),await waitForMetro(t,e,r)},answers=async(e,t)=>{try{return await fetch(`http://localhost:${e}/status`,{signal:AbortSignal.timeout(t)}),!0}catch{return!1}},systemTool=e=>path.join(process.env.SystemRoot??"C:/Windows","System32",e),isNodeProcess=e=>{try{process.kill(e,0)}catch{return!1}if("win32"===process.platform){return capture({file:systemTool("tasklist.exe"),args:["/FI",`PID eq ${e}`,"/FO","CSV","/NH"]},process.cwd()).stdout.toLowerCase().includes("node.exe")}return capture({file:"/bin/ps",args:["-p",String(e),"-o","comm="]},process.cwd()).stdout.trim().endsWith("node")},signalGroup=(e,t)=>{try{process.kill(-e,t)}catch{}},stopTree=async e=>{if("win32"===process.platform){const t=capture({file:systemTool("taskkill.exe"),args:["/PID",String(e),"/T","/F"]},process.cwd());if(0!==t.status&&isNodeProcess(e)){const r=`${t.stdout}${t.stderr}`.trim();fail("toolFailed",`taskkill could not stop pid ${e}: ${r}`)}return}signalGroup(e,"SIGTERM");const t=Date.now()+5e3;for(;isNodeProcess(e)&&Date.now()<t;)await sleep(250);isNodeProcess(e)&&signalGroup(e,"SIGKILL")},waitPortFree=async(e,t)=>{const r=Date.now()+1e4;for(;!await portIsFree(e);)Date.now()>=r&&fail("metroUnavailable",`port ${e} is still held 10 s after stopping pid ${t}; stop the holder and rerun`),await sleep(250)};export const ensureMetro=async(e,t,{fixedPort:r,profiling:o})=>{const s=o?"profiling":"development",a=r?8081:8100;for(let r=8081;r<=a;r++){const a=await probeMetro(r,e);if("ours"===a){const t=servedMode(e,r);return t!==s?fail("metroUnavailable",`Metro on port ${r}: stop the running Metro first; it serves ${t} JavaScript`):(say(`metro: reusing the server on port ${r}`),r)}if("other"===a){const s=readMetroRecord(e);if(s?.port===r&&isNodeProcess(s.pid)&&!await answers(r,5e3))return say(`metro: the server on port ${r} (pid ${s.pid}) does not answer; restarting it, log ${t}`),await stopTree(s.pid),await waitPortFree(r,s.pid),await startMetro(e,r,t,o),r}if("free"===a)return await startMetro(e,r,t,o),r}return fail("metroUnavailable",r?"port 8081 is held by another server (the iOS app always looks there); stop it and rerun":"ports 8081-8100 are all held by other servers; stop one and rerun")};export const restartMetro=async e=>{const t=path.join(logDir(e),"metro.log"),r=readMetroRecord(e);if(r&&isNodeProcess(r.pid))return await stopTree(r.pid),say(`metro: stopped pid ${r.pid} on port ${r.port}`),await waitPortFree(r.port,r.pid),await startMetro(e,r.port,t,"profiling"===r.mode),say(`metro: ready on port ${r.port}`),r.port;const o=r?.port??8081,s=await probeMetro(o,e);return"ours"===s?fail("metroUnavailable",`the Metro on port ${o} was not started by mobile-tools; stop it in its own terminal (Ctrl+C), then run mobile-tools metro restart`):"other"===s?fail("metroUnavailable",`port ${o} is held by another server; stop it and rerun`):(await startMetro(e,o,t,!1),say(`metro: ready on port ${o}`),o)};
@@ -1 +1 @@
1
- import{createRequire}from"node:module";import path from"node:path";export const createMetroConfig=o=>{const e=createRequire(path.join(o,"package.json"));let r;try{r=e("expo/metro-config")}catch{throw new Error(`@webority/mobile-tools/metro-config: expo is not installed in ${o}; run npx mobile-tools sync`)}const t=r.getDefaultConfig(o);return t.transformer.minifierConfig={...t.transformer.minifierConfig,compress:{...t.transformer.minifierConfig?.compress,drop_console:!0}},t};
1
+ import{createRequire}from"node:module";import path from"node:path";const FABRIC_PROD=/[\\/]react-native[\\/]Libraries[\\/]Renderer[\\/]implementations[\\/]ReactFabric-prod\.js$/,isReleaseBundle=()=>process.argv.includes("export:embed")||process.argv.includes("bundle"),applyProfiling=(e,r)=>{const o=e.resolver.resolveRequest;e.resolver.resolveRequest=(e,r,i)=>{const n=o?o(e,r,i):e.resolveRequest(e,r,i);return"sourceFile"===n.type&&void 0!==n.filePath&&FABRIC_PROD.test(n.filePath)?{...n,filePath:n.filePath.replace(/ReactFabric-prod\.js$/,"ReactFabric-profiling.js")}:n},e.transformer.minifierConfig={...e.transformer.minifierConfig,keep_fnames:!0,keep_classnames:!0};const i=e.serializer.getModulesRunBeforeMainModule;e.serializer.getModulesRunBeforeMainModule=e=>[r,...i(e)]};export const createMetroConfig=e=>{if(void 0!==process.env.WEBORITY_PROFILING&&isReleaseBundle())throw new Error("WEBORITY_PROFILING must never reach a release bundle");const r=createRequire(path.join(e,"package.json"));let o;try{o=r("expo/metro-config")}catch{throw new Error(`@webority/mobile-tools/metro-config: expo is not installed in ${e}; run npx mobile-tools sync`)}const i=o.getDefaultConfig(e);return i.transformer.minifierConfig={...i.transformer.minifierConfig,compress:{...i.transformer.minifierConfig?.compress,drop_console:!0}},"1"===process.env.WEBORITY_PROFILING&&applyProfiling(i,r.resolve("@webority/mobile-tools/profiling-hook")),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=")),s=1===n.length?n[0].slice(3):"";return s.endsWith(" OTA Updates")?fail("appConfigInvalid",`${t} is the shared OTA certificate (CN=${s}), 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(s)?s:fail("appConfigInvalid",`${t} has common name ${JSON.stringify(s)}, which is not a signing key id (a lowercase name, for example acme)`)};export const otaNativeSettings=(e,t,i,n)=>{const s=t.OTA_UPDATE_BASE_URL;if(!isSet(s)||!isSet(t[COMPONENT_KEY[n]]))return;const a=componentIdFor(t,n),o=t.OTA_PROJECT_ID;if(!isSet(o)||!WHOLE_NUMBER.test(String(o)))return fail("appConfigInvalid",`OTA_PROJECT_ID must be the update server's project id (a positive whole number), not ${JSON.stringify(o)}`);const r=t.OTA_CODE_SIGNING_CERT;if("string"!=typeof r||""===r.trim())return fail("appConfigInvalid","OTA_CODE_SIGNING_CERT must name the PEM certificate file, relative to the app folder");const p=path.join(e,r);if(!existsSync(p))return fail("appConfigInvalid",`OTA_CODE_SIGNING_CERT ${r} not found in ${e}`);const c=readFileSync(p,"utf8").replace(/\r\n/g,"\n").trim();return c.startsWith("-----BEGIN CERTIFICATE-----")?{updateUrl:`${String(s).replace(/\/+$/,"")}/ota/manifest/${o}/${a}/${i}`,runtimeVersion:String(t.VERSION_INTERNAL),certificate:c,keyId:codeSigningKeyIdOf(c,r)}:fail("appConfigInvalid",`${r} is not a PEM certificate`)};const escapeXml=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),escapeRegExp=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const androidOtaMetaData=e=>[["expo.modules.updates.ENABLED","true"],["expo.modules.updates.EXPO_UPDATE_URL",e.updateUrl],["expo.modules.updates.EXPO_RUNTIME_VERSION",e.runtimeVersion],["expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH","ALWAYS"],["expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS","0"],["expo.modules.updates.CODE_SIGNING_CERTIFICATE",e.certificate],["expo.modules.updates.CODE_SIGNING_METADATA",JSON.stringify({keyid:e.keyId,alg:CODE_SIGNING_ALG})]];const childIndent=(e,t)=>{const i=e.split(/\r?\n/).filter(e=>""!==e.trim()),n=/^[ \t]*/.exec(i[i.length-1]??"")?.[0]??"";return n.length>t.length?n:`${t} `};export const rewriteAndroidManifestOta=(e,t)=>{const i=eolOf(e);let n=e;const s=[];for(const[e,i]of androidOtaMetaData(t)){const t=escapeXml(i).replace(/\n/g,"&#10;"),a=new RegExp(`(<meta-data\\s+android:name="${escapeRegExp(e)}"\\s+android:value=")[^"]*(")`);a.test(n)?n=n.replace(a,(e,i,n)=>`${i}${t}${n}`):s.push([e,t])}if(0===s.length)return n;const a=/^([ \t]*)<\/application>/m.exec(n);if(!a)return fail("appConfigInvalid","AndroidManifest.xml has no </application> to add the expo-updates settings to");const o=childIndent(n.slice(0,a.index),a[1]),r=s.map(([e,t])=>`${o}<meta-data android:name="${e}" android:value="${t}" />`).join(i);return`${n.slice(0,a.index)}${r}${i}${n.slice(a.index)}`};export const iosOtaPlistKeys=e=>[["EXUpdatesEnabled",!0],["EXUpdatesURL",e.updateUrl],["EXUpdatesRuntimeVersion",e.runtimeVersion],["EXUpdatesCheckOnLaunch","ALWAYS"],["EXUpdatesLaunchWaitMs",0],["EXUpdatesCodeSigningCertificate",e.certificate],["EXUpdatesCodeSigningMetadata",{keyid:e.keyId,alg:CODE_SIGNING_ALG}]];const plistValue=(e,t,i)=>{if("boolean"==typeof e)return e?"<true/>":"<false/>";if("number"==typeof e)return`<integer>${e}</integer>`;if("string"==typeof e)return`<string>${escapeXml(e).replace(/\n/g,i)}</string>`;const n=`${t}${t.includes("\t")?"\t":" "}`,s=Object.entries(e).map(([e,t])=>`${n}<key>${escapeXml(e)}</key>${i}${n}<string>${escapeXml(t)}</string>`).join(i);return`<dict>${i}${s}${i}${t}</dict>`},PLIST_VALUE=String.raw`(?:<(?:true|false)\/>|<string>[\s\S]*?<\/string>|<integer>[^<]*<\/integer>|<dict>[\s\S]*?<\/dict>|<dict\/>)`;export const rewriteExpoPlistOta=(e,t)=>{const i=eolOf(e);let n=e;const s=[];for(const[e,a]of iosOtaPlistKeys(t)){const t=new RegExp(`([ \\t]*)(<key>${escapeRegExp(e)}<\\/key>\\s*)${PLIST_VALUE}`).exec(n);if(t){const e=t[1];n=`${n.slice(0,t.index)}${e}${t[2]}${plistValue(a,e,i)}${n.slice(t.index+t[0].length)}`}else s.push([e,a])}if(0===s.length)return n;const a=/^([ \t]*)<\/dict>\s*<\/plist>/m.exec(n);if(!a)return fail("appConfigInvalid","Expo.plist has no root </dict> to add the expo-updates settings to");const o=childIndent(n.slice(0,a.index),a[1]),r=s.map(([e,t])=>`${o}<key>${escapeXml(e)}</key>${i}${o}${plistValue(t,o,i)}`).join(i);return`${n.slice(0,a.index)}${r}${i}${n.slice(a.index)}`};
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=")),s=1===n.length?n[0].slice(3):"";return s.endsWith(" OTA Updates")?fail("appConfigInvalid",`${t} is the shared OTA certificate (CN=${s}), 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(s)?s:fail("appConfigInvalid",`${t} has common name ${JSON.stringify(s)}, which is not a signing key id (a lowercase name, for example acme)`)};export const otaNativeSettings=(e,t,i,n,s)=>{const a=t.OTA_UPDATE_BASE_URL;if(!isSet(a)||!isSet(t[COMPONENT_KEY[n]]))return;const o=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 c=path.join(e,p);if(!existsSync(c))return fail("appConfigInvalid",`OTA_CODE_SIGNING_CERT ${p} not found in ${e}`);const d=readFileSync(c,"utf8").replace(/\r\n/g,"\n").trim();return d.startsWith("-----BEGIN CERTIFICATE-----")?{updateUrl:`${String(a).replace(/\/+$/,"")}/ota/manifest/${r}/${o}/${i}`,runtimeVersion:s?`${String(t.VERSION_INTERNAL)}-otatest`:String(t.VERSION_INTERNAL),certificate:d,keyId:codeSigningKeyIdOf(d,p)}:fail("appConfigInvalid",`${p} is not a PEM certificate`)};const escapeXml=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),escapeRegExp=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const androidOtaMetaData=e=>[["expo.modules.updates.ENABLED","true"],["expo.modules.updates.EXPO_UPDATE_URL",e.updateUrl],["expo.modules.updates.EXPO_RUNTIME_VERSION",e.runtimeVersion],["expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH","ALWAYS"],["expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS","0"],["expo.modules.updates.CODE_SIGNING_CERTIFICATE",e.certificate],["expo.modules.updates.CODE_SIGNING_METADATA",JSON.stringify({keyid:e.keyId,alg:CODE_SIGNING_ALG})]];const childIndent=(e,t)=>{const i=e.split(/\r?\n/).filter(e=>""!==e.trim()),n=/^[ \t]*/.exec(i[i.length-1]??"")?.[0]??"";return n.length>t.length?n:`${t} `};export const rewriteAndroidManifestOta=(e,t)=>{const i=eolOf(e);let n=e;const s=[];for(const[e,i]of androidOtaMetaData(t)){const t=escapeXml(i).replace(/\n/g,"&#10;"),a=new RegExp(`(<meta-data\\s+android:name="${escapeRegExp(e)}"\\s+android:value=")[^"]*(")`);a.test(n)?n=n.replace(a,(e,i,n)=>`${i}${t}${n}`):s.push([e,t])}if(0===s.length)return n;const a=/^([ \t]*)<\/application>/m.exec(n);if(!a)return fail("appConfigInvalid","AndroidManifest.xml has no </application> to add the expo-updates settings to");const o=childIndent(n.slice(0,a.index),a[1]),r=s.map(([e,t])=>`${o}<meta-data android:name="${e}" android:value="${t}" />`).join(i);return`${n.slice(0,a.index)}${r}${i}${n.slice(a.index)}`};export const iosOtaPlistKeys=e=>[["EXUpdatesEnabled",!0],["EXUpdatesURL",e.updateUrl],["EXUpdatesRuntimeVersion",e.runtimeVersion],["EXUpdatesCheckOnLaunch","ALWAYS"],["EXUpdatesLaunchWaitMs",0],["EXUpdatesCodeSigningCertificate",e.certificate],["EXUpdatesCodeSigningMetadata",{keyid:e.keyId,alg:CODE_SIGNING_ALG}]];const plistValue=(e,t,i)=>{if("boolean"==typeof e)return e?"<true/>":"<false/>";if("number"==typeof e)return`<integer>${e}</integer>`;if("string"==typeof e)return`<string>${escapeXml(e).replace(/\n/g,i)}</string>`;const n=`${t}${t.includes("\t")?"\t":" "}`,s=Object.entries(e).map(([e,t])=>`${n}<key>${escapeXml(e)}</key>${i}${n}<string>${escapeXml(t)}</string>`).join(i);return`<dict>${i}${s}${i}${t}</dict>`},PLIST_VALUE=String.raw`(?:<(?:true|false)\/>|<string>[\s\S]*?<\/string>|<integer>[^<]*<\/integer>|<dict>[\s\S]*?<\/dict>|<dict\/>)`;export const rewriteExpoPlistOta=(e,t)=>{const i=eolOf(e);let n=e;const s=[];for(const[e,a]of iosOtaPlistKeys(t)){const t=new RegExp(`([ \\t]*)(<key>${escapeRegExp(e)}<\\/key>\\s*)${PLIST_VALUE}`).exec(n);if(t){const e=t[1];n=`${n.slice(0,t.index)}${e}${t[2]}${plistValue(a,e,i)}${n.slice(t.index+t[0].length)}`}else s.push([e,a])}if(0===s.length)return n;const a=/^([ \t]*)<\/dict>\s*<\/plist>/m.exec(n);if(!a)return fail("appConfigInvalid","Expo.plist has no root </dict> to add the expo-updates settings to");const o=childIndent(n.slice(0,a.index),a[1]),r=s.map(([e,t])=>`${o}<key>${escapeXml(e)}</key>${i}${o}${plistValue(t,o,i)}`).join(i);return`${n.slice(0,a.index)}${r}${i}${n.slice(a.index)}`};
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");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)=>{const i=path.join(e,"android","gradle.properties");existsSync(i)||fail("appConfigInvalid",`android/gradle.properties not found in ${e}`),usesFirebase(e)&&!existsSync(path.join(e,"android","app","google-services.json"))&&fail("appConfigInvalid","android/app/google-services.json is missing; the app uses @react-native-firebase, whose Gradle plugin needs it"),checkNdkHook(e);const s=rewriteGradleProperties(readFileSync(i,"utf8"),{APP_NAME:String(t.APP_NAME),APP_ID:gradleAppId(t.APP_ID,n,o.flavors),VERSION_DISPLAY:String(t.VERSION_DISPLAY),VERSION_INTERNAL:String(t.VERSION_INTERNAL)});report(e,i,writeIfChanged(i,s));const r=otaNativeSettings(e,t,n,"android");if(r){const t=path.join(e,ANDROID_MANIFEST);existsSync(t)||fail("appConfigInvalid",`${ANDROID_MANIFEST} not found: expo-updates reads its settings from it`),report(e,t,writeIfChanged(t,rewriteAndroidManifestOta(readFileSync(t,"utf8"),r)))}};export const stampIos=(e,t,n,o,i)=>{const s=path.join(e,"ios",i),r=path.join(s,"Info.plist"),a=path.join(e,"ios",`${i}.xcodeproj`,"project.pbxproj");for(const t of[r,a])existsSync(t)||fail("appConfigInvalid",`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,i);const p=`AppIcon-${n}`,c=path.join(s,"Images.xcassets"),l=existsSync(path.join(c,`${p}.appiconset`)),d=ENVIRONMENTS.filter(e=>e!==n&&existsSync(path.join(c,`AppIcon-${e}.appiconset`)));!l&&d.length>0&&fail("appConfigInvalid",`ios/${i}/Images.xcassets has AppIcon-${d[0]} but no ${p}: run mobile-tools icons`),report(e,r,writeIfChanged(r,rewriteInfoPlistDisplayName(readFileSync(r,"utf8"),String(t.APP_NAME))));const f={PRODUCT_BUNDLE_IDENTIFIER:applicationId(t.APP_ID,n),MARKETING_VERSION:String(t.VERSION_DISPLAY),CURRENT_PROJECT_VERSION:String(t.VERSION_INTERNAL)};l&&(f.ASSETCATALOG_COMPILER_APPICON_NAME=p),report(e,a,writeIfChanged(a,rewritePbxproj(readFileSync(a,"utf8"),f,{quote:["PRODUCT_BUNDLE_IDENTIFIER"]}))),l||say("prebuild: no AppIcon-<Env> sets, app icon left as the project sets it");const u=path.join(s,"GoogleService-Info.plist"),I=path.join(e,o.firebasePlistDir,`GoogleService-Info.${n}.plist`);if(!existsSync(I)&&usesFirebase(e)&&fail("appConfigInvalid",`${path.relative(e,I)} is missing; the app uses @react-native-firebase, and an iOS build without it reports no crashes`),existsSync(I)){const t=!existsSync(u)||!readFileSync(I).equals(readFileSync(u));t&&copyFileSync(I,u),report(e,u,t)}else existsSync(u)&&(rmSync(u),say(`prebuild: removed ${path.relative(e,u)}, no ${path.relative(e,I)}`));const g=otaNativeSettings(e,t,n,"ios");if(g){const t=path.join(e,expoPlistPath(i));existsSync(t)||fail("appConfigInvalid",`${expoPlistPath(i)} not found: add it to the Xcode project for expo-updates`),report(e,t,writeIfChanged(t,rewriteExpoPlistOta(readFileSync(t,"utf8"),g)))}};export const prebuild=e=>{const{appRoot:t,sameEnv:n,platform:o}=e;n&&e.env&&fail("badInput","pass --env or --same-env, not both");const i=n?lastCompiledEnvironment(t):e.env;if(!i)return fail("badInput","prebuild needs --env <Development|Staging|Production> or --same-env");const s=loadToolsConfig(t),r=resolveIosProjectName(t,s),a=loadAppSettings(t,i,s.requiredSettings);checkAppSettings(t,a,i);const p={...a,COMMIT:buildCommit(t,prebuildOutputs(r,Boolean(a.OTA_UPDATE_BASE_URL))),BUILD_TIME:(new Date).toISOString()};writeFileSync(path.join(t,COMPILED_FILE),`${JSON.stringify(p,null,2)}\n`,"utf8"),say(`prebuild: wrote ${COMPILED_FILE} (${i}, ${p.COMMIT})`);const c=existsSync(path.join(t,"android"));if(("android"===o||!o&&c)&&stampAndroid(t,p,i,s),"ios"===o||!o&&r){if(!r)return fail("appConfigInvalid",`no ios/ folder in ${t}`);stampIos(t,p,i,s,r)}return"Production"!==i&&say(`prebuild: tracked native files now carry ${i} values; run prebuild --env Production before committing them`),p};
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");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&&copyFileSync(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};
@@ -0,0 +1 @@
1
+ const scope=globalThis;if(void 0===scope.__REACT_DEVTOOLS_GLOBAL_HOOK__){const e=new Map,o=new Map;let t=1;scope.__REACT_DEVTOOLS_GLOBAL_HOOK__={supportsFiber:!0,renderers:e,inject(_){const n=t++;return e.set(n,_),o.set(n,new Set),n},onCommitFiberRoot(e,t){o.get(e)?.add(t)},onCommitFiberUnmount(){},onPostCommitFiberRoot(){},getFiberRoots:e=>o.get(e)??new Set,checkDCE(){}}}scope.__WEBORITY_PROFILING__=!0;export{};
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,resolveIosProjectName,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{describeAppleDevice,listAppleDevices,xcodebuildArgs}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCODEBUILD,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";export const readNamespace=i=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(i)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(i,e)=>{const a=i.filter(i=>i.endsWith(".apk")).sort();return a.find(i=>i.includes(e))??a.find(i=>i.includes("universal"))??a[0]};const adbOrFail=(i,e,a)=>{const o=capture({file:i,args:e},a);return 0!==o.status?fail("toolFailed",`adb ${e.join(" ")} failed: ${(o.stderr||o.stdout).trim()}`):o.stdout};export const installFailure=i=>/Failure \[[^\]]*\]/.exec(i)?.[0];const installOrFail=(i,e,a,o)=>{const r=adbOrFail(i,["-s",e,"install","-r",a],o),t=installFailure(r);void 0!==t&&fail("toolFailed",`adb install on ${e} failed: ${t}`)},elapsedSeconds=i=>((Date.now()-i)/1e3).toFixed(1),cachedBuild=(i,e,a)=>{const o=i.fingerprint.slice(0,12),r=e?void 0:findCachedBuild(i);if(r)return say(`cache: hit ${o} (fingerprint ${a} s), installing the cached build`),r;say(e?"cache: skipped (--rebuild), building":`cache: miss ${o} (fingerprint ${a} s), building`)},DEVICE_METRO_PORT=8081,runAndroid=async i=>{const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=path.join(e,"android"),t=path.join(r,"app","build.gradle");existsSync(t)||fail("appConfigInvalid",`android/app/build.gradle not found in ${e}`);const s=readNamespace(readFileSync(t,"utf8"))??fail("appConfigInvalid","android/app/build.gradle declares no namespace"),n=adbPath(process.env,process.platform);await connectLocalEmulators(n,e);const l=listAndroidDevices(n,e),d=await pickDevice(l,i.device,describeDevice,!0===process.stdin.isTTY);ABI_PATTERN.test(d.abi)||fail("deviceNotFound",`${describeDevice(d)} reports an ABI this build cannot target: "${d.abi}"`),say(`run: ${describeDevice(d)}`);const c=prebuild({appRoot:e,env:a,platform:"android"}),p=Date.now(),u=await nativeFingerprint(e,"android"),f={appId:baseAppId(e),platform:"android",variant:variantName(a,o.flavors,"debug"),abi:d.abi,fingerprint:u};let m=cachedBuild(f,i.rebuild,elapsedSeconds(p));if(void 0===m){const i=gradleTask(a,o.flavors,"apk","debug"),t=logDir(e);await buildOrFail(gradleInvocation(r,[`:app:${i}`,`-PreactNativeArchitectures=${d.abi}`],process.platform,process.env),r,path.join(t,`gradle-${a}-${logStamp()}.log`),`gradle ${i}`);const s=gradleOutputDir(e,a,o.flavors,"apk","debug"),n=existsSync(s)?chooseApk(readdirSync(s),d.abi):void 0;n||fail("artifactMissing",`the build produced no APK in ${s}`),m=path.join(s,n),storeCachedBuild(f,m),say(`cache: stored ${u.slice(0,12)}`)}say(`run: installing ${path.basename(m)} on ${d.serial}`),installOrFail(n,d.serial,m,e);const v=await ensureMetro(e,path.join(logDir(e),"metro.log"),{fixedPort:!1});if(adbOrFail(n,["-s",d.serial,"reverse","tcp:8081",`tcp:${v}`],e),"Development"===a){const i=localApiPort(String(c.BASE_URL),o);void 0!==i&&(adbOrFail(n,["-s",d.serial,"reverse",`tcp:${i}`,`tcp:${i}`],e),say(`run: device localhost:${i} reaches this machine's API`))}const b=applicationId(c.APP_ID,a);adbOrFail(n,["-s",d.serial,"shell","am","start","-n",`${b}/${s}.MainActivity`],e),say(`run: launched ${b} on ${d.model||d.serial}`)},runIos=async i=>{requireDarwin(process.platform,"run --platform ios");const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=resolveIosProjectName(e,o)??fail("appConfigInvalid",`no ios/ folder in ${e}`),t=path.join(e,"ios",`${r}.xcworkspace`);existsSync(t)||fail("appConfigInvalid",`ios/${r}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`);const s=await pickDevice(listAppleDevices(e),i.device,describeAppleDevice,!0===process.stdin.isTTY);say(`run: ${describeAppleDevice(s)}`);const n=prebuild({appRoot:e,env:a,platform:"ios"}),l=logDir(e);let d,c;if("simulator"===s.kind){const a=Date.now(),o=await nativeFingerprint(e,"ios");d={appId:baseAppId(e),platform:"ios",variant:"Debug-iphonesimulator",abi:"simulator",fingerprint:o},c=cachedBuild(d,i.rebuild,elapsedSeconds(a))}else say("cache: not used for a physical iPhone");if(void 0===c){await buildOrFail({file:XCODEBUILD,args:xcodebuildArgs(r,s.udid)},e,path.join(l,`xcodebuild-${a}-${logStamp()}.log`),"xcodebuild");const i=path.join(e,"ios","build","Build","Products","simulator"===s.kind?"Debug-iphonesimulator":"Debug-iphoneos"),o=existsSync(i)?readdirSync(i).filter(i=>i.endsWith(".app")):[],t=o.find(i=>i===`${r}.app`)??o[0];t||fail("artifactMissing",`the build produced no .app in ${i}`),c=path.join(i,t),d&&(storeCachedBuild(d,c),say(`cache: stored ${d.fingerprint.slice(0,12)}`))}const p=applicationId(n.APP_ID,a);await ensureMetro(e,path.join(l,"metro.log"),{fixedPort:!0});const u="simulator"===s.kind?[["simctl","install",s.udid,c],["simctl","launch",s.udid,p]]:[["devicectl","device","install","app","--device",s.udid,c],["devicectl","device","process","launch","--device",s.udid,p]];for(const i of u){const a=capture({file:XCRUN,args:i},e);0!==a.status&&fail("toolFailed",`xcrun ${i.slice(0,3).join(" ")} failed: ${(a.stderr||a.stdout).trim()}`)}say(`run: launched ${p} on ${s.name}`)};export const run=async i=>{requireSynced(i.appRoot),"ios"===i.platform?await runIos(i):await runAndroid(i)};
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,resolveIosProjectName,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{describeAppleDevice,listAppleDevices,xcodebuildArgs}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCODEBUILD,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{ensurePods,requireSynced}from"./sync.js";export const readNamespace=i=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(i)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(i,e)=>{const a=i.filter(i=>i.endsWith(".apk")).sort();return a.find(i=>i.includes(e))??a.find(i=>i.includes("universal"))??a[0]};const adbOrFail=(i,e,a)=>{const o=capture({file:i,args:e},a);return 0!==o.status?fail("toolFailed",`adb ${e.join(" ")} failed: ${(o.stderr||o.stdout).trim()}`):o.stdout};export const installFailure=i=>/Failure \[[^\]]*\]/.exec(i)?.[0];const installOrFail=(i,e,a,o)=>{const r=adbOrFail(i,["-s",e,"install","-r",a],o),t=installFailure(r);void 0!==t&&fail("toolFailed",`adb install on ${e} failed: ${t}`)},elapsedSeconds=i=>((Date.now()-i)/1e3).toFixed(1),cachedBuild=(i,e,a)=>{const o=i.fingerprint.slice(0,12),r=e?void 0:findCachedBuild(i);if(r)return say(`cache: hit ${o} (fingerprint ${a} s), installing the cached build`),r;say(e?"cache: skipped (--rebuild), building":`cache: miss ${o} (fingerprint ${a} s), building`)},DEVICE_METRO_PORT=8081,runAndroid=async i=>{const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=path.join(e,"android"),t=path.join(r,"app","build.gradle");existsSync(t)||fail("appConfigInvalid",`android/app/build.gradle not found in ${e}`);const s=readNamespace(readFileSync(t,"utf8"))??fail("appConfigInvalid","android/app/build.gradle declares no namespace"),n=adbPath(process.env,process.platform);await connectLocalEmulators(n,e);const d=listAndroidDevices(n,e),l=await pickDevice(d,i.device,describeDevice,!0===process.stdin.isTTY);ABI_PATTERN.test(l.abi)||fail("deviceNotFound",`${describeDevice(l)} reports an ABI this build cannot target: "${l.abi}"`),say(`run: ${describeDevice(l)}`);const c=prebuild({appRoot:e,env:a,platform:"android"}),p=Date.now(),u=await nativeFingerprint(e,"android"),f={appId:baseAppId(e),platform:"android",variant:variantName(a,o.flavors,"debug"),abi:l.abi,fingerprint:u};let m=cachedBuild(f,i.rebuild,elapsedSeconds(p));if(void 0===m){const i=gradleTask(a,o.flavors,"apk","debug"),t=logDir(e);await buildOrFail(gradleInvocation(r,[`:app:${i}`,`-PreactNativeArchitectures=${l.abi}`],process.platform,process.env),r,path.join(t,`gradle-${a}-${logStamp()}.log`),`gradle ${i}`);const s=gradleOutputDir(e,a,o.flavors,"apk","debug"),n=existsSync(s)?chooseApk(readdirSync(s),l.abi):void 0;n||fail("artifactMissing",`the build produced no APK in ${s}`),m=path.join(s,n),storeCachedBuild(f,m),say(`cache: stored ${u.slice(0,12)}`)}say(`run: installing ${path.basename(m)} on ${l.serial}`),installOrFail(n,l.serial,m,e);const v=await ensureMetro(e,path.join(logDir(e),"metro.log"),{fixedPort:!1,profiling:i.profiling});if(adbOrFail(n,["-s",l.serial,"reverse","tcp:8081",`tcp:${v}`],e),"Development"===a){const i=localApiPort(String(c.BASE_URL),o);void 0!==i&&(adbOrFail(n,["-s",l.serial,"reverse",`tcp:${i}`,`tcp:${i}`],e),say(`run: device localhost:${i} reaches this machine's API`))}const g=applicationId(c.APP_ID,a);adbOrFail(n,["-s",l.serial,"shell","am","start","-n",`${g}/${s}.MainActivity`],e),say(`run: launched ${g} on ${l.model||l.serial}`)},runIos=async i=>{requireDarwin(process.platform,"run --platform ios");const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=resolveIosProjectName(e,o)??fail("appConfigInvalid",`no ios/ folder in ${e}`);ensurePods(e,process.platform);const t=path.join(e,"ios",`${r}.xcworkspace`);existsSync(t)||fail("appConfigInvalid",`ios/${r}.xcworkspace not found: the pods step did not create it; see its output above`);const s=await pickDevice(listAppleDevices(e),i.device,describeAppleDevice,!0===process.stdin.isTTY);say(`run: ${describeAppleDevice(s)}`);const n=prebuild({appRoot:e,env:a,platform:"ios"}),d=logDir(e);let l,c;if("simulator"===s.kind){const a=Date.now(),o=await nativeFingerprint(e,"ios");l={appId:baseAppId(e),platform:"ios",variant:"Debug-iphonesimulator",abi:"simulator",fingerprint:o},c=cachedBuild(l,i.rebuild,elapsedSeconds(a))}else say("cache: not used for a physical iPhone");if(void 0===c){await buildOrFail({file:XCODEBUILD,args:xcodebuildArgs(r,s.udid)},e,path.join(d,`xcodebuild-${a}-${logStamp()}.log`),"xcodebuild");const i=path.join(e,"ios","build","Build","Products","simulator"===s.kind?"Debug-iphonesimulator":"Debug-iphoneos"),o=existsSync(i)?readdirSync(i).filter(i=>i.endsWith(".app")):[],t=o.find(i=>i===`${r}.app`)??o[0];t||fail("artifactMissing",`the build produced no .app in ${i}`),c=path.join(i,t),l&&(storeCachedBuild(l,c),say(`cache: stored ${l.fingerprint.slice(0,12)}`))}const p=applicationId(n.APP_ID,a);await ensureMetro(e,path.join(d,"metro.log"),{fixedPort:!0,profiling:i.profiling});const u="simulator"===s.kind?[["simctl","install",s.udid,c],["simctl","launch",s.udid,p]]:[["devicectl","device","install","app","--device",s.udid,c],["devicectl","device","process","launch","--device",s.udid,p]];for(const i of u){const a=capture({file:XCRUN,args:i},e);0!==a.status&&fail("toolFailed",`xcrun ${i.slice(0,3).join(" ")} failed: ${(a.stderr||a.stdout).trim()}`)}say(`run: launched ${p} on ${s.name}`)};export const run=async i=>{"ios"===i.platform&&ensurePods(i.appRoot,process.platform),requireSynced(i.appRoot),"ios"===i.platform?await runIos(i):await runAndroid(i)};
package/lib/sync.js CHANGED
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{existsSync,readdirSync,readFileSync,rmSync,statSync,writeFileSync}from"node:fs";import path from"node:path";import{runInherit}from"./exec.js";import{fail,say}from"./output.js";import{findOnPath,gradleInvocation,npmInvocation,pnpmInvocation}from"./platform.js";export const STAMP_FILE=path.join("node_modules",".webority-install.json");const INSTALL_FIELDS=["dependencies","devDependencies","optionalDependencies","peerDependencies","bundleDependencies","overrides","resolutions","pnpm","workspaces"];export const lockfileOf=e=>{const n=path.join(e,"pnpm-lock.yaml"),t=path.join(e,"package-lock.json");return existsSync(n)&&existsSync(t)?fail("appConfigInvalid","both pnpm-lock.yaml and package-lock.json exist: delete the one not in use"):existsSync(n)?{manager:"pnpm",file:n}:existsSync(t)?{manager:"npm",file:t}:fail("appConfigInvalid",`no package-lock.json or pnpm-lock.yaml in ${e}`)};const filesUnder=(e,n="")=>readdirSync(e,{withFileTypes:!0}).flatMap(t=>{const o=""===n?t.name:`${n}/${t.name}`;return t.isDirectory()?filesUnder(path.join(e,t.name),o):[o]}).sort();export const installHash=e=>{const{file:n}=lockfileOf(e),t=JSON.parse(readFileSync(path.join(e,"package.json"),"utf8")),o=createHash("sha256");o.update(`lockfile:${path.basename(n)}\0`),o.update(readFileSync(n)),o.update("\0package.json\0"),o.update(JSON.stringify(INSTALL_FIELDS.map(e=>[e,t[e]??null])));const s=path.join(e,"patches");if(existsSync(s))for(const e of filesUnder(s))o.update(`\0patches/${e}\0`),o.update(readFileSync(path.join(s,...e.split("/"))));return o.digest("hex")};export const readStamp=e=>{const n=path.join(e,STAMP_FILE);if(existsSync(n))try{return JSON.parse(readFileSync(n,"utf8"))}catch{return}};const podFix=e=>existsSync(path.join(e,"Gemfile"))?"cd ios && bundle exec pod install":"cd ios && pod install";export const podsStale=e=>{const n=path.join(e,"ios","Podfile.lock"),t=path.join(e,"ios","Pods","Manifest.lock");return!(!existsSync(n)||!existsSync(t))&&!readFileSync(n).equals(readFileSync(t))};export const checkSync=(e,n)=>{const t=[],o=readStamp(e),{file:s}=lockfileOf(e);if(o?o.hash!==installHash(e)&&t.push(`node_modules is stale: ${path.basename(s)}, package.json or patches/ changed since the last install. Fix: npx mobile-tools sync`):t.push("node_modules has no install stamp (never installed through mobile-tools). Fix: npx mobile-tools sync"),o){const n=changedNativePackages(o.nativePackages??{},nativePackages(e));n.length>0&&t.push(`node_modules native packages changed since the last install: ${n.join(", ")}. Fix: npx mobile-tools sync`)}return"darwin"===n&&podsStale(e)&&t.push(`ios/Pods is stale: ios/Podfile.lock differs from ios/Pods/Manifest.lock. Fix: ${podFix(e)}`),t};export const requireSynced=e=>{const n=checkSync(e,process.platform);n.length>0&&fail("installStale",n.join("\n"))};export const nativePackages=e=>{const n=path.join(e,"node_modules");if(!existsSync(n))return{};const t=[];for(const e of readdirSync(n,{withFileTypes:!0}))if(!e.name.startsWith(".")&&e.isDirectory())if(e.name.startsWith("@"))for(const o of readdirSync(path.join(n,e.name),{withFileTypes:!0}))o.isDirectory()&&t.push(path.join(n,e.name,o.name));else t.push(path.join(n,e.name));const o={};for(const e of t){const n=path.join(e,"android"),t=path.join(e,"package.json");if(existsSync(n)&&statSync(n).isDirectory()&&existsSync(t))try{const e=JSON.parse(readFileSync(t,"utf8"));e.name&&e.version&&(o[e.name]=e.version)}catch{}}return o};export const changedNativePackages=(e,n)=>[...new Set([...Object.keys(e),...Object.keys(n)])].filter(t=>e[t]!==n[t]).sort();export const writeStamp=e=>{const{manager:n}=lockfileOf(e),t={hash:installHash(e),manager:n,nativePackages:nativePackages(e)};return writeFileSync(path.join(e,STAMP_FILE),`${JSON.stringify(t,null,2)}\n`,"utf8"),t};export const installInvocation=(e,n)=>"pnpm"===e?pnpmInvocation(["install","--frozen-lockfile"],process.env,n,process.execPath):npmInvocation(["ci"],process.env,n,process.execPath);export const sync=(e,n)=>{const{manager:t}=lockfileOf(e),o=readStamp(e)?.nativePackages??nativePackages(e);say("sync: "+("pnpm"===t?"pnpm install --frozen-lockfile":"npm ci")),runInherit(installInvocation(t,n),e,`${t} install`);const s=writeStamp(e).nativePackages,a=changedNativePackages(o,s),i=path.join(e,"android"),c=existsSync(path.join(i,"app",".cxx"))||existsSync(path.join(i,"app","build"));if(a.length>0&&c){say(`sync: native packages changed (${a.join(", ")}): regenerating codegen`),runInherit(gradleInvocation(i,["generateCodegenArtifactsFromSchema"],n,process.env),i,"gradlew generateCodegenArtifactsFromSchema");for(const n of[path.join(i,"app",".cxx"),path.join(i,"app","build","generated","autolinking")])rmSync(n,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,n)}`)}if("darwin"===n&&existsSync(path.join(e,"ios","Podfile"))){const t=path.join(e,"ios","Podfile.lock"),o=path.join(e,"ios","Pods","Manifest.lock");if(!existsSync(t)||!existsSync(o)||podsStale(e)){const t=existsSync(path.join(e,"Gemfile")),o=findOnPath(t?"bundle":"pod",process.env,n);o||fail("toolMissing",`${t?"bundle":"pod"} not found on PATH; install CocoaPods, then ${podFix(e)}`),runInherit({file:o,args:t?["exec","pod","install"]:["install"]},path.join(e,"ios"),"pod install");const s=path.join(e,"ios","build","ModuleCache.noindex");existsSync(s)&&(rmSync(s,{recursive:!0,force:!0}),say(`sync: removed ${path.relative(e,s)}`))}}say("sync: done")};
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 t=""===n?s.name:`${n}/${s.name}`;return s.isDirectory()?filesUnder(path.join(e,s.name),t):[t]}).sort();export const installHash=e=>{const{file:n}=lockfileOf(e),s=JSON.parse(readFileSync(path.join(e,"package.json"),"utf8")),t=createHash("sha256");t.update(`lockfile:${path.basename(n)}\0`),t.update(readFileSync(n)),t.update("\0package.json\0"),t.update(JSON.stringify(INSTALL_FIELDS.map(e=>[e,s[e]??null])));const o=path.join(e,"patches");if(existsSync(o))for(const e of filesUnder(o))t.update(`\0patches/${e}\0`),t.update(readFileSync(path.join(o,...e.split("/"))));return t.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))};export const checkSync=(e,n)=>{const s=[],t=readStamp(e),{file:o}=lockfileOf(e);if(t?t.hash!==installHash(e)&&s.push(`node_modules is stale: ${path.basename(o)}, 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"),t){const n=changedNativePackages(t.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"darwin"===n&&podsStale(e)&&s.push(`ios/Pods is stale: ios/Podfile.lock differs from ios/Pods/Manifest.lock. Fix: ${podFix(e)}`),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 t of readdirSync(path.join(n,e.name),{withFileTypes:!0}))t.isDirectory()&&s.push(path.join(n,e.name,t.name));else s.push(path.join(n,e.name));const t={};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&&(t[e.name]=e.version)}catch{}}return t};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("darwin"!==n||!existsSync(path.join(e,"ios","Podfile")))return;const s=path.join(e,"ios","Pods"),t=path.join(e,PODS_RECORD);let o;if(existsSync(s)?existsSync(path.join(e,"ios","Podfile.lock"))?existsSync(path.join(s,"Manifest.lock"))?podsStale(e)?o="ios/Podfile.lock differs from ios/Pods/Manifest.lock":existsSync(t)?readFileSync(t,"utf8")!==JSON.stringify(nativePackages(e))&&(o="native packages changed since the last pod install"):o="no pod install record":o="ios/Pods/Manifest.lock is missing":o="ios/Podfile.lock is missing":o="ios/Pods is missing",void 0===o)return;say(`pods: ${o}; 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(t,JSON.stringify(nativePackages(e)),"utf8")};export const sync=(e,n)=>{const{manager:s}=lockfileOf(e),t=readStamp(e)?.nativePackages??nativePackages(e);say("sync: "+("pnpm"===s?"pnpm install --frozen-lockfile":"npm ci")),runInherit(installInvocation(s,n),e,`${s} install`);const o=writeStamp(e).nativePackages,i=changedNativePackages(t,o),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)}`)}ensurePods(e,n),say("sync: done")};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-tools",
3
- "version": "0.109.8",
3
+ "version": "0.110.0",
4
4
  "description": "Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run and per-environment launcher icons, the same on Windows and macOS.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "exports": {
26
26
  "./package.json": "./package.json",
27
- "./metro-config": "./lib/metroConfig.js"
27
+ "./metro-config": "./lib/metroConfig.js",
28
+ "./profiling-hook": "./lib/profilingHook.js"
28
29
  },
29
30
  "files": [
30
31
  "lib",