@webority/mobile-tools 0.109.3 → 0.109.4

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/lib/devices.js CHANGED
@@ -1 +1 @@
1
- import{Socket}from"node:net";import{createInterface}from"node:readline/promises";import{capture}from"./exec.js";import{listAppleDevices}from"./ios.js";import{fail,isJsonOutput,say,sayJson,warn}from"./output.js";import{adbPath,requireDarwin}from"./platform.js";export const parseAdbDevices=e=>e.split(/\r?\n/).map(e=>e.trim()).filter(e=>""!==e&&!e.startsWith("List of devices")&&!e.startsWith("*")).map(e=>{const[t,o=""]=e.split(/\s+/);return{serial:t,state:o}});const serialRank=e=>e.startsWith("emulator-")?1:e.includes(":")?2:0;export const dedupeDevices=e=>{const t=new Map;for(const o of e){const e=""===o.serialNo?`serial:${o.serial}`:`${o.serialNo}|${o.androidId}`;t.set(e,[...t.get(e)??[],o])}return[...t.values()].map(e=>{const t=e.map(e=>e.serial).sort((e,t)=>serialRank(e)-serialRank(t)||e.localeCompare(t)),o=e.find(e=>e.serial===t[0]);return{serial:t[0],serials:t,model:o.model,abi:o.abi}}).sort((e,t)=>e.serial.localeCompare(t.serial))};const PROBE_SCRIPT="getprop ro.serialno; getprop ro.product.model; getprop ro.product.cpu.abi; settings get secure android_id";export const probeDevice=(e,t,o)=>{const s=capture({file:e,args:["-s",t,"shell",PROBE_SCRIPT]},o);if(0!==s.status)return fail("toolFailed",`adb could not read ${t}: ${s.stderr.trim()||s.stdout.trim()}`);const[r="",i="",n="",a=""]=s.stdout.split(/\r?\n/).map(e=>e.trim());return{serial:t,serialNo:r,model:i,abi:n,androidId:a}};export const listAndroidDevices=(e,t)=>{const o=capture({file:e,args:["devices"]},t);if(0!==o.status)return fail("toolFailed",`adb devices failed: ${o.stderr.trim()}`);const s=parseAdbDevices(o.stdout),r=s.filter(e=>"device"!==e.state),i=s.filter(e=>"device"===e.state);if(0===i.length){const e=r.map(e=>`${e.serial} is ${e.state}`).join("; ");return fail("deviceNotFound",`no Android device ready${e?` (${e})`:""}. Connect a phone with USB debugging, start an emulator, or adb connect host:port.`)}return dedupeDevices(i.map(o=>probeDevice(e,o.serial,t)))};export const describeDevice=e=>`${e.model||"unknown model"} (${e.serial}, ${e.abi})`;export const findDevice=(e,t)=>e.find(e=>e.serials.includes(t))??e.find(e=>e.model.toLowerCase()===t.toLowerCase());export const pickDevice=async(e,t,o,s)=>{const r=e.map((e,t)=>` ${t+1}. ${o(e)}`).join("\n");if(t)return findDevice(e,t)??fail("deviceNotFound",`no device matches --device ${t}. Connected:\n${r}`);if(1===e.length)return e[0];if(!s)return fail("deviceAmbiguous",`several devices are connected; pass --device <serial|name>:\n${r}`);const i=createInterface({input:process.stdin,output:process.stdout});try{const t=await i.question(`Several devices are connected:\n${r}\nRun on which? [1-${e.length}] `);return e[Number(t.trim())-1]??fail("badInput",`no device numbered ${t.trim()}`)}finally{i.close()}};export const LOCAL_EMULATOR_PORTS=[...Array.from({length:16},(e,t)=>16384+32*t),7555];const isPortOpen=e=>new Promise(t=>{const o=new Socket;o.setTimeout(300),o.once("connect",()=>{o.destroy(),t(!0)}),o.once("timeout",()=>{o.destroy(),t(!1)}),o.once("error",()=>{o.destroy(),t(!1)}),o.connect(e,"127.0.0.1")});export const connectLocalEmulators=async(e,t)=>{const o=(await Promise.all(LOCAL_EMULATOR_PORTS.map(async e=>({port:e,open:await isPortOpen(e)})))).filter(e=>e.open).map(e=>e.port),s=capture({file:e,args:["devices"]},t),r=0===s.status?parseAdbDevices(s.stdout).map(e=>e.serial):[],i=[];for(const s of o){const o=`127.0.0.1:${s}`;if(r.includes(o))continue;const n=capture({file:e,args:["connect",o]},t),a=`${n.stdout}${n.stderr}`.trim();a.includes("connected to")?(say(`devices: connected ${o}`),i.push(o)):warn(`devices: ${o} is open but adb connect said: ${a}`)}return i};export const devices=async e=>{const{appRoot:t,platform:o}=e;let s;if("android"===o){const e=adbPath(process.env,process.platform);await connectLocalEmulators(e,t),s=listAndroidDevices(e,t).map(e=>({platform:"android",...e}))}else requireDarwin(process.platform,"devices --platform ios"),s=listAppleDevices(t).map(e=>({platform:"ios",...e}));if(isJsonOutput())sayJson({devices:s});else for(const e of s)if("android"===e.platform){const t=e.serials.length>1?` (also ${e.serials.slice(1).join(", ")})`:"";say(`${e.serial} ${e.model} ${e.abi}${t}`)}else say(`${e.udid} ${e.name} ${e.kind}`)};
1
+ import{Socket}from"node:net";import{createInterface}from"node:readline/promises";import{capture}from"./exec.js";import{listAppleDevices}from"./ios.js";import{fail,isJsonOutput,say,sayJson,warn}from"./output.js";import{adbPath,requireDarwin}from"./platform.js";export const parseAdbDevices=e=>e.split(/\r?\n/).map(e=>e.trim()).filter(e=>""!==e&&!e.startsWith("List of devices")&&!e.startsWith("*")).map(e=>{const[t,o=""]=e.split(/\s+/);return{serial:t,state:o}});const serialRank=e=>e.startsWith("emulator-")?1:e.includes(":")?2:0,deviceKey=e=>""!==e.serialNo?`${e.serialNo}|${e.androidId}`:/^[0-9a-f]{16}$/i.test(e.androidId)?`android:${e.androidId}`:`serial:${e.serial}`;export const dedupeDevices=e=>{const t=new Map;for(const o of e){const e=deviceKey(o);t.set(e,[...t.get(e)??[],o])}return[...t.values()].map(e=>{const t=e.map(e=>e.serial).sort((e,t)=>serialRank(e)-serialRank(t)||e.localeCompare(t)),o=e.find(e=>e.serial===t[0]);return{serial:t[0],serials:t,model:o.model,abi:o.abi}}).sort((e,t)=>e.serial.localeCompare(t.serial))};const PROBE_SCRIPT="getprop ro.serialno; getprop ro.product.model; getprop ro.product.cpu.abi; settings get secure android_id";export const probeDevice=(e,t,o)=>{const s=capture({file:e,args:["-s",t,"shell",PROBE_SCRIPT]},o);if(0!==s.status)return fail("toolFailed",`adb could not read ${t}: ${s.stderr.trim()||s.stdout.trim()}`);const[r="",i="",n="",a=""]=s.stdout.split(/\r?\n/).map(e=>e.trim());return{serial:t,serialNo:r,model:i,abi:n,androidId:a}};export const listAndroidDevices=(e,t)=>{const o=capture({file:e,args:["devices"]},t);if(0!==o.status)return fail("toolFailed",`adb devices failed: ${o.stderr.trim()}`);const s=parseAdbDevices(o.stdout),r=s.filter(e=>"device"!==e.state),i=s.filter(e=>"device"===e.state);if(0===i.length){const e=r.map(e=>`${e.serial} is ${e.state}`).join("; ");return fail("deviceNotFound",`no Android device ready${e?` (${e})`:""}. Connect a phone with USB debugging, start an emulator, or adb connect host:port.`)}return dedupeDevices(i.map(o=>probeDevice(e,o.serial,t)))};export const describeDevice=e=>`${e.model||"unknown model"} (${e.serial}, ${e.abi})`;export const findDevice=(e,t)=>e.find(e=>e.serials.includes(t))??e.find(e=>e.model.toLowerCase()===t.toLowerCase());export const pickDevice=async(e,t,o,s)=>{const r=e.map((e,t)=>` ${t+1}. ${o(e)}`).join("\n");if(t)return findDevice(e,t)??fail("deviceNotFound",`no device matches --device ${t}. Connected:\n${r}`);if(1===e.length)return e[0];if(!s)return fail("deviceAmbiguous",`several devices are connected; pass --device <serial|name>:\n${r}`);const i=createInterface({input:process.stdin,output:process.stdout});try{const t=await i.question(`Several devices are connected:\n${r}\nRun on which? [1-${e.length}] `);return e[Number(t.trim())-1]??fail("badInput",`no device numbered ${t.trim()}`)}finally{i.close()}};export const LOCAL_EMULATOR_PORTS=[...Array.from({length:16},(e,t)=>16384+32*t),7555];const isPortOpen=e=>new Promise(t=>{const o=new Socket;o.setTimeout(300),o.once("connect",()=>{o.destroy(),t(!0)}),o.once("timeout",()=>{o.destroy(),t(!1)}),o.once("error",()=>{o.destroy(),t(!1)}),o.connect(e,"127.0.0.1")});export const connectLocalEmulators=async(e,t)=>{const o=(await Promise.all(LOCAL_EMULATOR_PORTS.map(async e=>({port:e,open:await isPortOpen(e)})))).filter(e=>e.open).map(e=>e.port),s=capture({file:e,args:["devices"]},t),r=0===s.status?parseAdbDevices(s.stdout).map(e=>e.serial):[],i=[];for(const s of o){const o=`127.0.0.1:${s}`;if(r.includes(o))continue;const n=capture({file:e,args:["connect",o]},t),a=`${n.stdout}${n.stderr}`.trim();a.includes("connected to")?(say(`devices: connected ${o}`),i.push(o)):warn(`devices: ${o} is open but adb connect said: ${a}`)}return i};export const devices=async e=>{const{appRoot:t,platform:o}=e;let s;if("android"===o){const e=adbPath(process.env,process.platform);await connectLocalEmulators(e,t),s=listAndroidDevices(e,t).map(e=>({platform:"android",...e}))}else requireDarwin(process.platform,"devices --platform ios"),s=listAppleDevices(t).map(e=>({platform:"ios",...e}));if(isJsonOutput())sayJson({devices:s});else for(const e of s)if("android"===e.platform){const t=e.serials.length>1?` (also ${e.serials.slice(1).join(", ")})`:"";say(`${e.serial} ${e.model} ${e.abi}${t}`)}else say(`${e.udid} ${e.name} ${e.kind}`)};
package/lib/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(o,r)?"ours":"other"};const portIsFree=t=>new Promise(e=>{const r=createServer();r.once("error",()=>e(!1)),r.once("listening",()=>r.close(()=>e(!0))),r.listen(t)});export const probeMetro=async(t,e)=>{try{const r=await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(1e3)});return classifyStatus(await r.text(),r.headers.get("X-React-Native-Project-Root"),e)}catch{return await portIsFree(t)?"free":"other"}};const sleep=t=>new Promise(e=>setTimeout(e,t)),waitForMetro=async(t,e,r)=>{const o=Date.now()+12e4;for(;Date.now()<o;){if("ours"===await probeMetro(t,e))return;await new Promise(t=>setTimeout(t,500))}fail("metroUnavailable",`Metro did not answer on port ${t} within 2 minutes; see ${r}`)};export const METRO_RECORD_FILE="metro.json";export const readMetroRecord=t=>{const e=path.join(logDir(t),"metro.json");if(existsSync(e))try{const t=JSON.parse(readFileSync(e,"utf8"));return"number"==typeof t.port&&"number"==typeof t.pid?t:void 0}catch{return}};const writeMetroRecord=(t,e,r)=>{mkdirSync(logDir(t),{recursive:!0});const o={port:e,pid:r,appRoot:t,startedAtUtc:(new Date).toISOString()};writeFileSync(path.join(logDir(t),"metro.json"),JSON.stringify(o,null,2))},startMetro=async(t,e,r)=>{let o;try{o=createRequire(path.join(t,"package.json")).resolve("expo/bin/cli")}catch{return fail("toolMissing","expo is not installed in this app; run npx mobile-tools sync")}const s=startDetached({file:process.execPath,args:[o,"start","--port",String(e)]},t,r);writeMetroRecord(t,e,s),say(`metro: started on port ${e}, log ${r}`),await waitForMetro(e,t,r)},answers=async(t,e)=>{try{return await fetch(`http://localhost:${t}/status`,{signal:AbortSignal.timeout(e)}),!0}catch{return!1}},systemTool=t=>path.join(process.env.SystemRoot??"C:/Windows","System32",t),isNodeProcess=t=>{try{process.kill(t,0)}catch{return!1}if("win32"===process.platform){return capture({file:systemTool("tasklist.exe"),args:["/FI",`PID eq ${t}`,"/FO","CSV","/NH"]},process.cwd()).stdout.toLowerCase().includes("node.exe")}return capture({file:"/bin/ps",args:["-p",String(t),"-o","comm="]},process.cwd()).stdout.trim().endsWith("node")},signalGroup=(t,e)=>{try{process.kill(-t,e)}catch{}},stopTree=async t=>{if("win32"===process.platform){const e=capture({file:systemTool("taskkill.exe"),args:["/PID",String(t),"/T","/F"]},process.cwd());if(0!==e.status&&isNodeProcess(t)){const r=`${e.stdout}${e.stderr}`.trim();fail("toolFailed",`taskkill could not stop pid ${t}: ${r}`)}return}signalGroup(t,"SIGTERM");const e=Date.now()+5e3;for(;isNodeProcess(t)&&Date.now()<e;)await sleep(250);isNodeProcess(t)&&signalGroup(t,"SIGKILL")},waitPortFree=async(t,e)=>{const r=Date.now()+1e4;for(;!await portIsFree(t);)Date.now()>=r&&fail("metroUnavailable",`port ${t} is still held 10 s after stopping pid ${e}; stop the holder and rerun`),await sleep(250)};export const ensureMetro=async(t,e,{fixedPort:r})=>{const o=r?8081:8100;for(let r=8081;r<=o;r++){const o=await probeMetro(r,t);if("ours"===o)return say(`metro: reusing the server on port ${r}`),r;if("other"===o){const o=readMetroRecord(t);if(o?.port===r&&isNodeProcess(o.pid)&&!await answers(r,5e3))return say(`metro: the server on port ${r} (pid ${o.pid}) does not answer; restarting it, log ${e}`),await stopTree(o.pid),await waitPortFree(r,o.pid),await startMetro(t,r,e),r}if("free"===o)return await startMetro(t,r,e),r}return fail("metroUnavailable",r?"port 8081 is held by another server (the iOS app always looks there); stop it and rerun":"ports 8081-8100 are all held by other servers; stop one and rerun")};export const restartMetro=async t=>{const e=path.join(logDir(t),"metro.log"),r=readMetroRecord(t);if(r&&isNodeProcess(r.pid))return await stopTree(r.pid),say(`metro: stopped pid ${r.pid} on port ${r.port}`),await waitPortFree(r.port,r.pid),await startMetro(t,r.port,e),say(`metro: ready on port ${r.port}`),r.port;const o=r?.port??8081,s=await probeMetro(o,t);return"ours"===s?fail("metroUnavailable",`the Metro on port ${o} was not started by mobile-tools; stop it in its own terminal (Ctrl+C), then run mobile-tools metro restart`):"other"===s?fail("metroUnavailable",`port ${o} is held by another server; stop it and rerun`):(await startMetro(t,o,e),say(`metro: ready on port ${o}`),o)};
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)};
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);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 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)=>{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);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};
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,loadAppSettings,loadToolsConfig,localApiPort,resolveIosProjectName,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{describeAppleDevice,listAppleDevices,xcodebuildArgs}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCODEBUILD,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";export const readNamespace=i=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(i)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(i,e)=>{const a=i.filter(i=>i.endsWith(".apk")).sort();return a.find(i=>i.includes(e))??a.find(i=>i.includes("universal"))??a[0]};const adbOrFail=(i,e,a)=>{const o=capture({file:i,args:e},a);return 0!==o.status?fail("toolFailed",`adb ${e.join(" ")} failed: ${(o.stderr||o.stdout).trim()}`):o.stdout},elapsedSeconds=i=>((Date.now()-i)/1e3).toFixed(1),cachedBuild=(i,e,a)=>{const o=i.fingerprint.slice(0,12),r=e?void 0:findCachedBuild(i);if(r)return say(`cache: hit ${o} (fingerprint ${a} s), installing the cached build`),r;say(e?"cache: skipped (--rebuild), building":`cache: miss ${o} (fingerprint ${a} s), building`)},runAndroid=async i=>{const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=path.join(e,"android"),t=path.join(r,"app","build.gradle");existsSync(t)||fail("appConfigInvalid",`android/app/build.gradle not found in ${e}`);const s=readNamespace(readFileSync(t,"utf8"))??fail("appConfigInvalid","android/app/build.gradle declares no namespace"),n=adbPath(process.env,process.platform);await connectLocalEmulators(n,e);const d=listAndroidDevices(n,e),l=await pickDevice(d,i.device,describeDevice,!0===process.stdin.isTTY);ABI_PATTERN.test(l.abi)||fail("deviceNotFound",`${describeDevice(l)} reports an ABI this build cannot target: "${l.abi}"`),say(`run: ${describeDevice(l)}`);const c=prebuild({appRoot:e,env:a,platform:"android"}),p=Date.now(),u=await nativeFingerprint(e,"android"),f={appId:baseAppId(e),platform:"android",variant:variantName(a,o.flavors,"debug"),abi:l.abi,fingerprint:u};let m=cachedBuild(f,i.rebuild,elapsedSeconds(p));if(void 0===m){const i=gradleTask(a,o.flavors,"apk","debug"),t=logDir(e);await buildOrFail(gradleInvocation(r,[`:app:${i}`,`-PreactNativeArchitectures=${l.abi}`],process.platform,process.env),r,path.join(t,`gradle-${a}-${logStamp()}.log`),`gradle ${i}`);const s=gradleOutputDir(e,a,o.flavors,"apk","debug"),n=existsSync(s)?chooseApk(readdirSync(s),l.abi):void 0;n||fail("artifactMissing",`the build produced no APK in ${s}`),m=path.join(s,n),storeCachedBuild(f,m),say(`cache: stored ${u.slice(0,12)}`)}say(`run: installing ${path.basename(m)} on ${l.serial}`),adbOrFail(n,["-s",l.serial,"install","-r",m],e);const v=await ensureMetro(e,path.join(logDir(e),"metro.log"),{fixedPort:!1});if(adbOrFail(n,["-s",l.serial,"reverse","tcp:8081",`tcp:${v}`],e),"Development"===a){const i=localApiPort(String(loadAppSettings(e,"Development").BASE_URL),o);void 0!==i&&(adbOrFail(n,["-s",l.serial,"reverse",`tcp:${i}`,`tcp:${i}`],e),say(`run: device localhost:${i} reaches this machine's API`))}const g=applicationId(c.APP_ID,a);adbOrFail(n,["-s",l.serial,"shell","am","start","-n",`${g}/${s}.MainActivity`],e),say(`run: launched ${g} on ${l.model||l.serial}`)},runIos=async i=>{requireDarwin(process.platform,"run --platform ios");const{appRoot:e,env:a}=i,o=loadToolsConfig(e),r=resolveIosProjectName(e,o)??fail("appConfigInvalid",`no ios/ folder in ${e}`),t=path.join(e,"ios",`${r}.xcworkspace`);existsSync(t)||fail("appConfigInvalid",`ios/${r}.xcworkspace not found: run npx mobile-tools sync (it runs pod install)`);const s=await pickDevice(listAppleDevices(e),i.device,describeAppleDevice,!0===process.stdin.isTTY);say(`run: ${describeAppleDevice(s)}`);const n=prebuild({appRoot:e,env:a,platform:"ios"}),d=logDir(e);let l,c;if("simulator"===s.kind){const a=Date.now(),o=await nativeFingerprint(e,"ios");l={appId:baseAppId(e),platform:"ios",variant:"Debug-iphonesimulator",abi:"simulator",fingerprint:o},c=cachedBuild(l,i.rebuild,elapsedSeconds(a))}else say("cache: not used for a physical iPhone");if(void 0===c){await buildOrFail({file:XCODEBUILD,args:xcodebuildArgs(r,s.udid)},e,path.join(d,`xcodebuild-${a}-${logStamp()}.log`),"xcodebuild");const i=path.join(e,"ios","build","Build","Products","simulator"===s.kind?"Debug-iphonesimulator":"Debug-iphoneos"),o=existsSync(i)?readdirSync(i).filter(i=>i.endsWith(".app")):[],t=o.find(i=>i===`${r}.app`)??o[0];t||fail("artifactMissing",`the build produced no .app in ${i}`),c=path.join(i,t),l&&(storeCachedBuild(l,c),say(`cache: stored ${l.fingerprint.slice(0,12)}`))}const p=applicationId(n.APP_ID,a);await ensureMetro(e,path.join(d,"metro.log"),{fixedPort:!0});const u="simulator"===s.kind?[["simctl","install",s.udid,c],["simctl","launch",s.udid,p]]:[["devicectl","device","install","app","--device",s.udid,c],["devicectl","device","process","launch","--device",s.udid,p]];for(const i of u){const a=capture({file:XCRUN,args:i},e);0!==a.status&&fail("toolFailed",`xcrun ${i.slice(0,3).join(" ")} failed: ${(a.stderr||a.stdout).trim()}`)}say(`run: launched ${p} on ${s.name}`)};export const run=async i=>{requireSynced(i.appRoot),"ios"===i.platform?await runIos(i):await runAndroid(i)};
1
+ import{existsSync,readdirSync,readFileSync}from"node:fs";import path from"node:path";import{buildOrFail,logDir,logStamp}from"./buildLog.js";import{findCachedBuild,nativeFingerprint,storeCachedBuild}from"./cache.js";import{applicationId,baseAppId,gradleOutputDir,gradleTask,loadAppSettings,loadToolsConfig,localApiPort,resolveIosProjectName,variantName}from"./config.js";import{connectLocalEmulators,describeDevice,listAndroidDevices,pickDevice}from"./devices.js";import{capture}from"./exec.js";import{describeAppleDevice,listAppleDevices,xcodebuildArgs}from"./ios.js";import{ensureMetro}from"./metro.js";import{fail,say}from"./output.js";import{adbPath,gradleInvocation,requireDarwin,XCODEBUILD,XCRUN}from"./platform.js";import{prebuild}from"./prebuild.js";import{requireSynced}from"./sync.js";export const readNamespace=i=>/^\s*namespace\s*=?\s*["']([^"']+)["']/m.exec(i)?.[1];export const ABI_PATTERN=/^(arm64-v8a|armeabi-v7a|x86_64|x86)$/;export const chooseApk=(i,e)=>{const a=i.filter(i=>i.endsWith(".apk")).sort();return a.find(i=>i.includes(e))??a.find(i=>i.includes("universal"))??a[0]};const adbOrFail=(i,e,a)=>{const o=capture({file:i,args:e},a);return 0!==o.status?fail("toolFailed",`adb ${e.join(" ")} failed: ${(o.stderr||o.stdout).trim()}`):o.stdout};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`)},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(loadAppSettings(e,"Development").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 g=applicationId(c.APP_ID,a);adbOrFail(n,["-s",d.serial,"shell","am","start","-n",`${g}/${s}.MainActivity`],e),say(`run: launched ${g} 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)};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-tools",
3
- "version": "0.109.3",
3
+ "version": "0.109.4",
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",