catalyst-core 0.3.0-beta.5 → 0.3.0-canary.2
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/changelog.md +185 -71
- package/dist/errors/generateDocs.js +100 -0
- package/dist/errors/index.js +243 -0
- package/dist/errors/registry.js +483 -0
- package/dist/errors-index.json +506 -0
- package/dist/globals.d.ts +62 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.jsx +5 -8
- package/dist/logger.js +1 -1
- package/dist/native/androidProject/app/build.gradle.kts +142 -7
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +4 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +2 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +3 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +3 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +43 -8
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +181 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +156 -236
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileSizeRouterUtilsTest.kt +471 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +222 -226
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FrameworkServerUtilsTest.kt +300 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/IntentUtilsTest.kt +244 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/MetricsMonitorTest.kt +359 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +108 -63
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +329 -385
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/OfflineCacheServiceTest.kt +302 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/PerfEventBufferTest.kt +133 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/UtilsKtTest.kt +152 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +145 -324
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/BarcodeDetectorTest.kt +132 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/HoldControllerTest.kt +155 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/TorchControllerTest.kt +150 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateMachineTest.kt +120 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateTest.kt +62 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ViewfinderMapperTest.kt +143 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ZoomControllerTest.kt +421 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/design/DesignTokensTest.kt +207 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +368 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/FridaDetectorTest.kt +193 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckManagerTest.kt +260 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckSchedulerTest.kt +166 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DeviceInfoUtilsTest.kt +167 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DownloadUtilsTest.kt +197 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/NetworkMonitorTest.kt +271 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/PushNotificationUtilsTest.kt +119 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/SafeAreaUtilsTest.kt +108 -0
- package/dist/native/androidProject/build.gradle.kts +1 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +26 -0
- package/dist/native/androidProject/settings.gradle.kts +11 -8
- package/dist/native/androidSetup.js +2 -2
- package/dist/native/bridge/WebBridge.d.ts +98 -0
- package/dist/native/bridge/WebBridge.js +40 -6
- package/dist/native/bridge/constants/NativeInterfaces.d.ts +391 -0
- package/dist/native/bridge/errors.d.ts +67 -0
- package/dist/native/bridge/errors.js +66 -151
- package/dist/native/bridge/hooks/index.d.ts +13 -0
- package/dist/native/bridge/hooks/index.js +0 -6
- package/dist/native/bridge/hooks/useAI.d.ts +1 -0
- package/dist/native/bridge/hooks/useAI.js +3 -1
- package/dist/native/bridge/hooks/useCamera.d.ts +86 -0
- package/dist/native/bridge/hooks/useCamera.js +35 -6
- package/dist/native/bridge/hooks/useDataProtection.d.ts +17 -0
- package/dist/native/bridge/hooks/useDeviceInfo.d.ts +34 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +5 -0
- package/dist/native/bridge/hooks/useFilePicker.d.ts +53 -0
- package/dist/native/bridge/hooks/useFilePicker.js +13 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.d.ts +12 -0
- package/dist/native/bridge/hooks/useHapticFeedback.d.ts +76 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +6 -1
- package/dist/native/bridge/hooks/useIntent.d.ts +33 -0
- package/dist/native/bridge/hooks/useIntent.js +2 -0
- package/dist/native/bridge/hooks/useNativeTransition.d.ts +53 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +12 -5
- package/dist/native/bridge/hooks/useNetworkStatus.d.ts +22 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +7 -0
- package/dist/native/bridge/hooks/useNotification.d.ts +83 -0
- package/dist/native/bridge/hooks/useNotification.js +81 -34
- package/dist/native/bridge/hooks/useSafeArea.d.ts +12 -0
- package/dist/native/bridge/hooks/useSafeArea.js +8 -0
- package/dist/native/bridge/hooks/useVideoStream.d.ts +67 -0
- package/dist/native/bridge/hooks/useVideoStream.js +38 -52
- package/dist/native/bridge/hooks/utils.d.ts +4 -0
- package/dist/native/bridge/hooks.d.ts +1 -0
- package/dist/native/bridge/perf/collectors/ApiCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/ApiCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.d.ts +43 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/HookCollector.d.ts +37 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.d.ts +103 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.d.ts +53 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.d.ts +37 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.d.ts +47 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +8 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.d.ts +29 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.d.ts +31 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +4 -0
- package/dist/native/bridge/perf/core/constants.d.ts +76 -0
- package/dist/native/bridge/perf/core/measure.d.ts +39 -0
- package/dist/native/bridge/perf/core/measure.js +4 -0
- package/dist/native/bridge/perf/core/session.d.ts +49 -0
- package/dist/native/bridge/perf/core/session.js +4 -0
- package/dist/native/bridge/perf/core/store.d.ts +37 -0
- package/dist/native/bridge/perf/core/store.js +4 -0
- package/dist/native/bridge/perf/core/summary.d.ts +60 -0
- package/dist/native/bridge/perf/core/traceExport.d.ts +10 -0
- package/dist/native/bridge/perf/core/waterfalls.d.ts +2 -0
- package/dist/native/bridge/perf/index.d.ts +63 -0
- package/dist/native/bridge/perf/index.js +4 -0
- package/dist/native/bridge/safeArea.d.ts +11 -0
- package/dist/native/bridge/safeArea.js +2 -0
- package/dist/native/bridge/useBaseHook.d.ts +137 -0
- package/dist/native/bridge/useBaseHook.js +34 -4
- package/dist/native/bridge/utils/CameraUtils.d.ts +94 -0
- package/dist/native/bridge/utils/CameraUtils.js +4 -0
- package/dist/native/bridge/utils/FileObjectConverter.d.ts +36 -0
- package/dist/native/bridge/utils/NativeBridge.d.ts +270 -0
- package/dist/native/bridge/utils/NativeBridge.js +10 -0
- package/dist/native/buildAndroid/index.js +1 -1
- package/dist/native/buildAppAndroid.js +9 -0
- package/dist/native/buildAppIos.js +16 -1
- package/dist/native/buildErrorFormat.js +57 -0
- package/dist/native/buildIos/config.js +15 -1
- package/dist/native/buildIos/index.js +1 -1
- package/dist/native/buildIos/plugins.js +10 -1
- package/dist/native/iosnativeWebView/CoreLogic/Package.swift +36 -0
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamState.swift +1 -1
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamStateMachine.swift +6 -4
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/BootTimingUtility.swift +3 -3
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/FrameworkServerUtils.swift +14 -9
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NetworkMonitor.swift +11 -9
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NotificationHandlerProvider.swift +1 -1
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/URLWhitelistManager.swift +7 -7
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/AnyCodableTests.swift +154 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/BootTimingUtilityTests.swift +23 -5
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/CatalystConstantsTests.swift +192 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/ConfigMappingTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsErrorPathTests.swift +106 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsLoopbackTests.swift +243 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/FrameworkServerUtilsTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NetworkMonitorTests.swift +119 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NotificationHandlerProviderTests.swift +59 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/NotificationHandlerTests.swift +1 -1
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/URLWhitelistManagerTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateMachineTests.swift +171 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateTests.swift +91 -0
- package/dist/native/iosnativeWebView/Package.swift +10 -9
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +7 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +5 -4
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +6 -5
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +31 -30
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +22 -19
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ImageHandler.swift +40 -39
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +15 -15
- package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +4 -3
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +6 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +3 -5
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +20 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +6 -5
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +14 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +3 -3
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +9 -8
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +7 -6
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +24 -25
- package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +6 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +4 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +2 -22
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +2 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +15 -9
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +1 -0
- package/dist/native/setupEmulatorIos.js +2 -2
- package/dist/native/terminalProgress.js +17 -17
- package/dist/router/ClientRouter.d.ts +8 -0
- package/dist/router/ClientRouter.js +13 -11
- package/dist/router/ServerRouter.d.ts +13 -0
- package/dist/router/ServerRouter.js +19 -7
- package/dist/scripts/build.js +14 -2
- package/dist/scripts/loadEnvironmentVariables.js +3 -2
- package/dist/scripts/plugins.js +1 -1
- package/dist/scripts/preServerInit.js +49 -3
- package/dist/scripts/preflight.js +111 -0
- package/dist/scripts/scriptUtils.js +46 -1
- package/dist/scripts/serve.js +7 -3
- package/dist/scripts/start.js +8 -3
- package/dist/server/expressServer.d.ts +1 -0
- package/dist/server/expressServer.js +247 -271
- package/dist/server/manifestCache.d.ts +2 -0
- package/dist/server/manifestCache.js +25 -33
- package/dist/server/renderer/ChunkExtractor.d.ts +44 -0
- package/dist/server/renderer/ChunkExtractor.js +113 -125
- package/dist/server/renderer/document/Body.d.ts +21 -0
- package/dist/server/renderer/document/Body.jsx +45 -45
- package/dist/server/renderer/document/FastRefresh.d.ts +3 -0
- package/dist/server/renderer/document/FastRefresh.jsx +7 -13
- package/dist/server/renderer/document/Head.d.ts +20 -0
- package/dist/server/renderer/document/Head.jsx +32 -32
- package/dist/server/renderer/document/index.d.ts +2 -0
- package/dist/server/renderer/document/index.jsx +2 -2
- package/dist/server/renderer/extract.d.ts +67 -0
- package/dist/server/renderer/extract.js +92 -89
- package/dist/server/renderer/handler.d.ts +2 -0
- package/dist/server/renderer/handler.jsx +401 -404
- package/dist/server/renderer/index.d.ts +2 -0
- package/dist/server/renderer/index.js +5 -8
- package/dist/server/renderer/render.d.ts +35 -0
- package/dist/server/renderer/render.js +25 -32
- package/dist/server/utils/botDetectionMiddleware.d.ts +1 -0
- package/dist/server/utils/botDetectionMiddleware.js +33 -23
- package/dist/server/utils/cjsRequire.d.ts +8 -0
- package/dist/server/utils/cjsRequire.js +2 -2
- package/dist/server/utils/userAgentUtil.d.ts +7 -0
- package/dist/server/utils/userAgentUtil.js +68 -71
- package/dist/server/utils/validator.d.ts +31 -0
- package/dist/server/utils/validator.js +133 -158
- package/dist/vite/inject-cache-key-plugin.js +3 -27
- package/dist/vite/moduleAliases.js +36 -0
- package/dist/vite/node-loader.mjs +2 -29
- package/dist/vite/resolveDevServerConfig.js +4 -1
- package/dist/vite/vite.config.js +2 -29
- package/dist/web-router/components/MetaTag.d.ts +4 -0
- package/dist/web-router/components/MetaTag.jsx +26 -28
- package/dist/web-router/components/MetaTag.test.jsx +100 -0
- package/dist/web-router/components/RouterDataProvider.d.ts +96 -0
- package/dist/web-router/components/RouterDataProvider.jsx +260 -259
- package/dist/web-router/components/RouterDataProvider.server.test.jsx +98 -0
- package/dist/web-router/components/RouterDataProvider.test.jsx +310 -0
- package/dist/web-router/components/Split.d.ts +74 -0
- package/dist/web-router/components/Split.jsx +147 -156
- package/dist/web-router/components/Split.server.test.jsx +52 -0
- package/dist/web-router/components/Split.test.jsx +167 -0
- package/dist/web-router/components/SplitInview.d.ts +22 -0
- package/dist/web-router/components/SplitInview.jsx +78 -89
- package/dist/web-router/components/SplitInview.test.jsx +112 -0
- package/dist/web-router/components/SsrRequestContext.d.ts +20 -0
- package/dist/web-router/components/SsrRequestContext.jsx +17 -7
- package/dist/web-router/components/SsrRequestContext.test.jsx +53 -0
- package/dist/web-router/context.d.ts +15 -0
- package/dist/web-router/context.jsx +5 -5
- package/dist/web-router/context.test.jsx +10 -0
- package/dist/web-router/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +86 -86
- package/dist/web-router/utils/metaDataUtils.test.jsx +188 -0
- package/dist/web-router/vitest.setup.js +12 -0
- package/mcp_v2/knowledge-base.json +115 -17
- package/mcp_v2/mcp.js +141 -3
- package/mcp_v2/package-lock.json +2031 -0
- package/mcp_v2/package.json +13 -2
- package/mcp_v2/scripts/copyErrorsIndex.cjs +34 -0
- package/mcp_v2/test/errors.test.ts +123 -0
- package/mcp_v2/test/github.test.ts +177 -0
- package/mcp_v2/tools/errors.d.ts +20 -0
- package/mcp_v2/tools/errors.js +63 -0
- package/mcp_v2/tools/github.d.ts +41 -0
- package/mcp_v2/tools/github.js +231 -0
- package/mcp_v2/vitest.config.mjs +22 -0
- package/package.json +60 -11
- package/test/errors/docsDrift.test.ts +113 -0
- package/test/errors/index.test.ts +200 -0
- package/test/errors/registry.test.ts +108 -0
- package/test/scripts/preflight.test.ts +118 -0
- package/test/server/ChunkExtractor.test.ts +97 -0
- package/test/server/extract.test.ts +128 -0
- package/test/server/fixtures/template/server/document.jsx +19 -0
- package/test/server/fixtures/template/server/index.js +10 -0
- package/test/server/fixtures/template/src/js/containers/App/index.jsx +33 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +72 -0
- package/test/server/fixtures/template/src/js/routes/utils.jsx +39 -0
- package/test/server/fixtures/template/src/js/store/index.js +19 -0
- package/test/server/handler.assets.test.ts +330 -0
- package/test/server/handler.test.ts +208 -0
- package/test/server/manifestCache.test.ts +71 -0
- package/test/server/render.test.ts +71 -0
- package/test/server/scriptUtils.test.ts +103 -0
- package/test/server/userAgentUtil.test.ts +48 -0
- package/test/server/validator.test.ts +225 -0
- package/test/vite/resolveDevServerConfig.test.js +4 -1
- package/tsconfig.test.json +23 -0
- package/vitest.config.ts +93 -0
- package/dist/caching.js +0 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +0 -187
- package/dist/native/bridge/standardizedHooks.js +0 -611
- package/dist/router.js +0 -1
- package/dist/scripts/validator.js +0 -149
- package/dist/web-router/hooks.jsx +0 -72
- package/dist/web-router/types/components/MetaTag.d.ts +0 -2
- package/dist/web-router/types/components/RouterDataProvider.d.ts +0 -95
- package/dist/web-router/types/context.d.ts +0 -14
- package/dist/web-router/types/hooks.d.ts +0 -1
- package/dist/web-router/types/index.d.ts +0 -3
- package/dist/web-router/types/utils/metaDataUtils.d.ts +0 -40
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/CatalystConstants.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/ConfigConstants.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Notifications/NotificationModels.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Protocols/NotificationHandlerProtocol.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NullNotificationHandler.swift +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
ERROR_CODES,
|
|
4
|
+
CatalystError,
|
|
5
|
+
createError,
|
|
6
|
+
wrapForeignError,
|
|
7
|
+
wrapSSRError,
|
|
8
|
+
wrapProviderError,
|
|
9
|
+
formatError,
|
|
10
|
+
} from "../../src/errors/index.js"
|
|
11
|
+
|
|
12
|
+
// Framework-level (Tier 1) behavioral contract tests for errors/index.js —
|
|
13
|
+
// the "external contributor can't quietly change core behavior" lock. These
|
|
14
|
+
// fail the moment formatting/wrapping semantics drift from what's documented
|
|
15
|
+
// here, independent of the registry's actual content (see registry.test.js
|
|
16
|
+
// for that).
|
|
17
|
+
|
|
18
|
+
describe("createError()", () => {
|
|
19
|
+
it("uses registry defaults when no overrides are given", () => {
|
|
20
|
+
const err = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
21
|
+
expect(err).toBeInstanceOf(CatalystError)
|
|
22
|
+
expect(err.code).toBe(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
23
|
+
expect(err.category).toBe("PREFLIGHT")
|
|
24
|
+
expect(err.message).toBe("config not found in config folder")
|
|
25
|
+
expect(err.details).toBe("A config object must be exported from the config folder in your project root.")
|
|
26
|
+
expect(err.suggestedAction).toContain("config/config.json")
|
|
27
|
+
expect(err.docUrl).toBe("https://github.com/tata1mg/catalyst-core/blob/main/errors/PREFLIGHT/PREFLIGHT-001.md")
|
|
28
|
+
expect(err.cause).toBeUndefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("overrides message/details/suggestedAction when provided", () => {
|
|
32
|
+
const err = createError(ERROR_CODES.PREFLIGHT_CONFIG_KEY_MISSING, {
|
|
33
|
+
details: "NODE_SERVER_PORT key not found inside config.json",
|
|
34
|
+
})
|
|
35
|
+
expect(err.details).toBe("NODE_SERVER_PORT key not found inside config.json")
|
|
36
|
+
// message/suggestedAction fall back to the registry default when not overridden
|
|
37
|
+
expect(err.message).toBe("required key missing inside config.json")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("attaches cause only when explicitly provided (not even as undefined-valued own property)", () => {
|
|
41
|
+
const withoutCause = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
42
|
+
expect("cause" in withoutCause).toBe(false)
|
|
43
|
+
|
|
44
|
+
const original = new Error("boom")
|
|
45
|
+
const withCause = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING, { cause: original })
|
|
46
|
+
expect(withCause.cause).toBe(original)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("falls back to the UNKNOWN-category generic definition for an unrecognized code", () => {
|
|
50
|
+
const err = createError("NOT-A-REAL-CODE")
|
|
51
|
+
expect(err.category).toBe("UNKNOWN")
|
|
52
|
+
expect(err.code).toBe("NOT-A-REAL-CODE")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("sets name to CatalystError and is a real Error instance", () => {
|
|
56
|
+
const err = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
57
|
+
expect(err.name).toBe("CatalystError")
|
|
58
|
+
expect(err).toBeInstanceOf(Error)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("wrapForeignError()", () => {
|
|
63
|
+
it("maps BUNDLE/IOS/ANDROID stages to their per-stage wrapper code", () => {
|
|
64
|
+
expect(wrapForeignError("BUNDLE", new Error()).code).toBe(ERROR_CODES.BUNDLE_UPSTREAM_ERROR)
|
|
65
|
+
expect(wrapForeignError("IOS", new Error()).code).toBe(ERROR_CODES.IOS_UPSTREAM_ERROR)
|
|
66
|
+
expect(wrapForeignError("ANDROID", new Error()).code).toBe(ERROR_CODES.ANDROID_UPSTREAM_ERROR)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("throws on an unknown stage rather than silently producing a bad code", () => {
|
|
70
|
+
expect(() => wrapForeignError("NOT_A_STAGE", new Error())).toThrow(/unknown stage/)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("preserves the original error as cause, never reinterpreting its message", () => {
|
|
74
|
+
const original = new Error("EACCES: permission denied")
|
|
75
|
+
const wrapped = wrapForeignError("BUNDLE", original)
|
|
76
|
+
expect(wrapped.cause).toBe(original)
|
|
77
|
+
expect(wrapped.message).not.toBe(original.message)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("shows the upstream named error code when present (e.g. Rollup's PLUGIN_LOAD_ERROR)", () => {
|
|
81
|
+
// .code isn't part of the standard Error interface — it's a Node
|
|
82
|
+
// convention (see NodeJS.ErrnoException) that upstream build tools
|
|
83
|
+
// like Rollup also follow for their own error codes.
|
|
84
|
+
const original: NodeJS.ErrnoException = new Error("failed to load")
|
|
85
|
+
original.code = "PLUGIN_LOAD_ERROR"
|
|
86
|
+
const wrapped = wrapForeignError("BUNDLE", original)
|
|
87
|
+
expect(wrapped.message).toContain("PLUGIN_LOAD_ERROR")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("does NOT show a bare numeric process exit code as if it were a named upstream code", () => {
|
|
91
|
+
const original = new Error("process exited") as Error & { code: number }
|
|
92
|
+
original.code = 1 // numeric, e.g. a spawned child's exit status — not a named identifier
|
|
93
|
+
const wrapped = wrapForeignError("BUNDLE", original)
|
|
94
|
+
expect(wrapped.message).not.toContain("1")
|
|
95
|
+
expect(wrapped.message).toBe("Build failed (upstream: Vite/Rollup)")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("handles an error with no .code at all", () => {
|
|
99
|
+
const wrapped = wrapForeignError("IOS", new Error("generic failure"))
|
|
100
|
+
expect(wrapped.message).toBe("Build failed (upstream: Xcode/CocoaPods)")
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe("wrapSSRError()", () => {
|
|
105
|
+
it("maps each SSR stage to its RUNTIME-WEB code", () => {
|
|
106
|
+
expect(wrapSSRError("RENDER", new Error()).code).toBe(ERROR_CODES.RUNTIME_WEB_RENDER_FAILED)
|
|
107
|
+
expect(wrapSSRError("FETCHER", new Error()).code).toBe(ERROR_CODES.RUNTIME_WEB_FETCHER_FAILED)
|
|
108
|
+
expect(wrapSSRError("SERVER_SIDE_FUNCTION", new Error()).code).toBe(ERROR_CODES.RUNTIME_WEB_SERVER_SIDE_FUNCTION_FAILED)
|
|
109
|
+
expect(wrapSSRError("REQUEST_HANDLING", new Error()).code).toBe(ERROR_CODES.RUNTIME_WEB_REQUEST_HANDLING_FAILED)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("throws on an unknown stage rather than silently producing a bad code", () => {
|
|
113
|
+
expect(() => wrapSSRError("NOT_A_STAGE", new Error())).toThrow(/unknown stage/)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("preserves the original error verbatim as cause — never reinterprets it (could be app code, a dep, or catalyst-core itself)", () => {
|
|
117
|
+
const original = new TypeError("Cannot read properties of undefined")
|
|
118
|
+
const wrapped = wrapSSRError("FETCHER", original)
|
|
119
|
+
expect(wrapped.cause).toBe(original)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe("wrapProviderError()", () => {
|
|
124
|
+
it("constructs a cause carrying provider/status, and includes both in the message", () => {
|
|
125
|
+
const wrapped = wrapProviderError("openai", 429, '{"error":"rate limited"}')
|
|
126
|
+
expect(wrapped.code).toBe(ERROR_CODES.AI_UPSTREAM_ERROR)
|
|
127
|
+
expect(wrapped.cause.provider).toBe("openai")
|
|
128
|
+
expect(wrapped.cause.status).toBe(429)
|
|
129
|
+
expect(wrapped.cause.message).toContain("rate limited")
|
|
130
|
+
expect(wrapped.message).toContain("openai")
|
|
131
|
+
expect(wrapped.message).toContain("429")
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe("formatError()", () => {
|
|
136
|
+
const err = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
137
|
+
const errWithCause = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING, { cause: new Error("ENOENT: no such file") })
|
|
138
|
+
|
|
139
|
+
it("defaults to \"default\" mode when no mode is passed (WebBridge.js's browser call sites rely on this)", () => {
|
|
140
|
+
expect(formatError(err)).toBe(formatError(err, "default"))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("default mode: single-line-per-field, shows cause message when present", () => {
|
|
144
|
+
const output = formatError(errWithCause, "default")
|
|
145
|
+
expect(output).toContain(`[${ERROR_CODES.PREFLIGHT_CONFIG_MISSING}]`)
|
|
146
|
+
expect(output).toContain("ENOENT: no such file")
|
|
147
|
+
expect(output).toContain("Suggested action:")
|
|
148
|
+
expect(output).toContain("Docs:")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("default mode: falls back to details when there is no cause", () => {
|
|
152
|
+
const output = formatError(err, "default")
|
|
153
|
+
expect(output).toContain(err.details)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("verbose mode: includes code/category/problem/solution/docs, boxed", () => {
|
|
157
|
+
const output = formatError(errWithCause, "verbose")
|
|
158
|
+
expect(output).toContain(`Code: ${ERROR_CODES.PREFLIGHT_CONFIG_MISSING}`)
|
|
159
|
+
expect(output).toContain("Category: PREFLIGHT")
|
|
160
|
+
expect(output).toContain("Problem: ENOENT: no such file")
|
|
161
|
+
expect(output).toContain("Solution:")
|
|
162
|
+
expect(output).toContain("Docs:")
|
|
163
|
+
expect(output).toContain("┌")
|
|
164
|
+
expect(output).toContain("└")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("verbose mode: category falls back to UNKNOWN when the error has none", () => {
|
|
168
|
+
const bareErr = new CatalystError("SOME-CODE", { message: "x" })
|
|
169
|
+
const output = formatError(bareErr, "verbose")
|
|
170
|
+
expect(output).toContain("Category: UNKNOWN")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("debug mode: includes the full cause chain and a stack trace", () => {
|
|
174
|
+
const inner = new Error("root cause")
|
|
175
|
+
const middle = new Error("middle layer")
|
|
176
|
+
middle.cause = inner
|
|
177
|
+
const wrapped = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING, { cause: middle })
|
|
178
|
+
|
|
179
|
+
const output = formatError(wrapped, "debug", { node: "v20.4.0", platform: "darwin" })
|
|
180
|
+
expect(output).toContain("Caused by: middle layer")
|
|
181
|
+
expect(output).toContain(" Caused by: root cause")
|
|
182
|
+
expect(output).toContain("Environment:")
|
|
183
|
+
expect(output).toContain("node: v20.4.0")
|
|
184
|
+
expect(output).toContain("Stack trace:")
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("debug mode: omits the Environment section when no env is passed", () => {
|
|
188
|
+
const output = formatError(err, "debug")
|
|
189
|
+
expect(output).not.toContain("Environment:")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("debug mode: uses the cause's stack when a cause is present, not the wrapper's own stack", () => {
|
|
193
|
+
const original = new Error("original failure")
|
|
194
|
+
const wrapped = createError(ERROR_CODES.PREFLIGHT_CONFIG_MISSING, { cause: original })
|
|
195
|
+
const output = formatError(wrapped, "debug")
|
|
196
|
+
// The wrapper error's own stack frame (this test file) would appear either way,
|
|
197
|
+
// but the cause's message must be the one whose stack header shows up.
|
|
198
|
+
expect(output).toContain("original failure")
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { ERROR_CODES, ERROR_DEFINITIONS, getDefinition, getDocUrl } from "../../src/errors/registry.js"
|
|
3
|
+
|
|
4
|
+
// Framework-level (Tier 1) contract tests for the error registry itself —
|
|
5
|
+
// not testing any consumer of it. These exist so a malformed new error code
|
|
6
|
+
// fails CI with no human review needed (see issue #340/#411).
|
|
7
|
+
|
|
8
|
+
const CODE_FORMAT_RE = /^[A-Z][A-Z-]*-\d{3}$/
|
|
9
|
+
|
|
10
|
+
describe("ERROR_CODES / ERROR_DEFINITIONS shape invariants", () => {
|
|
11
|
+
const codeEntries = Object.entries(ERROR_CODES)
|
|
12
|
+
const codeValues = Object.values(ERROR_CODES)
|
|
13
|
+
|
|
14
|
+
it("has at least one code defined", () => {
|
|
15
|
+
expect(codeEntries.length).toBeGreaterThan(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("every ERROR_CODES value has a matching ERROR_DEFINITIONS entry", () => {
|
|
19
|
+
const missing = codeValues.filter((code) => !(code in ERROR_DEFINITIONS))
|
|
20
|
+
expect(missing).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("every ERROR_DEFINITIONS key corresponds to a declared ERROR_CODES value", () => {
|
|
24
|
+
const definedCodes = Object.keys(ERROR_DEFINITIONS)
|
|
25
|
+
const orphaned = definedCodes.filter((code) => !codeValues.includes(code))
|
|
26
|
+
expect(orphaned).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("code values are unique — no two ERROR_CODES keys share a code string", () => {
|
|
30
|
+
const seen = new Set()
|
|
31
|
+
const duplicates = []
|
|
32
|
+
for (const code of codeValues) {
|
|
33
|
+
if (seen.has(code)) duplicates.push(code)
|
|
34
|
+
seen.add(code)
|
|
35
|
+
}
|
|
36
|
+
expect(duplicates).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("every code matches the CATEGORY-NNN format", () => {
|
|
40
|
+
const malformed = codeValues.filter((code) => !CODE_FORMAT_RE.test(code))
|
|
41
|
+
expect(malformed).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("every code's category prefix matches its definition's category field", () => {
|
|
45
|
+
const mismatches = []
|
|
46
|
+
for (const [key, code] of codeEntries) {
|
|
47
|
+
const def = ERROR_DEFINITIONS[code]
|
|
48
|
+
if (!def) continue // covered by the "matching entry" test above
|
|
49
|
+
const prefix = code.slice(0, code.lastIndexOf("-"))
|
|
50
|
+
if (prefix !== def.category) {
|
|
51
|
+
mismatches.push(`${key} (${code}): prefix "${prefix}" !== category "${def.category}"`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
expect(mismatches).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("every definition has all required fields, non-empty", () => {
|
|
58
|
+
const REQUIRED_FIELDS = ["category", "defaultMessage", "defaultDetails", "suggestedAction"] as const
|
|
59
|
+
const problems: string[] = []
|
|
60
|
+
for (const [code, def] of Object.entries(ERROR_DEFINITIONS)) {
|
|
61
|
+
for (const field of REQUIRED_FIELDS) {
|
|
62
|
+
const value = def[field]
|
|
63
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
64
|
+
problems.push(`${code}: missing or empty "${field}"`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
expect(problems).toEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("no definition carries a `recoverable` field (dropped per 18530a89 — never reinstate)", () => {
|
|
72
|
+
const withRecoverable = Object.entries(ERROR_DEFINITIONS)
|
|
73
|
+
.filter(([, def]) => "recoverable" in def)
|
|
74
|
+
.map(([code]) => code)
|
|
75
|
+
expect(withRecoverable).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("getDefinition()", () => {
|
|
80
|
+
it("returns the real definition for a known code", () => {
|
|
81
|
+
const def = getDefinition(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
82
|
+
expect(def.category).toBe("PREFLIGHT")
|
|
83
|
+
expect(def.defaultMessage).toBe("config not found in config folder")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("falls back to a generic definition for an unrecognized code", () => {
|
|
87
|
+
const def = getDefinition("NOT-A-REAL-CODE")
|
|
88
|
+
expect(def).toBeDefined()
|
|
89
|
+
expect(def.defaultMessage).toBeTruthy()
|
|
90
|
+
expect(def.suggestedAction).toBeTruthy()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("never mislabels the fallback as RUNTIME-NATIVE — category is always UNKNOWN", () => {
|
|
94
|
+
const def = getDefinition("NOT-A-REAL-CODE")
|
|
95
|
+
expect(def.category).toBe("UNKNOWN")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("getDocUrl()", () => {
|
|
100
|
+
it("builds a repo-blob URL under the code's own category for a known code", () => {
|
|
101
|
+
const url = getDocUrl(ERROR_CODES.AI_DISABLED)
|
|
102
|
+
expect(url).toBe("https://github.com/tata1mg/catalyst-core/blob/main/errors/AI/AI-001.md")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("returns null for an unrecognized code rather than a broken URL", () => {
|
|
106
|
+
expect(getDocUrl("NOT-A-REAL-CODE")).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest"
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "fs"
|
|
3
|
+
import { tmpdir } from "os"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { runStaticPreflight } from "../../src/scripts/preflight.js"
|
|
6
|
+
import { ERROR_CODES } from "../../src/errors/registry.js"
|
|
7
|
+
|
|
8
|
+
// runStaticPreflight(appDir) reads config/config.json + package.json from a
|
|
9
|
+
// real directory and returns an array of CatalystError for every failure it
|
|
10
|
+
// finds (empty = the app would pass). These tests build throwaway app dirs
|
|
11
|
+
// and assert the codes.
|
|
12
|
+
|
|
13
|
+
const VALID_CONFIG = {
|
|
14
|
+
NODE_SERVER_HOSTNAME: "localhost",
|
|
15
|
+
NODE_SERVER_PORT: 3000,
|
|
16
|
+
BUILD_OUTPUT_PATH: "build",
|
|
17
|
+
PUBLIC_STATIC_ASSET_PATH: "/assets/",
|
|
18
|
+
PUBLIC_STATIC_ASSET_URL: "http://localhost:3000",
|
|
19
|
+
CLIENT_ENV_VARIABLES: [],
|
|
20
|
+
}
|
|
21
|
+
const VALID_PKG = {
|
|
22
|
+
name: "fixture-app",
|
|
23
|
+
_moduleAliases: {
|
|
24
|
+
"@api": "api.js",
|
|
25
|
+
"@containers": "src/js/containers",
|
|
26
|
+
"@server": "server",
|
|
27
|
+
"@config": "config",
|
|
28
|
+
"@css": "src/static/css",
|
|
29
|
+
"@routes": "src/js/routes/",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let appDir: string
|
|
34
|
+
|
|
35
|
+
function writeApp({ config, pkg }: { config?: unknown; pkg?: unknown }) {
|
|
36
|
+
if (config !== undefined) {
|
|
37
|
+
mkdirSync(path.join(appDir, "config"), { recursive: true })
|
|
38
|
+
writeFileSync(
|
|
39
|
+
path.join(appDir, "config", "config.json"),
|
|
40
|
+
typeof config === "string" ? config : JSON.stringify(config)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
if (pkg !== undefined) {
|
|
44
|
+
writeFileSync(
|
|
45
|
+
path.join(appDir, "package.json"),
|
|
46
|
+
typeof pkg === "string" ? pkg : JSON.stringify(pkg)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const codes = (dir: string) => runStaticPreflight(dir).map((e) => e.code)
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
appDir = mkdtempSync(path.join(tmpdir(), "preflight-test-"))
|
|
55
|
+
})
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("runStaticPreflight", () => {
|
|
61
|
+
it("returns [] for a fully valid app", () => {
|
|
62
|
+
writeApp({ config: VALID_CONFIG, pkg: VALID_PKG })
|
|
63
|
+
expect(runStaticPreflight(appDir)).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("PREFLIGHT-001 when config/config.json is absent", () => {
|
|
67
|
+
writeApp({ pkg: VALID_PKG })
|
|
68
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("PREFLIGHT-001 when config/config.json is malformed JSON", () => {
|
|
72
|
+
writeApp({ config: "{ not json", pkg: VALID_PKG })
|
|
73
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("PREFLIGHT-003 when a required config key is missing", () => {
|
|
77
|
+
const { NODE_SERVER_PORT: _omit, ...partial } = VALID_CONFIG
|
|
78
|
+
writeApp({ config: partial, pkg: VALID_PKG })
|
|
79
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_CONFIG_KEY_MISSING)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("PREFLIGHT-004 when package.json is absent", () => {
|
|
83
|
+
writeApp({ config: VALID_CONFIG })
|
|
84
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_PACKAGE_JSON_MISSING)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("PREFLIGHT-005 when package.json is malformed JSON", () => {
|
|
88
|
+
writeApp({ config: VALID_CONFIG, pkg: "{ nope" })
|
|
89
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_PACKAGE_JSON_INVALID)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("PREFLIGHT-006 when _moduleAliases is absent", () => {
|
|
93
|
+
writeApp({ config: VALID_CONFIG, pkg: { name: "x" } })
|
|
94
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_MODULE_ALIAS_MISSING)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("PREFLIGHT-009 when a required alias is missing", () => {
|
|
98
|
+
const { "@containers": _drop, ...aliases } = VALID_PKG._moduleAliases
|
|
99
|
+
writeApp({ config: VALID_CONFIG, pkg: { name: "x", _moduleAliases: aliases } })
|
|
100
|
+
expect(codes(appDir)).toContain(ERROR_CODES.PREFLIGHT_MODULE_ALIAS_KEY_MISSING)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("collects MULTIPLE failures in one pass (config + package.json both broken)", () => {
|
|
104
|
+
writeApp({}) // neither file written
|
|
105
|
+
const found = codes(appDir)
|
|
106
|
+
expect(found).toContain(ERROR_CODES.PREFLIGHT_CONFIG_MISSING)
|
|
107
|
+
expect(found).toContain(ERROR_CODES.PREFLIGHT_PACKAGE_JSON_MISSING)
|
|
108
|
+
expect(found.length).toBeGreaterThanOrEqual(2)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("every returned item is a CatalystError with a docUrl", () => {
|
|
112
|
+
writeApp({})
|
|
113
|
+
for (const err of runStaticPreflight(appDir)) {
|
|
114
|
+
expect(err.name).toBe("CatalystError")
|
|
115
|
+
expect(err.docUrl).toMatch(/\/errors\/PREFLIGHT\/PREFLIGHT-\d{3}\.md$/)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
2
|
+
import { ChunkExtractor } from "../../src/server/renderer/ChunkExtractor.js"
|
|
3
|
+
|
|
4
|
+
// Asset bucketing used by handler.jsx's collectAssets / _renderMarkUp
|
|
5
|
+
// (#348 coverage). Critical = <head>, deferred = after </body>.
|
|
6
|
+
|
|
7
|
+
let savedUrl: string | undefined
|
|
8
|
+
let savedPath: string | undefined
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
savedUrl = process.env.PUBLIC_STATIC_ASSET_URL
|
|
11
|
+
savedPath = process.env.PUBLIC_STATIC_ASSET_PATH
|
|
12
|
+
process.env.PUBLIC_STATIC_ASSET_URL = "https://cdn.example.com"
|
|
13
|
+
process.env.PUBLIC_STATIC_ASSET_PATH = "/static/"
|
|
14
|
+
})
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (savedUrl === undefined) delete process.env.PUBLIC_STATIC_ASSET_URL
|
|
17
|
+
else process.env.PUBLIC_STATIC_ASSET_URL = savedUrl
|
|
18
|
+
if (savedPath === undefined) delete process.env.PUBLIC_STATIC_ASSET_PATH
|
|
19
|
+
else process.env.PUBLIC_STATIC_ASSET_PATH = savedPath
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe("ChunkExtractor", () => {
|
|
23
|
+
it("constructs with empty manifests and yields empty buckets", () => {
|
|
24
|
+
const ce = new ChunkExtractor()
|
|
25
|
+
expect(ce.getCriticalAssets()).toEqual({ js: [], css: [] })
|
|
26
|
+
expect(ce.getDeferredAssets()).toEqual({ js: [], css: [] })
|
|
27
|
+
expect(ce.getRenderedComponentKeys()).toEqual([])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("loads essential entrypoints from assetManifest into the critical bucket as public URLs", () => {
|
|
31
|
+
const ce = new ChunkExtractor({
|
|
32
|
+
assetManifest: {
|
|
33
|
+
essential: {
|
|
34
|
+
entry: { file: "assets/entry-abc.js", css: ["assets/entry-abc.css"] },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
const critical = ce.getCriticalAssets()
|
|
39
|
+
expect(critical.js).toEqual(["https://cdn.example.com/static/assets/entry-abc.js"])
|
|
40
|
+
// CSS stays a relative path (read from disk later, not linked)
|
|
41
|
+
expect(critical.css).toEqual(["assets/entry-abc.css"])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("preloadRouteCss adds a matched route component's chunk to the critical bucket", () => {
|
|
45
|
+
const ce = new ChunkExtractor({
|
|
46
|
+
manifest: { "pages/Home": { file: "assets/home-xyz.js", css: ["assets/home-xyz.css"] } },
|
|
47
|
+
})
|
|
48
|
+
ce.preloadRouteCss([
|
|
49
|
+
{ route: { component: { __cacheKey: "pages/Home" } } },
|
|
50
|
+
{ route: {} }, // no component -> skipped, no throw
|
|
51
|
+
null, // -> skipped
|
|
52
|
+
])
|
|
53
|
+
const critical = ce.getCriticalAssets()
|
|
54
|
+
expect(critical.js).toEqual(["https://cdn.example.com/static/assets/home-xyz.js"])
|
|
55
|
+
expect(critical.css).toEqual(["assets/home-xyz.css"])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("addComponent puts discovered chunks in the deferred bucket and tracks the key", () => {
|
|
59
|
+
const ce = new ChunkExtractor({
|
|
60
|
+
manifest: { "widgets/Cart": { file: "assets/cart-123.js", css: ["assets/cart-123.css"] } },
|
|
61
|
+
})
|
|
62
|
+
ce.addComponent("widgets/Cart")
|
|
63
|
+
expect(ce.getRenderedComponentKeys()).toEqual(["widgets/Cart"])
|
|
64
|
+
expect(ce.getDeferredAssets().js).toEqual(["https://cdn.example.com/static/assets/cart-123.js"])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("addComponent falls back to a prefix (hashed) manifest key when the raw key is absent", () => {
|
|
68
|
+
const ce = new ChunkExtractor({
|
|
69
|
+
manifest: {
|
|
70
|
+
"widgets/Modal.a1b2c3.js": { file: "assets/modal-a1b2c3.js", css: ["assets/modal.css"] },
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
// raw "widgets/Modal.a1b2c3.js" isn't a key; "widgets/Modal" +
|
|
74
|
+
// "." prefix-matches it.
|
|
75
|
+
ce.addComponent("widgets/Modal")
|
|
76
|
+
expect(ce.getDeferredAssets().js).toEqual([
|
|
77
|
+
"https://cdn.example.com/static/assets/modal-a1b2c3.js",
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("addComponent is a no-op (but still tracks the key) when no manifest entry matches at all", () => {
|
|
82
|
+
const ce = new ChunkExtractor({ manifest: {} })
|
|
83
|
+
ce.addComponent("widgets/Unknown")
|
|
84
|
+
expect(ce.getRenderedComponentKeys()).toEqual(["widgets/Unknown"])
|
|
85
|
+
expect(ce.getDeferredAssets()).toEqual({ js: [], css: [] })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("does not double-count a JS URL already tracked as critical", () => {
|
|
89
|
+
const ce = new ChunkExtractor({
|
|
90
|
+
assetManifest: { essential: { e: { file: "assets/shared.js" } } },
|
|
91
|
+
manifest: { "widgets/Shared": { file: "assets/shared.js" } },
|
|
92
|
+
})
|
|
93
|
+
ce.addComponent("widgets/Shared")
|
|
94
|
+
expect(ce.getDeferredAssets().js).toEqual([])
|
|
95
|
+
expect(ce.getCriticalAssets().js).toEqual(["https://cdn.example.com/static/assets/shared.js"])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
generateCssLinkStrings,
|
|
4
|
+
generateModulePreloadLinkElements,
|
|
5
|
+
generateScriptElements,
|
|
6
|
+
generateScriptStrings,
|
|
7
|
+
getCachedDeferredCssPathsForRoute,
|
|
8
|
+
getDeferredPreloadScriptUrls,
|
|
9
|
+
getDeferredRouteKey,
|
|
10
|
+
readCssFromDisk,
|
|
11
|
+
registerDeferredAssetsForRoute,
|
|
12
|
+
} from "../../src/server/renderer/extract.js"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Pure asset-string / deferred-route-registry helpers pulled in by
|
|
16
|
+
// handler.jsx and _renderMarkUp (#348 coverage). The registry lives on
|
|
17
|
+
// process.deferredAssetsByRoute (module-level Map) — cleared between
|
|
18
|
+
// tests so cases don't leak.
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
(process as any).deferredAssetsByRoute = new Map()
|
|
22
|
+
})
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
(process as any).deferredAssetsByRoute = new Map()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("getDeferredRouteKey", () => {
|
|
28
|
+
it("returns the last matched route's path", () => {
|
|
29
|
+
const matches = [{ route: { path: "/" } }, { route: { path: "/product/:id" } }]
|
|
30
|
+
expect(getDeferredRouteKey({}, matches)).toBe("/product/:id")
|
|
31
|
+
})
|
|
32
|
+
it("returns null for no matches", () => {
|
|
33
|
+
expect(getDeferredRouteKey({}, [])).toBeNull()
|
|
34
|
+
expect(getDeferredRouteKey({})).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("registerDeferredAssetsForRoute / getCachedDeferredCssPathsForRoute", () => {
|
|
39
|
+
it("records new CSS paths once and reports only the not-yet-seen ones", () => {
|
|
40
|
+
const first = registerDeferredAssetsForRoute("/r", { css: ["a.css", "b.css"] } as any)
|
|
41
|
+
expect(first.newCssPaths).toEqual(["a.css", "b.css"])
|
|
42
|
+
|
|
43
|
+
const second = registerDeferredAssetsForRoute("/r", { css: ["b.css", "c.css"] } as any)
|
|
44
|
+
expect(second.newCssPaths).toEqual(["c.css"])
|
|
45
|
+
|
|
46
|
+
expect(getCachedDeferredCssPathsForRoute("/r").sort()).toEqual(["a.css", "b.css", "c.css"])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("skips JS URLs for bot requests but still records CSS", () => {
|
|
50
|
+
registerDeferredAssetsForRoute("/r", { css: ["x.css"], js: ["x.js"] } as any, true)
|
|
51
|
+
expect(getCachedDeferredCssPathsForRoute("/r")).toEqual(["x.css"])
|
|
52
|
+
expect(getDeferredPreloadScriptUrls("/r")).toEqual([])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("records JS URLs for non-bot requests", () => {
|
|
56
|
+
registerDeferredAssetsForRoute("/r", { js: ["a.js", "b.js"] } as any, false)
|
|
57
|
+
expect(getDeferredPreloadScriptUrls("/r").sort()).toEqual(["a.js", "b.js"])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("no-ops for a falsy routeKey", () => {
|
|
61
|
+
expect(registerDeferredAssetsForRoute(null, { css: ["a.css"] } as any)).toEqual({ newCssPaths: [] })
|
|
62
|
+
expect(getCachedDeferredCssPathsForRoute(null)).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("skips falsy entries in the css list", () => {
|
|
66
|
+
const { newCssPaths } = registerDeferredAssetsForRoute("/r", {
|
|
67
|
+
css: ["a.css", "", null, "b.css"],
|
|
68
|
+
} as any)
|
|
69
|
+
expect(newCssPaths).toEqual(["a.css", "b.css"])
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("getDeferredPreloadScriptUrls", () => {
|
|
74
|
+
it("excludes URLs already loaded as critical scripts", () => {
|
|
75
|
+
registerDeferredAssetsForRoute("/r", { js: ["keep.js", "crit.js"] } as any, false)
|
|
76
|
+
expect(getDeferredPreloadScriptUrls("/r", ["crit.js"])).toEqual(["keep.js"])
|
|
77
|
+
})
|
|
78
|
+
it("returns [] when the route has no record", () => {
|
|
79
|
+
expect(getDeferredPreloadScriptUrls("/never")).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("HTML / React element generators", () => {
|
|
84
|
+
it("generateScriptElements dedupes and emits module <script> elements", () => {
|
|
85
|
+
const els = generateScriptElements(["a.js", "a.js", "b.js"])
|
|
86
|
+
expect(els).toHaveLength(2)
|
|
87
|
+
expect(els[0].props).toMatchObject({ type: "module", src: "a.js" })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("generateModulePreloadLinkElements dedupes and honors the key prefix", () => {
|
|
91
|
+
const els = generateModulePreloadLinkElements(["a.js", "a.js"], "crit")
|
|
92
|
+
expect(els).toHaveLength(1)
|
|
93
|
+
expect(els[0].key).toBe("crit-0")
|
|
94
|
+
expect(els[0].props).toMatchObject({ rel: "modulepreload", href: "a.js" })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("generateCssLinkStrings builds deduped <link rel=stylesheet> strings", () => {
|
|
98
|
+
expect(generateCssLinkStrings(["a.css", "a.css", "b.css"])).toBe(
|
|
99
|
+
'<link rel="stylesheet" href="a.css"><link rel="stylesheet" href="b.css">',
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("generateScriptStrings builds deduped module <script> strings", () => {
|
|
104
|
+
expect(generateScriptStrings(["a.js", "a.js"])).toBe(
|
|
105
|
+
'<script type="module" src="a.js"></script>',
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("readCssFromDisk", () => {
|
|
111
|
+
it("returns an empty string when given no paths", () => {
|
|
112
|
+
expect(readCssFromDisk([], "/whatever")).toBe("")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("returns an empty string (not a throw) when the CSS files do not exist on disk", () => {
|
|
116
|
+
const out = readCssFromDisk(["does-not-exist.css"], "/tmp/nonexistent-build-dir")
|
|
117
|
+
expect(out).toBe("")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("skips http(s)-prefixed and duplicate asset entries", () => {
|
|
121
|
+
// http-prefixed -> not read from disk; duplicate -> read once.
|
|
122
|
+
const out = readCssFromDisk(
|
|
123
|
+
["https://cdn.example.com/x.css", "dup.css", "dup.css", ""],
|
|
124
|
+
"/tmp/nonexistent-build-dir",
|
|
125
|
+
)
|
|
126
|
+
expect(out).toBe("")
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Minimal CustomDocument for the SSR handler fixture (#348). Mirrors the
|
|
4
|
+
// shape a scaffolded app's server/document exports: a function taking the
|
|
5
|
+
// merged render props and returning the full <html> tree. Renders just
|
|
6
|
+
// enough (the app div via props.jsx, a marker for assertions) to let
|
|
7
|
+
// renderToPipeableStream produce a stream.
|
|
8
|
+
export default function CustomDocument(props) {
|
|
9
|
+
return (
|
|
10
|
+
<html lang={props.lang || "en"}>
|
|
11
|
+
<head>
|
|
12
|
+
<title>fixture</title>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div data-testid="doc-root">{props.jsx}</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
)
|
|
19
|
+
}
|