catalyst-core 0.2.0-beta.1 → 0.2.0-beta.3

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 (67) hide show
  1. package/dist/native/androidProject/app/build.gradle.kts +26 -8
  2. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +381 -78
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +29 -8
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +7 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +162 -26
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  13. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
  14. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  15. package/dist/native/bridge/WebBridge.js +2 -2
  16. package/dist/native/bridge/constants/NativeInterfaces.js +2 -1
  17. package/dist/native/bridge/perf/collectors/ApiCollector.js +22 -0
  18. package/dist/native/bridge/perf/collectors/CacheCollector.js +40 -0
  19. package/dist/native/bridge/perf/collectors/HookCollector.js +43 -0
  20. package/dist/native/bridge/perf/collectors/InsightsCollector.js +47 -0
  21. package/dist/native/bridge/perf/collectors/InteractionCollector.js +38 -0
  22. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +25 -0
  23. package/dist/native/bridge/perf/collectors/NavigationCollector.js +53 -0
  24. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +7 -0
  25. package/dist/native/bridge/perf/collectors/RenderCollector.js +40 -0
  26. package/dist/native/bridge/perf/collectors/ScrollCollector.js +25 -0
  27. package/dist/native/bridge/perf/collectors/ThreadCollector.js +27 -0
  28. package/dist/native/bridge/perf/core/constants.js +43 -0
  29. package/dist/native/bridge/perf/core/measure.js +40 -0
  30. package/dist/native/bridge/perf/core/session.js +34 -0
  31. package/dist/native/bridge/perf/core/store.js +1 -0
  32. package/dist/native/bridge/perf/core/summary.js +1 -0
  33. package/dist/native/bridge/perf/core/traceExport.js +1 -0
  34. package/dist/native/bridge/perf/core/waterfalls.js +1 -0
  35. package/dist/native/bridge/perf/index.js +81 -0
  36. package/dist/native/buildAndroid/assets.js +2 -2
  37. package/dist/native/buildIos/config.js +1 -1
  38. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  39. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  40. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  41. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +49 -4
  42. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  43. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
  44. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
  45. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  46. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
  47. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
  48. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
  49. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  50. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  51. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  52. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +141 -1
  53. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +157 -3
  54. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  55. package/dist/native/renameAndroidProject.js +2 -2
  56. package/dist/offline/catalyst-sw.js +1 -0
  57. package/dist/offline/registerServiceWorker.js +1 -0
  58. package/dist/router/ClientRouter.js +1 -1
  59. package/dist/scripts/build.js +1 -1
  60. package/dist/scripts/devBuild.js +1 -1
  61. package/dist/scripts/generateOfflineManifest.js +1 -0
  62. package/dist/server/expressServer.js +2 -1
  63. package/dist/server/renderer/document/Body.js +1 -1
  64. package/dist/server/renderer/handler.js +3 -3
  65. package/dist/webpack/base.babel.js +1 -1
  66. package/mcp_v2/knowledge-base.json +13 -0
  67. package/package.json +1 -1
