catalyst-core 0.2.0-beta.3 → 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 (160) 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/androidSetup.js +300 -8
  8. package/dist/native/bridge/WebBridge.js +481 -59
  9. package/dist/native/bridge/constants/NativeInterfaces.js +290 -45
  10. package/dist/native/bridge/errors.js +250 -26
  11. package/dist/native/bridge/hooks/index.js +125 -1
  12. package/dist/native/bridge/hooks/useCamera.js +347 -9
  13. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  14. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  15. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  16. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  17. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  18. package/dist/native/bridge/hooks/useIntent.js +95 -1
  19. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  20. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  21. package/dist/native/bridge/hooks/useNotification.js +318 -2
  22. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  23. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  24. package/dist/native/bridge/hooks/utils.js +27 -1
  25. package/dist/native/bridge/hooks.js +16 -1
  26. package/dist/native/bridge/perf/collectors/ApiCollector.js +66 -3
  27. package/dist/native/bridge/perf/collectors/CacheCollector.js +186 -11
  28. package/dist/native/bridge/perf/collectors/HookCollector.js +141 -19
  29. package/dist/native/bridge/perf/collectors/InsightsCollector.js +481 -23
  30. package/dist/native/bridge/perf/collectors/InteractionCollector.js +245 -10
  31. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +107 -8
  32. package/dist/native/bridge/perf/collectors/NavigationCollector.js +273 -32
  33. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +178 -2
  34. package/dist/native/bridge/perf/collectors/RenderCollector.js +258 -14
  35. package/dist/native/bridge/perf/collectors/ScrollCollector.js +95 -9
  36. package/dist/native/bridge/perf/collectors/ThreadCollector.js +90 -6
  37. package/dist/native/bridge/perf/core/constants.js +103 -20
  38. package/dist/native/bridge/perf/core/measure.js +179 -30
  39. package/dist/native/bridge/perf/core/session.js +89 -23
  40. package/dist/native/bridge/perf/core/store.js +554 -1
  41. package/dist/native/bridge/perf/core/summary.js +174 -1
  42. package/dist/native/bridge/perf/core/traceExport.js +240 -1
  43. package/dist/native/bridge/perf/core/waterfalls.js +146 -1
  44. package/dist/native/bridge/perf/index.js +359 -38
  45. package/dist/native/bridge/safeArea.js +53 -5
  46. package/dist/native/bridge/standardizedHooks.js +595 -82
  47. package/dist/native/bridge/useBaseHook.js +274 -38
  48. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  49. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  50. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  51. package/dist/native/buildAndroid/assets.js +510 -13
  52. package/dist/native/buildAndroid/build.js +296 -19
  53. package/dist/native/buildAndroid/config.js +87 -1
  54. package/dist/native/buildAndroid/index.js +374 -1
  55. package/dist/native/buildAppAndroid.js +34 -2
  56. package/dist/native/buildAppIos.js +62 -2
  57. package/dist/native/buildIos/assets.js +609 -8
  58. package/dist/native/buildIos/build.js +639 -20
  59. package/dist/native/buildIos/config.js +294 -12
  60. package/dist/native/buildIos/index.js +343 -6
  61. package/dist/native/buildIos/plugins.js +343 -16
  62. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  63. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  64. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  65. package/dist/native/internalPluginUtils.js +232 -7
  66. package/dist/native/package.json +3 -0
  67. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  68. package/dist/native/pluginComposerAndroid.js +407 -23
  69. package/dist/native/pluginComposerIos.js +435 -11
  70. package/dist/native/renameAndroidProject.js +1094 -88
  71. package/dist/native/setupEmulatorIos.js +289 -22
  72. package/dist/native/setupServer.js +160 -19
  73. package/dist/native/terminalProgress.js +199 -11
  74. package/dist/native/utils.js +193 -15
  75. package/dist/offline/catalyst-sw.js +145 -1
  76. package/dist/offline/registerServiceWorker.js +33 -1
  77. package/dist/otel.js +375 -46
  78. package/dist/router/ClientRouter.js +14 -2
  79. package/dist/router/ServerRouter.js +9 -1
  80. package/dist/router.js +1 -1
  81. package/dist/scripts/build.js +112 -3
  82. package/dist/scripts/checkVersion.js +3 -1
  83. package/dist/scripts/cleanCache.js +30 -1
  84. package/dist/scripts/generateOfflineManifest.js +116 -1
  85. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  86. package/dist/scripts/plugins.js +309 -1
  87. package/dist/scripts/preServerInit.js +13 -0
  88. package/dist/scripts/scriptUtils.js +54 -3
  89. package/dist/scripts/serve.js +51 -4
  90. package/dist/scripts/start.js +47 -10
  91. package/dist/scripts/validator.js +149 -1
  92. package/dist/sentry.cjs +127 -0
  93. package/dist/sentry.js +125 -5
  94. package/dist/server/expressServer.js +250 -5
  95. package/dist/server/manifestCache.js +36 -0
  96. package/dist/server/renderer/ChunkExtractor.js +144 -0
  97. package/dist/server/renderer/document/Body.jsx +59 -0
  98. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  99. package/dist/server/renderer/document/Head.jsx +60 -0
  100. package/dist/server/renderer/document/index.jsx +2 -0
  101. package/dist/server/renderer/extract.js +149 -31
  102. package/dist/server/renderer/handler.jsx +433 -0
  103. package/dist/server/renderer/index.js +8 -1
  104. package/dist/server/renderer/render.js +47 -0
  105. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  106. package/dist/server/utils/cjsRequire.js +10 -0
  107. package/dist/server/utils/userAgentUtil.js +87 -9
  108. package/dist/server/utils/validator.js +164 -1
  109. package/dist/vite/FastRefresh.jsx +20 -0
  110. package/dist/vite/inject-cache-key-plugin.js +276 -0
  111. package/dist/vite/loadCustomViteConfig.js +22 -0
  112. package/dist/vite/manifest-categorization-plugin.js +455 -0
  113. package/dist/vite/node-loader.mjs +458 -0
  114. package/dist/vite/scssParams.js +21 -0
  115. package/dist/vite/vite.config.client.js +130 -0
  116. package/dist/vite/vite.config.js +422 -0
  117. package/dist/vite/vite.config.server.js +84 -0
  118. package/dist/web-router/components/MetaTag.jsx +34 -0
  119. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  120. package/dist/web-router/components/Split.jsx +200 -0
  121. package/dist/web-router/components/SplitInview.jsx +106 -0
  122. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  123. package/dist/web-router/context.jsx +12 -0
  124. package/dist/web-router/hooks.jsx +72 -0
  125. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  126. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  127. package/dist/web-router/types/context.d.ts +14 -0
  128. package/dist/web-router/types/hooks.d.ts +1 -0
  129. package/dist/web-router/types/index.d.ts +3 -0
  130. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  131. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  132. package/mcp_v2/conversion-tasks.json +35 -17
  133. package/mcp_v2/knowledge-base.json +124 -13
  134. package/mcp_v2/lib/helpers.js +10 -0
  135. package/mcp_v2/mcp.js +4 -4
  136. package/mcp_v2/package.json +1 -0
  137. package/mcp_v2/tools/build.js +116 -20
  138. package/mcp_v2/tools/conversion.js +106 -5
  139. package/mcp_v2/tools/tasks.js +18 -14
  140. package/package.json +35 -68
  141. package/babel.config.js +0 -32
  142. package/dist/index.js +0 -1
  143. package/dist/scripts/devBuild.js +0 -3
  144. package/dist/scripts/devServe.js +0 -3
  145. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  146. package/dist/scripts/registerAliases.js +0 -6
  147. package/dist/server/renderer/document/Body.js +0 -11
  148. package/dist/server/renderer/document/Head.js +0 -9
  149. package/dist/server/renderer/document/index.js +0 -1
  150. package/dist/server/renderer/handler.js +0 -47
  151. package/dist/server/startServer.js +0 -57
  152. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  153. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  154. package/dist/webpack/babel.config.client.js +0 -1
  155. package/dist/webpack/babel.config.ssr.js +0 -1
  156. package/dist/webpack/base.babel.js +0 -7
  157. package/dist/webpack/development.client.babel.js +0 -10
  158. package/dist/webpack/production.client.babel.js +0 -4
  159. package/dist/webpack/production.ssr.babel.js +0 -8
  160. package/dist/webpack/scssParams.js +0 -9
