@ecopages/core 0.2.0-beta.1 → 0.2.0-beta.11

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 (216) hide show
  1. package/package.json +107 -3
  2. package/src/adapters/abstract/application-adapter.d.ts +73 -1
  3. package/src/adapters/abstract/application-adapter.js +89 -0
  4. package/src/adapters/abstract/server-adapter.d.ts +3 -0
  5. package/src/adapters/abstract/ws-pattern-matcher.d.ts +60 -0
  6. package/src/adapters/abstract/ws-pattern-matcher.js +61 -0
  7. package/src/adapters/bun/client-bridge.d.ts +1 -0
  8. package/src/adapters/bun/client-bridge.js +3 -0
  9. package/src/adapters/bun/create-app.d.ts +5 -3
  10. package/src/adapters/bun/create-app.js +44 -39
  11. package/src/adapters/bun/hmr-manager.d.ts +2 -1
  12. package/src/adapters/bun/hmr-manager.js +2 -2
  13. package/src/adapters/bun/server-adapter.d.ts +81 -20
  14. package/src/adapters/bun/server-adapter.js +220 -78
  15. package/src/adapters/bun/static-preview-host.js +25 -20
  16. package/src/adapters/create-app.d.ts +13 -0
  17. package/src/adapters/create-app.js +10 -1
  18. package/src/adapters/node/create-app.d.ts +4 -0
  19. package/src/adapters/node/create-app.js +38 -9
  20. package/src/adapters/node/node-client-bridge.d.ts +1 -0
  21. package/src/adapters/node/node-client-bridge.js +3 -0
  22. package/src/adapters/node/node-hmr-manager.d.ts +2 -1
  23. package/src/adapters/node/node-hmr-manager.js +2 -2
  24. package/src/adapters/node/server-adapter-dependencies.js +2 -0
  25. package/src/adapters/node/server-adapter.d.ts +55 -19
  26. package/src/adapters/node/server-adapter.js +140 -132
  27. package/src/adapters/node/static-content-server.d.ts +1 -0
  28. package/src/adapters/node/static-content-server.js +10 -2
  29. package/src/adapters/node/static-preview-host.js +27 -12
  30. package/src/adapters/shared/bun-user-websocket-lifecycle.d.ts +22 -0
  31. package/src/adapters/shared/bun-user-websocket-lifecycle.js +96 -0
  32. package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
  33. package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
  34. package/src/adapters/shared/hmr-html-response.d.ts +1 -1
  35. package/src/adapters/shared/hmr-html-response.js +4 -1
  36. package/src/adapters/shared/node-http-websocket-upgrades.d.ts +21 -0
  37. package/src/adapters/shared/node-http-websocket-upgrades.js +100 -0
  38. package/src/adapters/shared/port-manager.d.ts +116 -0
  39. package/src/adapters/shared/port-manager.js +196 -0
  40. package/src/adapters/shared/runtime-app-bootstrap.d.ts +2 -1
  41. package/src/adapters/shared/runtime-app-bootstrap.js +3 -6
  42. package/src/adapters/shared/runtime-server-lifecycle.d.ts +38 -0
  43. package/src/adapters/shared/runtime-server-lifecycle.js +39 -0
  44. package/src/adapters/shared/server-adapter.d.ts +2 -1
  45. package/src/adapters/shared/server-adapter.js +12 -4
  46. package/src/adapters/shared/server-route-handler.d.ts +4 -1
  47. package/src/adapters/shared/server-route-handler.js +5 -2
  48. package/src/adapters/shared/server-static-builder.d.ts +14 -29
  49. package/src/adapters/shared/server-static-builder.js +91 -70
  50. package/src/adapters/shared/shared-hmr-manager.d.ts +12 -0
  51. package/src/adapters/shared/shared-hmr-manager.js +64 -7
  52. package/src/adapters/shared/static-preview-host.d.ts +5 -0
  53. package/src/adapters/shared/websocket-lifecycle.d.ts +24 -0
  54. package/src/adapters/shared/websocket-lifecycle.js +55 -0
  55. package/src/build/README.md +77 -14
  56. package/src/build/app-build-manifest-runtime.d.ts +10 -0
  57. package/src/build/app-build-manifest-runtime.js +90 -0
  58. package/src/build/build-adapter.d.ts +17 -319
  59. package/src/build/build-adapter.js +28 -94
  60. package/src/build/build-contracts.d.ts +97 -0
  61. package/src/build/build-contracts.js +0 -0
  62. package/src/build/build-input-fingerprint.d.ts +28 -0
  63. package/src/build/build-input-fingerprint.js +31 -0
  64. package/src/build/build-profile-options.d.ts +7 -0
  65. package/src/build/build-profile-options.js +37 -0
  66. package/src/build/build-runtime.d.ts +24 -0
  67. package/src/build/build-runtime.js +65 -0
  68. package/src/build/deduping-build-executor.d.ts +28 -0
  69. package/src/build/deduping-build-executor.js +56 -0
  70. package/src/build/jsx-ownership-plugins.d.ts +24 -0
  71. package/src/build/jsx-ownership-plugins.js +41 -0
  72. package/src/build/pages-unified-graph-build.d.ts +31 -0
  73. package/src/build/pages-unified-graph-build.js +206 -0
  74. package/src/build/parallel-build-executor.d.ts +21 -0
  75. package/src/build/parallel-build-executor.js +52 -0
  76. package/src/build/production-build-cache.d.ts +20 -0
  77. package/src/build/production-build-cache.js +63 -0
  78. package/src/build/rolldown-adapter-helpers.d.ts +3 -6
  79. package/src/build/rolldown-adapter-helpers.js +71 -6
  80. package/src/build/rolldown-build-adapter.d.ts +2 -6
  81. package/src/build/rolldown-build-adapter.js +3 -3
  82. package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
  83. package/src/build/rolldown-build-invocation-metrics.js +43 -0
  84. package/src/build/rolldown-plugin-bridge.d.ts +15 -1
  85. package/src/build/rolldown-plugin-bridge.js +20 -5
  86. package/src/build/rolldown-source-transform-pass.d.ts +15 -0
  87. package/src/build/rolldown-source-transform-pass.js +58 -0
  88. package/src/build/runtime-build-executor.d.ts +8 -15
  89. package/src/build/runtime-build-executor.js +12 -15
  90. package/src/build/runtime-build-output-normalizer.d.ts +7 -0
  91. package/src/build/runtime-build-output-normalizer.js +94 -2
  92. package/src/build/server-entry-build-cache.d.ts +44 -0
  93. package/src/build/server-entry-build-cache.js +161 -0
  94. package/src/config/config-builder.d.ts +12 -7
  95. package/src/config/config-builder.js +12 -9
  96. package/src/dev/client-bridge-registry.d.ts +5 -0
  97. package/src/dev/client-bridge-registry.js +15 -0
  98. package/src/dev/dev-client-ownership.d.ts +7 -0
  99. package/src/dev/dev-client-ownership.js +6 -0
  100. package/src/dev/host-runtime.d.ts +3 -0
  101. package/src/dev/host-runtime.js +7 -0
  102. package/src/hmr/client/hmr-runtime.js +5 -2
  103. package/src/hmr/strategies/server-rendered-template-hmr-strategy.d.ts +17 -0
  104. package/src/hmr/strategies/server-rendered-template-hmr-strategy.js +29 -0
  105. package/src/integrations/ghtml/ghtml-renderer.d.ts +2 -6
  106. package/src/integrations/ghtml/ghtml-renderer.js +2 -51
  107. package/src/integrations/ghtml/ghtml.plugin.d.ts +3 -16
  108. package/src/integrations/ghtml/ghtml.plugin.js +7 -14
  109. package/src/plugins/define-integration.d.ts +26 -0
  110. package/src/plugins/define-integration.js +19 -0
  111. package/src/plugins/eco-component-meta-plugin.js +5 -8
  112. package/src/plugins/foreign-jsx-override-plugin.js +4 -6
  113. package/src/plugins/integration-plugin.d.ts +19 -9
  114. package/src/plugins/integration-plugin.js +7 -4
  115. package/src/plugins/jsx-import-source.utils.d.ts +8 -0
  116. package/src/plugins/jsx-import-source.utils.js +26 -0
  117. package/src/plugins/processor.d.ts +19 -4
  118. package/src/plugins/processor.js +22 -4
  119. package/src/plugins/source-transform.d.ts +31 -0
  120. package/src/plugins/source-transform.js +23 -1
  121. package/src/route-renderer/GRAPH.md +8 -8
  122. package/src/route-renderer/README.md +30 -28
  123. package/src/route-renderer/orchestration/component-graph-collectors.d.ts +10 -0
  124. package/src/route-renderer/orchestration/component-graph-collectors.js +143 -0
  125. package/src/route-renderer/orchestration/component-graph.d.ts +36 -0
  126. package/src/route-renderer/orchestration/component-graph.js +90 -0
  127. package/src/route-renderer/orchestration/component-render-context.d.ts +3 -10
  128. package/src/route-renderer/orchestration/component-render-context.js +3 -1
  129. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +1 -18
  130. package/src/route-renderer/orchestration/declared-ownership-graph.js +5 -31
  131. package/src/route-renderer/orchestration/document-shell-render.service.d.ts +54 -0
  132. package/src/route-renderer/orchestration/document-shell-render.service.js +54 -0
  133. package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +59 -29
  134. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +164 -35
  135. package/src/route-renderer/orchestration/global-injector-assets.service.d.ts +7 -0
  136. package/src/route-renderer/orchestration/global-injector-assets.service.js +32 -0
  137. package/src/route-renderer/orchestration/integration-renderer.d.ts +18 -54
  138. package/src/route-renderer/orchestration/integration-renderer.js +108 -194
  139. package/src/route-renderer/orchestration/integration-route-render-adapter.d.ts +38 -0
  140. package/src/route-renderer/orchestration/integration-route-render-adapter.js +27 -0
  141. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +4 -0
  142. package/src/route-renderer/orchestration/ownership-validation.service.js +11 -1
  143. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.d.ts +9 -0
  144. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.js +7 -0
  145. package/src/route-renderer/orchestration/page-browser-graph.service.d.ts +25 -0
  146. package/src/route-renderer/orchestration/page-browser-graph.service.js +174 -0
  147. package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
  148. package/src/route-renderer/orchestration/render-output.utils.js +4 -0
  149. package/src/route-renderer/orchestration/route-html-finalization.service.d.ts +17 -0
  150. package/src/route-renderer/orchestration/route-html-finalization.service.js +26 -0
  151. package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +24 -0
  152. package/src/route-renderer/orchestration/route-prepared-options.builder.js +52 -0
  153. package/src/route-renderer/orchestration/route-prepared-options.utils.d.ts +4 -0
  154. package/src/route-renderer/orchestration/route-prepared-options.utils.js +33 -0
  155. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +5 -38
  156. package/src/route-renderer/orchestration/route-render-orchestrator.js +51 -459
  157. package/src/route-renderer/orchestration/string-markup-renderer.d.ts +14 -0
  158. package/src/route-renderer/orchestration/string-markup-renderer.js +54 -0
  159. package/src/router/client/navigation-coordinator.d.ts +1 -1
  160. package/src/router/client/navigation-coordinator.js +12 -2
  161. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
  162. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
  163. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
  164. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
  165. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +32 -14
  166. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
  167. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +0 -3
  168. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -31
  169. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +0 -3
  170. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +2 -30
  171. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.d.ts +2 -0
  172. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.js +43 -0
  173. package/src/services/assets/browser-bundle.service.d.ts +3 -1
  174. package/src/services/assets/browser-bundle.service.js +17 -5
  175. package/src/services/html/html-transformer.service.d.ts +1 -1
  176. package/src/services/html/html-transformer.service.js +4 -4
  177. package/src/services/invalidation/development-invalidation.service.d.ts +10 -1
  178. package/src/services/invalidation/development-invalidation.service.js +31 -2
  179. package/src/services/module-loading/README.md +49 -0
  180. package/src/services/module-loading/app-server-module-transpiler.service.js +5 -17
  181. package/src/services/module-loading/page-module-import.service.d.ts +4 -1
  182. package/src/services/module-loading/page-module-import.service.js +61 -41
  183. package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
  184. package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
  185. package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
  186. package/src/services/module-loading/route-module-build-cache.js +38 -0
  187. package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
  188. package/src/services/module-loading/route-module-build-cache.store.js +207 -0
  189. package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
  190. package/src/services/module-loading/route-module-build-manifest.js +116 -0
  191. package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
  192. package/src/services/module-loading/route-module-dependency-hasher.js +121 -0
  193. package/src/services/module-loading/server-module-transpiler.service.js +1 -1
  194. package/src/static-site-generator/README.md +33 -0
  195. package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
  196. package/src/static-site-generator/static-build-invalidation.js +59 -0
  197. package/src/static-site-generator/static-export-context.d.ts +22 -0
  198. package/src/static-site-generator/static-export-context.js +0 -0
  199. package/src/static-site-generator/static-site-generator.d.ts +17 -20
  200. package/src/static-site-generator/static-site-generator.js +188 -85
  201. package/src/types/internal-types.d.ts +12 -9
  202. package/src/types/public-types.d.ts +87 -22
  203. package/src/utils/parse-cli-args.d.ts +1 -0
  204. package/src/utils/parse-cli-args.js +3 -0
  205. package/src/watchers/project-watcher.d.ts +14 -2
  206. package/src/watchers/project-watcher.js +50 -20
  207. package/src/build/rolldown-dev-build-adapter.d.ts +0 -82
  208. package/src/build/rolldown-dev-build-adapter.js +0 -166
  209. package/src/hmr/hmr.postcss.test.e2e.d.ts +0 -1
  210. package/src/hmr/hmr.postcss.test.e2e.js +0 -31
  211. package/src/hmr/hmr.test.e2e.d.ts +0 -1
  212. package/src/hmr/hmr.test.e2e.js +0 -43
  213. package/src/route-renderer/orchestration/ownership-planning.service.d.ts +0 -24
  214. package/src/route-renderer/orchestration/ownership-planning.service.js +0 -63
  215. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +0 -91
  216. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.js +0 -170
