catalyst-core 0.2.0-beta.2 → 0.3.0-beta.1

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.
Files changed (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -1,8 +1,609 @@
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};};
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const GOOGLE_SERVICES_FILENAME = "GoogleService-Info.plist";
7
+ const NOTIFICATION_ICONS = [{
8
+ sourceName: "notification-icon",
9
+ resourceName: "NotificationIcon"
10
+ }, {
11
+ sourceName: "notification-large",
12
+ resourceName: "NotificationLargeIcon"
13
+ }];
14
+ const NOTIFICATION_SOUNDS = [{
15
+ sourceName: "notification-sound-default",
16
+ resourceName: "notification_sound_default"
17
+ }, {
18
+ sourceName: "notification-sound-urgent",
19
+ resourceName: "notification_sound_urgent"
20
+ }];
21
+ function generateProjectObjectId(identifier, suffix = "") {
22
+ return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0, 24).toUpperCase();
23
+ }
24
+ module.exports = function createAssetsPhase(ctx) {
25
+ const {
26
+ WEBVIEW_CONFIG,
27
+ PROJECT_DIR,
28
+ PROJECT_NAME,
29
+ PUBLIC_PATH,
30
+ progress,
31
+ getXcodeProjectFilePath
32
+ } = ctx;
33
+
34
+ // ─── Google Services ─────────────────────────────────────────────────────
35
+
36
+ async function addGoogleServicesPlistToXcodeProject() {
37
+ try {
38
+ const projectFilePath = getXcodeProjectFilePath();
39
+ let projectContent;
40
+ try {
41
+ projectContent = fs.readFileSync(projectFilePath, "utf8");
42
+ } catch {
43
+ progress.log("Xcode project file not found while adding GoogleService-Info.plist", "warning");
44
+ return;
45
+ }
46
+ if (projectContent.includes(`/* ${GOOGLE_SERVICES_FILENAME} */`)) {
47
+ progress.log("GoogleService-Info.plist already registered in Xcode project", "info");
48
+ return;
49
+ }
50
+ const fileRefId = generateProjectObjectId(GOOGLE_SERVICES_FILENAME, "_ref");
51
+ const buildFileId = generateProjectObjectId(GOOGLE_SERVICES_FILENAME, "_build");
52
+ const fileRefEntry = `\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ${GOOGLE_SERVICES_FILENAME}; sourceTree = "<group>"; };`;
53
+ projectContent = projectContent.replace(/(\/\* End PBXFileReference section \*\/)/, `${fileRefEntry}\n$1`);
54
+ const buildFileEntry = `\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */; };`;
55
+ projectContent = projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/, `${buildFileEntry}\n$1`);
56
+ const groupPattern = /(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;
57
+ if (groupPattern.test(projectContent)) projectContent = projectContent.replace(groupPattern, `$1\n\t\t\t\t${fileRefId} /* ${GOOGLE_SERVICES_FILENAME} */,`);
58
+ const resourcesPattern = /(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;
59
+ if (resourcesPattern.test(projectContent)) projectContent = projectContent.replace(resourcesPattern, `$1\n\t\t\t\t${buildFileId} /* ${GOOGLE_SERVICES_FILENAME} in Resources */,`);
60
+ fs.writeFileSync(projectFilePath, projectContent, "utf8");
61
+ progress.log("Registered GoogleService-Info.plist with Xcode project", "success");
62
+ } catch (error) {
63
+ progress.log(`Warning: Failed to register GoogleService-Info.plist in Xcode project: ${error.message}`, "warning");
64
+ }
65
+ }
66
+ async function removeGoogleServicesPlistFromXcodeProject() {
67
+ try {
68
+ const projectFilePath = getXcodeProjectFilePath();
69
+ let projectContent;
70
+ try {
71
+ projectContent = fs.readFileSync(projectFilePath, "utf8");
72
+ } catch {
73
+ return;
74
+ }
75
+ 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")];
76
+ let modified = false;
77
+ for (const regex of regexes) {
78
+ if (regex.test(projectContent)) {
79
+ projectContent = projectContent.replace(regex, "");
80
+ modified = true;
81
+ }
82
+ }
83
+ if (modified) {
84
+ fs.writeFileSync(projectFilePath, projectContent, "utf8");
85
+ progress.log("Removed GoogleService-Info.plist references from Xcode project", "info");
86
+ }
87
+ } catch (error) {
88
+ progress.log(`Warning: Failed to clean GoogleService-Info.plist from Xcode project: ${error.message}`, "warning");
89
+ }
90
+ }
91
+ async function handleGoogleServicesPlist() {
92
+ try {
93
+ const rootPath = `${process.cwd()}/GoogleService-Info.plist`;
94
+ const iosPath = `${PROJECT_DIR}/${PROJECT_NAME}/GoogleService-Info.plist`;
95
+ if (fs.existsSync(rootPath)) {
96
+ progress.log("Found GoogleService-Info.plist in root directory", "info");
97
+ const targetDir = path.dirname(iosPath);
98
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, {
99
+ recursive: true
100
+ });
101
+ fs.copyFileSync(rootPath, iosPath);
102
+ progress.log("Copied GoogleService-Info.plist to iOS project", "success");
103
+ await addGoogleServicesPlistToXcodeProject();
104
+ return true;
105
+ } else if (fs.existsSync(iosPath)) {
106
+ progress.log("GoogleService-Info.plist already exists in iOS project", "info");
107
+ await addGoogleServicesPlistToXcodeProject();
108
+ return true;
109
+ } else {
110
+ await removeGoogleServicesPlistFromXcodeProject();
111
+ progress.log("GoogleService-Info.plist not found - Firebase push notifications will not work", "warning");
112
+ progress.log("Place GoogleService-Info.plist in project root", "info");
113
+ return false;
114
+ }
115
+ } catch (error) {
116
+ const rootPath = `${process.cwd()}/GoogleService-Info.plist`;
117
+ if (error.code === "EACCES" && fs.existsSync(rootPath)) throw new Error(`Permission denied copying GoogleService-Info.plist: ${error.message}`);
118
+ await removeGoogleServicesPlistFromXcodeProject();
119
+ progress.log(`Warning: Error handling GoogleService-Info.plist: ${error.message}`, "warning");
120
+ return false;
121
+ }
122
+ }
123
+
124
+ // ─── Notification assets ─────────────────────────────────────────────────
125
+
126
+ async function validateNotificationAsset(filePath, assetType) {
127
+ const stats = fs.statSync(filePath);
128
+ const fileSizeKB = stats.size / 1024;
129
+ 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");
130
+ }
131
+ async function processNotificationIcons() {
132
+ try {
133
+ const assetsPath = `${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;
134
+ const imageFormats = ["png", "jpg", "jpeg", "webp"];
135
+ let iconsProcessed = 0;
136
+ if (!fs.existsSync(PUBLIC_PATH)) {
137
+ progress.log(`Public directory not found at ${PUBLIC_PATH}`, "info");
138
+ return 0;
139
+ }
140
+ if (!fs.existsSync(assetsPath)) fs.mkdirSync(assetsPath, {
141
+ recursive: true
142
+ });
143
+ for (const icon of NOTIFICATION_ICONS) {
144
+ const imagesetPath = `${assetsPath}/${icon.resourceName}.imageset`;
145
+ if (fs.existsSync(imagesetPath)) {
146
+ fs.rmSync(imagesetPath, {
147
+ recursive: true,
148
+ force: true
149
+ });
150
+ progress.log(`Removed existing ${icon.resourceName}.imageset`, "info");
151
+ }
152
+ }
153
+ for (const icon of NOTIFICATION_ICONS) {
154
+ for (const format of imageFormats) {
155
+ const iconImagePath = `${PUBLIC_PATH}/${icon.sourceName}.${format}`;
156
+ if (fs.existsSync(iconImagePath)) {
157
+ validateNotificationAsset(iconImagePath, "icon");
158
+ const imagesetPath = `${assetsPath}/${icon.resourceName}.imageset`;
159
+ if (!fs.existsSync(imagesetPath)) fs.mkdirSync(imagesetPath, {
160
+ recursive: true
161
+ });
162
+ fs.copyFileSync(iconImagePath, `${imagesetPath}/${icon.sourceName}.${format}`);
163
+ const contentsJson = {
164
+ images: [{
165
+ filename: `${icon.sourceName}.${format}`,
166
+ idiom: "universal",
167
+ scale: "1x"
168
+ }, {
169
+ idiom: "universal",
170
+ scale: "2x"
171
+ }, {
172
+ idiom: "universal",
173
+ scale: "3x"
174
+ }],
175
+ info: {
176
+ author: "xcode",
177
+ version: 1
178
+ }
179
+ };
180
+ fs.writeFileSync(`${imagesetPath}/Contents.json`, JSON.stringify(contentsJson, null, 2));
181
+ progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}`, "success");
182
+ iconsProcessed++;
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ 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");
188
+ return iconsProcessed;
189
+ } catch (error) {
190
+ if (error.code === "EACCES") throw new Error(`Permission denied accessing notification icons: ${error.message}. Check directory permissions.`);
191
+ if (error.code === "ENOSPC") throw new Error(`Insufficient disk space to process notification icons: ${error.message}`);
192
+ progress.log(`Warning: Could not process notification icons: ${error.message}`, "warning");
193
+ return 0;
194
+ }
195
+ }
196
+ async function processNotificationSounds() {
197
+ try {
198
+ const bundlePath = `${PROJECT_DIR}/${PROJECT_NAME}`;
199
+ const audioFormats = ["mp3", "wav", "m4a", "caf"];
200
+ let soundsProcessed = 0;
201
+ if (!fs.existsSync(PUBLIC_PATH)) {
202
+ progress.log(`Public directory not found at ${PUBLIC_PATH}`, "info");
203
+ return 0;
204
+ }
205
+ for (const sound of NOTIFICATION_SOUNDS) {
206
+ for (const format of audioFormats) {
207
+ const existingSoundPath = `${bundlePath}/${sound.resourceName}.${format}`;
208
+ if (fs.existsSync(existingSoundPath)) {
209
+ fs.unlinkSync(existingSoundPath);
210
+ progress.log(`Removed existing ${sound.resourceName}.${format}`, "info");
211
+ }
212
+ }
213
+ }
214
+ for (const sound of NOTIFICATION_SOUNDS) {
215
+ for (const format of audioFormats) {
216
+ const soundPath = `${PUBLIC_PATH}/${sound.sourceName}.${format}`;
217
+ if (fs.existsSync(soundPath)) {
218
+ validateNotificationAsset(soundPath, "sound");
219
+ fs.copyFileSync(soundPath, `${bundlePath}/${sound.resourceName}.${format}`);
220
+ progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`, "success");
221
+ soundsProcessed++;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ 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");
227
+ return soundsProcessed;
228
+ } catch (error) {
229
+ if (error.code === "EACCES") throw new Error(`Permission denied accessing notification sounds: ${error.message}. Check directory permissions.`);
230
+ if (error.code === "ENOSPC") throw new Error(`Insufficient disk space to process notification sounds: ${error.message}`);
231
+ progress.log(`Warning: Could not process notification sounds: ${error.message}`, "warning");
232
+ return 0;
233
+ }
234
+ }
235
+ async function removeSoundFilesFromXcodeProject() {
236
+ try {
237
+ 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
238
+ let projectContent;
239
+ try {
240
+ projectContent = fs.readFileSync(projectFilePath, "utf8");
241
+ } catch {
242
+ return;
243
+ }
244
+ let modified = false;
245
+ for (const sound of NOTIFICATION_SOUNDS) {
246
+ const p = sound.resourceName;
247
+ 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")];
248
+ for (const regex of regexes) {
249
+ if (regex.test(projectContent)) {
250
+ projectContent = projectContent.replace(regex, "");
251
+ modified = true;
252
+ }
253
+ }
254
+ }
255
+ if (modified) {
256
+ fs.writeFileSync(projectFilePath, projectContent, "utf8");
257
+ progress.log("Removed notification sounds from Xcode project", "success");
258
+ }
259
+ } catch (error) {
260
+ progress.log(`Warning: Could not remove sound files from Xcode project: ${error.message}`, "warning");
261
+ }
262
+ }
263
+ async function addSoundFilesToXcodeProject() {
264
+ try {
265
+ 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
266
+ const bundlePath = `${PROJECT_DIR}/${PROJECT_NAME}`;
267
+ const audioFormats = ["mp3", "wav", "m4a", "caf"];
268
+ const soundFiles = [];
269
+ for (const sound of NOTIFICATION_SOUNDS) {
270
+ for (const format of audioFormats) {
271
+ const soundPath = `${bundlePath}/${sound.resourceName}.${format}`;
272
+ if (fs.existsSync(soundPath)) {
273
+ soundFiles.push({
274
+ filename: `${sound.resourceName}.${format}`,
275
+ resourceName: sound.resourceName,
276
+ format
277
+ });
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ if (soundFiles.length === 0) {
283
+ progress.log("No sound files to add to Xcode project", "info");
284
+ return;
285
+ }
286
+ let projectContent;
287
+ try {
288
+ projectContent = fs.readFileSync(projectFilePath, "utf8");
289
+ } catch {
290
+ throw new Error(`Xcode project file not found at: ${projectFilePath}`);
291
+ }
292
+ for (const soundFile of soundFiles) {
293
+ const fileRefId = generateProjectObjectId(soundFile.filename, "_ref");
294
+ const buildFileId = generateProjectObjectId(soundFile.filename, "_build");
295
+ if (projectContent.includes(`/* ${soundFile.filename} */`)) {
296
+ progress.log(`Sound ${soundFile.filename} already in Xcode project`, "info");
297
+ continue;
298
+ }
299
+ progress.log(`Adding ${soundFile.filename} to Xcode project`, "info");
300
+ const fileRefEntry = `\t\t${fileRefId} /* ${soundFile.filename} */ = {isa = PBXFileReference; lastKnownFileType = audio.${soundFile.format === "mp3" ? "mp3" : "wav"}; path = ${soundFile.filename}; sourceTree = "<group>"; };`;
301
+ projectContent = projectContent.replace(/(\/\* End PBXFileReference section \*\/)/, `${fileRefEntry}\n$1`);
302
+ const buildFileEntry = `\t\t${buildFileId} /* ${soundFile.filename} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${soundFile.filename} */; };`;
303
+ projectContent = projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/, `${buildFileEntry}\n$1`);
304
+ const groupPattern = /(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;
305
+ if (groupPattern.test(projectContent)) projectContent = projectContent.replace(groupPattern, `$1\n\t\t\t\t${fileRefId} /* ${soundFile.filename} */,`);
306
+ const resourcesPattern = /(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;
307
+ if (resourcesPattern.test(projectContent)) projectContent = projectContent.replace(resourcesPattern, `$1\n\t\t\t\t${buildFileId} /* ${soundFile.filename} in Resources */,`);
308
+ progress.log(`✅ Added ${soundFile.filename} to Xcode project`, "success");
309
+ }
310
+ fs.writeFileSync(projectFilePath, projectContent, "utf8");
311
+ progress.log(`Registered ${soundFiles.length} sound file(s) in Xcode project`, "success");
312
+ } catch (error) {
313
+ progress.log(`Warning: Could not add sound files to Xcode project: ${error.message}`, "warning");
314
+ throw error;
315
+ }
316
+ }
317
+ async function cleanupNotificationAssets(removeFirebaseConfig = false) {
318
+ try {
319
+ const assetsPath = `${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;
320
+ const bundlePath = `${PROJECT_DIR}/${PROJECT_NAME}`;
321
+ const audioFormats = ["mp3", "wav", "m4a", "caf"];
322
+ for (const icon of NOTIFICATION_ICONS) {
323
+ const imagesetPath = `${assetsPath}/${icon.resourceName}.imageset`;
324
+ if (fs.existsSync(imagesetPath)) {
325
+ fs.rmSync(imagesetPath, {
326
+ recursive: true,
327
+ force: true
328
+ });
329
+ progress.log(`Removed ${icon.resourceName}.imageset`, "info");
330
+ }
331
+ }
332
+ for (const sound of NOTIFICATION_SOUNDS) {
333
+ for (const format of audioFormats) {
334
+ const soundPath = `${bundlePath}/${sound.resourceName}.${format}`;
335
+ if (fs.existsSync(soundPath)) {
336
+ fs.unlinkSync(soundPath);
337
+ progress.log(`Removed ${sound.resourceName}.${format}`, "info");
338
+ }
339
+ }
340
+ }
341
+ await removeSoundFilesFromXcodeProject();
342
+ if (removeFirebaseConfig) {
343
+ const iosGoogleServicesPath = `${bundlePath}/${GOOGLE_SERVICES_FILENAME}`;
344
+ if (fs.existsSync(iosGoogleServicesPath)) {
345
+ fs.unlinkSync(iosGoogleServicesPath);
346
+ progress.log("Removed GoogleService-Info.plist from iOS project directory", "info");
347
+ }
348
+ await removeGoogleServicesPlistFromXcodeProject();
349
+ }
350
+ progress.log("Cleaned up notification assets", "success");
351
+ } catch (error) {
352
+ progress.log(`Warning: Error cleaning notification assets: ${error.message}`, "warning");
353
+ }
354
+ }
355
+ async function processNotificationAssets(webviewConfig) {
356
+ const hasNotificationConfig = !!webviewConfig.notifications?.enabled;
357
+ try {
358
+ await cleanupNotificationAssets(!hasNotificationConfig);
359
+ if (!hasNotificationConfig) {
360
+ progress.log("Notifications disabled - skipped asset processing", "info");
361
+ return;
362
+ }
363
+ const hasGoogleServices = await handleGoogleServicesPlist();
364
+ if (!hasGoogleServices) progress.log("Continuing without Firebase - only local notifications will work", "warning");
365
+ const iconsProcessed = await processNotificationIcons();
366
+ const soundsProcessed = await processNotificationSounds();
367
+ if (soundsProcessed > 0) await addSoundFilesToXcodeProject();
368
+ const totalAssets = iconsProcessed + soundsProcessed;
369
+ 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");
370
+ } catch (error) {
371
+ progress.log(`Warning: Error processing notifications: ${error.message}`, "warning");
372
+ }
373
+ }
374
+
375
+ // ─── Offline page ─────────────────────────────────────────────────────────
376
+
377
+ async function addOfflinePageToXcodeProject() {
378
+ try {
379
+ const projectFilePath = getXcodeProjectFilePath();
380
+ const offlineFileName = "offline.html";
381
+ const offlineRelativePath = "offline/offline.html";
382
+ let projectContent;
383
+ try {
384
+ projectContent = fs.readFileSync(projectFilePath, "utf8");
385
+ } catch {
386
+ progress.log("Xcode project file not found while adding offline.html", "warning");
387
+ return;
388
+ }
389
+ if (projectContent.includes(`/* ${offlineFileName} */`)) {
390
+ progress.log("offline.html already registered in Xcode project", "info");
391
+ return;
392
+ }
393
+ const fileRefId = generateProjectObjectId(offlineFileName, "_ref");
394
+ const buildFileId = generateProjectObjectId(offlineFileName, "_build");
395
+ const fileRefEntry = `\t\t${fileRefId} /* ${offlineFileName} */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = ${offlineRelativePath}; sourceTree = "<group>"; };`;
396
+ projectContent = projectContent.replace(/(\/\* End PBXFileReference section \*\/)/, `${fileRefEntry}\n$1`);
397
+ const buildFileEntry = `\t\t${buildFileId} /* ${offlineFileName} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${offlineFileName} */; };`;
398
+ projectContent = projectContent.replace(/(\/\* End PBXBuildFile section \*\/)/, `${buildFileEntry}\n$1`);
399
+ const groupPattern = /(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;
400
+ if (groupPattern.test(projectContent)) projectContent = projectContent.replace(groupPattern, `$1\n\t\t\t\t${fileRefId} /* ${offlineFileName} */,`);
401
+ const resourcesPattern = /(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;
402
+ if (resourcesPattern.test(projectContent)) projectContent = projectContent.replace(resourcesPattern, `$1\n\t\t\t\t${buildFileId} /* ${offlineFileName} in Resources */,`);
403
+ fs.writeFileSync(projectFilePath, projectContent, "utf8");
404
+ progress.log("Registered offline.html with Xcode project", "success");
405
+ } catch (error) {
406
+ progress.log(`Warning: Failed to register offline.html in Xcode project: ${error.message}`, "warning");
407
+ }
408
+ }
409
+ async function copyOfflinePage() {
410
+ try {
411
+ const sourcePath = path.join(PUBLIC_PATH, "offline.html"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
412
+ const destDir = `${PROJECT_DIR}/${PROJECT_NAME}/offline`;
413
+ if (!fs.existsSync(sourcePath)) {
414
+ progress.log("offline.html not found in public/; skipping offline asset copy", "warning");
415
+ return false;
416
+ }
417
+ fs.mkdirSync(destDir, {
418
+ recursive: true
419
+ });
420
+ fs.copyFileSync(sourcePath, `${destDir}/offline.html`);
421
+ progress.log("offline.html copied to iOS bundle", "success");
422
+ await addOfflinePageToXcodeProject();
423
+ return true;
424
+ } catch (error) {
425
+ progress.log(`Warning: Error copying offline.html: ${error.message}`, "warning");
426
+ return false;
427
+ }
428
+ }
429
+
430
+ // ─── Splash screen & app icon ─────────────────────────────────────────────
431
+
432
+ async function copySplashscreenAssets() {
433
+ try {
434
+ const publicDir = `${process.cwd()}/public/ios`;
435
+ const assetsDir = `${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;
436
+ if (!WEBVIEW_CONFIG.splashScreen) {
437
+ progress.log("No splash screen configuration found, skipping asset copy", "info");
438
+ return;
439
+ }
440
+ const imageExtensions = ["png", "jpg", "jpeg"];
441
+ let splashImageFound = false;
442
+ for (const ext of imageExtensions) {
443
+ const sourcePath = `${publicDir}/splashscreen.${ext}`;
444
+ if (fs.existsSync(sourcePath)) {
445
+ const imagesetDir = `${assetsDir}/launchscreen.imageset`;
446
+ if (!fs.existsSync(imagesetDir)) fs.mkdirSync(imagesetDir, {
447
+ recursive: true
448
+ });
449
+ fs.copyFileSync(sourcePath, `${imagesetDir}/launchscreen.${ext}`);
450
+ const contentsJson = {
451
+ images: [{
452
+ filename: `launchscreen.${ext}`,
453
+ idiom: "universal",
454
+ scale: "1x"
455
+ }],
456
+ info: {
457
+ author: "xcode",
458
+ version: 1
459
+ }
460
+ };
461
+ fs.writeFileSync(`${imagesetDir}/Contents.json`, JSON.stringify(contentsJson, null, 2));
462
+ progress.log(`Created launch screen imageset: launchscreen.${ext}`, "success");
463
+ splashImageFound = true;
464
+ break;
465
+ }
466
+ }
467
+ if (!splashImageFound) {
468
+ progress.log("No custom splash screen image found in public folder", "info");
469
+ progress.log("Supported formats: splashscreen.png, splashscreen.jpg, splashscreen.jpeg", "info");
470
+ }
471
+ } catch (error) {
472
+ progress.log(`Warning: Error copying splash screen assets: ${error.message}`, "warning");
473
+ }
474
+ }
475
+ async function copyAppIcon() {
476
+ try {
477
+ const publicDir = `${process.cwd()}/public/ios/appIcons`;
478
+ const assetsDir = `${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;
479
+ const iconSetDir = `${assetsDir}/AppIcon.appiconset`;
480
+ if (!fs.existsSync(publicDir)) {
481
+ progress.log("Public directory not found, skipping app icon copy", "info");
482
+ return;
483
+ }
484
+ const iconSizes = [{
485
+ size: "20x20",
486
+ idiom: "iphone",
487
+ scale: "2x"
488
+ }, {
489
+ size: "20x20",
490
+ idiom: "iphone",
491
+ scale: "3x"
492
+ }, {
493
+ size: "29x29",
494
+ idiom: "iphone",
495
+ scale: "2x"
496
+ }, {
497
+ size: "29x29",
498
+ idiom: "iphone",
499
+ scale: "3x"
500
+ }, {
501
+ size: "40x40",
502
+ idiom: "iphone",
503
+ scale: "2x"
504
+ }, {
505
+ size: "40x40",
506
+ idiom: "iphone",
507
+ scale: "3x"
508
+ }, {
509
+ size: "60x60",
510
+ idiom: "iphone",
511
+ scale: "2x"
512
+ }, {
513
+ size: "60x60",
514
+ idiom: "iphone",
515
+ scale: "3x"
516
+ }, {
517
+ size: "1024x1024",
518
+ idiom: "ios-marketing",
519
+ scale: "1x"
520
+ }];
521
+ const imageExtensions = ["png", "jpg", "jpeg"];
522
+ const findImagesRecursively = (dir, extensions) => {
523
+ let results = [];
524
+ try {
525
+ for (const item of fs.readdirSync(dir)) {
526
+ const fullPath = `${dir}${path.sep}${item}`;
527
+ const stat = fs.statSync(fullPath);
528
+ if (stat.isDirectory()) results = results.concat(findImagesRecursively(fullPath, extensions));else if (stat.isFile() && extensions.includes(path.extname(item).toLowerCase().slice(1))) results.push(fullPath);
529
+ }
530
+ } catch (err) {
531
+ /* ignore */
532
+ }
533
+ return results;
534
+ };
535
+ const allImages = findImagesRecursively(publicDir, imageExtensions);
536
+ const foundIcons = [];
537
+ if (!fs.existsSync(iconSetDir)) fs.mkdirSync(iconSetDir, {
538
+ recursive: true
539
+ });
540
+ const contentsPath = `${iconSetDir}/Contents.json`;
541
+ let contents;
542
+ try {
543
+ contents = JSON.parse(fs.readFileSync(contentsPath, "utf8"));
544
+ } catch {
545
+ contents = null;
546
+ }
547
+ if (!contents || !Array.isArray(contents.images)) contents = {
548
+ images: [],
549
+ info: {
550
+ author: "xcode",
551
+ version: 1
552
+ }
553
+ };
554
+ for (const {
555
+ size,
556
+ idiom,
557
+ scale
558
+ } of iconSizes) {
559
+ const expectedName = `icon-${size}-${scale}`;
560
+ let foundImage = null;
561
+ for (const ext of imageExtensions) {
562
+ const matchingImage = allImages.find(imgPath => path.basename(imgPath, `.${ext}`) === expectedName);
563
+ if (matchingImage) {
564
+ foundImage = {
565
+ path: matchingImage,
566
+ ext
567
+ };
568
+ break;
569
+ }
570
+ }
571
+ if (foundImage) {
572
+ const filename = `${expectedName}.${foundImage.ext}`;
573
+ fs.copyFileSync(foundImage.path, `${iconSetDir}/${filename}`);
574
+ foundIcons.push({
575
+ size,
576
+ scale,
577
+ filename,
578
+ idiom
579
+ });
580
+ const iconKey = `${size}-${idiom}-${scale}`;
581
+ contents.images = contents.images.filter(img => `${img.size}-${img.idiom}-${img.scale}` !== iconKey);
582
+ contents.images.push({
583
+ size,
584
+ idiom,
585
+ scale,
586
+ filename
587
+ });
588
+ }
589
+ }
590
+ if (foundIcons.length > 0) {
591
+ fs.writeFileSync(contentsPath, JSON.stringify(contents, null, 2));
592
+ progress.log(`Updated AppIcon.appiconset with ${foundIcons.length} icon(s):`, "success");
593
+ foundIcons.forEach(icon => progress.log(` • ${icon.size} @${icon.scale} (${icon.idiom})`, "info"));
594
+ } else {
595
+ progress.log("No app icon files found in public folder", "info");
596
+ progress.log("Expected naming pattern: icon-{size}-{scale}.{ext}", "info");
597
+ progress.log("Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png", "info");
598
+ }
599
+ } catch (error) {
600
+ progress.log(`Warning: Error copying app icons: ${error.message}`, "warning");
601
+ }
602
+ }
603
+ return {
604
+ processNotificationAssets,
605
+ copyOfflinePage,
606
+ copySplashscreenAssets,
607
+ copyAppIcon
608
+ };
609
+ };