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,7 +1,232 @@
1
- "use strict";const fs=require("fs");const path=require("path");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function readStringArray(value,fieldName,sourcePath,{required=false,nonEmpty=false}={}){if(!Array.isArray(value)){if(required){throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);}return[];}const result=value.map(entry=>mustBeNonEmptyString(entry,`${fieldName}[]`,sourcePath));if(nonEmpty&&result.length===0){throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);}return result;}function readPlainObject(value,fieldName,sourcePath){if(value==null){return{};}if(!value||typeof value!=="object"||Array.isArray(value)){throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);}return value;}function cloneJsonValue(value,fieldName,sourcePath){try{return JSON.parse(JSON.stringify(value));}catch(error){throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);}}function readIosUrlSchemes(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}return{name:entry.name==null?null:mustBeNonEmptyString(entry.name,`${entryField}.name`,sourcePath),schemes:readStringArray(entry.schemes,`${entryField}.schemes`,sourcePath,{required:true,nonEmpty:true})};});}function readIosDependencies(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}const url=mustBeNonEmptyString(entry.url,`${entryField}.url`,sourcePath);const packageIdentity=entry.package==null?derivePackageIdentityFromUrl(url,`${entryField}.url`,sourcePath):mustBeNonEmptyString(entry.package,`${entryField}.package`,sourcePath);const products=readStringArray(entry.products,`${entryField}.products`,sourcePath,{required:true,nonEmpty:true});if(new Set(products).size!==products.length){throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);}const hasFrom=entry.from!=null;const hasExact=entry.exact!=null;if(hasFrom===hasExact){throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);}return{url,package:packageIdentity,products,requirement:hasFrom?{type:"from",version:mustBeNonEmptyString(entry.from,`${entryField}.from`,sourcePath)}:{type:"exact",version:mustBeNonEmptyString(entry.exact,`${entryField}.exact`,sourcePath)}};});}function derivePackageIdentityFromUrl(url,fieldName,sourcePath){const sanitizedUrl=url.replace(/\/+$/,"");const packageIdentity=sanitizedUrl.split("/").pop()?.replace(/\.git$/,"");if(!packageIdentity){throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);}return packageIdentity;}function parsePluginManifest(pluginDir){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the filename is fixed.
2
- const manifestPath=path.join(pluginDir,"manifest.json");if(!fs.existsSync(manifestPath)){return null;}let manifestContent;try{manifestContent=fs.readFileSync(manifestPath,"utf8");}catch(error){throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${error.message}`);}let manifest;try{manifest=JSON.parse(manifestContent);}catch(error){throw new Error(`Invalid JSON in plugin manifest ${manifestPath}: ${error.message}`);}const id=mustBeNonEmptyString(manifest.id,"id",manifestPath);const configKey=mustBeNonEmptyString(manifest.configKey,"configKey",manifestPath);const platforms=readStringArray(manifest.platforms,"platforms",manifestPath,{required:true,nonEmpty:true});const androidConfig=platforms.includes("android")?manifest.android:null;const iosConfig=platforms.includes("ios")?manifest.ios:null;if(platforms.includes("android")&&(!androidConfig||typeof androidConfig!=="object")){throw new Error(`'android' config is required for plugin '${id}' in ${manifestPath}`);}if(platforms.includes("ios")&&(!iosConfig||typeof iosConfig!=="object")){throw new Error(`'ios' config is required for plugin '${id}' in ${manifestPath}`);}return{pluginDir,manifestPath,id,configKey,version:mustBeNonEmptyString(manifest.version,"version",manifestPath),displayName:mustBeNonEmptyString(manifest.displayName,"displayName",manifestPath),description:mustBeNonEmptyString(manifest.description,"description",manifestPath),category:mustBeNonEmptyString(manifest.category,"category",manifestPath),platforms,commands:readStringArray(manifest.commands,"commands",manifestPath,{required:true,nonEmpty:true}),android:androidConfig?{// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
3
- sourceDir:path.join(pluginDir,"android"),permissions:readStringArray(androidConfig.permissions,"android.permissions",manifestPath),dependencies:readStringArray(androidConfig.dependencies,"android.dependencies",manifestPath),className:mustBeNonEmptyString(androidConfig.className,"android.className",manifestPath)}:null,ios:iosConfig?{// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
4
- sourceDir:path.join(pluginDir,"ios"),dependencies:readIosDependencies(iosConfig.dependencies,"ios.dependencies",manifestPath),className:mustBeNonEmptyString(iosConfig.className,"ios.className",manifestPath),infoPlist:cloneJsonValue(readPlainObject(iosConfig.infoPlist,"ios.infoPlist",manifestPath),"ios.infoPlist",manifestPath),urlSchemes:readIosUrlSchemes(iosConfig.urlSchemes,"ios.urlSchemes",manifestPath),querySchemes:readStringArray(iosConfig.querySchemes,"ios.querySchemes",manifestPath),entitlements:cloneJsonValue(readPlainObject(iosConfig.entitlements,"ios.entitlements",manifestPath),"ios.entitlements",manifestPath),resources:readStringArray(iosConfig.resources,"ios.resources",manifestPath)}:null};}function discoverInternalPlugins(corePluginsRoot,log=()=>{}){if(!corePluginsRoot||!isDir(corePluginsRoot)){log(`No internal plugin directory found at ${corePluginsRoot||"<empty>"}`,"info");return[];}const plugins=[];const entries=fs.readdirSync(corePluginsRoot,{withFileTypes:true}).sort((left,right)=>left.name.localeCompare(right.name));for(const entry of entries){if(!entry.isDirectory()){continue;}// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync under the internal plugin root.
5
- const pluginDir=path.join(corePluginsRoot,entry.name);const parsed=parsePluginManifest(pluginDir);if(parsed){plugins.push(parsed);}}log(`Discovered ${plugins.length} internal plugin manifest(s)`,"info");return plugins;}function resolveInternalPluginsRoot(packageRoot){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
6
- const distPluginsPath=path.join(packageRoot,"dist","native","internal-plugins");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
7
- const srcPluginsPath=path.join(packageRoot,"src","native","internal-plugins");return fs.existsSync(distPluginsPath)?distPluginsPath:srcPluginsPath;}function resolvePluginConfig(WEBVIEW_CONFIG){const pluginConfig={};if(WEBVIEW_CONFIG.plugins!=null){if(typeof WEBVIEW_CONFIG.plugins!=="object"||Array.isArray(WEBVIEW_CONFIG.plugins)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}for(const[key,value]of Object.entries(WEBVIEW_CONFIG.plugins)){if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${key}' must be boolean`);}pluginConfig[key]=value;}}return pluginConfig;}module.exports={discoverInternalPlugins,parsePluginManifest,resolvePluginConfig,resolveInternalPluginsRoot};
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ function isDir(dirPath) {
6
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
7
+ }
8
+ function mustBeNonEmptyString(value, fieldName, sourcePath) {
9
+ if (typeof value !== "string" || !value.trim()) {
10
+ throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);
11
+ }
12
+ return value.trim();
13
+ }
14
+ function readStringArray(value, fieldName, sourcePath, {
15
+ required = false,
16
+ nonEmpty = false
17
+ } = {}) {
18
+ if (!Array.isArray(value)) {
19
+ if (required) {
20
+ throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);
21
+ }
22
+ return [];
23
+ }
24
+ const result = value.map(entry => mustBeNonEmptyString(entry, `${fieldName}[]`, sourcePath));
25
+ if (nonEmpty && result.length === 0) {
26
+ throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);
27
+ }
28
+ return result;
29
+ }
30
+ function readPlainObject(value, fieldName, sourcePath) {
31
+ if (value == null) {
32
+ return {};
33
+ }
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);
36
+ }
37
+ return value;
38
+ }
39
+ function cloneJsonValue(value, fieldName, sourcePath) {
40
+ try {
41
+ return JSON.parse(JSON.stringify(value));
42
+ } catch (error) {
43
+ throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);
44
+ }
45
+ }
46
+ function readIosUrlSchemes(value, fieldName, sourcePath) {
47
+ if (value == null) {
48
+ return [];
49
+ }
50
+ if (!Array.isArray(value)) {
51
+ throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);
52
+ }
53
+ return value.map((entry, index) => {
54
+ const entryField = `${fieldName}[${index}]`;
55
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
56
+ throw new Error(`'${entryField}' must be an object in ${sourcePath}`);
57
+ }
58
+ return {
59
+ name: entry.name == null ? null : mustBeNonEmptyString(entry.name, `${entryField}.name`, sourcePath),
60
+ schemes: readStringArray(entry.schemes, `${entryField}.schemes`, sourcePath, {
61
+ required: true,
62
+ nonEmpty: true
63
+ })
64
+ };
65
+ });
66
+ }
67
+ function readIosDependencies(value, fieldName, sourcePath) {
68
+ if (value == null) {
69
+ return [];
70
+ }
71
+ if (!Array.isArray(value)) {
72
+ throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);
73
+ }
74
+ return value.map((entry, index) => {
75
+ const entryField = `${fieldName}[${index}]`;
76
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
77
+ throw new Error(`'${entryField}' must be an object in ${sourcePath}`);
78
+ }
79
+ const url = mustBeNonEmptyString(entry.url, `${entryField}.url`, sourcePath);
80
+ const packageIdentity = entry.package == null ? derivePackageIdentityFromUrl(url, `${entryField}.url`, sourcePath) : mustBeNonEmptyString(entry.package, `${entryField}.package`, sourcePath);
81
+ const products = readStringArray(entry.products, `${entryField}.products`, sourcePath, {
82
+ required: true,
83
+ nonEmpty: true
84
+ });
85
+ if (new Set(products).size !== products.length) {
86
+ throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);
87
+ }
88
+ const hasFrom = entry.from != null;
89
+ const hasExact = entry.exact != null;
90
+ if (hasFrom === hasExact) {
91
+ throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);
92
+ }
93
+ return {
94
+ url,
95
+ package: packageIdentity,
96
+ products,
97
+ requirement: hasFrom ? {
98
+ type: "from",
99
+ version: mustBeNonEmptyString(entry.from, `${entryField}.from`, sourcePath)
100
+ } : {
101
+ type: "exact",
102
+ version: mustBeNonEmptyString(entry.exact, `${entryField}.exact`, sourcePath)
103
+ }
104
+ };
105
+ });
106
+ }
107
+ function derivePackageIdentityFromUrl(url, fieldName, sourcePath) {
108
+ const sanitizedUrl = url.replace(/\/+$/, "");
109
+ const packageIdentity = sanitizedUrl.split("/").pop()?.replace(/\.git$/, "");
110
+ if (!packageIdentity) {
111
+ throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);
112
+ }
113
+ return packageIdentity;
114
+ }
115
+ function parsePluginManifest(pluginDir) {
116
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the filename is fixed.
117
+ const manifestPath = path.join(pluginDir, "manifest.json");
118
+ if (!fs.existsSync(manifestPath)) {
119
+ return null;
120
+ }
121
+ let manifestContent;
122
+ try {
123
+ manifestContent = fs.readFileSync(manifestPath, "utf8");
124
+ } catch (error) {
125
+ throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${error.message}`);
126
+ }
127
+ let manifest;
128
+ try {
129
+ manifest = JSON.parse(manifestContent);
130
+ } catch (error) {
131
+ throw new Error(`Invalid JSON in plugin manifest ${manifestPath}: ${error.message}`);
132
+ }
133
+ const id = mustBeNonEmptyString(manifest.id, "id", manifestPath);
134
+ const configKey = mustBeNonEmptyString(manifest.configKey, "configKey", manifestPath);
135
+ const platforms = readStringArray(manifest.platforms, "platforms", manifestPath, {
136
+ required: true,
137
+ nonEmpty: true
138
+ });
139
+ const androidConfig = platforms.includes("android") ? manifest.android : null;
140
+ const iosConfig = platforms.includes("ios") ? manifest.ios : null;
141
+ if (platforms.includes("android") && (!androidConfig || typeof androidConfig !== "object")) {
142
+ throw new Error(`'android' config is required for plugin '${id}' in ${manifestPath}`);
143
+ }
144
+ if (platforms.includes("ios") && (!iosConfig || typeof iosConfig !== "object")) {
145
+ throw new Error(`'ios' config is required for plugin '${id}' in ${manifestPath}`);
146
+ }
147
+ return {
148
+ pluginDir,
149
+ manifestPath,
150
+ id,
151
+ configKey,
152
+ version: mustBeNonEmptyString(manifest.version, "version", manifestPath),
153
+ displayName: mustBeNonEmptyString(manifest.displayName, "displayName", manifestPath),
154
+ description: mustBeNonEmptyString(manifest.description, "description", manifestPath),
155
+ category: mustBeNonEmptyString(manifest.category, "category", manifestPath),
156
+ platforms,
157
+ commands: readStringArray(manifest.commands, "commands", manifestPath, {
158
+ required: true,
159
+ nonEmpty: true
160
+ }),
161
+ android: androidConfig ? {
162
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
163
+ sourceDir: path.join(pluginDir, "android"),
164
+ permissions: readStringArray(androidConfig.permissions, "android.permissions", manifestPath),
165
+ dependencies: readStringArray(androidConfig.dependencies, "android.dependencies", manifestPath),
166
+ className: mustBeNonEmptyString(androidConfig.className, "android.className", manifestPath)
167
+ } : null,
168
+ ios: iosConfig ? {
169
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
170
+ sourceDir: path.join(pluginDir, "ios"),
171
+ dependencies: readIosDependencies(iosConfig.dependencies, "ios.dependencies", manifestPath),
172
+ className: mustBeNonEmptyString(iosConfig.className, "ios.className", manifestPath),
173
+ infoPlist: cloneJsonValue(readPlainObject(iosConfig.infoPlist, "ios.infoPlist", manifestPath), "ios.infoPlist", manifestPath),
174
+ urlSchemes: readIosUrlSchemes(iosConfig.urlSchemes, "ios.urlSchemes", manifestPath),
175
+ querySchemes: readStringArray(iosConfig.querySchemes, "ios.querySchemes", manifestPath),
176
+ entitlements: cloneJsonValue(readPlainObject(iosConfig.entitlements, "ios.entitlements", manifestPath), "ios.entitlements", manifestPath),
177
+ resources: readStringArray(iosConfig.resources, "ios.resources", manifestPath)
178
+ } : null
179
+ };
180
+ }
181
+ function discoverInternalPlugins(corePluginsRoot, log = () => {}) {
182
+ if (!corePluginsRoot || !isDir(corePluginsRoot)) {
183
+ log(`No internal plugin directory found at ${corePluginsRoot || "<empty>"}`, "info");
184
+ return [];
185
+ }
186
+ const plugins = [];
187
+ const entries = fs.readdirSync(corePluginsRoot, {
188
+ withFileTypes: true
189
+ }).sort((left, right) => left.name.localeCompare(right.name));
190
+ for (const entry of entries) {
191
+ if (!entry.isDirectory()) {
192
+ continue;
193
+ }
194
+
195
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync under the internal plugin root.
196
+ const pluginDir = path.join(corePluginsRoot, entry.name);
197
+ const parsed = parsePluginManifest(pluginDir);
198
+ if (parsed) {
199
+ plugins.push(parsed);
200
+ }
201
+ }
202
+ log(`Discovered ${plugins.length} internal plugin manifest(s)`, "info");
203
+ return plugins;
204
+ }
205
+ function resolveInternalPluginsRoot(packageRoot) {
206
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
207
+ const distPluginsPath = path.join(packageRoot, "dist", "native", "internal-plugins");
208
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
209
+ const srcPluginsPath = path.join(packageRoot, "src", "native", "internal-plugins");
210
+ return fs.existsSync(distPluginsPath) ? distPluginsPath : srcPluginsPath;
211
+ }
212
+ function resolvePluginConfig(WEBVIEW_CONFIG) {
213
+ const pluginConfig = {};
214
+ if (WEBVIEW_CONFIG.plugins != null) {
215
+ if (typeof WEBVIEW_CONFIG.plugins !== "object" || Array.isArray(WEBVIEW_CONFIG.plugins)) {
216
+ throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");
217
+ }
218
+ for (const [key, value] of Object.entries(WEBVIEW_CONFIG.plugins)) {
219
+ if (typeof value !== "boolean") {
220
+ throw new Error(`'WEBVIEW_CONFIG.plugins.${key}' must be boolean`);
221
+ }
222
+ pluginConfig[key] = value;
223
+ }
224
+ }
225
+ return pluginConfig;
226
+ }
227
+ module.exports = {
228
+ discoverInternalPlugins,
229
+ parsePluginManifest,
230
+ resolvePluginConfig,
231
+ resolveInternalPluginsRoot
232
+ };
@@ -54,6 +54,21 @@ public enum CatalystConstants {
54
54
 
55
55
  // MARK: - Bridge Limits / Validation
56
56
  public enum Bridge {
57
+ public static let isDebugBuild: Bool = {
58
+ #if DEBUG
59
+ return true
60
+ #else
61
+ return false
62
+ #endif
63
+ }()
64
+
65
+ public static func isTraceExportEnabled(
66
+ debugBuild: Bool = isDebugBuild,
67
+ profilerEnabled: Bool = ConfigConstants.Profiler.enabled
68
+ ) -> Bool {
69
+ debugBuild && profilerEnabled
70
+ }
71
+
57
72
  // Safety limit for inbound JS message size (128 KB)
58
73
  public static let maxMessageSize: Int = 128 * 1024
59
74
  // Command execution timeout window
@@ -86,6 +101,10 @@ public enum CatalystConstants {
86
101
  "cancelTransition"
87
102
  ]
88
103
 
104
+ if isTraceExportEnabled() {
105
+ commands.insert("exportCatalystTrace")
106
+ }
107
+
89
108
  // Check if notifications are enabled via config
90
109
  // When enabled, CatalystNotifications module will be available at app layer
91
110
  if ConfigConstants.Notifications.enabled {
@@ -22,6 +22,10 @@ public enum ConfigConstants {
22
22
  public static let iosClientId = ""
23
23
  }
24
24
 
25
+ public enum Profiler {
26
+ public static let enabled = false
27
+ }
28
+
25
29
  public static let appInfo = "test-build"
26
30
  public static let cachePattern: [String] = []
27
31
  public static let LOCAL_IP = "localhost" // swiftlint:disable:this identifier_name
@@ -9,6 +9,10 @@
9
9
  import Foundation
10
10
  import os
11
11
 
12
+ enum AppBoot {
13
+ static let nativeStartMs = Int64(ProcessInfo.processInfo.systemUptime * 1000)
14
+ }
15
+
12
16
  #if DEBUG
13
17
  // Global app launch timestamp for all timing measurements
14
18
  let APP_LAUNCH_TIME = CFAbsoluteTimeGetCurrent()
@@ -23,4 +27,4 @@ public func logWithTimestamp(_ message: String) {
23
27
  #else
24
28
  // No-op in release builds
25
29
  public func logWithTimestamp(_ message: String) {}
26
- #endif
30
+ #endif
@@ -201,6 +201,7 @@ public final class CacheManager {
201
201
 
202
202
  private func revalidateResource(request: URLRequest) async {
203
203
  logger.info("🔄 Starting revalidation for: \(request.url?.absoluteString ?? "")")
204
+ let fetchStartMs = CatalystPerf.nativeTimeMs()
204
205
 
205
206
  do {
206
207
  let configuration = URLSessionConfiguration.ephemeral
@@ -210,6 +211,15 @@ public final class CacheManager {
210
211
 
211
212
  if let httpResponse = response as? HTTPURLResponse,
212
213
  isCacheableResponse(httpResponse, for: request) {
214
+ CatalystPerf.add([
215
+ "type": "network-fetch-complete",
216
+ "url": request.url?.absoluteString ?? "",
217
+ "statusCode": httpResponse.statusCode,
218
+ "resourceType": "document",
219
+ "nativeTime": fetchStartMs,
220
+ "nativeStartMs": fetchStartMs,
221
+ "durationMs": CatalystPerf.nativeTimeMs() - fetchStartMs,
222
+ ])
213
223
  storeCachedResponse(httpResponse, data: data, for: request)
214
224
  logger.info("Resource revalidated: \(request.url?.absoluteString ?? "")")
215
225
  }
@@ -0,0 +1,89 @@
1
+ import UIKit
2
+ import WebKit
3
+
4
+ final class DisplayLinkPerfMonitor {
5
+ private weak var webView: WKWebView?
6
+ private var displayLink: CADisplayLink?
7
+ private var dropStartMs: Int64?
8
+ private var minFps: Double = 60
9
+ private var fpsSum: Double = 0
10
+ private var sampleCount = 0
11
+ private var lastTimestamp: CFTimeInterval?
12
+ private var lastStableFps: Double = 60
13
+ private var dropBaselineFps: Double = 60
14
+
15
+ private let dropThresholdFps = 55.0
16
+ private let minDropDurationMs: Int64 = 250
17
+
18
+ init(webView: WKWebView) {
19
+ self.webView = webView
20
+ }
21
+
22
+ func start() {
23
+ guard displayLink == nil else { return }
24
+ let link = CADisplayLink(target: self, selector: #selector(tick(_:)))
25
+ link.add(to: .main, forMode: .common)
26
+ displayLink = link
27
+ }
28
+
29
+ func stop() {
30
+ displayLink?.invalidate()
31
+ displayLink = nil
32
+ dropStartMs = nil
33
+ sampleCount = 0
34
+ fpsSum = 0
35
+ minFps = 60
36
+ lastTimestamp = nil
37
+ lastStableFps = 60
38
+ dropBaselineFps = 60
39
+ }
40
+
41
+ @objc private func tick(_ link: CADisplayLink) {
42
+ defer { lastTimestamp = link.timestamp }
43
+ guard let lastTimestamp else { return }
44
+ let frameDuration = link.timestamp - lastTimestamp
45
+ guard frameDuration > 0 else { return }
46
+ let fps = min(60.0, 1.0 / frameDuration)
47
+ let now = CatalystPerf.nativeTimeMs()
48
+
49
+ if fps < dropThresholdFps {
50
+ if dropStartMs == nil {
51
+ dropStartMs = now
52
+ minFps = fps
53
+ fpsSum = 0
54
+ sampleCount = 0
55
+ dropBaselineFps = lastStableFps
56
+ }
57
+ minFps = min(minFps, fps)
58
+ fpsSum += fps
59
+ sampleCount += 1
60
+ return
61
+ }
62
+
63
+ guard let startMs = dropStartMs else {
64
+ lastStableFps = fps
65
+ return
66
+ }
67
+ let durationMs = now - startMs
68
+ if durationMs >= minDropDurationMs, sampleCount > 0 {
69
+ CatalystPerf.emit([
70
+ "type": "fps-drop-episode",
71
+ "nativeTime": startMs,
72
+ "startNativeTime": startMs,
73
+ "endNativeTime": now,
74
+ "durationMs": durationMs,
75
+ "minFps": minFps,
76
+ "avgFps": fpsSum / Double(sampleCount),
77
+ "deltaFps": dropBaselineFps - minFps,
78
+ "baselineFps": dropBaselineFps,
79
+ "thread": Thread.current.name ?? "main",
80
+ ], to: webView)
81
+ }
82
+
83
+ lastStableFps = fps
84
+ dropStartMs = nil
85
+ sampleCount = 0
86
+ fpsSum = 0
87
+ minFps = 60
88
+ }
89
+ }