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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Vite Plugin for Categorizing Assets by SSR Split Configuration
3
+ *
4
+ * This plugin categorizes build assets into four groups:
5
+ * - essential: Entry chunks + their transitive STATIC import closure (not dynamic)
6
+ * - ssrTrue: Assets loaded via split with ssr: true
7
+ * - ssrFalse: Assets loaded via split with ssr: false
8
+ * - orphan: Non-entry chunks with zero static importers (not split targets)
9
+ **/
10
+ import path from "path"
11
+ import fs from "fs"
12
+
13
+ /**
14
+ * Walk an AST node and invoke the visitor for every node.
15
+ */
16
+ function walk(node, visitor) {
17
+ if (!node || typeof node !== "object") return
18
+
19
+ visitor(node)
20
+
21
+ for (const key of Object.keys(node)) {
22
+ const child = node[key]
23
+ if (Array.isArray(child)) {
24
+ for (const item of child) {
25
+ if (item && typeof item.type === "string") {
26
+ walk(item, visitor)
27
+ }
28
+ }
29
+ } else if (child && typeof child.type === "string") {
30
+ walk(child, visitor)
31
+ }
32
+ }
33
+ }
34
+
35
+ export function manifestCategorizationPlugin(options = {}) {
36
+ const { outputFile = "asset-categories.json" } = options
37
+
38
+ // Collected during transform, resolved asynchronously in buildEnd
39
+ const pendingSplitPaths = [] // { importPath, importer, ssrValue }
40
+ const splitModules = new Map() // resolvedId -> { ssr: boolean, originalPath: string }
41
+
42
+ const moduleToChunk = new Map() // moduleId -> chunkFileName
43
+ const chunkDependencies = new Map() // chunkFileName -> Set<chunkFileName> (static imports only)
44
+
45
+ let processedManifest = null
46
+
47
+ function processBundle(bundle) {
48
+ // Pass 1: Build module → chunk mapping
49
+ for (const [fileName, chunk] of Object.entries(bundle)) {
50
+ if (chunk.type !== "chunk") continue
51
+ if (chunk.facadeModuleId) {
52
+ moduleToChunk.set(chunk.facadeModuleId, fileName)
53
+ }
54
+ if (chunk.modules) {
55
+ for (const moduleId of Object.keys(chunk.modules)) {
56
+ moduleToChunk.set(moduleId, fileName)
57
+ }
58
+ }
59
+ }
60
+
61
+ // Pass 2: Build static-only dependency graph (excludes dynamicImports intentionally)
62
+ for (const [fileName, chunk] of Object.entries(bundle)) {
63
+ if (chunk.type !== "chunk") continue
64
+ chunkDependencies.set(fileName, new Set(chunk.imports || []))
65
+ }
66
+
67
+ // Identify which chunks are split targets
68
+ const splitChunkNames = new Set()
69
+ for (const moduleId of splitModules.keys()) {
70
+ const chunkName = moduleToChunk.get(moduleId)
71
+ if (chunkName && bundle[chunkName]) splitChunkNames.add(chunkName)
72
+ }
73
+
74
+ // Build reverse adjacency map once — O(n) instead of O(n²) per lookup
75
+ const importedBy = new Map() // chunkFileName -> Set<chunkFileNames that statically import it>
76
+ for (const [chunk, deps] of chunkDependencies.entries()) {
77
+ for (const dep of deps) {
78
+ if (!importedBy.has(dep)) importedBy.set(dep, new Set())
79
+ importedBy.get(dep).add(chunk)
80
+ }
81
+ }
82
+
83
+ const isChunkOnlyUsedBySplits = (chunkName) => {
84
+ const importers = importedBy.get(chunkName)
85
+ if (!importers || importers.size === 0) return false
86
+ return [...importers].every((imp) => splitChunkNames.has(imp))
87
+ }
88
+
89
+ // Orphan chunks: no static importers, not an entry chunk, and not a split target.
90
+ // Split targets have zero static importers (they're dynamically imported) but
91
+ // are already categorized as ssrTrue/ssrFalse — they're not true orphans.
92
+ const orphanChunkNames = new Set()
93
+ for (const [fileName, chunk] of Object.entries(bundle)) {
94
+ if (chunk.type !== "chunk") continue
95
+ if (chunk.isEntry) continue
96
+ if (splitChunkNames.has(fileName)) continue
97
+ const importers = importedBy.get(fileName)
98
+ if (!importers || importers.size === 0) {
99
+ orphanChunkNames.add(fileName)
100
+ }
101
+ }
102
+
103
+ // BFS over STATIC imports from entry chunks only.
104
+ // Dynamic imports (dynamicImports) are intentionally excluded — they are lazy by design.
105
+ const entryChunks = Object.entries(bundle)
106
+ .filter(([, chunk]) => chunk.type === "chunk" && chunk.isEntry)
107
+ .map(([fileName]) => fileName)
108
+
109
+ const essentialChunkNames = new Set(entryChunks)
110
+ const queue = [...entryChunks]
111
+
112
+ while (queue.length > 0) {
113
+ const current = queue.shift()
114
+ for (const dep of chunkDependencies.get(current) || []) {
115
+ if (
116
+ !essentialChunkNames.has(dep) &&
117
+ !splitChunkNames.has(dep) &&
118
+ !isChunkOnlyUsedBySplits(dep) &&
119
+ !orphanChunkNames.has(dep)
120
+ ) {
121
+ essentialChunkNames.add(dep)
122
+ queue.push(dep)
123
+ }
124
+ }
125
+ }
126
+
127
+ const toEntry = (fileName, chunk) => ({
128
+ file: fileName,
129
+ src: chunk.facadeModuleId || "",
130
+ isEntry: chunk.isEntry || false,
131
+ css: chunk.css || [],
132
+ imports: chunk.imports || [],
133
+ dynamicImports: chunk.dynamicImports || [],
134
+ })
135
+
136
+ const categorizedChunks = { essential: {}, ssrTrue: {}, ssrFalse: {}, orphan: {} }
137
+
138
+ for (const fileName of essentialChunkNames) {
139
+ if (bundle[fileName]?.type === "chunk") {
140
+ categorizedChunks.essential[fileName] = toEntry(fileName, bundle[fileName])
141
+ }
142
+ }
143
+
144
+ const seen = new Set()
145
+ for (const [moduleId, splitInfo] of splitModules.entries()) {
146
+ const chunkName = moduleToChunk.get(moduleId)
147
+ if (chunkName && bundle[chunkName] && !seen.has(chunkName)) {
148
+ const category = splitInfo.ssr ? "ssrTrue" : "ssrFalse"
149
+ categorizedChunks[category][chunkName] = toEntry(chunkName, bundle[chunkName])
150
+ seen.add(chunkName)
151
+ }
152
+ }
153
+
154
+ for (const fileName of orphanChunkNames) {
155
+ if (bundle[fileName]?.type === "chunk") {
156
+ categorizedChunks.orphan[fileName] = toEntry(fileName, bundle[fileName])
157
+ }
158
+ }
159
+
160
+ processedManifest = {
161
+ ...categorizedChunks,
162
+ metadata: {
163
+ generatedAt: new Date().toISOString(),
164
+ totalAssets: Object.keys(bundle).filter((k) => bundle[k].type === "chunk").length,
165
+ dependencyStats: {
166
+ ssrTrueModules: Object.keys(categorizedChunks.ssrTrue).length,
167
+ ssrFalseModules: Object.keys(categorizedChunks.ssrFalse).length,
168
+ essentialModules: Object.keys(categorizedChunks.essential).length,
169
+ orphanModules: Object.keys(categorizedChunks.orphan).length,
170
+ },
171
+ },
172
+ }
173
+
174
+ return processedManifest
175
+ }
176
+
177
+ // Apply Vite manifest structure to categorized chunks
178
+ function applyViteManifestStructure(categorizedManifest, viteManifest) {
179
+ const newCategorizedChunks = {
180
+ essential: {},
181
+ ssrTrue: {},
182
+ ssrFalse: {},
183
+ orphan: {},
184
+ }
185
+
186
+ for (const category of ["essential", "ssrTrue", "ssrFalse", "orphan"]) {
187
+ for (const [chunkName, chunkData] of Object.entries(categorizedManifest[category])) {
188
+ let matchedViteKey = null
189
+ let matchedViteEntry = null
190
+
191
+ for (const [viteKey, viteEntry] of Object.entries(viteManifest)) {
192
+ if (viteEntry.file === chunkName) {
193
+ matchedViteKey = viteKey
194
+ matchedViteEntry = viteEntry
195
+ break
196
+ }
197
+ }
198
+
199
+ if (matchedViteEntry && matchedViteKey) {
200
+ newCategorizedChunks[category][matchedViteKey] = { ...matchedViteEntry }
201
+ } else {
202
+ console.log(`❌ No manifest match found for ${chunkName}, keeping original structure`)
203
+ newCategorizedChunks[category][chunkName] = chunkData
204
+ }
205
+ }
206
+ }
207
+
208
+ // Ensure entry points are always in essential
209
+ for (const [viteKey, viteEntry] of Object.entries(viteManifest)) {
210
+ if (
211
+ viteEntry.isEntry &&
212
+ !newCategorizedChunks.essential[viteKey] &&
213
+ !newCategorizedChunks.ssrTrue[viteKey] &&
214
+ !newCategorizedChunks.ssrFalse[viteKey] &&
215
+ !newCategorizedChunks.orphan[viteKey]
216
+ ) {
217
+ newCategorizedChunks.essential[viteKey] = { ...viteEntry }
218
+ }
219
+ }
220
+
221
+ Object.keys(categorizedManifest).forEach((category) => {
222
+ if (category !== "metadata") {
223
+ categorizedManifest[category] = newCategorizedChunks[category]
224
+ }
225
+ })
226
+ }
227
+
228
+ // Recursively collect all CSS files reachable from a manifest key
229
+ function collectTransitiveCss(viteKey, viteManifest, visited = new Set()) {
230
+ if (visited.has(viteKey)) return []
231
+ visited.add(viteKey)
232
+
233
+ const entry = viteManifest[viteKey]
234
+ if (!entry) return []
235
+
236
+ const css = [...(entry.css || [])]
237
+ for (const imp of entry.imports || []) {
238
+ css.push(...collectTransitiveCss(imp, viteManifest, visited))
239
+ }
240
+ return css
241
+ }
242
+
243
+ // Attach allCss (transitive) to every chunk in all categories
244
+ function enrichWithTransitiveCss(categorizedManifest, viteManifest) {
245
+ for (const category of ["essential", "ssrTrue", "ssrFalse", "orphan"]) {
246
+ for (const [key, chunk] of Object.entries(categorizedManifest[category])) {
247
+ const transitiveCss = collectTransitiveCss(key, viteManifest)
248
+ chunk.allCss = [...new Set(transitiveCss)]
249
+ }
250
+ }
251
+ }
252
+
253
+ // When Vite leaves facadeModuleId unset, the chunk lands in the manifest under
254
+ // `_<name>-<hash>.js` with no `src` field. The cacheKey baked into bundles by
255
+ // injectCacheKeyPlugin is the source-relative path (e.g. "../../src/js/pages/
256
+ // HealthRecords/HealthRecords.js"), so the runtime ChunkExtractor lookup misses
257
+ // and the route's CSS never reaches the inline <style>. Alias every split's
258
+ // entry under its source-path key so the existing direct lookup hits.
259
+ //
260
+ // Aliases share the same object as the canonical entry — safe because this runs
261
+ // after enrichWithTransitiveCss, so allCss is already populated.
262
+ function addSourcePathAliases(categorizedManifest) {
263
+ // Match injectCacheKeyPlugin's manifestDir convention exactly so the alias key
264
+ // equals the cacheKey string baked into chunks.
265
+ const manifestDir = path.join(process.env.src_path, "build", ".vite")
266
+
267
+ for (const [moduleId, splitInfo] of splitModules.entries()) {
268
+ const chunkFile = moduleToChunk.get(moduleId)
269
+ if (!chunkFile) continue
270
+
271
+ let sourcePathKey = path.relative(manifestDir, moduleId).replace(/\\/g, "/")
272
+ if (!sourcePathKey.endsWith(".js")) sourcePathKey += ".js"
273
+
274
+ const bucket = splitInfo.ssr ? "ssrTrue" : "ssrFalse"
275
+ // Already keyed by source path (non-anonymous chunk) — nothing to do.
276
+ if (categorizedManifest[bucket][sourcePathKey]) continue
277
+
278
+ let aliasTarget = null
279
+ for (const entry of Object.values(categorizedManifest[bucket])) {
280
+ if (entry?.file === chunkFile) {
281
+ aliasTarget = entry
282
+ break
283
+ }
284
+ }
285
+ if (aliasTarget) {
286
+ categorizedManifest[bucket][sourcePathKey] = aliasTarget
287
+ }
288
+ }
289
+ }
290
+
291
+ // Guard against silent CSS loss: every CSS file emitted to disk should be referenced
292
+ // by at least one bucket's css or allCss. If enrichWithTransitiveCss ever misses a
293
+ // branch of the import graph, the affected CSS would vanish from server responses
294
+ // with no runtime error — this check surfaces that as a build-time warning.
295
+ function validateCssCoverage(categorizedManifest, buildDir) {
296
+ const cssDir = path.join(buildDir, "client", "assets", "css")
297
+ if (!fs.existsSync(cssDir)) return
298
+
299
+ const emitted = new Set(
300
+ fs
301
+ .readdirSync(cssDir)
302
+ .filter((f) => f.endsWith(".css"))
303
+ .map((f) => `client/assets/css/${f}`)
304
+ )
305
+
306
+ const covered = new Set()
307
+ for (const category of ["essential", "ssrTrue", "ssrFalse", "orphan"]) {
308
+ for (const chunk of Object.values(categorizedManifest[category])) {
309
+ for (const c of chunk.css || []) covered.add(c)
310
+ for (const c of chunk.allCss || []) covered.add(c)
311
+ }
312
+ }
313
+
314
+ const missing = [...emitted].filter((c) => !covered.has(c))
315
+ if (missing.length > 0) {
316
+ console.warn(
317
+ `\n⚠️ [manifest-categorization] ${missing.length} emitted CSS file(s) are not ` +
318
+ `referenced by any bucket's css/allCss.\n` +
319
+ `At request time the ChunkExtractor may silently drop them, causing unstyled ` +
320
+ `content on affected routes. Likely cause: a regression in enrichWithTransitiveCss, ` +
321
+ `a new chunk shape Vite emits that the walker doesn't follow, or a categorization ` +
322
+ `gap that leaves some chunk unreachable from every bucket.\n\n` +
323
+ `Unreferenced files:\n${missing.map((m) => " " + m).join("\n")}\n\n` +
324
+ `Please raise an issue at https://github.com/tata1mg/catalyst-core/issues so the ` +
325
+ `maintainers can investigate.\n`
326
+ )
327
+ }
328
+ }
329
+
330
+ return {
331
+ name: "manifest-categorization",
332
+
333
+ // Scan source files to identify split calls, collect for async resolution in buildEnd
334
+ transform(code, id) {
335
+ if (id.includes("node_modules")) return null
336
+ if (!code.includes("split")) return null
337
+
338
+ let ast
339
+ try {
340
+ ast = this.parse(code)
341
+ } catch {
342
+ return null
343
+ }
344
+
345
+ walk(ast, (node) => {
346
+ if (node.type !== "CallExpression") return
347
+ if (node.callee.type !== "Identifier" || node.callee.name !== "split") return
348
+
349
+ const args = node.arguments
350
+ if (args.length === 0) return
351
+
352
+ // First arg must be a function that returns import("...")
353
+ const loader = args[0]
354
+ let importExpr
355
+
356
+ if (loader.type === "ArrowFunctionExpression") {
357
+ importExpr = loader.body
358
+ } else if (loader.type === "FunctionExpression") {
359
+ const stmts = loader.body.body
360
+ if (stmts.length !== 1 || stmts[0].type !== "ReturnStatement") return
361
+ importExpr = stmts[0].argument
362
+ } else {
363
+ return
364
+ }
365
+
366
+ if (!importExpr || importExpr.type !== "ImportExpression") return
367
+ if (importExpr.source.type !== "Literal" || typeof importExpr.source.value !== "string")
368
+ return
369
+
370
+ const importPath = importExpr.source.value
371
+
372
+ // Extract ssr value from options object (second arg) if present
373
+ let ssrValue = true // default
374
+ if (args.length >= 2 && args[1].type === "ObjectExpression") {
375
+ const ssrProp = args[1].properties.find(
376
+ (p) =>
377
+ p.type === "Property" &&
378
+ p.key.type === "Identifier" &&
379
+ p.key.name === "ssr" &&
380
+ p.value.type === "Literal"
381
+ )
382
+ if (ssrProp) {
383
+ ssrValue = ssrProp.value.value === true
384
+ }
385
+ }
386
+
387
+ pendingSplitPaths.push({ importPath, importer: id, ssrValue })
388
+ })
389
+
390
+ return null
391
+ },
392
+
393
+ // Resolve all split paths here — buildEnd is async-safe, unlike transform
394
+ async buildEnd() {
395
+ await Promise.all(
396
+ pendingSplitPaths.map(async ({ importPath, importer, ssrValue }) => {
397
+ try {
398
+ const resolved = await this.resolve(importPath, importer)
399
+ if (resolved) {
400
+ splitModules.set(resolved.id, {
401
+ ssr: ssrValue,
402
+ originalPath: importPath,
403
+ })
404
+ }
405
+ } catch (err) {
406
+ console.log(`❌ Error resolving ${importPath}:`, err.message)
407
+ }
408
+ })
409
+ )
410
+ },
411
+
412
+ generateBundle(options, bundle) {
413
+ processedManifest = processBundle(bundle)
414
+ },
415
+
416
+ // Write the file after all files are written (manifest.json is available)
417
+ closeBundle() {
418
+ let viteManifest = null
419
+
420
+ try {
421
+ const manifestPath = path.join(
422
+ process.env.src_path,
423
+ process.env.BUILD_OUTPUT_PATH || "build",
424
+ ".vite",
425
+ "manifest.json"
426
+ )
427
+ if (fs.existsSync(manifestPath)) {
428
+ const manifestContent = fs.readFileSync(manifestPath, "utf8")
429
+ viteManifest = JSON.parse(manifestContent)
430
+ } else {
431
+ console.log("❌ Could not find manifest.json at expected path:", manifestPath)
432
+ return
433
+ }
434
+ } catch (e) {
435
+ console.log("❌ Could not read manifest from file system:", e.message)
436
+ return
437
+ }
438
+
439
+ if (!processedManifest) {
440
+ console.log("❌ No processed manifest available")
441
+ return
442
+ }
443
+
444
+ applyViteManifestStructure(processedManifest, viteManifest)
445
+ enrichWithTransitiveCss(processedManifest, viteManifest)
446
+ addSourcePathAliases(processedManifest)
447
+
448
+ const buildDir = path.join(process.env.src_path, process.env.BUILD_OUTPUT_PATH || "build")
449
+ validateCssCoverage(processedManifest, buildDir)
450
+
451
+ const outputPath = path.join(buildDir, ".vite", outputFile)
452
+ fs.writeFileSync(outputPath, JSON.stringify(processedManifest, null, 2))
453
+ },
454
+ }
455
+ }