@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
@@ -4,6 +4,7 @@ import { SharedHmrManager } from '../shared/shared-hmr-manager.js';
4
4
  export interface NodeHmrManagerParams {
5
5
  appConfig: EcoPagesAppConfig;
6
6
  bridge: IClientBridge;
7
+ registrationTimeoutMs?: number;
7
8
  }
8
9
  /**
9
10
  * Node development HMR manager.
@@ -28,7 +29,7 @@ export declare class NodeHmrManager extends SharedHmrManager {
28
29
  * actually differs: dependency-graph storage, missing-file tolerance, and how
29
30
  * runtime bundle failures disable HMR.
30
31
  */
31
- constructor({ appConfig, bridge }: NodeHmrManagerParams);
32
+ constructor({ appConfig, bridge, registrationTimeoutMs }: NodeHmrManagerParams);
32
33
  /**
33
34
  * Reuses the shared in-memory dependency graph when the app already has one and
34
35
  * otherwise creates the default Node development graph.
@@ -13,8 +13,8 @@ class NodeHmrManager extends SharedHmrManager {
13
13
  * actually differs: dependency-graph storage, missing-file tolerance, and how
14
14
  * runtime bundle failures disable HMR.
15
15
  */
16
- constructor({ appConfig, bridge }) {
17
- super({ appConfig, bridge });
16
+ constructor({ appConfig, bridge, registrationTimeoutMs }) {
17
+ super({ appConfig, bridge, registrationTimeoutMs });
18
18
  }
19
19
  /**
20
20
  * Reuses the shared in-memory dependency graph when the app already has one and
@@ -1,4 +1,5 @@
1
1
  import { WebSocketServer } from "ws";
2
+ import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js";
2
3
  import { NodeClientBridge } from "./node-client-bridge.js";
3
4
  import { NodeHmrManager } from "./node-hmr-manager.js";
4
5
  class DefaultNodeServerDevRuntimeFactory {
@@ -6,6 +7,7 @@ class DefaultNodeServerDevRuntimeFactory {
6
7
  const websocketServer = new WebSocketServer({ noServer: true });
7
8
  const bridge = new NodeClientBridge();
8
9
  const hmrManager = new NodeHmrManager({ appConfig: options.appConfig, bridge });
10
+ setAppDevClientBridge(options.appConfig, bridge);
9
11
  return {
10
12
  websocketServer,
11
13
  bridge,
@@ -1,6 +1,6 @@
1
1
  import { type Server as NodeHttpServer } from 'node:http';
2
2
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
3
- import type { ApiHandler, ErrorHandler, StaticRoute } from '../../types/public-types.js';
3
+ import type { ApiHandler, ErrorHandler, StaticRoute, EcopagesWebSocketHandler } from '../../types/public-types.js';
4
4
  import { SharedServerAdapter } from '../shared/server-adapter.js';
5
5
  import type { ServerAdapterResult } from '../abstract/server-adapter.js';
6
6
  import { NodeHttpRequestBridge } from './http-request-bridge.js';
@@ -19,9 +19,14 @@ export interface NodeServerAdapterParams {
19
19
  apiHandlers?: ApiHandler[];
20
20
  staticRoutes?: StaticRoute[];
21
21
  errorHandler?: ErrorHandler;
22
+ websocketHandlers?: Map<string, EcopagesWebSocketHandler<any, any>>;
23
+ delegateBrowserReloadToHost?: boolean;
24
+ hostOwnsDevClient?: boolean;
22
25
  options?: {
23
26
  watch?: boolean;
24
27
  };
28
+ deferRuntimeAssetSetup?: boolean;
29
+ allowPortFallback?: boolean;
25
30
  previewHost?: StaticPreviewHost;
26
31
  requestBridge?: NodeHttpRequestBridge;
27
32
  devRuntimeFactory?: NodeServerDevRuntimeFactory;
@@ -29,6 +34,10 @@ export interface NodeServerAdapterParams {
29
34
  export interface NodeServerAdapterResult extends ServerAdapterResult {
30
35
  completeInitialization: (server: NodeServerInstance) => Promise<void>;
31
36
  handleRequest: (request: Request) => Promise<Response>;
37
+ attachUserWebSocketUpgrades: (server: NodeServerInstance, options?: {
38
+ passthroughUnmatched?: boolean;
39
+ }) => void;
40
+ dispose: () => Promise<void>;
32
41
  }
33
42
  /**
34
43
  * Node.js HTTP server adapter for the Ecopages runtime.
@@ -58,13 +67,47 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
58
67
  private errorHandler?;
59
68
  private bridge;
60
69
  private hmrManager;
70
+ private projectWatcher;
71
+ private adapterDisposed;
72
+ private readonly deferRuntimeAssetSetup;
73
+ private readonly allowPortFallback;
61
74
  private readonly previewHost;
62
75
  private readonly requestBridge;
63
76
  private readonly devRuntimeFactory;
64
- private shouldInjectHmrScript;
65
- private isHtmlResponse;
77
+ /**
78
+ * Reference to the application-level WebSocket handlers map.
79
+ *
80
+ * @remarks
81
+ * This is a reference to the map owned by `AbstractApplicationAdapter`,
82
+ * passed in via the constructor. The Node adapter reads from it to wire
83
+ * WebSocket upgrades for user-registered patterns.
84
+ */
85
+ protected websocketHandlers: Map<string, EcopagesWebSocketHandler<any, any>>;
86
+ /**
87
+ * Wires user WebSocket routes onto a foreign Node HTTP server.
88
+ *
89
+ * Host integrations such as the Vite plugin call this so `app.websocket()`
90
+ * handlers work while HTTP is still served by the host dev server.
91
+ */
92
+ attachUserWebSocketUpgrades(server: NodeServerInstance, options?: {
93
+ passthroughUnmatched?: boolean;
94
+ }): void;
95
+ private wireUserWebSocketUpgrades;
66
96
  private maybeInjectHmrScript;
67
- constructor(options: NodeServerAdapterParams);
97
+ /**
98
+ * @remarks
99
+ * `previewHost`, `requestBridge`, and `devRuntimeFactory` are optional on the
100
+ * public {@link NodeServerAdapterParams} so factory callers can omit them, but
101
+ * they are mandatory by the time the concrete adapter is constructed —
102
+ * {@link createNodeServerAdapter} fills in Node-specific defaults first. The
103
+ * constructor signature makes that invariant explicit instead of relying on
104
+ * non-null assertions.
105
+ */
106
+ constructor(options: NodeServerAdapterParams & {
107
+ previewHost: StaticPreviewHost;
108
+ requestBridge: NodeHttpRequestBridge;
109
+ devRuntimeFactory: NodeServerDevRuntimeFactory;
110
+ });
68
111
  /**
69
112
  * Prepares the adapter for use.
70
113
  *
@@ -79,27 +122,20 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
79
122
  * processors during their `setup()` calls.
80
123
  */
81
124
  initialize(): Promise<void>;
82
- private prepareRuntimePublicDir;
83
125
  getServerOptions(): NodeServeAdapterServerOptions;
84
126
  buildStatic(options?: {
85
127
  preview?: boolean;
128
+ force?: boolean;
86
129
  }): Promise<void>;
87
- private startBuildRuntimeServer;
88
- private getListeningServerOrigin;
130
+ createAdapter(): Promise<NodeServerAdapterResult>;
89
131
  /**
90
- * Gracefully shuts down the ephemeral build runtime server.
132
+ * Releases dev-time resources owned by the adapter.
91
133
  *
92
- * `closeAllConnections()` is called *before* `close()` because `server.close()`
93
- * only stops accepting new connections it waits for existing keep-alive
94
- * connections to finish naturally, which can stall the build indefinitely.
95
- * `closeAllConnections()` force-closes any lingering sockets immediately so
96
- * the `close()` callback fires promptly.
97
- *
98
- * The `NodeClientBridge` heartbeat is also destroyed here so its `setInterval`
99
- * does not prevent the Node.js process from exiting cleanly after the build.
134
+ * @remarks
135
+ * Safe to call multiple times. Does not stop the bound HTTP server — callers
136
+ * should shut down transport through the runtime host before disposing.
100
137
  */
101
- private stopBuildRuntimeServer;
102
- createAdapter(): Promise<NodeServerAdapterResult>;
138
+ dispose(): Promise<void>;
103
139
  /**
104
140
  * Handles a single incoming Web `Request` and returns a Web `Response`.
105
141
  *
@@ -123,7 +159,7 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
123
159
  * - Shared watcher bootstrapping listens for route-level file changes and
124
160
  * refreshes the router and response handlers when pages are added or removed.
125
161
  *
126
- * WebSocket upgrade requests that do not target `/_hmr` are rejected with an
162
+ * WebSocket upgrade requests that do not match a known path are rejected with an
127
163
  * immediate socket destroy to prevent unhandled upgrade leaks.
128
164
  */
129
165
  completeInitialization(server: NodeServerInstance): Promise<void>;
@@ -1,22 +1,23 @@
1
- import { createServer } from "node:http";
2
- import path from "node:path";
3
- import { fileSystem } from "@ecopages/file-system";
4
- import { getAppBrowserBuildPlugins, setupAppRuntimePlugins } from "../../build/build-adapter.js";
1
+ import {} from "node:http";
2
+ import { setupAppRuntimePlugins } from "../../build/build-adapter.js";
5
3
  import { installAppRuntimeBuildExecutor } from "../../build/runtime-build-executor.js";
6
- import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
7
4
  import { appLogger } from "../../global/app-logger.js";
8
5
  import { NodeClientBridge } from "./node-client-bridge.js";
9
6
  import { NodeHmrManager } from "./node-hmr-manager.js";
10
7
  import { ProjectWatcher } from "../../watchers/project-watcher.js";
8
+ import {
9
+ attachNodeHttpWebSocketUpgrades
10
+ } from "../shared/node-http-websocket-upgrades.js";
11
11
  import { StaticSiteGenerator } from "../../static-site-generator/static-site-generator.js";
12
12
  import { SharedServerAdapter } from "../shared/server-adapter.js";
13
13
  import { ServerStaticBuilder } from "../shared/server-static-builder.js";
14
14
  import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
15
15
  import {
16
- injectHmrRuntimeIntoHtmlResponse,
17
- isHtmlResponse,
18
- shouldInjectHmrHtmlResponse
19
- } from "../shared/hmr-html-response.js";
16
+ maybeInjectAdapterHmrHtmlResponse,
17
+ prepareRuntimePublicDir,
18
+ wireIntegrationHmrManagers,
19
+ disposeDevResources
20
+ } from "../shared/runtime-server-lifecycle.js";
20
21
  import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