@@ -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 {
@@ -38,6 +42,14 @@ public struct WebView: UIViewRepresentable, Equatable {
38
42
  logWithTimestamp("🔨 makeUIView() started")
39
43
 
40
44
  let configuration = WKWebViewConfiguration()
45
+ configuration.setURLSchemeHandler(
46
+ OfflineURLSchemeHandler.shared,
47
+ forURLScheme: OfflineCacheService.offlineHTTPScheme
48
+ )
49
+ configuration.setURLSchemeHandler(
50
+ OfflineURLSchemeHandler.shared,
51
+ forURLScheme: OfflineCacheService.offlineHTTPSScheme
52
+ )
41
53
 
42
54
  // Hook kept for legacy behavior; currently a no-op on iOS 15+.
43
55
  WebKitConfig.applySharedProcessPoolIfNeeded(to: configuration)
@@ -46,12 +58,33 @@ public struct WebView: UIViewRepresentable, Equatable {
46
58
  preferences.allowsContentJavaScript = true
47
59
  configuration.defaultWebpagePreferences = preferences
48
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
+
49
73
  let webViewCreateStart = CFAbsoluteTimeGetCurrent()
50
74
  let webView = WKWebView(frame: .zero, configuration: configuration)
51
75
  let webViewCreateTime = (CFAbsoluteTimeGetCurrent() - webViewCreateStart) * 1000
52
76
  logWithTimestamp("📦 WKWebView created (took \(String(format: "%.2f", webViewCreateTime))ms)")
77
+ CatalystPerf.add([
78
+ "type": "boot-webview-constructed",
79
+ "durationMs": webViewCreateTime,
80
+ ])
53
81
 
54
82
  webView.navigationDelegate = navigationDelegate
83
+ #if DEBUG
84
+ if ConfigConstants.Profiler.enabled {
85
+ webView.scrollView.delegate = context.coordinator
86
+ }
87
+ #endif
55
88
  webView.allowsBackForwardNavigationGestures = true
56
89
  webView.isOpaque = false
57
90
  webView.backgroundColor = .clear
@@ -93,17 +126,32 @@ public struct WebView: UIViewRepresentable, Equatable {
93
126
  let request = URLRequest(url: url)
94
127
  logWithTimestamp("🚀 Calling webView.load()")
95
128
  let loadStart = CFAbsoluteTimeGetCurrent()
129
+ CatalystPerf.add([
130
+ "type": "boot-load-url",
131
+ "url": url.absoluteString,
132
+ ])
96
133
  webView.load(request)
97
134
  let loadTime = (CFAbsoluteTimeGetCurrent() - loadStart) * 1000
98
135
  logWithTimestamp("✅ webView.load() returned (took \(String(format: "%.2f", loadTime))ms)")
99
136
  } else {
100
137
  logWithTimestamp("📴 Device offline on launch, showing offline page")
101
- _ = navigationDelegate.showOfflinePage(in: webView)
138
+ if OfflineCacheService.shared.loadSnapshot(in: webView, for: url) {
139
+ viewModel.setLoading(false, fromCache: true)
140
+ } else if navigationDelegate.showOfflinePage(in: webView) {
141
+ viewModel.setLoading(false, fromCache: true)
142
+ } else {
143
+ viewModel.setLoading(false, fromCache: false)
144
+ }
102
145
  }
103
146
  }
104
147
 
105
148
  let makeUIViewTime = (CFAbsoluteTimeGetCurrent() - makeUIViewStart) * 1000
106
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
107
155
 
108
156
  return webView
109
157
  }
@@ -131,6 +179,9 @@ public struct WebView: UIViewRepresentable, Equatable {
131
179
  }
132
180
 
133
181
  // Clean up native bridge
182
+ coordinator.stopPerfMonitoring()
183
+ coordinator.stopKeyboardPerfTracking()
184
+ webView.scrollView.delegate = nil
134
185
  coordinator.nativeBridge?.unregister()
135
186
  coordinator.nativeBridge = nil
136
187
  coordinator.pluginBridge?.unregister()
@@ -149,6 +200,10 @@ public struct WebView: UIViewRepresentable, Equatable {
149
200
  var pluginBridge: PluginBridge?
150
201
  var hostingController: UIViewController?
151
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?
152
207
 
153
208
  public init(_ parent: WebView) {
154
209
  self.parent = parent
@@ -175,6 +230,17 @@ public struct WebView: UIViewRepresentable, Equatable {
175
230
  pluginBridge.register()
176
231
  self.nativeBridge = bridge
177
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
178
244
  }
179
245
 
180
246
  @objc func handleCameraPinch(_ gesture: UIPinchGestureRecognizer) {
@@ -204,9 +270,83 @@ public struct WebView: UIViewRepresentable, Equatable {
204
270
 
205
271
  deinit {
206
272
  // Ensure observer is removed even if dismantleUIView is skipped.
273
+ CatalystPerf.reset()
274
+ stopPerfMonitoring()
275
+ stopKeyboardPerfTracking()
207
276
  if isObserverAdded, let webView = nativeBridge?.webView {
208
277
  webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
209
278
  }
210
279
  }
211
280
  }
212
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
+ }
@@ -9,9 +9,13 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
9
9
  private let offlineFileName = "offline.html"
10
10
  private let offlineSubdirectory = "offline"
11
11
  private var offlinePageVisible = false
12
+ private var offlineSnapshotVisibleURL: String?
12
13
  private var lastTargetURL: URL?
13
14
  private let initialURL: URL?
14
15
  private weak var cameraManager: NativeCameraManager?
16
+ private var pageLoadStartMs: Int64?
17
+ private var didEmitColdStart = false
18
+ private var hasStartedProfilerNavigation = false
15
19
 
16
20
  init(viewModel: WebViewModel, initialURL: URL?, cameraManager: NativeCameraManager? = nil) {
17
21
  self.viewModel = viewModel
@@ -46,6 +50,7 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
46
50
  // Only inject on main frame to match Android behavior (not on subresources)
47
51
  let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? false
48
52
  let isHttpScheme = ["http", "https"].contains(url.scheme?.lowercased() ?? "")
53
+ let isCatalystOfflineURL = OfflineCacheService.shared.originalURL(forOfflineURL: url) != nil
49
54
 
50
55
  if isMainFrame && isHttpScheme && httpMethod == "GET" {
51
56
  let safeAreaHeaders = viewModel.getSafeAreaHeaders()
@@ -86,7 +91,7 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
86
91
  return
87
92
  }
88
93
 
89
- if URLWhitelistManager.shared.isAccessControlEnabled {
94
+ if URLWhitelistManager.shared.isAccessControlEnabled && !isCatalystOfflineURL {
90
95
 
91
96
  // Check if URL is an external domain
92
97
  let isExternal = URLWhitelistManager.shared.isExternalDomain(url)
@@ -107,8 +112,10 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
107
112
  }
108
113
 
109
114
  logger.info("✅ URL passed whitelist checks, allowing navigation: \(url.absoluteString)")
110
- } else {
115
+ } else if !isCatalystOfflineURL {
111
116
  logger.info("⚠️ Access control disabled, allowing all navigation: \(url.absoluteString)")
117
+ } else {
118
+ logger.info("📴 Catalyst offline URL allowed: \(url.absoluteString)")
112
119
  }
113
120
 
114
121
  if ["http", "https"].contains(url.scheme?.lowercased() ?? "") {
@@ -120,16 +127,57 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
120
127
  let isCacheableMethod = httpMethod == "GET"
121
128
  logger.info("🔍 Cache check - Method: \(httpMethod), isCacheable: \(isCacheableMethod)")
122
129
 
130
+ if isMainFrame && isHttpScheme && isCacheableMethod {
131
+ if NetworkMonitor.shared.currentStatus.isOnline {
132
+ let isOfflineRoute = await OfflineCacheService.shared.prepareActiveOfflineRoute(
133
+ for: navigationAction.request,
134
+ webView: webView
135
+ )
136
+ offlineSnapshotVisibleURL = nil
137
+ if !isOfflineRoute {
138
+ logger.info("⏭️ Main-frame route is not currently offline eligible: \(url.absoluteString)")
139
+ }
140
+ OfflineCacheService.shared.storeRouteSnapshot(
141
+ for: navigationAction.request,
142
+ webView: webView
143
+ )
144
+ } else {
145
+ let loadedSnapshot = await MainActor.run {
146
+ loadCachedSnapshotIfNeeded(in: webView, for: url)
147
+ }
148
+
149
+ if loadedSnapshot {
150
+ logger.info("📴 Serving cached offline route snapshot: \(url.absoluteString)")
151
+ decisionHandler(.cancel)
152
+ return
153
+ }
154
+ OfflineCacheService.shared.clearActiveOfflineRoute()
155
+ }
156
+ } else if isMainFrame && isHttpScheme {
157
+ OfflineCacheService.shared.clearActiveOfflineRoute()
158
+ }
159
+
123
160
  if isCacheableMethod && CacheManager.shared.shouldCacheURL(url) {
124
- logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
161
+ logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
125
162
 
163
+ let cacheStartMs = CatalystPerf.nativeTimeMs()
126
164
  let (cachedData, cacheState, mimeType) = await CacheManager.shared.getCachedResource(
127
165
  for: navigationAction.request
128
166
  )
167
+ let cacheDurationMs = CatalystPerf.nativeTimeMs() - cacheStartMs
129
168
 
130
169
  switch cacheState {
131
170
  case .fresh, .stale:
132
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
+ ])
133
181
 
134
182
  if let cachedData = cachedData,
135
183
  let mimeType = mimeType {
@@ -148,6 +196,14 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
148
196
 
149
197
  case .expired:
150
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
+ ])
151
207
  break
152
208
  }
153
209
  } else {
@@ -201,6 +257,25 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
201
257
 
202
258
  func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
203
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
+ }
204
279
  // Stop camera on navigation — mirrors Android CustomWebview onPageStarted lambda
205
280
  cameraManager?.stop()
206
281
  Task { @MainActor in
@@ -227,6 +302,32 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
227
302
 
228
303
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
229
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
+
230
331
  Task { @MainActor in
231
332
  if let url = webView.url {
232
333
  if !isOfflinePageURL(url) {
@@ -253,12 +354,31 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
253
354
  }
254
355
 
255
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
+
256
364
  Task { @MainActor in
257
365
  viewModel.reset()
258
366
  logWithTimestamp("🔴 Navigation error: \(error.localizedDescription)")
259
367
  logger.error("Navigation failed: \(error.localizedDescription)")
260
368
 
261
369
  guard shouldShowOfflinePage(for: error), let webView else { return }
370
+ if let targetURL = lastTargetURL ?? webView.url,
371
+ isOfflineSnapshotVisible(for: targetURL) {
372
+ logger.info("📴 Ignoring repeated error for visible offline route snapshot")
373
+ return
374
+ }
375
+
376
+ if let targetURL = lastTargetURL ?? webView.url,
377
+ loadCachedSnapshotIfNeeded(in: webView, for: targetURL) {
378
+ logger.info("📴 Showing cached offline route snapshot")
379
+ return
380
+ }
381
+
262
382
  if showOfflinePage(in: webView) {
263
383
  logger.info("📴 Showing offline fallback page")
264
384
  } else {
@@ -320,6 +440,8 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
320
440
  }
321
441
 
322
442
  offlinePageVisible = false
443
+ offlineSnapshotVisibleURL = nil
444
+ OfflineCacheService.shared.clearActiveOfflineRoute()
323
445
  logWithTimestamp("🔄 Retry requested, online. Reloading: \(targetURL.absoluteString)")
324
446
  webView.load(URLRequest(url: targetURL))
325
447
  }
@@ -351,8 +473,11 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
351
473
  }
352
474
 
353
475
  offlinePageVisible = true
476
+ offlineSnapshotVisibleURL = nil
477
+ OfflineCacheService.shared.clearActiveOfflineRoute()
354
478
  let readAccessURL = offlineURL.deletingLastPathComponent()
355
479
  webView.loadFileURL(offlineURL, allowingReadAccessTo: readAccessURL)
480
+ viewModel.setLoading(false, fromCache: true)
356
481
  return true
357
482
  }
358
483
 
@@ -396,4 +521,33 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
396
521
 
397
522
  return true
398
523
  }
524
+
525
+ private func loadCachedSnapshotIfNeeded(in webView: WKWebView, for url: URL) -> Bool {
526
+ let key = snapshotKey(for: url)
527
+ if offlineSnapshotVisibleURL == key {
528
+ return true
529
+ }
530
+
531
+ if OfflineCacheService.shared.loadSnapshot(in: webView, for: url) {
532
+ offlinePageVisible = false
533
+ offlineSnapshotVisibleURL = key
534
+ viewModel.setLoading(false, fromCache: true)
535
+ return true
536
+ }
537
+
538
+ offlineSnapshotVisibleURL = nil
539
+ return false
540
+ }
541
+
542
+ private func isOfflineSnapshotVisible(for url: URL) -> Bool {
543
+ offlineSnapshotVisibleURL == snapshotKey(for: url)
544
+ }
545
+
546
+ private func snapshotKey(for url: URL) -> String {
547
+ guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
548
+ return url.absoluteString
549
+ }
550
+ components.fragment = nil
551
+ return components.url?.absoluteString ?? url.absoluteString
552
+ }
399
553
  }
