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.
- package/dist/native/androidProject/app/build.gradle.kts +26 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +381 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +29 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +7 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +162 -26
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/bridge/WebBridge.js +2 -2
- package/dist/native/bridge/constants/NativeInterfaces.js +2 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +22 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +43 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +47 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +38 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +53 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +7 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +27 -0
- package/dist/native/bridge/perf/core/constants.js +43 -0
- package/dist/native/bridge/perf/core/measure.js +40 -0
- package/dist/native/bridge/perf/core/session.js +34 -0
- package/dist/native/bridge/perf/core/store.js +1 -0
- package/dist/native/bridge/perf/core/summary.js +1 -0
- package/dist/native/bridge/perf/core/traceExport.js +1 -0
- package/dist/native/bridge/perf/core/waterfalls.js +1 -0
- package/dist/native/bridge/perf/index.js +81 -0
- package/dist/native/buildAndroid/assets.js +2 -2
- package/dist/native/buildIos/config.js +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +49 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +141 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +157 -3
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- package/dist/native/renameAndroidProject.js +2 -2
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/server/expressServer.js +2 -1
- package/dist/server/renderer/document/Body.js +1 -1
- package/dist/server/renderer/handler.js +3 -3
- package/dist/webpack/base.babel.js +1 -1
- package/mcp_v2/knowledge-base.json +13 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift
CHANGED
|
@@ -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)
|
|
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")
|
|
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;
|
package/dist/scripts/build.js
CHANGED
|
@@ -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();
|
package/dist/scripts/devBuild.js
CHANGED
|
@@ -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`,`
|
|
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)();
|
|
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]});}
|