catalyst-core 0.1.0-beta.4 β†’ 0.1.0-beta.6

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.
@@ -1,304 +1,2 @@
1
- "use strict";const{execFile,execSync,execFileSync}=require("child_process");const fs=require("fs");const path=require("path");const TerminalProgress=require("./TerminalProgress.js").default;const crypto=require("crypto");const{runInteractiveCommand}=require("./utils.js");const{composeIosPlugins}=require("./pluginComposerIos.js");const{resolveInternalPluginsRoot,resolvePluginConfig}=require("./internalPluginUtils.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);// Configuration constants
2
- const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const MANAGED_BASELINE_SUFFIX=".catalyst-base";const PLUGIN_RESOURCE_ROOT="PluginResources";function packageRequirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function mergePackageDependency(map,dependency,sourceLabel){const existing=map.get(dependency.url);if(!existing){map.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...new Set(dependency.products)]});return;}if(packageRequirementKey(existing)!==packageRequirementKey(dependency)){throw new Error(`iOS package dependency conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.requirement.type}:${existing.requirement.version}' vs '${dependency.requirement.type}:${dependency.requirement.version}'`);}if(existing.package!==dependency.package){throw new Error(`iOS package identity conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.package}' vs '${dependency.package}'`);}existing.products=[...new Set([...existing.products,...dependency.products])].sort();}function formatSwiftPackageRequirement(dependency){if(dependency.requirement.type==="from"){return`from: "${dependency.requirement.version}"`;}if(dependency.requirement.type==="exact"){return`exact: "${dependency.requirement.version}"`;}throw new Error(`Unsupported iOS package requirement type: ${dependency.requirement.type}`);}function formatSwiftPackageEntries(dependencies){return dependencies.map(dependency=>` .package(url: "${dependency.url}", ${formatSwiftPackageRequirement(dependency)})`);}function formatSwiftProductEntries(dependencies){return dependencies.flatMap(dependency=>dependency.products.map(product=>` .product(name: "${product}", package: "${dependency.package}")`));}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing iOS build metadata`);}function ensureManagedBaseline(filePath){const baselinePath=`${filePath}${MANAGED_BASELINE_SUFFIX}`;if(!fs.existsSync(baselinePath)){if(!fs.existsSync(filePath)){throw new Error(`Managed baseline source file not found: ${filePath}`);}fs.copyFileSync(filePath,baselinePath);}return baselinePath;}function restoreManagedFileFromBaseline(filePath){const baselinePath=ensureManagedBaseline(filePath);fs.copyFileSync(baselinePath,filePath);}function readPlistObject(filePath){const output=execFileSync("plutil",["-convert","json","-o","-",filePath],{encoding:"utf8"});return JSON.parse(output);}function writePlistObject(filePath,value){const tempPath=path.join(PROJECT_DIR,`.catalyst-${path.basename(filePath)}-${process.pid}.json`);fs.writeFileSync(tempPath,JSON.stringify(value,null,2),"utf8");try{execFileSync("plutil",["-convert","xml1","-o",filePath,tempPath]);}finally{fs.rmSync(tempPath,{force:true});}}function mergeIntoTopLevelObject(target,source,fieldName){for(const[key,value]of Object.entries(source||{})){target[key]=mergeStructuredValues(target[key],value,`${fieldName}.${key}`);}}function mergeUrlSchemes(plistObject,entries){const existing=Array.isArray(plistObject.CFBundleURLTypes)?plistObject.CFBundleURLTypes:[];const merged=existing.map(entry=>({...entry,CFBundleURLSchemes:Array.isArray(entry?.CFBundleURLSchemes)?[...new Set(entry.CFBundleURLSchemes)]:[]}));for(const entry of entries){const targetName=entry.name||null;let match=merged.find(item=>item.CFBundleURLName===targetName);if(!match){match={CFBundleURLName:targetName,CFBundleURLSchemes:[]};merged.push(match);}match.CFBundleURLSchemes=[...new Set([...match.CFBundleURLSchemes,...entry.schemes])];}if(merged.length>0){plistObject.CFBundleURLTypes=merged;}}function mergeQuerySchemes(plistObject,schemes){const existing=Array.isArray(plistObject.LSApplicationQueriesSchemes)?plistObject.LSApplicationQueriesSchemes:[];const merged=[...new Set([...existing,...schemes.filter(Boolean)])];if(merged.length>0){plistObject.LSApplicationQueriesSchemes=merged;}}function formatPbxprojPath(value){return /[^A-Za-z0-9_./-]/.test(value)?`"${value}"`:value;}function detectPbxprojFileType(filePath){const extension=path.extname(filePath).toLowerCase();const fileTypes={".json":"text.json",".plist":"text.plist.xml",".txt":"text",".html":"text.html",".js":"sourcecode.javascript",".css":"text.css",".png":"image.png",".jpg":"image.jpeg",".jpeg":"image.jpeg",".gif":"image.gif",".svg":"image.svg",".mp3":"audio.mp3",".wav":"audio.wav",".m4a":"audio.m4a",".caf":"audio.caf",".pdf":"image.pdf"};return fileTypes[extension]||"file";}function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}function replaceRequired(content,pattern,replacement,errorMessage){if(!pattern.test(content)){throw new Error(errorMessage);}return content.replace(pattern,replacement);}// Define build steps for progress tracking
3
- const steps={config:"Generating Required Configuration for build",deviceDetection:"Detecting Physical Device",launchSimulator:"Launch iOS Simulator",clean:"Clean Build Artifacts",assets:"Process Notification Assets",build:"Build IOS Project",findApp:"Locate Built Application",install:"Install Application",launch:"Launch Application"};// Configure progress display
4
- const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);const shellCommand=process.platform==="win32"?"cmd.exe":"sh";const shellArgs=command=>process.platform==="win32"?["/d","/s","/c",command]:["-c",command];// Utility function to run shell commands
5
- function runCommand(command,options={}){return new Promise((resolve,reject)=>{execFile(shellCommand,shellArgs(command),{maxBuffer:1024*1024*10,...options},(error,stdout,stderr)=>{if(error){console.error(`Command failed: ${command}`);console.error(`Error: ${error.message}`);console.error(`stderr: ${stderr}`);reject(error);return;}if(stderr){console.warn(`Warning: ${stderr}`);}resolve(stdout.trim());});});}async function getBootedSimulatorUUID(modelName){const UUID_RE=/[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/i;try{const output=await runInteractiveCommand("xcrun",["simctl","list","devices"],{});const lines=output.split("\n");// First pass: find a booted simulator matching modelName
6
- for(const line of lines){if(line.includes(modelName)&&line.includes("Booted")){const m=line.match(UUID_RE);if(m){console.log(`Found booted simulator of model ${modelName}`);return m[0];}}}// Second pass: any booted simulator
7
- console.log(`No booted simulator of model ${modelName} found, checking for any booted simulator...`);for(const line of lines){if(line.includes("Booted")){const m=line.match(UUID_RE);if(m){console.log("Found another booted simulator, will use it instead");return m[0];}}}return null;}catch(error){console.log("No booted simulators found");return null;}}async function getBootedSimulatorInfo(){try{const listCommand="xcrun simctl list devices --json";const simulatorList=JSON.parse(execSync(listCommand).toString());for(const runtime in simulatorList.devices){const devices=simulatorList.devices[runtime];for(const device of devices){if(device.state==="Booted"){return{udid:device.udid,name:device.name,runtime:runtime};}}}return null;}catch(error){console.log("Failed to get booted simulator info:",error.message);return null;}}async function generatePackageSwift(pluginDependencies=[]){try{const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;const baseCoreDependencies=[{url:"https://github.com/kylef/JSONSchema.swift",package:"JSONSchema.swift",requirement:{type:"from",version:"0.6.0"},products:["JSONSchema"]},{url:"https://github.com/google/GoogleSignIn-iOS",package:"GoogleSignIn-iOS",requirement:{type:"from",version:"7.0.0"},products:["GoogleSignIn"]}];const notificationsDependencies=isNotificationsEnabled?[{url:"https://github.com/firebase/firebase-ios-sdk",package:"firebase-ios-sdk",requirement:{type:"from",version:"12.3.0"},products:["FirebaseCore","FirebaseMessaging"]}]:[];const coreDependencyMap=new Map();baseCoreDependencies.forEach(dependency=>mergePackageDependency(coreDependencyMap,dependency,"base core"));pluginDependencies.forEach(dependency=>mergePackageDependency(coreDependencyMap,dependency,"plugin manifests"));const coreDependencies=[...coreDependencyMap.values()].sort((left,right)=>left.url.localeCompare(right.url));const packageDependencyMap=new Map();coreDependencies.forEach(dependency=>mergePackageDependency(packageDependencyMap,dependency,"core target"));notificationsDependencies.forEach(dependency=>mergePackageDependency(packageDependencyMap,dependency,"notifications target"));const packageDependencies=[...packageDependencyMap.values()].sort((left,right)=>left.url.localeCompare(right.url));progress.log(`πŸ”§ Generating Package.swift (notifications: ${isNotificationsEnabled})`,"info");// Create hash of current config to detect changes
8
- const configHash=crypto.createHash("md5").update(JSON.stringify({notifications:isNotificationsEnabled,googleSignIn:isGoogleSignInEnabled,pluginDependencies:coreDependencies})).digest("hex");const hashFilePath=path.join(PROJECT_DIR,".package-config-hash");const targetPath=path.join(PROJECT_DIR,"Package.swift");let shouldUpdate=true;// Check if we need to update based on config change
9
- if(fs.existsSync(hashFilePath)){const previousHash=fs.readFileSync(hashFilePath,"utf8");if(previousHash===configHash){shouldUpdate=false;progress.log("Package.swift already up to date","info");}}// Ensure Package.swift exists even if hash matches
10
- if(!fs.existsSync(targetPath)){shouldUpdate=true;progress.log("Package.swift missing, will generate it now","info");}if(shouldUpdate){progress.log("Generating Package.swift dynamically","info");// Build the Package.swift content dynamically
11
- let packageContent=`// swift-tools-version: 5.9
12
- // Auto-generated Package.swift - DO NOT EDIT MANUALLY
13
- // Generated based on config: notifications.enabled = ${isNotificationsEnabled}
14
- import PackageDescription
15
-
16
- let package = Package(
17
- name: "iosnativeWebView",
18
- platforms: [.iOS(.v17)],
19
- products: [
20
- .library(name: "CatalystCore", targets: ["CatalystCore"])`;// Add CatalystNotifications product only if enabled
21
- if(isNotificationsEnabled){packageContent+=`,
22
- .library(name: "CatalystNotifications", targets: ["CatalystNotifications"])`;}packageContent+=`
23
- ],
24
- dependencies: [
25
- ${formatSwiftPackageEntries(packageDependencies).join(",\n")}`;packageContent+=`
26
- ],
27
- targets: [
28
- // Core functionality (WebView, bridge, utils, constants)
29
- // App-level files (AppDelegate, ContentView) are in iosnativeWebView/ directory
30
- .target(
31
- name: "CatalystCore",
32
- dependencies: [
33
- ${formatSwiftProductEntries(coreDependencies).join(",\n")}
34
- ],
35
- path: "Sources/Core"
36
- )`;// Add CatalystNotifications target only if enabled
37
- if(isNotificationsEnabled){packageContent+=`,
38
- // Notifications functionality (optional, includes Firebase)
39
- .target(
40
- name: "CatalystNotifications",
41
- dependencies: [
42
- "CatalystCore",
43
- ${formatSwiftProductEntries(notificationsDependencies).join(",\n")}
44
- ],
45
- path: "Sources/CatalystNotifications"
46
- )`;}packageContent+=`
47
- ]
48
- )
49
- `;// Write the generated Package.swift
50
- fs.writeFileSync(targetPath,packageContent,"utf8");progress.log(`Generated Package.swift with ${isNotificationsEnabled?"notifications enabled":"notifications disabled"}`,"success");// Save hash for future comparison
51
- fs.writeFileSync(hashFilePath,configHash);// Force Xcode to re-resolve packages (clean caches to avoid stale pins)
52
- progress.log("Resolving package dependencies...","info");try{// Clear SPM build and resolution caches
53
- execSync(`cd "${PROJECT_DIR}" && rm -rf .build`,{stdio:"ignore"});try{fs.rmSync(path.join(PROJECT_DIR,"Package.resolved"),{force:true});}catch{progress.log("");}execSync(`cd "${PROJECT_DIR}" && rm -rf .swiftpm`,{stdio:"ignore"});// Resolve using -project flag to ensure correct project context
54
- const projectPath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`);execSync(`cd "${PROJECT_DIR}" && xcodebuild -resolvePackageDependencies -project "${projectPath}" -scheme "${SCHEME_NAME}"`,{stdio:"inherit"});progress.log("Package dependencies resolved successfully","success");}catch(error){// Critical error for notifications-enabled builds
55
- if(isNotificationsEnabled){progress.log(`❌ CRITICAL: Package resolution failed. Firebase dependencies required for notifications could not be resolved.`,"error");throw new Error(`Package resolution failed: ${error.message}. This is required when notifications are enabled.`);}else{progress.log(`Warning: Package resolution may have failed: ${error.message}`,"warning");}}}progress.log("βœ… Package.swift ready","success");}catch(error){progress.log(`❌ Failed to generate Package.swift: ${error.message}`,"error");throw error;}}async function updateXcodeProjectPackageDependencies(){try{const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;const projectFilePath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");progress.log(`πŸ”§ Updating Xcode package dependencies (notifications: ${isNotificationsEnabled})`,"info");if(!fs.existsSync(projectFilePath)){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}let projectContent=fs.readFileSync(projectFilePath,"utf8");// Use deterministic IDs for CatalystNotifications entries
56
- const NOTIF_BUILD_FILE_ID="C99974352E97D56900C25611";const NOTIF_PRODUCT_ID="C99974362E97D56900C25611";// Check if CatalystNotifications is already in the project
57
- const hasNotifications=projectContent.includes("/* CatalystNotifications */");if(isNotificationsEnabled&&!hasNotifications){progress.log("Adding CatalystNotifications to Xcode project","info");// 1. Add to PBXBuildFile section (after CatalystCore)
58
- projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/ = {isa = PBXBuildFile; productRef = C99974332E97D56900C25611 \/\* CatalystCore \*\/; };)/,`$1\n\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = ${NOTIF_PRODUCT_ID} /* CatalystNotifications */; };`);// 2. Add to PBXFrameworksBuildPhase files array
59
- projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/,)/,`$1\n\t\t\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */,`);// 3. Add to packageProductDependencies array
60
- projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);// 4. Add to XCSwiftPackageProductDependency section
61
- projectContent=projectContent.replace(/(\/\* End XCSwiftPackageProductDependency section \*\/)/,`\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = C99974322E97D56900C25611 /* XCLocalSwiftPackageReference "." */;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n$1`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("βœ… CatalystNotifications added to Xcode project","success");}else if(!isNotificationsEnabled&&hasNotifications){progress.log("Removing CatalystNotifications from Xcode project","info");// Remove all CatalystNotifications entries
62
- projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("βœ… CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(pluginComposition={}){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const pluginUrlSchemes=pluginComposition.urlSchemes||[];const pluginQuerySchemes=pluginComposition.querySchemes||[];const pluginInfoPlist=pluginComposition.infoPlist||{};plistTargets.forEach(plistPath=>{if(!fs.existsSync(plistPath)){return;}restoreManagedFileFromBaseline(plistPath);const plistObject=readPlistObject(plistPath);plistObject.CFBundleDisplayName=iosConfig.appName||"Catalyst Application";mergeIntoTopLevelObject(plistObject,pluginInfoPlist,"ios.infoPlist");if(isGoogleSignInEnabled&&resolvedReversedClientId){mergeUrlSchemes(plistObject,[{name:"googleSignIn",schemes:[resolvedReversedClientId]}]);mergeQuerySchemes(plistObject,["google",resolvedReversedClientId]);}mergeUrlSchemes(plistObject,pluginUrlSchemes);mergeQuerySchemes(plistObject,pluginQuerySchemes);writePlistObject(plistPath,plistObject);});}catch(err){progress.fail("config",err);process.exit(1);}}async function updateEntitlements(pluginComposition={}){try{const entitlementsPath=path.join(PROJECT_DIR,PROJECT_NAME,`${PROJECT_NAME}.entitlements`);if(!fs.existsSync(entitlementsPath)){return;}restoreManagedFileFromBaseline(entitlementsPath);const entitlementsObject=readPlistObject(entitlementsPath);mergeIntoTopLevelObject(entitlementsObject,pluginComposition.entitlements||{},"ios.entitlements");writePlistObject(entitlementsPath,entitlementsObject);}catch(error){progress.fail("config",error);process.exit(1);}}async function removePluginResourcesFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT")return;throw err;}const patterns=[/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/ = \{isa = PBXFileReference;[^\n]*path = [^;]*PluginResources\/[^;]+; sourceTree = "<group>"; \};\n/g,/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/ = \{isa = PBXBuildFile;[^\n]*\};\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/,\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/,\n/g];let modified=false;for(const pattern of patterns){const nextContent=projectContent.replace(pattern,"");if(nextContent!==projectContent){projectContent=nextContent;modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed managed plugin resources from Xcode project","info");}}catch(error){throw new Error(`Could not remove plugin resources from Xcode project: ${error.message}`);}}async function addPluginResourcesToXcodeProject(resources){try{if(resources.length===0){return;}const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT"){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}throw err;}for(const resource of resources){const label=resource.bundleRelativePath;const fileRefId=generateProjectObjectId(label,"_ref");const buildFileId=generateProjectObjectId(label,"_build");const pbxprojPath=formatPbxprojPath(label);const fileType=detectPbxprojFileType(label);const fileRefEntry=`\t\t${fileRefId} /* ${label} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; path = ${pbxprojPath}; sourceTree = "<group>"; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`,"PBXFileReference section not found while registering plugin resources");const buildFileEntry=`\t\t${buildFileId} /* ${label} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${label} */; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`,"PBXBuildFile section not found while registering plugin resources");const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;projectContent=replaceRequired(projectContent,groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${label} */,`,"iosnativeWebView group not found while registering plugin resources");const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;projectContent=replaceRequired(projectContent,resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${label} in Resources */,`,"Resources build phase not found while registering plugin resources");}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${resources.length} managed plugin resource(s) in Xcode project`,"success");}catch(error){throw new Error(`Could not add plugin resources to Xcode project: ${error.message}`);}}async function syncPluginResources(pluginComposition={}){try{const resources=pluginComposition.resources||[];const pluginResourceDir=path.join(PROJECT_DIR,PROJECT_NAME,PLUGIN_RESOURCE_ROOT);fs.rmSync(pluginResourceDir,{recursive:true,force:true});await removePluginResourcesFromXcodeProject();if(resources.length===0){progress.log("No managed plugin resources to sync","info");return;}for(const resource of resources){const normalizedBundleRelativePath=path.posix.normalize(resource.bundleRelativePath||"");const expectedPrefix=`${PLUGIN_RESOURCE_ROOT}/`;if(!normalizedBundleRelativePath.startsWith(expectedPrefix)||normalizedBundleRelativePath.includes("../")||path.posix.isAbsolute(normalizedBundleRelativePath)){throw new Error(`Invalid managed plugin resource path: ${resource.bundleRelativePath}`);}const targetPath=path.resolve(PROJECT_DIR,PROJECT_NAME,normalizedBundleRelativePath);if(targetPath!==pluginResourceDir&&!targetPath.startsWith(`${pluginResourceDir}${path.sep}`)){throw new Error(`Managed plugin resource escaped bundle directory: ${resource.bundleRelativePath}`);}fs.mkdirSync(path.dirname(targetPath),{recursive:true});fs.copyFileSync(resource.sourcePath,targetPath);}await addPluginResourcesToXcodeProject(resources);progress.log(`Synced ${resources.length} managed plugin resource(s)`,"success");}catch(error){progress.log(`❌ Failed to sync plugin resources: ${error.message}`,"error");throw error;}}// Function to convert JSON value to Swift property
63
- function generateSwiftProperty(key,value,indent=" "){if(value===null||value===undefined){return`${indent}public static let ${key}: String? = nil`;}// Special handling for cachePattern - always convert to array
64
- if(key==="cachePattern"){if(typeof value==="string"){// Convert single string to array
65
- return`${indent}public static let ${key}: [String] = ["${value}"]`;}else if(Array.isArray(value)){const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="string"){return`${indent}public static let ${key} = "${value}"`;}if(typeof value==="number"){return Number.isInteger(value)?`${indent}public static let ${key} = ${value}`:`${indent}public static let ${key} = ${value}`;}if(typeof value==="boolean"){return`${indent}public static let ${key} = ${value}`;}if(Array.isArray(value)){if(value.length===0){return`${indent}public static let ${key}: [String] = []`;}// Determine array type from first element
66
- const firstElement=value[0];if(typeof firstElement==="string"){const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}else if(typeof firstElement==="number"){const arrayType=Number.isInteger(firstElement)?"Int":"Double";const arrayValues=value.join(", ");return`${indent}public static let ${key}: [${arrayType}] = [${arrayValues}]`;}else if(typeof firstElement==="boolean"){const arrayValues=value.join(", ");return`${indent}public static let ${key}: [Bool] = [${arrayValues}]`;}else{// Mixed array - convert to strings
67
- const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="object"&&value!==null){// Generate nested enum for objects
68
- let nestedContent=`${indent}public enum ${key.charAt(0).toUpperCase()+key.slice(1)} {\n`;for(const[nestedKey,nestedValue]of Object.entries(value)){nestedContent+=generateSwiftProperty(nestedKey,nestedValue,indent+" ")+"\n";}nestedContent+=`${indent}}`;return nestedContent;}// Fallback to string
69
- return`${indent}public static let ${key} = "${value}"`;}async function generateConfigConstants(){progress.start("config");try{const appConfigPath=path.join(PROJECT_DIR,"Sources","Core","Constants","ConfigConstants.swift");const appConfigDir=path.dirname(appConfigPath);if(!fs.existsSync(appConfigDir)){fs.mkdirSync(appConfigDir,{recursive:true});}// Initialize base config with required URL
70
- let configContent=`// This file is auto-generated. Do not edit.
71
- import Foundation
72
-
73
- public enum ConfigConstants {
74
- public static let url = "${url}"`;// Track keys already added to avoid duplicates
75
- const addedKeys=new Set();// Process all properties from WEBVIEW_CONFIG
76
- if(WEBVIEW_CONFIG&&typeof WEBVIEW_CONFIG==="object"){for(const[key,value]of Object.entries(WEBVIEW_CONFIG)){// Skip 'ios' and 'android' keys to avoid duplication
77
- if(key==="ios"||key==="android")continue;if(key==="notifications"){progress.log(`Processing notifications config: ${JSON.stringify(value)}`,"info");}configContent+="\n"+generateSwiftProperty(key,value);addedKeys.add(key);}}// Process iOS-specific config
78
- if(iosConfig&&typeof iosConfig==="object"){configContent+="\n \n // iOS-specific configuration";for(const[key,value]of Object.entries(iosConfig)){// Skip if key was already added from WEBVIEW_CONFIG
79
- if(addedKeys.has(key))continue;configContent+="\n"+generateSwiftProperty(key,value);}}// Add URL whitelisting configuration if it exists
80
- if(WEBVIEW_CONFIG.accessControl){const accessControl=WEBVIEW_CONFIG.accessControl;configContent+=`
81
- public static let accessControlEnabled = ${accessControl.enabled||false}`;if(accessControl.allowedUrls&&Array.isArray(accessControl.allowedUrls)){const allowedUrls=accessControl.allowedUrls.map(url=>`"${url}"`).join(", ");configContent+=`
82
- public static let allowedUrls: [String] = [${allowedUrls}]`;}else if(accessControl.allowedUrls&&typeof accessControl.allowedUrls==="string"){// Handle comma-separated string format
83
- const allowedUrls=accessControl.allowedUrls.split(",").map(url=>url.trim()).filter(url=>url.length>0).map(url=>`"${url}"`).join(", ");configContent+=`
84
- public static let allowedUrls: [String] = [${allowedUrls}]`;}}else{// Default values when no access control is configured
85
- configContent+=`
86
- public static let accessControlEnabled = false
87
- public static let allowedUrls: [String] = []`;}// Add splash screen configuration from WEBVIEW_CONFIG.splashScreen
88
- const splashConfig=WEBVIEW_CONFIG.splashScreen;if(splashConfig){configContent+=`
89
-
90
- // Splash Screen Configuration
91
- public static let splashScreenEnabled = true`;if(splashConfig.duration){// Convert milliseconds to seconds for iOS TimeInterval
92
- const durationInSeconds=splashConfig.duration/1000.0;configContent+=`
93
- public static let splashScreenDuration: TimeInterval? = ${durationInSeconds}`;}else{configContent+=`
94
- public static let splashScreenDuration: TimeInterval? = nil`;}if(splashConfig.backgroundColor){configContent+=`
95
- public static let splashScreenBackgroundColor = "${splashConfig.backgroundColor}"`;}else{configContent+=`
96
- public static let splashScreenBackgroundColor = "#ffffff"`;}// Add splash screen image styling configuration
97
- const imageWidth=splashConfig.imageWidth||120;const imageHeight=splashConfig.imageHeight||120;const cornerRadius=splashConfig.cornerRadius||20;configContent+=`
98
- public static let splashScreenImageWidth: CGFloat = ${imageWidth}
99
- public static let splashScreenImageHeight: CGFloat = ${imageHeight}
100
- public static let splashScreenCornerRadius: CGFloat = ${cornerRadius}`;}else{configContent+=`
101
-
102
- // Splash Screen Configuration
103
- public static let splashScreenEnabled = false
104
- public static let splashScreenDuration: TimeInterval? = nil
105
- public static let splashScreenBackgroundColor = "#ffffff"
106
- public static let splashScreenImageWidth: CGFloat = 120
107
- public static let splashScreenImageHeight: CGFloat = 120
108
- public static let splashScreenCornerRadius: CGFloat = 20`;}// Ensure Notifications.enabled always exists (default to false if not configured)
109
- if(!addedKeys.has("notifications")){progress.log("Notifications not found in config, adding default (false)","info");configContent+="\n public enum Notifications {\n public static let enabled = false\n }";addedKeys.add("notifications");}else{progress.log("Notifications config was processed from WEBVIEW_CONFIG","info");}// Ensure Google Sign-In defaults exist to keep compile-time references stable
110
- if(!addedKeys.has("googleSignIn")){progress.log("Google Sign-In not found in config, adding defaults (disabled)","info");configContent+='\n public enum GoogleSignIn {\n public static let enabled = false\n public static let clientId = ""\n public static let iosClientId = ""\n }';addedKeys.add("googleSignIn");}// Ensure EdgeToEdge.enabled always exists (default to false if not configured)
111
- if(!addedKeys.has("edgeToEdge")){progress.log("EdgeToEdge not found in config, adding default (false)","info");configContent+="\n public enum EdgeToEdge {\n public static let enabled = false\n }";addedKeys.add("edgeToEdge");}else{progress.log("EdgeToEdge config was processed from WEBVIEW_CONFIG","info");}// Close the enum
112
- configContent+=`
113
- }`;fs.writeFileSync(appConfigPath,configContent,"utf8");progress.log("Configuration constants generated successfully (SPM Package)","success");progress.complete("config");}catch(error){progress.fail("config",error.message);process.exit(1);}}// MARK: - Notification Asset Processing
114
- // Notification asset definitions
115
- const NOTIFICATION_ICONS=[{sourceName:"notification-icon",resourceName:"NotificationIcon"},{sourceName:"notification-large",resourceName:"NotificationLargeIcon"}];const NOTIFICATION_SOUNDS=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];async function addGoogleServicesPlistToXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();if(!fs.existsSync(projectFilePath)){progress.log("Xcode project file not found while adding GoogleService-Info.plist","warning");return;}let projectContent=fs.readFileSync(projectFilePath,"utf8");if(projectContent.includes(`/* ${GOOGLE_SERVICES_FILENAME} */`)){progress.log("GoogleService-Info.plist already registered in Xcode project","info");return;}const fileRefId=generateProjectObjectId(GOOGLE_SERVICES_FILENAME,"_ref");const buildFileId=generateProjectObjectId(GOOGLE_SERVICES_FILENAME,"_build");const fileRefEntry=`\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ${GOOGLE_SERVICES_FILENAME}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);const buildFileEntry=`\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent)){projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */,`);}const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent)){projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */,`);}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Registered GoogleService-Info.plist with Xcode project","success");}catch(error){progress.log(`Warning: Failed to register GoogleService-Info.plist in Xcode project: ${error.message}`,"warning");}}async function removeGoogleServicesPlistFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();if(!fs.existsSync(projectFilePath)){return;}let projectContent=fs.readFileSync(projectFilePath,"utf8");const fileRefRegex=new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} \\*\\/ = \\{isa = PBXFileReference;[^}]*\\};\\n`,"g");const buildFileRegex=new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} in Resources \\*\\/ = \\{isa = PBXBuildFile;[^}]*\\};\\n`,"g");const groupRefRegex=new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} \\*\\/,\\n`,"g");const resourceRefRegex=new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} in Resources \\*\\/,\\n`,"g");const replacements=[fileRefRegex,buildFileRegex,groupRefRegex,resourceRefRegex];let modified=false;for(const regex of replacements){if(regex.test(projectContent)){projectContent=projectContent.replace(regex,"");modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed GoogleService-Info.plist references from Xcode project","info");}}catch(error){progress.log(`Warning: Failed to clean GoogleService-Info.plist from Xcode project: ${error.message}`,"warning");}}async function handleGoogleServicesPlist(){try{const rootGoogleServicesPath=`${process.env.PWD}/GoogleService-Info.plist`;const iosGoogleServicesPath=`${PROJECT_DIR}/${PROJECT_NAME}/GoogleService-Info.plist`;// Check if GoogleService-Info.plist exists in the root directory
116
- if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found GoogleService-Info.plist in root directory","info");// Create the directory if it doesn't exist
117
- const targetDir=path.dirname(iosGoogleServicesPath);if(!fs.existsSync(targetDir)){fs.mkdirSync(targetDir,{recursive:true});}// Copy the file to the iOS project
118
- fs.copyFileSync(rootGoogleServicesPath,iosGoogleServicesPath);progress.log("Copied GoogleService-Info.plist to iOS project","success");await addGoogleServicesPlistToXcodeProject();return true;}else if(fs.existsSync(iosGoogleServicesPath)){progress.log("GoogleService-Info.plist already exists in iOS project","info");await addGoogleServicesPlistToXcodeProject();return true;}else{await removeGoogleServicesPlistFromXcodeProject();progress.log("GoogleService-Info.plist not found - Firebase push notifications will not work","warning");progress.log("Place GoogleService-Info.plist in project root","info");return false;}}catch(error){// Critical error if file exists but can't be copied
119
- const rootGoogleServicesPath=`${process.env.PWD}/GoogleService-Info.plist`;if(error.code==="EACCES"&&fs.existsSync(rootGoogleServicesPath)){throw new Error(`Permission denied copying GoogleService-Info.plist: ${error.message}`);}await removeGoogleServicesPlistFromXcodeProject();progress.log(`Warning: Error handling GoogleService-Info.plist: ${error.message}`,"warning");return false;}}async function validateNotificationAsset(filePath,assetType){const stats=fs.statSync(filePath);const fileSizeKB=stats.size/1024;if(assetType==="icon"){// Warn if icon is too large (recommended <100KB)
120
- if(fileSizeKB>100){progress.log(`Warning: Notification icon ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <100KB for optimal performance)`,"warning");}}else if(assetType==="sound"){// Warn if sound is too large (recommended <1MB)
121
- if(fileSizeKB>1024){progress.log(`Warning: Notification sound ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <1MB)`,"warning");}}}async function processNotificationIcons(){try{const assetsPath=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const imageFormats=["png","jpg","jpeg","webp"];let iconsProcessed=0;// Ensure public directory exists
122
- if(!fs.existsSync(PUBLIC_PATH)){progress.log(`Public directory not found at ${PUBLIC_PATH}`,"info");return 0;}// Create Assets.xcassets if it doesn't exist
123
- if(!fs.existsSync(assetsPath)){fs.mkdirSync(assetsPath,{recursive:true});}// Remove existing notification icon imagesets to avoid conflicts when assets are updated
124
- for(const icon of NOTIFICATION_ICONS){const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(fs.existsSync(imagesetPath)){fs.rmSync(imagesetPath,{recursive:true,force:true});progress.log(`Removed existing ${icon.resourceName}.imageset`,"info");}}// Process notification icons
125
- for(const icon of NOTIFICATION_ICONS){for(const format of imageFormats){const iconImagePath=`${PUBLIC_PATH}/${icon.sourceName}.${format}`;if(fs.existsSync(iconImagePath)){// Validate asset size
126
- validateNotificationAsset(iconImagePath,"icon");// Create imageset directory
127
- const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(!fs.existsSync(imagesetPath)){fs.mkdirSync(imagesetPath,{recursive:true});}// Copy icon
128
- const destImagePath=`${imagesetPath}/${icon.sourceName}.${format}`;fs.copyFileSync(iconImagePath,destImagePath);// Create Contents.json for the imageset
129
- const contentsJson={images:[{filename:`${icon.sourceName}.${format}`,idiom:"universal",scale:"1x"},{idiom:"universal",scale:"2x"},{idiom:"universal",scale:"3x"}],info:{author:"xcode",version:1}};fs.writeFileSync(`${imagesetPath}/Contents.json`,JSON.stringify(contentsJson,null,2));progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}`,"success");iconsProcessed++;break;}}}if(iconsProcessed>0){progress.log(`Processed ${iconsProcessed} notification icon(s) from public/`,"success");}else{progress.log("No notification icons found in public/ - using default bell icon","info");}return iconsProcessed;}catch(error){// Distinguish between critical and non-critical errors
130
- if(error.code==="EACCES"){throw new Error(`Permission denied accessing notification icons: ${error.message}. Check directory permissions.`);}else if(error.code==="ENOSPC"){throw new Error(`Insufficient disk space to process notification icons: ${error.message}`);}else{progress.log(`Warning: Could not process notification icons: ${error.message}`,"warning");return 0;}}}async function processNotificationSounds(){try{const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];let soundsProcessed=0;// Ensure public directory exists
131
- if(!fs.existsSync(PUBLIC_PATH)){progress.log(`Public directory not found at ${PUBLIC_PATH}`,"info");return 0;}// Remove existing notification sounds to avoid conflicts
132
- for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const existingSoundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(existingSoundPath)){fs.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}// Process notification sounds
133
- for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${PUBLIC_PATH}/${sound.sourceName}.${format}`;if(fs.existsSync(soundPath)){// Validate asset size
134
- validateNotificationAsset(soundPath,"sound");const destSoundPath=`${bundlePath}/${sound.resourceName}.${format}`;fs.copyFileSync(soundPath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");soundsProcessed++;break;}}}if(soundsProcessed>0){progress.log(`Processed ${soundsProcessed} notification sound(s) from public/`,"success");}else{progress.log("No notification sounds found in public/ - using system default sounds","info");}return soundsProcessed;}catch(error){// Distinguish between critical and non-critical errors
135
- if(error.code==="EACCES"){throw new Error(`Permission denied accessing notification sounds: ${error.message}. Check directory permissions.`);}else if(error.code==="ENOSPC"){throw new Error(`Insufficient disk space to process notification sounds: ${error.message}`);}else{progress.log(`Warning: Could not process notification sounds: ${error.message}`,"warning");return 0;}}}async function removeSoundFilesFromXcodeProject(){try{const projectFilePath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");if(!fs.existsSync(projectFilePath)){return;}let projectContent=fs.readFileSync(projectFilePath,"utf8");let modified=false;// Remove all notification sound file references
136
- for(const sound of NOTIFICATION_SOUNDS){// Use the resourceName from each sound object
137
- const pattern=sound.resourceName;// Remove PBXFileReference entries
138
- const fileRefRegex=new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${pattern}\\.[a-z0-9]+ \\*\\/ = \\{isa = PBXFileReference;[^}]*\\};\\n`,"g");if(fileRefRegex.test(projectContent)){projectContent=projectContent.replace(fileRefRegex,"");modified=true;}// Remove PBXBuildFile entries
139
- const buildFileRegex=new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${pattern}\\.[a-z0-9]+ in Resources \\*\\/ = \\{isa = PBXBuildFile;[^}]*\\};\\n`,"g");if(buildFileRegex.test(projectContent)){projectContent=projectContent.replace(buildFileRegex,"");modified=true;}// Remove from PBXGroup
140
- const groupRefRegex=new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${pattern}\\.[a-z0-9]+ \\*\\/,\\n`,"g");if(groupRefRegex.test(projectContent)){projectContent=projectContent.replace(groupRefRegex,"");modified=true;}// Remove from PBXResourcesBuildPhase
141
- const resourceRefRegex=new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${pattern}\\.[a-z0-9]+ in Resources \\*\\/,\\n`,"g");if(resourceRefRegex.test(projectContent)){projectContent=projectContent.replace(resourceRefRegex,"");modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed notification sounds from Xcode project","success");}}catch(error){progress.log(`Warning: Could not remove sound files from Xcode project: ${error.message}`,"warning");}}async function cleanupNotificationAssets(removeFirebaseConfig=false){try{const assetsPath=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];// Remove existing notification icon imagesets
142
- for(const icon of NOTIFICATION_ICONS){const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(fs.existsSync(imagesetPath)){fs.rmSync(imagesetPath,{recursive:true,force:true});progress.log(`Removed ${icon.resourceName}.imageset`,"info");}}// Remove existing notification sounds from filesystem
143
- for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);progress.log(`Removed ${sound.resourceName}.${format}`,"info");}}}// Remove sound files from Xcode project
144
- await removeSoundFilesFromXcodeProject();if(removeFirebaseConfig){const iosGoogleServicesPath=`${bundlePath}/${GOOGLE_SERVICES_FILENAME}`;if(fs.existsSync(iosGoogleServicesPath)){fs.unlinkSync(iosGoogleServicesPath);progress.log("Removed GoogleService-Info.plist from iOS project directory","info");}await removeGoogleServicesPlistFromXcodeProject();}progress.log("Cleaned up notification assets","success");}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}async function addSoundFilesToXcodeProject(){try{const projectFilePath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];if(!fs.existsSync(projectFilePath)){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}// Find all sound files that were copied
145
- const soundFiles=[];for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(soundPath)){soundFiles.push({filename:`${sound.resourceName}.${format}`,resourceName:sound.resourceName,format:format});break;}}}if(soundFiles.length===0){progress.log("No sound files to add to Xcode project","info");return;}let projectContent=fs.readFileSync(projectFilePath,"utf8");for(const soundFile of soundFiles){const fileRefId=generateProjectObjectId(soundFile.filename,"_ref");const buildFileId=generateProjectObjectId(soundFile.filename,"_build");// Check if this sound file is already in the project
146
- if(projectContent.includes(`/* ${soundFile.filename} */`)){progress.log(`Sound ${soundFile.filename} already in Xcode project`,"info");continue;}progress.log(`Adding ${soundFile.filename} to Xcode project`,"info");// 1. Add PBXFileReference entry
147
- const fileRefEntry=`\t\t${fileRefId} /* ${soundFile.filename} */ = {isa = PBXFileReference; lastKnownFileType = audio.${soundFile.format==="mp3"?"mp3":"wav"}; path = ${soundFile.filename}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);// 2. Add PBXBuildFile entry
148
- const buildFileEntry=`\t\t${buildFileId} /* ${soundFile.filename} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${soundFile.filename} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);// 3. Add to PBXGroup (find the iosnativeWebView group)
149
- const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent)){projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${soundFile.filename} */,`);}// 4. Add to PBXResourcesBuildPhase
150
- const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent)){projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${soundFile.filename} in Resources */,`);}progress.log(`βœ… Added ${soundFile.filename} to Xcode project`,"success");}// Write back the modified project file
151
- fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${soundFiles.length} sound file(s) in Xcode project`,"success");}catch(error){progress.log(`Warning: Could not add sound files to Xcode project: ${error.message}`,"warning");throw error;}}async function processNotificationAssets(webviewConfig){const hasNotificationConfig=!!webviewConfig.notifications?.enabled;try{// Always clean up notification assets first
152
- await cleanupNotificationAssets(!hasNotificationConfig);if(!hasNotificationConfig){progress.log("Notifications disabled - skipped asset processing","info");return;}// Handle GoogleService-Info.plist file for Firebase
153
- const hasGoogleServices=await handleGoogleServicesPlist();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}// Process notification assets
154
- const iconsProcessed=await processNotificationIcons();const soundsProcessed=await processNotificationSounds();// Add sound files to Xcode project so they're included in the bundle
155
- if(soundsProcessed>0){await addSoundFilesToXcodeProject();}const totalAssets=iconsProcessed+soundsProcessed;if(totalAssets>0){progress.log(`Notification asset processing completed: ${totalAssets} asset(s) processed`,"success");}else{progress.log("No notification assets found - using system defaults","info");}}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addOfflinePageToXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();const offlineFileName="offline.html";const offlineRelativePath="offline/offline.html";if(!fs.existsSync(projectFilePath)){progress.log("Xcode project file not found while adding offline.html","warning");return;}let projectContent=fs.readFileSync(projectFilePath,"utf8");if(projectContent.includes(`/* ${offlineFileName} */`)){progress.log("offline.html already registered in Xcode project","info");return;}const fileRefId=generateProjectObjectId(offlineFileName,"_ref");const buildFileId=generateProjectObjectId(offlineFileName,"_build");const fileRefEntry=`\t\t${fileRefId} /* ${offlineFileName} */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = ${offlineRelativePath}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);const buildFileEntry=`\t\t${buildFileId} /* ${offlineFileName} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${offlineFileName} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent)){projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${offlineFileName} */,`);}const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent)){projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${offlineFileName} in Resources */,`);}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Registered offline.html with Xcode project","success");}catch(error){progress.log(`Warning: Failed to register offline.html in Xcode project: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=path.join(PUBLIC_PATH,"offline.html");const destDir=`${PROJECT_DIR}/${PROJECT_NAME}/offline`;const destPath=`${destDir}/offline.html`;if(!fs.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return false;}fs.mkdirSync(destDir,{recursive:true});fs.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to iOS bundle","success");await addOfflinePageToXcodeProject();return true;}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");return false;}}async function generateXCConfig(){try{// Path to Shared.xcconfig
156
- const xconfigPath=path.join(PROJECT_DIR,"Configurations","Shared.xcconfig");const configDir=path.dirname(xconfigPath);if(!fs.existsSync(configDir)){fs.mkdirSync(configDir,{recursive:true});}// Extract values from config with defaults
157
- const bundleId=iosConfig.bundleIdentifier||iosConfig.appBundleId||"com.catalyst.framework.app";const appName=iosConfig.appName||"Catalyst App";const teamId=iosConfig.developmentTeam||"";const swiftVersion=iosConfig.deployment?.swiftVersion||"5.9";const deploymentTarget=iosConfig.deployment?.target||"17.0";const marketingVersion=iosConfig.version?`${iosConfig.version.major||1}.${iosConfig.version.minor||0}.${iosConfig.version.patch||0}`:"1.0.0";const buildNumber=iosConfig.version?.buildNumber||1;const provisioningProfile=iosConfig.provisioningProfile||"";// Generate Shared.xcconfig content
158
- const xconfigContent=`// This file is auto-generated by buildAppIos.js. Do not edit.
159
- //
160
- // Shared configuration for both Debug and Release builds
161
- // All values are dynamically injected from config.json
162
- //
163
-
164
- // Product Configuration
165
- PRODUCT_BUNDLE_IDENTIFIER = ${bundleId}
166
- APP_DISPLAY_NAME = ${appName}
167
-
168
- // Version Information
169
- MARKETING_VERSION = ${marketingVersion}
170
- CURRENT_PROJECT_VERSION = ${buildNumber}
171
-
172
- // Development Team (for code signing)
173
- DEVELOPMENT_TEAM = ${teamId}
174
-
175
- // Swift Configuration
176
- SWIFT_VERSION = ${swiftVersion}
177
-
178
- // iOS Deployment Target (minimum iOS version)
179
- IPHONEOS_DEPLOYMENT_TARGET = ${deploymentTarget}
180
-
181
- // Common build settings
182
- TARGETED_DEVICE_FAMILY = 1,2
183
- ENABLE_PREVIEWS = YES
184
- SWIFT_EMIT_LOC_STRINGS = YES
185
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
186
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
187
-
188
- // Code signing
189
- CODE_SIGN_IDENTITY = Apple Development
190
- CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
191
- CODE_SIGN_STYLE = Manual
192
- PROVISIONING_PROFILE_SPECIFIER =
193
- PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = ${provisioningProfile}
194
-
195
- // Module settings
196
- CLANG_ENABLE_MODULES = NO
197
- GENERATE_INFOPLIST_FILE = YES
198
-
199
- // Run path search paths
200
- LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
201
-
202
- // Development assets
203
- DEVELOPMENT_ASSET_PATHS = "iosnativeWebView/Preview Content"
204
-
205
- // Privacy descriptions
206
- INFOPLIST_KEY_NSCameraUsageDescription = This app requires camera access to capture images
207
- INFOPLIST_KEY_NSLocalNetworkUsageDescription = Debug web content in Safari Web Inspector
208
-
209
- // UI Configuration
210
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES
211
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES
212
- INFOPLIST_KEY_UILaunchScreen_Generation = YES
213
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
214
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
215
- `;fs.writeFileSync(xconfigPath,xconfigContent,"utf8");console.log(`βœ… Generated Shared.xcconfig with Swift ${swiftVersion}, iOS ${deploymentTarget}, Bundle ID: ${bundleId}`);}catch(error){console.error(`❌ Failed to generate Shared.xcconfig: ${error.message}`);throw error;}}async function cleanBuildArtifacts(){progress.start("clean");try{const xcuserdataPath=path.join(`${PROJECT_NAME}.xcodeproj`,`project.xcworkspace`,"xcuserdata");if(fs.existsSync(xcuserdataPath)){await runCommand(`rm -rf "${xcuserdataPath}"`);}const derivedDataPath=path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData");await runCommand(`rm -rf "${derivedDataPath}/${PROJECT_NAME}-*"`);try{await runCommand(`xcodebuild clean -scheme "${SCHEME_NAME}" -sdk iphonesimulator -configuration Debug`);}catch(error){progress.log("Warning: Clean command returned non-zero exit code","warning");}progress.complete("clean");}catch(error){progress.fail("clean",error.message);process.exit(1);}}async function buildXcodeProject(){progress.start("build");try{const derivedDataPath=path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData");try{progress.log("Building for specified simulator...","info");await buildProject(SCHEME_NAME,"iphonesimulator",`platform=iOS Simulator,name=${IPHONE_MODEL}`,APP_BUNDLE_ID,derivedDataPath,PROJECT_NAME);}catch(buildError){progress.log("Initial build failed, attempting fallback...","warning");const bootedUUID=await getBootedSimulatorUUID(IPHONE_MODEL);if(bootedUUID){progress.log(`Found booted simulator, retrying build...`,"info");await buildProject(SCHEME_NAME,"iphonesimulator",`platform=iOS Simulator,id=${bootedUUID}`,APP_BUNDLE_ID,derivedDataPath,PROJECT_NAME);}else{throw buildError;}}progress.complete("build");}catch(error){progress.fail("build",error.message);progress.printTreeContent("Troubleshooting Guide",["Build failed. Please try the following steps:",{text:'Run "npm run setupEmulator:ios" to reconfigure iOS settings',indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Check if Xcode is properly installed and updated",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Verify selected simulator exists",indent:1,prefix:"Ò””Ò”€ ",color:"yellow"},"\nVerify Configuration:",{text:`Selected Simulator: ${IPHONE_MODEL}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`Server URL: ${url}`,indent:1,prefix:"Ò””Ò”€ ",color:"gray"}]);process.exit(1);}}async function findAppPath(){progress.start("findApp");try{const DERIVED_DATA_DIR=path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData");let APP_PATH="";try{APP_PATH=execSync(`find "${DERIVED_DATA_DIR}" -path "*${PROJECT_NAME}-*" -prune -not -path "*/Index.noindex*" -path "*/Build/Products/Debug-iphonesimulator/${PROJECT_NAME}.app" -type d | head -n 1`).toString().trim();}catch(error){progress.log("Primary app path search failed, trying fallback...","warning");}if(!APP_PATH){try{APP_PATH=execSync(`find "${DERIVED_DATA_DIR}" -path "*${PROJECT_NAME}-*" -name "${PROJECT_NAME}.app" -type d -not -path "*/Index.noindex*" | head -n 1`).toString().trim();}catch(error){throw new Error("Could not locate built application");}}if(!APP_PATH){progress.fail("findApp","No .app file found");process.exit(1);}progress.complete("findApp");return APP_PATH;}catch(error){progress.fail("findApp",error.message);throw error;}}async function moveAppToBuildOutput(APP_PATH){try{const buildType=iosConfig.buildType||"debug";const appName=iosConfig.appName||"app";const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");// DD-MM-YYYY format
216
- const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,":");// HH-MM-SS AM/PM format
217
- const destinationDir=path.join(process.env.PWD,BUILD_OUTPUT_PATH,"native","ios",currentDate,buildType);const destinationPath=path.join(destinationDir,`${appName}-${currentTime}.app`);// Create destination directory if it doesn't exist
218
- if(!fs.existsSync(destinationDir)){fs.mkdirSync(destinationDir,{recursive:true});}// Copy the app to the destination using shell command
219
- await runCommand(`cp -R "${APP_PATH}" "${destinationPath}"`);return destinationPath;}catch(error){console.error("Error moving app to build output:",error.message);throw error;}}async function installAndLaunchApp(APP_PATH){progress.start("install");try{await uninstallExistingApp();await installApp(APP_PATH);await waitForInstallation();progress.complete("install");progress.start("launch");await launchAndVerifyApp();await focusSimulator();progress.complete("launch");}catch(error){const currentStep=progress.currentStep.id;progress.fail(currentStep,error.message);process.exit(1);}}async function uninstallExistingApp(){console.log("Uninstalling the app if it exists...");await runCommand(`xcrun simctl uninstall booted "${APP_BUNDLE_ID}"`);}async function installApp(APP_PATH){console.log("Installing the app...");try{await runCommand(`xcrun simctl install booted "${APP_PATH}"`);}catch(error){console.error("Error installing the app:",error);process.exit(1);}}async function waitForInstallation(){console.log("Waiting for the app to be fully installed...");for(let i=0;i<30;i++){try{execSync(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`);console.log("App installed successfully.");break;}catch(error){if(i===29){console.log("Timeout: App installation took too long.");process.exit(1);}await new Promise(resolve=>setTimeout(resolve,1000));}}}async function launchAndVerifyApp(){console.log("Launching the app...");try{await runCommand(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`);await verifyAppLaunch();}catch(error){await handleLaunchError(error);}}async function verifyAppLaunch(){console.log("Waiting for the app to launch...");for(let i=0;i<10;i++){try{const launchResult=execSync(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`).toString();if(launchResult.includes("already launched")){console.log("App launched successfully.");break;}}catch(error){if(i===9){console.log("Warning: App launch might have failed or taken too long.");}}await new Promise(resolve=>setTimeout(resolve,1000));}}async function handleLaunchError(error){console.error("Error launching the app:",error);console.log("Checking app container...");try{await runCommand(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`);}catch(containerError){console.log("App container not found");}process.exit(1);}async function focusSimulator(){console.log("Focusing on Simulator...");await runCommand(`osascript -e 'tell application "Simulator" to activate'`);}// Physical Device Detection Functions
220
- async function detectPhysicalDevices(){progress.start("deviceDetection");try{progress.log("Scanning for connected physical devices...","info");let physicalDevices=[];// Priority 1: Check if UDID is specified in config
221
- const configuredUDID=iosConfig.deviceUDID;if(configuredUDID){progress.log(`Using configured device UDID: ${configuredUDID}`,"info");// Verify the configured device is actually connected
222
- try{const instrumentsOutput=execSync("instruments -s devices").toString();if(instrumentsOutput.includes(configuredUDID)){// Extract device name from instruments output
223
- const deviceLine=instrumentsOutput.split("\n").find(line=>line.includes(configuredUDID));if(deviceLine){const nameMatch=deviceLine.match(/^(.+?)\s+\(/);const versionMatch=deviceLine.match(/\((\d+\.\d+(?:\.\d+)?)\)/);const deviceName=nameMatch?nameMatch[1].trim():"Physical Device";const deviceVersion=versionMatch?versionMatch[1]:"Unknown";progress.log(`βœ… Found configured physical device: ${deviceName} (${deviceVersion})`,"success");physicalDevices.push({name:deviceName,version:deviceVersion,udid:configuredUDID,type:"physical"});progress.complete("deviceDetection");return physicalDevices[0];}}else{progress.log(`⚠️ Configured device UDID not found in connected devices`,"warning");progress.log("Falling back to auto-detection...","info");}}catch(error){progress.log(`Error verifying configured device: ${error.message}`,"warning");progress.log("Falling back to auto-detection...","info");}}else{progress.log("No device UDID configured, using auto-detection","info");}// Priority 2: Auto-detect using multiple fallback methods
224
- // Try instruments first
225
- try{const instrumentsOutput=execSync("instruments -s devices").toString();const lines=instrumentsOutput.split("\n");for(const line of lines){// Match physical devices (have UDID but not simulator indicators)
226
- const deviceMatch=line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\](?:\s+\(Simulator\))?$/);if(deviceMatch&&!line.includes("(Simulator)")){const[,name,version,udid]=deviceMatch;physicalDevices.push({name:name.trim(),version:version,udid:udid,type:"physical"});}}if(physicalDevices.length>0){progress.log(`Found ${physicalDevices.length} physical device(s) via instruments`,"success");}}catch(error){progress.log("instruments command failed, trying xcodebuild...","warning");}// Fallback: Use xcodebuild to get available destinations
227
- if(physicalDevices.length===0){try{const xcodebuildOutput=execSync(`xcodebuild -scheme "${SCHEME_NAME}" -showdestinations`).toString();const lines=xcodebuildOutput.split("\n");progress.log("Scanning xcodebuild destinations for physical devices...","info");for(const line of lines){// Look for physical iOS devices in xcodebuild output
228
- const physicalMatch=line.match(/\{\s*platform:iOS,\s*arch:(\w+),\s*id:([A-F0-9-]+),\s*name:(.+?)\s*\}/);if(physicalMatch){const[,arch,udid,name]=physicalMatch;progress.log(`Found device candidate: ${name.trim()} - ${udid}`,"info");// Filter out placeholder devices
229
- if(!udid.includes("placeholder")&&udid.length>20){progress.log(`Òœ… Valid physical device: ${name.trim()}`,"success");physicalDevices.push({name:name.trim(),version:"Unknown",udid:udid,arch:arch,type:"physical"});}else{progress.log(`ҝŒ Skipping placeholder: ${name.trim()}`,"warning");}}}}catch(error){progress.log("xcodebuild destinations failed, trying instruments...","warning");progress.log(`Error: ${error.message}`,"error");}}// Fallback: Try instruments if xcodebuild didn't work
230
- if(physicalDevices.length===0){try{const instrumentsOutput=execSync("instruments -s devices").toString();const lines=instrumentsOutput.split("\n");for(const line of lines){// Match physical devices (have UDID but not simulator indicators)
231
- const deviceMatch=line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\](?:\s+\(Simulator\))?$/);if(deviceMatch&&!line.includes("(Simulator)")){const[,name,version,udid]=deviceMatch;physicalDevices.push({name:name.trim(),version:version,udid:udid,type:"physical"});}}}catch(error){progress.log("Instruments command also failed, trying devicectl...","warning");}}// Last fallback: Try using xcrun devicectl (iOS 17+)
232
- if(physicalDevices.length===0){try{const devicectlOutput=execSync("xcrun devicectl list devices").toString();const lines=devicectlOutput.split("\n");for(const line of lines){// Parse devicectl output format
233
- if(line.includes("Connected")&&!line.includes("Simulator")){const udidMatch=line.match(/([A-F0-9-]{36})/);const nameMatch=line.match(/^(.+?)\s+\(/);if(udidMatch&&nameMatch){physicalDevices.push({name:nameMatch[1].trim(),version:"Unknown",udid:udidMatch[1],type:"physical"});}}}}catch(error){progress.log("devicectl command not available or failed","warning");}}if(physicalDevices.length>0){progress.log(`Found ${physicalDevices.length} physical device(s):`,"success");physicalDevices.forEach(device=>{progress.log(` Γ°ΕΈβ€œΒ± ${device.name} (${device.version||"Unknown iOS"}) - ${device.udid}`,"info");});progress.complete("deviceDetection");return physicalDevices[0];// Return first available device
234
- }else{progress.log("No physical devices detected","warning");progress.complete("deviceDetection");return null;}}catch(error){progress.fail("deviceDetection",error.message);return null;}}async function buildProject(scheme,sdk,destination,bundleId,derivedDataPath,projectName){// Get the booted device info first
235
- const bootedInfo=await getBootedSimulatorInfo();if(!bootedInfo){throw new Error("No booted simulator found");}// Use specific UDID to avoid multiple device conflicts
236
- const destinationWithUDID=`platform=iOS Simulator,id=${bootedInfo.udid}`;console.log(`Building with destination: ${destinationWithUDID}`);// Notifications are controlled via Package.swift - canImport() detects availability
237
- const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;if(isNotificationsEnabled){progress.log("Building with notifications enabled (CatalystNotifications module included)","info");}else{progress.log("Building without notifications (Firebase excluded)","info");}const buildCommand=`xcodebuild \
238
- -scheme "${scheme}" \
239
- -sdk ${sdk} \
240
- -configuration Debug \
241
- -destination "${destinationWithUDID}" \
242
- PRODUCT_BUNDLE_IDENTIFIER="${bundleId}" \
243
- DEVELOPMENT_TEAM="" \
244
- CODE_SIGN_IDENTITY="" \
245
- CODE_SIGNING_REQUIRED=NO \
246
- CODE_SIGNING_ALLOWED=NO \
247
- ONLY_ACTIVE_ARCH=YES \
248
- BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products" \
249
- CONFIGURATION_BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products/Debug-iphonesimulator" \
250
- OS_ACTIVITY_MODE=debug \
251
- SWIFT_DEBUG_LOG=1 \
252
- build`;return runCommand(buildCommand,{maxBuffer:1024*1024*10});}// Physical Device Build Functions
253
- async function buildProjectForPhysicalDevice(scheme,bundleId,derivedDataPath,projectName,device){progress.log(`Building for physical device: ${device.name}`,"info");progress.log("Using Xcode project signing configuration","info");progress.log(`Overriding bundle ID to: ${bundleId}`,"info");progress.log(`Current directory: ${process.cwd()}`,"info");progress.log(`Looking for .xcodeproj in: ${process.cwd()}`,"info");// Check if we're in the right directory
254
- const projectPath=`${process.cwd()}/${projectName}.xcodeproj`;if(!require("fs").existsSync(projectPath)){throw new Error(`Xcode project not found at: ${projectPath}. Current directory: ${process.cwd()}`);}// Notifications are controlled via Package.swift - canImport() detects availability
255
- const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;if(isNotificationsEnabled){progress.log("Building with notifications enabled (CatalystNotifications module included)","info");}else{progress.log("Building without notifications (Firebase excluded)","info");}// Working build command based on successful test (without extra BUILD_DIR params that might cause issues)
256
- const buildCommand=`xcodebuild \
257
- -scheme ${scheme} \
258
- -sdk iphoneos \
259
- -configuration Debug \
260
- -destination platform=iOS,id=${device.udid} \
261
- PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
262
- ONLY_ACTIVE_ARCH=YES \
263
- build`;progress.log("Building with Xcode project signing settings...","info");progress.log(`Executing command: ${buildCommand}`,"info");return runCommand(buildCommand,{maxBuffer:1024*1024*10});}async function findPhysicalDeviceAppPath(){const DERIVED_DATA_DIR=path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData");let APP_PATH="";try{// Search for app in proper Build/Products directory (not Index.noindex)
264
- APP_PATH=execSync(`find "${DERIVED_DATA_DIR}" -name "${PROJECT_NAME}.app" -path "*/Build/Products/Debug-iphoneos/*" -not -path "*/Index.noindex/*" -type d | head -n 1`).toString().trim();}catch(error){progress.log("Primary app path search failed for physical device, trying fallback...","warning");}if(!APP_PATH){try{// Fallback: search in our custom build directory
265
- APP_PATH=execSync(`find "${DERIVED_DATA_DIR}" -path "*${PROJECT_NAME}-Build/Build/Products/Debug-iphoneos/${PROJECT_NAME}.app" -type d | head -n 1`).toString().trim();}catch(error){progress.log("Fallback app path search also failed","warning");}}if(!APP_PATH){throw new Error("No .app file found for physical device. Check if build completed successfully.");}progress.log(`Found app bundle: ${APP_PATH}`,"success");return APP_PATH;}async function installAndLaunchOnPhysicalDevice(APP_PATH,device){progress.start("install");try{progress.log(`Installing app on physical device: ${device.name}`,"info");// Use xcrun devicectl (working method)
266
- try{await runCommand(`xcrun devicectl device install app --device ${device.udid} "${APP_PATH}"`);progress.log("App installed successfully using devicectl","success");}catch(devicectlError){throw new Error(`devicectl installation failed: ${devicectlError.message}`);}progress.complete("install");progress.start("launch");try{// Try to launch the app using devicectl
267
- await runCommand(`xcrun devicectl device process launch --device ${device.udid} "${APP_BUNDLE_ID}"`);progress.log("App launched successfully using devicectl","success");}catch(launchError){progress.log("App installation completed, but launch failed. You can manually launch the app on your device.","warning");progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`,"info");}progress.complete("launch");}catch(error){const currentStep=progress.currentStep.id;progress.fail(currentStep,error.message);progress.printTreeContent("Physical Device Installation Failed",["Installation failed. Common solutions:",{text:"Ensure device is connected and unlocked",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Trust the development certificate on your device",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Check that device is in developer mode",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Verify app bundle is valid",indent:1,prefix:"Ò””Ò”€ ",color:"yellow"},"","Device Details:",{text:`Device: ${device.name}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`UDID: ${device.udid}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`App Bundle: ${APP_PATH}`,indent:1,prefix:"Ò””Ò”€ ",color:"gray"}]);throw error;}}async function launchIOSSimulator(simulatorName){progress.start("launchSimulator");try{progress.log("Launching iOS Simulator...");// Get all simulators including both available and booted ones
268
- const allSimulatorInfo=execSync("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);// Search through all runtimes and their devices
269
- let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;let foundDevices=[];// Iterate through all runtimes and their devices
270
- Object.entries(simulatorsJson.devices).forEach(([runtime,devices])=>{devices.forEach(device=>{if(device.name===simulatorName){foundDevices.push({name:device.name,state:device.state,udid:device.udid,runtime:runtime});foundSimulator=device;foundSimulatorId=device.udid;isBooted=device.state==="Booted";}});});if(!foundSimulator){console.log(`Configured simulator "${simulatorName}" not found.`);return;}if(!isBooted){progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`,"warning");// Check if the simulator is in a valid state to be booted
271
- if(foundSimulator.state==="Shutdown"){progress.log(`Simulator is shutdown, attempting to boot...`,"info");try{await runCommand(`xcrun simctl boot ${foundSimulatorId}`);progress.log(`Successfully booted simulator`,"success");}catch(bootError){progress.log(`Boot failed: ${bootError.message}`,"error");// Try to find an alternative booted simulator
272
- progress.log(`Looking for any booted simulator as fallback...`,"info");const fallbackUUID=await getBootedSimulatorUUID(simulatorName);if(fallbackUUID){progress.log(`Found fallback booted simulator: ${fallbackUUID}`,"success");foundSimulatorId=fallbackUUID;}else{throw new Error(`Failed to boot simulator and no fallback available: ${bootError.message}`);}}}else{progress.log(`Simulator state is '${foundSimulator.state}', not 'Shutdown'. Checking if we can use it...`,"warning");if(foundSimulator.state==="Booted"){progress.log(`Wait, simulator is actually booted! Using it.`,"success");}else{throw new Error(`Simulator is in unexpected state: ${foundSimulator.state}`);}}}else{progress.log(`Simulator ${simulatorName} is already booted`,"success");}// Open Simulator.app and focus
273
- progress.log("Opening Simulator.app...");await runCommand("open -a Simulator");// Give the simulator a moment to open/focus
274
- await new Promise(resolve=>setTimeout(resolve,1000));// Activate the Simulator.app window to bring it to front
275
- await runCommand("osascript -e 'tell application \"Simulator\" to activate'");progress.log("iOS Simulator launched successfully.","success");progress.complete("launchSimulator");}catch(error){progress.fail("launchSimulator",error.message);// Show detailed troubleshooting info
276
- progress.printTreeContent("Simulator Troubleshooting",["iOS Simulator failed to launch. Common solutions:",{text:"Delete and recreate the simulator in Xcode",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Reset simulator content: Device > Erase All Content and Settings",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Check available simulators: xcrun simctl list devices",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Restart Xcode and Simulator app",indent:1,prefix:"Ò””Ò”€ ",color:"yellow"},"","Error Details:",{text:`Simulator: ${simulatorName}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`Error: ${error.message}`,indent:1,prefix:"Ò””Ò”€ ",color:"red"}]);console.error("Failed to launch iOS Simulator. Error:",error.message);process.exit(1);}}// Separate build function with proper device routing
277
- async function buildForIOS(pluginComposition={}){const originalDir=process.cwd();try{await generatePackageSwift(pluginComposition.iosDependencies);await updateXcodeProjectPackageDependencies();// Process notification assets
278
- progress.start("assets");await processNotificationAssets(WEBVIEW_CONFIG);await copyOfflinePage();progress.complete("assets");await generateXCConfig();await copySplashscreenAssets();await copyAppIcon();progress.log("Changing directory to: "+PROJECT_DIR,"info");process.chdir(PROJECT_DIR);// Force physical device detection (bypass shouldUsePhysicalDevice check)
279
- const physicalDevice=await detectPhysicalDevices();let APP_PATH;let targetInfo;if(physicalDevice){// Physical device workflow
280
- progress.log("ðŸ”Β₯ Building for physical device workflow","success");targetInfo={type:"physical",name:physicalDevice.name,udid:physicalDevice.udid};await cleanBuildArtifacts();progress.start("build");try{await buildProjectForPhysicalDevice(SCHEME_NAME,APP_BUNDLE_ID,path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData"),PROJECT_NAME,physicalDevice);progress.complete("build");}catch(error){progress.fail("build",error.message);progress.printTreeContent("Physical Device Build Failed",["Build failed. Please check:",{text:"Code signing certificates are properly installed",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Provisioning profile matches your bundle ID",indent:1,prefix:"Ò”œÒ”€ ",color:"yellow"},{text:"Device is connected and trusted",indent:1,prefix:"Ò””Ò”€ ",color:"yellow"}]);throw error;}progress.start("findApp");try{APP_PATH=await findPhysicalDeviceAppPath();progress.log("Found app at: "+APP_PATH,"success");progress.complete("findApp");}catch(error){progress.fail("findApp",error.message);throw error;}await installAndLaunchOnPhysicalDevice(APP_PATH,physicalDevice);}else{// Simulator workflow (with moveAppToBuildOutput improvement)
281
- progress.log("Γ°ΕΈβ€œΒ± Building for simulator workflow","info");targetInfo={type:"simulator",name:IPHONE_MODEL};await launchIOSSimulator(IPHONE_MODEL);await cleanBuildArtifacts();await buildXcodeProject();APP_PATH=await findAppPath();progress.log("Found app at: "+APP_PATH,"success");await installAndLaunchApp(APP_PATH);// Move app to organized build output directory
282
- const MOVED_APP_PATH=await moveAppToBuildOutput(APP_PATH);APP_PATH=MOVED_APP_PATH;}progress.printTreeContent("Build Summary",["Build completed successfully:",{text:`Target: ${targetInfo.type==="physical"?"Γ°ΕΈβ€œΒ± Physical Device":"Γ°ΕΈβ€œΒ± Simulator"}`,indent:1,prefix:"Ò”œÒ”€ ",color:"green"},{text:`Device: ${targetInfo.name}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`App Path: ${APP_PATH}`,indent:1,prefix:"Ò”œÒ”€ ",color:"gray"},{text:`URL: ${url}`,indent:1,prefix:"Ò””Ò”€ ",color:"gray"}]);return{success:true,targetInfo,appPath:APP_PATH};}catch(error){progress.log("Build failed: "+error.message,"error");throw error;}finally{process.chdir(originalDir);}}async function copySplashscreenAssets(){try{const publicDir=`${process.env.PWD}/public/ios`;const assetsDir=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;// Check if splash screen is configured
283
- if(!WEBVIEW_CONFIG.splashScreen){progress.log("No splash screen configuration found, skipping asset copy","info");return;}// Look for splash screen image in public folder (similar to Android)
284
- const imageExtensions=["png","jpg","jpeg"];let splashImageFound=false;for(const ext of imageExtensions){const sourcePath=`${publicDir}/splashscreen.${ext}`;if(fs.existsSync(sourcePath)){// Create launchscreen.imageset directory in Assets.xcassets
285
- const imagesetDir=`${assetsDir}/launchscreen.imageset`;if(!fs.existsSync(imagesetDir)){fs.mkdirSync(imagesetDir,{recursive:true});}// Copy the image to the imageset with a standard name
286
- const destinationPath=`${imagesetDir}/launchscreen.${ext}`;fs.copyFileSync(sourcePath,destinationPath);// Create Contents.json for the imageset
287
- const contentsJson={images:[{filename:`launchscreen.${ext}`,idiom:"universal",scale:"1x"}],info:{author:"xcode",version:1}};fs.writeFileSync(`${imagesetDir}/Contents.json`,JSON.stringify(contentsJson,null,2));progress.log(`Created launch screen imageset: launchscreen.${ext}`,"success");splashImageFound=true;break;}}if(!splashImageFound){progress.log("No custom splash screen image found in public folder","info");progress.log("Supported formats: splashscreen.png, splashscreen.jpg, splashscreen.jpeg","info");}}catch(error){progress.log(`Warning: Error copying splash screen assets: ${error.message}`,"warning");}}async function copyAppIcon(){try{const publicDir=`${process.env.PWD}/public/ios/appIcons`;const assetsDir=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const iconSetDir=`${assetsDir}/AppIcon.appiconset`;// Check if public directory exists
288
- if(!fs.existsSync(publicDir)){progress.log("Public directory not found, skipping app icon copy","info");return;}// Define iPhone icon sizes with their configurations
289
- const iconSizes=[{size:"20x20",idiom:"iphone",scale:"2x"},{size:"20x20",idiom:"iphone",scale:"3x"},{size:"29x29",idiom:"iphone",scale:"2x"},{size:"29x29",idiom:"iphone",scale:"3x"},{size:"40x40",idiom:"iphone",scale:"2x"},{size:"40x40",idiom:"iphone",scale:"3x"},{size:"60x60",idiom:"iphone",scale:"2x"},{size:"60x60",idiom:"iphone",scale:"3x"},{size:"1024x1024",idiom:"ios-marketing",scale:"1x"}];const imageExtensions=["png","jpg","jpeg"];// Recursively find all image files in a directory
290
- const findImagesRecursively=(dir,extensions)=>{let results=[];try{const items=fs.readdirSync(dir);for(const item of items){const fullPath=`${dir}${path.sep}${item}`;const stat=fs.statSync(fullPath);if(stat.isDirectory()){results=results.concat(findImagesRecursively(fullPath,extensions));}else if(stat.isFile()){const ext=path.extname(item).toLowerCase().slice(1);if(extensions.includes(ext)){results.push(fullPath);}}}}catch(err){// Ignore directory read errors
291
- }return results;};// Get all image files from public directory
292
- const allImages=findImagesRecursively(publicDir,imageExtensions);const foundIcons=[];// Create icon set directory
293
- if(!fs.existsSync(iconSetDir)){fs.mkdirSync(iconSetDir,{recursive:true});}// Load existing Contents.json or create new
294
- const contentsPath=`${iconSetDir}/Contents.json`;let contents;if(fs.existsSync(contentsPath)){try{contents=JSON.parse(fs.readFileSync(contentsPath,"utf8"));}catch{contents=null;}}if(!contents||!Array.isArray(contents.images)){contents={images:[],info:{author:"xcode",version:1}};}// Map to track which icons we've added
295
- const addedIcons=new Set();// Search for icons matching the expected sizes
296
- for(const iconConfig of iconSizes){const{size,idiom,scale}=iconConfig;// Expected filename pattern: icon-{size}-{scale}
297
- // Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png
298
- const expectedName=`icon-${size}-${scale}`;let foundImage=null;// Search for matching file with any supported extension
299
- for(const ext of imageExtensions){const matchingImage=allImages.find(imgPath=>{const basename=path.basename(imgPath,`.${ext}`);return basename===expectedName;});if(matchingImage){foundImage={path:matchingImage,ext};break;}}if(foundImage){const filename=`${expectedName}.${foundImage.ext}`;const destinationPath=`${iconSetDir}/${filename}`;// Copy the icon
300
- fs.copyFileSync(foundImage.path,destinationPath);foundIcons.push({size,scale,filename,idiom});// Create unique key for this icon entry
301
- const iconKey=`${size}-${idiom}-${scale}`;addedIcons.add(iconKey);// Remove existing entry with same size/idiom/scale
302
- contents.images=contents.images.filter(img=>`${img.size}-${img.idiom}-${img.scale}`!==iconKey);// Add new entry
303
- contents.images.push({size,idiom,scale,filename});}}if(foundIcons.length>0){// Write updated Contents.json
304
- fs.writeFileSync(contentsPath,JSON.stringify(contents,null,2));progress.log(`Updated AppIcon.appiconset with ${foundIcons.length} icon(s):`,"success");foundIcons.forEach(icon=>{progress.log(` β€’ ${icon.size} @${icon.scale} (${icon.idiom})`,"info");});}else{progress.log("No app icon files found in public folder","info");progress.log("Expected naming pattern: icon-{size}-{scale}.{ext}","info");progress.log("Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png","info");}}catch(error){progress.log(`Warning: Error copying app icons: ${error.message}`,"warning");}}async function main(){try{progress.log("Starting build process...","info");const pluginConfig=resolvePluginConfig(WEBVIEW_CONFIG);const pluginComposition=composeIosPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),iosProjectPath:PROJECT_DIR,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await generateConfigConstants();await updateInfoPlist(pluginComposition);await updateEntitlements(pluginComposition);await syncPluginResources(pluginComposition);await buildForIOS(pluginComposition);}catch(error){progress.log("Build failed: "+error.message,"error");process.exit(1);}process.exit(0);}main();
1
+ "use strict";const path=require("path");const{createIosBuild,pwd}=require("./buildIos/index.js");const{composeIosPlugins}=require("./pluginComposerIos.js");const{resolveInternalPluginsRoot,resolvePluginConfig}=require("./internalPluginUtils.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);async function main(){const build=createIosBuild({WEBVIEW_CONFIG,BUILD_OUTPUT_PATH});const{progress,generateConfigConstants,updateInfoPlist,updateEntitlements,syncPluginResources,buildForIOS,PROJECT_DIR}=build;try{progress.log("Starting build process...","info");const pluginConfig=resolvePluginConfig(WEBVIEW_CONFIG);const pluginComposition=composeIosPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),iosProjectPath:PROJECT_DIR,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await generateConfigConstants();await updateInfoPlist(pluginComposition);await updateEntitlements(pluginComposition);await syncPluginResources(pluginComposition);await buildForIOS(pluginComposition);}catch(error){progress.log("Build failed: "+error.message,"error");process.exit(1);}process.exit(0);}if(require.main===module){main();}// Legacy re-exports for any tooling that imports from this file directly
2
+ module.exports={createIosBuild,pwd};
@@ -0,0 +1,8 @@
1
+ "use strict";const fs=require("fs");const path=require("path");const crypto=require("crypto");const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const NOTIFICATION_ICONS=[{sourceName:"notification-icon",resourceName:"NotificationIcon"},{sourceName:"notification-large",resourceName:"NotificationLargeIcon"}];const NOTIFICATION_SOUNDS=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}module.exports=function createAssetsPhase(ctx){const{WEBVIEW_CONFIG,PROJECT_DIR,PROJECT_NAME,PUBLIC_PATH,progress,getXcodeProjectFilePath}=ctx;// ─── Google Services ─────────────────────────────────────────────────────
2
+ async function addGoogleServicesPlistToXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{progress.log("Xcode project file not found while adding GoogleService-Info.plist","warning");return;}if(projectContent.includes(`/* ${GOOGLE_SERVICES_FILENAME} */`)){progress.log("GoogleService-Info.plist already registered in Xcode project","info");return;}const fileRefId=generateProjectObjectId(GOOGLE_SERVICES_FILENAME,"_ref");const buildFileId=generateProjectObjectId(GOOGLE_SERVICES_FILENAME,"_build");const fileRefEntry=`\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ${GOOGLE_SERVICES_FILENAME}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);const buildFileEntry=`\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent))projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */,`);const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent))projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */,`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Registered GoogleService-Info.plist with Xcode project","success");}catch(error){progress.log(`Warning: Failed to register GoogleService-Info.plist in Xcode project: ${error.message}`,"warning");}}async function removeGoogleServicesPlistFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{return;}const regexes=[new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} \\*\\/ = \\{isa = PBXFileReference;[^}]*\\};\\n`,"g"),new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} in Resources \\*\\/ = \\{isa = PBXBuildFile;[^}]*\\};\\n`,"g"),new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} \\*\\/,\\n`,"g"),new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${GOOGLE_SERVICES_FILENAME} in Resources \\*\\/,\\n`,"g")];let modified=false;for(const regex of regexes){if(regex.test(projectContent)){projectContent=projectContent.replace(regex,"");modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed GoogleService-Info.plist references from Xcode project","info");}}catch(error){progress.log(`Warning: Failed to clean GoogleService-Info.plist from Xcode project: ${error.message}`,"warning");}}async function handleGoogleServicesPlist(){try{const rootPath=`${process.cwd()}/GoogleService-Info.plist`;const iosPath=`${PROJECT_DIR}/${PROJECT_NAME}/GoogleService-Info.plist`;if(fs.existsSync(rootPath)){progress.log("Found GoogleService-Info.plist in root directory","info");const targetDir=path.dirname(iosPath);if(!fs.existsSync(targetDir))fs.mkdirSync(targetDir,{recursive:true});fs.copyFileSync(rootPath,iosPath);progress.log("Copied GoogleService-Info.plist to iOS project","success");await addGoogleServicesPlistToXcodeProject();return true;}else if(fs.existsSync(iosPath)){progress.log("GoogleService-Info.plist already exists in iOS project","info");await addGoogleServicesPlistToXcodeProject();return true;}else{await removeGoogleServicesPlistFromXcodeProject();progress.log("GoogleService-Info.plist not found - Firebase push notifications will not work","warning");progress.log("Place GoogleService-Info.plist in project root","info");return false;}}catch(error){const rootPath=`${process.cwd()}/GoogleService-Info.plist`;if(error.code==="EACCES"&&fs.existsSync(rootPath))throw new Error(`Permission denied copying GoogleService-Info.plist: ${error.message}`);await removeGoogleServicesPlistFromXcodeProject();progress.log(`Warning: Error handling GoogleService-Info.plist: ${error.message}`,"warning");return false;}}// ─── Notification assets ─────────────────────────────────────────────────
3
+ async function validateNotificationAsset(filePath,assetType){const stats=fs.statSync(filePath);const fileSizeKB=stats.size/1024;if(assetType==="icon"&&fileSizeKB>100)progress.log(`Warning: Notification icon ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <100KB for optimal performance)`,"warning");else if(assetType==="sound"&&fileSizeKB>1024)progress.log(`Warning: Notification sound ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <1MB)`,"warning");}async function processNotificationIcons(){try{const assetsPath=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const imageFormats=["png","jpg","jpeg","webp"];let iconsProcessed=0;if(!fs.existsSync(PUBLIC_PATH)){progress.log(`Public directory not found at ${PUBLIC_PATH}`,"info");return 0;}if(!fs.existsSync(assetsPath))fs.mkdirSync(assetsPath,{recursive:true});for(const icon of NOTIFICATION_ICONS){const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(fs.existsSync(imagesetPath)){fs.rmSync(imagesetPath,{recursive:true,force:true});progress.log(`Removed existing ${icon.resourceName}.imageset`,"info");}}for(const icon of NOTIFICATION_ICONS){for(const format of imageFormats){const iconImagePath=`${PUBLIC_PATH}/${icon.sourceName}.${format}`;if(fs.existsSync(iconImagePath)){validateNotificationAsset(iconImagePath,"icon");const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(!fs.existsSync(imagesetPath))fs.mkdirSync(imagesetPath,{recursive:true});fs.copyFileSync(iconImagePath,`${imagesetPath}/${icon.sourceName}.${format}`);const contentsJson={images:[{filename:`${icon.sourceName}.${format}`,idiom:"universal",scale:"1x"},{idiom:"universal",scale:"2x"},{idiom:"universal",scale:"3x"}],info:{author:"xcode",version:1}};fs.writeFileSync(`${imagesetPath}/Contents.json`,JSON.stringify(contentsJson,null,2));progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}`,"success");iconsProcessed++;break;}}}if(iconsProcessed>0)progress.log(`Processed ${iconsProcessed} notification icon(s) from public/`,"success");else progress.log("No notification icons found in public/ - using default bell icon","info");return iconsProcessed;}catch(error){if(error.code==="EACCES")throw new Error(`Permission denied accessing notification icons: ${error.message}. Check directory permissions.`);if(error.code==="ENOSPC")throw new Error(`Insufficient disk space to process notification icons: ${error.message}`);progress.log(`Warning: Could not process notification icons: ${error.message}`,"warning");return 0;}}async function processNotificationSounds(){try{const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];let soundsProcessed=0;if(!fs.existsSync(PUBLIC_PATH)){progress.log(`Public directory not found at ${PUBLIC_PATH}`,"info");return 0;}for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const existingSoundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(existingSoundPath)){fs.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${PUBLIC_PATH}/${sound.sourceName}.${format}`;if(fs.existsSync(soundPath)){validateNotificationAsset(soundPath,"sound");fs.copyFileSync(soundPath,`${bundlePath}/${sound.resourceName}.${format}`);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");soundsProcessed++;break;}}}if(soundsProcessed>0)progress.log(`Processed ${soundsProcessed} notification sound(s) from public/`,"success");else progress.log("No notification sounds found in public/ - using system default sounds","info");return soundsProcessed;}catch(error){if(error.code==="EACCES")throw new Error(`Permission denied accessing notification sounds: ${error.message}. Check directory permissions.`);if(error.code==="ENOSPC")throw new Error(`Insufficient disk space to process notification sounds: ${error.message}`);progress.log(`Warning: Could not process notification sounds: ${error.message}`,"warning");return 0;}}async function removeSoundFilesFromXcodeProject(){try{const projectFilePath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
4
+ let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{return;}let modified=false;for(const sound of NOTIFICATION_SOUNDS){const p=sound.resourceName;const regexes=[new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${p}\\.[a-z0-9]+ \\*\\/ = \\{isa = PBXFileReference;[^}]*\\};\\n`,"g"),new RegExp(`\\t\\t[A-F0-9]+ \\/\\* ${p}\\.[a-z0-9]+ in Resources \\*\\/ = \\{isa = PBXBuildFile;[^}]*\\};\\n`,"g"),new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${p}\\.[a-z0-9]+ \\*\\/,\\n`,"g"),new RegExp(`\\t\\t\\t\\t[A-F0-9]+ \\/\\* ${p}\\.[a-z0-9]+ in Resources \\*\\/,\\n`,"g")];for(const regex of regexes){if(regex.test(projectContent)){projectContent=projectContent.replace(regex,"");modified=true;}}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed notification sounds from Xcode project","success");}}catch(error){progress.log(`Warning: Could not remove sound files from Xcode project: ${error.message}`,"warning");}}async function addSoundFilesToXcodeProject(){try{const projectFilePath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
5
+ const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];const soundFiles=[];for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(soundPath)){soundFiles.push({filename:`${sound.resourceName}.${format}`,resourceName:sound.resourceName,format});break;}}}if(soundFiles.length===0){progress.log("No sound files to add to Xcode project","info");return;}let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{throw new Error(`Xcode project file not found at: ${projectFilePath}`);}for(const soundFile of soundFiles){const fileRefId=generateProjectObjectId(soundFile.filename,"_ref");const buildFileId=generateProjectObjectId(soundFile.filename,"_build");if(projectContent.includes(`/* ${soundFile.filename} */`)){progress.log(`Sound ${soundFile.filename} already in Xcode project`,"info");continue;}progress.log(`Adding ${soundFile.filename} to Xcode project`,"info");const fileRefEntry=`\t\t${fileRefId} /* ${soundFile.filename} */ = {isa = PBXFileReference; lastKnownFileType = audio.${soundFile.format==="mp3"?"mp3":"wav"}; path = ${soundFile.filename}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);const buildFileEntry=`\t\t${buildFileId} /* ${soundFile.filename} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${soundFile.filename} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent))projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${soundFile.filename} */,`);const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent))projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${soundFile.filename} in Resources */,`);progress.log(`βœ… Added ${soundFile.filename} to Xcode project`,"success");}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${soundFiles.length} sound file(s) in Xcode project`,"success");}catch(error){progress.log(`Warning: Could not add sound files to Xcode project: ${error.message}`,"warning");throw error;}}async function cleanupNotificationAssets(removeFirebaseConfig=false){try{const assetsPath=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const bundlePath=`${PROJECT_DIR}/${PROJECT_NAME}`;const audioFormats=["mp3","wav","m4a","caf"];for(const icon of NOTIFICATION_ICONS){const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(fs.existsSync(imagesetPath)){fs.rmSync(imagesetPath,{recursive:true,force:true});progress.log(`Removed ${icon.resourceName}.imageset`,"info");}}for(const sound of NOTIFICATION_SOUNDS){for(const format of audioFormats){const soundPath=`${bundlePath}/${sound.resourceName}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);progress.log(`Removed ${sound.resourceName}.${format}`,"info");}}}await removeSoundFilesFromXcodeProject();if(removeFirebaseConfig){const iosGoogleServicesPath=`${bundlePath}/${GOOGLE_SERVICES_FILENAME}`;if(fs.existsSync(iosGoogleServicesPath)){fs.unlinkSync(iosGoogleServicesPath);progress.log("Removed GoogleService-Info.plist from iOS project directory","info");}await removeGoogleServicesPlistFromXcodeProject();}progress.log("Cleaned up notification assets","success");}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}async function processNotificationAssets(webviewConfig){const hasNotificationConfig=!!webviewConfig.notifications?.enabled;try{await cleanupNotificationAssets(!hasNotificationConfig);if(!hasNotificationConfig){progress.log("Notifications disabled - skipped asset processing","info");return;}const hasGoogleServices=await handleGoogleServicesPlist();if(!hasGoogleServices)progress.log("Continuing without Firebase - only local notifications will work","warning");const iconsProcessed=await processNotificationIcons();const soundsProcessed=await processNotificationSounds();if(soundsProcessed>0)await addSoundFilesToXcodeProject();const totalAssets=iconsProcessed+soundsProcessed;if(totalAssets>0)progress.log(`Notification asset processing completed: ${totalAssets} asset(s) processed`,"success");else progress.log("No notification assets found - using system defaults","info");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}// ─── Offline page ─────────────────────────────────────────────────────────
6
+ async function addOfflinePageToXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();const offlineFileName="offline.html";const offlineRelativePath="offline/offline.html";let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{progress.log("Xcode project file not found while adding offline.html","warning");return;}if(projectContent.includes(`/* ${offlineFileName} */`)){progress.log("offline.html already registered in Xcode project","info");return;}const fileRefId=generateProjectObjectId(offlineFileName,"_ref");const buildFileId=generateProjectObjectId(offlineFileName,"_build");const fileRefEntry=`\t\t${fileRefId} /* ${offlineFileName} */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = ${offlineRelativePath}; sourceTree = "<group>"; };`;projectContent=projectContent.replace(/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`);const buildFileEntry=`\t\t${buildFileId} /* ${offlineFileName} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${offlineFileName} */; };`;projectContent=projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`);const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;if(groupPattern.test(projectContent))projectContent=projectContent.replace(groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${offlineFileName} */,`);const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;if(resourcesPattern.test(projectContent))projectContent=projectContent.replace(resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${offlineFileName} in Resources */,`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Registered offline.html with Xcode project","success");}catch(error){progress.log(`Warning: Failed to register offline.html in Xcode project: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=path.join(PUBLIC_PATH,"offline.html");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
7
+ const destDir=`${PROJECT_DIR}/${PROJECT_NAME}/offline`;if(!fs.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return false;}fs.mkdirSync(destDir,{recursive:true});fs.copyFileSync(sourcePath,`${destDir}/offline.html`);progress.log("offline.html copied to iOS bundle","success");await addOfflinePageToXcodeProject();return true;}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");return false;}}// ─── Splash screen & app icon ─────────────────────────────────────────────
8
+ async function copySplashscreenAssets(){try{const publicDir=`${process.cwd()}/public/ios`;const assetsDir=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;if(!WEBVIEW_CONFIG.splashScreen){progress.log("No splash screen configuration found, skipping asset copy","info");return;}const imageExtensions=["png","jpg","jpeg"];let splashImageFound=false;for(const ext of imageExtensions){const sourcePath=`${publicDir}/splashscreen.${ext}`;if(fs.existsSync(sourcePath)){const imagesetDir=`${assetsDir}/launchscreen.imageset`;if(!fs.existsSync(imagesetDir))fs.mkdirSync(imagesetDir,{recursive:true});fs.copyFileSync(sourcePath,`${imagesetDir}/launchscreen.${ext}`);const contentsJson={images:[{filename:`launchscreen.${ext}`,idiom:"universal",scale:"1x"}],info:{author:"xcode",version:1}};fs.writeFileSync(`${imagesetDir}/Contents.json`,JSON.stringify(contentsJson,null,2));progress.log(`Created launch screen imageset: launchscreen.${ext}`,"success");splashImageFound=true;break;}}if(!splashImageFound){progress.log("No custom splash screen image found in public folder","info");progress.log("Supported formats: splashscreen.png, splashscreen.jpg, splashscreen.jpeg","info");}}catch(error){progress.log(`Warning: Error copying splash screen assets: ${error.message}`,"warning");}}async function copyAppIcon(){try{const publicDir=`${process.cwd()}/public/ios/appIcons`;const assetsDir=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const iconSetDir=`${assetsDir}/AppIcon.appiconset`;if(!fs.existsSync(publicDir)){progress.log("Public directory not found, skipping app icon copy","info");return;}const iconSizes=[{size:"20x20",idiom:"iphone",scale:"2x"},{size:"20x20",idiom:"iphone",scale:"3x"},{size:"29x29",idiom:"iphone",scale:"2x"},{size:"29x29",idiom:"iphone",scale:"3x"},{size:"40x40",idiom:"iphone",scale:"2x"},{size:"40x40",idiom:"iphone",scale:"3x"},{size:"60x60",idiom:"iphone",scale:"2x"},{size:"60x60",idiom:"iphone",scale:"3x"},{size:"1024x1024",idiom:"ios-marketing",scale:"1x"}];const imageExtensions=["png","jpg","jpeg"];const findImagesRecursively=(dir,extensions)=>{let results=[];try{for(const item of fs.readdirSync(dir)){const fullPath=`${dir}${path.sep}${item}`;const stat=fs.statSync(fullPath);if(stat.isDirectory())results=results.concat(findImagesRecursively(fullPath,extensions));else if(stat.isFile()&&extensions.includes(path.extname(item).toLowerCase().slice(1)))results.push(fullPath);}}catch(err){/* ignore */}return results;};const allImages=findImagesRecursively(publicDir,imageExtensions);const foundIcons=[];if(!fs.existsSync(iconSetDir))fs.mkdirSync(iconSetDir,{recursive:true});const contentsPath=`${iconSetDir}/Contents.json`;let contents;try{contents=JSON.parse(fs.readFileSync(contentsPath,"utf8"));}catch{contents=null;}if(!contents||!Array.isArray(contents.images))contents={images:[],info:{author:"xcode",version:1}};for(const{size,idiom,scale}of iconSizes){const expectedName=`icon-${size}-${scale}`;let foundImage=null;for(const ext of imageExtensions){const matchingImage=allImages.find(imgPath=>path.basename(imgPath,`.${ext}`)===expectedName);if(matchingImage){foundImage={path:matchingImage,ext};break;}}if(foundImage){const filename=`${expectedName}.${foundImage.ext}`;fs.copyFileSync(foundImage.path,`${iconSetDir}/${filename}`);foundIcons.push({size,scale,filename,idiom});const iconKey=`${size}-${idiom}-${scale}`;contents.images=contents.images.filter(img=>`${img.size}-${img.idiom}-${img.scale}`!==iconKey);contents.images.push({size,idiom,scale,filename});}}if(foundIcons.length>0){fs.writeFileSync(contentsPath,JSON.stringify(contents,null,2));progress.log(`Updated AppIcon.appiconset with ${foundIcons.length} icon(s):`,"success");foundIcons.forEach(icon=>progress.log(` β€’ ${icon.size} @${icon.scale} (${icon.idiom})`,"info"));}else{progress.log("No app icon files found in public folder","info");progress.log("Expected naming pattern: icon-{size}-{scale}.{ext}","info");progress.log("Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png","info");}}catch(error){progress.log(`Warning: Error copying app icons: ${error.message}`,"warning");}}return{processNotificationAssets,copyOfflinePage,copySplashscreenAssets,copyAppIcon};};