@@ -1,31 +1,149 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=exports.cacheAndFetchAssets=void 0;var _path=_interopRequireDefault(require("path"));var _fs=_interopRequireDefault(require("fs"));var _otel=require("../../otel");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}// Use decimal megabytes (1 MB = 1,000,000 bytes) for all memory logs
2
- const BYTES_PER_MB=1_000_000;const toMB=bytes=>(bytes/BYTES_PER_MB).toFixed(2);const SSR_SERVICE=process.env.SERVICE_NAME||`pwa-${process.env.APPLICATION}-node-server-otel`;/**
3
- * Caches CSS file content (shared across all routes)
4
- * @param {string} assetName - CSS filename
5
- * @param {string} assetPath - Full path to CSS file
6
- */function cacheCSS(assetName,assetPath){if(!process.cssCache){process.cssCache={};}if(!process.cssCache[assetName]){const css=_fs.default.readFileSync(assetPath);process.cssCache[assetName]=css;const cssCacheSize=Object.keys(process.cssCache).length;const cssCacheTotalSizeBytes=Object.values(process.cssCache).reduce((total,cssContent)=>total+Buffer.byteLength(cssContent),0);const memoryUsage=process.memoryUsage();const availableMemoryBytes=(memoryUsage?.heapTotal??0)-(memoryUsage?.heapUsed??0);logger.info(`Last Cached CSS - Asset: ${assetName}, `+`CSS Cache Entries: ${cssCacheSize}, `+`CSS Cache Total Size: ${toMB(cssCacheTotalSizeBytes)} MB, `+`RSS: ${toMB(memoryUsage?.rss??0)} MB, `+`Heap Total: ${toMB(memoryUsage?.heapTotal??0)} MB, `+`Heap Used: ${toMB(memoryUsage?.heapUsed??0)} MB, `+`External: ${toMB(memoryUsage?.external??0)} MB, `+`Array Buffers: ${toMB(memoryUsage?.arrayBuffers??0)} MB, `+`Available Memory: ${toMB(availableMemoryBytes)} MB, `+`Timestamp: ${new Date().toISOString()}`);}return process.cssCache[assetName];}/**
7
- * Extracts CSS and preload links from cached ChunkExtractor
8
- * @param {string} routePath - Route path
9
- * @returns {object} { css: string, preloadJSLinks: array } or null if not cached
10
- */function getAssetsFromCachedExtractor(routePath){const isProd=process.env.NODE_ENV==="production";// Only use cached extractor in production
11
- if(!isProd||!process.extractorCache||!process.extractorCache[routePath]){return null;}const extractor=process.extractorCache[routePath];const linkElements=extractor.getLinkElements({fetchpriority:"low"});// Extractor hasn't run renderToString yet — no chunks collected
12
- if(!linkElements||linkElements.length===0){return null;}const preloadJSLinks=linkElements.filter(asset=>asset?.props?.as==="script");const cssAssets=linkElements.filter(e=>{const href=e?.props?.href;return href&&href.endsWith(".css");});// Build inline CSS from cached CSS files
13
- const cssContents=[];for(const cssElement of cssAssets){const assetName=_path.default.basename(cssElement.props.href);// Check if CSS content is cached
14
- if(process.cssCache&&process.cssCache[assetName]){cssContents.push(process.cssCache[assetName]);}else{// CSS file not in cache yet — fall back to full render path
15
- return null;}}const cachedCss=cssContents.join("");return{css:cachedCss,preloadJSLinks:preloadJSLinks};}/**
16
- * Builds inline CSS from link elements by reading and caching CSS files
17
- * @param {array} linkElements - Link elements from ChunkExtractor
18
- * @returns {string} Concatenated CSS content
19
- */function buildInlineCSS(linkElements){if(!Array.isArray(linkElements)||linkElements.length===0){return"";}const cssContents=[];for(const element of linkElements){const href=element?.props?.href;if(href&&href.endsWith(".css")){const assetName=_path.default.basename(href);const assetPath=_path.default.resolve(process.env.src_path,`${process.env.BUILD_OUTPUT_PATH}/public`,assetName// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - assetName is reduced to a basename and CSS is read from the build output directory.
20
- );// Read and cache CSS content
21
- const cssContent=cacheCSS(assetName,assetPath);cssContents.push(cssContent);}}return cssContents.join("");}/**
22
- * Phase 2: called from renderMarkUp's onAllReady after renderToPipeableStream finishes.
23
- * Builds the inline CSS (<style> block) and script tags that are appended to the stream.
24
- * In production, CSS is inlined from disk-cached files; in dev, style tags are used directly.
25
- * Bots receive no JS — only the fully rendered HTML is needed for crawling.
26
- */const cacheAndFetchAssets=exports.cacheAndFetchAssets=(0,_otel.withSyncObservability)(SSR_SERVICE,function cacheAndFetchAssets({webExtractor,res,isBot}){let firstFoldCss="";let firstFoldJS="";const isProd=process.env.NODE_ENV==="production";const{routePath}=res.locals;const linkElements=webExtractor.getLinkElements({fetchpriority:"low"});if(routePath){if(isProd){firstFoldCss=buildInlineCSS(linkElements);if(firstFoldCss?.length)firstFoldCss=`<style>${firstFoldCss}</style>`;}else{firstFoldCss=webExtractor.getStyleTags();}firstFoldJS=!isBot?webExtractor.getScriptTags({fetchpriority:"low"}):"";}return{firstFoldCss,firstFoldJS};},"cacheAndFetchAssets");/**
27
- * Checks if we have cached assets for this route
28
- * Called during Phase 1 to determine if we can skip renderToString
29
- * @param {object} res - Response object
30
- * @param {object} route - Route configuration
31
- */var _default=exports.default=(0,_otel.withSyncObservability)(SSR_SERVICE,function extractAssets(res,route){try{const routePath=route.path;const cached=getAssetsFromCachedExtractor(routePath);if(cached&&(cached.css||cached.preloadJSLinks)){res.locals.pageCss=cached.css;res.locals.preloadJSLinks=cached.preloadJSLinks;return;}logger.info({message:"Cache Missed",uri:routePath});}catch(error){logger.error("Error in extracting assets:"+error);}},"extractAssets");
1
+ import React from "react"
2
+ import path from "path"
3
+ import fs from "fs"
4
+
5
+ // ── CSS: read from disk and inline as <style> ──────────────────────────
6
+
7
+ // Process-level cache — survives across requests, reset on deploy.
8
+ if (!process.cssFileCache) process.cssFileCache = {}
9
+
10
+ // Per-route deferred asset paths/URLs learned from past SSRs — inlined in <head> on later visits
11
+ // so late <style> after </body> does not re-layout already-painted content. CSS file bodies use
12
+ // process.cssFileCache inside readCssFromDisk (no repeat disk read).
13
+ if (!process.deferredAssetsByRoute) process.deferredAssetsByRoute = new Map()
14
+
15
+ const routeRecord = (routeKey) => {
16
+ let rec = process.deferredAssetsByRoute.get(routeKey)
17
+ if (!rec) {
18
+ rec = { css: new Set(), js: new Set() }
19
+ process.deferredAssetsByRoute.set(routeKey, rec)
20
+ }
21
+ return rec
22
+ }
23
+
24
+ /** Stable key for caching deferred chunks — uses the matched route pattern (e.g. "/product/:name/:id")
25
+ * so all pages on the same route share one entry regardless of their actual URL parameters. */
26
+ export const getDeferredRouteKey = (req, allMatches = []) => {
27
+ return allMatches?.length ? (allMatches[allMatches.length - 1]?.route?.path ?? null) : null
28
+ }
29
+
30
+ /** CSS paths (manifest-relative) previously deferred on this route — for <head> inlining. */
31
+ export const getCachedDeferredCssPathsForRoute = (routeKey) => {
32
+ if (!routeKey) return []
33
+ const rec = process.deferredAssetsByRoute.get(routeKey)
34
+ return rec ? [...rec.css] : []
35
+ }
36
+
37
+ /**
38
+ * Record deferred asset paths/URLs for this route. Returns CSS paths not yet on the route so they
39
+ * can be inlined after </body> only once; on later visits those paths are inlined in <head> instead.
40
+ * JS URLs are always emitted in HTML on every navigation — skipping "cached" scripts would omit modules.
41
+ * @returns {{ newCssPaths: string[] }}
42
+ */
43
+ export const registerDeferredAssetsForRoute = (routeKey, { css = [], js = [] } = {}, isBot = false) => {
44
+ if (!routeKey) return { newCssPaths: [] }
45
+ const rec = routeRecord(routeKey)
46
+ const newCssPaths = []
47
+ for (const p of css) {
48
+ if (!p) continue
49
+ if (!rec.css.has(p)) newCssPaths.push(p)
50
+ rec.css.add(p)
51
+ }
52
+ if (!isBot) {
53
+ for (const url of js) {
54
+ if (url) rec.js.add(url)
55
+ }
56
+ }
57
+ return { newCssPaths }
58
+ }
59
+
60
+ /**
61
+ * Cached deferred script URLs for this route, excluding URLs already loaded as critical scripts.
62
+ * @param {string|null} routeKey - Matched route pattern.
63
+ * @param {Iterable<string>} excludeUrls - Critical / head script src URLs.
64
+ * @returns {string[]}
65
+ */
66
+ export const getDeferredPreloadScriptUrls = (routeKey, excludeUrls = []) => {
67
+ if (!routeKey) return []
68
+ const rec = process.deferredAssetsByRoute.get(routeKey)
69
+ if (!rec) return []
70
+ const exclude = new Set(excludeUrls)
71
+ return [...rec.js].filter((url) => url && !exclude.has(url))
72
+ }
73
+
74
+ /**
75
+ * React <link rel="modulepreload"> elements (deduped). Use before matching <script type="module">.
76
+ * @param {string[]} jsUrls
77
+ * @param {string} [keyPrefix] - Unique prefix for React keys when rendering multiple lists.
78
+ */
79
+ export const generateModulePreloadLinkElements = (jsUrls = [], keyPrefix = "modulepreload") =>
80
+ [...new Set(jsUrls)].map((url, i) =>
81
+ React.createElement("link", {
82
+ key: `${keyPrefix}-${i}`,
83
+ rel: "modulepreload",
84
+ href: url,
85
+ fetchPriority: "high",
86
+ })
87
+ )
88
+
89
+ /**
90
+ * Read CSS files from disk and return concatenated CSS string for inlining.
91
+ * @param {string[]} cssPaths - Relative CSS paths (from manifest).
92
+ * @param {string} basePath - Build output directory on disk.
93
+ * @returns {string} Concatenated CSS content.
94
+ */
95
+ export const readCssFromDisk = (cssPaths = [], basePath) => {
96
+ if (!cssPaths.length) return ""
97
+
98
+ const seen = new Set()
99
+ const chunks = []
100
+
101
+ for (const asset of cssPaths) {
102
+ if (!asset || seen.has(asset)) continue
103
+ seen.add(asset)
104
+ if (asset.startsWith("http")) continue
105
+
106
+ const filePath = path.isAbsolute(asset) ? asset : path.join(basePath, asset.replace(/^\/+/, ""))
107
+
108
+ try {
109
+ if (!process.cssFileCache[filePath]) {
110
+ process.cssFileCache[filePath] = fs.readFileSync(filePath, "utf8")
111
+ }
112
+ if (process.cssFileCache[filePath]) {
113
+ chunks.push(process.cssFileCache[filePath])
114
+ }
115
+ } catch {
116
+ // Silently skip unreadable assets in production
117
+ }
118
+ }
119
+
120
+ return chunks.join("\n")
121
+ }
122
+
123
+ // ── React elements (for SSR rendering inside <Head>) ───────────────────
124
+
125
+ /**
126
+ * <script type="module"> React elements for JS assets.
127
+ */
128
+ export const generateScriptElements = (jsUrls = []) =>
129
+ [...new Set(jsUrls)].map((url, i) =>
130
+ React.createElement("script", { key: `js-${i}`, type: "module", src: url })
131
+ )
132
+
133
+ // ── HTML strings (for streaming injection after body via res.write) ────
134
+
135
+ /**
136
+ * <link rel="stylesheet"> HTML strings for deferred CSS (non-blocking, after body).
137
+ */
138
+ export const generateCssLinkStrings = (cssUrls = []) =>
139
+ [...new Set(cssUrls)].map((url) => `<link rel="stylesheet" href="${url}">`).join("")
140
+
141
+ /**
142
+ * <link rel="modulepreload"> + <script type="module"> HTML strings.
143
+ */
144
+ export const generateScriptStrings = (jsUrls = []) =>
145
+ [...new Set(jsUrls)]
146
+ .map((url) => {
147
+ return `<script type="module" src="${url}"></script>`
148
+ })
149
+ .join("")
@@ -0,0 +1,433 @@
1
+ import React from "react"
2
+ import { renderStart, renderEnd } from "./render.js"
3
+ import { Provider } from "react-redux"
4
+ import { Body } from "./document/Body.jsx"
5
+ import { Head } from "./document/Head.jsx"
6
+
7
+ import { StaticRouter } from "react-router-dom/server"
8
+ import ServerRouter from "../../router/ServerRouter.js"
9
+ import { renderToPipeableStream } from "react-dom/server"
10
+ import { getUserAgentDetails } from "../utils/userAgentUtil.js"
11
+ import { serverDataFetcher, matchRoutes as NestedMatchRoutes, getMetaData } from "../../index.jsx"
12
+ import { validateConfigureStore, validateGetRoutes, safeCall } from "../utils/validator.js"
13
+ import { ChunkExtractor } from "./ChunkExtractor.js"
14
+ import {
15
+ readCssFromDisk,
16
+ generateScriptElements,
17
+ generateCssLinkStrings,
18
+ generateScriptStrings,
19
+ getDeferredRouteKey,
20
+ getCachedDeferredCssPathsForRoute,
21
+ registerDeferredAssetsForRoute,
22
+ getDeferredPreloadScriptUrls,
23
+ generateModulePreloadLinkElements,
24
+ } from "./extract.js"
25
+ import path from "path"
26
+
27
+ import CustomDocument from "@catalyst/template/server/document"
28
+
29
+ import App from "@catalyst/template/src/js/containers/App/index"
30
+ import { getRoutes } from "@catalyst/template/src/js/routes/utils"
31
+ import createStore from "@catalyst/template/src/js/store/index.js"
32
+ import { SsrRequestProvider } from "../../web-router/components/SsrRequestContext.jsx"
33
+ import { getManifest, getAssetManifest } from "../manifestCache.js"
34
+
35
+ const DEFAULT_SAFE_AREA_INSETS = { top: 0, right: 0, bottom: 0, left: 0 }
36
+
37
+ const parseSafeAreaFromHeaders = (req) => {
38
+ const readEdge = (header) => {
39
+ const raw = req.get(header) ?? req.headers[header.toLowerCase()]
40
+ const value = Number(raw)
41
+ return Number.isFinite(value) && value >= 0 ? value : null
42
+ }
43
+
44
+ const top = readEdge("X-Safe-Area-Top")
45
+ const right = readEdge("X-Safe-Area-Right")
46
+ const bottom = readEdge("X-Safe-Area-Bottom")
47
+ const left = readEdge("X-Safe-Area-Left")
48
+
49
+ if (![top, right, bottom, left].some((value) => value !== null)) return null
50
+
51
+ return {
52
+ top: top ?? 0,
53
+ right: right ?? 0,
54
+ bottom: bottom ?? 0,
55
+ left: left ?? 0,
56
+ }
57
+ }
58
+
59
+ // Routes are static for the lifetime of the server — resolve once and reuse
60
+ // the same instance per request to avoid per-request allocation.
61
+ let _cachedRoutes
62
+ const getCachedRoutes = () => {
63
+ if (_cachedRoutes === undefined) {
64
+ _cachedRoutes = validateGetRoutes(getRoutes) ? getRoutes() : null
65
+ }
66
+ return _cachedRoutes
67
+ }
68
+
69
+ // Try to import user-defined hooks. These are optional — apps that don't export them
70
+ // will get undefined, and safeCall is a no-op for non-functions.
71
+ let _onRouteMatch,
72
+ _onFetcherSuccess,
73
+ _onFetcherError,
74
+ _onAppServerSideSuccess,
75
+ _onAppServerSideError,
76
+ _onRenderError,
77
+ _onRequestError
78
+ try {
79
+ const hooks = await import("@catalyst/template/server/index.js")
80
+ _onRouteMatch = hooks.onRouteMatch
81
+ _onFetcherSuccess = hooks.onFetcherSuccess
82
+ _onFetcherError = hooks.onFetcherError
83
+ _onAppServerSideSuccess = hooks.onAppServerSideSuccess
84
+ _onAppServerSideError = hooks.onAppServerSideError
85
+ _onRenderError = hooks.onRenderError
86
+ _onRequestError = hooks.onRequestError
87
+ } catch {
88
+ // No hooks file — all hooks remain undefined, safeCall will skip them
89
+ }
90
+
91
+ // Passthrough no-ops used when OTEL_ENABLE is not set; replaced below if enabled.
92
+ let withObservability = (_service, fn) => fn
93
+ let withSyncObservability = (_service, fn) => fn
94
+
95
+ if (process.env.OTEL_ENABLE === true) {
96
+ try {
97
+ const otel = await import("../../otel.js")
98
+ withObservability = otel.withObservability
99
+ withSyncObservability = otel.withSyncObservability
100
+ } catch {
101
+ // otel packages not installed — continue without tracing
102
+ }
103
+ }
104
+
105
+ const SSR_SERVICE = process.env.SERVICE_NAME || `pwa-${process.env.APPLICATION}-node-server`
106
+
107
+ const traceHook = (fn, spanName) =>
108
+ typeof fn === "function" ? withSyncObservability(SSR_SERVICE, fn, spanName) : fn
109
+
110
+ const onRouteMatch = traceHook(_onRouteMatch, "onRouteMatch")
111
+ const onFetcherSuccess = traceHook(_onFetcherSuccess, "onFetcherSuccess")
112
+ const onFetcherError = traceHook(_onFetcherError, "onFetcherError")
113
+ const onAppServerSideSuccess = traceHook(_onAppServerSideSuccess, "onAppServerSideSuccess")
114
+ const onAppServerSideError = traceHook(_onAppServerSideError, "onAppServerSideError")
115
+ const onRenderError = traceHook(_onRenderError, "onRenderError")
116
+ const onRequestError = traceHook(_onRequestError, "onRequestError")
117
+
118
+ // ── Asset collection ───────────────────────────────────────────────────
119
+ const _collectAssets = (req, allMatches) => {
120
+ const chunkExtractor = new ChunkExtractor({
121
+ manifest: getManifest() || {},
122
+ assetManifest: getAssetManifest() || {},
123
+ })
124
+
125
+ // Add route-matched CSS/JS to critical bucket (loaded in <head>)
126
+ chunkExtractor.preloadRouteCss(allMatches)
127
+
128
+ return chunkExtractor
129
+ }
130
+
131
+ const collectAssets = withSyncObservability(SSR_SERVICE, _collectAssets, "collectAssets")
132
+
133
+ // ── JSX tree ───────────────────────────────────────────────────────────
134
+ const getComponent = (store, context, req, fetcherData, isBot) => (
135
+ <div id="app">
136
+ <SsrRequestProvider value={{ isBot }}>
137
+ <Provider store={store}>
138
+ <StaticRouter context={context} location={req.originalUrl}>
139
+ <ServerRouter store={store} intialData={fetcherData} />
140
+ </StaticRouter>
141
+ </Provider>
142
+ </SsrRequestProvider>
143
+ </div>
144
+ )
145
+
146
+ // ── Render and stream ──────────────────────────────────────────────────
147
+ const _renderMarkUp = async (
148
+ errorCode,
149
+ req,
150
+ res,
151
+ metaTags,
152
+ fetcherData,
153
+ store,
154
+ allMatches,
155
+ context,
156
+ chunkExtractor
157
+ ) => {
158
+ const deviceDetails = getUserAgentDetails(req.headers["user-agent"] || "")
159
+ // Match mweb's wider definition: synthetic monitors (StatusCake) and AI crawlers
160
+ // also need the bot code path — no JS, and split(... ssr: false ...) widgets that
161
+ // previously read state.shellReducer.isBot now read this via SsrRequestContext.
162
+ const isBot = !!(deviceDetails.googleBot || deviceDetails.aiBot || deviceDetails.statusCakeBot)
163
+ const safeArea = parseSafeAreaFromHeaders(req) || { ...DEFAULT_SAFE_AREA_INSETS }
164
+ const previousSafeArea = globalThis.__SAFE_AREA_INITIAL__
165
+ globalThis.__SAFE_AREA_INITIAL__ = safeArea
166
+
167
+ const cleanupSafeArea = () => {
168
+ if (previousSafeArea === undefined) {
169
+ delete globalThis.__SAFE_AREA_INITIAL__
170
+ } else {
171
+ globalThis.__SAFE_AREA_INITIAL__ = previousSafeArea
172
+ }
173
+ }
174
+
175
+ // Critical assets → <head>
176
+ const criticalAssets = chunkExtractor ? chunkExtractor.getCriticalAssets() : { js: [], css: [] }
177
+
178
+ // Inline critical CSS from disk (small thanks to natural code-splitting)
179
+ const buildDir = path.join(process.env.src_path, process.env.BUILD_OUTPUT_PATH || "build")
180
+ const inlineCss = readCssFromDisk(criticalAssets.css, buildDir)
181
+
182
+ const deferredRouteKey = getDeferredRouteKey(req, allMatches)
183
+ const deferredRouteInlineCss = readCssFromDisk(
184
+ getCachedDeferredCssPathsForRoute(deferredRouteKey),
185
+ buildDir
186
+ )
187
+
188
+ const jsScripts = generateScriptElements(criticalAssets.js)
189
+ const criticalPreloadLinks = generateModulePreloadLinkElements(criticalAssets.js, "critical-js")
190
+ const deferredPreloadUrls = getDeferredPreloadScriptUrls(deferredRouteKey, criticalAssets.js)
191
+ const deferredPreloadLinks = generateModulePreloadLinkElements(deferredPreloadUrls, "deferred-js")
192
+
193
+ // Build Head props
194
+ const shellStart = renderStart({
195
+ inlineCss,
196
+ deferredRouteInlineCss,
197
+ jsScripts,
198
+ criticalPreloadLinks,
199
+ deferredPreloadLinks,
200
+ metaTags,
201
+ isBot,
202
+ fetcherData,
203
+ })
204
+
205
+ const state = store.getState()
206
+ const jsx = getComponent(store, context, req, fetcherData, isBot)
207
+ const shellEnd = renderEnd(state, res, jsx, errorCode, fetcherData)
208
+
209
+ const finalProps = { ...shellStart, ...shellEnd, jsx, req, res, safeArea }
210
+
211
+ const CompleteDocument = () => {
212
+ if (CustomDocument) {
213
+ return CustomDocument(finalProps)
214
+ }
215
+ return (
216
+ <html lang={finalProps.lang}>
217
+ <Head
218
+ isBot={finalProps.isBot}
219
+ inlineCss={finalProps.inlineCss}
220
+ deferredRouteInlineCss={finalProps.deferredRouteInlineCss}
221
+ jsScripts={finalProps.jsScripts}
222
+ criticalPreloadLinks={finalProps.criticalPreloadLinks}
223
+ deferredPreloadLinks={finalProps.deferredPreloadLinks}
224
+ fetcherData={finalProps.fetcherData}
225
+ metaTags={finalProps.metaTags}
226
+ publicAssetPath={finalProps.publicAssetPath}
227
+ />
228
+ <Body
229
+ initialState={finalProps.initialState}
230
+ jsx={finalProps.jsx}
231
+ statusCode={finalProps.statusCode}
232
+ fetcherData={finalProps.fetcherData}
233
+ safeArea={finalProps.safeArea}
234
+ />
235
+ </html>
236
+ )
237
+ }
238
+
239
+ try {
240
+ const status = errorCode || (allMatches.length && allMatches[0]?.route?.path === "*" ? 404 : 200)
241
+ res.set({ "content-type": "text/html; charset=utf-8" })
242
+ res.status(status)
243
+
244
+ return new Promise((resolve, reject) => {
245
+ const { pipe } = renderToPipeableStream(<CompleteDocument />, {
246
+ onShellReady() {
247
+ res.setHeader("content-type", "text/html")
248
+ pipe(res)
249
+ },
250
+
251
+ onAllReady() {
252
+ // Deferred assets — injected after body (non-blocking)
253
+ const deferredAssets = chunkExtractor
254
+ ? chunkExtractor.getDeferredAssets()
255
+ : { js: [], css: [] }
256
+
257
+ // Tell client which components were SSR'd so split() can
258
+ // eagerly import them (prevents Suspense fallback flash)
259
+ res.write(`<script>window.__CATALYST_IS_BOT__=${isBot ? "true" : "false"};</script>`)
260
+ if (chunkExtractor) {
261
+ const renderedKeys = chunkExtractor.getRenderedComponentKeys()
262
+ res.write(
263
+ `<script>window.__SSR_RENDERED_COMPONENTS__=new Set(${JSON.stringify(renderedKeys)})</script>`
264
+ )
265
+ }
266
+
267
+ const { newCssPaths } = registerDeferredAssetsForRoute(
268
+ deferredRouteKey,
269
+ deferredAssets,
270
+ isBot
271
+ )
272
+ if (newCssPaths.length) {
273
+ res.write(`<style>${readCssFromDisk(newCssPaths, buildDir)}</style>`)
274
+ }
275
+ if (!isBot) {
276
+ res.write(generateScriptStrings(deferredAssets.js))
277
+ }
278
+
279
+ res.end()
280
+ cleanupSafeArea()
281
+ resolve()
282
+ },
283
+
284
+ onError(error) {
285
+ console.error("Error in renderToPipeableStream:", error)
286
+ safeCall(onRenderError, { req, res, store, error })
287
+ cleanupSafeArea()
288
+ reject(error)
289
+ },
290
+ })
291
+ })
292
+ } catch (error) {
293
+ cleanupSafeArea()
294
+ console.error("Error in rendering document on server:", error)
295
+ safeCall(onRenderError, { req, res, store, error })
296
+ return Promise.reject(error)
297
+ }
298
+ }
299
+
300
+ const tracedRenderMarkUp = withObservability(SSR_SERVICE, _renderMarkUp, "renderMarkUp")
301
+ const tracedAppServerSideFunction = withObservability(
302
+ SSR_SERVICE,
303
+ (args) => App.serverSideFunction(args),
304
+ "App.serverSideFunction"
305
+ )
306
+ const tracedServerDataFetcher = withObservability(SSR_SERVICE, serverDataFetcher, "serverDataFetcher")
307
+ const tracedGetMetaData = withSyncObservability(SSR_SERVICE, getMetaData, "getMetaData")
308
+
309
+ // ── Express middleware ──────────────────────────────────────────────────
310
+ /**
311
+ * SSR request handler. Execution pipeline per request:
312
+ * 1. Match route → collect assets
313
+ * 2. App.serverSideFunction (app-level server hook)
314
+ * 3. serverDataFetcher (route-level data fetching)
315
+ * 4. renderMarkUp → renderToPipeableStream (stream to client)
316
+ *
317
+ * res.headersSent is checked after each async step: if a user hook
318
+ * (onRouteMatch, onFetcherSuccess, etc.) has already sent a response
319
+ * (e.g. a redirect), we bail out early without attempting another render.
320
+ */
321
+ async function _handler(req, res) {
322
+ try {
323
+ let context = {}
324
+ let fetcherData = {}
325
+ const store = validateConfigureStore(createStore) ? await createStore({}, req, res) : null
326
+
327
+ const cachedRoutes = getCachedRoutes()
328
+ const allMatches = cachedRoutes ? NestedMatchRoutes(cachedRoutes, req.originalUrl) || [] : []
329
+ let allTags = []
330
+
331
+ safeCall(onRouteMatch, { req, res, matches: allMatches, store })
332
+
333
+ if (res.headersSent) return
334
+
335
+ try {
336
+ await tracedAppServerSideFunction({ store, req, res })
337
+ safeCall(onAppServerSideSuccess, { req, res, store })
338
+
339
+ if (res.headersSent) return
340
+
341
+ try {
342
+ fetcherData = await tracedServerDataFetcher(
343
+ { routes: cachedRoutes, req, res, url: req.originalUrl },
344
+ { store }
345
+ )
346
+
347
+ if (res.headersSent) return
348
+
349
+ const err = fetcherData?.[req.originalUrl]?.error
350
+ allTags = tracedGetMetaData(allMatches, fetcherData)
351
+ const chunkExtractor = collectAssets(req, allMatches)
352
+
353
+ if (err) {
354
+ safeCall(onFetcherError, { req, res, store, error: err })
355
+
356
+ if (res.headersSent) return
357
+
358
+ const statusCode = err.status_code || 404
359
+ await tracedRenderMarkUp(
360
+ statusCode,
361
+ req,
362
+ res,
363
+ allTags,
364
+ fetcherData,
365
+ store,
366
+ allMatches,
367
+ context,
368
+ chunkExtractor
369
+ )
370
+ } else {
371
+ safeCall(onFetcherSuccess, { req, res, store })
372
+
373
+ if (res.headersSent) return
374
+
375
+ await tracedRenderMarkUp(
376
+ null,
377
+ req,
378
+ res,
379
+ allTags,
380
+ fetcherData,
381
+ store,
382
+ allMatches,
383
+ context,
384
+ chunkExtractor
385
+ )
386
+ }
387
+ } catch (error) {
388
+ console.error("Error in executing serverFetcher functions:", error)
389
+ safeCall(onFetcherError, { req, res, store, error })
390
+
391
+ if (res.headersSent) return
392
+
393
+ const chunkExtractor = collectAssets(req, allMatches)
394
+ await tracedRenderMarkUp(
395
+ 404,
396
+ req,
397
+ res,
398
+ allTags,
399
+ fetcherData,
400
+ store,
401
+ allMatches,
402
+ context,
403
+ chunkExtractor
404
+ )
405
+ }
406
+ } catch (error) {
407
+ console.error("Error in executing serverSideFunction inside App:", error)
408
+ safeCall(onAppServerSideError, { req, res, store, error })
409
+
410
+ if (res.headersSent) return
411
+
412
+ const chunkExtractor = collectAssets(req, allMatches)
413
+ await tracedRenderMarkUp(
414
+ error.status_code,
415
+ req,
416
+ res,
417
+ allTags,
418
+ fetcherData,
419
+ store,
420
+ allMatches,
421
+ context,
422
+ chunkExtractor
423
+ )
424
+ }
425
+ } catch (error) {
426
+ console.error("Error in handling document request:", error)
427
+ safeCall(onRequestError, { req, res, error })
428
+ }
429
+ }
430
+
431
+ const handler = withObservability(SSR_SERVICE, _handler, "handler")
432
+
433
+ export default handler
@@ -1 +1,8 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _express=_interopRequireDefault(require("express"));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const router=_express.default.Router();router.use(function rendererMiddleware(req,res,next){let handler=require("./handler").default;if(res.locals.rendererWrapper){logger.debug({message:"Handler wrapped"});res.locals.rendererWrapper(handler)(req,res,next);}else{handler(req,res,next);}});var _default=exports.default=router;
1
+ import express from "express"
2
+ import handler from "./handler.jsx"
3
+
4
+ const router = express.Router()
5
+
6
+ router.use(handler)
7
+
8
+ export default router
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Returns data used by the Head component for page rendering.
3
+ */
4
+ export const renderStart = ({
5
+ inlineCss,
6
+ deferredRouteInlineCss,
7
+ jsScripts,
8
+ criticalPreloadLinks,
9
+ deferredPreloadLinks,
10
+ metaTags,
11
+ isBot,
12
+ fetcherData,
13
+ }) => {
14
+ const { IS_DEV_COMMAND, WEBPACK_DEV_SERVER_HOSTNAME, WEBPACK_DEV_SERVER_PORT } = process.env
15
+
16
+ let publicAssetPath = `${process.env.PUBLIC_STATIC_ASSET_URL}${process.env.PUBLIC_STATIC_ASSET_PATH}`
17
+
18
+ if (JSON.parse(IS_DEV_COMMAND)) {
19
+ publicAssetPath = `http://${WEBPACK_DEV_SERVER_HOSTNAME}:${WEBPACK_DEV_SERVER_PORT}/assets/`
20
+ }
21
+
22
+ return {
23
+ inlineCss,
24
+ deferredRouteInlineCss,
25
+ jsScripts,
26
+ criticalPreloadLinks,
27
+ deferredPreloadLinks,
28
+ metaTags,
29
+ isBot,
30
+ publicAssetPath,
31
+ fetcherData,
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Returns data used by the Body component for page rendering.
37
+ */
38
+ export const renderEnd = (initialState = {}, res, jsx, errorCode, fetcherData) => {
39
+ return {
40
+ initialState,
41
+ firstFoldCss: "",
42
+ firstFoldJS: "",
43
+ jsx,
44
+ errorCode,
45
+ fetcherData,
46
+ }
47
+ }