@@ -1,24 +1,28 @@
1
- import path from "node:path";
2
1
  import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
3
- import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
4
2
  import { appLogger } from "../../global/app-logger.js";
5
3
  import { HttpError } from "../../errors/http-error.js";
6
4
  import { createRequire } from "../../utils/locals-utils.js";
5
+ import { findWebSocketRoute } from "../abstract/ws-pattern-matcher.js";
7
6
  import { fileSystem } from "@ecopages/file-system";
8
- import { getAppBrowserBuildPlugins, setupAppRuntimePlugins } from "../../build/build-adapter.js";
7
+ import { setupAppRuntimePlugins } from "../../build/build-adapter.js";
9
8
  import { installAppRuntimeBuildExecutor } from "../../build/runtime-build-executor.js";
10
9
  import { StaticSiteGenerator } from "../../static-site-generator/static-site-generator.js";
11
10
  import { ProjectWatcher } from "../../watchers/project-watcher.js";
12
11
  import { SharedServerAdapter } from "../shared/server-adapter.js";
13
12
  import { ApiResponseBuilder } from "../shared/api-response.js";
14
13
  import { ServerStaticBuilder } from "../shared/server-static-builder.js";
15
- import {
16
- injectHmrRuntimeIntoHtmlResponse,
17
- isHtmlResponse,
18
- shouldInjectHmrHtmlResponse
19
- } from "../shared/hmr-html-response.js";
14
+ import { attachNodeHttpWebSocketUpgrades } from "../shared/node-http-websocket-upgrades.js";
15
+ import { createBunUserWebSocketLifecycle } from "../shared/bun-user-websocket-lifecycle.js";
16
+ import { createEcopagesSocket } from "../shared/websocket-lifecycle.js";
20
17
  import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
