@ecopages/core 0.2.0-beta.2 → 0.2.0-beta.21

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 (273) hide show
  1. package/README.md +1 -1
  2. package/package.json +163 -3
  3. package/src/adapters/abstract/application-adapter.d.ts +104 -5
  4. package/src/adapters/abstract/application-adapter.js +125 -1
  5. package/src/adapters/abstract/server-adapter.d.ts +6 -2
  6. package/src/adapters/abstract/ws-pattern-matcher.d.ts +60 -0
  7. package/src/adapters/abstract/ws-pattern-matcher.js +61 -0
  8. package/src/adapters/bun/client-bridge.d.ts +1 -0
  9. package/src/adapters/bun/client-bridge.js +3 -0
  10. package/src/adapters/bun/create-app.d.ts +6 -4
  11. package/src/adapters/bun/create-app.js +65 -43
  12. package/src/adapters/bun/server-adapter.d.ts +90 -22
  13. package/src/adapters/bun/server-adapter.js +266 -84
  14. package/src/adapters/bun/static-preview-host.js +25 -20
  15. package/src/adapters/create-app.d.ts +13 -11
  16. package/src/adapters/create-app.js +2 -51
  17. package/src/adapters/index.d.ts +1 -1
  18. package/src/adapters/index.js +1 -2
  19. package/src/adapters/node/create-app.d.ts +5 -1
  20. package/src/adapters/node/create-app.js +51 -12
  21. package/src/adapters/node/node-client-bridge.d.ts +1 -0
  22. package/src/adapters/node/node-client-bridge.js +3 -0
  23. package/src/adapters/node/server-adapter-dependencies.js +4 -0
  24. package/src/adapters/node/server-adapter.d.ts +62 -20
  25. package/src/adapters/node/server-adapter.js +160 -131
  26. package/src/adapters/node/static-content-server.d.ts +1 -0
  27. package/src/adapters/node/static-content-server.js +10 -2
  28. package/src/adapters/node/static-preview-host.js +27 -12
  29. package/src/adapters/shared/bun-user-websocket-lifecycle.d.ts +22 -0
  30. package/src/adapters/shared/bun-user-websocket-lifecycle.js +96 -0
  31. package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
  32. package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
  33. package/src/adapters/shared/fs-server-response-matcher.d.ts +2 -0
  34. package/src/adapters/shared/fs-server-response-matcher.js +40 -18
  35. package/src/adapters/shared/hmr-entrypoint-registrar.d.ts +21 -30
  36. package/src/adapters/shared/hmr-entrypoint-registrar.js +49 -54
  37. package/src/adapters/shared/hmr-html-response.d.ts +1 -1
  38. package/src/adapters/shared/hmr-html-response.js +4 -1
  39. package/src/adapters/shared/node-http-websocket-upgrades.d.ts +21 -0
  40. package/src/adapters/shared/node-http-websocket-upgrades.js +100 -0
  41. package/src/adapters/shared/port-manager.d.ts +116 -0
  42. package/src/adapters/shared/port-manager.js +196 -0
  43. package/src/adapters/shared/runtime-app-bootstrap.d.ts +2 -1
  44. package/src/adapters/shared/runtime-app-bootstrap.js +3 -6
  45. package/src/adapters/shared/runtime-server-lifecycle.d.ts +46 -0
  46. package/src/adapters/shared/runtime-server-lifecycle.js +50 -0
  47. package/src/adapters/shared/server-adapter.d.ts +2 -1
  48. package/src/adapters/shared/server-adapter.js +13 -6
  49. package/src/adapters/shared/server-route-handler.d.ts +4 -1
  50. package/src/adapters/shared/server-route-handler.js +5 -2
  51. package/src/adapters/shared/server-static-builder.d.ts +23 -32
  52. package/src/adapters/shared/server-static-builder.js +100 -73
  53. package/src/adapters/shared/shared-hmr-manager.d.ts +45 -11
  54. package/src/adapters/shared/shared-hmr-manager.js +204 -58
  55. package/src/adapters/shared/static-preview-host.d.ts +5 -0
  56. package/src/adapters/shared/websocket-lifecycle.d.ts +24 -0
  57. package/src/adapters/shared/websocket-lifecycle.js +55 -0
  58. package/src/build/README.md +77 -14
  59. package/src/build/app-build-manifest-runtime.d.ts +9 -0
  60. package/src/build/app-build-manifest-runtime.js +87 -0
  61. package/src/build/build-adapter.d.ts +17 -319
  62. package/src/build/build-adapter.js +28 -94
  63. package/src/build/build-contracts.d.ts +97 -0
  64. package/src/build/build-contracts.js +0 -0
  65. package/src/build/build-input-fingerprint.d.ts +28 -0
  66. package/src/build/build-input-fingerprint.js +31 -0
  67. package/src/build/build-profile-options.d.ts +7 -0
  68. package/src/build/build-profile-options.js +37 -0
  69. package/src/build/build-runtime.d.ts +24 -0
  70. package/src/build/build-runtime.js +65 -0
  71. package/src/build/deduping-build-executor.d.ts +28 -0
  72. package/src/build/deduping-build-executor.js +56 -0
  73. package/src/build/jsx-ownership-plugins.d.ts +24 -0
  74. package/src/build/jsx-ownership-plugins.js +41 -0
  75. package/src/build/pages-unified-graph-build.d.ts +31 -0
  76. package/src/build/pages-unified-graph-build.js +206 -0
  77. package/src/build/parallel-build-executor.d.ts +21 -0
  78. package/src/build/parallel-build-executor.js +52 -0
  79. package/src/build/production-build-cache.d.ts +20 -0
  80. package/src/build/production-build-cache.js +63 -0
  81. package/src/build/rolldown-adapter-helpers.d.ts +3 -6
  82. package/src/build/rolldown-adapter-helpers.js +71 -6
  83. package/src/build/rolldown-build-adapter.d.ts +2 -6
  84. package/src/build/rolldown-build-adapter.js +3 -3
  85. package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
  86. package/src/build/rolldown-build-invocation-metrics.js +43 -0
  87. package/src/build/rolldown-plugin-bridge.d.ts +15 -1
  88. package/src/build/rolldown-plugin-bridge.js +20 -5
  89. package/src/build/rolldown-source-transform-pass.d.ts +15 -0
  90. package/src/build/rolldown-source-transform-pass.js +58 -0
  91. package/src/build/runtime-build-executor.d.ts +8 -15
  92. package/src/build/runtime-build-executor.js +12 -15
  93. package/src/build/runtime-build-output-normalizer.d.ts +7 -0
  94. package/src/build/runtime-build-output-normalizer.js +94 -2
  95. package/src/build/server-entry-build-cache.d.ts +44 -0
  96. package/src/build/server-entry-build-cache.js +161 -0
  97. package/src/client/navigation-scripts/classification.d.ts +16 -0
  98. package/src/client/navigation-scripts/classification.js +41 -0
  99. package/src/client/navigation-scripts/index.d.ts +3 -0
  100. package/src/client/navigation-scripts/index.js +23 -0
  101. package/src/client/navigation-scripts/navigation-scripts.test.browser.js +65 -0
  102. package/src/client/navigation-scripts/registry.d.ts +11 -0
  103. package/src/client/navigation-scripts/registry.js +12 -0
  104. package/src/client/navigation-scripts/rerun-queue.d.ts +37 -0
  105. package/src/client/navigation-scripts/rerun-queue.js +73 -0
  106. package/src/client/scroll.d.ts +17 -0
  107. package/src/client/scroll.js +25 -0
  108. package/src/client/scroll.test.browser.js +37 -0
  109. package/src/client/view-transitions.d.ts +16 -0
  110. package/src/client/view-transitions.js +63 -0
  111. package/src/config/config-builder.d.ts +12 -7
  112. package/src/config/config-builder.js +12 -9
  113. package/src/dev/client-bridge-registry.d.ts +5 -0
  114. package/src/dev/client-bridge-registry.js +15 -0
  115. package/src/dev/dev-client-ownership.d.ts +7 -0
  116. package/src/dev/dev-client-ownership.js +6 -0
  117. package/src/dev/hmr-manager-registry.d.ts +8 -0
  118. package/src/dev/hmr-manager-registry.js +15 -0
  119. package/src/dev/host-runtime.d.ts +3 -0
  120. package/src/dev/host-runtime.js +7 -0
  121. package/src/dev/runtime-server-started-message.d.ts +2 -0
  122. package/src/dev/runtime-server-started-message.js +6 -0
  123. package/src/eco/README.md +11 -0
  124. package/src/eco/eco-declared-component.d.ts +14 -0
  125. package/src/eco/eco-declared-component.js +16 -0
  126. package/src/eco/eco.browser.js +5 -6
  127. package/src/eco/eco.js +5 -6
  128. package/src/eco/eco.types.d.ts +2 -2
  129. package/src/eco/page-layout-normalization.d.ts +21 -0
  130. package/src/eco/page-layout-normalization.js +59 -0
  131. package/src/errors/index.d.ts +1 -0
  132. package/src/errors/index.js +7 -1
  133. package/src/errors/undeclared-component-dependency-error.d.ts +11 -0
  134. package/src/errors/undeclared-component-dependency-error.js +17 -0
  135. package/src/hmr/client/hmr-runtime.js +5 -2
  136. package/src/hmr/hmr-entrypoint-output.d.ts +43 -0
  137. package/src/hmr/hmr-entrypoint-output.js +53 -0
  138. package/src/hmr/hmr-strategy.d.ts +15 -0
  139. package/src/hmr/hmr-strategy.js +19 -0
  140. package/src/hmr/strategies/js-hmr-strategy.d.ts +3 -5
  141. package/src/hmr/strategies/js-hmr-strategy.js +67 -19
  142. package/src/hmr/strategies/server-rendered-template-hmr-strategy.d.ts +17 -0
  143. package/src/hmr/strategies/server-rendered-template-hmr-strategy.js +29 -0
  144. package/src/integrations/ghtml/ghtml-renderer.d.ts +2 -6
  145. package/src/integrations/ghtml/ghtml-renderer.js +2 -51
  146. package/src/integrations/ghtml/ghtml.plugin.d.ts +3 -16
  147. package/src/integrations/ghtml/ghtml.plugin.js +7 -14
  148. package/src/plugins/define-integration.d.ts +26 -0
  149. package/src/plugins/define-integration.js +19 -0
  150. package/src/plugins/eco-component-meta-plugin.js +5 -8
  151. package/src/plugins/foreign-jsx-override-plugin.js +4 -6
  152. package/src/plugins/integration-plugin.d.ts +19 -9
  153. package/src/plugins/integration-plugin.js +7 -4
  154. package/src/plugins/jsx-import-source.utils.d.ts +8 -0
  155. package/src/plugins/jsx-import-source.utils.js +26 -0
  156. package/src/plugins/processor.d.ts +19 -4
  157. package/src/plugins/processor.js +22 -4
  158. package/src/plugins/source-transform.d.ts +31 -0
  159. package/src/plugins/source-transform.js +23 -1
  160. package/src/route-renderer/GRAPH.md +8 -8
  161. package/src/route-renderer/README.md +30 -28
  162. package/src/route-renderer/orchestration/component-graph-collectors.d.ts +10 -0
  163. package/src/route-renderer/orchestration/component-graph-collectors.js +143 -0
  164. package/src/route-renderer/orchestration/component-graph.d.ts +36 -0
  165. package/src/route-renderer/orchestration/component-graph.js +98 -0
  166. package/src/route-renderer/orchestration/component-render-context.d.ts +3 -10
  167. package/src/route-renderer/orchestration/component-render-context.js +3 -1
  168. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +1 -18
  169. package/src/route-renderer/orchestration/declared-ownership-graph.js +5 -31
  170. package/src/route-renderer/orchestration/document-shell-render.service.d.ts +90 -0
  171. package/src/route-renderer/orchestration/document-shell-render.service.js +120 -0
  172. package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +59 -29
  173. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +164 -35
  174. package/src/route-renderer/orchestration/global-injector-assets.service.d.ts +7 -0
  175. package/src/route-renderer/orchestration/global-injector-assets.service.js +32 -0
  176. package/src/route-renderer/orchestration/integration-renderer.d.ts +18 -54
  177. package/src/route-renderer/orchestration/integration-renderer.js +113 -195
  178. package/src/route-renderer/orchestration/integration-route-render-adapter.d.ts +38 -0
  179. package/src/route-renderer/orchestration/integration-route-render-adapter.js +27 -0
  180. package/src/route-renderer/orchestration/layout-shell-props.service.d.ts +20 -0
  181. package/src/route-renderer/orchestration/layout-shell-props.service.js +31 -0
  182. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +5 -0
  183. package/src/route-renderer/orchestration/ownership-validation.service.js +29 -1
  184. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.d.ts +9 -0
  185. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.js +7 -0
  186. package/src/route-renderer/orchestration/page-browser-graph.service.d.ts +25 -0
  187. package/src/route-renderer/orchestration/page-browser-graph.service.js +174 -0
  188. package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
  189. package/src/route-renderer/orchestration/render-output.utils.js +4 -0
  190. package/src/route-renderer/orchestration/route-html-finalization.service.d.ts +17 -0
  191. package/src/route-renderer/orchestration/route-html-finalization.service.js +26 -0
  192. package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +26 -0
  193. package/src/route-renderer/orchestration/route-prepared-options.builder.js +54 -0
  194. package/src/route-renderer/orchestration/route-prepared-options.utils.d.ts +4 -0
  195. package/src/route-renderer/orchestration/route-prepared-options.utils.js +33 -0
  196. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +8 -39
  197. package/src/route-renderer/orchestration/route-render-orchestrator.js +53 -461
  198. package/src/route-renderer/orchestration/string-markup-renderer.d.ts +14 -0
  199. package/src/route-renderer/orchestration/string-markup-renderer.js +55 -0
  200. package/src/route-renderer/page-loading/component-dependency-collection.js +5 -2
  201. package/src/router/client/link-intent.d.ts +11 -21
  202. package/src/router/client/link-intent.js +10 -0
  203. package/src/router/client/link-intent.test.browser.js +12 -1
  204. package/src/router/client/link-navigation-policy.d.ts +45 -0
  205. package/src/router/client/link-navigation-policy.js +101 -0
  206. package/src/router/client/link-navigation-policy.test.browser.d.ts +1 -0
  207. package/src/router/client/link-navigation-policy.test.browser.js +92 -0
  208. package/src/router/client/navigation-coordinator.d.ts +1 -1
  209. package/src/router/client/navigation-coordinator.js +12 -2
  210. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
  211. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
  212. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
  213. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
  214. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts +0 -1
  215. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +18 -15
  216. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
  217. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +0 -3
  218. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -31
  219. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +0 -3
  220. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +2 -30
  221. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.d.ts +2 -0
  222. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.js +43 -0
  223. package/src/services/assets/browser-bundle.service.d.ts +3 -1
  224. package/src/services/assets/browser-bundle.service.js +17 -5
  225. package/src/services/html/html-transformer.service.d.ts +1 -1
  226. package/src/services/html/html-transformer.service.js +4 -4
  227. package/src/services/invalidation/development-invalidation.service.d.ts +20 -2
  228. package/src/services/invalidation/development-invalidation.service.js +56 -2
  229. package/src/services/module-loading/README.md +49 -0
  230. package/src/services/module-loading/app-server-module-transpiler.service.js +5 -17
  231. package/src/services/module-loading/page-module-import.service.d.ts +5 -1
  232. package/src/services/module-loading/page-module-import.service.js +96 -45
  233. package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
  234. package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
  235. package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
  236. package/src/services/module-loading/route-module-build-cache.js +38 -0
  237. package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
  238. package/src/services/module-loading/route-module-build-cache.store.js +207 -0
  239. package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
  240. package/src/services/module-loading/route-module-build-manifest.js +116 -0
  241. package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
  242. package/src/services/module-loading/route-module-dependency-hasher.js +122 -0
  243. package/src/services/module-loading/server-module-transpiler.service.js +1 -1
  244. package/src/static-site-generator/README.md +33 -0
  245. package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
  246. package/src/static-site-generator/static-build-invalidation.js +59 -0
  247. package/src/static-site-generator/static-export-context.d.ts +22 -0
  248. package/src/static-site-generator/static-export-context.js +0 -0
  249. package/src/static-site-generator/static-site-generator.d.ts +17 -20
  250. package/src/static-site-generator/static-site-generator.js +188 -85
  251. package/src/types/internal-types.d.ts +16 -9
  252. package/src/types/public-types.d.ts +144 -40
  253. package/src/utils/parse-cli-args.d.ts +2 -0
  254. package/src/utils/parse-cli-args.js +6 -0
  255. package/src/utils/server-utils.module.d.ts +3 -2
  256. package/src/utils/server-utils.module.js +5 -6
  257. package/src/utils/static-file-extensions.d.ts +12 -0
  258. package/src/utils/static-file-extensions.js +48 -0
  259. package/src/watchers/project-watcher-ignore.d.ts +11 -0
  260. package/src/watchers/project-watcher-ignore.js +16 -0
  261. package/src/watchers/project-watcher.d.ts +26 -3
  262. package/src/watchers/project-watcher.js +132 -30
  263. package/src/watchers/project-watcher.test-helpers.js +5 -5
  264. package/src/build/rolldown-dev-build-adapter.d.ts +0 -82
  265. package/src/build/rolldown-dev-build-adapter.js +0 -166
  266. package/src/hmr/hmr.postcss.test.e2e.js +0 -31
  267. package/src/hmr/hmr.test.e2e.js +0 -43
  268. package/src/route-renderer/orchestration/ownership-planning.service.d.ts +0 -24
  269. package/src/route-renderer/orchestration/ownership-planning.service.js +0 -63
  270. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +0 -91
  271. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.js +0 -170
  272. /package/src/{hmr/hmr.postcss.test.e2e.d.ts → client/navigation-scripts/navigation-scripts.test.browser.d.ts} +0 -0
  273. /package/src/{hmr/hmr.test.e2e.d.ts → client/scroll.test.browser.d.ts} +0 -0
