@ecopages/core 0.2.0-alpha.22 → 0.2.0-alpha.24

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 (515) hide show
  1. package/package.json +93 -226
  2. package/src/adapters/abstract/application-adapter.test.ts +172 -0
  3. package/src/adapters/abstract/application-adapter.ts +379 -0
  4. package/src/adapters/abstract/router-adapter.ts +30 -0
  5. package/src/adapters/abstract/server-adapter.ts +79 -0
  6. package/src/adapters/bun/client-bridge.ts +62 -0
  7. package/src/adapters/bun/create-app.ts +180 -0
  8. package/src/adapters/bun/hmr-manager.test.ts +267 -0
  9. package/src/adapters/bun/hmr-manager.ts +406 -0
  10. package/src/adapters/bun/index.ts +2 -0
  11. package/src/adapters/bun/server-adapter.ts +500 -0
  12. package/src/adapters/bun/server-lifecycle.ts +124 -0
  13. package/src/adapters/create-app.test.ts +10 -0
  14. package/src/adapters/create-app.ts +91 -0
  15. package/src/adapters/index.ts +2 -0
  16. package/src/adapters/node/create-app.test.ts +53 -0
  17. package/src/adapters/node/create-app.ts +183 -0
  18. package/src/adapters/node/node-client-bridge.test.ts +198 -0
  19. package/src/adapters/node/node-client-bridge.ts +79 -0
  20. package/src/adapters/node/node-hmr-manager.test.ts +322 -0
  21. package/src/adapters/node/node-hmr-manager.ts +378 -0
  22. package/src/adapters/node/server-adapter.ts +502 -0
  23. package/src/adapters/node/static-content-server.test.ts +60 -0
  24. package/src/adapters/node/static-content-server.ts +239 -0
  25. package/src/adapters/shared/api-response.test.ts +97 -0
  26. package/src/adapters/shared/api-response.ts +104 -0
  27. package/src/adapters/shared/application-adapter.ts +199 -0
  28. package/src/adapters/shared/define-api-handler.ts +66 -0
  29. package/src/adapters/shared/explicit-static-route-matcher.test.ts +381 -0
  30. package/src/adapters/shared/explicit-static-route-matcher.ts +140 -0
  31. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +90 -0
  32. package/src/adapters/shared/file-route-middleware-pipeline.ts +127 -0
  33. package/src/adapters/shared/fs-server-response-factory.test.ts +187 -0
  34. package/src/adapters/shared/fs-server-response-factory.ts +118 -0
  35. package/src/adapters/shared/fs-server-response-matcher.test.ts +285 -0
  36. package/src/adapters/shared/fs-server-response-matcher.ts +189 -0
  37. package/src/adapters/shared/hmr-entrypoint-registrar.ts +149 -0
  38. package/src/adapters/shared/hmr-html-response.ts +52 -0
  39. package/src/adapters/shared/hmr-manager.contract.test.ts +232 -0
  40. package/src/adapters/shared/hmr-manager.dispatch.test.ts +220 -0
  41. package/src/adapters/shared/render-context.test.ts +150 -0
  42. package/src/adapters/shared/render-context.ts +123 -0
  43. package/src/adapters/shared/runtime-bootstrap.ts +79 -0
  44. package/src/adapters/shared/server-adapter.test.ts +77 -0
  45. package/src/adapters/shared/server-adapter.ts +493 -0
  46. package/src/adapters/shared/server-route-handler.test.ts +110 -0
  47. package/src/adapters/shared/server-route-handler.ts +153 -0
  48. package/src/adapters/shared/server-static-builder.test.ts +338 -0
  49. package/src/adapters/shared/server-static-builder.ts +170 -0
  50. package/src/build/build-adapter-serialization.test.ts +281 -0
  51. package/src/build/build-adapter.test.ts +1240 -0
  52. package/src/build/build-adapter.ts +1012 -0
  53. package/src/build/build-manifest.ts +54 -0
  54. package/src/build/build-types.ts +83 -0
  55. package/src/build/dev-build-coordinator.ts +220 -0
  56. package/src/build/esbuild-build-adapter.ts +660 -0
  57. package/src/build/runtime-build-executor.test.ts +81 -0
  58. package/src/build/runtime-build-executor.ts +40 -0
  59. package/src/build/runtime-specifier-alias-plugin.test.ts +67 -0
  60. package/src/build/runtime-specifier-alias-plugin.ts +62 -0
  61. package/src/build/runtime-specifier-aliases.ts +135 -0
  62. package/src/config/config-builder.test.ts +443 -0
  63. package/src/config/config-builder.ts +742 -0
  64. package/src/config/config-builder.typecheck.test.ts +96 -0
  65. package/src/config/{constants.d.ts → constants.ts} +22 -13
  66. package/src/dev/sc-server.ts +143 -0
  67. package/src/eco/eco.browser.test.ts +43 -0
  68. package/src/eco/eco.browser.ts +118 -0
  69. package/src/eco/eco.test.ts +654 -0
  70. package/src/eco/eco.ts +205 -0
  71. package/src/eco/eco.types.ts +221 -0
  72. package/src/eco/eco.utils.test.ts +219 -0
  73. package/src/eco/eco.utils.ts +5 -0
  74. package/src/eco/global-injector-map.test.ts +42 -0
  75. package/src/eco/global-injector-map.ts +112 -0
  76. package/src/eco/lazy-injector-map.test.ts +66 -0
  77. package/src/eco/lazy-injector-map.ts +120 -0
  78. package/src/eco/module-dependencies.test.ts +30 -0
  79. package/src/eco/module-dependencies.ts +75 -0
  80. package/src/errors/http-error.test.ts +134 -0
  81. package/src/errors/http-error.ts +72 -0
  82. package/src/errors/{index.d.ts → index.ts} +2 -2
  83. package/src/errors/locals-access-error.ts +7 -0
  84. package/src/global/app-logger.ts +4 -0
  85. package/src/global/utils.test.ts +12 -0
  86. package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-HMR-Server-Integration-should-have-HMR-script-injected-in-page-1.png +0 -0
  87. package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-HMR-Server-Integration-should-load-fixture-app-page-1.png +0 -0
  88. package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-WebSocket-Connection-should-connect-to-correct-HMR-endpoint-1.png +0 -0
  89. package/src/hmr/client/hmr-runtime.ts +160 -0
  90. package/src/hmr/hmr-strategy.test.ts +124 -0
  91. package/src/hmr/hmr-strategy.ts +177 -0
  92. package/src/hmr/hmr.postcss.test.e2e.ts +41 -0
  93. package/src/hmr/hmr.test.e2e.ts +66 -0
  94. package/src/hmr/strategies/default-hmr-strategy.ts +60 -0
  95. package/src/hmr/strategies/js-hmr-strategy.test.ts +335 -0
  96. package/src/hmr/strategies/js-hmr-strategy.ts +320 -0
  97. package/src/index.browser.ts +3 -0
  98. package/src/index.ts +15 -0
  99. package/src/integrations/ghtml/ghtml-renderer.test.ts +253 -0
  100. package/src/integrations/ghtml/ghtml-renderer.ts +97 -0
  101. package/src/integrations/ghtml/ghtml.constants.ts +1 -0
  102. package/src/integrations/ghtml/ghtml.plugin.ts +28 -0
  103. package/src/plugins/alias-resolver-plugin.test.ts +41 -0
  104. package/src/plugins/alias-resolver-plugin.ts +63 -0
  105. package/src/plugins/eco-component-meta-plugin.test.ts +406 -0
  106. package/src/plugins/eco-component-meta-plugin.ts +495 -0
  107. package/src/plugins/foreign-jsx-override-plugin.test.ts +65 -0
  108. package/src/plugins/foreign-jsx-override-plugin.ts +67 -0
  109. package/src/plugins/integration-plugin.test.ts +156 -0
  110. package/src/plugins/integration-plugin.ts +311 -0
  111. package/src/plugins/processor.test.ts +148 -0
  112. package/src/plugins/processor.ts +240 -0
  113. package/src/plugins/{runtime-capability.d.ts → runtime-capability.ts} +8 -3
  114. package/src/plugins/source-transform.test.ts +82 -0
  115. package/src/plugins/source-transform.ts +123 -0
  116. package/src/route-renderer/orchestration/boundary-planning.service.ts +146 -0
  117. package/src/route-renderer/orchestration/component-render-context.ts +318 -0
  118. package/src/route-renderer/orchestration/integration-renderer.test.ts +2088 -0
  119. package/src/route-renderer/orchestration/integration-renderer.ts +1285 -0
  120. package/src/route-renderer/orchestration/page-packaging.service.test.ts +76 -0
  121. package/src/route-renderer/orchestration/page-packaging.service.ts +85 -0
  122. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +25 -0
  123. package/src/route-renderer/orchestration/queued-boundary-runtime.service.test.ts +319 -0
  124. package/src/route-renderer/orchestration/queued-boundary-runtime.service.ts +289 -0
  125. package/src/route-renderer/orchestration/render-execution.service.test.ts +196 -0
  126. package/src/route-renderer/orchestration/render-execution.service.ts +182 -0
  127. package/src/route-renderer/orchestration/render-output.utils.ts +302 -0
  128. package/src/route-renderer/orchestration/render-preparation.service.test.ts +569 -0
  129. package/src/route-renderer/orchestration/render-preparation.service.ts +508 -0
  130. package/src/route-renderer/orchestration/route-shell-composer.service.ts +162 -0
  131. package/src/route-renderer/orchestration/template-serialization.test.ts +110 -0
  132. package/src/route-renderer/orchestration/template-serialization.ts +117 -0
  133. package/src/route-renderer/page-loading/component-dependency-collection.ts +196 -0
  134. package/src/route-renderer/page-loading/declared-asset-collection.ts +156 -0
  135. package/src/route-renderer/page-loading/dependency-resolver.test.ts +665 -0
  136. package/src/route-renderer/page-loading/dependency-resolver.ts +150 -0
  137. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +75 -0
  138. package/src/route-renderer/page-loading/lazy-entry-collection.ts +167 -0
  139. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +74 -0
  140. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +60 -0
  141. package/src/route-renderer/page-loading/module-declaration-scripts.ts +16 -0
  142. package/src/route-renderer/page-loading/page-dependency-bundling.ts +205 -0
  143. package/src/route-renderer/page-loading/page-module-loader.test.ts +183 -0
  144. package/src/route-renderer/page-loading/page-module-loader.ts +184 -0
  145. package/src/route-renderer/route-renderer.ts +136 -0
  146. package/src/router/client/link-intent.test.browser.ts +51 -0
  147. package/src/router/client/link-intent.ts +92 -0
  148. package/src/router/client/navigation-coordinator.test.ts +237 -0
  149. package/src/router/client/navigation-coordinator.ts +453 -0
  150. package/src/router/server/fs-router-scanner.test.ts +83 -0
  151. package/src/router/server/fs-router-scanner.ts +224 -0
  152. package/src/router/server/fs-router.test.ts +214 -0
  153. package/src/router/server/fs-router.ts +122 -0
  154. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +66 -0
  155. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +476 -0
  156. package/src/services/assets/asset-processing-service/asset-processing.service.ts +345 -0
  157. package/src/services/assets/asset-processing-service/asset.factory.test.ts +63 -0
  158. package/src/services/assets/asset-processing-service/asset.factory.ts +105 -0
  159. package/src/services/assets/asset-processing-service/assets.types.ts +125 -0
  160. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.test.ts +74 -0
  161. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.ts +96 -0
  162. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.test.ts +67 -0
  163. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.ts +78 -0
  164. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +104 -0
  165. package/src/services/assets/asset-processing-service/index.ts +5 -0
  166. package/src/services/assets/asset-processing-service/{processor.interface.d.ts → processor.interface.ts} +10 -5
  167. package/src/services/assets/asset-processing-service/processor.registry.ts +18 -0
  168. package/src/services/assets/asset-processing-service/processors/base/base-processor.test.ts +59 -0
  169. package/src/services/assets/asset-processing-service/processors/base/base-processor.ts +83 -0
  170. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +174 -0
  171. package/src/services/assets/asset-processing-service/processors/index.ts +5 -0
  172. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +192 -0
  173. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +134 -0
  174. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.test.ts +326 -0
  175. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +110 -0
  176. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.test.ts +227 -0
  177. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +87 -0
  178. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.test.ts +261 -0
  179. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +71 -0
  180. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +81 -0
  181. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +65 -0
  182. package/src/services/assets/browser-bundle.service.test.ts +66 -0
  183. package/src/services/assets/browser-bundle.service.ts +109 -0
  184. package/src/services/cache/cache.types.ts +126 -0
  185. package/src/services/cache/index.ts +18 -0
  186. package/src/services/cache/memory-cache-store.test.ts +225 -0
  187. package/src/services/cache/memory-cache-store.ts +130 -0
  188. package/src/services/cache/page-cache-service.test.ts +175 -0
  189. package/src/services/cache/page-cache-service.ts +202 -0
  190. package/src/services/cache/page-request-cache-coordinator.service.test.ts +79 -0
  191. package/src/services/cache/page-request-cache-coordinator.service.ts +131 -0
  192. package/src/services/html/html-rewriter-provider.service.test.ts +183 -0
  193. package/src/services/html/html-rewriter-provider.service.ts +104 -0
  194. package/src/services/html/html-transformer.service.test.ts +479 -0
  195. package/src/services/html/html-transformer.service.ts +275 -0
  196. package/src/services/invalidation/development-invalidation.service.test.ts +87 -0
  197. package/src/services/invalidation/development-invalidation.service.ts +262 -0
  198. package/src/services/module-loading/app-module-loader.service.ts +9 -0
  199. package/src/services/module-loading/app-server-module-transpiler.service.test.ts +130 -0
  200. package/src/services/module-loading/app-server-module-transpiler.service.ts +143 -0
  201. package/src/services/module-loading/host-module-loader-registry.ts +15 -0
  202. package/src/services/module-loading/{module-loading-types.d.ts → module-loading-types.ts} +1 -0
  203. package/src/services/module-loading/node-bootstrap-plugin.test.ts +335 -0
  204. package/src/services/module-loading/node-bootstrap-plugin.ts +297 -0
  205. package/src/services/module-loading/page-module-import.service.test.ts +504 -0
  206. package/src/services/module-loading/page-module-import.service.ts +252 -0
  207. package/src/services/module-loading/server-module-transpiler.service.test.ts +243 -0
  208. package/src/services/module-loading/server-module-transpiler.service.ts +104 -0
  209. package/src/services/module-loading/source-module-support.ts +19 -0
  210. package/src/services/runtime-state/dev-graph.service.ts +217 -0
  211. package/src/services/runtime-state/entrypoint-dependency-graph.service.ts +136 -0
  212. package/src/services/runtime-state/runtime-specifier-registry.service.ts +96 -0
  213. package/src/services/runtime-state/server-invalidation-state.service.ts +68 -0
  214. package/src/services/validation/schema-validation-service.test.ts +223 -0
  215. package/src/services/validation/schema-validation-service.ts +204 -0
  216. package/src/services/validation/{standard-schema.types.d.ts → standard-schema.types.ts} +20 -17
  217. package/src/static-site-generator/static-site-generator.test.ts +316 -0
  218. package/src/static-site-generator/static-site-generator.ts +462 -0
  219. package/src/types/internal-types.ts +242 -0
  220. package/src/types/public-types.ts +1443 -0
  221. package/src/utils/deep-merge.test.ts +114 -0
  222. package/src/utils/deep-merge.ts +47 -0
  223. package/src/utils/hash.ts +5 -0
  224. package/src/utils/html-escaping.ts +9 -0
  225. package/src/utils/invariant.test.ts +22 -0
  226. package/src/utils/invariant.ts +15 -0
  227. package/src/utils/locals-utils.ts +37 -0
  228. package/src/utils/parse-cli-args.test.ts +69 -0
  229. package/src/utils/parse-cli-args.ts +105 -0
  230. package/src/utils/path-utils.module.ts +14 -0
  231. package/src/utils/path-utils.test.ts +15 -0
  232. package/src/utils/resolve-work-dir.ts +45 -0
  233. package/src/utils/runtime.ts +44 -0
  234. package/src/utils/server-utils.module.ts +67 -0
  235. package/src/utils/server-utils.test.ts +38 -0
  236. package/src/watchers/project-watcher.integration.test.ts +337 -0
  237. package/src/watchers/project-watcher.test-helpers.ts +41 -0
  238. package/src/watchers/project-watcher.test.ts +768 -0
  239. package/src/watchers/project-watcher.ts +357 -0
  240. package/CHANGELOG.md +0 -51
  241. package/src/adapters/abstract/application-adapter.d.ts +0 -194
  242. package/src/adapters/abstract/application-adapter.js +0 -121
  243. package/src/adapters/abstract/router-adapter.d.ts +0 -26
  244. package/src/adapters/abstract/router-adapter.js +0 -5
  245. package/src/adapters/abstract/server-adapter.d.ts +0 -69
  246. package/src/adapters/abstract/server-adapter.js +0 -15
  247. package/src/adapters/bun/client-bridge.d.ts +0 -34
  248. package/src/adapters/bun/client-bridge.js +0 -48
  249. package/src/adapters/bun/create-app.d.ts +0 -52
  250. package/src/adapters/bun/create-app.js +0 -116
  251. package/src/adapters/bun/hmr-manager.d.ts +0 -143
  252. package/src/adapters/bun/hmr-manager.js +0 -333
  253. package/src/adapters/bun/index.d.ts +0 -2
  254. package/src/adapters/bun/index.js +0 -8
  255. package/src/adapters/bun/server-adapter.d.ts +0 -155
  256. package/src/adapters/bun/server-adapter.js +0 -374
  257. package/src/adapters/bun/server-lifecycle.d.ts +0 -63
  258. package/src/adapters/bun/server-lifecycle.js +0 -92
  259. package/src/adapters/create-app.d.ts +0 -20
  260. package/src/adapters/create-app.js +0 -66
  261. package/src/adapters/index.d.ts +0 -2
  262. package/src/adapters/index.js +0 -8
  263. package/src/adapters/node/create-app.d.ts +0 -18
  264. package/src/adapters/node/create-app.js +0 -149
  265. package/src/adapters/node/node-client-bridge.d.ts +0 -26
  266. package/src/adapters/node/node-client-bridge.js +0 -66
  267. package/src/adapters/node/node-hmr-manager.d.ts +0 -133
  268. package/src/adapters/node/node-hmr-manager.js +0 -311
  269. package/src/adapters/node/server-adapter.d.ts +0 -161
  270. package/src/adapters/node/server-adapter.js +0 -359
  271. package/src/adapters/node/static-content-server.d.ts +0 -60
  272. package/src/adapters/node/static-content-server.js +0 -194
  273. package/src/adapters/shared/api-response.d.ts +0 -52
  274. package/src/adapters/shared/api-response.js +0 -96
  275. package/src/adapters/shared/application-adapter.d.ts +0 -18
  276. package/src/adapters/shared/application-adapter.js +0 -90
  277. package/src/adapters/shared/define-api-handler.d.ts +0 -25
  278. package/src/adapters/shared/define-api-handler.js +0 -15
  279. package/src/adapters/shared/explicit-static-route-matcher.d.ts +0 -38
  280. package/src/adapters/shared/explicit-static-route-matcher.js +0 -103
  281. package/src/adapters/shared/file-route-middleware-pipeline.d.ts +0 -65
  282. package/src/adapters/shared/file-route-middleware-pipeline.js +0 -99
  283. package/src/adapters/shared/fs-server-response-factory.d.ts +0 -19
  284. package/src/adapters/shared/fs-server-response-factory.js +0 -97
  285. package/src/adapters/shared/fs-server-response-matcher.d.ts +0 -67
  286. package/src/adapters/shared/fs-server-response-matcher.js +0 -147
  287. package/src/adapters/shared/hmr-entrypoint-registrar.d.ts +0 -55
  288. package/src/adapters/shared/hmr-entrypoint-registrar.js +0 -87
  289. package/src/adapters/shared/hmr-html-response.d.ts +0 -22
  290. package/src/adapters/shared/hmr-html-response.js +0 -32
  291. package/src/adapters/shared/render-context.d.ts +0 -15
  292. package/src/adapters/shared/render-context.js +0 -72
  293. package/src/adapters/shared/runtime-bootstrap.d.ts +0 -38
  294. package/src/adapters/shared/runtime-bootstrap.js +0 -43
  295. package/src/adapters/shared/server-adapter.d.ts +0 -97
  296. package/src/adapters/shared/server-adapter.js +0 -390
  297. package/src/adapters/shared/server-route-handler.d.ts +0 -89
  298. package/src/adapters/shared/server-route-handler.js +0 -111
  299. package/src/adapters/shared/server-static-builder.d.ts +0 -70
  300. package/src/adapters/shared/server-static-builder.js +0 -100
  301. package/src/build/build-adapter.d.ts +0 -239
  302. package/src/build/build-adapter.js +0 -642
  303. package/src/build/build-manifest.d.ts +0 -27
  304. package/src/build/build-manifest.js +0 -30
  305. package/src/build/build-types.d.ts +0 -57
  306. package/src/build/build-types.js +0 -0
  307. package/src/build/dev-build-coordinator.d.ts +0 -72
  308. package/src/build/dev-build-coordinator.js +0 -154
  309. package/src/build/esbuild-build-adapter.d.ts +0 -78
  310. package/src/build/esbuild-build-adapter.js +0 -505
  311. package/src/build/runtime-build-executor.d.ts +0 -14
  312. package/src/build/runtime-build-executor.js +0 -22
  313. package/src/build/runtime-specifier-alias-plugin.d.ts +0 -15
  314. package/src/build/runtime-specifier-alias-plugin.js +0 -35
  315. package/src/build/runtime-specifier-aliases.d.ts +0 -5
  316. package/src/build/runtime-specifier-aliases.js +0 -95
  317. package/src/config/config-builder.d.ts +0 -252
  318. package/src/config/config-builder.js +0 -603
  319. package/src/config/constants.js +0 -25
  320. package/src/dev/sc-server.d.ts +0 -30
  321. package/src/dev/sc-server.js +0 -111
  322. package/src/eco/eco.browser.d.ts +0 -2
  323. package/src/eco/eco.browser.js +0 -83
  324. package/src/eco/eco.d.ts +0 -9
  325. package/src/eco/eco.js +0 -85
  326. package/src/eco/eco.types.d.ts +0 -178
  327. package/src/eco/eco.types.js +0 -0
  328. package/src/eco/eco.utils.d.ts +0 -1
  329. package/src/eco/eco.utils.js +0 -10
  330. package/src/eco/global-injector-map.d.ts +0 -16
  331. package/src/eco/global-injector-map.js +0 -80
  332. package/src/eco/lazy-injector-map.d.ts +0 -8
  333. package/src/eco/lazy-injector-map.js +0 -70
  334. package/src/eco/module-dependencies.d.ts +0 -18
  335. package/src/eco/module-dependencies.js +0 -49
  336. package/src/errors/http-error.d.ts +0 -31
  337. package/src/errors/http-error.js +0 -50
  338. package/src/errors/index.js +0 -4
  339. package/src/errors/locals-access-error.d.ts +0 -4
  340. package/src/errors/locals-access-error.js +0 -9
  341. package/src/global/app-logger.d.ts +0 -2
  342. package/src/global/app-logger.js +0 -6
  343. package/src/hmr/client/hmr-runtime.d.ts +0 -5
  344. package/src/hmr/client/hmr-runtime.js +0 -109
  345. package/src/hmr/hmr-strategy.d.ts +0 -162
  346. package/src/hmr/hmr-strategy.js +0 -44
  347. package/src/hmr/hmr.postcss.test.e2e.d.ts +0 -1
  348. package/src/hmr/hmr.postcss.test.e2e.js +0 -31
  349. package/src/hmr/hmr.test.e2e.d.ts +0 -1
  350. package/src/hmr/hmr.test.e2e.js +0 -43
  351. package/src/hmr/strategies/default-hmr-strategy.d.ts +0 -43
  352. package/src/hmr/strategies/default-hmr-strategy.js +0 -34
  353. package/src/hmr/strategies/js-hmr-strategy.d.ts +0 -139
  354. package/src/hmr/strategies/js-hmr-strategy.js +0 -178
  355. package/src/index.browser.d.ts +0 -3
  356. package/src/index.browser.js +0 -4
  357. package/src/index.d.ts +0 -6
  358. package/src/index.js +0 -21
  359. package/src/integrations/ghtml/ghtml-renderer.d.ts +0 -20
  360. package/src/integrations/ghtml/ghtml-renderer.js +0 -63
  361. package/src/integrations/ghtml/ghtml.constants.d.ts +0 -1
  362. package/src/integrations/ghtml/ghtml.constants.js +0 -4
  363. package/src/integrations/ghtml/ghtml.plugin.d.ts +0 -16
  364. package/src/integrations/ghtml/ghtml.plugin.js +0 -20
  365. package/src/plugins/alias-resolver-plugin.d.ts +0 -2
  366. package/src/plugins/alias-resolver-plugin.js +0 -53
  367. package/src/plugins/eco-component-meta-plugin.d.ts +0 -108
  368. package/src/plugins/eco-component-meta-plugin.js +0 -163
  369. package/src/plugins/foreign-jsx-override-plugin.d.ts +0 -31
  370. package/src/plugins/foreign-jsx-override-plugin.js +0 -35
  371. package/src/plugins/integration-plugin.d.ts +0 -219
  372. package/src/plugins/integration-plugin.js +0 -196
  373. package/src/plugins/processor.d.ts +0 -95
  374. package/src/plugins/processor.js +0 -136
  375. package/src/plugins/runtime-capability.js +0 -0
  376. package/src/plugins/source-transform.d.ts +0 -46
  377. package/src/plugins/source-transform.js +0 -71
  378. package/src/route-renderer/orchestration/boundary-planning.service.d.ts +0 -25
  379. package/src/route-renderer/orchestration/boundary-planning.service.js +0 -97
  380. package/src/route-renderer/orchestration/component-render-context.d.ts +0 -83
  381. package/src/route-renderer/orchestration/component-render-context.js +0 -147
  382. package/src/route-renderer/orchestration/integration-renderer.d.ts +0 -554
  383. package/src/route-renderer/orchestration/integration-renderer.js +0 -957
  384. package/src/route-renderer/orchestration/queued-boundary-runtime.service.d.ts +0 -89
  385. package/src/route-renderer/orchestration/queued-boundary-runtime.service.js +0 -155
  386. package/src/route-renderer/orchestration/render-execution.service.d.ts +0 -43
  387. package/src/route-renderer/orchestration/render-execution.service.js +0 -106
  388. package/src/route-renderer/orchestration/render-output.utils.d.ts +0 -46
  389. package/src/route-renderer/orchestration/render-output.utils.js +0 -65
  390. package/src/route-renderer/orchestration/render-preparation.service.d.ts +0 -120
  391. package/src/route-renderer/orchestration/render-preparation.service.js +0 -341
  392. package/src/route-renderer/orchestration/route-shell-composer.service.d.ts +0 -50
  393. package/src/route-renderer/orchestration/route-shell-composer.service.js +0 -81
  394. package/src/route-renderer/orchestration/template-serialization.d.ts +0 -38
  395. package/src/route-renderer/orchestration/template-serialization.js +0 -45
  396. package/src/route-renderer/page-loading/dependency-resolver.d.ts +0 -35
  397. package/src/route-renderer/page-loading/dependency-resolver.js +0 -444
  398. package/src/route-renderer/page-loading/page-module-loader.d.ts +0 -90
  399. package/src/route-renderer/page-loading/page-module-loader.js +0 -127
  400. package/src/route-renderer/route-renderer.d.ts +0 -67
  401. package/src/route-renderer/route-renderer.js +0 -103
  402. package/src/router/client/link-intent.js +0 -34
  403. package/src/router/client/link-intent.test.browser.d.ts +0 -1
  404. package/src/router/client/link-intent.test.browser.js +0 -43
  405. package/src/router/client/navigation-coordinator.d.ts +0 -149
  406. package/src/router/client/navigation-coordinator.js +0 -215
  407. package/src/router/server/fs-router-scanner.d.ts +0 -41
  408. package/src/router/server/fs-router-scanner.js +0 -161
  409. package/src/router/server/fs-router.d.ts +0 -26
  410. package/src/router/server/fs-router.js +0 -100
  411. package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +0 -120
  412. package/src/services/assets/asset-processing-service/asset-processing.service.js +0 -331
  413. package/src/services/assets/asset-processing-service/asset.factory.d.ts +0 -17
  414. package/src/services/assets/asset-processing-service/asset.factory.js +0 -82
  415. package/src/services/assets/asset-processing-service/assets.types.d.ts +0 -89
  416. package/src/services/assets/asset-processing-service/assets.types.js +0 -0
  417. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +0 -55
  418. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +0 -48
  419. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +0 -20
  420. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +0 -41
  421. package/src/services/assets/asset-processing-service/index.d.ts +0 -5
  422. package/src/services/assets/asset-processing-service/index.js +0 -5
  423. package/src/services/assets/asset-processing-service/processor.interface.js +0 -6
  424. package/src/services/assets/asset-processing-service/processor.registry.d.ts +0 -8
  425. package/src/services/assets/asset-processing-service/processor.registry.js +0 -15
  426. package/src/services/assets/asset-processing-service/processors/base/base-processor.d.ts +0 -24
  427. package/src/services/assets/asset-processing-service/processors/base/base-processor.js +0 -64
  428. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.d.ts +0 -17
  429. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.js +0 -72
  430. package/src/services/assets/asset-processing-service/processors/index.d.ts +0 -5
  431. package/src/services/assets/asset-processing-service/processors/index.js +0 -5
  432. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.d.ts +0 -5
  433. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js +0 -57
  434. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts +0 -9
  435. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +0 -88
  436. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.d.ts +0 -7
  437. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +0 -75
  438. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +0 -5
  439. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +0 -25
  440. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +0 -9
  441. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +0 -66
  442. package/src/services/assets/browser-bundle.service.d.ts +0 -32
  443. package/src/services/assets/browser-bundle.service.js +0 -33
  444. package/src/services/cache/cache.types.d.ts +0 -107
  445. package/src/services/cache/cache.types.js +0 -0
  446. package/src/services/cache/index.d.ts +0 -7
  447. package/src/services/cache/index.js +0 -7
  448. package/src/services/cache/memory-cache-store.d.ts +0 -42
  449. package/src/services/cache/memory-cache-store.js +0 -98
  450. package/src/services/cache/page-cache-service.d.ts +0 -70
  451. package/src/services/cache/page-cache-service.js +0 -152
  452. package/src/services/cache/page-request-cache-coordinator.service.d.ts +0 -75
  453. package/src/services/cache/page-request-cache-coordinator.service.js +0 -109
  454. package/src/services/html/html-rewriter-provider.service.d.ts +0 -37
  455. package/src/services/html/html-rewriter-provider.service.js +0 -68
  456. package/src/services/html/html-transformer.service.d.ts +0 -77
  457. package/src/services/html/html-transformer.service.js +0 -215
  458. package/src/services/invalidation/development-invalidation.service.d.ts +0 -74
  459. package/src/services/invalidation/development-invalidation.service.js +0 -190
  460. package/src/services/module-loading/app-module-loader.service.d.ts +0 -28
  461. package/src/services/module-loading/app-module-loader.service.js +0 -35
  462. package/src/services/module-loading/app-server-module-transpiler.service.d.ts +0 -24
  463. package/src/services/module-loading/app-server-module-transpiler.service.js +0 -109
  464. package/src/services/module-loading/host-module-loader-registry.d.ts +0 -4
  465. package/src/services/module-loading/host-module-loader-registry.js +0 -15
  466. package/src/services/module-loading/module-loading-types.js +0 -0
  467. package/src/services/module-loading/node-bootstrap-plugin.d.ts +0 -42
  468. package/src/services/module-loading/node-bootstrap-plugin.js +0 -204
  469. package/src/services/module-loading/page-module-import.service.d.ts +0 -76
  470. package/src/services/module-loading/page-module-import.service.js +0 -173
  471. package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -72
  472. package/src/services/module-loading/server-module-transpiler.service.js +0 -64
  473. package/src/services/runtime-state/dev-graph.service.d.ts +0 -118
  474. package/src/services/runtime-state/dev-graph.service.js +0 -162
  475. package/src/services/runtime-state/entrypoint-dependency-graph.service.d.ts +0 -41
  476. package/src/services/runtime-state/entrypoint-dependency-graph.service.js +0 -85
  477. package/src/services/runtime-state/runtime-specifier-registry.service.d.ts +0 -69
  478. package/src/services/runtime-state/runtime-specifier-registry.service.js +0 -37
  479. package/src/services/runtime-state/server-invalidation-state.service.d.ts +0 -26
  480. package/src/services/runtime-state/server-invalidation-state.service.js +0 -35
  481. package/src/services/validation/schema-validation-service.d.ts +0 -122
  482. package/src/services/validation/schema-validation-service.js +0 -101
  483. package/src/services/validation/standard-schema.types.js +0 -0
  484. package/src/static-site-generator/static-site-generator.d.ts +0 -104
  485. package/src/static-site-generator/static-site-generator.js +0 -338
  486. package/src/types/internal-types.d.ts +0 -231
  487. package/src/types/internal-types.js +0 -0
  488. package/src/types/public-types.d.ts +0 -1219
  489. package/src/types/public-types.js +0 -0
  490. package/src/utils/deep-merge.d.ts +0 -14
  491. package/src/utils/deep-merge.js +0 -32
  492. package/src/utils/hash.d.ts +0 -1
  493. package/src/utils/hash.js +0 -7
  494. package/src/utils/html-escaping.d.ts +0 -7
  495. package/src/utils/html-escaping.js +0 -6
  496. package/src/utils/html.js +0 -4
  497. package/src/utils/invariant.d.ts +0 -5
  498. package/src/utils/invariant.js +0 -11
  499. package/src/utils/locals-utils.d.ts +0 -15
  500. package/src/utils/locals-utils.js +0 -24
  501. package/src/utils/parse-cli-args.d.ts +0 -27
  502. package/src/utils/parse-cli-args.js +0 -62
  503. package/src/utils/path-utils.module.d.ts +0 -5
  504. package/src/utils/path-utils.module.js +0 -14
  505. package/src/utils/resolve-work-dir.d.ts +0 -11
  506. package/src/utils/resolve-work-dir.js +0 -31
  507. package/src/utils/runtime.d.ts +0 -11
  508. package/src/utils/runtime.js +0 -40
  509. package/src/utils/server-utils.module.d.ts +0 -19
  510. package/src/utils/server-utils.module.js +0 -56
  511. package/src/watchers/project-watcher.d.ts +0 -136
  512. package/src/watchers/project-watcher.js +0 -275
  513. package/src/watchers/project-watcher.test-helpers.d.ts +0 -4
  514. package/src/watchers/project-watcher.test-helpers.js +0 -52
  515. /package/src/utils/{html.d.ts → html.ts} +0 -0