18
+ import {
19
+ disposeDevResources,
20
+ maybeInjectAdapterHmrHtmlResponse,
21
+ prepareRuntimePublicDir,
22
+ wireIntegrationHmrManagers
23
+ } from "../shared/runtime-server-lifecycle.js";
21
24
  import { ClientBridge } from "./client-bridge.js";
25
+ import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js";
22
26
  import { HmrManager } from "./hmr-manager.js";
23
27
  import { BunStaticPreviewHost } from "./static-preview-host.js";
24
28
  class BunServerAdapter extends SharedServerAdapter {
@@ -29,7 +33,59 @@ class BunServerAdapter extends SharedServerAdapter {
29
33
  hmrManager;
30
34
  initializationPromise = null;
31
35
  fullyInitialized = false;
36
+ projectWatcher = null;
37
+ adapterDisposed = false;
38
+ deferRuntimeAssetSetup;
39
+ allowPortFallback;
32
40
  previewHost;
41
+ /**
42
+ * Reference to the application-level WebSocket handlers map.
43
+ *
44
+ * @remarks
45
+ * This is a reference to the map owned by `AbstractApplicationAdapter`,
46
+ * passed in via the constructor. The Bun adapter reads from it to wire
47
+ * WebSocket upgrades for user-registered patterns.
48
+ */
49
+ websocketHandlers = /* @__PURE__ */ new Map();
50
+ registerBunRuntimePlugin(plugin) {
51
+ Bun.plugin(plugin);
52
+ }
53
+ adaptBunWebSocket(ws, kind, params, search, context) {
54
+ return createEcopagesSocket(
55
+ {
56
+ send: (data) => ws.send(data),
57
+ close: (code, reason) => ws.close(code, reason)
58
+ },
59
+ { kind, params, search, context }
60
+ );
61
+ }
62
+ /**
63
+ * Resolves the per-connection context for a Bun user connection.
64
+ *
65
+ * @remarks
66
+ * `context()` is invoked exactly once per accepted connection. Its
67
+ * resolved value is shared by all subsequent lifecycle hooks. If
68
+ * `context()` throws, the connection is closed immediately with code
69
+ * 1011 (server error) and the error is logged.
70
+ *
71
+ * @param request - The original upgrade request (best-effort reconstructed)
72
+ * @param handler - The registered handler
73
+ * @param kind - The registered route pattern
74
+ * @param params - Dynamic path parameters
75
+ * @param search - Query string parameters
76
+ * @returns The resolved per-connection context
77
+ */
78
+ async resolveBunContext(request, handler, kind, params, search) {
79
+ if (!handler.context) {
80
+ return void 0;
81
+ }
82
+ try {
83
+ return await handler.context({ request, kind, params, search });
84
+ } catch (error) {
85
+ appLogger.error(`[WS:${kind}] context() failed; closing connection.`, error);
86
+ throw error;
87
+ }
88
+ }
33
89
  /**
34
90
  * Creates a Bun server adapter with already-resolved runtime collaborators.
35
91
  *
@@ -46,18 +102,38 @@ class BunServerAdapter extends SharedServerAdapter {
46
102
  apiHandlers,
47
103
  staticRoutes,
48
104
  errorHandler,
105
+ websocketHandlers,
49
106
  options,
107
+ hostOwnsDevClient,
108
+ deferRuntimeAssetSetup,
109
+ allowPortFallback,
50
110
  hmrManager,
51
111
  bridge,
52
112
  previewHost
53
113
  }) {
54
114
  super({ appConfig, runtimeOrigin, serveOptions, options });
115
+ this.deferRuntimeAssetSetup = deferRuntimeAssetSetup === true;
116
+ this.allowPortFallback = allowPortFallback !== false;
55
117
  this.apiHandlers = apiHandlers || [];
56
118
  this.staticRoutes = staticRoutes || [];
57
119
  this.errorHandler = errorHandler;
58
120
  this.bridge = bridge;
59
121
  this.hmrManager = hmrManager;
60
122
  this.previewHost = previewHost;
123
+ this.hostOwnsDevClient = hostOwnsDevClient === true;
124
+ if (websocketHandlers) {
125
+ this.websocketHandlers = websocketHandlers;
126
+ }
127
+ }
128
+ /**
129
+ * Wires user WebSocket routes onto a Node HTTP server used by host integrations.
130
+ */
131
+ attachUserWebSocketUpgrades(server, options) {
132
+ attachNodeHttpWebSocketUpgrades(server, {
133
+ runtimeOrigin: this.runtimeOrigin,
134
+ websocketHandlers: this.websocketHandlers,
135
+ passthroughUnmatched: options?.passthroughUnmatched
136
+ });
61
137
  }
62
138
  /**
63
139
  * Returns whether adapter-level HTML responses still need HMR runtime injection.
@@ -67,25 +143,12 @@ class BunServerAdapter extends SharedServerAdapter {
67
143
  * adapter-level check exists for explicit API handlers that return HTML and
68
144
  * would otherwise bypass the route wrapper entirely.
69
145
  */
70
- shouldInjectHmrScript() {
71
- return shouldInjectHmrHtmlResponse(this.options?.watch === true, this.hmrManager);
72
- }
73
- /**
74
- * Delegates the HTML-response test to the shared response helper used by both
75
- * adapters.
76
- */
77
- isHtmlResponse(response) {
78
- return isHtmlResponse(response);
79
- }
80
- /**
81
- * Injects HMR script into HTML responses in development mode.
82
- * Ensures explicit API handlers that return HTML get auto-reload capability.
83
- */
84
146
  async maybeInjectHmrScript(response) {
85
- if (this.shouldInjectHmrScript() && this.isHtmlResponse(response)) {
86
- return injectHmrRuntimeIntoHtmlResponse(response);
87
- }
88
- return response;
147
+ return maybeInjectAdapterHmrHtmlResponse(response, {
148
+ watch: this.options?.watch === true,
149
+ hmrManager: this.hmrManager,
150
+ hostOwnsDevClient: this.hostOwnsDevClient
151
+ });
89
152
  }
90
153
  /**
91
154
  * Initializes the server adapter's core runtime components.
@@ -93,27 +156,25 @@ class BunServerAdapter extends SharedServerAdapter {
93
156
  async initialize() {
94
157
  installAppRuntimeBuildExecutor(this.appConfig);
95
158
  this.staticSiteGenerator = new StaticSiteGenerator({ appConfig: this.appConfig });
96
- await this.hmrManager.buildRuntime();
97
- this.prepareRuntimePublicDir();
159
+ if (this.options?.watch) {
160
+ await this.hmrManager.buildRuntime();
161
+ }
162
+ prepareRuntimePublicDir(this.appConfig);
98
163
  const staticBuilderOptions = {
99
164
  appConfig: this.appConfig,
100
165
  staticSiteGenerator: this.staticSiteGenerator,
101
166
  serveOptions: this.serveOptions,
102
- apiHandlers: this.apiHandlers
167
+ runtimeOrigin: this.runtimeOrigin,
168
+ apiHandlers: this.apiHandlers,
169
+ hmrManager: this.hmrManager,
170
+ onRuntimePlugin: (plugin) => {
171
+ this.registerBunRuntimePlugin(plugin);
172
+ }
103
173
  };
104
174
  this.staticBuilder = new ServerStaticBuilder(staticBuilderOptions);
105
- await this.initializeRuntimePlugins({ watch: this.options?.watch });
106
- }
107
- /**
108
- * Copies the source `public` directory into the runtime output and ensures the
109
- * HMR assets directory exists before any runtime bundles are emitted.
110
- */
111
- prepareRuntimePublicDir() {
112
- const srcPublicDir = path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.publicDir);
113
- if (fileSystem.exists(srcPublicDir)) {
114
- fileSystem.copyDir(srcPublicDir, path.join(this.appConfig.rootDir, this.appConfig.distDir));
175
+ if (!this.deferRuntimeAssetSetup) {
176
+ await this.initializeRuntimePlugins({ watch: this.options?.watch });
115
177
  }
116
- fileSystem.ensureDir(path.join(this.appConfig.absolutePaths.distDir, RESOLVED_ASSETS_DIR));
117
178
  }
118
179
  /**
119
180
  * Registers runtime plugins and propagates the final HMR manager into each
@@ -132,14 +193,10 @@ class BunServerAdapter extends SharedServerAdapter {
132
193
  runtimeOrigin: this.runtimeOrigin,
133
194
  hmrManager: this.hmrManager,
134
195
  onRuntimePlugin: (plugin) => {
135
- Bun.plugin(plugin);
196
+ this.registerBunRuntimePlugin(plugin);
136
197
  }
137
198
  });
138
- const browserBuildPlugins = getAppBrowserBuildPlugins(this.appConfig);
139
- this.hmrManager.setPlugins(browserBuildPlugins);
140
- for (const integration of this.appConfig.integrations) {
141
- integration.setHmrManager(this.hmrManager);
142
- }
199
+ wireIntegrationHmrManagers(this.appConfig, this.hmrManager);
143
200
  } catch (error) {
144
201
  appLogger.error(`Failed to initialize plugins: ${error instanceof Error ? error.message : String(error)}`);
145
202
  throw error;
@@ -173,54 +230,114 @@ class BunServerAdapter extends SharedServerAdapter {
173
230
  config: this.appConfig,
174
231
  refreshRouterRoutesCallback: this.refreshRouterRoutes.bind(this),
175
232
  hmrManager: this.hmrManager,
176
- bridge: this.bridge
233
+ bridge: this.bridge,
234
+ hostOwnsDevClient: this.hostOwnsDevClient
177
235
  });
236
+ this.projectWatcher = watcher;
178
237
  await watcher.createWatcherSubscription();
179
238
  }
180
239
  /**
181
240
  * Builds the `Bun.serve()` options for the current adapter state.
182
241
  *
183
242
  * @remarks
184
- * The HMR-enabled variant wraps the base fetch handler so one Bun server can
185
- * serve normal requests, accept HMR websocket upgrades, and expose the HMR
186
- * runtime asset without splitting responsibility across separate listeners.
243
+ * When HMR is enabled the websocket dispatcher merges HMR and user handlers,
244
+ * routing by the `kind` field embedded in socket data at upgrade time.
245
+ * This prevents user-registered websocket handlers from being silently overwritten
246
+ * by the HMR handler in development mode.
247
+ *
248
+ * User WebSocket paths are intercepted in the fetch handler via the pattern matcher.
249
+ * The upgrade happens implicitly — no manual GET route registration needed.
187
250
  */
188
251
  getServerOptions({ enableHmr = false } = {}) {
189
252
  appLogger.debug(`[BunServerAdapter] getServerOptions called with enableHmr: ${enableHmr}`);
190
253
  const serverOptions = this.buildServerSettings();
254
+ const hasUserWs = this.websocketHandlers.size > 0;
255
+ if (!enableHmr && !hasUserWs) {
256
+ return serverOptions;
257
+ }
258
+ const userLifecycle = this.createBunUserLifecycle();
191
259
  if (enableHmr) {
192
- const originalFetch = serverOptions.fetch;
193
- const hmrHandler = this.hmrManager.getWebSocketHandler();
194
- const hmrManager = this.hmrManager;
195
260
  serverOptions.development = true;
196
- serverOptions.websocket = hmrHandler;
197
- serverOptions.fetch = async function(request, _server) {
198
- const url = new URL(request.url);
199
- appLogger.debug(`[HMR] Request: ${url.pathname}`);
261
+ serverOptions.websocket = this.createHmrAwareWebSocketHandler(userLifecycle);
262
+ } else {
263
+ serverOptions.websocket = userLifecycle;
264
+ }
265
+ serverOptions.fetch = this.wrapFetchWithWebSocketUpgrades(serverOptions.fetch, {
266
+ serveHmrEndpoints: enableHmr
267
+ });
268
+ return serverOptions;
269
+ }
270
+ createBunUserLifecycle() {
271
+ return createBunUserWebSocketLifecycle({
272
+ runtimeOrigin: this.runtimeOrigin,
273
+ userHandlers: this.websocketHandlers,
274
+ resolveContext: this.resolveBunContext.bind(this),
275
+ adaptSocket: (ws, kind, params, search, context) => this.adaptBunWebSocket(ws, kind, params, search, context)
276
+ });
277
+ }
278
+ /**
279
+ * @remarks
280
+ * HMR and user sockets share one Bun `websocket` handler. Upgrade tags HMR
281
+ * connections with `kind: '__hmr__'`; everything else routes to the user lifecycle.
282
+ */
283
+ createHmrAwareWebSocketHandler(userLifecycle) {
284
+ const hmrHandler = this.hmrManager.getWebSocketHandler();
285
+ const isHmrSocket = (ws) => (ws.data?.kind ?? "__hmr__") === "__hmr__";
286
+ return {
287
+ open(ws) {
288
+ if (isHmrSocket(ws)) return void hmrHandler.open?.(ws);
289
+ userLifecycle.open(ws);
290
+ },
291
+ message(ws, msg) {
292
+ if (isHmrSocket(ws)) return void hmrHandler.message?.(ws, msg);
293
+ userLifecycle.message(ws, msg);
294
+ },
295
+ close(ws, code, reason) {
296
+ if (isHmrSocket(ws)) return void hmrHandler.close?.(ws, code, reason);
297
+ userLifecycle.close(ws, code, reason);
298
+ },
299
+ error(ws, error) {
300
+ if (isHmrSocket(ws)) return void appLogger.error("[HMR] WebSocket error:", error);
301
+ userLifecycle.error(ws, error);
302
+ }
303
+ };
304
+ }
305
+ /**
306
+ * @remarks
307
+ * When `serveHmrEndpoints` is set, `/_hmr` and `/_hmr_runtime.js` are checked
308
+ * before user websocket patterns so HMR upgrades are never captured by app routes.
309
+ * Production mode with only user handlers skips the HMR branch entirely.
310
+ */
311
+ wrapFetchWithWebSocketUpgrades(originalFetch, { serveHmrEndpoints }) {
312
+ const matchRoute = (pathname) => findWebSocketRoute(this.websocketHandlers, pathname);
313
+ const hmrManager = this.hmrManager;
314
+ return async function(request, server) {
315
+ const url = new URL(request.url);
316
+ if (serveHmrEndpoints) {
200
317
  if (url.pathname === "/_hmr") {
201
- const success = this.upgrade(request, {
202
- data: void 0
203
- });
204
- if (success) return;
205
- return new Response("WebSocket upgrade failed", { status: 400 });
318
+ const success = this.upgrade(request, { data: { kind: "__hmr__", params: {}, search: {} } });
319
+ return success ? void 0 : new Response("WebSocket upgrade failed", { status: 400 });
206
320
  }
207
321
  if (url.pathname === "/_hmr_runtime.js") {
208
- appLogger.debug(`[HMR] Serving runtime from ${hmrManager.getRuntimePath()}`);
209
322
  return new Response(fileSystem.readFileAsBuffer(hmrManager.getRuntimePath()), {
210
323
  headers: { "Content-Type": "application/javascript" }
211
324
  });
212
325
  }
213
- let response;
214
- if (originalFetch) {
215
- const res = await originalFetch.call(this, request, this);
216
- response = res instanceof Response ? res : new Response("Not Found", { status: 404 });
217
- } else {
218
- response = new Response("Not Found", { status: 404 });
219
- }
220
- return response;
221
- };
222
- }
223
- return serverOptions;
326
+ }
327
+ const wsMatch = matchRoute(url.pathname);
328
+ if (wsMatch && request.headers.get("upgrade")?.toLowerCase() === "websocket") {
329
+ const search = Object.fromEntries(url.searchParams.entries());
330
+ const success = this.upgrade(request, {
331
+ data: { kind: wsMatch.kind, params: wsMatch.params, search, upgradeUrl: request.url }
332
+ });
333
+ return success ? void 0 : new Response("WebSocket upgrade failed", { status: 400 });
334
+ }
335
+ if (!originalFetch) {
336
+ return new Response("Not Found", { status: 404 });
337
+ }
338
+ const res = await originalFetch.call(this, request, server);
339
+ return res instanceof Response ? res : new Response("Not Found", { status: 404 });
340
+ };
224
341
  }
225
342
  /**
226
343
  * Helper method to retrieve and parse the request body.
@@ -306,9 +423,9 @@ class BunServerAdapter extends SharedServerAdapter {
306
423
  hmrManager: this.hmrManager
307
424
  });
308
425
  }
309
- const buildRuntimeOrigin = this.serverInstance ? `http://${this.serverInstance.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${this.serverInstance.port || DEFAULT_ECOPAGES_PORT}` : void 0;
426
+ const buildRuntimeOrigin = resolveServeRuntimeOrigin(this.serveOptions);
310
427
  await this.staticBuilder.build(
311
- { ...options, preview: false, baseUrl: buildRuntimeOrigin },
428
+ { baseUrl: buildRuntimeOrigin, force: options?.force },
312
429
  {
313
430
  router: this.router,
314
431
  routeRendererFactory: this.routeRendererFactory,
@@ -323,7 +440,8 @@ class BunServerAdapter extends SharedServerAdapter {
323
440
  const activePreviewPort = await this.previewHost.start({
324
441
  appConfig: this.appConfig,
325
442
  hostname: String(previewHostname),
326
- port: previewPort
443
+ port: previewPort,
444
+ allowPortFallback: this.allowPortFallback
327
445
  });
328
446
  if (activePreviewPort) {
329
447
  appLogger.info(`Preview running at http://${previewHostname}:${activePreviewPort}`);
@@ -380,9 +498,32 @@ class BunServerAdapter extends SharedServerAdapter {
380
498
  getServerOptions: this.getServerOptions.bind(this),
381
499
  buildStatic: this.buildStatic.bind(this),
382
500
  completeInitialization: this.completeInitialization.bind(this),
383
- handleRequest: this.handleRequest.bind(this)
501
+ handleRequest: this.handleRequest.bind(this),
502
+ attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this),
503
+ dispose: this.dispose.bind(this)
384
504
  };
385
505
  }
506
+ /**
507
+ * Releases dev-time resources owned by the adapter.
508
+ *
509
+ * @remarks
510
+ * Safe to call multiple times. Does not stop the bound Bun server — callers
511
+ * should shut down transport through the runtime host before disposing.
512
+ */
513
+ async dispose() {
514
+ if (this.adapterDisposed) {
515
+ return;
516
+ }
517
+ this.adapterDisposed = true;
518
+ await disposeDevResources({
519
+ projectWatcher: this.projectWatcher,
520
+ appConfig: this.appConfig,
521
+ hmrManager: this.hmrManager,
522
+ bridge: this.bridge,
523
+ previewHost: this.previewHost
524
+ });
525
+ this.projectWatcher = null;
526
+ }
386
527
  /**
387
528
  * Handles HTTP requests by passing them securely to the shared core router adapter.
388
529
  *
@@ -434,6 +575,7 @@ async function createBunServerAdapter(params) {
434
575
  const runtimeOrigin = params.runtimeOrigin ?? resolveServeRuntimeOrigin(params.serveOptions);
435
576
  const bridge = params.bridge ?? new ClientBridge();
436
577
  const hmrManager = params.hmrManager ?? new HmrManager({ appConfig: params.appConfig, bridge });
578
+ setAppDevClientBridge(params.appConfig, bridge);
437
579
  const previewHost = params.previewHost ?? new BunStaticPreviewHost();
438
580
  const adapter = new BunServerAdapter({
439
581
  ...params,
@@ -1,5 +1,6 @@
1
1
  import { StaticContentServer } from "../../dev/sc-server.js";
2
2
  import { appLogger } from "../../global/app-logger.js";
3
+ import { PortManager } from "../shared/port-manager.js";
3
4
  class BunStaticPreviewHost {
4
5
  constructor(previewServerFactory = StaticContentServer, logger = appLogger) {
5
6
  this.previewServerFactory = previewServerFactory;
@@ -10,30 +11,34 @@ class BunStaticPreviewHost {
10
11
  previewServer = null;
11
12
  async start(options) {
12
13
  await this.stop();
13
- await new Promise((resolve) => setTimeout(resolve, 100));
14
- for (let attempt = 0; attempt < 20; attempt += 1) {
15
- try {
16
- this.previewServer = this.previewServerFactory.createServer({
14
+ let previewServer = null;
15
+ const portManager = new PortManager({
16
+ startOnPort: async (port) => {
17
+ const candidate = this.previewServerFactory.createServer({
17
18
  appConfig: options.appConfig,
18
- options: { port: options.port }
19
+ options: { port }
19
20
  });
20
- const previewPort = this.previewServer.server?.port;
21
- if (previewPort) {
22
- return previewPort;
21
+ const boundPort = candidate.server?.port ?? null;
22
+ if (!boundPort) {
23
+ candidate.stop();
24
+ return null;
23
25
  }
24
- break;
25
- } catch (error) {
26
- const errorMessage = error instanceof Error ? error.message : String(error);
27
- const errorCode = typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
28
- const isPortReleaseRace = errorCode === "EADDRINUSE" || errorMessage.includes("EADDRINUSE");
29
- if (!isPortReleaseRace || attempt === 19) {
30
- throw error;
31
- }
32
- await new Promise((resolve) => setTimeout(resolve, 100));
33
- }
26
+ previewServer = candidate;
27
+ return boundPort;
28
+ },
29
+ warn: (message) => appLogger.warn(message)
30
+ });
31
+ const previewPort = await portManager.bind({
32
+ preferredPort: options.port,
33
+ allowPortFallback: options.allowPortFallback === true
34
+ });
35
+ if (!previewPort) {
36
+ this.previewServer = null;
37
+ this.logger.error("Failed to start preview server");
38
+ return null;
34
39
  }
35
- this.logger.error("Failed to start preview server");
36
- return null;
40
+ this.previewServer = previewServer;
41
+ return previewPort;
37
42
  }
38
43
  async stop() {
39
44
  this.previewServer?.stop();
@@ -2,8 +2,20 @@ import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
2
  import { AbstractApplicationAdapter } from './abstract/application-adapter.js';
3
3
  import type { ApplicationAdapterOptions } from './abstract/application-adapter.js';
4
4
  import { SharedApplicationAdapter } from './shared/application-adapter.js';
5
+ export type EcopagesRuntimeAdapter = 'auto' | 'node' | 'bun';
5
6
  export interface EcopagesAppOptions extends ApplicationAdapterOptions {
6
7
  appConfig: EcoPagesAppConfig;
8
+ /**
9
+ * Selects the runtime adapter used by the universal `createApp()` entrypoint.
10
+ *
11
+ * @default 'auto'
12
+ *
13
+ * @remarks
14
+ * This is separate from `runtime`, which configures embedded host behavior.
15
+ * Use `auto` to select Bun when the current process exposes `globalThis.Bun`
16
+ * and Node otherwise.
17
+ */
18
+ adapter?: EcopagesRuntimeAdapter;
7
19
  serverOptions?: Record<string, any>;
8
20
  }