21
22
  import { NodeClientAbortError, NodeHttpRequestBridge } from "./http-request-bridge.js";
22
23
  import { NodeStaticPreviewHost } from "./static-preview-host.js";
@@ -29,29 +30,73 @@ class NodeServerAdapter extends SharedServerAdapter {
29
30
  errorHandler;
30
31
  bridge = null;
31
32
  hmrManager = null;
33
+ projectWatcher = null;
34
+ adapterDisposed = false;
35
+ deferRuntimeAssetSetup;
36
+ allowPortFallback;
32
37
  previewHost;
33
38
  requestBridge;
34
39
  devRuntimeFactory;
35
- shouldInjectHmrScript() {
36
- return shouldInjectHmrHtmlResponse(this.options?.watch === true, this.hmrManager ?? void 0);
40
+ /**
41
+ * Reference to the application-level WebSocket handlers map.
42
+ *
43
+ * @remarks
44
+ * This is a reference to the map owned by `AbstractApplicationAdapter`,
45
+ * passed in via the constructor. The Node adapter reads from it to wire
46
+ * WebSocket upgrades for user-registered patterns.
47
+ */
48
+ websocketHandlers = /* @__PURE__ */ new Map();
49
+ /**
50
+ * Wires user WebSocket routes onto a foreign Node HTTP server.
51
+ *
52
+ * Host integrations such as the Vite plugin call this so `app.websocket()`
53
+ * handlers work while HTTP is still served by the host dev server.
54
+ */
55
+ attachUserWebSocketUpgrades(server, options) {
56
+ attachNodeHttpWebSocketUpgrades(server, {
57
+ runtimeOrigin: this.runtimeOrigin,
58
+ websocketHandlers: this.websocketHandlers,
59
+ passthroughUnmatched: options?.passthroughUnmatched
60
+ });
37
61
  }
38
- isHtmlResponse(response) {
39
- return isHtmlResponse(response);
62
+ wireUserWebSocketUpgrades(server, preflight) {
63
+ attachNodeHttpWebSocketUpgrades(server, {
64
+ runtimeOrigin: this.runtimeOrigin,
65
+ websocketHandlers: this.websocketHandlers,
66
+ passthroughUnmatched: false,
67
+ preflight
68
+ });
40
69
  }
41
70
  async maybeInjectHmrScript(response) {
42
- if (this.shouldInjectHmrScript() && this.isHtmlResponse(response)) {
43
- return injectHmrRuntimeIntoHtmlResponse(response);
44
- }
45
- return response;
71
+ return maybeInjectAdapterHmrHtmlResponse(response, {
72
+ watch: this.options?.watch === true,
73
+ hmrManager: this.hmrManager ?? void 0,
74
+ hostOwnsDevClient: this.hostOwnsDevClient
75
+ });
46
76
  }
77
+ /**
78
+ * @remarks
79
+ * `previewHost`, `requestBridge`, and `devRuntimeFactory` are optional on the
80
+ * public {@link NodeServerAdapterParams} so factory callers can omit them, but
81
+ * they are mandatory by the time the concrete adapter is constructed —
82
+ * {@link createNodeServerAdapter} fills in Node-specific defaults first. The
83
+ * constructor signature makes that invariant explicit instead of relying on
84
+ * non-null assertions.
85
+ */
47
86
  constructor(options) {
48
87
  super(options);
88
+ this.deferRuntimeAssetSetup = options.deferRuntimeAssetSetup === true;
89
+ this.allowPortFallback = options.allowPortFallback !== false;
49
90
  this.apiHandlers = options.apiHandlers || [];
50
91
  this.staticRoutes = options.staticRoutes || [];
51
92
  this.errorHandler = options.errorHandler;
52
93
  this.previewHost = options.previewHost;
53
94
  this.requestBridge = options.requestBridge;
54
95
  this.devRuntimeFactory = options.devRuntimeFactory;
96
+ if (options.websocketHandlers) {
97
+ this.websocketHandlers = options.websocketHandlers;
98
+ }
99
+ this.hostOwnsDevClient = options.hostOwnsDevClient === true;
55
100
  }
56
101
  /**
57
102
  * Prepares the adapter for use.
@@ -68,12 +113,14 @@ class NodeServerAdapter extends SharedServerAdapter {
68
113
  */
69
114
  async initialize() {
70
115
  installAppRuntimeBuildExecutor(this.appConfig);
71
- this.prepareRuntimePublicDir();
72
- await setupAppRuntimePlugins({
73
- appConfig: this.appConfig,
74
- runtimeOrigin: this.runtimeOrigin,
75
- hmrManager: this.hmrManager ?? void 0
76
- });
116
+ prepareRuntimePublicDir(this.appConfig);
117
+ if (!this.deferRuntimeAssetSetup) {
118
+ await setupAppRuntimePlugins({
119
+ appConfig: this.appConfig,
120
+ runtimeOrigin: this.runtimeOrigin,
121
+ hmrManager: this.hmrManager ?? void 0
122
+ });
123
+ }
77
124
  await this.initializeSharedRouteHandling({
78
125
  staticRoutes: this.staticRoutes,
79
126
  hmrManager: this.hmrManager ?? void 0
@@ -83,17 +130,12 @@ class NodeServerAdapter extends SharedServerAdapter {
83
130
  appConfig: this.appConfig,
84
131
  staticSiteGenerator: this.staticSiteGenerator,
85
132
  serveOptions: this.serveOptions,
86
- apiHandlers: this.apiHandlers
133
+ runtimeOrigin: this.runtimeOrigin,
134
+ apiHandlers: this.apiHandlers,
135
+ hmrManager: this.hmrManager ?? void 0
87
136
  });
88
137
  this.initialized = true;
89
138
  }
90
- prepareRuntimePublicDir() {
91
- const srcPublicDir = path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.publicDir);
92
- if (fileSystem.exists(srcPublicDir)) {
93
- fileSystem.copyDir(srcPublicDir, path.join(this.appConfig.rootDir, this.appConfig.distDir));
94
- }
95
- fileSystem.ensureDir(path.join(this.appConfig.absolutePaths.distDir, RESOLVED_ASSETS_DIR));
96
- }
97
139
  getServerOptions() {
98
140
  return {
99
141
  ...this.serveOptions
@@ -103,96 +145,29 @@ class NodeServerAdapter extends SharedServerAdapter {
103
145
  if (!this.initialized) {
104
146
  await this.initialize();
105
147
  }
106
- const buildServer = await this.startBuildRuntimeServer();
107
- const buildRuntimeOrigin = this.getListeningServerOrigin(buildServer);
108
- try {
109
- await this.staticBuilder.build(
110
- { preview: false, baseUrl: buildRuntimeOrigin },
111
- {
112
- router: this.router,
113
- routeRendererFactory: this.routeRendererFactory,
114
- staticRoutes: this.staticRoutes
115
- }
116
- );
117
- } finally {
118
- await this.stopBuildRuntimeServer(buildServer);
119
- }
148
+ const baseUrl = resolveServeRuntimeOrigin(this.serveOptions);
149
+ await this.staticBuilder.build(
150
+ { baseUrl, force: options?.force },
151
+ {
152
+ router: this.router,
153
+ routeRendererFactory: this.routeRendererFactory,
154
+ staticRoutes: this.staticRoutes
155
+ }
156
+ );
120
157
  if (!options?.preview) {
121
158
  return;
122
159
  }
123
- await this.previewHost.start({
160
+ const activePreviewPort = await this.previewHost.start({
124
161
  appConfig: this.appConfig,
125
162
  hostname: String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME),
126
- port: Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT)
163
+ port: Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT),
164
+ allowPortFallback: this.allowPortFallback
127
165
  });
128
- const previewHostname = this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME;
129
- const previewPort = this.serveOptions.port || DEFAULT_ECOPAGES_PORT;
130
- appLogger.info(`Preview running at http://${previewHostname}:${previewPort}`);
131
- }
132
- async startBuildRuntimeServer() {
133
- const hostname = String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME);
134
- const port = 0;
135
- const server = createServer(async (req, res) => {
136
- try {
137
- const webRequest = this.requestBridge.createWebRequest(req, this.runtimeOrigin);
138
- const response = await this.handleRequest(webRequest);
139
- await this.requestBridge.sendNodeResponse(res, response);
140
- } catch (error) {
141
- if (error instanceof NodeClientAbortError) {
142
- return;
143
- }
144
- appLogger.error("Node static build runtime request failed", error);
145
- res.statusCode = 500;
146
- res.end("Internal Server Error");
147
- }
148
- });
149
- await new Promise((resolve, reject) => {
150
- server.once("error", reject);
151
- server.listen(port, hostname, () => {
152
- server.off("error", reject);
153
- resolve();
154
- });
155
- });
156
- this.serverInstance = server;
157
- appLogger.info(`Server running at ${this.getListeningServerOrigin(server)}`);
158
- return server;
159
- }
160
- getListeningServerOrigin(server) {
161
- const address = server.address();
162
- const hostname = String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME);
163
- if (!address || typeof address === "string") {
164
- throw new Error("Build runtime server did not expose a numeric listening port");
165
- }
166
- return `http://${hostname}:${address.port}`;
167
- }
168
- /**
169
- * Gracefully shuts down the ephemeral build runtime server.
170
- *
171
- * `closeAllConnections()` is called *before* `close()` because `server.close()`
172
- * only stops accepting new connections — it waits for existing keep-alive
173
- * connections to finish naturally, which can stall the build indefinitely.
174
- * `closeAllConnections()` force-closes any lingering sockets immediately so
175
- * the `close()` callback fires promptly.
176
- *
177
- * The `NodeClientBridge` heartbeat is also destroyed here so its `setInterval`
178
- * does not prevent the Node.js process from exiting cleanly after the build.
179
- */
180
- async stopBuildRuntimeServer(server) {
181
- await new Promise((resolve, reject) => {
182
- server.close((error) => {
183
- if (error) {
184
- reject(error);
185
- return;
186
- }
187
- resolve();
188
- });
189
- server.closeAllConnections();
190
- });
191
- if (this.serverInstance === server) {
192
- this.serverInstance = null;
166
+ if (!activePreviewPort) {
167
+ return;
193
168
  }
194
- this.bridge?.destroy();
195
- this.bridge = null;
169
+ const previewHostname = this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME;
170
+ appLogger.info(`Preview running at http://${previewHostname}:${activePreviewPort}`);
196
171
  }
197
172
  async createAdapter() {
198
173
  await this.initialize();
@@ -200,9 +175,34 @@ class NodeServerAdapter extends SharedServerAdapter {
200
175
  getServerOptions: this.getServerOptions.bind(this),
201
176
  buildStatic: this.buildStatic.bind(this),
202
177
  completeInitialization: this.completeInitialization.bind(this),
203
- handleRequest: this.handleRequest.bind(this)
178
+ handleRequest: this.handleRequest.bind(this),
179
+ attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this),
180
+ dispose: this.dispose.bind(this)
204
181
  };
205
182
  }
183
+ /**
184
+ * Releases dev-time resources owned by the adapter.
185
+ *
186
+ * @remarks
187
+ * Safe to call multiple times. Does not stop the bound HTTP server — callers
188
+ * should shut down transport through the runtime host before disposing.
189
+ */
190
+ async dispose() {
191
+ if (this.adapterDisposed) {
192
+ return;
193
+ }
194
+ this.adapterDisposed = true;
195
+ await disposeDevResources({
196
+ projectWatcher: this.projectWatcher,
197
+ appConfig: this.appConfig,
198
+ hmrManager: this.hmrManager,
199
+ bridge: this.bridge,
200
+ previewHost: this.previewHost
201
+ });
202
+ this.projectWatcher = null;
203
+ this.hmrManager = null;
204
+ this.bridge = null;
205
+ }
206
206
  /**
207
207
  * Handles a single incoming Web `Request` and returns a Web `Response`.
208
208
  *
@@ -244,11 +244,12 @@ class NodeServerAdapter extends SharedServerAdapter {
244
244
  * - Shared watcher bootstrapping listens for route-level file changes and
245
245
  * refreshes the router and response handlers when pages are added or removed.
246
246
  *
247
- * WebSocket upgrade requests that do not target `/_hmr` are rejected with an
247
+ * WebSocket upgrade requests that do not match a known path are rejected with an
248
248
  * immediate socket destroy to prevent unhandled upgrade leaks.
249
249
  */
250
250
  async completeInitialization(server) {
251
251
  this.serverInstance = server;
252
+ const hasUserWs = this.websocketHandlers.size > 0;
252
253
  if (this.options?.watch) {
253
254
  const devRuntime = this.devRuntimeFactory.create({ appConfig: this.appConfig });
254
255
  const wss = devRuntime.websocketServer;
@@ -256,23 +257,26 @@ class NodeServerAdapter extends SharedServerAdapter {
256
257
  this.hmrManager = devRuntime.hmrManager;
257
258
  this.hmrManager.setEnabled(true);
258
259
  await this.hmrManager.buildRuntime();
259
- server.on("upgrade", (req, socket, head) => {
260
+ const hmrPreflight = (req, socket, head) => {
260
261
  const url = new URL(req.url ?? "/", this.runtimeOrigin);
261
- if (url.pathname === "/_hmr") {
262
- wss.handleUpgrade(req, socket, head, (ws) => {
263
- this.bridge.subscribe(ws);
264
- ws.on("close", () => this.bridge.unsubscribe(ws));
265
- ws.on("error", (err) => appLogger.error("[HMR] WebSocket error:", err));
266
- });
267
- } else {
268
- socket.destroy();
269
- }
270
- });
271
- const browserBuildPlugins = getAppBrowserBuildPlugins(this.appConfig);
272
- this.hmrManager.setPlugins(browserBuildPlugins);
273
- for (const integration of this.appConfig.integrations) {
274
- integration.setHmrManager(this.hmrManager);
262
+ if (url.pathname !== "/_hmr") return false;
263
+ wss.handleUpgrade(req, socket, head, (ws) => {
264
+ this.bridge.subscribe(ws);
265
+ ws.on("close", () => this.bridge.unsubscribe(ws));
266
+ ws.on("error", (err) => appLogger.error("[HMR] WebSocket error:", err));
267
+ });
268
+ return true;
269
+ };
270
+ if (hasUserWs) {
271
+ this.wireUserWebSocketUpgrades(server, hmrPreflight);
272
+ } else {
273
+ server.on("upgrade", (req, socket, head) => {
274
+ if (!hmrPreflight(req, socket, head)) {
275
+ socket.destroy();
276
+ }
277
+ });
275
278
  }
279
+ wireIntegrationHmrManagers(this.appConfig, this.hmrManager);
276
280
  this.configureSharedResponseHandlers(this.staticRoutes, this.hmrManager);
277
281
  const watcher = new ProjectWatcher({
278
282
  config: this.appConfig,
@@ -281,9 +285,13 @@ class NodeServerAdapter extends SharedServerAdapter {
281
285
  hmrManager: this.hmrManager
282
286
  }),
283
287
  hmrManager: this.hmrManager,
284
- bridge: this.bridge
288
+ bridge: this.bridge,
289
+ hostOwnsDevClient: this.hostOwnsDevClient
285
290
  });
291
+ this.projectWatcher = watcher;
286
292
  await watcher.createWatcherSubscription();
293
+ } else if (hasUserWs) {
294
+ this.wireUserWebSocketUpgrades(server);
287
295
  }
288
296
  appLogger.debug("Node server adapter initialization completed", {
289
297
  apiHandlers: this.apiHandlers.length,
@@ -56,5 +56,6 @@ export declare class NodeStaticContentServer {
56
56
  * Stops the static preview server and optionally closes active connections.
57
57
  */
58
58
  stop(force?: boolean): Promise<void>;
59
+ [Symbol.asyncDispose](): Promise<void>;
59
60
  }
60
61
  export {};
@@ -161,8 +161,13 @@ class NodeStaticContentServer {
161
161
  this.server = createServer(this.handleRequest.bind(this));
162
162
  const hostname = this.options.hostname ?? DEFAULT_ECOPAGES_HOSTNAME;
163
163
  const port = this.options.port ?? DEFAULT_ECOPAGES_PORT;
164
- await new Promise((resolve) => {
165
- this.server.listen(port, hostname, () => resolve());
164
+ await new Promise((resolve, reject) => {
165
+ const activeServer = this.server;
166
+ activeServer.once("error", reject);
167
+ activeServer.listen(port, hostname, () => {
168
+ activeServer.off("error", reject);
169
+ resolve();
170
+ });
166
171
  });
167
172
  return this.server;
168
173
  }
@@ -188,6 +193,9 @@ class NodeStaticContentServer {
188
193
  }
189
194
  });
190
195
  }
196
+ async [Symbol.asyncDispose]() {
197
+ await this.stop();
198
+ }
191
199
  }
192
200
  export {
193
201
  NodeStaticContentServer
@@ -1,3 +1,5 @@
1
+ import { appLogger } from "../../global/app-logger.js";
2
+ import { PortManager } from "../shared/port-manager.js";
1
3
  import { NodeStaticContentServer } from "./static-content-server.js";
2
4
  class NodeStaticPreviewHost {
3
5
  /**
@@ -20,20 +22,33 @@ class NodeStaticPreviewHost {
20
22
  */
21
23
  async start(options) {
22
24
  await this.stop();
23
- const previewServer = new this.previewServerFactory({
24
- appConfig: options.appConfig,
25
- options: {
26
- hostname: options.hostname,
27
- port: options.port
28
- }
25
+ const portManager = new PortManager({
26
+ startOnPort: async (port) => {
27
+ const previewServer = new this.previewServerFactory({
28
+ appConfig: options.appConfig,
29
+ options: {
30
+ hostname: options.hostname,
31
+ port
32
+ }
33
+ });
34
+ const server = await previewServer.start();
35
+ this.previewServer = previewServer;
36
+ const address = server.address();
37
+ if (address && typeof address === "object") {
38
+ return address.port;
39
+ }
40
+ return port;
41
+ },
42
+ warn: (message) => appLogger.warn(message)
43
+ });
44
+ const previewPort = await portManager.bind({
45
+ preferredPort: options.port,
46
+ allowPortFallback: options.allowPortFallback === true
29
47
  });
30
- const server = await previewServer.start();
31
- this.previewServer = previewServer;
32
- const address = server.address();
33
- if (address && typeof address === "object") {
34
- return address.port;
48
+ if (!previewPort) {
49
+ this.previewServer = null;
35
50
  }
36
- return options.port;
51
+ return previewPort;
37
52
  }
38
53
  /**
39
54
  * Stops the active preview server and coalesces overlapping stop requests onto
@@ -0,0 +1,22 @@
1
+ import type { ServerWebSocket } from 'bun';
2
+ import type { EcopagesSocket, EcopagesWebSocketHandler } from '../../types/public-types.js';
3
+ export type BunUserWebSocketData = {
4
+ kind: string;
5
+ params: Record<string, string>;
6
+ search: Record<string, string>;
7
+ upgradeUrl?: string;
8
+ context?: unknown;
9
+ [key: string]: unknown;
10
+ };
11
+ export type BunUserWebSocketLifecycleDeps<TWsData extends BunUserWebSocketData> = {
12
+ runtimeOrigin: string;
13
+ userHandlers: Map<string, EcopagesWebSocketHandler<any, any>>;
14
+ resolveContext: <TContext, TParams extends Record<string, string>>(request: Request, handler: EcopagesWebSocketHandler<TContext, TParams>, kind: string, params: TParams, search: Record<string, string>) => Promise<TContext>;
15
+ adaptSocket: <TContext, TParams extends Record<string, string>>(ws: ServerWebSocket<TWsData>, kind: string, params: TParams, search: Record<string, string>, context: TContext) => EcopagesSocket<TContext, TParams>;
16
+ };
17
+ export declare function createBunUserWebSocketLifecycle<TWsData extends BunUserWebSocketData>(deps: BunUserWebSocketLifecycleDeps<TWsData>): {
18
+ open(ws: ServerWebSocket<TWsData>): void;
19
+ message(ws: ServerWebSocket<TWsData>, msg: string | Buffer): void;
20
+ close(ws: ServerWebSocket<TWsData>, code: number, reason: string): void;
21
+ error(ws: ServerWebSocket<TWsData>, error: Error): void;
22
+ };