@@ -0,0 +1,2088 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { eco } from '../../eco/eco.ts';
3
+ import { IntegrationRenderer, type RenderToResponseContext } from './integration-renderer.ts';
4
+ import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
5
+ import type { AssetProcessingService, ProcessedAsset } from '../../services/assets/asset-processing-service/index.ts';
6
+ import { getComponentRenderContext, type ComponentBoundaryRuntime } from './component-render-context.ts';
7
+ import type {
8
+ BaseIntegrationContext,
9
+ ComponentRenderInput,
10
+ ComponentRenderResult,
11
+ BoundaryRenderPayload,
12
+ RouteRendererBody,
13
+ EcoPagesElement,
14
+ IntegrationRendererRenderOptions,
15
+ EcoPageFile,
16
+ RouteRendererOptions,
17
+ EcoComponent,
18
+ HtmlTemplateProps,
19
+ } from '../../types/public-types.ts';
20
+ import type { EcoPageComponent } from '../../eco/eco.types.ts';
21
+ import { runWithComponentRenderContext } from './component-render-context.ts';
22
+
23
+ function createBoundaryMarker(nodeId: string, componentRef: string, propsRef: string): string {
24
+ return `<eco-marker data-eco-node-id="${nodeId}" data-eco-component-ref="${componentRef}" data-eco-props-ref="${propsRef}"></eco-marker>`;
25
+ }
26
+
27
+ /**
28
+ * Concrete implementation with ed file loading for testing purposes.
29
+ */
30
+ class TestIntegrationRenderer extends IntegrationRenderer<EcoPagesElement> {
31
+ name = 'test-renderer';
32
+ BoundaryRuntimeCreationCount = 0;
33
+ BoundaryRenderCount = 0;
34
+
35
+ /** Mock data container for page module */
36
+ PageModule: EcoPageFile | null = null;
37
+ /** Mock HTML template container */
38
+ HtmlTemplate: EcoComponent<HtmlTemplateProps> | null = null;
39
+ RenderedBody: RouteRendererBody = '<html><body>Test Page</body></html>';
40
+ RenderBodyFactory:
41
+ | ((context: ReturnType<typeof getComponentRenderContext>) => RouteRendererBody | Promise<RouteRendererBody>)
42
+ | null = null;
43
+ MockComponentRenderResult: ComponentRenderResult | null = null;
44
+ ImportedFiles: string[] = [];
45
+
46
+ async render(_options: IntegrationRendererRenderOptions<EcoPagesElement>): Promise<RouteRendererBody> {
47
+ if (this.RenderBodyFactory) {
48
+ return await this.RenderBodyFactory(getComponentRenderContext());
49
+ }
50
+
51
+ return this.RenderedBody;
52
+ }
53
+
54
+ override async renderComponent(_input: ComponentRenderInput): Promise<ComponentRenderResult> {
55
+ if (this.MockComponentRenderResult) {
56
+ return this.MockComponentRenderResult;
57
+ }
58
+
59
+ return super.renderComponent(_input);
60
+ }
61
+
62
+ override async renderComponentBoundary(input: ComponentRenderInput): Promise<ComponentRenderResult> {
63
+ this.BoundaryRenderCount += 1;
64
+ return await super.renderComponentBoundary(input);
65
+ }
66
+
67
+ async renderToResponse<P>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response> {
68
+ const viewFn = view as (props: P) => EcoPagesElement;
69
+ const content = viewFn(props);
70
+
71
+ let body: string;
72
+ if (ctx.partial) {
73
+ body = content as string;
74
+ } else {
75
+ const Layout = view.config?.layout as ((props: { children: string }) => string) | undefined;
76
+ const children = Layout ? Layout({ children: content as string }) : content;
77
+ body = `<!DOCTYPE html><html><body>${children}</body></html>`;
78
+ }
79
+
80
+ const headers = new Headers({ 'Content-Type': 'text/html; charset=utf-8' });
81
+ if (ctx.headers) {
82
+ for (const [key, value] of Object.entries(ctx.headers)) {
83
+ headers.set(key, value);
84
+ }
85
+ }
86
+
87
+ return new Response(body, {
88
+ status: ctx.status ?? 200,
89
+ headers,
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Override protected methods to return data.
95
+ */
96
+ protected override async importPageFile(_file: string): Promise<EcoPageFile> {
97
+ this.ImportedFiles.push(_file);
98
+ if (!this.PageModule) throw new Error('Mock page module not set');
99
+ return this.PageModule;
100
+ }
101
+
102
+ protected override async getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>> {
103
+ if (!this.HtmlTemplate) throw new Error('Mock HTML template not set');
104
+ return this.HtmlTemplate;
105
+ }
106
+
107
+ /**
108
+ * Override to avoid asset processing service dependency in tests.
109
+ */
110
+ protected override async resolveDependencies(
111
+ _components: (EcoComponent | Partial<EcoComponent>)[],
112
+ ): Promise<ProcessedAsset[]> {
113
+ return [];
114
+ }
115
+
116
+ protected override async buildRouteRenderAssets(_file: string): Promise<ProcessedAsset[]> {
117
+ return [];
118
+ }
119
+
120
+ /**
121
+ * Expose protected method for testing.
122
+ */
123
+ public async testPrepareRenderOptions(options: RouteRendererOptions) {
124
+ return this.prepareRenderOptions(options);
125
+ }
126
+
127
+ public async testProcessComponentDependencies(components: (EcoComponent | Partial<EcoComponent>)[]) {
128
+ return this.processComponentDependencies(components);
129
+ }
130
+
131
+ public testShouldResolveBoundaryInOwningRenderer(input: {
132
+ currentIntegration: string;
133
+ targetIntegration?: string;
134
+ }) {
135
+ return this.shouldResolveBoundaryInOwningRenderer(input);
136
+ }
137
+
138
+ public testHasForeignBoundaryDescendants(component: EcoComponent) {
139
+ return this.hasForeignBoundaryDescendants(component);
140
+ }
141
+
142
+ public async testResolveBoundaryInOwningRenderer(
143
+ input: ComponentRenderInput,
144
+ rendererCache = new Map<string, IntegrationRenderer<any>>(),
145
+ ) {
146
+ return this.resolveBoundaryInOwningRenderer(input, rendererCache);
147
+ }
148
+
149
+ public async testGetHtmlTemplate() {
150
+ return this.getHtmlTemplate();
151
+ }
152
+
153
+ public async testBaseGetHtmlTemplate() {
154
+ return super.getHtmlTemplate();
155
+ }
156
+
157
+ public testGetRendererBootstrapDependencies(partial = false) {
158
+ return this.getRendererBootstrapDependencies(partial);
159
+ }
160
+
161
+ public async testFinalizeResolvedHtml(options: { html: string; partial?: boolean }) {
162
+ return this.finalizeResolvedHtml({
163
+ html: options.html,
164
+ partial: options.partial,
165
+ });
166
+ }
167
+
168
+ public async testRenderPartialViewResponse<P>(input: {
169
+ view: EcoComponent<P>;
170
+ props: P;
171
+ ctx?: RenderToResponseContext;
172
+ renderInline?: () => Promise<BodyInit>;
173
+ transformHtml?: (html: string) => string;
174
+ }) {
175
+ return this.renderPartialViewResponse({
176
+ view: input.view,
177
+ props: input.props,
178
+ ctx: input.ctx ?? { partial: true },
179
+ renderInline: input.renderInline,
180
+ transformHtml: input.transformHtml,
181
+ });
182
+ }
183
+
184
+ public async testRenderViewWithDocumentShell<P>(input: {
185
+ view: EcoComponent<P>;
186
+ props: P;
187
+ ctx?: RenderToResponseContext;
188
+ layout?: EcoComponent;
189
+ }) {
190
+ return this.renderViewWithDocumentShell({
191
+ view: input.view,
192
+ props: input.props,
193
+ ctx: input.ctx ?? {},
194
+ layout: input.layout,
195
+ });
196
+ }
197
+
198
+ public async testRenderBoundary(input: ComponentRenderInput) {
199
+ return this.renderBoundary(input);
200
+ }
201
+
202
+ protected override createComponentBoundaryRuntime(options: {
203
+ boundaryInput: ComponentRenderInput;
204
+ rendererCache: Map<string, IntegrationRenderer<any>>;
205
+ }): ComponentBoundaryRuntime {
206
+ this.BoundaryRuntimeCreationCount += 1;
207
+ return super.createComponentBoundaryRuntime(options);
208
+ }
209
+ }
210
+
211
+ describe('IntegrationRenderer', () => {
212
+ const AppConfig = {
213
+ absolutePaths: {
214
+ pagesDir: '/app/pages',
215
+ htmlTemplatePath: '/app/index.ghtml.ts',
216
+ },
217
+ integrations: [],
218
+ defaultMetadata: {
219
+ title: 'Default Title',
220
+ description: 'Default Description',
221
+ },
222
+ srcDir: '/app',
223
+ } as unknown as EcoPagesAppConfig;
224
+
225
+ const AssetService = {
226
+ processDependencies: vi.fn(() => Promise.resolve([])),
227
+ } as unknown as AssetProcessingService;
228
+
229
+ it('should extract cache strategy from page component (static property)', async () => {
230
+ const renderer = new TestIntegrationRenderer({
231
+ appConfig: AppConfig,
232
+ assetProcessingService: AssetService,
233
+ runtimeOrigin: 'http://localhost:3000',
234
+ });
235
+
236
+ /** Mock page with cache strategy */
237
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
238
+ /** Simulate eco.page() attaching cache config */
239
+ PageIdx.cache = { revalidate: 60 };
240
+
241
+ renderer.PageModule = {
242
+ default: PageIdx,
243
+ };
244
+ renderer.HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
245
+
246
+ const options: RouteRendererOptions = {
247
+ file: '/app/pages/cached-page.ts',
248
+ params: {},
249
+ query: {},
250
+ };
251
+
252
+ const result = await renderer.testPrepareRenderOptions(options);
253
+
254
+ expect(result.cacheStrategy).toEqual({ revalidate: 60 });
255
+ });
256
+
257
+ it('should return undefined cache strategy if not present', async () => {
258
+ const renderer = new TestIntegrationRenderer({
259
+ appConfig: AppConfig,
260
+ assetProcessingService: AssetService,
261
+ runtimeOrigin: 'http://localhost:3000',
262
+ });
263
+
264
+ /** Mock page without cache strategy */
265
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
266
+
267
+ renderer.PageModule = {
268
+ default: PageIdx,
269
+ };
270
+ renderer.HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
271
+
272
+ const options: RouteRendererOptions = {
273
+ file: '/app/pages/simple-page.ts',
274
+ params: {},
275
+ query: {},
276
+ };
277
+
278
+ const result = await renderer.testPrepareRenderOptions(options);
279
+
280
+ expect(result.cacheStrategy).toBeUndefined();
281
+ });
282
+
283
+ it('should resolve static props and metadata correctly', async () => {
284
+ const renderer = new TestIntegrationRenderer({
285
+ appConfig: AppConfig,
286
+ assetProcessingService: AssetService,
287
+ runtimeOrigin: 'http://localhost:3000',
288
+ });
289
+
290
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
291
+ /** Attached static methods */
292
+ PageIdx.staticProps = async () => ({ props: { title: 'Dynamic Title' } });
293
+ PageIdx.metadata = async ({ props }: { props: Record<string, unknown> }) => ({
294
+ title: props.title as string,
295
+ description: 'Dynamic Description',
296
+ });
297
+
298
+ renderer.PageModule = {
299
+ default: PageIdx,
300
+ };
301
+ renderer.HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
302
+
303
+ const options: RouteRendererOptions = {
304
+ file: '/app/pages/props-page.ts',
305
+ params: {},
306
+ query: {},
307
+ };
308
+
309
+ const result = await renderer.testPrepareRenderOptions(options);
310
+
311
+ expect(result.props).toEqual({ title: 'Dynamic Title' });
312
+ expect(result.metadata?.title).toBe('Dynamic Title');
313
+ });
314
+
315
+ it('should prefer renderer module html template path when provided', async () => {
316
+ const renderer = new TestIntegrationRenderer({
317
+ appConfig: {
318
+ ...AppConfig,
319
+ runtime: {
320
+ rendererModuleContext: {
321
+ htmlTemplateModulePath: '/virtual/includes/html.kita.tsx',
322
+ },
323
+ },
324
+ } as EcoPagesAppConfig,
325
+ assetProcessingService: AssetService,
326
+ runtimeOrigin: 'http://localhost:3000',
327
+ });
328
+
329
+ renderer.PageModule = {
330
+ default: (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>,
331
+ };
332
+
333
+ await renderer.testBaseGetHtmlTemplate();
334
+
335
+ expect(renderer.ImportedFiles).toContain('/virtual/includes/html.kita.tsx');
336
+ });
337
+
338
+ it('should expose island bootstrap dependencies from renderer modules', () => {
339
+ const renderer = new TestIntegrationRenderer({
340
+ appConfig: {
341
+ ...AppConfig,
342
+ runtime: {
343
+ rendererModuleContext: {
344
+ islandClientModuleId: 'virtual:ecopages/island-client.ts',
345
+ },
346
+ },
347
+ } as EcoPagesAppConfig,
348
+ assetProcessingService: AssetService,
349
+ runtimeOrigin: 'http://localhost:3000',
350
+ });
351
+
352
+ expect(renderer.testGetRendererBootstrapDependencies()).toEqual([
353
+ {
354
+ attributes: {
355
+ crossorigin: 'anonymous',
356
+ 'data-ecopages-runtime': 'islands',
357
+ type: 'module',
358
+ },
359
+ content: 'import "virtual:ecopages/island-client.ts";',
360
+ inline: true,
361
+ kind: 'script',
362
+ packageRole: 'keep-separate',
363
+ position: 'body',
364
+ },
365
+ ]);
366
+ expect(renderer.testGetRendererBootstrapDependencies(true)).toEqual([]);
367
+ });
368
+
369
+ it('should inject the island client bootstrap into finalized full-document HTML', async () => {
370
+ const renderer = new TestIntegrationRenderer({
371
+ appConfig: {
372
+ ...AppConfig,
373
+ runtime: {
374
+ rendererModuleContext: {
375
+ islandClientModuleId: 'virtual:ecopages/island-client.ts',
376
+ },
377
+ },
378
+ } as EcoPagesAppConfig,
379
+ assetProcessingService: AssetService,
380
+ runtimeOrigin: 'http://localhost:3000',
381
+ });
382
+
383
+ const html = await renderer.testFinalizeResolvedHtml({
384
+ html: '<!DOCTYPE html><html><body><main>hello</main></body></html>',
385
+ });
386
+
387
+ expect(html).toContain('data-ecopages-runtime="islands"');
388
+ expect(html).toContain('import "virtual:ecopages/island-client.ts";');
389
+ });
390
+
391
+ it('should keep layout locals safe and page locals guarded on static pages', async () => {
392
+ const renderer = new TestIntegrationRenderer({
393
+ appConfig: AppConfig,
394
+ assetProcessingService: AssetService,
395
+ runtimeOrigin: 'http://localhost:3000',
396
+ });
397
+
398
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
399
+
400
+ renderer.PageModule = {
401
+ default: PageIdx,
402
+ };
403
+ renderer.HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
404
+
405
+ const result = await renderer.testPrepareRenderOptions({
406
+ file: '/app/pages/static-page.ts',
407
+ params: {},
408
+ query: {},
409
+ });
410
+
411
+ expect(result.locals).toBeUndefined();
412
+ expect(() => (result.pageLocals as Record<string, unknown>).session).toThrow();
413
+ });
414
+
415
+ it('should provide both locals and pageLocals on dynamic pages', async () => {
416
+ const renderer = new TestIntegrationRenderer({
417
+ appConfig: AppConfig,
418
+ assetProcessingService: AssetService,
419
+ runtimeOrigin: 'http://localhost:3000',
420
+ });
421
+
422
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
423
+ PageIdx.cache = 'dynamic';
424
+
425
+ renderer.PageModule = {
426
+ default: PageIdx,
427
+ };
428
+ renderer.HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
429
+
430
+ const incomingLocals = { session: { userId: 'u-1' } } as Record<string, unknown>;
431
+ const result = await renderer.testPrepareRenderOptions({
432
+ file: '/app/pages/dynamic-page.ts',
433
+ params: {},
434
+ query: {},
435
+ locals: incomingLocals,
436
+ });
437
+
438
+ expect(result.locals).toBe(incomingLocals);
439
+ expect(result.pageLocals).toBe(incomingLocals);
440
+ });
441
+
442
+ it('should include a boundary plan for page, layout, and html template roots', async () => {
443
+ const renderer = new TestIntegrationRenderer({
444
+ appConfig: {
445
+ ...AppConfig,
446
+ integrations: [
447
+ {
448
+ name: 'foreign-renderer',
449
+ } as unknown as EcoPagesAppConfig['integrations'][number],
450
+ ],
451
+ } as EcoPagesAppConfig,
452
+ assetProcessingService: AssetService,
453
+ runtimeOrigin: 'http://localhost:3000',
454
+ });
455
+
456
+ const ForeignComponent = (() => '<aside>Foreign</aside>') as EcoComponent<Record<string, unknown>>;
457
+ ForeignComponent.config = {
458
+ integration: 'foreign-renderer',
459
+ __eco: {
460
+ id: 'foreign-component',
461
+ file: '/app/components/foreign-component.tsx',
462
+ integration: 'foreign-renderer',
463
+ },
464
+ };
465
+
466
+ const Layout = (() => '<main>Layout</main>') as EcoComponent<Record<string, unknown>>;
467
+ Layout.config = {
468
+ __eco: {
469
+ id: 'layout-component',
470
+ file: '/app/layouts/default.tsx',
471
+ integration: 'test-renderer',
472
+ },
473
+ dependencies: {
474
+ components: [ForeignComponent],
475
+ },
476
+ };
477
+
478
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
479
+ PageIdx.config = {
480
+ layout: Layout,
481
+ __eco: {
482
+ id: 'page-component',
483
+ file: '/app/pages/index.tsx',
484
+ integration: 'test-renderer',
485
+ },
486
+ };
487
+
488
+ const HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
489
+ HtmlTemplate.config = {
490
+ __eco: {
491
+ id: 'html-template',
492
+ file: '/app/index.ghtml.ts',
493
+ integration: 'test-renderer',
494
+ },
495
+ };
496
+
497
+ renderer.PageModule = {
498
+ default: PageIdx,
499
+ };
500
+ renderer.HtmlTemplate = HtmlTemplate;
501
+
502
+ const result = await renderer.testPrepareRenderOptions({
503
+ file: '/app/pages/index.tsx',
504
+ params: {},
505
+ query: {},
506
+ });
507
+
508
+ expect(result.boundaryPlan).toEqual(
509
+ expect.objectContaining({
510
+ foreignEdgeCount: 1,
511
+ hasValidationErrors: false,
512
+ rendererNames: expect.arrayContaining(['test-renderer', 'foreign-renderer']),
513
+ root: expect.objectContaining({
514
+ source: 'route',
515
+ children: expect.arrayContaining([
516
+ expect.objectContaining({ source: 'html-template' }),
517
+ expect.objectContaining({ source: 'layout' }),
518
+ expect.objectContaining({ source: 'page' }),
519
+ ]),
520
+ }),
521
+ }),
522
+ );
523
+ });
524
+
525
+ it('should record validation errors for unknown foreign integration owners', async () => {
526
+ const renderer = new TestIntegrationRenderer({
527
+ appConfig: AppConfig,
528
+ assetProcessingService: AssetService,
529
+ runtimeOrigin: 'http://localhost:3000',
530
+ });
531
+
532
+ const UnknownForeignComponent = (() => '<aside>Foreign</aside>') as EcoComponent<Record<string, unknown>>;
533
+ UnknownForeignComponent.config = {
534
+ integration: 'missing-renderer',
535
+ __eco: {
536
+ id: 'missing-foreign-component',
537
+ file: '/app/components/missing-foreign-component.tsx',
538
+ integration: 'missing-renderer',
539
+ },
540
+ };
541
+
542
+ const PageIdx = (() => 'Page Content') as EcoPageComponent<any>;
543
+ PageIdx.config = {
544
+ __eco: {
545
+ id: 'page-component',
546
+ file: '/app/pages/index.tsx',
547
+ integration: 'test-renderer',
548
+ },
549
+ dependencies: {
550
+ components: [UnknownForeignComponent],
551
+ },
552
+ };
553
+
554
+ const HtmlTemplate = (() => 'HTML Template') as EcoComponent<HtmlTemplateProps>;
555
+ HtmlTemplate.config = {
556
+ __eco: {
557
+ id: 'html-template',
558
+ file: '/app/index.ghtml.ts',
559
+ integration: 'test-renderer',
560
+ },
561
+ };
562
+
563
+ renderer.PageModule = {
564
+ default: PageIdx,
565
+ };
566
+ renderer.HtmlTemplate = HtmlTemplate;
567
+
568
+ const result = await renderer.testPrepareRenderOptions({
569
+ file: '/app/pages/index.tsx',
570
+ params: {},
571
+ query: {},
572
+ });
573
+
574
+ expect(result.boundaryPlan?.hasValidationErrors).toBe(true);
575
+ expect(result.boundaryPlan?.validationErrors).toEqual(
576
+ expect.arrayContaining([
577
+ expect.objectContaining({
578
+ code: 'UNKNOWN_INTEGRATION_OWNER',
579
+ componentId: 'missing-foreign-component',
580
+ integrationName: 'missing-renderer',
581
+ }),
582
+ ]),
583
+ );
584
+ });
585
+
586
+ it('should expose a compatibility boundary payload contract', async () => {
587
+ const renderer = new TestIntegrationRenderer({
588
+ appConfig: AppConfig,
589
+ assetProcessingService: AssetService,
590
+ runtimeOrigin: 'http://localhost:3000',
591
+ });
592
+
593
+ renderer.MockComponentRenderResult = {
594
+ html: '<main data-root="true">Hello</main>',
595
+ canAttachAttributes: true,
596
+ rootTag: 'main',
597
+ integrationName: 'test-renderer',
598
+ rootAttributes: { 'data-eco-component-id': 'root-1' },
599
+ assets: [
600
+ {
601
+ kind: 'script',
602
+ inline: true,
603
+ content: 'console.log("boundary")',
604
+ position: 'body',
605
+ },
606
+ ],
607
+ };
608
+
609
+ const payload = await renderer.testRenderBoundary({
610
+ component: (() => '<main>Hello</main>') as EcoComponent<Record<string, unknown>>,
611
+ props: {},
612
+ });
613
+
614
+ expect(payload).toEqual<BoundaryRenderPayload>({
615
+ html: '<main data-root="true">Hello</main>',
616
+ assets: [
617
+ {
618
+ kind: 'script',
619
+ inline: true,
620
+ content: 'console.log("boundary")',
621
+ position: 'body',
622
+ },
623
+ ],
624
+ rootTag: 'main',
625
+ rootAttributes: { 'data-eco-component-id': 'root-1' },
626
+ attachmentPolicy: { kind: 'first-element' },
627
+ integrationName: 'test-renderer',
628
+ });
629
+ });
630
+
631
+ it('should resolve foreign-owned boundaries in the owning renderer', () => {
632
+ const renderer = new TestIntegrationRenderer({
633
+ appConfig: AppConfig,
634
+ assetProcessingService: AssetService,
635
+ runtimeOrigin: 'http://localhost:3000',
636
+ });
637
+
638
+ const result = renderer.testShouldResolveBoundaryInOwningRenderer({
639
+ currentIntegration: 'ghtml',
640
+ targetIntegration: 'react',
641
+ });
642
+
643
+ expect(result).toBe(true);
644
+ });
645
+
646
+ it('should keep same-integration boundaries in the current render pass', () => {
647
+ const renderer = new TestIntegrationRenderer({
648
+ appConfig: AppConfig,
649
+ assetProcessingService: AssetService,
650
+ runtimeOrigin: 'http://localhost:3000',
651
+ });
652
+
653
+ const result = renderer.testShouldResolveBoundaryInOwningRenderer({
654
+ currentIntegration: 'react',
655
+ targetIntegration: 'react',
656
+ });
657
+
658
+ expect(result).toBe(false);
659
+ });
660
+
661
+ it('should delegate foreign component boundaries through the shared ownership helper', async () => {
662
+ const foreignRenderer = {
663
+ renderComponentBoundary: vi.fn(async () => ({
664
+ html: '<aside>Owned by foreign renderer</aside>',
665
+ canAttachAttributes: true,
666
+ rootTag: 'aside',
667
+ integrationName: 'foreign-renderer',
668
+ })),
669
+ } as unknown as IntegrationRenderer;
670
+
671
+ const initializeRenderer = vi.fn(() => foreignRenderer);
672
+ const renderer = new TestIntegrationRenderer({
673
+ appConfig: {
674
+ ...AppConfig,
675
+ integrations: [
676
+ {
677
+ name: 'foreign-renderer',
678
+ initializeRenderer,
679
+ } as unknown as EcoPagesAppConfig['integrations'][number],
680
+ ],
681
+ } as EcoPagesAppConfig,
682
+ assetProcessingService: AssetService,
683
+ runtimeOrigin: 'http://localhost:3000',
684
+ });
685
+
686
+ const ForeignComponent = (() => '<aside>Foreign</aside>') as EcoComponent<Record<string, unknown>>;
687
+ ForeignComponent.config = {
688
+ integration: 'foreign-renderer',
689
+ __eco: {
690
+ id: 'foreign-component',
691
+ file: '/app/components/foreign-component.tsx',
692
+ integration: 'foreign-renderer',
693
+ },
694
+ };
695
+
696
+ const rendererCache = new Map<string, IntegrationRenderer<any>>();
697
+ const result = await renderer.testResolveBoundaryInOwningRenderer(
698
+ {
699
+ component: ForeignComponent,
700
+ props: { label: 'foreign' },
701
+ integrationContext: { rendererCache },
702
+ },
703
+ rendererCache,
704
+ );
705
+
706
+ expect(result).toEqual(
707
+ expect.objectContaining({
708
+ html: '<aside>Owned by foreign renderer</aside>',
709
+ integrationName: 'foreign-renderer',
710
+ }),
711
+ );
712
+ expect(initializeRenderer).toHaveBeenCalledTimes(1);
713
+ expect(foreignRenderer.renderComponentBoundary).toHaveBeenCalledTimes(1);
714
+ });
715
+
716
+ it('should preserve shared integration context fields when delegating to the owning renderer', async () => {
717
+ const foreignRenderer = {
718
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) => ({
719
+ html: `<aside>${String(
720
+ (input.integrationContext as BaseIntegrationContext | undefined)?.componentInstanceId ?? 'missing',
721
+ )}</aside>`,
722
+ canAttachAttributes: true,
723
+ rootTag: 'aside',
724
+ integrationName: 'foreign-renderer',
725
+ })),
726
+ } as unknown as IntegrationRenderer;
727
+ const initializeRenderer = vi.fn(() => foreignRenderer);
728
+
729
+ const renderer = new TestIntegrationRenderer({
730
+ appConfig: {
731
+ ...AppConfig,
732
+ integrations: [
733
+ {
734
+ name: 'foreign-renderer',
735
+ initializeRenderer,
736
+ } as unknown as EcoPagesAppConfig['integrations'][number],
737
+ ],
738
+ } as EcoPagesAppConfig,
739
+ assetProcessingService: AssetService,
740
+ runtimeOrigin: 'http://localhost:3000',
741
+ });
742
+
743
+ const ForeignComponent = (() => '<aside>Foreign</aside>') as EcoComponent<Record<string, unknown>>;
744
+ ForeignComponent.config = {
745
+ integration: 'foreign-renderer',
746
+ __eco: {
747
+ id: 'foreign-component',
748
+ file: '/app/components/foreign-component.tsx',
749
+ integration: 'foreign-renderer',
750
+ },
751
+ };
752
+
753
+ const rendererCache = new Map<string, IntegrationRenderer<any>>();
754
+ await renderer.testResolveBoundaryInOwningRenderer(
755
+ {
756
+ component: ForeignComponent,
757
+ props: { label: 'foreign' },
758
+ integrationContext: {
759
+ componentInstanceId: 'host-1',
760
+ } satisfies BaseIntegrationContext,
761
+ },
762
+ rendererCache,
763
+ );
764
+
765
+ expect(foreignRenderer.renderComponentBoundary).toHaveBeenCalledWith(
766
+ expect.objectContaining({
767
+ integrationContext: expect.objectContaining({
768
+ componentInstanceId: 'host-1',
769
+ rendererCache,
770
+ }),
771
+ }),
772
+ );
773
+ });
774
+
775
+ it('should stop boundary delegation when the resolved owner renderer is the current renderer', async () => {
776
+ const renderer = new TestIntegrationRenderer({
777
+ appConfig: {
778
+ ...AppConfig,
779
+ integrations: [
780
+ {
781
+ name: 'foreign-renderer',
782
+ initializeRenderer: () => renderer,
783
+ } as unknown as EcoPagesAppConfig['integrations'][number],
784
+ ],
785
+ } as EcoPagesAppConfig,
786
+ assetProcessingService: AssetService,
787
+ runtimeOrigin: 'http://localhost:3000',
788
+ });
789
+
790
+ const ForeignComponent = (() => '<aside>Foreign</aside>') as EcoComponent<Record<string, unknown>>;
791
+ ForeignComponent.config = {
792
+ integration: 'foreign-renderer',
793
+ __eco: {
794
+ id: 'foreign-component',
795
+ file: '/app/components/foreign-component.tsx',
796
+ integration: 'foreign-renderer',
797
+ },
798
+ };
799
+
800
+ const rendererCache = new Map<string, IntegrationRenderer<any>>();
801
+ const result = await renderer.testResolveBoundaryInOwningRenderer(
802
+ {
803
+ component: ForeignComponent,
804
+ props: { label: 'foreign' },
805
+ integrationContext: { rendererCache },
806
+ },
807
+ rendererCache,
808
+ );
809
+
810
+ expect(result).toBeUndefined();
811
+ });
812
+
813
+ it('should prefer processed lazy script srcUrl for _resolvedLazyTriggers', async () => {
814
+ let capturedDeps: unknown[] = [];
815
+ const LazySrcUrl = '/assets/_hmr/components/lit-counter/lit-counter.script.js';
816
+ const Service = {
817
+ processDependencies: vi.fn(async (deps: unknown[]) => {
818
+ capturedDeps = deps;
819
+ const lazyFileDep = (
820
+ deps as Array<{
821
+ kind?: string;
822
+ source?: string;
823
+ filepath?: string;
824
+ attributes?: Record<string, string>;
825
+ }>
826
+ ).find((dep) => dep.kind === 'script' && dep.source === 'file' && Boolean(dep.filepath));
827
+
828
+ return [
829
+ {
830
+ kind: 'script',
831
+ filepath: lazyFileDep?.filepath,
832
+ attributes: lazyFileDep?.attributes,
833
+ srcUrl: LazySrcUrl,
834
+ },
835
+ ] as ProcessedAsset[];
836
+ }),
837
+ } as unknown as AssetProcessingService;
838
+
839
+ const renderer = new TestIntegrationRenderer({
840
+ appConfig: AppConfig,
841
+ assetProcessingService: Service,
842
+ runtimeOrigin: 'http://localhost:3000',
843
+ });
844
+
845
+ const component = ((_) => '<lit-counter></lit-counter>') as EcoComponent<Record<string, unknown>>;
846
+ component.config = {
847
+ __eco: {
848
+ id: 'lit-counter',
849
+ integration: 'lit',
850
+ file: '/app/components/lit-counter/lit-counter.kita.tsx',
851
+ },
852
+ dependencies: {
853
+ scripts: [
854
+ {
855
+ src: './lit-counter.script.ts',
856
+ lazy: { 'on:interaction': 'click,mouseenter,focusin' },
857
+ },
858
+ ],
859
+ },
860
+ };
861
+
862
+ await renderer.testProcessComponentDependencies([component]);
863
+
864
+ expect(component.config._resolvedLazyScripts).toBeUndefined();
865
+ expect(component.config._resolvedLazyTriggers).toHaveLength(1);
866
+ expect(component.config._resolvedLazyTriggers?.[0]?.rules).toEqual([
867
+ {
868
+ 'on:interaction': {
869
+ value: 'click,mouseenter,focusin',
870
+ scripts: [LazySrcUrl],
871
+ },
872
+ },
873
+ ]);
874
+ expect(
875
+ capturedDeps.some((dep) => {
876
+ if (!dep || typeof dep !== 'object') return false;
877
+ const candidate = dep as { source?: string; importPath?: string };
878
+ return candidate.source === 'node-module' && candidate.importPath === '@ecopages/scripts-injector';
879
+ }),
880
+ ).toBe(false);
881
+ });
882
+
883
+ it('should fallback to static lazy script URL when processed srcUrl is unavailable', async () => {
884
+ const Service = {
885
+ processDependencies: vi.fn(async () => {
886
+ return [
887
+ {
888
+ kind: 'script',
889
+ filepath: '/app/components/lit-counter/lit-counter.script.ts',
890
+ },
891
+ ] as ProcessedAsset[];
892
+ }),
893
+ } as unknown as AssetProcessingService;
894
+
895
+ const renderer = new TestIntegrationRenderer({
896
+ appConfig: AppConfig,
897
+ assetProcessingService: Service,
898
+ runtimeOrigin: 'http://localhost:3000',
899
+ });
900
+
901
+ const component = ((_) => '<lit-counter></lit-counter>') as EcoComponent<Record<string, unknown>>;
902
+ component.config = {
903
+ __eco: {
904
+ id: 'lit-counter',
905
+ integration: 'lit',
906
+ file: '/app/components/lit-counter/lit-counter.kita.tsx',
907
+ },
908
+ dependencies: {
909
+ scripts: [
910
+ {
911
+ src: './lit-counter.script.ts',
912
+ lazy: { 'on:interaction': 'click,mouseenter,focusin' },
913
+ },
914
+ ],
915
+ },
916
+ };
917
+
918
+ await renderer.testProcessComponentDependencies([component]);
919
+
920
+ expect(component.config._resolvedLazyScripts).toBeUndefined();
921
+ expect(component.config._resolvedLazyTriggers).toHaveLength(1);
922
+ expect(component.config._resolvedLazyTriggers?.[0]?.rules).toEqual([
923
+ {
924
+ 'on:interaction': {
925
+ value: 'click,mouseenter,focusin',
926
+ scripts: ['/assets/components/lit-counter/lit-counter.script.js'],
927
+ },
928
+ },
929
+ ]);
930
+ });
931
+
932
+ describe('renderToResponse', () => {
933
+ it('should render a view with default status 200', async () => {
934
+ const renderer = new TestIntegrationRenderer({
935
+ appConfig: AppConfig,
936
+ assetProcessingService: AssetService,
937
+ runtimeOrigin: 'http://localhost:3000',
938
+ });
939
+
940
+ const View = ((props: { title: string }) => `<h1>${props.title}</h1>`) as EcoComponent<{
941
+ title: string;
942
+ }>;
943
+
944
+ const response = await renderer.renderToResponse(View, { title: 'Hello' }, {});
945
+
946
+ expect(response.status).toBe(200);
947
+ expect(response.headers.get('Content-Type')).toBe('text/html; charset=utf-8');
948
+ const body = await response.text();
949
+ expect(body).toContain('<h1>Hello</h1>');
950
+ });
951
+
952
+ it('should render a partial view without layout', async () => {
953
+ const renderer = new TestIntegrationRenderer({
954
+ appConfig: AppConfig,
955
+ assetProcessingService: AssetService,
956
+ runtimeOrigin: 'http://localhost:3000',
957
+ });
958
+
959
+ const View = ((props: { content: string }) => `<div>${props.content}</div>`) as EcoComponent<{
960
+ content: string;
961
+ }>;
962
+
963
+ const response = await renderer.renderToResponse(View, { content: 'Partial' }, { partial: true });
964
+
965
+ const body = await response.text();
966
+ expect(body).toBe('<div>Partial</div>');
967
+ expect(body).not.toContain('<!DOCTYPE html>');
968
+ });
969
+
970
+ it('should apply custom status code', async () => {
971
+ const renderer = new TestIntegrationRenderer({
972
+ appConfig: AppConfig,
973
+ assetProcessingService: AssetService,
974
+ runtimeOrigin: 'http://localhost:3000',
975
+ });
976
+
977
+ const View = (() => '<p>Not Found</p>') as EcoComponent<object>;
978
+
979
+ const response = await renderer.renderToResponse(View, {}, { status: 404 });
980
+
981
+ expect(response.status).toBe(404);
982
+ });
983
+
984
+ it('should apply custom headers', async () => {
985
+ const renderer = new TestIntegrationRenderer({
986
+ appConfig: AppConfig,
987
+ assetProcessingService: AssetService,
988
+ runtimeOrigin: 'http://localhost:3000',
989
+ });
990
+
991
+ const View = (() => '<p>Cached</p>') as EcoComponent<object>;
992
+
993
+ const response = await renderer.renderToResponse(
994
+ View,
995
+ {},
996
+ {
997
+ headers: {
998
+ 'Cache-Control': 'max-age=3600',
999
+ 'X-Custom-Header': 'test-value',
1000
+ },
1001
+ },
1002
+ );
1003
+
1004
+ expect(response.headers.get('Cache-Control')).toBe('max-age=3600');
1005
+ expect(response.headers.get('X-Custom-Header')).toBe('test-value');
1006
+ });
1007
+
1008
+ it('should render with layout when not partial', async () => {
1009
+ const renderer = new TestIntegrationRenderer({
1010
+ appConfig: AppConfig,
1011
+ assetProcessingService: AssetService,
1012
+ runtimeOrigin: 'http://localhost:3000',
1013
+ });
1014
+
1015
+ const Layout = ((props: { children: string }) =>
1016
+ `<main class="layout">${props.children}</main>`) as EcoComponent<{ children: string }>;
1017
+
1018
+ const View = ((props: { message: string }) => `<p>${props.message}</p>`) as EcoComponent<{
1019
+ message: string;
1020
+ }>;
1021
+ View.config = { layout: Layout };
1022
+
1023
+ const response = await renderer.renderToResponse(View, { message: 'With Layout' }, {});
1024
+
1025
+ const body = await response.text();
1026
+ expect(body).toContain('<main class="layout">');
1027
+ expect(body).toContain('<p>With Layout</p>');
1028
+ });
1029
+ });
1030
+
1031
+ describe('renderComponent', () => {
1032
+ it('should render a component with structured output', async () => {
1033
+ const renderer = new TestIntegrationRenderer({
1034
+ appConfig: AppConfig,
1035
+ assetProcessingService: AssetService,
1036
+ runtimeOrigin: 'http://localhost:3000',
1037
+ });
1038
+
1039
+ const View = ((props: { title: string }) => `<h1>${props.title}</h1>`) as EcoComponent<{ title: string }>;
1040
+
1041
+ const result = await renderer.renderComponent({
1042
+ component: View,
1043
+ props: { title: 'Hello Component' },
1044
+ });
1045
+
1046
+ expect(result.integrationName).toBe('test-renderer');
1047
+ expect(result.canAttachAttributes).toBe(true);
1048
+ expect(result.rootTag).toBe('h1');
1049
+ expect(result.html).toContain('<h1>Hello Component</h1>');
1050
+ });
1051
+ });
1052
+
1053
+ describe('execute component-level artifacts', () => {
1054
+ it('should handle stream-like render bodies without re-consuming disturbed responses', async () => {
1055
+ const renderer = new TestIntegrationRenderer({
1056
+ appConfig: AppConfig,
1057
+ assetProcessingService: AssetService,
1058
+ runtimeOrigin: 'http://localhost:3000',
1059
+ });
1060
+
1061
+ renderer.RenderedBody = new Response('<html><body><main>Stream Body</main></body></html>').body as BodyInit;
1062
+ renderer.PageModule = {
1063
+ default: (() => '<main>Stream Body</main>') as unknown as EcoPageComponent<any>,
1064
+ };
1065
+ renderer.HtmlTemplate = (() =>
1066
+ '<html><body><main>Stream Body</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1067
+
1068
+ const result = await renderer.execute({
1069
+ file: '/app/pages/index.ts',
1070
+ params: {},
1071
+ query: {},
1072
+ });
1073
+
1074
+ const body = await new Response(result.body as BodyInit).text();
1075
+ expect(body).toContain('<main>Stream Body</main>');
1076
+ });
1077
+
1078
+ it('should include renderComponent assets and apply root attributes', async () => {
1079
+ const appConfig = {
1080
+ ...AppConfig,
1081
+ } as EcoPagesAppConfig;
1082
+
1083
+ const renderer = new TestIntegrationRenderer({
1084
+ appConfig,
1085
+ assetProcessingService: AssetService,
1086
+ runtimeOrigin: 'http://localhost:3000',
1087
+ });
1088
+
1089
+ renderer.RenderedBody = '<html><body><main>Test Page</main></body></html>';
1090
+ renderer.MockComponentRenderResult = {
1091
+ html: '<main>Test Page</main>',
1092
+ canAttachAttributes: true,
1093
+ rootTag: 'main',
1094
+ integrationName: 'test-renderer',
1095
+ rootAttributes: { 'data-eco-component-id': 'eco-page-root' },
1096
+ assets: [
1097
+ {
1098
+ kind: 'script',
1099
+ srcUrl: '/assets/island.js',
1100
+ position: 'head',
1101
+ } as ProcessedAsset,
1102
+ ],
1103
+ };
1104
+
1105
+ renderer.PageModule = {
1106
+ default: (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>,
1107
+ };
1108
+ renderer.HtmlTemplate = (() =>
1109
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1110
+
1111
+ const result = await renderer.execute({
1112
+ file: '/app/pages/index.ts',
1113
+ params: {},
1114
+ query: {},
1115
+ });
1116
+
1117
+ const body = await new Response(result.body as BodyInit).text();
1118
+ expect(body).toContain('<main data-eco-component-id="eco-page-root">Test Page</main>');
1119
+
1120
+ const processedDeps = (renderer as any).htmlTransformer.getProcessedDependencies();
1121
+ expect(processedDeps.some((dep: ProcessedAsset) => dep.srcUrl === '/assets/island.js')).toBe(true);
1122
+ });
1123
+
1124
+ it('should not force render nested dependency components without resolved props context', async () => {
1125
+ const explicitRenderer = {
1126
+ renderComponent: vi.fn(async () => ({
1127
+ html: '<aside>Nested</aside>',
1128
+ canAttachAttributes: true,
1129
+ rootTag: 'aside',
1130
+ integrationName: 'explicit-renderer',
1131
+ })),
1132
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1133
+ explicitRenderer.renderComponent(input),
1134
+ ),
1135
+ } as unknown as IntegrationRenderer;
1136
+
1137
+ const appConfig = {
1138
+ ...AppConfig,
1139
+ integrations: [
1140
+ {
1141
+ name: 'explicit-renderer',
1142
+ initializeRenderer: () => explicitRenderer,
1143
+ },
1144
+ ],
1145
+ } as unknown as EcoPagesAppConfig;
1146
+
1147
+ const renderer = new TestIntegrationRenderer({
1148
+ appConfig,
1149
+ assetProcessingService: AssetService,
1150
+ runtimeOrigin: 'http://localhost:3000',
1151
+ });
1152
+
1153
+ renderer.RenderedBody = '<html><body><main>Test Page</main></body></html>';
1154
+ renderer.MockComponentRenderResult = {
1155
+ html: '<main>Test Page</main>',
1156
+ canAttachAttributes: true,
1157
+ rootTag: 'main',
1158
+ integrationName: 'test-renderer',
1159
+ rootAttributes: { 'data-eco-component-id': 'eco-page-root' },
1160
+ };
1161
+
1162
+ const NestedComponent = (() => '<aside>Nested</aside>') as EcoComponent<Record<string, unknown>>;
1163
+ NestedComponent.config = {
1164
+ integration: 'explicit-renderer',
1165
+ __eco: {
1166
+ id: 'nested-component',
1167
+ file: '/app/components/nested-component.ts',
1168
+ integration: 'test-renderer',
1169
+ },
1170
+ };
1171
+
1172
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1173
+ Page.config = {
1174
+ dependencies: {
1175
+ components: [NestedComponent],
1176
+ },
1177
+ };
1178
+
1179
+ renderer.PageModule = {
1180
+ default: Page,
1181
+ };
1182
+ renderer.HtmlTemplate = (() =>
1183
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1184
+
1185
+ const result = await renderer.execute({
1186
+ file: '/app/pages/index.ts',
1187
+ params: {},
1188
+ query: {},
1189
+ });
1190
+
1191
+ expect((explicitRenderer.renderComponent as any).mock.calls).toHaveLength(0);
1192
+
1193
+ const body = await new Response(result.body as BodyInit).text();
1194
+ expect(body).toContain('<main data-eco-component-id="eco-page-root">Test Page</main>');
1195
+
1196
+ const processedDeps = (renderer as any).htmlTransformer.getProcessedDependencies();
1197
+ expect(processedDeps.some((dep: ProcessedAsset) => dep.srcUrl === '/assets/nested-explicit.js')).toBe(
1198
+ false,
1199
+ );
1200
+ });
1201
+
1202
+ it('should include global integration dependencies for referenced component integrations once', async () => {
1203
+ const explicitIntegrationDependency = {
1204
+ kind: 'script',
1205
+ srcUrl: '/assets/react-runtime.js',
1206
+ position: 'head',
1207
+ } as ProcessedAsset;
1208
+
1209
+ const appConfig = {
1210
+ ...AppConfig,
1211
+ integrations: [
1212
+ {
1213
+ name: 'react',
1214
+ initializeRenderer: () => renderer as unknown as IntegrationRenderer,
1215
+ getResolvedIntegrationDependencies: () => [explicitIntegrationDependency],
1216
+ },
1217
+ ],
1218
+ } as unknown as EcoPagesAppConfig;
1219
+
1220
+ const renderer = new TestIntegrationRenderer({
1221
+ appConfig,
1222
+ assetProcessingService: AssetService,
1223
+ runtimeOrigin: 'http://localhost:3000',
1224
+ });
1225
+
1226
+ renderer.RenderedBody = '<html><body><main>Test Page</main></body></html>';
1227
+ renderer.MockComponentRenderResult = {
1228
+ html: '<main>Test Page</main>',
1229
+ canAttachAttributes: true,
1230
+ rootTag: 'main',
1231
+ integrationName: 'test-renderer',
1232
+ };
1233
+
1234
+ const ReactNested = (() => '<div>React Nested</div>') as EcoComponent<Record<string, unknown>>;
1235
+ ReactNested.config = {
1236
+ integration: 'react',
1237
+ __eco: {
1238
+ id: 'react-nested',
1239
+ file: '/app/components/react-nested.react.tsx',
1240
+ integration: 'react',
1241
+ },
1242
+ };
1243
+
1244
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1245
+ Page.config = {
1246
+ dependencies: {
1247
+ components: [ReactNested],
1248
+ },
1249
+ };
1250
+
1251
+ renderer.PageModule = {
1252
+ default: Page,
1253
+ };
1254
+ renderer.HtmlTemplate = (() =>
1255
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1256
+
1257
+ await renderer.execute({
1258
+ file: '/app/pages/index.ts',
1259
+ params: {},
1260
+ query: {},
1261
+ });
1262
+
1263
+ const processedDeps = (renderer as any).htmlTransformer.getProcessedDependencies();
1264
+ expect(
1265
+ processedDeps.filter((dep: ProcessedAsset) => dep.srcUrl === '/assets/react-runtime.js'),
1266
+ ).toHaveLength(1);
1267
+ });
1268
+
1269
+ it('should fail route execution when unresolved boundary artifact HTML is returned', async () => {
1270
+ const explicitRenderer = {
1271
+ renderComponent: vi.fn(async () => ({
1272
+ html: '<aside>Nested Render</aside>',
1273
+ canAttachAttributes: true,
1274
+ rootTag: 'aside',
1275
+ integrationName: 'explicit-renderer',
1276
+ rootAttributes: { 'data-eco-component-id': 'nested-1' },
1277
+ assets: [
1278
+ {
1279
+ kind: 'script',
1280
+ srcUrl: '/assets/nested-render.js',
1281
+ position: 'head',
1282
+ } as ProcessedAsset,
1283
+ ],
1284
+ })),
1285
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1286
+ explicitRenderer.renderComponent(input),
1287
+ ),
1288
+ } as unknown as IntegrationRenderer;
1289
+
1290
+ const appConfig = {
1291
+ ...AppConfig,
1292
+ integrations: [
1293
+ {
1294
+ name: 'explicit-renderer',
1295
+ initializeRenderer: () => explicitRenderer,
1296
+ },
1297
+ ],
1298
+ } as unknown as EcoPagesAppConfig;
1299
+
1300
+ const renderer = new TestIntegrationRenderer({
1301
+ appConfig,
1302
+ assetProcessingService: AssetService,
1303
+ runtimeOrigin: 'http://localhost:3000',
1304
+ });
1305
+
1306
+ renderer.RenderedBody =
1307
+ '<html><body><main>Before<eco-marker data-eco-node-id="n_1" data-eco-component-ref="nested-component" data-eco-props-ref="props-1"></eco-marker>After</main></body></html>';
1308
+ renderer.MockComponentRenderResult = {
1309
+ html: '<main>Test Page</main>',
1310
+ canAttachAttributes: true,
1311
+ rootTag: 'main',
1312
+ integrationName: 'test-renderer',
1313
+ };
1314
+
1315
+ const NestedComponent = (() => '<aside>Nested</aside>') as EcoComponent<Record<string, unknown>>;
1316
+ NestedComponent.config = {
1317
+ integration: 'explicit-renderer',
1318
+ __eco: {
1319
+ id: 'nested-component',
1320
+ file: '/app/components/nested-component.ts',
1321
+ integration: 'explicit-renderer',
1322
+ },
1323
+ };
1324
+
1325
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1326
+ Page.config = {
1327
+ dependencies: {
1328
+ components: [NestedComponent],
1329
+ },
1330
+ };
1331
+
1332
+ renderer.PageModule = {
1333
+ default: Page,
1334
+ } as unknown as EcoPageFile;
1335
+ renderer.RenderBodyFactory = () => renderer.RenderedBody;
1336
+ renderer.HtmlTemplate = (() =>
1337
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1338
+
1339
+ await expect(
1340
+ renderer.execute({
1341
+ file: '/app/pages/index.ts',
1342
+ params: {},
1343
+ query: {},
1344
+ }),
1345
+ ).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
1346
+ expect((explicitRenderer.renderComponent as any).mock.calls).toHaveLength(0);
1347
+ });
1348
+
1349
+ it('should fail route execution when unresolved boundary artifact HTML remains inside surrounding shell html', async () => {
1350
+ const explicitRenderer = {
1351
+ renderComponent: vi.fn(async () => ({
1352
+ html: '<aside data-explicit-shell="nested"><span>Nested Render</span></aside>',
1353
+ canAttachAttributes: true,
1354
+ rootTag: 'aside',
1355
+ integrationName: 'explicit-renderer',
1356
+ rootAttributes: { 'data-eco-component-id': 'nested-contract-root' },
1357
+ assets: [
1358
+ {
1359
+ kind: 'script',
1360
+ srcUrl: '/assets/explicit-contract.js',
1361
+ position: 'head',
1362
+ } as ProcessedAsset,
1363
+ ],
1364
+ })),
1365
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1366
+ explicitRenderer.renderComponent(input),
1367
+ ),
1368
+ } as unknown as IntegrationRenderer;
1369
+
1370
+ const appConfig = {
1371
+ ...AppConfig,
1372
+ integrations: [
1373
+ {
1374
+ name: 'explicit-renderer',
1375
+ initializeRenderer: () => explicitRenderer,
1376
+ },
1377
+ ],
1378
+ } as unknown as EcoPagesAppConfig;
1379
+
1380
+ const renderer = new TestIntegrationRenderer({
1381
+ appConfig,
1382
+ assetProcessingService: AssetService,
1383
+ runtimeOrigin: 'http://localhost:3000',
1384
+ });
1385
+
1386
+ renderer.RenderedBody =
1387
+ '<html><body><main><section data-shell="outer">Before<eco-marker data-eco-node-id="n_1" data-eco-component-ref="nested-component" data-eco-props-ref="props-1"></eco-marker>After</section></main></body></html>';
1388
+ renderer.MockComponentRenderResult = {
1389
+ html: '<main>Test Page</main>',
1390
+ canAttachAttributes: true,
1391
+ rootTag: 'main',
1392
+ integrationName: 'test-renderer',
1393
+ };
1394
+
1395
+ const NestedComponent = (() => '<aside>Nested</aside>') as EcoComponent<Record<string, unknown>>;
1396
+ NestedComponent.config = {
1397
+ integration: 'explicit-renderer',
1398
+ __eco: {
1399
+ id: 'nested-component',
1400
+ file: '/app/components/nested-component.ts',
1401
+ integration: 'explicit-renderer',
1402
+ },
1403
+ };
1404
+
1405
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1406
+ Page.config = {
1407
+ dependencies: {
1408
+ components: [NestedComponent],
1409
+ },
1410
+ };
1411
+
1412
+ renderer.PageModule = {
1413
+ default: Page,
1414
+ } as unknown as EcoPageFile;
1415
+ renderer.RenderBodyFactory = () => renderer.RenderedBody;
1416
+ renderer.HtmlTemplate = (() =>
1417
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1418
+
1419
+ await expect(
1420
+ renderer.execute({
1421
+ file: '/app/pages/index.ts',
1422
+ params: {},
1423
+ query: {},
1424
+ }),
1425
+ ).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
1426
+ expect((explicitRenderer.renderComponent as any).mock.calls).toHaveLength(0);
1427
+ });
1428
+
1429
+ it('should fail route execution for deep multi-level unresolved boundary artifacts', async () => {
1430
+ const renderOrder: string[] = [];
1431
+ const explicitRenderer = {
1432
+ renderComponent: vi.fn(async (input: ComponentRenderInput) => {
1433
+ const componentId = input.component.config?.__eco?.id;
1434
+ renderOrder.push(componentId as string);
1435
+
1436
+ if (componentId === 'leaf-component') {
1437
+ return {
1438
+ html: '<span>Leaf Render</span>',
1439
+ canAttachAttributes: true,
1440
+ rootTag: 'span',
1441
+ integrationName: 'explicit-renderer',
1442
+ };
1443
+ }
1444
+
1445
+ if (componentId === 'parent-component') {
1446
+ return {
1447
+ html: `<section>${input.children ?? ''}</section>`,
1448
+ canAttachAttributes: true,
1449
+ rootTag: 'section',
1450
+ integrationName: 'explicit-renderer',
1451
+ };
1452
+ }
1453
+
1454
+ return {
1455
+ html: `<article>${input.children ?? ''}</article>`,
1456
+ canAttachAttributes: true,
1457
+ rootTag: 'article',
1458
+ integrationName: 'explicit-renderer',
1459
+ rootAttributes: { 'data-eco-component-id': 'root-node' },
1460
+ };
1461
+ }),
1462
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1463
+ explicitRenderer.renderComponent(input),
1464
+ ),
1465
+ } as unknown as IntegrationRenderer;
1466
+
1467
+ const appConfig = {
1468
+ ...AppConfig,
1469
+ integrations: [
1470
+ {
1471
+ name: 'explicit-renderer',
1472
+ initializeRenderer: () => explicitRenderer,
1473
+ },
1474
+ ],
1475
+ } as unknown as EcoPagesAppConfig;
1476
+
1477
+ const renderer = new TestIntegrationRenderer({
1478
+ appConfig,
1479
+ assetProcessingService: AssetService,
1480
+ runtimeOrigin: 'http://localhost:3000',
1481
+ });
1482
+
1483
+ const _parentMarker = createBoundaryMarker('n_2', 'parent-component', 'props-parent');
1484
+ const _leafMarker = createBoundaryMarker('n_3', 'leaf-component', 'props-leaf');
1485
+
1486
+ renderer.RenderedBody = `<html><body><main>${createBoundaryMarker('n_1', 'root-component', 'props-root')}</main></body></html>`;
1487
+ renderer.MockComponentRenderResult = {
1488
+ html: '<main>Test Page</main>',
1489
+ canAttachAttributes: true,
1490
+ rootTag: 'main',
1491
+ integrationName: 'test-renderer',
1492
+ };
1493
+
1494
+ const RootComponent = (() => '<article>Root</article>') as EcoComponent<Record<string, unknown>>;
1495
+ RootComponent.config = {
1496
+ integration: 'explicit-renderer',
1497
+ __eco: {
1498
+ id: 'root-component',
1499
+ file: '/app/components/root-component.ts',
1500
+ integration: 'explicit-renderer',
1501
+ },
1502
+ };
1503
+
1504
+ const ParentComponent = (() => '<section>Parent</section>') as EcoComponent<Record<string, unknown>>;
1505
+ ParentComponent.config = {
1506
+ integration: 'explicit-renderer',
1507
+ __eco: {
1508
+ id: 'parent-component',
1509
+ file: '/app/components/parent-component.ts',
1510
+ integration: 'explicit-renderer',
1511
+ },
1512
+ };
1513
+
1514
+ const LeafComponent = (() => '<span>Leaf</span>') as EcoComponent<Record<string, unknown>>;
1515
+ LeafComponent.config = {
1516
+ integration: 'explicit-renderer',
1517
+ __eco: {
1518
+ id: 'leaf-component',
1519
+ file: '/app/components/leaf-component.ts',
1520
+ integration: 'explicit-renderer',
1521
+ },
1522
+ };
1523
+
1524
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1525
+ Page.config = {
1526
+ dependencies: {
1527
+ components: [RootComponent, ParentComponent, LeafComponent],
1528
+ },
1529
+ };
1530
+
1531
+ renderer.PageModule = {
1532
+ default: Page,
1533
+ } as unknown as EcoPageFile;
1534
+ renderer.RenderBodyFactory = () => renderer.RenderedBody;
1535
+ renderer.HtmlTemplate = (() =>
1536
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1537
+
1538
+ await expect(
1539
+ renderer.execute({
1540
+ file: '/app/pages/index.ts',
1541
+ params: {},
1542
+ query: {},
1543
+ }),
1544
+ ).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
1545
+ expect(renderOrder).toEqual([]);
1546
+ });
1547
+
1548
+ it('should fail route execution when props reference is missing', async () => {
1549
+ const explicitRenderer = {
1550
+ renderComponent: vi.fn(async () => ({
1551
+ html: '<aside>Nested Render</aside>',
1552
+ canAttachAttributes: true,
1553
+ rootTag: 'aside',
1554
+ integrationName: 'explicit-renderer',
1555
+ })),
1556
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1557
+ explicitRenderer.renderComponent(input),
1558
+ ),
1559
+ } as unknown as IntegrationRenderer;
1560
+
1561
+ const appConfig = {
1562
+ ...AppConfig,
1563
+ integrations: [
1564
+ {
1565
+ name: 'explicit-renderer',
1566
+ initializeRenderer: () => explicitRenderer,
1567
+ },
1568
+ ],
1569
+ } as unknown as EcoPagesAppConfig;
1570
+
1571
+ const renderer = new TestIntegrationRenderer({
1572
+ appConfig,
1573
+ assetProcessingService: AssetService,
1574
+ runtimeOrigin: 'http://localhost:3000',
1575
+ });
1576
+
1577
+ renderer.RenderedBody =
1578
+ '<html><body><main><eco-marker data-eco-node-id="n_1" data-eco-component-ref="nested-component" data-eco-props-ref="props-missing"></eco-marker></main></body></html>';
1579
+ renderer.MockComponentRenderResult = {
1580
+ html: '<main>Test Page</main>',
1581
+ canAttachAttributes: true,
1582
+ rootTag: 'main',
1583
+ integrationName: 'test-renderer',
1584
+ };
1585
+
1586
+ const NestedComponent = (() => '<aside>Nested</aside>') as EcoComponent<Record<string, unknown>>;
1587
+ NestedComponent.config = {
1588
+ integration: 'explicit-renderer',
1589
+ __eco: {
1590
+ id: 'nested-component',
1591
+ file: '/app/components/nested-component.ts',
1592
+ integration: 'explicit-renderer',
1593
+ },
1594
+ };
1595
+
1596
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1597
+ Page.config = {
1598
+ dependencies: {
1599
+ components: [NestedComponent],
1600
+ },
1601
+ };
1602
+
1603
+ renderer.PageModule = {
1604
+ default: Page,
1605
+ } as unknown as EcoPageFile;
1606
+ renderer.HtmlTemplate = (() =>
1607
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1608
+
1609
+ await expect(
1610
+ renderer.execute({
1611
+ file: '/app/pages/index.ts',
1612
+ params: {},
1613
+ query: {},
1614
+ }),
1615
+ ).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
1616
+ });
1617
+
1618
+ it('should not recursively resolve boundary artifacts that were only passed through resolved child html', async () => {
1619
+ const renderer = new TestIntegrationRenderer({
1620
+ appConfig: AppConfig,
1621
+ assetProcessingService: AssetService,
1622
+ runtimeOrigin: 'http://localhost:3000',
1623
+ });
1624
+
1625
+ renderer.MockComponentRenderResult = {
1626
+ html: '<section><eco-marker data-eco-node-id="n_2" data-eco-component-ref="nested-component" data-eco-props-ref="p_2"></eco-marker></section>',
1627
+ canAttachAttributes: true,
1628
+ rootTag: 'section',
1629
+ integrationName: 'test-renderer',
1630
+ };
1631
+
1632
+ const Component = (() => '<section />') as EcoComponent<Record<string, unknown>>;
1633
+ Component.config = {
1634
+ integration: 'test-renderer',
1635
+ __eco: {
1636
+ id: 'component',
1637
+ file: '/app/components/component.ts',
1638
+ integration: 'test-renderer',
1639
+ },
1640
+ };
1641
+
1642
+ await expect(
1643
+ renderer.renderComponentBoundary({
1644
+ component: Component,
1645
+ props: {},
1646
+ children: '<aside>already resolved child html</aside>',
1647
+ }),
1648
+ ).resolves.toEqual(
1649
+ expect.objectContaining({
1650
+ html: '<section><eco-marker data-eco-node-id="n_2" data-eco-component-ref="nested-component" data-eco-props-ref="p_2"></eco-marker></section>',
1651
+ }),
1652
+ );
1653
+ });
1654
+
1655
+ it('fails fast when a renderer without a boundary runtime crosses into a foreign owner', async () => {
1656
+ const foreignRenderer = {
1657
+ renderComponent: vi.fn(async () => ({
1658
+ html: '<span>resolved nested marker</span>',
1659
+ canAttachAttributes: true,
1660
+ rootTag: 'span',
1661
+ integrationName: 'foreign-renderer',
1662
+ })),
1663
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1664
+ foreignRenderer.renderComponent(input),
1665
+ ),
1666
+ } as unknown as IntegrationRenderer;
1667
+
1668
+ const appConfig = {
1669
+ ...AppConfig,
1670
+ integrations: [
1671
+ {
1672
+ name: 'foreign-renderer',
1673
+ initializeRenderer: () => foreignRenderer,
1674
+ },
1675
+ ],
1676
+ } as unknown as EcoPagesAppConfig;
1677
+
1678
+ const renderer = new TestIntegrationRenderer({
1679
+ appConfig,
1680
+ assetProcessingService: AssetService,
1681
+ runtimeOrigin: 'http://localhost:3000',
1682
+ });
1683
+
1684
+ const ForeignComponent = eco.component<{}, string>({
1685
+ integration: 'foreign-renderer',
1686
+ render: () => '<span>foreign</span>',
1687
+ });
1688
+
1689
+ const ShellComponent = eco.component<{ children?: string }, string>({
1690
+ integration: 'test-renderer',
1691
+ dependencies: {
1692
+ components: [ForeignComponent],
1693
+ },
1694
+ render: ({ children }) => `<section>${children ?? ''}${ForeignComponent({})}</section>`,
1695
+ });
1696
+
1697
+ const passedThroughMarker = createBoundaryMarker('n_passed', 'passed-through-component', 'p_passed');
1698
+
1699
+ await expect(
1700
+ renderer.renderComponentBoundary({
1701
+ component: ShellComponent,
1702
+ props: { children: passedThroughMarker },
1703
+ children: passedThroughMarker,
1704
+ }),
1705
+ ).rejects.toThrow('without a renderer-owned boundary runtime');
1706
+ expect(foreignRenderer.renderComponent).toHaveBeenCalledTimes(0);
1707
+ });
1708
+
1709
+ it('fails route execution when deep mixed-integration boundary artifacts are returned at the route level', async () => {
1710
+ const renderOrder: string[] = [];
1711
+ const explicitRenderer = {
1712
+ renderComponent: vi.fn(async (input: ComponentRenderInput) => {
1713
+ const componentId = input.component.config?.__eco?.id as string | undefined;
1714
+ if (componentId) {
1715
+ renderOrder.push(componentId);
1716
+ }
1717
+
1718
+ if (componentId === 'leaf-component') {
1719
+ return {
1720
+ html: '<span data-leaf="true">Leaf Render</span>',
1721
+ canAttachAttributes: true,
1722
+ rootTag: 'span',
1723
+ integrationName: 'explicit-renderer',
1724
+ };
1725
+ }
1726
+
1727
+ if (componentId === 'parent-component') {
1728
+ return {
1729
+ html: `<section data-parent="true">${input.children ?? ''}</section>`,
1730
+ canAttachAttributes: true,
1731
+ rootTag: 'section',
1732
+ integrationName: 'explicit-renderer',
1733
+ };
1734
+ }
1735
+
1736
+ return {
1737
+ html: `<article data-root="true">${input.children ?? ''}</article>`,
1738
+ canAttachAttributes: true,
1739
+ rootTag: 'article',
1740
+ integrationName: 'explicit-renderer',
1741
+ };
1742
+ }),
1743
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) =>
1744
+ explicitRenderer.renderComponent(input),
1745
+ ),
1746
+ } as unknown as IntegrationRenderer;
1747
+
1748
+ const appConfig = {
1749
+ ...AppConfig,
1750
+ integrations: [
1751
+ {
1752
+ name: 'explicit-renderer',
1753
+ initializeRenderer: () => explicitRenderer,
1754
+ },
1755
+ ],
1756
+ } as unknown as EcoPagesAppConfig;
1757
+
1758
+ const renderer = new TestIntegrationRenderer({
1759
+ appConfig,
1760
+ assetProcessingService: AssetService,
1761
+ runtimeOrigin: 'http://localhost:3000',
1762
+ });
1763
+
1764
+ const _parentMarker = createBoundaryMarker('n_2', 'parent-component', 'props-parent');
1765
+ const _leafMarker = createBoundaryMarker('n_3', 'leaf-component', 'props-leaf');
1766
+
1767
+ renderer.RenderedBody = `<html><body><main><div data-shell="deep">${createBoundaryMarker('n_1', 'root-component', 'props-root')}</div></main></body></html>`;
1768
+ renderer.MockComponentRenderResult = {
1769
+ html: '<main>Test Page</main>',
1770
+ canAttachAttributes: true,
1771
+ rootTag: 'main',
1772
+ integrationName: 'test-renderer',
1773
+ };
1774
+
1775
+ const RootComponent = (() => '<article>Root</article>') as EcoComponent<Record<string, unknown>>;
1776
+ RootComponent.config = {
1777
+ integration: 'explicit-renderer',
1778
+ __eco: {
1779
+ id: 'root-component',
1780
+ file: '/app/components/root-component.ts',
1781
+ integration: 'explicit-renderer',
1782
+ },
1783
+ };
1784
+
1785
+ const ParentComponent = (() => '<section>Parent</section>') as EcoComponent<Record<string, unknown>>;
1786
+ ParentComponent.config = {
1787
+ integration: 'explicit-renderer',
1788
+ __eco: {
1789
+ id: 'parent-component',
1790
+ file: '/app/components/parent-component.ts',
1791
+ integration: 'explicit-renderer',
1792
+ },
1793
+ };
1794
+
1795
+ const LeafComponent = (() => '<span>Leaf</span>') as EcoComponent<Record<string, unknown>>;
1796
+ LeafComponent.config = {
1797
+ integration: 'explicit-renderer',
1798
+ __eco: {
1799
+ id: 'leaf-component',
1800
+ file: '/app/components/leaf-component.ts',
1801
+ integration: 'explicit-renderer',
1802
+ },
1803
+ };
1804
+
1805
+ const Page = (() => '<main>Test Page</main>') as unknown as EcoPageComponent<any>;
1806
+ Page.config = {
1807
+ dependencies: {
1808
+ components: [RootComponent, ParentComponent, LeafComponent],
1809
+ },
1810
+ };
1811
+
1812
+ renderer.PageModule = {
1813
+ default: Page,
1814
+ } as unknown as EcoPageFile;
1815
+ renderer.RenderBodyFactory = () => renderer.RenderedBody;
1816
+ renderer.HtmlTemplate = (() =>
1817
+ '<html><body><main>Test Page</main></body></html>') as EcoComponent<HtmlTemplateProps>;
1818
+
1819
+ await expect(
1820
+ renderer.execute({
1821
+ file: '/app/pages/index.ts',
1822
+ params: {},
1823
+ query: {},
1824
+ }),
1825
+ ).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
1826
+ expect(renderOrder).toEqual([]);
1827
+ });
1828
+
1829
+ it('renders same-integration leaf components under their own integration context', async () => {
1830
+ const renderer = new TestIntegrationRenderer({
1831
+ appConfig: AppConfig,
1832
+ assetProcessingService: AssetService,
1833
+ runtimeOrigin: 'http://localhost:3000',
1834
+ });
1835
+
1836
+ const LeafComponent = eco.component<{}, string>({
1837
+ integration: 'test-renderer',
1838
+ render: () => '<section>Leaf Render</section>',
1839
+ });
1840
+
1841
+ const result = await runWithComponentRenderContext(
1842
+ {
1843
+ currentIntegration: 'foreign-renderer',
1844
+ },
1845
+ async () =>
1846
+ renderer.renderComponentBoundary({
1847
+ component: LeafComponent,
1848
+ props: {},
1849
+ }),
1850
+ );
1851
+
1852
+ expect(result.value.html).toBe('<section>Leaf Render</section>');
1853
+ expect(result.value.html).not.toContain('<eco-marker');
1854
+ expect(renderer.BoundaryRuntimeCreationCount).toBe(0);
1855
+ });
1856
+
1857
+ it('uses inline partial rendering when no foreign boundaries are present', async () => {
1858
+ const renderer = new TestIntegrationRenderer({
1859
+ appConfig: AppConfig,
1860
+ assetProcessingService: AssetService,
1861
+ runtimeOrigin: 'http://localhost:3000',
1862
+ });
1863
+ const View = (() => '<section>Inline</section>') as EcoComponent<Record<string, unknown>>;
1864
+ View.config = {
1865
+ integration: 'test-renderer',
1866
+ __eco: {
1867
+ id: 'inline-view',
1868
+ file: '/app/components/inline-view.ts',
1869
+ integration: 'test-renderer',
1870
+ },
1871
+ };
1872
+
1873
+ let inlineRenderCount = 0;
1874
+ const response = await renderer.testRenderPartialViewResponse({
1875
+ view: View,
1876
+ props: {},
1877
+ renderInline: async () => {
1878
+ inlineRenderCount += 1;
1879
+ return '<section>Inline</section>';
1880
+ },
1881
+ });
1882
+
1883
+ expect(await response.text()).toBe('<section>Inline</section>');
1884
+ expect(inlineRenderCount).toBe(1);
1885
+ expect(renderer.BoundaryRenderCount).toBe(0);
1886
+ });
1887
+
1888
+ it('falls back to boundary partial rendering when compatibility is needed', async () => {
1889
+ const renderer = new TestIntegrationRenderer({
1890
+ appConfig: AppConfig,
1891
+ assetProcessingService: AssetService,
1892
+ runtimeOrigin: 'http://localhost:3000',
1893
+ });
1894
+ const ForeignChild = (() => '<span>Foreign</span>') as EcoComponent<Record<string, unknown>>;
1895
+ ForeignChild.config = {
1896
+ integration: 'react',
1897
+ __eco: {
1898
+ id: 'foreign-child',
1899
+ file: '/app/components/foreign-child.tsx',
1900
+ integration: 'react',
1901
+ },
1902
+ };
1903
+
1904
+ const View = (() => '<section>Boundary</section>') as EcoComponent<Record<string, unknown>>;
1905
+ View.config = {
1906
+ integration: 'test-renderer',
1907
+ __eco: {
1908
+ id: 'boundary-view',
1909
+ file: '/app/components/boundary-view.ts',
1910
+ integration: 'test-renderer',
1911
+ },
1912
+ dependencies: { components: [ForeignChild] },
1913
+ };
1914
+ renderer.MockComponentRenderResult = {
1915
+ html: '<section>Boundary</section>',
1916
+ canAttachAttributes: true,
1917
+ rootTag: 'section',
1918
+ integrationName: 'test-renderer',
1919
+ };
1920
+
1921
+ let inlineRenderCount = 0;
1922
+ const response = await renderer.testRenderPartialViewResponse({
1923
+ view: View,
1924
+ props: {},
1925
+ renderInline: async () => {
1926
+ inlineRenderCount += 1;
1927
+ return '<section>Inline</section>';
1928
+ },
1929
+ });
1930
+
1931
+ expect(await response.text()).toBe('<section>Boundary</section>');
1932
+ expect(inlineRenderCount).toBe(0);
1933
+ expect(renderer.BoundaryRenderCount).toBe(1);
1934
+ });
1935
+
1936
+ it('reuses one foreign renderer instance across shared view shell composition', async () => {
1937
+ const foreignRenderer = {
1938
+ renderComponentBoundary: vi.fn(async (input: ComponentRenderInput) => {
1939
+ const componentId = input.component.config?.__eco?.id;
1940
+
1941
+ if (componentId === 'foreign-html-template') {
1942
+ return {
1943
+ html: `<html><body>${String(input.children ?? '')}</body></html>`,
1944
+ canAttachAttributes: true,
1945
+ rootTag: 'html',
1946
+ integrationName: 'foreign-renderer',
1947
+ };
1948
+ }
1949
+
1950
+ if (componentId === 'foreign-layout') {
1951
+ return {
1952
+ html: `<main>${String(input.children ?? '')}</main>`,
1953
+ canAttachAttributes: true,
1954
+ rootTag: 'main',
1955
+ integrationName: 'foreign-renderer',
1956
+ };
1957
+ }
1958
+
1959
+ return {
1960
+ html: '<section>Foreign View</section>',
1961
+ canAttachAttributes: true,
1962
+ rootTag: 'section',
1963
+ integrationName: 'foreign-renderer',
1964
+ };
1965
+ }),
1966
+ } as unknown as IntegrationRenderer;
1967
+
1968
+ const initializeRenderer = vi.fn(() => foreignRenderer);
1969
+ const appConfig = {
1970
+ ...AppConfig,
1971
+ integrations: [
1972
+ {
1973
+ name: 'foreign-renderer',
1974
+ initializeRenderer,
1975
+ },
1976
+ ],
1977
+ } as unknown as EcoPagesAppConfig;
1978
+
1979
+ const renderer = new TestIntegrationRenderer({
1980
+ appConfig,
1981
+ assetProcessingService: AssetService,
1982
+ runtimeOrigin: 'http://localhost:3000',
1983
+ });
1984
+
1985
+ const View = (() => '<section>Foreign View</section>') as EcoComponent<Record<string, unknown>>;
1986
+ View.config = {
1987
+ integration: 'foreign-renderer',
1988
+ __eco: {
1989
+ id: 'foreign-view',
1990
+ file: '/app/components/foreign-view.ts',
1991
+ integration: 'foreign-renderer',
1992
+ },
1993
+ };
1994
+
1995
+ const Layout = (() => '<main />') as EcoComponent<Record<string, unknown>>;
1996
+ Layout.config = {
1997
+ integration: 'foreign-renderer',
1998
+ __eco: {
1999
+ id: 'foreign-layout',
2000
+ file: '/app/components/foreign-layout.ts',
2001
+ integration: 'foreign-renderer',
2002
+ },
2003
+ };
2004
+
2005
+ renderer.HtmlTemplate = (() => '<html><body></body></html>') as EcoComponent<HtmlTemplateProps>;
2006
+ renderer.HtmlTemplate.config = {
2007
+ integration: 'foreign-renderer',
2008
+ __eco: {
2009
+ id: 'foreign-html-template',
2010
+ file: '/app/components/foreign-html-template.ts',
2011
+ integration: 'foreign-renderer',
2012
+ },
2013
+ };
2014
+
2015
+ const response = await renderer.testRenderViewWithDocumentShell({
2016
+ view: View,
2017
+ props: {},
2018
+ layout: Layout,
2019
+ });
2020
+
2021
+ expect(await response.text()).toContain('<main><section>Foreign View</section></main>');
2022
+ expect(initializeRenderer).toHaveBeenCalledTimes(1);
2023
+ expect(foreignRenderer.renderComponentBoundary).toHaveBeenCalledTimes(3);
2024
+ });
2025
+
2026
+ it('should skip foreign-boundary wrapping for pure same-integration component trees', () => {
2027
+ const renderer = new TestIntegrationRenderer({
2028
+ appConfig: AppConfig,
2029
+ assetProcessingService: AssetService,
2030
+ runtimeOrigin: 'http://localhost:3000',
2031
+ });
2032
+
2033
+ const Child = (() => '<span>Child</span>') as EcoComponent<Record<string, unknown>>;
2034
+ Child.config = {
2035
+ integration: 'test-renderer',
2036
+ __eco: {
2037
+ id: 'child-component',
2038
+ file: '/app/components/child-component.ts',
2039
+ integration: 'test-renderer',
2040
+ },
2041
+ };
2042
+
2043
+ const Root = (() => '<section>Root</section>') as EcoComponent<Record<string, unknown>>;
2044
+ Root.config = {
2045
+ integration: 'test-renderer',
2046
+ __eco: {
2047
+ id: 'root-component',
2048
+ file: '/app/components/root-component.ts',
2049
+ integration: 'test-renderer',
2050
+ },
2051
+ dependencies: { components: [Child] },
2052
+ };
2053
+
2054
+ expect(renderer.testHasForeignBoundaryDescendants(Root)).toBe(false);
2055
+ });
2056
+
2057
+ it('should detect nested cross-integration component trees', () => {
2058
+ const renderer = new TestIntegrationRenderer({
2059
+ appConfig: AppConfig,
2060
+ assetProcessingService: AssetService,
2061
+ runtimeOrigin: 'http://localhost:3000',
2062
+ });
2063
+
2064
+ const ForeignChild = (() => '<span>Child</span>') as EcoComponent<Record<string, unknown>>;
2065
+ ForeignChild.config = {
2066
+ integration: 'react',
2067
+ __eco: {
2068
+ id: 'foreign-child-component',
2069
+ file: '/app/components/foreign-child-component.tsx',
2070
+ integration: 'react',
2071
+ },
2072
+ };
2073
+
2074
+ const Root = (() => '<section>Root</section>') as EcoComponent<Record<string, unknown>>;
2075
+ Root.config = {
2076
+ integration: 'test-renderer',
2077
+ __eco: {
2078
+ id: 'root-component',
2079
+ file: '/app/components/root-component.ts',
2080
+ integration: 'test-renderer',
2081
+ },
2082
+ dependencies: { components: [ForeignChild] },
2083
+ };
2084
+
2085
+ expect(renderer.testHasForeignBoundaryDescendants(Root)).toBe(true);
2086
+ });
2087
+ });
2088
+ });