@@ -37,15 +37,15 @@ class FileSystemResponseMatcher {
37
37
  * @returns Static file response or rendered 404 response.
38
38
  */
39
39
  async handleNoMatch(requestUrl) {
40
- const isStaticFileRequest = ServerUtils.hasKnownExtension(requestUrl);
40
+ const isStaticFileRequest = ServerUtils.hasKnownStaticExtension(requestUrl);
41
41
  if (!isStaticFileRequest) {
42
- return this.renderCustomNotFoundResponse();
42
+ return this.renderCustomNotFoundResponseOrServerError(requestUrl);
43
43
  }
44
44
  const relativeUrl = requestUrl.startsWith("/") ? requestUrl.slice(1) : requestUrl;
45
45
  const filePath = path.join(this.assetPrefix, relativeUrl);
46
46
  const contentType = ServerUtils.getContentType(filePath);
47
47
  const response = await this.fileSystemResponseFactory.createFileResponse(filePath, contentType);
48
- return response ?? this.renderCustomNotFoundResponse();
48
+ return response ?? this.renderCustomNotFoundResponseOrServerError(requestUrl);
49
49
  }
50
50
  /**
51
51
  * Handles a matched file-system page route.
@@ -100,17 +100,13 @@ class FileSystemResponseMatcher {
100
100
  return error;
101
101
  }
102
102
  if (error instanceof LocalsAccessError) {
103
- return new Response(error.message, {
104
- status: 500,
105
- headers: { "Content-Type": "text/plain; charset=utf-8" }
106
- });
107
- }
108
- if (error instanceof Error) {
109
- if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
110
- appLogger.error(`[FileSystemResponseMatcher] ${error.message} at ${match.requestedPathname}`);
111
- }
103
+ return this.createInternalServerErrorResponse(error.message, match.requestedPathname, error);
112
104
  }
113
- return this.renderCustomNotFoundResponse();
105
+ return this.createInternalServerErrorResponse(
106
+ error instanceof Error ? error.message : "Internal Server Error",
107
+ match.requestedPathname,
108
+ error
109
+ );
114
110
  }
115
111
  }
116
112
  /**
@@ -119,12 +115,9 @@ class FileSystemResponseMatcher {
119
115
  */
120
116
  async renderCustomNotFoundResponse() {
121
117
  const error404TemplatePath = this.appConfig.absolutePaths.error404TemplatePath;
118
+ let routeRenderer;
122
119
  try {
123
- const routeRenderer = this.routeRendererFactory.getPageRenderer(error404TemplatePath);
124
- const result = await routeRenderer.execute({
125
- file: error404TemplatePath
126
- });
127
- return this.fileSystemResponseFactory.createHtmlNotFoundResponse(result.body);
120
+ routeRenderer = this.routeRendererFactory.getPageRenderer(error404TemplatePath);
128
121
  } catch {
129
122
  appLogger.debug(
130
123
  "Custom 404 template not found, falling back to default 404 response",
@@ -132,6 +125,35 @@ class FileSystemResponseMatcher {
132
125
  );
133
126
  return this.fileSystemResponseFactory.createDefaultNotFoundResponse();
134
127
  }
128
+ const result = await routeRenderer.execute({
129
+ file: error404TemplatePath
130
+ });
131
+ return this.fileSystemResponseFactory.createHtmlNotFoundResponse(result.body);
132
+ }
133
+ async renderCustomNotFoundResponseOrServerError(pathname) {
134
+ try {
135
+ return await this.renderCustomNotFoundResponse();
136
+ } catch (error) {
137
+ if (error instanceof Response) {
138
+ return error;
139
+ }
140
+ return this.createInternalServerErrorResponse(
141
+ error instanceof Error ? error.message : "Internal Server Error",
142
+ pathname,
143
+ error
144
+ );
145
+ }
146
+ }
147
+ createInternalServerErrorResponse(message, pathname, error) {
148
+ if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
149
+ appLogger.error(`[FileSystemResponseMatcher] ${message} at ${pathname}`, error);
150
+ } else {
151
+ appLogger.error(`[FileSystemResponseMatcher] Render error at ${pathname}`, error);
152
+ }
153
+ return new Response("Internal Server Error", {
154
+ status: 500,
155
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
156
+ });
135
157
  }
136
158
  async createExecutionPlan(match, request) {
137
159
  const cacheKey = this.pageRequestCacheCoordinator.buildCacheKey({
@@ -1,20 +1,4 @@
1
- /**
2
- * Shared runtime state used while registering HMR-owned entrypoints.
3
- */
4
- export interface HmrEntrypointRegistrarOptions {
5
- /** Absolute source directory used to derive the emitted HMR path. */
6
- srcDir: string;
7
- /** Absolute distribution directory where HMR outputs are written. */
8
- distDir: string;
9
- /** In-flight registrations keyed by normalized absolute entrypoint path. */
10
- entrypointRegistrations: Map<string, Promise<string>>;
11
- /** Stable entrypoint-to-output mapping retained once an entrypoint is registered. */
12
- watchedFiles: Map<string, string>;
13
- /** Runtime-specific cleanup invoked when an entrypoint registration fails. */
14
- clearFailedRegistration?: (entrypointPath: string) => void;
15
- /** Development-only guardrail for integrations that never finish producing output. */
16
- registrationTimeoutMs: number;
17
- }
1
+ import { type ResolvedHmrEntrypoint } from '../../hmr/hmr-entrypoint-output.js';
18
2
  /**
19
3
  * Runtime-specific hooks required to materialize a single HMR entrypoint.
20
4
  */
@@ -28,28 +12,35 @@ export interface HmrEntrypointRegistrationOptions {
28
12
  */
29
13
  getMissingOutputError(entrypointPath: string, outputPath: string): Error;
30
14
  }
15
+ export interface HmrEntrypointRegistrarOptions {
16
+ /** Absolute source directory used to derive the emitted HMR path. */
17
+ srcDir: string;
18
+ /** Absolute distribution directory where HMR outputs are written. */
19
+ distDir: string;
20
+ /** Runtime-specific cleanup invoked when an entrypoint registration fails. */
21
+ clearFailedRegistration?: (entrypointPath: string) => void;
22
+ }
31
23
  /**
32
24
  * Coordinates the shared HMR entrypoint registration lifecycle for both Node and Bun managers.
33
25
  *
34
- * The registrar owns the cross-runtime policy: normalize entrypoint identities, dedupe concurrent
35
- * registrations, derive the emitted `_hmr` output path, clear stale output before rebuilding, and
36
- * apply the development timeout that prevents unresolved registrations from hanging navigation.
37
- * Runtime-specific managers remain responsible for the actual emit step and any cleanup outside
38
- * this shared registration flow.
26
+ * @remarks
27
+ * The registrar owns in-flight deduplication and the registered entrypoint table.
28
+ * A source path is committed only after emit succeeds and the expected output exists on disk.
39
29
  */
40
30
  export declare class HmrEntrypointRegistrar {
41
31
  private readonly options;
32
+ private readonly inFlight;
33
+ private readonly registered;
42
34
  constructor(options: HmrEntrypointRegistrarOptions);
35
+ getRegistered(): ReadonlyMap<string, ResolvedHmrEntrypoint>;
36
+ getWatchedFiles(): Map<string, string>;
37
+ clearRegistration(entrypointPath: string): void;
38
+ clearAll(): void;
43
39
  /**
44
- * Registers a single source entrypoint and returns the browser URL for its emitted HMR module.
40
+ * Registers a single source entrypoint and returns its verified HMR artifact.
45
41
  *
46
- * Concurrent requests for the same normalized entrypoint share the same in-flight promise so the
47
- * integration only builds once per registration cycle.
42
+ * Concurrent requests for the same normalized entrypoint share the same in-flight promise.
48
43
  */
49
- registerEntrypoint(entrypointPath: string, registrationOptions: HmrEntrypointRegistrationOptions): Promise<string>;
44
+ registerEntrypoint(entrypointPath: string, registrationOptions: HmrEntrypointRegistrationOptions): Promise<ResolvedHmrEntrypoint>;
50
45
  private registerEntrypointInternal;
51
- private awaitEntrypointRegistration;
52
- private getEntrypointOutput;
53
- private removeStaleEntrypointOutput;
54
- private encodeDynamicSegments;
55
46
  }
@@ -1,85 +1,80 @@
1
1
  import path from "node:path";
2
2
  import { fileSystem } from "@ecopages/file-system";
3
- import { appLogger } from "../../global/app-logger.js";
4
- import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
3
+ import {
4
+ removeStaleHmrEntrypointOutput,
5
+ resolveHmrEntrypointOutputPaths
6
+ } from "../../hmr/hmr-entrypoint-output.js";
5
7
  class HmrEntrypointRegistrar {
6
8
  options;
9
+ inFlight = /* @__PURE__ */ new Map();
10
+ registered = /* @__PURE__ */ new Map();
7
11
  constructor(options) {
8
12
  this.options = options;
9
13
  }
14
+ getRegistered() {
15
+ return this.registered;
16
+ }
17
+ getWatchedFiles() {
18
+ const watchedFiles = /* @__PURE__ */ new Map();
19
+ for (const [sourcePath, entrypoint] of this.registered) {
20
+ watchedFiles.set(sourcePath, entrypoint.outputUrl);
21
+ }
22
+ return watchedFiles;
23
+ }
24
+ clearRegistration(entrypointPath) {
25
+ this.registered.delete(path.resolve(entrypointPath));
26
+ }
27
+ clearAll() {
28
+ this.inFlight.clear();
29
+ this.registered.clear();
30
+ }
10
31
  /**
11
- * Registers a single source entrypoint and returns the browser URL for its emitted HMR module.
32
+ * Registers a single source entrypoint and returns its verified HMR artifact.
12
33
  *
13
- * Concurrent requests for the same normalized entrypoint share the same in-flight promise so the
14
- * integration only builds once per registration cycle.
34
+ * Concurrent requests for the same normalized entrypoint share the same in-flight promise.
15
35
  */
16
36
  async registerEntrypoint(entrypointPath, registrationOptions) {
17
37
  const normalizedEntrypoint = path.resolve(entrypointPath);
18
- const existingRegistration = this.options.entrypointRegistrations.get(normalizedEntrypoint);
38
+ const existing = this.registered.get(normalizedEntrypoint);
39
+ if (existing && fileSystem.exists(existing.outputPath)) {
40
+ return existing;
41
+ }
42
+ const existingRegistration = this.inFlight.get(normalizedEntrypoint);
19
43
  if (existingRegistration) {
20
- return await this.awaitEntrypointRegistration(existingRegistration, normalizedEntrypoint);
44
+ return await existingRegistration;
21
45
  }
22
46
  const registration = this.registerEntrypointInternal(normalizedEntrypoint, registrationOptions);
23
- this.options.entrypointRegistrations.set(normalizedEntrypoint, registration);
47
+ this.inFlight.set(normalizedEntrypoint, registration);
24
48
  try {
25
- return await this.awaitEntrypointRegistration(registration, normalizedEntrypoint);
49
+ return await registration;
26
50
  } catch (error) {
27
51
  this.options.clearFailedRegistration?.(normalizedEntrypoint);
52
+ this.registered.delete(normalizedEntrypoint);
28
53
  throw error;
29
54
  } finally {
30
- this.options.entrypointRegistrations.delete(normalizedEntrypoint);
55
+ if (this.inFlight.get(normalizedEntrypoint) === registration) {
56
+ this.inFlight.delete(normalizedEntrypoint);
57
+ }
31
58
  }
32
59
  }
33
60
  async registerEntrypointInternal(entrypointPath, registrationOptions) {
34
- if (this.options.watchedFiles.has(entrypointPath)) {
35
- return this.options.watchedFiles.get(entrypointPath);
36
- }
37
- const { outputPath, outputUrl } = this.getEntrypointOutput(entrypointPath);
38
- this.options.watchedFiles.set(entrypointPath, outputUrl);
39
- this.removeStaleEntrypointOutput(outputPath);
61
+ const { outputPath, outputUrl } = resolveHmrEntrypointOutputPaths(
62
+ this.options.srcDir,
63
+ this.options.distDir,
64
+ entrypointPath
65
+ );
66
+ removeStaleHmrEntrypointOutput(outputPath, "HMR");
40
67
  await registrationOptions.emit(entrypointPath, outputPath);
41
68
  if (!fileSystem.exists(outputPath)) {
42
69
  throw registrationOptions.getMissingOutputError(entrypointPath, outputPath);
43
70
  }
44
- return outputUrl;
45
- }
46
- async awaitEntrypointRegistration(registration, entrypointPath) {
47
- if (process.env.NODE_ENV !== "development") {
48
- return await registration;
49
- }
50
- return await Promise.race([
51
- registration,
52
- new Promise((_, reject) => {
53
- setTimeout(() => {
54
- reject(new Error(`[HMR] Timed out registering entrypoint: ${entrypointPath}`));
55
- }, this.options.registrationTimeoutMs);
56
- })
57
- ]);
58
- }
59
- getEntrypointOutput(entrypointPath) {
60
- const relativePath = path.relative(this.options.srcDir, entrypointPath);
61
- const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js");
62
- const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
63
- const urlPath = encodedPathJs.split(path.sep).join("/");
64
- return {
65
- outputUrl: `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath)}`,
66
- outputPath: path.join(this.options.distDir, urlPath)
71
+ const resolved = {
72
+ sourcePath: entrypointPath,
73
+ outputPath,
74
+ outputUrl
67
75
  };
68
- }
69
- removeStaleEntrypointOutput(outputPath) {
70
- if (!fileSystem.exists(outputPath)) {
71
- return;
72
- }
73
- try {
74
- fileSystem.remove(outputPath);
75
- } catch (error) {
76
- appLogger.warn(
77
- `[HMR] Failed to remove stale entrypoint output ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
78
- );
79
- }
80
- }
81
- encodeDynamicSegments(filepath) {
82
- return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
76
+ this.registered.set(entrypointPath, resolved);
77
+ return resolved;
83
78
  }
84
79
  }
85
80
  export {
@@ -11,7 +11,7 @@ export declare function isHtmlResponse(response: Response): boolean;
11
11
  * responses flow through different layers, but both need identical injection
12
12
  * behavior in watch mode.
13
13
  */
14
- export declare function shouldInjectHmrHtmlResponse(watch: boolean, hmrManager?: Pick<IHmrManager, 'isEnabled'>): boolean;
14
+ export declare function shouldInjectHmrHtmlResponse(watch: boolean, hmrManager?: Pick<IHmrManager, 'isEnabled'>, hostOwnsDevClient?: boolean): boolean;
15
15
  /**
16
16
  * Injects the development HMR runtime script into an HTML response if it is not
17
17
  * already present.
@@ -4,7 +4,10 @@ function isHtmlResponse(response) {
4
4
  const contentType = response.headers.get("Content-Type");
5
5
  return contentType !== null && contentType.startsWith("text/html");
6
6
  }
7
- function shouldInjectHmrHtmlResponse(watch, hmrManager) {
7
+ function shouldInjectHmrHtmlResponse(watch, hmrManager, hostOwnsDevClient = false) {
8
+ if (hostOwnsDevClient) {
9
+ return false;
10
+ }
8
11
  return watch && hmrManager?.isEnabled() === true;
9
12
  }
10
13
  async function injectHmrRuntimeIntoHtmlResponse(response) {
@@ -0,0 +1,21 @@
1
+ import type { Duplex } from 'node:stream';
2
+ import type { Server as NodeHttpServer, IncomingMessage } from 'node:http';
3
+ import type { EcopagesWebSocketHandler } from '../../types/public-types.js';
4
+ export type NodeHttpWebSocketUpgradePreflight = (req: IncomingMessage, socket: Duplex, head: Buffer) => boolean;
5
+ export type AttachNodeHttpWebSocketUpgradesOptions = {
6
+ runtimeOrigin: string;
7
+ websocketHandlers: Map<string, EcopagesWebSocketHandler<any, any>>;
8
+ /**
9
+ * When true, unmatched upgrade requests are left for other listeners (e.g.
10
+ * Vite HMR). When false, unmatched upgrades are destroyed immediately.
11
+ */
12
+ passthroughUnmatched?: boolean;
13
+ preflight?: NodeHttpWebSocketUpgradePreflight;
14
+ };
15
+ /**
16
+ * Wires Ecopages user WebSocket routes onto a Node HTTP server's `upgrade` event.
17
+ *
18
+ * Used by the Node adapter in standalone mode and by host integrations such as
19
+ * the Vite plugin that embed Ecopages behind a foreign HTTP server.
20
+ */
21
+ export declare function attachNodeHttpWebSocketUpgrades(server: NodeHttpServer, options: AttachNodeHttpWebSocketUpgradesOptions): void;
@@ -0,0 +1,100 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { appLogger } from "../../global/app-logger.js";
3
+ import { findWebSocketRoute } from "../abstract/ws-pattern-matcher.js";
4
+ import { createEcopagesSocket, invokeWebSocketHandlerHook, toWebSocketCloseInfo } from "./websocket-lifecycle.js";
5
+ const attachedUpgradeServers = /* @__PURE__ */ new WeakSet();
6
+ function adaptNodeWebSocket(ws, kind, params, search, context) {
7
+ return createEcopagesSocket(
8
+ {
9
+ send: (data) => ws.send(data),
10
+ close: (code, reason) => ws.close(code, reason)
11
+ },
12
+ { kind, params, search, context }
13
+ );
14
+ }
15
+ function toUpgradeRequest(runtimeOrigin, req) {
16
+ const upgradeUrl = new URL(req.url ?? "/", runtimeOrigin);
17
+ return new Request(upgradeUrl, {
18
+ method: req.method,
19
+ headers: req.headers
20
+ });
21
+ }
22
+ async function resolveNodeContext(request, handler, kind, params, search) {
23
+ if (!handler.context) {
24
+ return void 0;
25
+ }
26
+ try {
27
+ return await handler.context({ request, kind, params, search });
28
+ } catch (error) {
29
+ appLogger.error(`[WS:${kind}] context() failed; closing connection.`, error);
30
+ throw error;
31
+ }
32
+ }
33
+ async function setupNodeWebSocketConnection(ws, wsMatch, req, runtimeOrigin, search) {
34
+ const handler = wsMatch.handler;
35
+ const baseRequest = toUpgradeRequest(runtimeOrigin, req);
36
+ let context;
37
+ try {
38
+ context = await resolveNodeContext(baseRequest, handler, wsMatch.kind, wsMatch.params, search);
39
+ } catch {
40
+ ws.close(1011, "context initialization failed");
41
+ return;
42
+ }
43
+ const socket = adaptNodeWebSocket(ws, wsMatch.kind, wsMatch.params, search, context);
44
+ try {
45
+ await handler.onConnect?.(socket);
46
+ } catch (error) {
47
+ appLogger.error(`[WS:${wsMatch.kind}] onConnect failed:`, error);
48
+ ws.close(1011, "onConnect failed");
49
+ return;
50
+ }
51
+ ws.on("message", (msg, isBinary) => {
52
+ const message = isBinary ? {
53
+ kind: "binary",
54
+ data: msg instanceof ArrayBuffer ? new Uint8Array(msg) : Array.isArray(msg) ? new Uint8Array(Buffer.concat(msg)) : new Uint8Array(msg)
55
+ } : { kind: "text", text: msg.toString() };
56
+ invokeWebSocketHandlerHook(wsMatch.kind, "onMessage", handler.onMessage?.(socket, message));
57
+ });
58
+ ws.on("close", (code, reason) => {
59
+ const event = toWebSocketCloseInfo(code, reason.toString());
60
+ invokeWebSocketHandlerHook(wsMatch.kind, "onClose", handler.onClose?.(socket, event));
61
+ });
62
+ ws.on("error", (err) => {
63
+ appLogger.error(`[WS:${wsMatch.kind}] error:`, err);
64
+ invokeWebSocketHandlerHook(wsMatch.kind, "onError", handler.onError?.(socket, err));
65
+ });
66
+ }
67
+ function attachNodeHttpWebSocketUpgrades(server, options) {
68
+ if (options.websocketHandlers.size === 0) {
69
+ return;
70
+ }
71
+ if (attachedUpgradeServers.has(server)) {
72
+ appLogger.warn("[WS] WebSocket upgrades already attached to this HTTP server; skipping duplicate attach.");
73
+ return;
74
+ }
75
+ attachedUpgradeServers.add(server);
76
+ const userWss = new WebSocketServer({ noServer: true });
77
+ server.on("upgrade", (req, socket, head) => {
78
+ if (options.preflight?.(req, socket, head)) {
79
+ return;
80
+ }
81
+ const url = new URL(req.url ?? "/", options.runtimeOrigin);
82
+ const wsMatch = findWebSocketRoute(options.websocketHandlers, url.pathname);
83
+ if (!wsMatch) {
84
+ if (!options.passthroughUnmatched) {
85
+ socket.destroy();
86
+ }
87
+ return;
88
+ }
89
+ userWss.handleUpgrade(req, socket, head, (ws) => {
90
+ const search = Object.fromEntries(url.searchParams.entries());
91
+ void setupNodeWebSocketConnection(ws, wsMatch, req, options.runtimeOrigin, search).catch((error) => {
92
+ appLogger.error(`[WS:${wsMatch.kind}] unexpected error:`, error);
93
+ ws.close(1011, "internal error");
94
+ });
95
+ });
96
+ });
97
+ }
98
+ export {
99
+ attachNodeHttpWebSocketUpgrades
100
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Detects whether a thrown error indicates the requested port is already in
3
+ * use.
4
+ *
5
+ * @remarks
6
+ * Matches Node's `code: 'EADDRINUSE'` and Bun's surface, where the error
7
+ * carries the same `code` plus a human-readable message such as
8
+ * `"Failed to start server. Is port 3000 in use?"`. Older Bun builds that do
9
+ * not set `.code` are covered by a fallback match on `syscall === 'listen'`
10
+ * plus the message shape.
11
+ */
12
+ export declare function isPortInUseError(error: unknown): boolean;
13
+ /**
14
+ * Probes whether a TCP port is currently bindable from this process. Uses a
15
+ * short-lived Node `net.Server` so both Node and Bun can run it via their
16
+ * Node-compatible core.
17
+ *
18
+ * @remarks
19
+ * A successful probe only guarantees the port was free at probe time; callers
20
+ * must still handle EADDRINUSE when they actually bind, since release races
21
+ * can flip the answer between probe and bind.
22
+ */
23
+ export declare function isPortAvailable(port: number, hostname?: string): Promise<boolean>;
24
+ /**
25
+ * Default interactive prompt. Asks a yes/no question on stdin/stdout and
26
+ * auto-approves (returns `true`) after `timeoutMs` with no input.
27
+ *
28
+ * @returns `true` when the user accepted (or timed out), `false` when they declined.
29
+ */
30
+ export type PromptFunction = (message: string, timeoutMs: number) => Promise<boolean>;
31
+ export interface PortManagerOptions {
32
+ /**
33
+ * Bind factory. Should throw on EADDRINUSE (or return `null`/`undefined`
34
+ * when it declines without a port collision). Returning a non-null port
35
+ * marks the bind as successful.
36
+ */
37
+ startOnPort: (port: number) => Promise<number | null | undefined>;
38
+ /** Emits the "moved to port X" warning when the bound port differs from the preferred port. */
39
+ warn?: (message: string) => void;
40
+ /** Infers a free port to display in the prompt. Defaults to {@link isPortAvailable}. */
41
+ probePort?: (port: number) => Promise<boolean>;
42
+ /** Custom prompter. Defaults to a readline-based yes/no prompt on the process stdio. */
43
+ prompt?: PromptFunction;
44
+ /** Enables the prompt flow at all. Defaults to `process.stdin.isTTY && process.stdout.isTTY`. */
45
+ interactive?: boolean;
46
+ /** Auto-approve timeout (ms) for the prompt. Defaults to 10000. */
47
+ autoApproveMs?: number;
48
+ /** Maximum port offset to scan when falling back. Defaults to 20. */
49
+ maxPortOffset?: number;
50
+ /** Retry attempts for the same port before declaring it taken. Defaults to 2. */
51
+ releaseRaceRetries?: number;
52
+ /** Backoff between same-port retries. Defaults to 100 ms. */
53
+ releaseRaceDelayMs?: number;
54
+ }
55
+ export interface BindPortOptions {
56
+ /** Port the caller would prefer to bind. */
57
+ preferredPort: number;
58
+ /**
59
+ * Whether the caller permits moving to the next free port automatically when
60
+ * the preferred port is busy and no interactive prompt is available
61
+ * (non-TTY / CI). Explicit ports that fall back silently in CI would surprise
62
+ * operators, so this is honoured by the non-interactive path only.
63
+ */
64
+ allowPortFallback: boolean;
65
+ }
66
+ /**
67
+ * Owns preview-port assignment: probing, race-release retries, fallback, and
68
+ * an optional interactive auto-approve prompt.
69
+ *
70
+ * @remarks
71
+ * Single-process API. The manager does not cache ports across calls; each
72
+ * `bind()` runs fresh from the preferred port.
73
+ */
74
+ export declare class PortManager {
75
+ private readonly startOnPort;
76
+ private readonly warn;
77
+ private readonly probePort;
78
+ private readonly prompt;
79
+ private readonly interactive;
80
+ private readonly autoApproveMs;
81
+ private readonly maxPortOffset;
82
+ private readonly releaseRaceRetries;
83
+ private readonly releaseRaceDelayMs;
84
+ constructor(options: PortManagerOptions);
85
+ /**
86
+ * Binds the preview server to the preferred port.
87
+ *
88
+ * @remarks
89
+ * When the preferred port is free, binds it directly — no prompt, no
90
+ * fallback. The interactive prompt only fires on a genuine EADDRINUSE
91
+ * collision (TTY runs), and auto-approves after
92
+ * {@link PortManagerOptions.autoApproveMs}. In non-interactive (CI) runs,
93
+ * the manager falls back silently only when
94
+ * {@link BindPortOptions.allowPortFallback} is true (the default-port
95
+ * path); pinned ports rethrow EADDRINUSE so Docker-style fixed-port
96
+ * deployments fail loudly instead of silently moving.
97
+ */
98
+ bind(options: BindPortOptions): Promise<number | null>;
99
+ private shouldFallback;
100
+ private fallForward;
101
+ /**
102
+ * Attempts to bind via {@link PortManagerOptions.startOnPort} on `port`,
103
+ * retrying EADDRINUSE collisions up to
104
+ * {@link PortManagerOptions.releaseRaceRetries} to ride through TIME_WAIT
105
+ * race release.
106
+ *
107
+ * @remarks
108
+ * Only EADDRINUSE is retryable. Any other error is rethrown synchronously
109
+ * rather than swallowed as a soft "port taken" result, so a misconfigured
110
+ * factory surfaces loudly instead of causing a silent fallback. A `null`
111
+ * return from the factory (no throw) is reported as `factory-refused`, not
112
+ * conflated with a real collision.
113
+ */
114
+ private tryPort;
115
+ private findNextFreePort;
116
+ }