@webority/mobile-tools 0.98.0 → 0.99.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
@@ -23,6 +23,7 @@ npx mobile-tools prebuild --same-env [--platform android|ios]
23
23
  - Stamps `COMMIT` (the short git sha, with `-dirty` when the app folder has changes other than the files prebuild writes) and `BUILD_TIME` (UTC, ISO 8601).
24
24
  - Fails, naming the key, when the merged settings lack `APP_NAME`, `APP_ID`, `ENVIRONMENT`, `BASE_URL`, `VERSION_DISPLAY` or `VERSION_INTERNAL`.
25
25
  - Fails when `ENVIRONMENT` differs from the `--env` given, when `VERSION_INTERNAL` is not a positive whole number, or when an `APP_ID_SUFFIX` disagrees with the environment.
26
+ - Checks the app settings rules and warns about each break: a Staging or Production `BASE_URL` that is not https or points at this machine; `API_TIMEOUT`, `UPLOAD_TIMEOUT`, `MAX_RETRY_ATTEMPTS`, `LOGGING_ENABLED`, `PUSH_ENABLED`, `LOG_LEVEL` and `MIN_SUPPORTED_INTERNAL_VERSION` with the wrong type; a `MIN_SUPPORTED_INTERNAL_VERSION` above `VERSION_INTERNAL`; and a missing `PUSH_ENABLED` in an app that depends on `@react-native-firebase/messaging`. Set `MOBILE_TOOLS_STRICT_SETTINGS=1` to stop the prebuild on any of them.
26
27
  - **Android** (`android/gradle.properties`): sets `APP_NAME`, `APP_ID`, `VERSION_DISPLAY` and `VERSION_INTERNAL`. Every other line, comment and line ending stays as it was.
27
28
  - With flavors, `APP_ID` is the base id. Each flavor's `applicationIdSuffix` adds the suffix.
28
29
  - Without flavors, `APP_ID` is the suffixed id.
