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.
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +14 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +19 -12
- package/dist/native/androidSetup.js +3 -2
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +17 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -148
- package/dist/native/buildAppIos.js +2 -306
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +39 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +17 -9
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/server/renderer/handler.js +1 -1
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/mcp_v2/tools/knowledge.js +2 -2
- package/package.json +1 -1
|
@@ -1,306 +1,2 @@
|
|
|
1
|
-
"use strict";const
|
|
2
|
-
|
|
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};};
|