9
21
  export type UniversalEcopagesApp = AbstractApplicationAdapter<EcopagesAppOptions, unknown, Request>;
@@ -17,4 +29,5 @@ export declare class EcopagesApp extends SharedApplicationAdapter<EcopagesAppOpt
17
29
  protected initializeServerAdapter(): Promise<UniversalEcopagesApp>;
18
30
  start(): Promise<unknown>;
19
31
  fetch(request: Request): Promise<Response>;
32
+ stop(force?: boolean): Promise<void>;
20
33
  }
@@ -3,8 +3,9 @@ import { SharedApplicationAdapter } from "./shared/application-adapter.js";
3
3
  import { createApp as createBunApp } from "./bun/create-app.js";
4
4
  import { createNodeApp } from "./node/create-app.js";
5
5
  async function createRuntimeApp(options) {
6
+ const adapter = options.adapter ?? "auto";
6
7
  const bun = globalThis.Bun;
7
- if (bun) {
8
+ if (adapter === "bun" || adapter === "auto" && bun) {
8
9
  return await createBunApp(options);
9
10
  }
10
11
  return await createNodeApp(options);
@@ -59,6 +60,14 @@ class EcopagesApp extends SharedApplicationAdapter {
59
60
  }
60
61
  return candidate.fetch(request);
61
62
  }
63
+ async stop(force = true) {
64
+ const runtimeApp = this.runtimeApp ?? (this.runtimeAppPromise ? await this.runtimeAppPromise : null);
65
+ if (runtimeApp) {
66
+ await runtimeApp.stop(force);
67
+ }
68
+ this.runtimeApp = null;
69
+ this.runtimeAppPromise = null;
70
+ }
62
71
  }