@@ -0,0 +1 @@
1
+ import{existsSync,readFileSync}from"node:fs";import path from"node:path";import{fail,warn}from"./output.js";export const STRICT_SETTINGS_ENV="MOBILE_TOOLS_STRICT_SETTINGS";const WHOLE_NUMBER=/^[1-9][0-9]*$/,WHOLE_NUMBER_OR_ZERO=/^[0-9]+$/,LOCAL_HOSTS=new Set(["localhost","127.0.0.1","10.0.2.2"]),isSet=t=>null!=t&&""!==t;export const appDependencies=t=>{const e=path.join(t,"package.json");if(!existsSync(e))return new Set;let s;try{s=JSON.parse(readFileSync(e,"utf8"))}catch{return fail("appConfigInvalid",`package.json in ${t} does not parse`)}return new Set([...Object.keys(s.dependencies??{}),...Object.keys(s.devDependencies??{})])};export const appSettingsProblems=(t,e,s)=>{const n=[];if("Staging"===e||"Production"===e){const s=JSON.stringify(t.BASE_URL);let i;try{i=new URL(String(t.BASE_URL))}catch{i=void 0}i?("https:"!==i.protocol&&n.push(`BASE_URL ${s} must be https in a ${e} build`),LOCAL_HOSTS.has(i.hostname)&&n.push(`BASE_URL ${s} points at this machine in a ${e} build`)):n.push(`BASE_URL ${s} is not a URL`)}for(const e of["API_TIMEOUT","UPLOAD_TIMEOUT"]){const s=t[e];isSet(s)&&!WHOLE_NUMBER.test(String(s))&&n.push(`${e} must be a positive whole number, not ${JSON.stringify(s)}`)}isSet(t.MAX_RETRY_ATTEMPTS)&&!WHOLE_NUMBER_OR_ZERO.test(String(t.MAX_RETRY_ATTEMPTS))&&n.push(`MAX_RETRY_ATTEMPTS must be a whole number of zero or more, not ${JSON.stringify(t.MAX_RETRY_ATTEMPTS)}`);for(const e of["LOGGING_ENABLED","PUSH_ENABLED"]){const s=t[e];isSet(s)&&"boolean"!=typeof s&&n.push(`${e} must be true or false, not ${JSON.stringify(s)}`)}isSet(t.LOG_LEVEL)&&"string"!=typeof t.LOG_LEVEL&&n.push(`LOG_LEVEL must be a non-empty string, not ${JSON.stringify(t.LOG_LEVEL)}`);const i=t.MIN_SUPPORTED_INTERNAL_VERSION;return isSet(i)&&(WHOLE_NUMBER.test(String(i))?Number(i)>Number(t.VERSION_INTERNAL)&&n.push(`MIN_SUPPORTED_INTERNAL_VERSION ${JSON.stringify(i)} is above VERSION_INTERNAL ${JSON.stringify(t.VERSION_INTERNAL)}, so this build would block itself`):n.push(`MIN_SUPPORTED_INTERNAL_VERSION must be a positive whole number, not ${JSON.stringify(i)}`)),s.has("@react-native-firebase/messaging")&&!isSet(t.PUSH_ENABLED)&&n.push("PUSH_ENABLED is missing: the app depends on @react-native-firebase/messaging, so it must state whether push is on"),n};export const checkAppSettings=(t,e,s)=>{const n=appSettingsProblems(e,s,appDependencies(t));if(0!==n.length){"1"===process.env[STRICT_SETTINGS_ENV]&&fail("appConfigInvalid",`appsettings (${s}) break the app settings rules: ${n.join("; ")}`);for(const t of n)warn(`prebuild: warning: ${t}`)}};
package/lib/prebuild.js CHANGED
@@ -1 +1 @@
1
- import{copyFileSync,existsSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{applicationId,COMPILED_FILE,ENVIRONMENTS,gradleAppId,lastCompiledEnvironment,loadAppSettings,loadToolsConfig,resolveIosProjectName}from"./config.js";import{capture}from"./exec.js";import{otaNativeSettings,rewriteAndroidManifestOta,rewriteExpoPlistOta}from"./otaNative.js";import{fail,say}from"./output.js";import{findOnPath}from"./platform.js";const eolOf=e=>e.includes("\r\n")?"\r\n":"\n";export const rewriteGradleProperties=(e,t)=>{const o=eolOf(e),n=e.split(o),i=new Map(Object.entries(t)),s=n.map(e=>{const t=/^\s*([A-Za-z0-9_.-]+)\s*[=:]/.exec(e);if(t&&i.has(t[1])){const e=t[1],o=i.get(e);return i.delete(e),`${e}=${o}`}return e});if(0===i.size)return s.join(o);const r=[...i].map(([e,t])=>`${e}=${t}`);return s.length>1&&""===s[s.length-1]?s.splice(s.length-1,0,...r):1===s.length&&""===s[0]?s.splice(0,1,...r):s.push(...r),s.join(o)};const pbxValue=e=>/^[A-Za-z0-9._/-]+$/.test(e)?e:`"${e.replace(/"/g,'\\"')}"`;export const rewritePbxproj=(e,t,o={})=>{let n=e;for(const[e,i]of Object.entries(t)){const t=new RegExp(`(\\s)${e} = [^;\\n]*;`,"g");t.test(n)||fail("appConfigInvalid",`${e} not found in project.pbxproj`);const s=o.quote?.includes(e)?`"${i.replace(/"/g,'\\"')}"`:pbxValue(i);n=n.replace(t,(t,o)=>`${o}${e} = ${s};`)}return n};const escapeXml=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");export const rewriteInfoPlistDisplayName=(e,t)=>{const o=/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/;return o.test(e)||fail("appConfigInvalid","CFBundleDisplayName not found in Info.plist"),e.replace(o,(e,o,n)=>`${o}${escapeXml(t)}${n}`)};const pbxObject=(e,t)=>{const o=new RegExp(`\\n\\s*${t}(?: /\\*[^\\n]*?\\*/)? = \\{`).exec(e);if(!o)return;let n=0;for(let t=o.index+o[0].length-1;t<e.length;t++)if(e.startsWith("/*",t)){if(t=e.indexOf("*/",t+2)+1,0===t)return}else if('"'===e[t])for(t++;t<e.length&&'"'!==e[t];t++)"\\"===e[t]&&t++;else if("{"===e[t])n++;else if("}"===e[t]&&0===--n)return e.slice(o.index,t+1)},unquote=e=>e.startsWith('"')&&e.endsWith('"')?e.slice(1,-1).replace(/\\"/g,'"'):e,shipsInfoPlist=e=>/^com\.apple\.product-type\.(application|.*extension)/.test(e);export const shippedInfoPlists=e=>{const t=new Map,o=/\n\s*(\w+)(?: \/\*[^\n]*?\*\/)? = \{\s*isa = PBXNativeTarget;/g;for(const[,n]of e.matchAll(o)){const o=pbxObject(e,n)??"",i=unquote(/\sproductType = ([^;\n]*);/.exec(o)?.[1]??"");if(!shipsInfoPlist(i))continue;const s=unquote(/\sname = ([^;\n]*);/.exec(o)?.[1]??n),r=/\sbuildConfigurationList = (\w+)/.exec(o)?.[1],a=r&&pbxObject(e,r)||"",p=/buildConfigurations = \(([^)]*)\)/.exec(a)?.[1]??"";for(const[o]of p.matchAll(/\w+(?= \/\*|,)/g)){const n=/\sINFOPLIST_FILE = ([^;\n]*);/.exec(pbxObject(e,o)??"")?.[1];n&&!t.has(unquote(n))&&t.set(unquote(n),s)}}return[...t].map(([e,t])=>({target:t,file:e}))};const BUILD_SETTING_KEYS=[["CFBundleIdentifier","PRODUCT_BUNDLE_IDENTIFIER"],["CFBundleShortVersionString","MARKETING_VERSION"],["CFBundleVersion","CURRENT_PROJECT_VERSION"]];export const fixedInfoPlistValues=e=>BUILD_SETTING_KEYS.flatMap(([t,o])=>{const n=new RegExp(`<key>${t}</key>\\s*(<[^>]*>)?([^<]*)`).exec(e);if(!n)return[];const i="<string>"===n[1]?n[2].trim():n[1]??n[2].trim();return i===`$(${o})`||i===`\${${o}}`?[]:[{key:t,value:i,setting:o}]});export const checkInfoPlistReferences=(e,t)=>{const o=path.join(e,"ios"),n=readFileSync(path.join(o,`${t}.xcodeproj`,"project.pbxproj"),"utf8"),i=[{target:t,file:`${t}/Info.plist`},...shippedInfoPlists(n)],s=[],r=new Set;for(const{target:t,file:n}of i){const i=n.replace(/^(\$\(SRCROOT\)|\$\{SRCROOT\}|\$\(PROJECT_DIR\))\//,"");if(/\$[({]/.test(i)){s.push(`target ${t}: INFOPLIST_FILE ${n} uses a build setting prebuild cannot resolve`);continue}const a=path.resolve(o,i);if(r.has(a))continue;r.add(a);const p=path.relative(e,a).replace(/\\/g,"/");if(existsSync(a))for(const{key:e,value:t,setting:o}of fixedInfoPlistValues(readFileSync(a,"utf8")))s.push(`${p}: ${e} is "${t}", must be $(${o})`);else s.push(`${p} not found (INFOPLIST_FILE of target ${t})`)}s.length>0&&fail("appConfigInvalid",`Info.plist values that ignore what prebuild writes into project.pbxproj:\n ${s.join("\n ")}`)};const ANDROID_MANIFEST="android/app/src/main/AndroidManifest.xml",expoPlistPath=e=>`ios/${e}/Supporting/Expo.plist`;export const prebuildOutputs=(e,t=!1)=>{const o=[COMPILED_FILE,"android/gradle.properties"];return t&&o.push(ANDROID_MANIFEST),e&&(o.push(`ios/${e}/Info.plist`,`ios/${e}.xcodeproj/project.pbxproj`,`ios/${e}/GoogleService-Info.plist`),t&&o.push(expoPlistPath(e))),o};export const buildCommit=(e,t)=>{const o=findOnPath("git",process.env,process.platform);if(!o)return"nogit";const n=capture({file:o,args:["rev-parse","--short","HEAD"]},e);if(0!==n.status)return"nogit";const i=capture({file:o,args:["status","--porcelain","--",".",...t.map(e=>`:(exclude)${e}`)]},e);return 0!==i.status?fail("toolFailed",`git status failed in ${e}: ${i.stderr.trim()}`):""===i.stdout.trim()?n.stdout.trim():`${n.stdout.trim()}-dirty`};const writeIfChanged=(e,t)=>(!existsSync(e)||readFileSync(e,"utf8")!==t)&&(writeFileSync(e,t,"utf8"),!0),report=(e,t,o)=>{say(`prebuild: ${o?"wrote":"unchanged"} ${path.relative(e,t)}`)};export const usesFirebase=e=>{const t=path.join(e,"package.json");if(!existsSync(t))return!1;const o=JSON.parse(readFileSync(t,"utf8"));return Boolean(o.dependencies?.["@react-native-firebase/app"])};export const checkNdkHook=e=>{const t=path.join(e,"android","build.gradle");if(!existsSync(t))return;const o=readFileSync(t,"utf8");/withId\(\s*["']com\.android\.library["']\s*\)/.test(o)&&/\.ndkVersion\s*=\s*rootProject\.ext\.ndkVersion/.test(o)||fail("appConfigInvalid",'android/build.gradle does not hand the app NDK to library modules; add after apply plugin "com.facebook.react.rootproject": subprojects { project -> project.plugins.withId("com.android.library") { project.android.ndkVersion = rootProject.ext.ndkVersion } }')};export const stampAndroid=(e,t,o,n)=>{const i=path.join(e,"android","gradle.properties");existsSync(i)||fail("appConfigInvalid",`android/gradle.properties not found in ${e}`),usesFirebase(e)&&!existsSync(path.join(e,"android","app","google-services.json"))&&fail("appConfigInvalid","android/app/google-services.json is missing; the app uses @react-native-firebase, whose Gradle plugin needs it"),checkNdkHook(e);const s=rewriteGradleProperties(readFileSync(i,"utf8"),{APP_NAME:String(t.APP_NAME),APP_ID:gradleAppId(t.APP_ID,o,n.flavors),VERSION_DISPLAY:String(t.VERSION_DISPLAY),VERSION_INTERNAL:String(t.VERSION_INTERNAL)});report(e,i,writeIfChanged(i,s));const r=otaNativeSettings(e,t,o,"android");if(r){const t=path.join(e,ANDROID_MANIFEST);existsSync(t)||fail("appConfigInvalid",`${ANDROID_MANIFEST} not found: expo-updates reads its settings from it`),report(e,t,writeIfChanged(t,rewriteAndroidManifestOta(readFileSync(t,"utf8"),r)))}};export const stampIos=(e,t,o,n,i)=>{const s=path.join(e,"ios",i),r=path.join(s,"Info.plist"),a=path.join(e,"ios",`${i}.xcodeproj`,"project.pbxproj");for(const t of[r,a])existsSync(t)||fail("appConfigInvalid",`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,i);const p=`AppIcon-${o}`,l=path.join(s,"Images.xcassets"),c=existsSync(path.join(l,`${p}.appiconset`)),d=ENVIRONMENTS.filter(e=>e!==o&&existsSync(path.join(l,`AppIcon-${e}.appiconset`)));!c&&d.length>0&&fail("appConfigInvalid",`ios/${i}/Images.xcassets has AppIcon-${d[0]} but no ${p}: run mobile-tools icons`),report(e,r,writeIfChanged(r,rewriteInfoPlistDisplayName(readFileSync(r,"utf8"),String(t.APP_NAME))));const f={PRODUCT_BUNDLE_IDENTIFIER:applicationId(t.APP_ID,o),MARKETING_VERSION:String(t.VERSION_DISPLAY),CURRENT_PROJECT_VERSION:String(t.VERSION_INTERNAL)};c&&(f.ASSETCATALOG_COMPILER_APPICON_NAME=p),report(e,a,writeIfChanged(a,rewritePbxproj(readFileSync(a,"utf8"),f,{quote:["PRODUCT_BUNDLE_IDENTIFIER"]}))),c||say("prebuild: no AppIcon-<Env> sets, app icon left as the project sets it");const u=path.join(s,"GoogleService-Info.plist"),I=path.join(e,n.firebasePlistDir,`GoogleService-Info.${o}.plist`);if(!existsSync(I)&&usesFirebase(e)&&fail("appConfigInvalid",`${path.relative(e,I)} is missing; the app uses @react-native-firebase, and an iOS build without it reports no crashes`),existsSync(I)){const t=!existsSync(u)||!readFileSync(I).equals(readFileSync(u));t&&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,o,"ios");if(g){const t=path.join(e,expoPlistPath(i));existsSync(t)||fail("appConfigInvalid",`${expoPlistPath(i)} not found: add it to the Xcode project for expo-updates`),report(e,t,writeIfChanged(t,rewriteExpoPlistOta(readFileSync(t,"utf8"),g)))}};export const prebuild=e=>{const{appRoot:t,sameEnv:o,platform:n}=e;o&&e.env&&fail("badInput","pass --env or --same-env, not both");const i=o?lastCompiledEnvironment(t):e.env;if(!i)return fail("badInput","prebuild needs --env <Development|Staging|Production> or --same-env");const s=loadToolsConfig(t),r=resolveIosProjectName(t,s),a=loadAppSettings(t,i),p={...a,COMMIT:buildCommit(t,prebuildOutputs(r,Boolean(a.OTA_UPDATE_BASE_URL))),BUILD_TIME:(new Date).toISOString()};writeFileSync(path.join(t,COMPILED_FILE),`${JSON.stringify(p,null,2)}\n`,"utf8"),say(`prebuild: wrote ${COMPILED_FILE} (${i}, ${p.COMMIT})`);const l=existsSync(path.join(t,"android"));if(("android"===n||!n&&l)&&stampAndroid(t,p,i,s),"ios"===n||!n&&r){if(!r)return fail("appConfigInvalid",`no ios/ folder in ${t}`);stampIos(t,p,i,s,r)}return p};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-tools",
3
- "version": "0.98.0",
3
+ "version": "0.99.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",