@webority/mobile-tools 0.6.0 → 0.7.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/lib/icons.js CHANGED
@@ -1 +1 @@
1
- import{existsSync,mkdirSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{ENVIRONMENTS,flavorOf,loadToolsConfig,resolveIosProjectName}from"./config.js";import{ADAPTIVE_DP,ADAPTIVE_ICON_XML,ADAPTIVE_VISIBLE_DP,backgroundColorXml,circleMaskSvg,DENSITIES,IOS_ICON_FILE,iosContentsJson,LEGACY_DP,RIBBONS,ribbonSvg,toHex}from"./iconArt.js";import{fail,say}from"./output.js";const MASTER=1024,PNG={compressionLevel:9,adaptiveFiltering:!1,palette:!1,force:!0},ICON_NAMES=["ic_launcher","ic_launcher_round","ic_launcher_foreground"],removeOtherFormats=o=>{if(existsSync(o))for(const r of readdirSync(o)){const{name:e,ext:n}=path.parse(r);ICON_NAMES.includes(e)&&".png"!==n&&rmSync(path.join(o,r))}},write=(o,r)=>{mkdirSync(path.dirname(o),{recursive:!0}),writeFileSync(o,r)},assertNoColorClash=(o,r)=>{if(existsSync(o))for(const e of readdirSync(o))e!==r&&e.endsWith(".xml")&&readFileSync(path.join(o,e),"utf8").includes('name="ic_launcher_background"')&&fail(`${path.join(o,e)} already defines ic_launcher_background; remove it there`)},loadMaster=async(o,r)=>{const e=await o(r).metadata();if("png"!==e.format&&"svg"!==e.format)return fail(`icon source must be a PNG or an SVG, not ${e.format??"unknown"}: ${r}`);const n=e.width??0,t=e.height??0;if(n!==t)return fail(`icon source must be square, ${r} is ${n}x${t}`);if("png"===e.format&&n<1024)return fail(`a PNG icon source must be at least 1024x1024, ${r} is ${n}x${t}`);return o(r,"svg"===e.format?{density:73728/n}:{}).resize(1024,1024,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).ensureAlpha().png(PNG).toBuffer()},cornerColor=async(o,r)=>{const{data:e}=await o(r).raw().toBuffer({resolveWithObject:!0});return 0===e[3]?"#FFFFFF":toHex(e[0],e[1],e[2])},withRibbon=async(o,r,e,n)=>{const t=RIBBONS[e];return t?o(r).composite([{input:Buffer.from(ribbonSvg(1024,n,t))}]).png(PNG).toBuffer():r},renderAndroid=async(o,r,e,n,t)=>{const a=path.join(r,"android","app","src",flavorOf(e),"res"),i=await withRibbon(o,n,e,{x:0,y:0,size:1024}),s=Math.round(1024*ADAPTIVE_VISIBLE_DP/ADAPTIVE_DP),c=Math.round((1024-s)/2),f=await o(n).resize(s,s).png(PNG).toBuffer(),p=await o({create:{width:1024,height:1024,channels:4,background:{r:0,g:0,b:0,alpha:0}}}).composite([{input:f,left:c,top:c}]).png(PNG).toBuffer(),l=await withRibbon(o,p,e,{x:c,y:c,size:s});for(const[r,e]of DENSITIES){const n=path.join(a,`mipmap-${r}`);removeOtherFormats(n);const t=Math.round(LEGACY_DP*e),s=Math.round(ADAPTIVE_DP*e);write(path.join(n,"ic_launcher.png"),await o(i).resize(t,t).png(PNG).toBuffer()),write(path.join(n,"ic_launcher_round.png"),await o(await o(i).resize(t,t).png(PNG).toBuffer()).composite([{input:Buffer.from(circleMaskSvg(t)),blend:"dest-in"}]).png(PNG).toBuffer()),write(path.join(n,"ic_launcher_foreground.png"),await o(l).resize(s,s).png(PNG).toBuffer())}const u=path.join(a,"mipmap-anydpi-v26");removeOtherFormats(u),write(path.join(u,"ic_launcher.xml"),ADAPTIVE_ICON_XML),write(path.join(u,"ic_launcher_round.xml"),ADAPTIVE_ICON_XML);const d=path.join(a,"values");assertNoColorClash(d,"ic_launcher_background.xml"),write(path.join(d,"ic_launcher_background.xml"),backgroundColorXml(t)),say(`icons: ${path.relative(r,a)}`)},renderIos=async(o,r,e,n,t,a)=>{const i=path.join(r,"ios",e,"Images.xcassets");existsSync(i)||fail(`${path.relative(r,i)} not found: check "iosProjectName"`);const s=path.join(i,`AppIcon-${n}.appiconset`);rmSync(s,{recursive:!0,force:!0});const c=await withRibbon(o,t,n,{x:0,y:0,size:1024});write(path.join(s,IOS_ICON_FILE),await o(c).flatten({background:a}).png(PNG).toBuffer()),write(path.join(s,"Contents.json"),iosContentsJson()),say(`icons: ${path.relative(r,s)}`)};export const icons=async o=>{const{appRoot:r}=o,e=loadToolsConfig(r),n=o.source??e.iconSource??fail('no icon source: pass --source <png|svg> or set "iconSource" in mobile-tools.config.json',2),t=path.resolve(r,n);existsSync(t)||fail(`icon source not found: ${t}`);const{default:a}=await(import("sharp")),i=await loadMaster(a,t),s=await cornerColor(a,i),c=resolveIosProjectName(r,e),f=existsSync(path.join(r,"android"));f&&!e.flavors&&say('icons: Android skipped, "flavors" is false: per-environment icons live in flavor source sets');for(const o of ENVIRONMENTS)f&&e.flavors&&await renderAndroid(a,r,o,i,s),c&&await renderIos(a,r,c,o,i,s)};
1
+ import{existsSync,mkdirSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import path from"node:path";import{ENVIRONMENTS,flavorOf,loadToolsConfig,resolveIosProjectName}from"./config.js";import{ADAPTIVE_DP,ADAPTIVE_ICON_XML,ADAPTIVE_VISIBLE_DP,backgroundColorXml,circleMaskSvg,DENSITIES,IOS_ICON_FILE,iosContentsJson,LEGACY_DP,RIBBONS,ribbonSvg,toHex}from"./iconArt.js";import{fail,say}from"./output.js";const MASTER=1024,PNG={compressionLevel:9,adaptiveFiltering:!1,palette:!1,force:!0},ICON_NAMES=["ic_launcher","ic_launcher_round","ic_launcher_foreground"],removeOtherFormats=o=>{if(existsSync(o))for(const n of readdirSync(o)){const{name:e,ext:r}=path.parse(n);ICON_NAMES.includes(e)&&".png"!==r&&rmSync(path.join(o,n))}},write=(o,n)=>{mkdirSync(path.dirname(o),{recursive:!0}),writeFileSync(o,n)},assertNoColorClash=(o,n)=>{if(existsSync(o))for(const e of readdirSync(o))e!==n&&e.endsWith(".xml")&&readFileSync(path.join(o,e),"utf8").includes('name="ic_launcher_background"')&&fail(`${path.join(o,e)} already defines ic_launcher_background; remove it there`)},loadMaster=async(o,n)=>{const e=await o(n).metadata();if("png"!==e.format&&"svg"!==e.format)return fail(`icon source must be a PNG or an SVG, not ${e.format??"unknown"}: ${n}`);const r=e.width??0,t=e.height??0;if(r!==t)return fail(`icon source must be square, ${n} is ${r}x${t}`);if("png"===e.format&&r<1024)return fail(`a PNG icon source must be at least 1024x1024, ${n} is ${r}x${t}`);return o(n,"svg"===e.format?{density:73728/r}:{}).resize(1024,1024,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).ensureAlpha().png(PNG).toBuffer()},cornerColor=async(o,n)=>{const{data:e}=await o(n).raw().toBuffer({resolveWithObject:!0});return 0===e[3]?"#FFFFFF":toHex(e[0],e[1],e[2])},withRibbon=async(o,n,e,r)=>{const t=RIBBONS[e];return t?o(n).composite([{input:Buffer.from(ribbonSvg(1024,r,t))}]).png(PNG).toBuffer():n},renderAndroid=async(o,n,e,r,t)=>{const a=path.join(n,"android","app","src",flavorOf(e),"res"),i=await withRibbon(o,r,e,{x:0,y:0,size:1024}),s=Math.round(1024*ADAPTIVE_VISIBLE_DP/ADAPTIVE_DP),c=Math.round((1024-s)/2),p=await o(r).resize(s,s).png(PNG).toBuffer(),f=await o({create:{width:1024,height:1024,channels:4,background:{r:0,g:0,b:0,alpha:0}}}).composite([{input:p,left:c,top:c}]).png(PNG).toBuffer(),l=await withRibbon(o,f,e,{x:c,y:c,size:s});for(const[n,e]of DENSITIES){const r=path.join(a,`mipmap-${n}`);removeOtherFormats(r);const t=Math.round(LEGACY_DP*e),s=Math.round(ADAPTIVE_DP*e);write(path.join(r,"ic_launcher.png"),await o(i).resize(t,t).png(PNG).toBuffer()),write(path.join(r,"ic_launcher_round.png"),await o(await o(i).resize(t,t).png(PNG).toBuffer()).composite([{input:Buffer.from(circleMaskSvg(t)),blend:"dest-in"}]).png(PNG).toBuffer()),write(path.join(r,"ic_launcher_foreground.png"),await o(l).resize(s,s).png(PNG).toBuffer())}const u=path.join(a,"mipmap-anydpi-v26");removeOtherFormats(u),write(path.join(u,"ic_launcher.xml"),ADAPTIVE_ICON_XML),write(path.join(u,"ic_launcher_round.xml"),ADAPTIVE_ICON_XML);const h=path.join(a,"values");assertNoColorClash(h,"ic_launcher_background.xml"),write(path.join(h,"ic_launcher_background.xml"),backgroundColorXml(t)),say(`icons: ${path.relative(n,a)}`)},renderIos=async(o,n,e,r,t,a)=>{const i=path.join(n,"ios",e,"Images.xcassets");existsSync(i)||fail(`${path.relative(n,i)} not found: check "iosProjectName"`);const s=path.join(i,`AppIcon-${r}.appiconset`);rmSync(s,{recursive:!0,force:!0});const c=await withRibbon(o,t,r,{x:0,y:0,size:1024});write(path.join(s,IOS_ICON_FILE),await o(c).flatten({background:a}).png(PNG).toBuffer()),write(path.join(s,"Contents.json"),iosContentsJson()),say(`icons: ${path.relative(n,s)}`)},appEnvironments=o=>{const n=ENVIRONMENTS.filter(n=>existsSync(path.join(o,`appsettings.${n}.json`)));return 0===n.length&&fail(`no appsettings.<Env>.json in ${o} (${ENVIRONMENTS.join(", ")}): run mobile-tools from the app's root folder`),n};export const icons=async o=>{const{appRoot:n}=o,e=appEnvironments(n),r=loadToolsConfig(n),t=o.source??r.iconSource??fail('no icon source: pass --source <png|svg> or set "iconSource" in mobile-tools.config.json',2),a=path.resolve(n,t);existsSync(a)||fail(`icon source not found: ${a}`);const{default:i}=await(import("sharp")),s=await loadMaster(i,a),c=await cornerColor(i,s),p=resolveIosProjectName(n,r),f=existsSync(path.join(n,"android"));f&&!r.flavors&&say('icons: Android skipped, "flavors" is false: per-environment icons live in flavor source sets');for(const o of e)f&&r.flavors&&await renderAndroid(i,n,o,s,c),p&&await renderIos(i,n,p,o,s,c)};
package/lib/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),i=e.split(o),n=new Map(Object.entries(t)),r=i.map(e=>{const t=/^\s*([A-Za-z0-9_.-]+)\s*[=:]/.exec(e);if(t&&n.has(t[1])){const e=t[1],o=n.get(e);return n.delete(e),`${e}=${o}`}return e});if(0===n.size)return r.join(o);const s=[...n].map(([e,t])=>`${e}=${t}`);return r.length>1&&""===r[r.length-1]?r.splice(r.length-1,0,...s):1===r.length&&""===r[0]?r.splice(0,1,...s):r.push(...s),r.join(o)};const pbxValue=e=>/^[A-Za-z0-9._/-]+$/.test(e)?e:`"${e.replace(/"/g,'\\"')}"`;export const rewritePbxproj=(e,t,o={})=>{let i=e;for(const[e,n]of Object.entries(t)){const t=new RegExp(`(\\s)${e} = [^;\\n]*;`,"g");t.test(i)||fail(`${e} not found in project.pbxproj`);const r=o.quote?.includes(e)?`"${n.replace(/"/g,'\\"')}"`:pbxValue(n);i=i.replace(t,(t,o)=>`${o}${e} = ${r};`)}return i};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("CFBundleDisplayName not found in Info.plist"),e.replace(o,(e,o,i)=>`${o}${escapeXml(t)}${i}`)};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 i=capture({file:o,args:["rev-parse","--short","HEAD"]},e);if(0!==i.status)return"nogit";const n=capture({file:o,args:["status","--porcelain","--",".",...t.map(e=>`:(exclude)${e}`)]},e);return 0!==n.status?fail(`git status failed in ${e}: ${n.stderr.trim()}`):""===n.stdout.trim()?i.stdout.trim():`${i.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('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,i)=>{const n=path.join(e,"android","gradle.properties");existsSync(n)||fail(`android/gradle.properties not found in ${e}`),usesFirebase(e)&&!existsSync(path.join(e,"android","app","google-services.json"))&&fail("android/app/google-services.json is missing; the app uses @react-native-firebase, whose Gradle plugin needs it"),checkNdkHook(e);const r=rewriteGradleProperties(readFileSync(n,"utf8"),{APP_NAME:String(t.APP_NAME),APP_ID:gradleAppId(t.APP_ID,o,i.flavors),VERSION_DISPLAY:String(t.VERSION_DISPLAY),VERSION_INTERNAL:String(t.VERSION_INTERNAL)});report(e,n,writeIfChanged(n,r));const s=otaNativeSettings(e,t,o,"android");if(s){const t=path.join(e,ANDROID_MANIFEST);existsSync(t)||fail(`${ANDROID_MANIFEST} not found: expo-updates reads its settings from it`),report(e,t,writeIfChanged(t,rewriteAndroidManifestOta(readFileSync(t,"utf8"),s)))}};export const stampIos=(e,t,o,i,n)=>{const r=path.join(e,"ios",n),s=path.join(r,"Info.plist"),a=path.join(e,"ios",`${n}.xcodeproj`,"project.pbxproj");for(const t of[s,a])existsSync(t)||fail(`${path.relative(e,t)} not found: check "iosProjectName"`);const p=`AppIcon-${o}`,c=path.join(r,"Images.xcassets"),l=existsSync(path.join(c,`${p}.appiconset`)),d=ENVIRONMENTS.filter(e=>e!==o&&existsSync(path.join(c,`AppIcon-${e}.appiconset`)));!l&&d.length>0&&fail(`ios/${n}/Images.xcassets has AppIcon-${d[0]} but no ${p}: run mobile-tools icons`),report(e,s,writeIfChanged(s,rewriteInfoPlistDisplayName(readFileSync(s,"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)};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(r,"GoogleService-Info.plist"),I=path.join(e,i.firebasePlistDir,`GoogleService-Info.${o}.plist`);if(!existsSync(I)&&usesFirebase(e)&&fail(`${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 S=otaNativeSettings(e,t,o,"ios");if(S){const t=path.join(e,expoPlistPath(n));existsSync(t)||fail(`${expoPlistPath(n)} 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:o,platform:i}=e;o&&e.env&&fail("pass --env or --same-env, not both",2);const n=o?lastCompiledEnvironment(t):e.env;if(!n)return fail("prebuild needs --env <Development|Staging|Production> or --same-env",2);const r=loadToolsConfig(t),s=resolveIosProjectName(t,r),a=loadAppSettings(t,n),p={...a,COMMIT:buildCommit(t,prebuildOutputs(s,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} (${n}, ${p.COMMIT})`);const c=existsSync(path.join(t,"android"));if(("android"===i||!i&&c)&&stampAndroid(t,p,n,r),"ios"===i||!i&&s){if(!s)return fail(`no ios/ folder in ${t}`);stampIos(t,p,n,r,s)}return p};
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(`${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("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(`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(`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('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(`android/gradle.properties not found in ${e}`),usesFirebase(e)&&!existsSync(path.join(e,"android","app","google-services.json"))&&fail("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(`${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(`${path.relative(e,t)} not found: check "iosProjectName"`);checkInfoPlistReferences(e,i);const p=`AppIcon-${o}`,c=path.join(s,"Images.xcassets"),l=existsSync(path.join(c,`${p}.appiconset`)),d=ENVIRONMENTS.filter(e=>e!==o&&existsSync(path.join(c,`AppIcon-${e}.appiconset`)));!l&&d.length>0&&fail(`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)};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,n.firebasePlistDir,`GoogleService-Info.${o}.plist`);if(!existsSync(I)&&usesFirebase(e)&&fail(`${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(`${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("pass --env or --same-env, not both",2);const i=o?lastCompiledEnvironment(t):e.env;if(!i)return fail("prebuild needs --env <Development|Staging|Production> or --same-env",2);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 c=existsSync(path.join(t,"android"));if(("android"===n||!n&&c)&&stampAndroid(t,p,i,s),"ios"===n||!n&&r){if(!r)return fail(`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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run, per-environment launcher icons and over-the-air deploys, the same on Windows and macOS.",
5
5
  "keywords": [
6
6
  "react-native",