@@ -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
  // ========================================
@@ -56,9 +56,9 @@ const keystorePath=_path.default.join(tempDeploymentPath,"app","keystore.jks");i
56
56
  await cleanupTempDeployment(tempDeploymentPath);return{...aabResult,finalAabPath,finalAabName,outputDir};}catch(error){throw new Error(`Failed to finalize AAB: ${error.message}`);}}async function cleanupTempDeployment(tempDeploymentPath){if(!tempDeploymentPath||!_fs.default.existsSync(tempDeploymentPath)){return;}try{progress.log("Cleaning up temporary deployment files...","info");(0,_utils.runCommand)(`rm -rf "${tempDeploymentPath}"`);progress.log("Temporary files cleaned up successfully","success");}catch(error){progress.log(`Warning: Could not cleanup temporary directory: ${error.message}`,"warning");}}async function updateFileContents(projectPaths,androidConfig){const{newProjectPath}=projectPaths;const{oldProjectName,newProjectName,packageName}=androidConfig;progress.log("Updating file contents and names...","info");try{// STEP 1: Update packageName in build.gradle.kts and source files
57
57
  if(packageName){const OLD_PACKAGE="io.yourname.androidproject";progress.log(`Replacing package name: ${OLD_PACKAGE} → ${packageName}`,"info");// Update build.gradle.kts
58
58
  const buildGradlePath=_path.default.join(newProjectPath,"app","build.gradle.kts");if(_fs.default.existsSync(buildGradlePath)){let content=_fs.default.readFileSync(buildGradlePath,"utf8");content=content.replace(/namespace = "io\.yourname\.androidproject"/g,`namespace = "${packageName}"`);content=content.replace(/applicationId = "io\.yourname\.androidproject"/g,`applicationId = "${packageName}"`);_fs.default.writeFileSync(buildGradlePath,content,"utf8");progress.log(`✅ Updated build.gradle.kts with package: ${packageName}`,"success");}// Update source directory structure for all source sets
59
- const sourceSets=["main","test","noFcm","withFcm"];const newPackageParts=packageName.split(".");let totalUpdatedFiles=0;for(const sourceSet of sourceSets){const oldPackagePath=_path.default.join(newProjectPath,"app","src",sourceSet,"java","io","yourname","androidproject");if(_fs.default.existsSync(oldPackagePath)){const newPackagePath=_path.default.join(newProjectPath,"app","src",sourceSet,"java",...newPackageParts);// Create new package directory
59
+ const sourceSets=["main","test","noFcm","withFcm"];const newPackageParts=packageName.split(".");let totalUpdatedFiles=0;for(const sourceSet of sourceSets){const oldPackagePath=_path.default.join(newProjectPath,"app","src",sourceSet,"java","io","yourname","androidproject");if(_fs.default.existsSync(oldPackagePath)){const newPackagePath=_path.default.join(newProjectPath,"app","src",sourceSet,"java",...newPackageParts);const packagePathChanged=_path.default.resolve(oldPackagePath)!==_path.default.resolve(newPackagePath);if(packagePathChanged){// Create new package directory
60
60
  const newPackageDir=_path.default.dirname(newPackagePath);_fs.default.mkdirSync(newPackageDir,{recursive:true})// Move source files
61
- ;(0,_utils.runCommand)(`mv "${oldPackagePath}" "${newPackagePath}"`);progress.log(`✅ Moved ${sourceSet} source files to: ${newPackagePath}`,"success");// Update package declarations in source files
61
+ ;(0,_utils.runCommand)(`mv "${oldPackagePath}" "${newPackagePath}"`);progress.log(`✅ Moved ${sourceSet} source files to: ${newPackagePath}`,"success");}else{progress.log(`Package path unchanged for ${sourceSet}; skipping source directory move`,"info");}// Update package declarations in source files
62
62
  const findCommand=`find "${newPackagePath}" -type f \\( -name "*.kt" -o -name "*.java" \\)`;const sourceFiles=(0,_utils.runCommand)(findCommand).trim().split("\n").filter(f=>f.trim());let updatedCount=0;for(const file of sourceFiles){let content=_fs.default.readFileSync(file,"utf8");if(content.includes(OLD_PACKAGE)){content=content.replace(new RegExp(OLD_PACKAGE,"g"),packageName);_fs.default.writeFileSync(file,content,"utf8");updatedCount++;}}totalUpdatedFiles+=updatedCount;progress.log(`✅ Updated package declarations in ${updatedCount} ${sourceSet} files`,"success");// Clean up old empty directories
63
63
  try{;(0,_utils.runCommand)(`find "${_path.default.join(newProjectPath,"app","src",sourceSet,"java")}" -type d -empty -delete`);}catch(err){// Ignore cleanup errors
64
64
  }}}if(totalUpdatedFiles>0){progress.log(`✅ Total package declarations updated: ${totalUpdatedFiles} files`,"success");}// Update AndroidManifest.xml
@@ -0,0 +1 @@
1
+ "use strict";/* eslint-disable no-restricted-globals */const MANIFEST_URL="/catalyst-offline-manifest.json";const OFFLINE_URL="/offline.html";const CACHE_PREFIX="catalyst-offline";const MANIFEST_CACHE=`${CACHE_PREFIX}-manifest`;self.addEventListener("install",event=>{event.waitUntil((async()=>{await loadManifest(true);await cacheOfflineFallback();await self.skipWaiting();})());});self.addEventListener("activate",event=>{event.waitUntil((async()=>{await cleanupOldCaches();await self.clients.claim();})());});self.addEventListener("fetch",event=>{const{request}=event;if(request.method!=="GET")return;if(request.mode==="navigate"||request.destination==="document"){event.respondWith(handleNavigation(request));return;}if(["script","style","font","image"].includes(request.destination)){event.respondWith(handleAsset(request));}});async function loadManifest(preferNetwork){const cache=await caches.open(MANIFEST_CACHE);if(preferNetwork){try{const response=await fetch(MANIFEST_URL,{cache:"no-store"});if(response.ok){await cache.put(MANIFEST_URL,response.clone());const manifest=await response.json();cleanupOldCaches(manifest);return manifest;}}catch(_){}}const cached=await cache.match(MANIFEST_URL);if(cached)return cached.json();return{buildId:"unknown",routes:[]};}async function cacheOfflineFallback(){try{const cache=await caches.open(`${CACHE_PREFIX}-fallback`);const response=await fetch(OFFLINE_URL,{cache:"no-store"});if(response.ok)await cache.put(OFFLINE_URL,response);}catch(_){}}function routeCacheName(manifest){return`${CACHE_PREFIX}-routes-${manifest.buildId}`;}function assetCacheName(manifest){return`${CACHE_PREFIX}-assets-${manifest.buildId}`;}async function cleanupOldCaches(manifest){manifest=manifest||(await loadManifest(false));if(!manifest.buildId||manifest.buildId==="unknown")return;const keep=new Set([MANIFEST_CACHE,`${CACHE_PREFIX}-fallback`,routeCacheName(manifest),assetCacheName(manifest)]);const cacheNames=await caches.keys();await Promise.all(cacheNames.filter(name=>name.startsWith(CACHE_PREFIX)&&!keep.has(name)).map(name=>caches.delete(name)));}function isOfflineRoute(url,manifest){const{pathname}=new URL(url);return(manifest.routes||[]).some(route=>{try{return new RegExp(route.regex).test(pathname);}catch(_){return false;}});}function canStore(response){return!!(response&&(response.ok||response.type==="opaque"));}async function handleNavigation(request){const manifest=await loadManifest(true);if(!isOfflineRoute(request.url,manifest))return fetch(request);const cache=await caches.open(routeCacheName(manifest));try{const response=await fetch(request);const contentType=response.headers.get("content-type")||"";if(canStore(response)&&contentType.includes("text/html")){await cache.put(request.url,response.clone());}return response;}catch(_){const cached=await cache.match(request.url);if(cached)return cached;const offlineResponse=await caches.match(OFFLINE_URL);if(offlineResponse)return offlineResponse;return fetch(OFFLINE_URL);}}async function handleAsset(request){const manifest=await loadManifest(false);const cache=await caches.open(assetCacheName(manifest));const cached=await cache.match(request);if(cached)return cached;const response=await fetch(request);if(canStore(response))await cache.put(request,response.clone());return response;}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.registerCatalystServiceWorker=registerCatalystServiceWorker;let registered=false;function isNativeWebView(){if(typeof window==="undefined")return false;return!!(window.NativeBridge||window.webkit?.messageHandlers?.NativeBridge);}function registerCatalystServiceWorker(){if(registered)return;if(typeof window==="undefined"||typeof navigator==="undefined")return;if(process.env.NODE_ENV!=="production")return;if(isNativeWebView())return;if(!("serviceWorker"in navigator))return;registered=true;const register=()=>{navigator.serviceWorker.register("/catalyst-sw.js",{scope:"/",updateViaCache:"none"}).catch(()=>{registered=false;});};if(document.readyState==="complete"){register();}else{window.addEventListener("load",register,{once:true});}}
@@ -1,3 +1,3 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _router=require("@tata1mg/router");const{preparedRoutes}=require(`${process.env.src_path}/src/js/routes/utils.js`);/**
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _router=require("@tata1mg/router");var _registerServiceWorker=require("../offline/registerServiceWorker.js");const{preparedRoutes}=require(`${process.env.src_path}/src/js/routes/utils.js`);(0,_registerServiceWorker.registerCatalystServiceWorker)();/**
2
2
  * @returns returns browsers routers (client routes)
3
3
  */const clientRouter=({store,routerInitialState})=>(0,_router.createBrowserRouter)(preparedRoutes({store,routerInitialState}));var _default=exports.default=clientRouter;
@@ -1,3 +1,3 @@
1
1
  "use strict";const path=require("path");const{green,cyan,yellow}=require("picocolors");const{name}=require(`${process.cwd()}/package.json`);const{BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);const{arrayToObject,printBundleInformation,runBuildCommands}=require("./scriptUtils.js");/**
2
2
  * @description - creates a production build of the application.
3
- */function build(){const isWindows=process.platform==="win32";const commandLineArguments=process.argv.slice(2);const argumentsObject=arrayToObject(commandLineArguments);const dirname=path.resolve(__dirname,"../../");const commands=["node ./dist/scripts/checkVersion",`${isWindows?"rd -r -fo":"rm -rf"} ${process.cwd()}/${BUILD_OUTPUT_PATH} & node ./dist/scripts/loadScriptsBeforeServerStarts.js`,`cross-env APPLICATION=${name||"catalyst_app"} webpack --config ./dist/webpack/production.client.babel.js --progress`,`cross-env APPLICATION=${name||"catalyst_app"} SSR=true webpack --config ./dist/webpack/production.ssr.babel.js`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ./dist/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --ignore '**/*.test.js,./dist/server/renderer/handler.js' --quiet`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ${process.cwd()}/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --quiet`];console.log("Creating an optimized production build...");runBuildCommands({commands,cwd:dirname,env:{...process.env,src_path:process.cwd(),BUILD_OUTPUT_PATH:BUILD_OUTPUT_PATH,NODE_ENV:"production",IS_DEV_COMMAND:false,...argumentsObject}});console.log(green("Compiled successfully."));console.log("\nFile sizes after gzip:\n");printBundleInformation();console.log(`\nThe ${cyan(BUILD_OUTPUT_PATH)} folder is ready to be deployed.`);console.log("You may serve it with a serve command:");console.log(cyan("\n npm run serve"));console.log("\nFind out more about deployment here:");console.log(yellow("\n https://catalyst.1mg.com/public_docs/content/Deployment%20and%20Production/deployment\n"));}build();
3
+ */function build(){const isWindows=process.platform==="win32";const commandLineArguments=process.argv.slice(2);const argumentsObject=arrayToObject(commandLineArguments);const dirname=path.resolve(__dirname,"../../");const commands=["node ./dist/scripts/checkVersion",`${isWindows?"rd -r -fo":"rm -rf"} ${process.cwd()}/${BUILD_OUTPUT_PATH} & node ./dist/scripts/loadScriptsBeforeServerStarts.js`,`cross-env APPLICATION=${name||"catalyst_app"} webpack --config ./dist/webpack/production.client.babel.js --progress`,`cross-env APPLICATION=${name||"catalyst_app"} SSR=true webpack --config ./dist/webpack/production.ssr.babel.js`,`cross-env APPLICATION=${name||"catalyst_app"} node ./dist/scripts/generateOfflineManifest.js`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ./dist/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --extensions .js,.ts,.jsx,.tsx --ignore '**/*.test.js,./dist/server/renderer/handler.js' --quiet`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ${process.cwd()}/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --extensions .js,.ts,.jsx,.tsx --quiet`];console.log("Creating an optimized production build...");runBuildCommands({commands,cwd:dirname,env:{...process.env,src_path:process.cwd(),BUILD_OUTPUT_PATH:BUILD_OUTPUT_PATH,NODE_ENV:"production",IS_DEV_COMMAND:false,...argumentsObject}});console.log(green("Compiled successfully."));console.log("\nFile sizes after gzip:\n");printBundleInformation();console.log(`\nThe ${cyan(BUILD_OUTPUT_PATH)} folder is ready to be deployed.`);console.log("You may serve it with a serve command:");console.log(cyan("\n npm run serve"));console.log("\nFind out more about deployment here:");console.log(yellow("\n https://catalyst.1mg.com/public_docs/content/Deployment%20and%20Production/deployment\n"));}build();
@@ -1,3 +1,3 @@
1
1
  "use strict";const path=require("path");const{green,cyan,yellow}=require("picocolors");const{name}=require(`${process.cwd()}/package.json`);const{BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);const{arrayToObject,printBundleInformation,runBuildCommands}=require("./scriptUtils.js");/**
2
2
  * @description - creates a production build of the application.
3
- */function devBuild(){const isWindows=process.platform==="win32";const commandLineArguments=process.argv.slice(2);const argumentsObject=arrayToObject(commandLineArguments);const dirname=path.resolve(__dirname,"../../");const commands=["node ./dist/scripts/checkVersion",`${isWindows?"rd -r -fo":"rm -rf"} ${process.cwd()}/${BUILD_OUTPUT_PATH} & node ./dist/scripts/loadScriptsBeforeServerStarts.js`,`cross-env APPLICATION=${name||"catalyst_app"} webpack --config ./dist/webpack/production.client.babel.js --progress`,` cross-env APPLICATION=${name||"catalyst_app"} SSR=true webpack --config ./dist/webpack/production.ssr.babel.js`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ./dist/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --ignore '**/*.test.js,./dist/server/renderer/handler.js' --quiet`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ${process.cwd()}/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --quiet`];console.log("Creating an optimized local build...");runBuildCommands({commands,cwd:dirname,env:{...process.env,src_path:process.cwd(),BUILD_OUTPUT_PATH:BUILD_OUTPUT_PATH,NODE_ENV:"production",IS_DEV_COMMAND:true,...argumentsObject}});console.log(green("Compiled successfully."));console.log("\nFile sizes after gzip:\n");printBundleInformation();console.log(`\nThe ${cyan(BUILD_OUTPUT_PATH)} folder is ready to be deployed.`);console.log("You may serve it with a serve command:");console.log(cyan("\n npm run devServe"));console.log("\nFind out more about deployment here:");console.log(yellow("\n https://catalyst.1mg.com/public_docs/content/Deployment%20and%20Production/deployment\n"));}devBuild();
3
+ */function devBuild(){const isWindows=process.platform==="win32";const commandLineArguments=process.argv.slice(2);const argumentsObject=arrayToObject(commandLineArguments);const dirname=path.resolve(__dirname,"../../");const commands=["node ./dist/scripts/checkVersion",`${isWindows?"rd -r -fo":"rm -rf"} ${process.cwd()}/${BUILD_OUTPUT_PATH} & node ./dist/scripts/loadScriptsBeforeServerStarts.js`,`cross-env APPLICATION=${name||"catalyst_app"} webpack --config ./dist/webpack/production.client.babel.js --progress`,`cross-env APPLICATION=${name||"catalyst_app"} SSR=true webpack --config ./dist/webpack/production.ssr.babel.js`,`cross-env APPLICATION=${name||"catalyst_app"} node ./dist/scripts/generateOfflineManifest.js`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ./dist/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --ignore '**/*.test.js,./dist/server/renderer/handler.js' --quiet`,`cross-env APPLICATION=${name||"catalyst_app"} npx babel ${process.cwd()}/server --out-dir ${process.cwd()}/${BUILD_OUTPUT_PATH} --quiet`];console.log("Creating an optimized local build...");runBuildCommands({commands,cwd:dirname,env:{...process.env,src_path:process.cwd(),BUILD_OUTPUT_PATH:BUILD_OUTPUT_PATH,NODE_ENV:"production",IS_DEV_COMMAND:true,...argumentsObject}});console.log(green("Compiled successfully."));console.log("\nFile sizes after gzip:\n");printBundleInformation();console.log(`\nThe ${cyan(BUILD_OUTPUT_PATH)} folder is ready to be deployed.`);console.log("You may serve it with a serve command:");console.log(cyan("\n npm run devServe"));console.log("\nFind out more about deployment here:");console.log(yellow("\n https://catalyst.1mg.com/public_docs/content/Deployment%20and%20Production/deployment\n"));}devBuild();
@@ -0,0 +1 @@
1
+ "use strict";const fs=require("fs");const path=require("path");function toRoutePath(basePath,routePath,isIndex){if(isIndex||routePath===undefined||routePath===null||routePath===""){return basePath||"/";}const nextPath=String(routePath).startsWith("/")?routePath:`${basePath}/${routePath}`;return`/${nextPath}`.replace(/\/+/g,"/").replace(/\/$/,"")||"/";}function routeRegex(pattern,exact=false){if(pattern==="*"||pattern==="/*")return"^/.*$";if(pattern==="/")return exact?"^/$":"^/.*$";const parts=pattern.split("/").filter(Boolean);const regex=parts.map(part=>{if(part==="*")return".*";if(part.startsWith(":"))return"[^/]+";return part.replace(/[|\\{}()[\]^$+?.]/g,"\\$&");}).join("/");return exact?`^/${regex}$`:`^/${regex}(?:/.*)?$`;}function collectOfflineRoutes(routes,basePath="",parentOffline=false,result=[]){for(const route of Array.isArray(routes)?routes:[]){if(!route||typeof route!=="object")continue;const offline=Object.prototype.hasOwnProperty.call(route,"offline")?route.offline===true:parentOffline;const isIndexRoute=route.index===true||route.path==="";const pattern=toRoutePath(basePath,route.path,isIndexRoute);const regex=routeRegex(pattern,isIndexRoute);if(offline&&!result.some(item=>item.pattern===pattern&&item.regex===regex)){result.push({pattern,regex});}collectOfflineRoutes(route.children,pattern,offline,result);}return result;}function loadAppRoutes(){require("./loadScriptsBeforeServerStarts.js");const emptyModule=()=>null;[".css",".scss",".sass",".less",".png",".jpg",".jpeg",".gif",".svg",".webp"].forEach(ext=>{require.extensions[ext]=emptyModule;});require("@babel/register")({babelrc:false,extensions:[".js",".jsx",".ts",".tsx"],ignore:[/node_modules/],presets:[["@babel/preset-env",{targets:{node:"current"}}],["@babel/preset-react",{runtime:"automatic"}],["@babel/preset-typescript",{isTSX:true,allExtensions:true}]],plugins:["@loadable/babel-plugin"]});const{getRoutes}=require(path.join(process.env.src_path,"src/js/routes/utils.js"));return typeof getRoutes==="function"?getRoutes():[];}function copyIfExists(source,destination){if(fs.existsSync(source))fs.copyFileSync(source,destination);}function main(){const appConfig=require(path.join(process.env.src_path,"config/config.json"));const publicDir=path.join(process.env.src_path,process.env.BUILD_OUTPUT_PATH||appConfig.BUILD_OUTPUT_PATH,"public");const routes=collectOfflineRoutes(loadAppRoutes());fs.mkdirSync(publicDir,{recursive:true});fs.writeFileSync(path.join(publicDir,"catalyst-offline-manifest.json"),`${JSON.stringify({schemaVersion:1,buildId:Date.now().toString(36),routes},null,2)}\n`);copyIfExists(path.join(process.env.src_path,"public/offline.html"),path.join(publicDir,"offline.html"));fs.copyFileSync(path.join(__dirname,"../offline/catalyst-sw.js"),path.join(publicDir,"catalyst-sw.js"));console.log(`Catalyst offline manifest generated: ${routes.length} route(s)`);}main();
@@ -1,4 +1,5 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _path=_interopRequireDefault(require("path"));var _express=_interopRequireDefault(require("express"));var _bodyParser=_interopRequireDefault(require("body-parser"));var _compression=_interopRequireDefault(require("compression"));var _cookieParser=_interopRequireDefault(require("cookie-parser"));var _expressStaticGzip=_interopRequireDefault(require("express-static-gzip"));var _renderer=_interopRequireDefault(require("@catalyst/server/renderer"));var _server=require("@catalyst/template/server/server.js");var _validator=require("@catalyst/server/utils/validator");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const env=process.env.NODE_ENV||"development";const app=(0,_express.default)();app.use(_bodyParser.default.json({limit:"100kb"}));// Handles "bytestream array" content types sent as raw application/* bodies
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _path=_interopRequireDefault(require("path"));var _fs=_interopRequireDefault(require("fs"));var _express=_interopRequireDefault(require("express"));var _bodyParser=_interopRequireDefault(require("body-parser"));var _compression=_interopRequireDefault(require("compression"));var _cookieParser=_interopRequireDefault(require("cookie-parser"));var _expressStaticGzip=_interopRequireDefault(require("express-static-gzip"));var _renderer=_interopRequireDefault(require("@catalyst/server/renderer"));var _server=require("@catalyst/template/server/server.js");var _validator=require("@catalyst/server/utils/validator");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const env=process.env.NODE_ENV||"development";const app=(0,_express.default)();const sendBuiltPublicFile=(res,fileName,headers={})=>{const publicFilePath=_path.default.join(process.env.src_path,`./${process.env.BUILD_OUTPUT_PATH}/public`,fileName);if(!_fs.default.existsSync(publicFilePath)){res.status(404).end();return;}res.set(headers);res.sendFile(publicFilePath);};app.get("/catalyst-sw.js",(_req,res)=>{sendBuiltPublicFile(res,"catalyst-sw.js",{"Content-Type":"application/javascript; charset=utf-8","Cache-Control":"no-cache, no-store, must-revalidate","Service-Worker-Allowed":"/"});});app.get("/catalyst-offline-manifest.json",(_req,res)=>{sendBuiltPublicFile(res,"catalyst-offline-manifest.json",{"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-cache, no-store, must-revalidate"});});app.get("/offline.html",(_req,res)=>{sendBuiltPublicFile(res,"offline.html",{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-cache, no-store, must-revalidate"});});// This middleware is being used to extract the body of the request
2
+ app.use(_bodyParser.default.json({limit:"100kb"}));// Handles "bytestream array" content types sent as raw application/* bodies
2
3
  app.use(_bodyParser.default.raw({type:"application/*",limit:"100kb"}));app.use((0,_cookieParser.default)());if((0,_validator.validateMiddleware)(_server.addMiddlewares))(0,_server.addMiddlewares)(app);app.use((0,_compression.default)());// Serve pre-compressed static assets (brotli preferred) from the build output directory
3
4
  if(env==="production"){app.use(process.env.PUBLIC_STATIC_ASSET_PATH,(0,_expressStaticGzip.default)(_path.default.join(process.env.src_path,`./${process.env.BUILD_OUTPUT_PATH}/public`),{enableBrotli:true,orderPreference:["br","gzip","deflate"]}));}else{app.use(process.env.PUBLIC_STATIC_ASSET_PATH,_express.default.static(_path.default.join(process.env.src_path,`./${process.env.BUILD_OUTPUT_PATH}/public`)));}// Catch-all: every non-asset request is handled by the SSR renderer
4
5
  app.use("*",_renderer.default);var _default=exports.default=app;
@@ -5,7 +5,7 @@
5
5
  * @param {object} firstFoldCss - style elements extracted for initial page load
6
6
  * @param {object} fetcherData - contains data from executing serverFetcher function
7
7
  * @param {object} children - contains any child elements defined within the component
8
- */const DEFAULT_SAFE_AREA={top:0,right:0,bottom:0,left:0};function Body({jsx="",initialState={},fetcherData={},children,safeArea=DEFAULT_SAFE_AREA}){return/*#__PURE__*/(0,_jsxRuntime.jsxs)("body",{children:[/*#__PURE__*/(0,_jsxRuntime.jsx)("script",{/* eslint-disable-next-line risxss/catch-potential-xss-react */dangerouslySetInnerHTML:{__html:`window.__SAFE_AREA_INITIAL__ = ${JSON.stringify(safeArea)}`}}),jsx,/*#__PURE__*/(0,_jsxRuntime.jsx)("script",{/* eslint-disable */dangerouslySetInnerHTML:{__html:`
8
+ */const DEFAULT_SAFE_AREA={top:0,right:0,bottom:0,left:0};function Body({jsx="",initialState={},fetcherData={},children,safeArea=DEFAULT_SAFE_AREA,nativeWebView=false}){return/*#__PURE__*/(0,_jsxRuntime.jsxs)("body",{children:[/*#__PURE__*/(0,_jsxRuntime.jsx)("script",{/* eslint-disable-next-line risxss/catch-potential-xss-react */dangerouslySetInnerHTML:{__html:`window.__SAFE_AREA_INITIAL__ = ${JSON.stringify(safeArea)}; window.__CATALYST_NATIVE_WEBVIEW__ = ${nativeWebView?"true":"false"}`}}),jsx,/*#__PURE__*/(0,_jsxRuntime.jsx)("script",{/* eslint-disable */dangerouslySetInnerHTML:{__html:`
9
9
  window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
10
10
  window.__ROUTER_INITIAL_DATA__ = ${JSON.stringify(fetcherData)}
11
11
  `}}),children]});}