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
@@ -31,6 +31,10 @@ public struct WebView: UIViewRepresentable, Equatable {
31
31
 
32
32
  let totalTime = (CFAbsoluteTimeGetCurrent() - start) * 1000
33
33
  logWithTimestamp("🏗️ WebView init completed (total: \(String(format: "%.2f", totalTime))ms, protocol: \(String(format: "%.2f", protocolTime))ms)")
34
+ CatalystPerf.add([
35
+ "type": "boot-activity-created",
36
+ "durationMs": totalTime,
37
+ ])
34
38
  }
35
39
 
36
40
  public func makeUIView(context: Context) -> WKWebView {
@@ -54,12 +58,33 @@ public struct WebView: UIViewRepresentable, Equatable {
54
58
  preferences.allowsContentJavaScript = true
55
59
  configuration.defaultWebpagePreferences = preferences
56
60
 
61
+ #if DEBUG
62
+ if ConfigConstants.Profiler.enabled {
63
+ configuration.userContentController.addUserScript(
64
+ WKUserScript(
65
+ source: "window.__CATALYST_PROFILER_ENABLED = true;",
66
+ injectionTime: .atDocumentStart,
67
+ forMainFrameOnly: true
68
+ )
69
+ )
70
+ }
71
+ #endif
72
+
57
73
  let webViewCreateStart = CFAbsoluteTimeGetCurrent()
58
74
  let webView = WKWebView(frame: .zero, configuration: configuration)
59
75
  let webViewCreateTime = (CFAbsoluteTimeGetCurrent() - webViewCreateStart) * 1000
60
76
  logWithTimestamp("📦 WKWebView created (took \(String(format: "%.2f", webViewCreateTime))ms)")
77
+ CatalystPerf.add([
78
+ "type": "boot-webview-constructed",
79
+ "durationMs": webViewCreateTime,
80
+ ])
61
81
 
62
82
  webView.navigationDelegate = navigationDelegate
83
+ #if DEBUG
84
+ if ConfigConstants.Profiler.enabled {
85
+ webView.scrollView.delegate = context.coordinator
86
+ }
87
+ #endif
63
88
  webView.allowsBackForwardNavigationGestures = true
64
89
  webView.isOpaque = false
65
90
  webView.backgroundColor = .clear
@@ -101,6 +126,10 @@ public struct WebView: UIViewRepresentable, Equatable {
101
126
  let request = URLRequest(url: url)
102
127
  logWithTimestamp("🚀 Calling webView.load()")
103
128
  let loadStart = CFAbsoluteTimeGetCurrent()
129
+ CatalystPerf.add([
130
+ "type": "boot-load-url",
131
+ "url": url.absoluteString,
132
+ ])
104
133
  webView.load(request)
105
134
  let loadTime = (CFAbsoluteTimeGetCurrent() - loadStart) * 1000
106
135
  logWithTimestamp("✅ webView.load() returned (took \(String(format: "%.2f", loadTime))ms)")
@@ -118,6 +147,11 @@ public struct WebView: UIViewRepresentable, Equatable {
118
147
 
119
148
  let makeUIViewTime = (CFAbsoluteTimeGetCurrent() - makeUIViewStart) * 1000
120
149
  logWithTimestamp("🔨 makeUIView() completed (took \(String(format: "%.2f", makeUIViewTime))ms)")
150
+ #if DEBUG
151
+ if ConfigConstants.Profiler.enabled {
152
+ context.coordinator.startKeyboardPerfTracking(webView)
153
+ }
154
+ #endif
121
155
 
122
156
  return webView
123
157
  }
@@ -145,6 +179,9 @@ public struct WebView: UIViewRepresentable, Equatable {
145
179
  }
146
180
 
147
181
  // Clean up native bridge
182
+ coordinator.stopPerfMonitoring()
183
+ coordinator.stopKeyboardPerfTracking()
184
+ webView.scrollView.delegate = nil
148
185
  coordinator.nativeBridge?.unregister()
149
186
  coordinator.nativeBridge = nil
150
187
  coordinator.pluginBridge?.unregister()
@@ -163,6 +200,10 @@ public struct WebView: UIViewRepresentable, Equatable {
163
200
  var pluginBridge: PluginBridge?
164
201
  var hostingController: UIViewController?
165
202
  var isObserverAdded = false
203
+ private weak var perfWebView: WKWebView?
204
+ private var keyboardObserverTokens: [NSObjectProtocol] = []
205
+ private var scrollSessionOpen = false
206
+ private var fpsMonitor: DisplayLinkPerfMonitor?
166
207
 
167
208
  public init(_ parent: WebView) {
168
209
  self.parent = parent
@@ -189,6 +230,17 @@ public struct WebView: UIViewRepresentable, Equatable {
189
230
  pluginBridge.register()
190
231
  self.nativeBridge = bridge
191
232
  self.pluginBridge = pluginBridge
233
+ #if DEBUG
234
+ if ConfigConstants.Profiler.enabled {
235
+ self.fpsMonitor = DisplayLinkPerfMonitor(webView: webView)
236
+ self.fpsMonitor?.start()
237
+ }
238
+ #endif
239
+ }
240
+
241
+ func stopPerfMonitoring() {
242
+ fpsMonitor?.stop()
243
+ fpsMonitor = nil
192
244
  }
193
245
 
194
246
  @objc func handleCameraPinch(_ gesture: UIPinchGestureRecognizer) {
@@ -218,9 +270,83 @@ public struct WebView: UIViewRepresentable, Equatable {
218
270
 
219
271
  deinit {
220
272
  // Ensure observer is removed even if dismantleUIView is skipped.
273
+ CatalystPerf.reset()
274
+ stopPerfMonitoring()
275
+ stopKeyboardPerfTracking()
221
276
  if isObserverAdded, let webView = nativeBridge?.webView {
222
277
  webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
223
278
  }
224
279
  }
225
280
  }
226
281
  }
282
+
283
+ extension WebView.Coordinator: UIScrollViewDelegate {
284
+ public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
285
+ guard !scrollSessionOpen else { return }
286
+ scrollSessionOpen = true
287
+ CatalystPerf.emit([
288
+ "type": "scroll-start",
289
+ "nativeTime": CatalystPerf.nativeTimeMs(),
290
+ ], to: perfWebView)
291
+ }
292
+
293
+ public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
294
+ if !decelerate {
295
+ closeScrollSession()
296
+ }
297
+ }
298
+
299
+ public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
300
+ closeScrollSession()
301
+ }
302
+
303
+ private func closeScrollSession() {
304
+ guard scrollSessionOpen else { return }
305
+ scrollSessionOpen = false
306
+ CatalystPerf.emit([
307
+ "type": "scroll-end",
308
+ "nativeTime": CatalystPerf.nativeTimeMs(),
309
+ ], to: perfWebView)
310
+ }
311
+
312
+ func startKeyboardPerfTracking(_ webView: WKWebView) {
313
+ perfWebView = webView
314
+ stopKeyboardPerfTracking()
315
+
316
+ let center = NotificationCenter.default
317
+ keyboardObserverTokens = [
318
+ center.addObserver(
319
+ forName: UIResponder.keyboardWillShowNotification,
320
+ object: nil,
321
+ queue: .main
322
+ ) { [weak self] notification in
323
+ self?.emitKeyboardEvent("keyboard-show", notification: notification)
324
+ },
325
+ center.addObserver(
326
+ forName: UIResponder.keyboardWillHideNotification,
327
+ object: nil,
328
+ queue: .main
329
+ ) { [weak self] notification in
330
+ self?.emitKeyboardEvent("keyboard-hide", notification: notification)
331
+ },
332
+ ]
333
+ }
334
+
335
+ func stopKeyboardPerfTracking() {
336
+ let center = NotificationCenter.default
337
+ keyboardObserverTokens.forEach { center.removeObserver($0) }
338
+ keyboardObserverTokens.removeAll()
339
+ perfWebView = nil
340
+ }
341
+
342
+ private func emitKeyboardEvent(_ type: String, notification: Notification) {
343
+ let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
344
+ let durationSeconds = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
345
+ CatalystPerf.emit([
346
+ "type": type,
347
+ "nativeTime": CatalystPerf.nativeTimeMs(),
348
+ "keyboardHeight": frame?.height ?? 0,
349
+ "durationMs": Int((durationSeconds ?? 0) * 1000),
350
+ ], to: perfWebView)
351
+ }
352
+ }
@@ -13,6 +13,9 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
13
13
  private var lastTargetURL: URL?
14
14
  private let initialURL: URL?
15
15
  private weak var cameraManager: NativeCameraManager?
16
+ private var pageLoadStartMs: Int64?
17
+ private var didEmitColdStart = false
18
+ private var hasStartedProfilerNavigation = false
16
19
 
17
20
  init(viewModel: WebViewModel, initialURL: URL?, cameraManager: NativeCameraManager? = nil) {
18
21
  self.viewModel = viewModel
@@ -155,15 +158,26 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
155
158
  }
156
159
 
157
160
  if isCacheableMethod && CacheManager.shared.shouldCacheURL(url) {
158
- logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
161
+ logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
159
162
 
163
+ let cacheStartMs = CatalystPerf.nativeTimeMs()
160
164
  let (cachedData, cacheState, mimeType) = await CacheManager.shared.getCachedResource(
161
165
  for: navigationAction.request
162
166
  )
167
+ let cacheDurationMs = CatalystPerf.nativeTimeMs() - cacheStartMs
163
168
 
164
169
  switch cacheState {
165
170
  case .fresh, .stale:
166
171
  logger.info("✅ Serving fresh/stale cached content")
172
+ CatalystPerf.add([
173
+ "type": "cache-hit-memory",
174
+ "url": url.absoluteString,
175
+ "resourceType": "document",
176
+ "nativeTime": cacheStartMs,
177
+ "nativeStartMs": cacheStartMs,
178
+ "durationMs": cacheDurationMs,
179
+ "source": cacheState == .fresh ? "fresh" : "stale",
180
+ ])
167
181
 
168
182
  if let cachedData = cachedData,
169
183
  let mimeType = mimeType {
@@ -182,6 +196,14 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
182
196
 
183
197
  case .expired:
184
198
  logger.info("♻️ Cache expired, fetching fresh content")
199
+ CatalystPerf.add([
200
+ "type": "cache-miss-fetch",
201
+ "url": url.absoluteString,
202
+ "resourceType": "document",
203
+ "nativeTime": cacheStartMs,
204
+ "nativeStartMs": cacheStartMs,
205
+ "durationMs": cacheDurationMs,
206
+ ])
185
207
  break
186
208
  }
187
209
  } else {
@@ -235,6 +257,25 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
235
257
 
236
258
  func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
237
259
  logWithTimestamp("📡 didStartProvisionalNavigation - loading started")
260
+ if hasStartedProfilerNavigation {
261
+ CatalystPerf.reset()
262
+ }
263
+ hasStartedProfilerNavigation = true
264
+ let nativeNow = CatalystPerf.nativeTimeMs()
265
+ pageLoadStartMs = nativeNow
266
+ CatalystPerf.injectNativeTimeOffset(into: webView)
267
+ if let url = webView.url?.absoluteString ?? lastTargetURL?.absoluteString {
268
+ CatalystPerf.add([
269
+ "type": "boot-page-started",
270
+ "nativeTime": nativeNow,
271
+ "url": url,
272
+ ])
273
+ CatalystPerf.add([
274
+ "type": "page-load-start",
275
+ "nativeTime": nativeNow,
276
+ "url": url,
277
+ ])
278
+ }
238
279
  // Stop camera on navigation — mirrors Android CustomWebview onPageStarted lambda
239
280
  cameraManager?.stop()
240
281
  Task { @MainActor in
@@ -261,6 +302,32 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
261
302
 
262
303
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
263
304
  logWithTimestamp("🎉 didFinish - page fully loaded")
305
+ let nativeNow = CatalystPerf.nativeTimeMs()
306
+ let url = webView.url?.absoluteString ?? lastTargetURL?.absoluteString ?? ""
307
+ let durationMs = pageLoadStartMs.map { nativeNow - $0 } ?? 0
308
+ CatalystPerf.add([
309
+ "type": "boot-page-finished",
310
+ "nativeTime": nativeNow,
311
+ "url": url,
312
+ ])
313
+ CatalystPerf.add([
314
+ "type": "page-load-end",
315
+ "nativeTime": nativeNow,
316
+ "url": url,
317
+ "durationMs": durationMs,
318
+ ])
319
+ if !didEmitColdStart {
320
+ didEmitColdStart = true
321
+ CatalystPerf.add([
322
+ "type": "cold-start",
323
+ "nativeTime": nativeNow,
324
+ "url": url,
325
+ "durationMs": nativeNow - AppBoot.nativeStartMs,
326
+ ])
327
+ }
328
+ CatalystPerf.memorySnapshot(to: nil, label: "page-finish")
329
+ CatalystPerf.scheduleFlush(webView)
330
+
264
331
  Task { @MainActor in
265
332
  if let url = webView.url {
266
333
  if !isOfflinePageURL(url) {
@@ -287,6 +354,13 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
287
354
  }
288
355
 
289
356
  private func handleNavigationError(_ error: Error, webView: WKWebView?) {
357
+ CatalystPerf.emit([
358
+ "type": "page-load-error",
359
+ "nativeTime": CatalystPerf.nativeTimeMs(),
360
+ "url": webView?.url?.absoluteString ?? lastTargetURL?.absoluteString ?? "",
361
+ "description": error.localizedDescription,
362
+ ], to: webView)
363
+
290
364
  Task { @MainActor in
291
365
  viewModel.reset()
292
366
  logWithTimestamp("🔴 Navigation error: \(error.localizedDescription)")
@@ -386,6 +386,19 @@ final class BridgeMessageValidatorTests: XCTestCase {
386
386
  XCTAssertEqual(result.command, "openCamera")
387
387
  }
388
388
 
389
+ func testTraceExportAvailabilityRequiresDebugProfiler() {
390
+ XCTAssertTrue(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: true, profilerEnabled: true))
391
+ XCTAssertFalse(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: false, profilerEnabled: true))
392
+ XCTAssertFalse(CatalystConstants.Bridge.isTraceExportEnabled(debugBuild: true, profilerEnabled: false))
393
+ }
394
+
395
+ func testTraceExportAllowlistMatchesAvailabilityGate() {
396
+ XCTAssertEqual(
397
+ CatalystConstants.Bridge.validCommands.contains("exportCatalystTrace"),
398
+ CatalystConstants.Bridge.isTraceExportEnabled()
399
+ )
400
+ }
401
+
389
402
  // ========================================
390
403
  // MARK: - Helper Methods
391
404
  // ========================================
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -1 +1,227 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;class PluginNativeBridge{constructor(){this.handlers=new Map();this.requestCount=0;this.isInitialized=false;}createRequestId=()=>{this.requestCount+=1;return`plugin_${Date.now()}_${this.requestCount}`;};normalizeRequiredString=(value,fieldName)=>{if(typeof value!=="string"||!value.trim()){throw new Error(`${fieldName} must be a non-empty string`);}return value.trim();};normalizeCommand=command=>{if(command==null){return null;}return this.normalizeRequiredString(command,"command");};getHandlerKey=({pluginId,eventName,command=null}={})=>{return JSON.stringify([this.normalizeRequiredString(pluginId,"pluginId"),this.normalizeRequiredString(eventName,"eventName"),this.normalizeCommand(command)]);};getHandlerSet=(key,create=false)=>{let handlers=this.handlers.get(key);if(!handlers&&create){handlers=new Set();this.handlers.set(key,handlers);}return handlers||null;};addHandler=({pluginId,eventName,command}={},handler)=>{if(typeof handler!=="function"){throw new Error("handler must be a function");}this.getHandlerSet(this.getHandlerKey({pluginId,eventName,command}),true).add(handler);};removeHandler=({pluginId,eventName,command}={},handler)=>{const key=this.getHandlerKey({pluginId,eventName,command});const handlers=this.getHandlerSet(key,false);if(!handlers){return false;}if(handler==null){handlers.clear();this.handlers.delete(key);return true;}const removed=handlers.delete(handler);if(handlers.size===0){this.handlers.delete(key);}return removed;};ensureInitialized=()=>{if(typeof window==="undefined"||this.isInitialized){return this;}window.PluginBridgeWeb=window.PluginBridgeWeb||{};window.PluginBridgeWeb.callback=this.dispatchCallback;window.PluginBridgeWeb.dispatch=message=>{let parsed=message;if(typeof parsed==="string"){try{parsed=JSON.parse(parsed);}catch{return false;}}if(!parsed||typeof parsed!=="object"){return false;}const payload=Object.prototype.hasOwnProperty.call(parsed,"payload")?parsed.payload:null;return this.dispatchCallback(parsed.pluginId,parsed.eventName,payload,parsed.requestId??null,parsed.command??null);};this.isInitialized=true;return this;};hasAndroidBridge=()=>typeof window!=="undefined"&&!!window.PluginBridge;hasIOSBridge=()=>typeof window!=="undefined"&&!!window.webkit?.messageHandlers?.PluginBridge;init=()=>this.ensureInitialized();assertInitialized=()=>{if(!this.isInitialized){throw new Error("PluginBridge.init() must be called before using emit() or register()");}};reportHandlerError=error=>{if(typeof queueMicrotask==="function"){queueMicrotask(()=>{throw error;});return;}setTimeout(()=>{throw error;},0);};emit=({pluginId,command,data=null}={})=>{const normalizedPluginId=this.normalizeRequiredString(pluginId,"pluginId");const normalizedCommand=this.normalizeRequiredString(command,"command");if(typeof window==="undefined"){throw new Error("PluginBridge is not available in this environment");}this.assertInitialized();const payload={pluginId:normalizedPluginId,command:normalizedCommand,data,requestId:this.createRequestId()};if(this.hasAndroidBridge()){window.PluginBridge.emit(JSON.stringify(payload));return;}if(this.hasIOSBridge()){window.webkit.messageHandlers.PluginBridge.postMessage(payload);return;}throw new Error("PluginBridge is not available in this environment");};register=({pluginId,eventName,command,handler}={})=>{this.assertInitialized();this.addHandler({pluginId,eventName,command},handler);return()=>this.removeHandler({pluginId,eventName,command},handler);};unregister=({pluginId,eventName,command,handler}={})=>{if(typeof handler!=="function"){throw new Error("handler must be a function");}return this.removeHandler({pluginId,eventName,command},handler);};dispatchCallback=(pluginId,eventName,payload,requestId=null,command=null)=>{void requestId;const normalizedPluginId=typeof pluginId==="string"&&pluginId.trim()?pluginId.trim():null;const normalizedEventName=typeof eventName==="string"&&eventName.trim()?eventName.trim():null;if(normalizedPluginId==null||normalizedEventName==null){return false;}const normalizedCommand=typeof command==="string"&&command.trim()?command.trim():null;const toCall=new Set();const collectHandlers=commandScope=>{const handlers=this.getHandlerSet(JSON.stringify([normalizedPluginId,normalizedEventName,commandScope]),false);if(!handlers){return;}handlers.forEach(handler=>{toCall.add(handler);});};if(normalizedCommand!=null){collectHandlers(normalizedCommand);}collectHandlers(null);if(toCall.size===0){return false;}const meta={pluginId:normalizedPluginId,eventName:normalizedEventName,command:normalizedCommand};toCall.forEach(handler=>{try{handler(payload,meta);}catch(error){this.reportHandlerError(error);}});return true;};}const pluginNativeBridge=new PluginNativeBridge();var _default=exports.default=pluginNativeBridge;
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ class PluginNativeBridge {
8
+ constructor() {
9
+ this.handlers = new Map();
10
+ this.requestCount = 0;
11
+ this.isInitialized = false;
12
+ }
13
+ createRequestId = () => {
14
+ this.requestCount += 1;
15
+ return `plugin_${Date.now()}_${this.requestCount}`;
16
+ };
17
+ normalizeRequiredString = (value, fieldName) => {
18
+ if (typeof value !== "string" || !value.trim()) {
19
+ throw new Error(`${fieldName} must be a non-empty string`);
20
+ }
21
+ return value.trim();
22
+ };
23
+ normalizeCommand = command => {
24
+ if (command == null) {
25
+ return null;
26
+ }
27
+ return this.normalizeRequiredString(command, "command");
28
+ };
29
+ getHandlerKey = ({
30
+ pluginId,
31
+ eventName,
32
+ command = null
33
+ } = {}) => {
34
+ return JSON.stringify([this.normalizeRequiredString(pluginId, "pluginId"), this.normalizeRequiredString(eventName, "eventName"), this.normalizeCommand(command)]);
35
+ };
36
+ getHandlerSet = (key, create = false) => {
37
+ let handlers = this.handlers.get(key);
38
+ if (!handlers && create) {
39
+ handlers = new Set();
40
+ this.handlers.set(key, handlers);
41
+ }
42
+ return handlers || null;
43
+ };
44
+ addHandler = ({
45
+ pluginId,
46
+ eventName,
47
+ command
48
+ } = {}, handler) => {
49
+ if (typeof handler !== "function") {
50
+ throw new Error("handler must be a function");
51
+ }
52
+ this.getHandlerSet(this.getHandlerKey({
53
+ pluginId,
54
+ eventName,
55
+ command
56
+ }), true).add(handler);
57
+ };
58
+ removeHandler = ({
59
+ pluginId,
60
+ eventName,
61
+ command
62
+ } = {}, handler) => {
63
+ const key = this.getHandlerKey({
64
+ pluginId,
65
+ eventName,
66
+ command
67
+ });
68
+ const handlers = this.getHandlerSet(key, false);
69
+ if (!handlers) {
70
+ return false;
71
+ }
72
+ if (handler == null) {
73
+ handlers.clear();
74
+ this.handlers.delete(key);
75
+ return true;
76
+ }
77
+ const removed = handlers.delete(handler);
78
+ if (handlers.size === 0) {
79
+ this.handlers.delete(key);
80
+ }
81
+ return removed;
82
+ };
83
+ ensureInitialized = () => {
84
+ if (typeof window === "undefined" || this.isInitialized) {
85
+ return this;
86
+ }
87
+ window.PluginBridgeWeb = window.PluginBridgeWeb || {};
88
+ window.PluginBridgeWeb.callback = this.dispatchCallback;
89
+ window.PluginBridgeWeb.dispatch = message => {
90
+ let parsed = message;
91
+ if (typeof parsed === "string") {
92
+ try {
93
+ parsed = JSON.parse(parsed);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ if (!parsed || typeof parsed !== "object") {
99
+ return false;
100
+ }
101
+ const payload = Object.prototype.hasOwnProperty.call(parsed, "payload") ? parsed.payload : null;
102
+ return this.dispatchCallback(parsed.pluginId, parsed.eventName, payload, parsed.requestId ?? null, parsed.command ?? null);
103
+ };
104
+ this.isInitialized = true;
105
+ return this;
106
+ };
107
+ hasAndroidBridge = () => typeof window !== "undefined" && !!window.PluginBridge;
108
+ hasIOSBridge = () => typeof window !== "undefined" && !!window.webkit?.messageHandlers?.PluginBridge;
109
+ init = () => this.ensureInitialized();
110
+ assertInitialized = () => {
111
+ if (!this.isInitialized) {
112
+ throw new Error("PluginBridge.init() must be called before using emit() or register()");
113
+ }
114
+ };
115
+ reportHandlerError = error => {
116
+ if (typeof queueMicrotask === "function") {
117
+ queueMicrotask(() => {
118
+ throw error;
119
+ });
120
+ return;
121
+ }
122
+ setTimeout(() => {
123
+ throw error;
124
+ }, 0);
125
+ };
126
+ emit = ({
127
+ pluginId,
128
+ command,
129
+ data = null
130
+ } = {}) => {
131
+ const normalizedPluginId = this.normalizeRequiredString(pluginId, "pluginId");
132
+ const normalizedCommand = this.normalizeRequiredString(command, "command");
133
+ if (typeof window === "undefined") {
134
+ throw new Error("PluginBridge is not available in this environment");
135
+ }
136
+ this.assertInitialized();
137
+ const payload = {
138
+ pluginId: normalizedPluginId,
139
+ command: normalizedCommand,
140
+ data,
141
+ requestId: this.createRequestId()
142
+ };
143
+ if (this.hasAndroidBridge()) {
144
+ window.PluginBridge.emit(JSON.stringify(payload));
145
+ return;
146
+ }
147
+ if (this.hasIOSBridge()) {
148
+ window.webkit.messageHandlers.PluginBridge.postMessage(payload);
149
+ return;
150
+ }
151
+ throw new Error("PluginBridge is not available in this environment");
152
+ };
153
+ register = ({
154
+ pluginId,
155
+ eventName,
156
+ command,
157
+ handler
158
+ } = {}) => {
159
+ this.assertInitialized();
160
+ this.addHandler({
161
+ pluginId,
162
+ eventName,
163
+ command
164
+ }, handler);
165
+ return () => this.removeHandler({
166
+ pluginId,
167
+ eventName,
168
+ command
169
+ }, handler);
170
+ };
171
+ unregister = ({
172
+ pluginId,
173
+ eventName,
174
+ command,
175
+ handler
176
+ } = {}) => {
177
+ if (typeof handler !== "function") {
178
+ throw new Error("handler must be a function");
179
+ }
180
+ return this.removeHandler({
181
+ pluginId,
182
+ eventName,
183
+ command
184
+ }, handler);
185
+ };
186
+ dispatchCallback = (pluginId, eventName, payload, requestId = null, command = null) => {
187
+ void requestId;
188
+ const normalizedPluginId = typeof pluginId === "string" && pluginId.trim() ? pluginId.trim() : null;
189
+ const normalizedEventName = typeof eventName === "string" && eventName.trim() ? eventName.trim() : null;
190
+ if (normalizedPluginId == null || normalizedEventName == null) {
191
+ return false;
192
+ }
193
+ const normalizedCommand = typeof command === "string" && command.trim() ? command.trim() : null;
194
+ const toCall = new Set();
195
+ const collectHandlers = commandScope => {
196
+ const handlers = this.getHandlerSet(JSON.stringify([normalizedPluginId, normalizedEventName, commandScope]), false);
197
+ if (!handlers) {
198
+ return;
199
+ }
200
+ handlers.forEach(handler => {
201
+ toCall.add(handler);
202
+ });
203
+ };
204
+ if (normalizedCommand != null) {
205
+ collectHandlers(normalizedCommand);
206
+ }
207
+ collectHandlers(null);
208
+ if (toCall.size === 0) {
209
+ return false;
210
+ }
211
+ const meta = {
212
+ pluginId: normalizedPluginId,
213
+ eventName: normalizedEventName,
214
+ command: normalizedCommand
215
+ };
216
+ toCall.forEach(handler => {
217
+ try {
218
+ handler(payload, meta);
219
+ } catch (error) {
220
+ this.reportHandlerError(error);
221
+ }
222
+ });
223
+ return true;
224
+ };
225
+ }
226
+ const pluginNativeBridge = new PluginNativeBridge();
227
+ var _default = exports.default = pluginNativeBridge;