63
72
  export {
64
73
  EcopagesApp,
@@ -7,6 +7,7 @@ export declare class NodeEcopagesApp extends SharedApplicationAdapter<EcopagesAp
7
7
  serverAdapter: NodeServerAdapterResult | undefined;
8
8
  private server;
9
9
  private runtimeOrigin;
10
+ private stopped;
10
11
  private readonly runtimeHost;
11
12
  constructor(options: EcopagesAppOptions, dependencies: {
12
13
  runtimeHost: RuntimeHost<NodeServerInstance, {
@@ -19,6 +20,9 @@ export declare class NodeEcopagesApp extends SharedApplicationAdapter<EcopagesAp
19
20
  protected initializeServerAdapter(): Promise<NodeServerAdapterResult>;
20
21
  start(): Promise<NodeServerInstance | void>;
21
22
  fetch(request: Request): Promise<Response>;
23
+ attachWebSocketUpgrades(httpServer: import('node:http').Server, options?: {
24
+ passthroughUnmatched?: boolean;
25
+ }): Promise<void>;
22
26
  }
23
27
  export declare function createNodeApp(options: EcopagesAppOptions): Promise<NodeEcopagesApp>;
24
28
  export declare function createApp(options: EcopagesAppOptions): Promise<NodeEcopagesApp>;
@@ -1,13 +1,15 @@
1
1
  import { appLogger } from "../../global/app-logger.js";
2
2
  import { SharedApplicationAdapter } from "../shared/application-adapter.js";
3
- import { resolveRuntimeBinding } from "../shared/runtime-app-bootstrap.js";
3
+ import { resolveRuntimeBinding, resolveStaticRuntimeMode } from "../shared/runtime-app-bootstrap.js";
4
4
  import { createNodeServerAdapter } from "./server-adapter.js";
5
5
  import { NodeHttpRequestBridge } from "./http-request-bridge.js";
6
6
  import { NodeRuntimeHost } from "./runtime-host.js";
7
+ import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
7
8
  class NodeEcopagesApp extends SharedApplicationAdapter {
8
9
  serverAdapter;
9
10
  server = null;
10
11
  runtimeOrigin = "";
12
+ stopped = false;
11
13
  runtimeHost;
12
14
  constructor(options, dependencies) {
13
15
  super(options);
@@ -17,12 +19,18 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
17
19
  return createNodeServerAdapter(params);
18
20
  }
19
21
  async stop(force = true) {
20
- if (!this.server) {
22
+ if (this.stopped) {
21
23
  return;
22
24
  }
23
- const activeServer = this.server;
24
- this.server = null;
25
- await this.runtimeHost.stop(activeServer, { force });
25
+ if (this.server) {
26
+ const activeServer = this.server;
27
+ this.server = null;
28
+ await this.runtimeHost.stop(activeServer, { force });
29
+ }
30
+ if (this.serverAdapter) {
31
+ await this.serverAdapter.dispose();
32
+ }
33
+ this.stopped = true;
26
34
  }
27
35
  async initializeServerAdapter() {
28
36
  const binding = resolveRuntimeBinding({
@@ -36,22 +44,32 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
36
44
  apiHandlers: this.apiHandlers,
37
45
  staticRoutes: this.staticRoutes,
38
46
  errorHandler: this.errorHandler,
47
+ websocketHandlers: this.websocketHandlers.size > 0 ? this.websocketHandlers : void 0,
39
48
  options: { watch: binding.watch },
40
- serveOptions: binding.serveOptions
49
+ serveOptions: binding.serveOptions,
50
+ hostOwnsDevClient: hostOwnsDevClient(this.runtimeOptions),
51
+ deferRuntimeAssetSetup: resolveStaticRuntimeMode({
52
+ appConfig: this.appConfig,
53
+ cliArgs: this.cliArgs
54
+ }).canBuildWithoutRuntimeServer,
55
+ allowPortFallback: binding.allowPortFallback
41
56
  });
42
57
  }
43
58
  async start() {
59
+ if (this.stopped) {
60
+ this.serverAdapter = void 0;
61
+ this.stopped = false;
62
+ }
44
63
  if (!this.serverAdapter) {
45
64
  this.serverAdapter = await this.initializeServerAdapter();
46
65
  }
47
66
  if (this.server) {
48
67
  return this.server;
49
68
  }
50
- const { build, preview } = this.cliArgs;
69
+ const { build, preview, force } = this.cliArgs;
51
70
  if (build || preview) {
52
71
  appLogger.debugTime("Building static pages");
53
- await this.serverAdapter.buildStatic({ preview });
54
- await this.stop(true);
72
+ await this.serverAdapter.buildStatic({ preview, force });
55
73
  appLogger.debugTimeEnd("Building static pages");
56
74
  if (build) {
57
75
  process.exit(0);
@@ -76,6 +94,17 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
76
94
  }
77
95
  return this.serverAdapter.handleRequest(request);
78
96
  }
97
+ async attachWebSocketUpgrades(httpServer, options) {
98
+ if (!this.serverAdapter) {
99
+ this.serverAdapter = await this.initializeServerAdapter();
100
+ }
101
+ if (!this.server) {
102
+ this.server = httpServer;
103
+ await this.serverAdapter.completeInitialization(httpServer);
104
+ return;
105
+ }
106
+ this.serverAdapter.attachUserWebSocketUpgrades(httpServer, options);
107
+ }
79
108
  }
80
109
  async function createNodeApp(options) {
81
110
  return new NodeEcopagesApp(options, {
@@ -23,4 +23,5 @@ export declare class NodeClientBridge implements IClientBridge {
23
23
  * Call this when the dev server is shutting down.
24
24
  */
25
25
  destroy(): void;
26
+ [Symbol.dispose](): void;
26
27
  }
@@ -60,6 +60,9 @@ class NodeClientBridge {
60
60
  }
61
61
  this.subscribers.clear();
62
62
  }
63
+ [Symbol.dispose]() {
64
+ this.destroy();
65
+ }
63
66
  }
64
67
  export {
65
68
  NodeClientBridge