catalyst-core 0.1.0-beta.5 β†’ 0.1.0-beta.7

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,306 +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){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename uses path.basename and stays under the fixed project directory.
3
- 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
4
- 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
5
- 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
6
- 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
7
- 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
8
- 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
9
- 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
10
- 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
11
- 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
12
- let packageContent=`// swift-tools-version: 5.9
13
- // Auto-generated Package.swift - DO NOT EDIT MANUALLY
14
- // Generated based on config: notifications.enabled = ${isNotificationsEnabled}
15
- import PackageDescription
16
-
17
- let package = Package(
18
- name: "iosnativeWebView",
19
- platforms: [.iOS(.v17)],
20
- products: [
21
- .library(name: "CatalystCore", targets: ["CatalystCore"])`;// Add CatalystNotifications product only if enabled
22
- if(isNotificationsEnabled){packageContent+=`,
23
- .library(name: "CatalystNotifications", targets: ["CatalystNotifications"])`;}packageContent+=`
24
- ],
25
- dependencies: [
26
- ${formatSwiftPackageEntries(packageDependencies).join(",\n")}`;packageContent+=`
27
- ],
28
- targets: [
29
- // Core functionality (WebView, bridge, utils, constants)
30
- // App-level files (AppDelegate, ContentView) are in iosnativeWebView/ directory
31
- .target(
32
- name: "CatalystCore",
33
- dependencies: [
34
- ${formatSwiftProductEntries(coreDependencies).join(",\n")}
35
- ],
36
- path: "Sources/Core"
37
- )`;// Add CatalystNotifications target only if enabled
38
- if(isNotificationsEnabled){packageContent+=`,
39
- // Notifications functionality (optional, includes Firebase)
40
- .target(
41
- name: "CatalystNotifications",
42
- dependencies: [
43
- "CatalystCore",
44
- ${formatSwiftProductEntries(notificationsDependencies).join(",\n")}
45
- ],
46
- path: "Sources/CatalystNotifications"
47
- )`;}packageContent+=`
48
- ]
49
- )
50
- `;// Write the generated Package.swift
51
- fs.writeFileSync(targetPath,packageContent,"utf8");progress.log(`Generated Package.swift with ${isNotificationsEnabled?"notifications enabled":"notifications disabled"}`,"success");// Save hash for future comparison
52
- fs.writeFileSync(hashFilePath,configHash);// Force Xcode to re-resolve packages (clean caches to avoid stale pins)
53
- progress.log("Resolving package dependencies...","info");try{// Clear SPM build and resolution caches
54
- 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
55
- 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
56
- 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
57
- const NOTIF_BUILD_FILE_ID="C99974352E97D56900C25611";const NOTIF_PRODUCT_ID="C99974362E97D56900C25611";// Check if CatalystNotifications is already in the project
58
- const hasNotifications=projectContent.includes("/* CatalystNotifications */");if(isNotificationsEnabled&&!hasNotifications){progress.log("Adding CatalystNotifications to Xcode project","info");// 1. Add to PBXBuildFile section (after CatalystCore)
59
- 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
60
- 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
61
- projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);// 4. Add to XCSwiftPackageProductDependency section
62
- 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
63
- 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}`);}// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The normalized path is validated above and containment is enforced immediately below.
64
- 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
65
- 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
66
- if(key==="cachePattern"){if(typeof value==="string"){// Convert single string to array
67
- 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
68
- 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
69
- 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
70
- 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
71
- 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
72
- let configContent=`// This file is auto-generated. Do not edit.
73
- import Foundation
74
-
75
- public enum ConfigConstants {
76
- public static let url = "${url}"`;// Track keys already added to avoid duplicates
77
- const addedKeys=new Set();// Process all properties from WEBVIEW_CONFIG
78
- if(WEBVIEW_CONFIG&&typeof WEBVIEW_CONFIG==="object"){for(const[key,value]of Object.entries(WEBVIEW_CONFIG)){// Skip 'ios' and 'android' keys to avoid duplication
79
- 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
80
- 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
81
- if(addedKeys.has(key))continue;configContent+="\n"+generateSwiftProperty(key,value);}}// Add URL whitelisting configuration if it exists
82
- if(WEBVIEW_CONFIG.accessControl){const accessControl=WEBVIEW_CONFIG.accessControl;configContent+=`
83
- public static let accessControlEnabled = ${accessControl.enabled||false}`;if(accessControl.allowedUrls&&Array.isArray(accessControl.allowedUrls)){const allowedUrls=accessControl.allowedUrls.map(url=>`"${url}"`).join(", ");configContent+=`
84
- public static let allowedUrls: [String] = [${allowedUrls}]`;}else if(accessControl.allowedUrls&&typeof accessControl.allowedUrls==="string"){// Handle comma-separated string format
85
- const allowedUrls=accessControl.allowedUrls.split(",").map(url=>url.trim()).filter(url=>url.length>0).map(url=>`"${url}"`).join(", ");configContent+=`
86
- public static let allowedUrls: [String] = [${allowedUrls}]`;}}else{// Default values when no access control is configured
87
- configContent+=`
88
- public static let accessControlEnabled = false
89
- public static let allowedUrls: [String] = []`;}// Add splash screen configuration from WEBVIEW_CONFIG.splashScreen
90
- const splashConfig=WEBVIEW_CONFIG.splashScreen;if(splashConfig){configContent+=`
91
-
92
- // Splash Screen Configuration
93
- public static let splashScreenEnabled = true`;if(splashConfig.duration){// Convert milliseconds to seconds for iOS TimeInterval
94
- const durationInSeconds=splashConfig.duration/1000.0;configContent+=`
95
- public static let splashScreenDuration: TimeInterval? = ${durationInSeconds}`;}else{configContent+=`
96
- public static let splashScreenDuration: TimeInterval? = nil`;}if(splashConfig.backgroundColor){configContent+=`
97
- public static let splashScreenBackgroundColor = "${splashConfig.backgroundColor}"`;}else{configContent+=`
98
- public static let splashScreenBackgroundColor = "#ffffff"`;}// Add splash screen image styling configuration
99
- const imageWidth=splashConfig.imageWidth||120;const imageHeight=splashConfig.imageHeight||120;const cornerRadius=splashConfig.cornerRadius||20;configContent+=`
100
- public static let splashScreenImageWidth: CGFloat = ${imageWidth}
101
- public static let splashScreenImageHeight: CGFloat = ${imageHeight}
102
- public static let splashScreenCornerRadius: CGFloat = ${cornerRadius}`;}else{configContent+=`
103
-
104
- // Splash Screen Configuration
105
- public static let splashScreenEnabled = false
106
- public static let splashScreenDuration: TimeInterval? = nil
107
- public static let splashScreenBackgroundColor = "#ffffff"
108
- public static let splashScreenImageWidth: CGFloat = 120
109
- public static let splashScreenImageHeight: CGFloat = 120
110
- public static let splashScreenCornerRadius: CGFloat = 20`;}// Ensure Notifications.enabled always exists (default to false if not configured)
111
- 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
112
- 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)
113
- 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
114
- configContent+=`
115
- }`;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
116
- // Notification asset definitions
117
- 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
118
- if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found GoogleService-Info.plist in root directory","info");// Create the directory if it doesn't exist
119
- const targetDir=path.dirname(iosGoogleServicesPath);if(!fs.existsSync(targetDir)){fs.mkdirSync(targetDir,{recursive:true});}// Copy the file to the iOS project
120
- 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
121
- 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)
122
- 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)
123
- 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
124
- 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
125
- if(!fs.existsSync(assetsPath)){fs.mkdirSync(assetsPath,{recursive:true});}// Remove existing notification icon imagesets to avoid conflicts when assets are updated
126
- 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
127
- 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
128
- validateNotificationAsset(iconImagePath,"icon");// Create imageset directory
129
- const imagesetPath=`${assetsPath}/${icon.resourceName}.imageset`;if(!fs.existsSync(imagesetPath)){fs.mkdirSync(imagesetPath,{recursive:true});}// Copy icon
130
- const destImagePath=`${imagesetPath}/${icon.sourceName}.${format}`;fs.copyFileSync(iconImagePath,destImagePath);// Create Contents.json for the imageset
131
- 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
132
- 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
133
- if(!fs.existsSync(PUBLIC_PATH)){progress.log(`Public directory not found at ${PUBLIC_PATH}`,"info");return 0;}// Remove existing notification sounds to avoid conflicts
134
- 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
135
- 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
136
- 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
137
- 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
138
- for(const sound of NOTIFICATION_SOUNDS){// Use the resourceName from each sound object
139
- const pattern=sound.resourceName;// Remove PBXFileReference entries
140
- 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
141
- 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
142
- 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
143
- 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
144
- 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
145
- 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
146
- 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
147
- 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
148
- 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
149
- 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
150
- 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)
151
- 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
152
- 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
153
- 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
154
- await cleanupNotificationAssets(!hasNotificationConfig);if(!hasNotificationConfig){progress.log("Notifications disabled - skipped asset processing","info");return;}// Handle GoogleService-Info.plist file for Firebase
155
- const hasGoogleServices=await handleGoogleServicesPlist();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}// Process notification assets
156
- const iconsProcessed=await processNotificationIcons();const soundsProcessed=await processNotificationSounds();// Add sound files to Xcode project so they're included in the bundle
157
- 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
158
- 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
159
- 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
160
- const xconfigContent=`// This file is auto-generated by buildAppIos.js. Do not edit.
161
- //
162
- // Shared configuration for both Debug and Release builds
163
- // All values are dynamically injected from config.json
164
- //
165
-
166
- // Product Configuration
167
- PRODUCT_BUNDLE_IDENTIFIER = ${bundleId}
168
- APP_DISPLAY_NAME = ${appName}
169
-
170
- // Version Information
171
- MARKETING_VERSION = ${marketingVersion}
172
- CURRENT_PROJECT_VERSION = ${buildNumber}
173
-
174
- // Development Team (for code signing)
175
- DEVELOPMENT_TEAM = ${teamId}
176
-
177
- // Swift Configuration
178
- SWIFT_VERSION = ${swiftVersion}
179
-
180
- // iOS Deployment Target (minimum iOS version)
181
- IPHONEOS_DEPLOYMENT_TARGET = ${deploymentTarget}
182
-
183
- // Common build settings
184
- TARGETED_DEVICE_FAMILY = 1,2
185
- ENABLE_PREVIEWS = YES
186
- SWIFT_EMIT_LOC_STRINGS = YES
187
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
188
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
189
-
190
- // Code signing
191
- CODE_SIGN_IDENTITY = Apple Development
192
- CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
193
- CODE_SIGN_STYLE = Manual
194
- PROVISIONING_PROFILE_SPECIFIER =
195
- PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = ${provisioningProfile}
196
-
197
- // Module settings
198
- CLANG_ENABLE_MODULES = NO
199
- GENERATE_INFOPLIST_FILE = YES
200
-
201
- // Run path search paths
202
- LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
203
-
204
- // Development assets
205
- DEVELOPMENT_ASSET_PATHS = "iosnativeWebView/Preview Content"
206
-
207
- // Privacy descriptions
208
- INFOPLIST_KEY_NSCameraUsageDescription = This app requires camera access to capture images
209
- INFOPLIST_KEY_NSLocalNetworkUsageDescription = Debug web content in Safari Web Inspector
210
-
211
- // UI Configuration
212
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES
213
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES
214
- INFOPLIST_KEY_UILaunchScreen_Generation = YES
215
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
216
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
217
- `;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
218
- 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
219
- 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
220
- if(!fs.existsSync(destinationDir)){fs.mkdirSync(destinationDir,{recursive:true});}// Copy the app to the destination using shell command
221
- 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
222
- 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
223
- const configuredUDID=iosConfig.deviceUDID;if(configuredUDID){progress.log(`Using configured device UDID: ${configuredUDID}`,"info");// Verify the configured device is actually connected
224
- try{const instrumentsOutput=execSync("instruments -s devices").toString();if(instrumentsOutput.includes(configuredUDID)){// Extract device name from instruments output
225
- 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
226
- // Try instruments first
227
- 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)
228
- 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
229
- 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
230
- 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
231
- 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
232
- 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)
233
- 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+)
234
- 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
235
- 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
236
- }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
237
- const bootedInfo=await getBootedSimulatorInfo();if(!bootedInfo){throw new Error("No booted simulator found");}// Use specific UDID to avoid multiple device conflicts
238
- const destinationWithUDID=`platform=iOS Simulator,id=${bootedInfo.udid}`;console.log(`Building with destination: ${destinationWithUDID}`);// Notifications are controlled via Package.swift - canImport() detects availability
239
- 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 \
240
- -scheme "${scheme}" \
241
- -sdk ${sdk} \
242
- -configuration Debug \
243
- -destination "${destinationWithUDID}" \
244
- PRODUCT_BUNDLE_IDENTIFIER="${bundleId}" \
245
- DEVELOPMENT_TEAM="" \
246
- CODE_SIGN_IDENTITY="" \
247
- CODE_SIGNING_REQUIRED=NO \
248
- CODE_SIGNING_ALLOWED=NO \
249
- ONLY_ACTIVE_ARCH=YES \
250
- BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products" \
251
- CONFIGURATION_BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products/Debug-iphonesimulator" \
252
- OS_ACTIVITY_MODE=debug \
253
- SWIFT_DEBUG_LOG=1 \
254
- build`;return runCommand(buildCommand,{maxBuffer:1024*1024*10});}// Physical Device Build Functions
255
- 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
256
- 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
257
- 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)
258
- const buildCommand=`xcodebuild \
259
- -scheme ${scheme} \
260
- -sdk iphoneos \
261
- -configuration Debug \
262
- -destination platform=iOS,id=${device.udid} \
263
- PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
264
- ONLY_ACTIVE_ARCH=YES \
265
- 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)
266
- 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
267
- 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)
268
- 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
269
- 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
270
- const allSimulatorInfo=execSync("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);// Search through all runtimes and their devices
271
- let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;let foundDevices=[];// Iterate through all runtimes and their devices
272
- 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
273
- 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
274
- 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
275
- progress.log("Opening Simulator.app...");await runCommand("open -a Simulator");// Give the simulator a moment to open/focus
276
- await new Promise(resolve=>setTimeout(resolve,1000));// Activate the Simulator.app window to bring it to front
277
- 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
278
- 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
279
- async function buildForIOS(pluginComposition={}){const originalDir=process.cwd();try{await generatePackageSwift(pluginComposition.iosDependencies);await updateXcodeProjectPackageDependencies();// Process notification assets
280
- 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)
281
- const physicalDevice=await detectPhysicalDevices();let APP_PATH;let targetInfo;if(physicalDevice){// Physical device workflow
282
- 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)
283
- 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
284
- 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
285
- 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)
286
- 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
287
- const imagesetDir=`${assetsDir}/launchscreen.imageset`;if(!fs.existsSync(imagesetDir)){fs.mkdirSync(imagesetDir,{recursive:true});}// Copy the image to the imageset with a standard name
288
- const destinationPath=`${imagesetDir}/launchscreen.${ext}`;fs.copyFileSync(sourcePath,destinationPath);// Create Contents.json for the imageset
289
- 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
290
- if(!fs.existsSync(publicDir)){progress.log("Public directory not found, skipping app icon copy","info");return;}// Define iPhone icon sizes with their configurations
291
- 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
292
- 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
293
- }return results;};// Get all image files from public directory
294
- const allImages=findImagesRecursively(publicDir,imageExtensions);const foundIcons=[];// Create icon set directory
295
- if(!fs.existsSync(iconSetDir)){fs.mkdirSync(iconSetDir,{recursive:true});}// Load existing Contents.json or create new
296
- 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
297
- const addedIcons=new Set();// Search for icons matching the expected sizes
298
- for(const iconConfig of iconSizes){const{size,idiom,scale}=iconConfig;// Expected filename pattern: icon-{size}-{scale}
299
- // Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png
300
- const expectedName=`icon-${size}-${scale}`;let foundImage=null;// Search for matching file with any supported extension
301
- 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
302
- fs.copyFileSync(foundImage.path,destinationPath);foundIcons.push({size,scale,filename,idiom});// Create unique key for this icon entry
303
- const iconKey=`${size}-${idiom}-${scale}`;addedIcons.add(iconKey);// Remove existing entry with same size/idiom/scale
304
- contents.images=contents.images.filter(img=>`${img.size}-${img.idiom}-${img.scale}`!==iconKey);// Add new entry
305
- contents.images.push({size,idiom,scale,filename});}}if(foundIcons.length>0){// Write updated Contents.json
306
- 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};};