catalyst-core 0.1.0-beta.5 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/mcp_v2/tools/knowledge.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const os=require("os");const path=require("path");const{execSync}=require("child_process");const{runInteractiveCommand}=require("../utils.js");module.exports=function createBuildPhase(ctx){const{WEBVIEW_CONFIG,PROJECT_NAME,SCHEME_NAME,APP_BUNDLE_ID,IPHONE_MODEL,url,progress,runCommand}=ctx;// ─── Simulator helpers ────────────────────────────────────────────────────
|
|
2
|
+
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");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];}}}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 simulatorList=JSON.parse(execSync("xcrun simctl list devices --json").toString());for(const runtime in simulatorList.devices){for(const device of simulatorList.devices[runtime]){if(device.state==="Booted")return{udid:device.udid,name:device.name,runtime};}}return null;}catch(error){console.log("Failed to get booted simulator info:",error.message);return null;}}// ─── Clean ────────────────────────────────────────────────────────────────
|
|
3
|
+
async function cleanBuildArtifacts(){progress.start("clean");try{const xcuserdataPath=path.join(`${PROJECT_NAME}.xcodeproj`,`project.xcworkspace`,"xcuserdata");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
4
|
+
if(fs.existsSync(xcuserdataPath))await runCommand(`rm -rf "${xcuserdataPath}"`);const derivedDataPath=path.join(os.homedir(),"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);throw error;}}// ─── Build commands ───────────────────────────────────────────────────────
|
|
5
|
+
function makeBuildCommand(scheme,sdk,bundleId,derivedDataPath,projectName,udid,action){return`xcodebuild \
|
|
6
|
+
-scheme "${scheme}" \
|
|
7
|
+
-sdk ${sdk} \
|
|
8
|
+
-configuration Debug \
|
|
9
|
+
-destination "platform=iOS Simulator,id=${udid}" \
|
|
10
|
+
PRODUCT_BUNDLE_IDENTIFIER="${bundleId}" \
|
|
11
|
+
DEVELOPMENT_TEAM="" \
|
|
12
|
+
CODE_SIGN_IDENTITY="" \
|
|
13
|
+
CODE_SIGNING_REQUIRED=NO \
|
|
14
|
+
CODE_SIGNING_ALLOWED=NO \
|
|
15
|
+
ONLY_ACTIVE_ARCH=YES \
|
|
16
|
+
BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products" \
|
|
17
|
+
CONFIGURATION_BUILD_DIR="${derivedDataPath}/${projectName}-Build/Build/Products/Debug-iphonesimulator" \
|
|
18
|
+
OS_ACTIVITY_MODE=debug \
|
|
19
|
+
SWIFT_DEBUG_LOG=1 \
|
|
20
|
+
${action}`;}async function buildProject(scheme,sdk,destination,bundleId,derivedDataPath,projectName){const bootedInfo=await getBootedSimulatorInfo();if(!bootedInfo)throw new Error("No booted simulator found");console.log(`Building with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;progress.log(isNotificationsEnabled?"Building with notifications enabled (CatalystNotifications module included)":"Building without notifications (Firebase excluded)","info");return runCommand(makeBuildCommand(scheme,sdk,bundleId,derivedDataPath,projectName,bootedInfo.udid,"build"),{maxBuffer:1024*1024*10});}async function buildProjectForTesting(scheme,sdk,destination,bundleId,derivedDataPath,projectName){const bootedInfo=await getBootedSimulatorInfo();if(!bootedInfo)throw new Error("No booted simulator found");console.log(`Building for testing with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;progress.log(isNotificationsEnabled?"Building for testing with notifications enabled":"Building for testing without notifications","info");return runCommand(makeBuildCommand(scheme,sdk,bundleId,derivedDataPath,projectName,bootedInfo.udid,"build-for-testing"),{maxBuffer:1024*1024*10});}async function buildXcodeProject(){progress.start("build");try{const derivedDataPath=path.join(os.homedir(),"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"}]);throw error;}}// ─── Find / install / launch ──────────────────────────────────────────────
|
|
21
|
+
async function findAppPath(){progress.start("findApp");try{const DERIVED_DATA_DIR=path.join(os.homedir(),"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();// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
22
|
+
}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();// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
23
|
+
}catch(error){throw new Error("Could not locate built application");}}if(!APP_PATH)throw new Error("No .app file found");progress.complete("findApp");return APP_PATH;}catch(error){progress.fail("findApp",error.message);throw error;}}async function moveAppToBuildOutput(APP_PATH){const{BUILD_OUTPUT_PATH,iosConfig}=ctx;try{const buildType=iosConfig.buildType||"debug";const appName=iosConfig.appName||"app";const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,"-");const destinationDir=path.join(process.cwd(),BUILD_OUTPUT_PATH,"native","ios",currentDate,buildType);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
24
|
+
const destinationPath=path.join(destinationDir,`${appName}-${currentTime}.app`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
25
|
+
if(!fs.existsSync(destinationDir))fs.mkdirSync(destinationDir,{recursive:true});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 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);throw error;}}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;}// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
26
|
+
catch(error){if(i===29)throw new Error("Timeout: App installation took 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{console.log("App container not found");}throw 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();// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
27
|
+
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 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 focusSimulator(){console.log("Focusing on Simulator...");await runCommand(`osascript -e 'tell application "Simulator" to activate'`);}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);throw error;}}// ─── Physical device ──────────────────────────────────────────────────────
|
|
28
|
+
async function detectPhysicalDevices(){progress.start("deviceDetection");const{iosConfig}=ctx;try{progress.log("Scanning for connected physical devices...","info");let physicalDevices=[];const configuredUDID=iosConfig.deviceUDID;if(configuredUDID){progress.log(`Using configured device UDID: ${configuredUDID}`,"info");try{const instrumentsOutput=execSync("instruments -s devices").toString();if(instrumentsOutput.includes(configuredUDID)){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");}const tryInstruments=()=>{try{const out=execSync("instruments -s devices").toString();return out.split("\n").filter(line=>{const m=line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\](?:\s+\(Simulator\))?$/);return m&&!line.includes("(Simulator)");}).map(line=>{const[,name,version,udid]=line.match(/^(.+?)\s+\((\d+\.\d+(?:\.\d+)?)\)\s+\[([A-F0-9-]{36})\]/);return{name:name.trim(),version,udid,type:"physical"};});}catch{return[];}};physicalDevices=tryInstruments();if(physicalDevices.length>0)progress.log(`Found ${physicalDevices.length} physical device(s) via instruments`,"success");if(physicalDevices.length===0){try{const xcodebuildOutput=execSync(`xcodebuild -scheme "${SCHEME_NAME}" -showdestinations`).toString();// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
29
|
+
progress.log("Scanning xcodebuild destinations for physical devices...","info");for(const line of xcodebuildOutput.split("\n")){const m=line.match(/\{\s*platform:iOS,\s*arch:(\w+),\s*id:([A-F0-9-]+),\s*name:(.+?)\s*\}/);if(m){const[,arch,udid,name]=m;progress.log(`Found device candidate: ${name.trim()} - ${udid}`,"info");if(!udid.includes("placeholder")&&udid.length>20){progress.log(`✅ Valid physical device: ${name.trim()}`,"success");physicalDevices.push({name:name.trim(),version:"Unknown",udid,arch,type:"physical"});}else{progress.log(`❌ Skipping placeholder: ${name.trim()}`,"warning");}}}}catch(error){progress.log("xcodebuild destinations failed","warning");progress.log(`Error: ${error.message}`,"error");}}if(physicalDevices.length===0)physicalDevices=tryInstruments();if(physicalDevices.length===0){try{for(const line of execSync("xcrun devicectl list devices").toString().split("\n")){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{progress.log("devicectl command not available or failed","warning");}}if(physicalDevices.length>0){progress.log(`Found ${physicalDevices.length} physical device(s):`,"success");physicalDevices.forEach(d=>progress.log(` 📱 ${d.name} (${d.version||"Unknown iOS"}) - ${d.udid}`,"info"));progress.complete("deviceDetection");return physicalDevices[0];}else{progress.log("No physical devices detected","warning");progress.complete("deviceDetection");return null;}}catch(error){progress.fail("deviceDetection",error.message);return null;}}async function buildProjectForPhysicalDevice(scheme,bundleId,derivedDataPath,projectName,device){progress.log(`Building for physical device: ${device.name}`,"info");const projectPath=`${process.cwd()}/${projectName}.xcodeproj`;if(!fs.existsSync(projectPath))throw new Error(`Xcode project not found at: ${projectPath}. Current directory: ${process.cwd()}`);const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;progress.log(isNotificationsEnabled?"Building with notifications enabled":"Building without notifications","info");const buildCommand=`xcodebuild \
|
|
30
|
+
-scheme ${scheme} \
|
|
31
|
+
-sdk iphoneos \
|
|
32
|
+
-configuration Debug \
|
|
33
|
+
-destination platform=iOS,id=${device.udid} \
|
|
34
|
+
PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
|
|
35
|
+
ONLY_ACTIVE_ARCH=YES \
|
|
36
|
+
build`;progress.log(`Executing command: ${buildCommand}`,"info");return runCommand(buildCommand,{maxBuffer:1024*1024*10});}async function findPhysicalDeviceAppPath(){const DERIVED_DATA_DIR=path.join(os.homedir(),"Library/Developer/Xcode/DerivedData");let APP_PATH="";try{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();// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
37
|
+
}catch{progress.log("Primary app path search failed for physical device, trying fallback...","warning");}if(!APP_PATH){try{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();}// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
38
|
+
catch{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");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{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;}}// ─── Launch simulator ─────────────────────────────────────────────────────
|
|
39
|
+
async function launchIOSSimulator(simulatorName){progress.start("launchSimulator");try{progress.log("Launching iOS Simulator...");const simulatorsJson=JSON.parse(execSync("xcrun simctl list devices -j").toString());let foundSimulator=null,foundSimulatorId=null,isBooted=false;Object.entries(simulatorsJson.devices).forEach(([,devices])=>{devices.forEach(device=>{if(device.name===simulatorName){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");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");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");}progress.log("Opening Simulator.app...");await runCommand("open -a Simulator");await new Promise(resolve=>setTimeout(resolve,1000));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);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"}]);throw error;}}return{cleanBuildArtifacts,buildXcodeProject,buildProjectForTesting,findAppPath,moveAppToBuildOutput,installAndLaunchApp,detectPhysicalDevices,buildProjectForPhysicalDevice,findPhysicalDeviceAppPath,installAndLaunchOnPhysicalDevice,launchIOSSimulator,getBootedSimulatorUUID};};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");// ─── Pure helpers (no ctx dependency) ────────────────────────────────────────
|
|
2
|
+
function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined)return JSON.parse(JSON.stringify(incoming));if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key))continue;seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming))return existing;throw new Error(`Conflicting values for '${fieldName}' while composing iOS build metadata`);}function generateSwiftProperty(key,value,indent=" "){if(value===null||value===undefined)return`${indent}public static let ${key}: String? = nil`;if(key==="cachePattern"){if(typeof value==="string")return`${indent}public static let ${key}: [String] = ["${value}"]`;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`${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] = []`;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";return`${indent}public static let ${key}: [${arrayType}] = [${value.join(", ")}]`;}else if(typeof firstElement==="boolean"){return`${indent}public static let ${key}: [Bool] = [${value.join(", ")}]`;}else{const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="object"&&value!==null){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;}return`${indent}public static let ${key} = "${value}"`;}// ─── Module factory ───────────────────────────────────────────────────────────
|
|
3
|
+
module.exports=function createConfigPhase(ctx){const{WEBVIEW_CONFIG,iosConfig,PROJECT_DIR,url,progress,restoreManagedFileFromBaseline,readPlistObject,writePlistObject}=ctx;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;}async function generateConfigConstants(){progress.start("config");try{const appConfigPath=path.join(PROJECT_DIR,"Sources","Core","Constants","ConfigConstants.swift");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
4
|
+
const appConfigDir=path.dirname(appConfigPath);if(!fs.existsSync(appConfigDir))fs.mkdirSync(appConfigDir,{recursive:true});let configContent=`// This file is auto-generated. Do not edit.
|
|
5
|
+
import Foundation
|
|
6
|
+
|
|
7
|
+
public enum ConfigConstants {
|
|
8
|
+
public static let url = "${url}"`;const addedKeys=new Set();if(WEBVIEW_CONFIG&&typeof WEBVIEW_CONFIG==="object"){for(const[key,value]of Object.entries(WEBVIEW_CONFIG)){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);}}if(iosConfig&&typeof iosConfig==="object"){configContent+="\n \n // iOS-specific configuration";for(const[key,value]of Object.entries(iosConfig)){if(addedKeys.has(key))continue;configContent+="\n"+generateSwiftProperty(key,value);}}if(WEBVIEW_CONFIG.accessControl){const accessControl=WEBVIEW_CONFIG.accessControl;configContent+=`\n public static let accessControlEnabled = ${accessControl.enabled||false}`;if(accessControl.allowedUrls&&Array.isArray(accessControl.allowedUrls)){const allowedUrls=accessControl.allowedUrls.map(u=>`"${u}"`).join(", ");configContent+=`\n public static let allowedUrls: [String] = [${allowedUrls}]`;}else if(accessControl.allowedUrls&&typeof accessControl.allowedUrls==="string"){const allowedUrls=accessControl.allowedUrls.split(",").map(u=>u.trim()).filter(u=>u.length>0).map(u=>`"${u}"`).join(", ");configContent+=`\n public static let allowedUrls: [String] = [${allowedUrls}]`;}}else{configContent+=`\n public static let accessControlEnabled = false\n public static let allowedUrls: [String] = []`;}const splashConfig=WEBVIEW_CONFIG.splashScreen;if(splashConfig){configContent+=`\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = true`;configContent+=splashConfig.duration?`\n public static let splashScreenDuration: TimeInterval? = ${splashConfig.duration/1000.0}`:`\n public static let splashScreenDuration: TimeInterval? = nil`;configContent+=splashConfig.backgroundColor?`\n public static let splashScreenBackgroundColor = "${splashConfig.backgroundColor}"`:`\n public static let splashScreenBackgroundColor = "#ffffff"`;configContent+=`\n public static let splashScreenImageWidth: CGFloat = ${splashConfig.imageWidth||120}`;configContent+=`\n public static let splashScreenImageHeight: CGFloat = ${splashConfig.imageHeight||120}`;configContent+=`\n public static let splashScreenCornerRadius: CGFloat = ${splashConfig.cornerRadius||20}`;}else{configContent+=`\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = false\n public static let splashScreenDuration: TimeInterval? = nil\n public static let splashScreenBackgroundColor = "#ffffff"\n public static let splashScreenImageWidth: CGFloat = 120\n public static let splashScreenImageHeight: CGFloat = 120\n public static let splashScreenCornerRadius: CGFloat = 20`;}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");}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");}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");}if(!addedKeys.has("initial_url")){configContent+='\n public static let initial_url = ""';addedKeys.add("initial_url");}// Ensure appInfo always exists (default to empty string if not configured)
|
|
9
|
+
if(!addedKeys.has("appInfo")){configContent+='\n public static let appInfo = ""';addedKeys.add("appInfo");}configContent+=`\n}`;fs.writeFileSync(appConfigPath,configContent,"utf8");progress.log("Configuration constants generated successfully (SPM Package)","success");progress.complete("config");}catch(error){progress.fail("config",error.message);throw error;}}async function generateXCConfig(){try{const xconfigPath=path.join(PROJECT_DIR,"Configurations","Shared.xcconfig");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
10
|
+
const configDir=path.dirname(xconfigPath);if(!fs.existsSync(configDir))fs.mkdirSync(configDir,{recursive:true});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||"";const xconfigContent=`// This file is auto-generated by buildAppIos.js. Do not edit.
|
|
11
|
+
//
|
|
12
|
+
// Shared configuration for both Debug and Release builds
|
|
13
|
+
// All values are dynamically injected from config.json
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
// Product Configuration
|
|
17
|
+
PRODUCT_BUNDLE_IDENTIFIER = ${bundleId}
|
|
18
|
+
APP_DISPLAY_NAME = ${appName}
|
|
19
|
+
|
|
20
|
+
// Version Information
|
|
21
|
+
MARKETING_VERSION = ${marketingVersion}
|
|
22
|
+
CURRENT_PROJECT_VERSION = ${buildNumber}
|
|
23
|
+
|
|
24
|
+
// Development Team (for code signing)
|
|
25
|
+
DEVELOPMENT_TEAM = ${teamId}
|
|
26
|
+
|
|
27
|
+
// Swift Configuration
|
|
28
|
+
SWIFT_VERSION = ${swiftVersion}
|
|
29
|
+
|
|
30
|
+
// iOS Deployment Target (minimum iOS version)
|
|
31
|
+
IPHONEOS_DEPLOYMENT_TARGET = ${deploymentTarget}
|
|
32
|
+
|
|
33
|
+
// Common build settings
|
|
34
|
+
TARGETED_DEVICE_FAMILY = 1,2
|
|
35
|
+
ENABLE_PREVIEWS = YES
|
|
36
|
+
SWIFT_EMIT_LOC_STRINGS = YES
|
|
37
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
|
38
|
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
|
|
39
|
+
|
|
40
|
+
// Code signing
|
|
41
|
+
CODE_SIGN_IDENTITY = Apple Development
|
|
42
|
+
CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
|
|
43
|
+
CODE_SIGN_STYLE = Manual
|
|
44
|
+
PROVISIONING_PROFILE_SPECIFIER =
|
|
45
|
+
PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = ${provisioningProfile}
|
|
46
|
+
|
|
47
|
+
// Module settings
|
|
48
|
+
CLANG_ENABLE_MODULES = NO
|
|
49
|
+
GENERATE_INFOPLIST_FILE = YES
|
|
50
|
+
|
|
51
|
+
// Run path search paths
|
|
52
|
+
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
|
|
53
|
+
|
|
54
|
+
// Development assets
|
|
55
|
+
DEVELOPMENT_ASSET_PATHS = "iosnativeWebView/Preview Content"
|
|
56
|
+
|
|
57
|
+
// Privacy descriptions
|
|
58
|
+
INFOPLIST_KEY_NSCameraUsageDescription = This app requires camera access to capture images
|
|
59
|
+
INFOPLIST_KEY_NSLocalNetworkUsageDescription = Debug web content in Safari Web Inspector
|
|
60
|
+
|
|
61
|
+
// UI Configuration
|
|
62
|
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES
|
|
63
|
+
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES
|
|
64
|
+
INFOPLIST_KEY_UILaunchScreen_Generation = YES
|
|
65
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
|
|
66
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
|
|
67
|
+
`;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 updateInfoPlist(pluginComposition={}){const PROJECT_NAME=ctx.PROJECT_NAME;const isGoogleSignInEnabled=ctx.isGoogleSignInEnabled;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
68
|
+
const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
69
|
+
const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
70
|
+
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=>!value?"":value.split(".").reverse().join(".");const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){throw new Error("Google Sign-In enabled but no valid clientId found");}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);});}async function updateEntitlements(pluginComposition={}){const PROJECT_NAME=ctx.PROJECT_NAME;const entitlementsPath=path.join(PROJECT_DIR,PROJECT_NAME,`${PROJECT_NAME}.entitlements`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
71
|
+
if(!fs.existsSync(entitlementsPath))return;restoreManagedFileFromBaseline(entitlementsPath);const entitlementsObject=readPlistObject(entitlementsPath);mergeIntoTopLevelObject(entitlementsObject,pluginComposition.entitlements||{},"ios.entitlements");writePlistObject(entitlementsPath,entitlementsObject);}return{generateConfigConstants,generateXCConfig,updateInfoPlist,updateEntitlements};};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";const{execFile,execFileSync}=require("child_process");const fs=require("fs");const path=require("path");const TerminalProgress=require("../TerminalProgress.js").default;const createConfigPhase=require("./config.js");const createPluginsPhase=require("./plugins.js");const createAssetsPhase=require("./assets.js");const createBuildPhase=require("./build.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const MANAGED_BASELINE_SUFFIX=".catalyst-base";const shellCommand=process.platform==="win32"?"cmd.exe":"sh";const shellArgs=command=>process.platform==="win32"?["/d","/s","/c",command]:["-c",command];function createIosBuild(config){const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;const url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.cwd()}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME=iosConfig.scheme||"iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;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"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);// ─── Low-level helpers shared across modules ──────────────────────────────
|
|
2
|
+
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());});});}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}function ensureManagedBaseline(filePath){const baselinePath=`${filePath}${MANAGED_BASELINE_SUFFIX}`;if(!fs.existsSync(baselinePath)){if(!fs.existsSync(filePath))throw new Error(`Managed baseline source file not found: ${filePath}`);fs.copyFileSync(filePath,baselinePath);}return baselinePath;}function restoreManagedFileFromBaseline(filePath){const baselinePath=ensureManagedBaseline(filePath);fs.copyFileSync(baselinePath,filePath);}function readPlistObject(filePath){const output=execFileSync("plutil",["-convert","json","-o","-",filePath],{encoding:"utf8"});return JSON.parse(output);}function writePlistObject(filePath,value){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename uses path.basename and stays under the fixed project directory.
|
|
3
|
+
const tempPath=path.join(PROJECT_DIR,`.catalyst-${path.basename(filePath)}-${process.pid}.json`);fs.writeFileSync(tempPath,JSON.stringify(value,null,2),"utf8");try{execFileSync("plutil",["-convert","xml1","-o",filePath,tempPath]);}finally{fs.rmSync(tempPath,{force:true});}}// ─── Shared context passed to all phase modules ───────────────────────────
|
|
4
|
+
const ctx={WEBVIEW_CONFIG,BUILD_OUTPUT_PATH,iosConfig,isGoogleSignInEnabled,PROJECT_DIR,PROJECT_NAME,SCHEME_NAME,APP_BUNDLE_ID,IPHONE_MODEL,url,PUBLIC_PATH,progress,runCommand,getXcodeProjectFilePath,ensureManagedBaseline,restoreManagedFileFromBaseline,readPlistObject,writePlistObject};// ─── Instantiate phase modules ────────────────────────────────────────────
|
|
5
|
+
const{generateConfigConstants,generateXCConfig,updateInfoPlist,updateEntitlements}=createConfigPhase(ctx);const{generatePackageSwift,updateXcodeProjectPackageDependencies,syncPluginResources}=createPluginsPhase(ctx);const{processNotificationAssets,copyOfflinePage,copySplashscreenAssets,copyAppIcon}=createAssetsPhase(ctx);const{cleanBuildArtifacts,buildXcodeProject,buildProjectForTesting,findAppPath,moveAppToBuildOutput,installAndLaunchApp,detectPhysicalDevices,buildProjectForPhysicalDevice,findPhysicalDeviceAppPath,installAndLaunchOnPhysicalDevice,launchIOSSimulator,getBootedSimulatorUUID}=createBuildPhase(ctx);// ─── Orchestrated build flows ─────────────────────────────────────────────
|
|
6
|
+
async function buildForIOS(pluginComposition={}){const originalDir=process.cwd();try{await generatePackageSwift(pluginComposition.iosDependencies);await updateXcodeProjectPackageDependencies();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);const physicalDevice=await detectPhysicalDevices();let APP_PATH,targetInfo;if(physicalDevice){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{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);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 buildIosForTesting(pluginComposition={}){const originalDir=process.cwd();try{await generateConfigConstants();await updateInfoPlist(pluginComposition);await updateEntitlements(pluginComposition);await syncPluginResources(pluginComposition);await generatePackageSwift(pluginComposition.iosDependencies);await updateXcodeProjectPackageDependencies();await processNotificationAssets(WEBVIEW_CONFIG);await copyOfflinePage();await generateXCConfig();await copySplashscreenAssets();await copyAppIcon();progress.log("Changing directory to: "+PROJECT_DIR,"info");process.chdir(PROJECT_DIR);await cleanBuildArtifacts();const derivedDataPath=path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData");progress.start("build");try{await buildProjectForTesting(SCHEME_NAME,"iphonesimulator",`platform=iOS Simulator,name=${IPHONE_MODEL}`,APP_BUNDLE_ID,derivedDataPath,PROJECT_NAME);progress.complete("build");}catch(buildError){progress.log("build-for-testing failed, attempting fallback with booted simulator...","warning");const bootedUUID=await getBootedSimulatorUUID(IPHONE_MODEL);if(bootedUUID){await buildProjectForTesting(SCHEME_NAME,"iphonesimulator",`platform=iOS Simulator,id=${bootedUUID}`,APP_BUNDLE_ID,derivedDataPath,PROJECT_NAME);progress.complete("build");}else{progress.fail("build",buildError.message);throw buildError;}}progress.log("✅ build-for-testing complete — test bundle ready for xctest runner","success");return{success:true};}catch(error){progress.log("buildIosForTesting failed: "+error.message,"error");throw error;}finally{process.chdir(originalDir);}}return{generateConfigConstants,updateInfoPlist,updateEntitlements,syncPluginResources,generatePackageSwift,updateXcodeProjectPackageDependencies,processNotificationAssets,copyOfflinePage,generateXCConfig,copySplashscreenAssets,copyAppIcon,buildForIOS,buildIosForTesting,progress,WEBVIEW_CONFIG,PROJECT_DIR,SCHEME_NAME};}module.exports={createIosBuild,pwd};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const crypto=require("crypto");const{execSync}=require("child_process");const PLUGIN_RESOURCE_ROOT="PluginResources";// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
2
|
+
function packageRequirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function mergePackageDependency(map,dependency,sourceLabel){const existing=map.get(dependency.url);if(!existing){map.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...new Set(dependency.products)]});return;}if(packageRequirementKey(existing)!==packageRequirementKey(dependency)){throw new Error(`iOS package dependency conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.requirement.type}:${existing.requirement.version}' vs '${dependency.requirement.type}:${dependency.requirement.version}'`);}if(existing.package!==dependency.package){throw new Error(`iOS package identity conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.package}' vs '${dependency.package}'`);}existing.products=[...new Set([...existing.products,...dependency.products])].sort();}function formatSwiftPackageRequirement(dependency){if(dependency.requirement.type==="from")return`from: "${dependency.requirement.version}"`;if(dependency.requirement.type==="exact")return`exact: "${dependency.requirement.version}"`;throw new Error(`Unsupported iOS package requirement type: ${dependency.requirement.type}`);}function formatSwiftPackageEntries(dependencies){return dependencies.map(d=>` .package(url: "${d.url}", ${formatSwiftPackageRequirement(d)})`);}function formatSwiftProductEntries(dependencies){return dependencies.flatMap(d=>d.products.map(p=>` .product(name: "${p}", package: "${d.package}")`));}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 replaceRequired(content,pattern,replacement,errorMessage){if(!pattern.test(content))throw new Error(errorMessage);return content.replace(pattern,replacement);}// ─── Module factory ───────────────────────────────────────────────────────────
|
|
3
|
+
module.exports=function createPluginsPhase(ctx){const{WEBVIEW_CONFIG,PROJECT_DIR,PROJECT_NAME,SCHEME_NAME,isGoogleSignInEnabled,progress,getXcodeProjectFilePath}=ctx;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(d=>mergePackageDependency(coreDependencyMap,d,"base core"));pluginDependencies.forEach(d=>mergePackageDependency(coreDependencyMap,d,"plugin manifests"));const coreDependencies=[...coreDependencyMap.values()].sort((l,r)=>l.url.localeCompare(r.url));const packageDependencyMap=new Map();coreDependencies.forEach(d=>mergePackageDependency(packageDependencyMap,d,"core target"));notificationsDependencies.forEach(d=>mergePackageDependency(packageDependencyMap,d,"notifications target"));const packageDependencies=[...packageDependencyMap.values()].sort((l,r)=>l.url.localeCompare(r.url));progress.log(`🔧 Generating Package.swift (notifications: ${isNotificationsEnabled})`,"info");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");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
4
|
+
const targetPath=path.join(PROJECT_DIR,"Package.swift");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
5
|
+
let shouldUpdate=true;try{const previousHash=fs.readFileSync(hashFilePath,"utf8");if(previousHash===configHash){shouldUpdate=false;progress.log("Package.swift already up to date","info");}}catch{/* hash file absent — proceed with update */}try{fs.accessSync(targetPath);}catch{shouldUpdate=true;progress.log("Package.swift missing, will generate it now","info");}if(shouldUpdate){progress.log("Generating Package.swift dynamically","info");let packageContent=`// swift-tools-version: 5.9
|
|
6
|
+
// Auto-generated Package.swift - DO NOT EDIT MANUALLY
|
|
7
|
+
// Generated based on config: notifications.enabled = ${isNotificationsEnabled}
|
|
8
|
+
import PackageDescription
|
|
9
|
+
|
|
10
|
+
let package = Package(
|
|
11
|
+
name: "iosnativeWebView",
|
|
12
|
+
platforms: [.iOS(.v17)],
|
|
13
|
+
products: [
|
|
14
|
+
.library(name: "CatalystCore", targets: ["CatalystCore"])`;if(isNotificationsEnabled){packageContent+=`,\n .library(name: "CatalystNotifications", targets: ["CatalystNotifications"])`;}packageContent+=`
|
|
15
|
+
],
|
|
16
|
+
dependencies: [
|
|
17
|
+
${formatSwiftPackageEntries(packageDependencies).join(",\n")}
|
|
18
|
+
],
|
|
19
|
+
targets: [
|
|
20
|
+
// Core functionality (WebView, bridge, utils, constants)
|
|
21
|
+
// App-level files (AppDelegate, ContentView) are in iosnativeWebView/ directory
|
|
22
|
+
.target(
|
|
23
|
+
name: "CatalystCore",
|
|
24
|
+
dependencies: [
|
|
25
|
+
${formatSwiftProductEntries(coreDependencies).join(",\n")}
|
|
26
|
+
],
|
|
27
|
+
path: "Sources/Core"
|
|
28
|
+
)`;if(isNotificationsEnabled){packageContent+=`,
|
|
29
|
+
// Notifications functionality (optional, includes Firebase)
|
|
30
|
+
.target(
|
|
31
|
+
name: "CatalystNotifications",
|
|
32
|
+
dependencies: [
|
|
33
|
+
"CatalystCore",
|
|
34
|
+
${formatSwiftProductEntries(notificationsDependencies).join(",\n")}
|
|
35
|
+
],
|
|
36
|
+
path: "Sources/CatalystNotifications"
|
|
37
|
+
)`;}packageContent+=`\n ]\n)\n`;fs.writeFileSync(targetPath,packageContent,"utf8");progress.log(`Generated Package.swift with ${isNotificationsEnabled?"notifications enabled":"notifications disabled"}`,"success");fs.writeFileSync(hashFilePath,configHash);progress.log("Resolving package dependencies...","info");try{execSync(`cd "${PROJECT_DIR}" && rm -rf .build`,{stdio:"ignore"});// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
38
|
+
try{fs.rmSync(path.join(PROJECT_DIR,"Package.resolved"),{force:true});}catch{progress.log("");}// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
39
|
+
execSync(`cd "${PROJECT_DIR}" && rm -rf .swiftpm`,{stdio:"ignore"});// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
40
|
+
const projectPath=path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
41
|
+
execSync(`cd "${PROJECT_DIR}" && xcodebuild -resolvePackageDependencies -project "${projectPath}" -scheme "${SCHEME_NAME}"`,{stdio:"inherit"});// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
42
|
+
progress.log("Package dependencies resolved successfully","success");}catch(error){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");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
43
|
+
progress.log(`🔧 Updating Xcode package dependencies (notifications: ${isNotificationsEnabled})`,"info");let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch{throw new Error(`Xcode project file not found at: ${projectFilePath}`);}const NOTIF_BUILD_FILE_ID="C99974352E97D56900C25611";const NOTIF_PRODUCT_ID="C99974362E97D56900C25611";const hasNotifications=projectContent.includes("/* CatalystNotifications */");if(isNotificationsEnabled&&!hasNotifications){progress.log("Adding CatalystNotifications to Xcode project","info");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 */; };`);projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/,)/,`$1\n\t\t\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */,`);projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);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");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 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");projectContent=replaceRequired(projectContent,/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/,`$1\n\t\t\t\t${fileRefId} /* ${label} */,`,"iosnativeWebView group not found while registering plugin resources");projectContent=replaceRequired(projectContent,/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/,`$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);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
44
|
+
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.
|
|
45
|
+
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;}}return{generatePackageSwift,updateXcodeProjectPackageDependencies,syncPluginResources};};
|
|
@@ -24,8 +24,9 @@ public enum ConfigConstants {
|
|
|
24
24
|
|
|
25
25
|
public static let appInfo = "test-build"
|
|
26
26
|
public static let cachePattern: [String] = []
|
|
27
|
-
public static let LOCAL_IP = "localhost"
|
|
27
|
+
public static let LOCAL_IP = "localhost" // swiftlint:disable:this identifier_name
|
|
28
28
|
public static let port = ""
|
|
29
|
+
public static let initial_url = "" // swiftlint:disable:this identifier_name
|
|
29
30
|
|
|
30
31
|
public enum SplashScreen {
|
|
31
32
|
public static let imageHeight = 200
|
|
@@ -13,6 +13,14 @@ public struct ContentView: View {
|
|
|
13
13
|
onError: { _ in }
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
private static var startURL: String {
|
|
17
|
+
let base = ConfigConstants.url
|
|
18
|
+
let initial = ConfigConstants.initial_url
|
|
19
|
+
guard !initial.isEmpty else { return base }
|
|
20
|
+
let path = initial.hasPrefix("/") ? initial : "/\(initial)"
|
|
21
|
+
return base + path
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
public init() {}
|
|
17
25
|
|
|
18
26
|
public var body: some View {
|
|
@@ -25,18 +33,18 @@ public struct ContentView: View {
|
|
|
25
33
|
// Normal remote URL - isolated from state changes
|
|
26
34
|
// Conditionally apply edge-to-edge based on config (matches Android behavior)
|
|
27
35
|
if ConfigConstants.EdgeToEdge.enabled {
|
|
28
|
-
WebViewContainer(urlString:
|
|
36
|
+
WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
|
|
29
37
|
.ignoresSafeArea()
|
|
30
38
|
.onAppear {
|
|
31
|
-
logger.info("WebView appeared with URL: \(
|
|
39
|
+
logger.info("WebView appeared with URL: \(ContentView.startURL) [Edge-to-edge: enabled]")
|
|
32
40
|
}
|
|
33
41
|
} else {
|
|
34
|
-
WebViewContainer(urlString:
|
|
42
|
+
WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
|
|
35
43
|
.onAppear {
|
|
36
|
-
logger.info("WebView appeared with URL: \(
|
|
44
|
+
logger.info("WebView appeared with URL: \(ContentView.startURL) [Edge-to-edge: disabled]")
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
|
-
|
|
47
|
+
|
|
40
48
|
// Show splash screen if enabled in configuration
|
|
41
49
|
if ConfigConstants.splashScreenEnabled {
|
|
42
50
|
SplashView(webViewModel: webViewModel).zIndex(1)
|
|
@@ -46,11 +54,11 @@ public struct ContentView: View {
|
|
|
46
54
|
ProgressView()
|
|
47
55
|
.scaleEffect(1.5)
|
|
48
56
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
Text("\(Int(webViewModel.loadingProgress * 100))%")
|
|
51
59
|
.foregroundColor(.blue)
|
|
52
60
|
.padding(.top, 8)
|
|
53
|
-
|
|
61
|
+
|
|
54
62
|
if webViewModel.isLoadingFromCache {
|
|
55
63
|
Text("Loading from cache...")
|
|
56
64
|
.foregroundColor(.blue)
|
|
@@ -111,11 +119,11 @@ struct LoadingOverlay: View {
|
|
|
111
119
|
// UIViewControllerRepresentable for hosting native view controllers
|
|
112
120
|
struct HostingController: UIViewControllerRepresentable {
|
|
113
121
|
var viewController: UIViewController
|
|
114
|
-
|
|
122
|
+
|
|
115
123
|
func makeUIViewController(context: Context) -> UIViewController {
|
|
116
124
|
return viewController
|
|
117
125
|
}
|
|
118
|
-
|
|
126
|
+
|
|
119
127
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
|
|
120
128
|
// No update needed
|
|
121
129
|
}
|
|
@@ -200,7 +200,7 @@ final class BootTimingUtilityTests: XCTestCase {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
waitForExpectations(timeout:
|
|
203
|
+
waitForExpectations(timeout: 10.0) { error in
|
|
204
204
|
XCTAssertNil(error, "Concurrent logging should complete without errors")
|
|
205
205
|
}
|
|
206
206
|
}
|
|
@@ -250,10 +250,9 @@ final class ConfigMappingTests: XCTestCase {
|
|
|
250
250
|
// This test validates that ConfigConstants structure is correctly defined
|
|
251
251
|
|
|
252
252
|
// Verify String fields are accessible and non-nil
|
|
253
|
-
let stringFields = [
|
|
253
|
+
let stringFields: [String] = [
|
|
254
254
|
ConfigConstants.url,
|
|
255
255
|
ConfigConstants.LOCAL_IP,
|
|
256
|
-
ConfigConstants.port,
|
|
257
256
|
ConfigConstants.appInfo,
|
|
258
257
|
ConfigConstants.appBundleId,
|
|
259
258
|
ConfigConstants.appName,
|
|
@@ -266,6 +265,9 @@ final class ConfigMappingTests: XCTestCase {
|
|
|
266
265
|
XCTAssertNotNil(field, "String field should not be nil")
|
|
267
266
|
}
|
|
268
267
|
|
|
268
|
+
// Verify Int fields are accessible
|
|
269
|
+
_ = ConfigConstants.port
|
|
270
|
+
|
|
269
271
|
// Verify Array fields are accessible
|
|
270
272
|
XCTAssertNotNil(ConfigConstants.cachePattern, "cachePattern should be accessible")
|
|
271
273
|
XCTAssertNotNil(ConfigConstants.allowedUrls, "allowedUrls should be accessible")
|
|
@@ -3,7 +3,8 @@ const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const exist
|
|
|
3
3
|
const updatedConfig={...existingConfig,// Preserve all existing keys
|
|
4
4
|
WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,ios:newConfig.WEBVIEW_CONFIG.ios}// Update only WEBVIEW_CONFIG
|
|
5
5
|
};// Save the merged config
|
|
6
|
-
_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}async function setupIOSEnvironment(){try{progress.start("platform");if(process.platform!=="darwin"){progress.log("iOS Simulator is only available on macOS.");process.exit(1);}progress.complete("platform");progress.start("config");const{WEBVIEW_CONFIG}=await initializeConfig();progress.complete("config");progress.start("simulator");await configureSimulator(WEBVIEW_CONFIG);progress.complete("simulator");progress.start("launch");await launchIOSSimulator(WEBVIEW_CONFIG.ios.simulatorName);progress.complete("launch");progress.start("saveConfig");const config=await(0,_utils.validateAndCompleteConfig)("ios",configPath);progress.complete("saveConfig");progress.start("setupServer");await setupServer(configPath);progress.start("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"ios: iOS-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"appBundleId: iOS application bundle identifier",indent:2,prefix:"├─ ",color:"gray"},{text:"simulatorName: Selected iOS simulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}async function displayPaginatedList(items,startIndex){const endIndex=Math.min(startIndex+ITEMS_PER_PAGE,items.length);for(let i=startIndex;i<endIndex;i++){console.log(`${i+1}. ${items[i]}`);}if(endIndex<items.length){console.log(`\nShowing ${startIndex+1}-${endIndex} of ${items.length} items`);console.log('Type "more" to see more items, or enter your selection: ');return true;}return false;}async function getRuntime(){const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const parsedRuntimes=JSON.parse(runtimesOutput).runtimes;const availableRuntime=parsedRuntimes.find(runtime=>runtime.isAvailable||runtime.availability==="(available)"||runtime.availability==="available");if(!availableRuntime){const runningSimulators=(0,_child_process.execSync)("xcrun simctl list devices booted").toString();if(runningSimulators.includes("(Booted)")&&parsedRuntimes.length>0){console.log(`Using runtime: ${parsedRuntimes[0].name}`);return parsedRuntimes[0];}console.error("No available iOS runtime found. Please install one through Xcode.");process.exit(1);}console.log(`Using runtime: ${availableRuntime.name}`);return availableRuntime;}async function configureSimulator(config){
|
|
6
|
+
_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}async function setupIOSEnvironment(){try{progress.start("platform");if(process.platform!=="darwin"){progress.log("iOS Simulator is only available on macOS.");process.exit(1);}progress.complete("platform");progress.start("config");const{WEBVIEW_CONFIG}=await initializeConfig();progress.complete("config");progress.start("simulator");await configureSimulator(WEBVIEW_CONFIG);progress.complete("simulator");progress.start("launch");await launchIOSSimulator(WEBVIEW_CONFIG.ios.simulatorName);progress.complete("launch");progress.start("saveConfig");const config=await(0,_utils.validateAndCompleteConfig)("ios",configPath);progress.complete("saveConfig");progress.start("setupServer");await setupServer(configPath);progress.start("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"ios: iOS-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"appBundleId: iOS application bundle identifier",indent:2,prefix:"├─ ",color:"gray"},{text:"simulatorName: Selected iOS simulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}async function displayPaginatedList(items,startIndex){const endIndex=Math.min(startIndex+ITEMS_PER_PAGE,items.length);for(let i=startIndex;i<endIndex;i++){console.log(`${i+1}. ${items[i]}`);}if(endIndex<items.length){console.log(`\nShowing ${startIndex+1}-${endIndex} of ${items.length} items`);console.log('Type "more" to see more items, or enter your selection: ');return true;}return false;}async function getRuntime(){const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const parsedRuntimes=JSON.parse(runtimesOutput).runtimes;const availableRuntime=parsedRuntimes.find(runtime=>runtime.isAvailable||runtime.availability==="(available)"||runtime.availability==="available");if(!availableRuntime){const runningSimulators=(0,_child_process.execSync)("xcrun simctl list devices booted").toString();if(runningSimulators.includes("(Booted)")&&parsedRuntimes.length>0){console.log(`Using runtime: ${parsedRuntimes[0].name}`);return parsedRuntimes[0];}console.error("No available iOS runtime found. Please install one through Xcode.");process.exit(1);}console.log(`Using runtime: ${availableRuntime.name}`);return availableRuntime;}async function configureSimulator(config){// If a simulator is already configured, skip the interactive picker
|
|
7
|
+
if(config.ios.simulatorName){progress.log(`Using configured simulator: ${config.ios.simulatorName}`);return;}progress.pause();// Get the current runtime first
|
|
7
8
|
const runtime=await getRuntime();// Get simulators specifically for this runtime
|
|
8
9
|
const simulatorsJson=JSON.parse((0,_child_process.execSync)("xcrun simctl list devices -j").toString());const runtimeSimulators=simulatorsJson.devices[runtime.identifier]||[];// Filter for available simulators only
|
|
9
10
|
const availableSimulators=runtimeSimulators.filter(sim=>sim.availability==="(available)"||sim.isAvailable).map(sim=>`${sim.name} (${sim.udid})`);if(availableSimulators.length===0){progress.log(`No available simulators found for runtime: ${runtime.name}`,"error");progress.resume();process.exit(1);return;}let startIndex=0;console.log(`\nAvailable simulators for ${runtime.name} (or type 'new' to create a new one):`);// eslint-disable-next-line no-constant-condition
|
|
@@ -26,9 +27,10 @@ const simulatorName=selectedSimulator.split(" (")[0];config.ios.simulatorName=si
|
|
|
26
27
|
// }
|
|
27
28
|
// }
|
|
28
29
|
async function launchIOSSimulator(simulatorName){progress.log("Launching iOS Simulator...");try{// Get all simulators including both available and booted ones
|
|
29
|
-
const allSimulatorInfo=(0,_child_process.execSync)("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);// Search through all runtimes and their devices
|
|
30
|
-
let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;//
|
|
31
|
-
Object.
|
|
30
|
+
const allSimulatorInfo=(0,_child_process.execSync)("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const availableRuntimeIds=new Set(JSON.parse(runtimesOutput).runtimes.filter(r=>r.isAvailable||r.availability==="(available)"||r.availability==="available").map(r=>r.identifier));// Search through all runtimes and their devices
|
|
31
|
+
let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;// Prefer devices under an available runtime; fall back to already-booted ones
|
|
32
|
+
Object.entries(simulatorsJson.devices).forEach(([runtimeId,devices])=>{const isAvailableRuntime=availableRuntimeIds.has(runtimeId);devices.forEach(device=>{if(device.name===simulatorName){const alreadyBooted=device.state==="Booted";// Take this match if: no match yet, or this runtime is available and current isn't, or device is already booted
|
|
33
|
+
if(!foundSimulator||isAvailableRuntime&&!foundSimulator._fromAvailableRuntime||alreadyBooted){foundSimulator={...device,_fromAvailableRuntime:isAvailableRuntime};foundSimulatorId=device.udid;isBooted=alreadyBooted;}}});});if(!foundSimulator){console.log(`Configured simulator "${simulatorName}" not found.`);return;}if(!isBooted){console.log(`Booting simulator: ${simulatorName}`);(0,_utils.runCommand)(`xcrun simctl boot ${foundSimulatorId}`);}else{console.log(`Simulator ${simulatorName} is already booted`);}// Open Simulator.app and focus
|
|
32
34
|
progress.log("Opening Simulator.app...");(0,_utils.runCommand)("open -a Simulator");// Give the simulator a moment to open/focus
|
|
33
35
|
await new Promise(resolve=>setTimeout(resolve,1000));// Activate the Simulator.app window to bring it to front
|
|
34
36
|
(0,_utils.runCommand)("osascript -e 'tell application \"Simulator\" to activate'");console.log("iOS Simulator launched successfully.");}catch(error){console.error("Failed to launch iOS Simulator. Error:",error.message);}// process.exit(0);
|
|
@@ -61,7 +61,7 @@ function fetchRaw(url) {
|
|
|
61
61
|
// v0.1.0, v0.1.0-canary.8, v0.0.3-canary-18, v0.1.0-beta.2
|
|
62
62
|
function sanitizeGithubTag(version) {
|
|
63
63
|
if (!version) return "main"
|
|
64
|
-
return /^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+([
|
|
64
|
+
return /^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+([.-]\d+)?)?$/.test(version) ? version : "main"
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Fetch first available file — used for KB-miss GitHub fallback
|
|
@@ -101,7 +101,7 @@ async function fetchFromGithub(files, installedVersion) {
|
|
|
101
101
|
// Strip all FTS5 special characters before quoting so keywords are treated as literals.
|
|
102
102
|
function buildFtsQuery(keywords) {
|
|
103
103
|
return keywords
|
|
104
|
-
.map((k) => `"${k.replace(/[^a-zA-Z0-9 _
|
|
104
|
+
.map((k) => `"${k.replace(/[^a-zA-Z0-9 _-]/g, " ").trim()}"`)
|
|
105
105
|
.filter((k) => k.length > 2) // drop empty/whitespace-only terms after stripping
|
|
106
106
|
.join(" OR ")
|
|
107
107
|
}
|