@ecopages/core 0.2.0-alpha.39 → 0.2.0-alpha.40

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 (56) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/bun/create-app.d.ts +8 -1
  3. package/src/adapters/bun/create-app.js +52 -65
  4. package/src/adapters/bun/hmr-manager.d.ts +19 -103
  5. package/src/adapters/bun/hmr-manager.js +26 -280
  6. package/src/adapters/bun/runtime-host.d.ts +52 -0
  7. package/src/adapters/bun/runtime-host.js +56 -0
  8. package/src/adapters/bun/server-adapter.d.ts +89 -28
  9. package/src/adapters/bun/server-adapter.js +113 -61
  10. package/src/adapters/bun/static-preview-host.d.ts +28 -0
  11. package/src/adapters/bun/static-preview-host.js +45 -0
  12. package/src/adapters/node/create-app.d.ts +9 -3
  13. package/src/adapters/node/create-app.js +24 -81
  14. package/src/adapters/node/http-request-bridge.d.ts +57 -0
  15. package/src/adapters/node/http-request-bridge.js +118 -0
  16. package/src/adapters/node/node-hmr-manager.d.ts +22 -91
  17. package/src/adapters/node/node-hmr-manager.js +26 -272
  18. package/src/adapters/node/runtime-host.d.ts +57 -0
  19. package/src/adapters/node/runtime-host.js +92 -0
  20. package/src/adapters/node/server-adapter-dependencies.d.ts +19 -0
  21. package/src/adapters/node/server-adapter-dependencies.js +18 -0
  22. package/src/adapters/node/server-adapter.d.ts +10 -37
  23. package/src/adapters/node/server-adapter.js +55 -125
  24. package/src/adapters/node/static-preview-host.d.ts +55 -0
  25. package/src/adapters/node/static-preview-host.js +68 -0
  26. package/src/adapters/shared/runtime-app-bootstrap.d.ts +26 -0
  27. package/src/adapters/shared/runtime-app-bootstrap.js +46 -0
  28. package/src/adapters/shared/runtime-host.d.ts +12 -0
  29. package/src/adapters/shared/runtime-host.js +0 -0
  30. package/src/adapters/shared/shared-hmr-manager.d.ts +59 -0
  31. package/src/adapters/shared/shared-hmr-manager.js +239 -0
  32. package/src/adapters/shared/static-preview-host.d.ts +10 -0
  33. package/src/adapters/shared/static-preview-host.js +0 -0
  34. package/src/build/build-adapter.js +12 -1
  35. package/src/build/esbuild-build-adapter.d.ts +1 -0
  36. package/src/build/esbuild-build-adapter.js +13 -0
  37. package/src/hmr/strategies/js-hmr-strategy.js +0 -4
  38. package/src/plugins/integration-plugin.d.ts +6 -1
  39. package/src/route-renderer/orchestration/integration-renderer.d.ts +32 -14
  40. package/src/route-renderer/orchestration/integration-renderer.js +80 -14
  41. package/src/route-renderer/orchestration/processed-asset-dedupe.d.ts +1 -0
  42. package/src/route-renderer/orchestration/processed-asset-dedupe.js +15 -11
  43. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +22 -8
  44. package/src/route-renderer/orchestration/route-render-orchestrator.js +59 -10
  45. package/src/services/assets/asset-processing-service/page-package.d.ts +4 -1
  46. package/src/services/assets/asset-processing-service/page-package.js +11 -5
  47. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +3 -1
  48. package/src/services/html/html-rewriter-provider.service.d.ts +3 -0
  49. package/src/services/html/html-transformer.service.d.ts +10 -1
  50. package/src/services/html/html-transformer.service.js +80 -9
  51. package/src/services/module-loading/page-module-import.service.js +2 -2
  52. package/src/types/public-types.d.ts +24 -7
  53. package/src/adapters/bun/server-lifecycle.d.ts +0 -63
  54. package/src/adapters/bun/server-lifecycle.js +0 -92
  55. package/src/adapters/shared/runtime-bootstrap.d.ts +0 -38
  56. package/src/adapters/shared/runtime-bootstrap.js +0 -43
@@ -0,0 +1,92 @@
1
+ import { createServer } from "node:http";
2
+ import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
3
+ import { appLogger } from "../../global/app-logger.js";
4
+ import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
5
+ import { isNodeClientAbortError, NodeHttpRequestBridge } from "./http-request-bridge.js";
6
+ class NodeRuntimeHost {
7
+ /**
8
+ * Creates a Node runtime host with injectable request bridging and server
9
+ * creation seams for tests and alternate hosts.
10
+ */
11
+ constructor(requestBridge, serverFactory = createServer) {
12
+ this.requestBridge = requestBridge;
13
+ this.serverFactory = serverFactory;
14
+ }
15
+ requestBridge;
16
+ serverFactory;
17
+ /**
18
+ * Starts the Node HTTP server and wires each request through the shared Web
19
+ * request pipeline.
20
+ *
21
+ * @remarks
22
+ * Client disconnects are treated as normal aborts and do not trigger adapter
23
+ * error logging or the runtime host's `onError` callback.
24
+ */
25
+ async start(options) {
26
+ const hostname = String(options.serveOptions.hostname ?? DEFAULT_ECOPAGES_HOSTNAME);
27
+ const port = Number(options.serveOptions.port ?? DEFAULT_ECOPAGES_PORT);
28
+ const runtimeOrigin = resolveServeRuntimeOrigin({ hostname, port });
29
+ const server = this.serverFactory(async (req, res) => {
30
+ try {
31
+ const webRequest = this.requestBridge.createWebRequest(req, runtimeOrigin);
32
+ const response = await options.handleRequest(webRequest);
33
+ await this.requestBridge.sendNodeResponse(res, response);
34
+ } catch (error) {
35
+ if (isNodeClientAbortError(error)) {
36
+ return;
37
+ }
38
+ appLogger.error("Node server adapter request failed", error);
39
+ res.statusCode = 500;
40
+ res.end("Internal Server Error");
41
+ await options.onError(error instanceof Error ? error : new Error(String(error)));
42
+ }
43
+ });
44
+ await new Promise((resolve) => {
45
+ server.listen(port, hostname, () => resolve());
46
+ });
47
+ return server;
48
+ }
49
+ /**
50
+ * Stops the Node HTTP server and, by default, force-closes any remaining open
51
+ * connections.
52
+ */
53
+ async stop(server, options) {
54
+ await new Promise((resolve, reject) => {
55
+ server.close((error) => {
56
+ if (error) {
57
+ reject(error);
58
+ return;
59
+ }
60
+ resolve();
61
+ });
62
+ if (options?.force ?? true) {
63
+ server.closeAllConnections();
64
+ }
65
+ });
66
+ }
67
+ /**
68
+ * Resolves the public runtime origin from the bound Node listener.
69
+ *
70
+ * @remarks
71
+ * The host preserves the configured hostname rather than echoing the raw socket
72
+ * address because users care about the requested host contract, not the local
73
+ * bind interface that Node chose internally.
74
+ */
75
+ getOrigin(server, fallbackServeOptions) {
76
+ const address = server.address();
77
+ const fallbackHostname = fallbackServeOptions.hostname ?? DEFAULT_ECOPAGES_HOSTNAME;
78
+ if (address && typeof address === "object") {
79
+ return resolveServeRuntimeOrigin({
80
+ hostname: fallbackHostname,
81
+ port: address.port
82
+ });
83
+ }
84
+ return resolveServeRuntimeOrigin({
85
+ hostname: fallbackHostname,
86
+ port: fallbackServeOptions.port
87
+ });
88
+ }
89
+ }
90
+ export {
91
+ NodeRuntimeHost
92
+ };
@@ -0,0 +1,19 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
3
+ import { NodeClientBridge } from './node-client-bridge.js';
4
+ import { NodeHmrManager } from './node-hmr-manager.js';
5
+ export interface NodeServerDevRuntime {
6
+ websocketServer: WebSocketServer;
7
+ bridge: NodeClientBridge;
8
+ hmrManager: NodeHmrManager;
9
+ }
10
+ export interface NodeServerDevRuntimeFactory {
11
+ create(options: {
12
+ appConfig: EcoPagesAppConfig;
13
+ }): NodeServerDevRuntime;
14
+ }
15
+ export declare class DefaultNodeServerDevRuntimeFactory implements NodeServerDevRuntimeFactory {
16
+ create(options: {
17
+ appConfig: EcoPagesAppConfig;
18
+ }): NodeServerDevRuntime;
19
+ }
@@ -0,0 +1,18 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { NodeClientBridge } from "./node-client-bridge.js";
3
+ import { NodeHmrManager } from "./node-hmr-manager.js";
4
+ class DefaultNodeServerDevRuntimeFactory {
5
+ create(options) {
6
+ const websocketServer = new WebSocketServer({ noServer: true });
7
+ const bridge = new NodeClientBridge();
8
+ const hmrManager = new NodeHmrManager({ appConfig: options.appConfig, bridge });
9
+ return {
10
+ websocketServer,
11
+ bridge,
12
+ hmrManager
13
+ };
14
+ }
15
+ }
16
+ export {
17
+ DefaultNodeServerDevRuntimeFactory
18
+ };
@@ -3,6 +3,9 @@ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
3
3
  import type { ApiHandler, ErrorHandler, StaticRoute } 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
+ import { NodeHttpRequestBridge } from './http-request-bridge.js';
7
+ import type { StaticPreviewHost } from '../shared/static-preview-host.js';
8
+ import { type NodeServerDevRuntimeFactory } from './server-adapter-dependencies.js';
6
9
  export type NodeServerInstance = NodeHttpServer;
7
10
  export type NodeServeAdapterServerOptions = {
8
11
  port?: number;
@@ -19,6 +22,9 @@ export interface NodeServerAdapterParams {
19
22
  options?: {
20
23
  watch?: boolean;
21
24
  };
25
+ previewHost?: StaticPreviewHost;
26
+ requestBridge?: NodeHttpRequestBridge;
27
+ devRuntimeFactory?: NodeServerDevRuntimeFactory;
22
28
  }
23
29
  export interface NodeServerAdapterResult extends ServerAdapterResult {
24
30
  completeInitialization: (server: NodeServerInstance) => Promise<void>;
@@ -50,9 +56,11 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
50
56
  private apiHandlers;
51
57
  private staticRoutes;
52
58
  private errorHandler?;
53
- private previewServer;
54
59
  private bridge;
55
60
  private hmrManager;
61
+ private readonly previewHost;
62
+ private readonly requestBridge;
63
+ private readonly devRuntimeFactory;
56
64
  private shouldInjectHmrScript;
57
65
  private isHtmlResponse;
58
66
  private maybeInjectHmrScript;
@@ -71,46 +79,11 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
71
79
  * processors during their `setup()` calls.
72
80
  */
73
81
  initialize(): Promise<void>;
82
+ private prepareRuntimePublicDir;
74
83
  getServerOptions(): NodeServeAdapterServerOptions;
75
84
  buildStatic(options?: {
76
85
  preview?: boolean;
77
86
  }): Promise<void>;
78
- /**
79
- * Converts a Node.js `IncomingMessage` into a Web API `Request`.
80
- *
81
- * Multi-value headers (e.g. `set-cookie`) are appended individually so no
82
- * value is silently dropped.
83
- *
84
- * For methods that carry a body (`POST`, `PUT`, `PATCH`, …), the raw
85
- * `IncomingMessage` stream is wrapped in a `ReadableStream` rather than
86
- * cast directly to `BodyInit`. See the inline doc block inside the `if`
87
- * branch for the rationale (client-abort handling).
88
- *
89
- * `duplex: 'half'` is required by the `fetch` spec when a streaming body is
90
- * provided — without it Node.js 18+ throws a `TypeError`.
91
- */
92
- private createWebRequest;
93
- /**
94
- * Writes a Web `Response` back to a Node.js `ServerResponse`.
95
- *
96
- * The entire body is buffered via `arrayBuffer()` before writing. This is
97
- * intentional for the current use-case (SSR pages and API routes), where
98
- * responses are typically small and fully materialised. Streaming responses
99
- * are not yet supported.
100
- */
101
- private sendNodeResponse;
102
- /**
103
- * Starts an ephemeral HTTP server used *only* during a static site generation
104
- * run.
105
- *
106
- * Static generation works by having the `StaticSiteGenerator` issue real HTTP
107
- * requests to a live server for each route, capturing the rendered HTML. This
108
- * approach reuses the normal request pipeline (middleware, caching, API
109
- * handlers) without any special-casing for the build path.
110
- *
111
- * The server is torn down immediately after generation completes via
112
- * `stopBuildRuntimeServer`, so it never overlaps with the actual dev/prod server.
113
- */
114
87
  private startBuildRuntimeServer;
115
88
  private getListeningServerOrigin;
116
89
  /**
@@ -1,40 +1,38 @@
1
1
  import { createServer } from "node:http";
2
+ import path from "node:path";
2
3
  import { WebSocketServer } from "ws";
4
+ import { fileSystem } from "@ecopages/file-system";
5
+ import { getAppBrowserBuildPlugins, setupAppRuntimePlugins } from "../../build/build-adapter.js";
6
+ import { installAppRuntimeBuildExecutor } from "../../build/runtime-build-executor.js";
7
+ import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
3
8
  import { appLogger } from "../../global/app-logger.js";
4
9
  import { NodeClientBridge } from "./node-client-bridge.js";
5
10
  import { NodeHmrManager } from "./node-hmr-manager.js";
11
+ import { ProjectWatcher } from "../../watchers/project-watcher.js";
6
12
  import { StaticSiteGenerator } from "../../static-site-generator/static-site-generator.js";
7
13
  import { SharedServerAdapter } from "../shared/server-adapter.js";
8
14
  import { ServerStaticBuilder } from "../shared/server-static-builder.js";
9
- import {
10
- bindSharedRuntimeHmrManager,
11
- initializeSharedRuntimePlugins,
12
- installSharedRuntimeBuildExecutor,
13
- prepareSharedRuntimePublicDir,
14
- startSharedProjectWatching
15
- } from "../shared/runtime-bootstrap.js";
16
- import { NodeStaticContentServer } from "./static-content-server.js";
17
15
  import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
18
16
  import {
19
17
  injectHmrRuntimeIntoHtmlResponse,
20
18
  isHtmlResponse,
21
19
  shouldInjectHmrHtmlResponse
22
20
  } from "../shared/hmr-html-response.js";
23
- class ClientAbortError extends Error {
24
- constructor() {
25
- super("Client closed the request");
26
- this.name = "ClientAbortError";
27
- }
28
- }
21
+ import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
22
+ import { NodeClientAbortError, NodeHttpRequestBridge } from "./http-request-bridge.js";
23
+ import { NodeStaticPreviewHost } from "./static-preview-host.js";
24
+ import { DefaultNodeServerDevRuntimeFactory } from "./server-adapter-dependencies.js";
29
25
  class NodeServerAdapter extends SharedServerAdapter {
30
26
  serverInstance = null;
31
27
  initialized = false;
32
28
  apiHandlers;
33
29
  staticRoutes;
34
30
  errorHandler;
35
- previewServer = null;
36
31
  bridge = null;
37
32
  hmrManager = null;
33
+ previewHost;
34
+ requestBridge;
35
+ devRuntimeFactory;
38
36
  shouldInjectHmrScript() {
39
37
  return shouldInjectHmrHtmlResponse(this.options?.watch === true, this.hmrManager ?? void 0);
40
38
  }
@@ -52,6 +50,9 @@ class NodeServerAdapter extends SharedServerAdapter {
52
50
  this.apiHandlers = options.apiHandlers || [];
53
51
  this.staticRoutes = options.staticRoutes || [];
54
52
  this.errorHandler = options.errorHandler;
53
+ this.previewHost = options.previewHost;
54
+ this.requestBridge = options.requestBridge;
55
+ this.devRuntimeFactory = options.devRuntimeFactory;
55
56
  }
56
57
  /**
57
58
  * Prepares the adapter for use.
@@ -67,11 +68,11 @@ class NodeServerAdapter extends SharedServerAdapter {
67
68
  * processors during their `setup()` calls.
68
69
  */
69
70
  async initialize() {
70
- installSharedRuntimeBuildExecutor(this.appConfig, {
71
+ installAppRuntimeBuildExecutor(this.appConfig, {
71
72
  development: this.options?.watch === true
72
73
  });
73
- prepareSharedRuntimePublicDir(this.appConfig);
74
- await initializeSharedRuntimePlugins({
74
+ this.prepareRuntimePublicDir();
75
+ await setupAppRuntimePlugins({
75
76
  appConfig: this.appConfig,
76
77
  runtimeOrigin: this.runtimeOrigin,
77
78
  hmrManager: this.hmrManager ?? void 0
@@ -89,6 +90,13 @@ class NodeServerAdapter extends SharedServerAdapter {
89
90
  });
90
91
  this.initialized = true;
91
92
  }
93
+ prepareRuntimePublicDir() {
94
+ const srcPublicDir = path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.publicDir);
95
+ if (fileSystem.exists(srcPublicDir)) {
96
+ fileSystem.copyDir(srcPublicDir, path.join(this.appConfig.rootDir, this.appConfig.distDir));
97
+ }
98
+ fileSystem.ensureDir(path.join(this.appConfig.absolutePaths.distDir, RESOLVED_ASSETS_DIR));
99
+ }
92
100
  getServerOptions() {
93
101
  return {
94
102
  ...this.serveOptions
@@ -115,117 +123,27 @@ class NodeServerAdapter extends SharedServerAdapter {
115
123
  if (!options?.preview) {
116
124
  return;
117
125
  }
118
- if (this.previewServer) {
119
- await this.previewServer.stop();
120
- }
121
- this.previewServer = new NodeStaticContentServer({
126
+ await this.previewHost.start({
122
127
  appConfig: this.appConfig,
123
- options: {
124
- hostname: this.serveOptions.hostname,
125
- port: Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT)
126
- }
128
+ hostname: String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME),
129
+ port: Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT)
127
130
  });
128
- await this.previewServer.start();
129
131
  const previewHostname = this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME;
130
132
  const previewPort = this.serveOptions.port || DEFAULT_ECOPAGES_PORT;
131
133
  appLogger.info(`Preview running at http://${previewHostname}:${previewPort}`);
132
134
  }
133
- /**
134
- * Converts a Node.js `IncomingMessage` into a Web API `Request`.
135
- *
136
- * Multi-value headers (e.g. `set-cookie`) are appended individually so no
137
- * value is silently dropped.
138
- *
139
- * For methods that carry a body (`POST`, `PUT`, `PATCH`, …), the raw
140
- * `IncomingMessage` stream is wrapped in a `ReadableStream` rather than
141
- * cast directly to `BodyInit`. See the inline doc block inside the `if`
142
- * branch for the rationale (client-abort handling).
143
- *
144
- * `duplex: 'half'` is required by the `fetch` spec when a streaming body is
145
- * provided — without it Node.js 18+ throws a `TypeError`.
146
- */
147
- createWebRequest(req) {
148
- const url = new URL(req.url ?? "/", this.runtimeOrigin);
149
- const headers = new Headers();
150
- for (const [key, value] of Object.entries(req.headers)) {
151
- if (Array.isArray(value)) {
152
- for (const item of value) {
153
- headers.append(key, item);
154
- }
155
- continue;
156
- }
157
- if (value !== void 0) {
158
- headers.set(key, value);
159
- }
160
- }
161
- const method = (req.method ?? "GET").toUpperCase();
162
- const requestInit = {
163
- method,
164
- headers
165
- };
166
- if (method !== "GET" && method !== "HEAD") {
167
- const body = new ReadableStream({
168
- start(controller) {
169
- req.on("data", (chunk) => controller.enqueue(chunk));
170
- req.once("end", () => controller.close());
171
- req.once("aborted", () => {
172
- controller.error(new ClientAbortError());
173
- });
174
- req.once("error", (err) => {
175
- const isClientAbort = err.code === "ECONNRESET";
176
- controller.error(isClientAbort ? new ClientAbortError() : err);
177
- });
178
- },
179
- cancel() {
180
- req.destroy();
181
- }
182
- });
183
- requestInit.body = body;
184
- requestInit.duplex = "half";
185
- }
186
- return new Request(url, requestInit);
187
- }
188
- /**
189
- * Writes a Web `Response` back to a Node.js `ServerResponse`.
190
- *
191
- * The entire body is buffered via `arrayBuffer()` before writing. This is
192
- * intentional for the current use-case (SSR pages and API routes), where
193
- * responses are typically small and fully materialised. Streaming responses
194
- * are not yet supported.
195
- */
196
- async sendNodeResponse(res, response) {
197
- res.statusCode = response.status;
198
- response.headers.forEach((value, key) => {
199
- res.setHeader(key, value);
200
- });
201
- if (!response.body) {
202
- res.end();
203
- return;
204
- }
205
- const body = Buffer.from(await response.arrayBuffer());
206
- res.end(body);
207
- }
208
- /**
209
- * Starts an ephemeral HTTP server used *only* during a static site generation
210
- * run.
211
- *
212
- * Static generation works by having the `StaticSiteGenerator` issue real HTTP
213
- * requests to a live server for each route, capturing the rendered HTML. This
214
- * approach reuses the normal request pipeline (middleware, caching, API
215
- * handlers) without any special-casing for the build path.
216
- *
217
- * The server is torn down immediately after generation completes via
218
- * `stopBuildRuntimeServer`, so it never overlaps with the actual dev/prod server.
219
- */
220
135
  async startBuildRuntimeServer() {
221
136
  const hostname = String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME);
222
137
  const port = 0;
223
138
  const server = createServer(async (req, res) => {
224
139
  try {
225
- const webRequest = this.createWebRequest(req);
140
+ const webRequest = this.requestBridge.createWebRequest(req, this.runtimeOrigin);
226
141
  const response = await this.handleRequest(webRequest);
227
- await this.sendNodeResponse(res, response);
142
+ await this.requestBridge.sendNodeResponse(res, response);
228
143
  } catch (error) {
144
+ if (error instanceof NodeClientAbortError) {
145
+ return;
146
+ }
229
147
  appLogger.error("Node static build runtime request failed", error);
230
148
  res.statusCode = 500;
231
149
  res.end("Internal Server Error");
@@ -310,7 +228,7 @@ class NodeServerAdapter extends SharedServerAdapter {
310
228
  });
311
229
  return await this.maybeInjectHmrScript(response);
312
230
  } catch (error) {
313
- if (error instanceof ClientAbortError) {
231
+ if (error instanceof NodeClientAbortError) {
314
232
  return new Response(null, { status: 499 });
315
233
  }
316
234
  throw error;
@@ -335,9 +253,10 @@ class NodeServerAdapter extends SharedServerAdapter {
335
253
  async completeInitialization(server) {
336
254
  this.serverInstance = server;
337
255
  if (this.options?.watch) {
338
- const wss = new WebSocketServer({ noServer: true });
339
- this.bridge = new NodeClientBridge();
340
- this.hmrManager = new NodeHmrManager({ appConfig: this.appConfig, bridge: this.bridge });
256
+ const devRuntime = this.devRuntimeFactory.create({ appConfig: this.appConfig });
257
+ const wss = devRuntime.websocketServer;
258
+ this.bridge = devRuntime.bridge;
259
+ this.hmrManager = devRuntime.hmrManager;
341
260
  this.hmrManager.setEnabled(true);
342
261
  await this.hmrManager.buildRuntime();
343
262
  server.on("upgrade", (req, socket, head) => {
@@ -352,10 +271,14 @@ class NodeServerAdapter extends SharedServerAdapter {
352
271
  socket.destroy();
353
272
  }
354
273
  });
355
- bindSharedRuntimeHmrManager(this.appConfig, this.hmrManager);
274
+ const browserBuildPlugins = getAppBrowserBuildPlugins(this.appConfig);
275
+ this.hmrManager.setPlugins(browserBuildPlugins);
276
+ for (const integration of this.appConfig.integrations) {
277
+ integration.setHmrManager(this.hmrManager);
278
+ }
356
279
  this.configureSharedResponseHandlers(this.staticRoutes, this.hmrManager);
357
- await startSharedProjectWatching({
358
- appConfig: this.appConfig,
280
+ const watcher = new ProjectWatcher({
281
+ config: this.appConfig,
359
282
  refreshRouterRoutesCallback: this.createSharedWatchRefreshCallback({
360
283
  staticRoutes: this.staticRoutes,
361
284
  hmrManager: this.hmrManager
@@ -363,6 +286,7 @@ class NodeServerAdapter extends SharedServerAdapter {
363
286
  hmrManager: this.hmrManager,
364
287
  bridge: this.bridge
365
288
  });
289
+ await watcher.createWatcherSubscription();
366
290
  }
367
291
  appLogger.debug("Node server adapter initialization completed", {
368
292
  apiHandlers: this.apiHandlers.length,
@@ -373,10 +297,16 @@ class NodeServerAdapter extends SharedServerAdapter {
373
297
  }
374
298
  }
375
299
  async function createNodeServerAdapter(params) {
376
- const runtimeOrigin = params.runtimeOrigin ?? `http://${params.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${params.serveOptions.port || DEFAULT_ECOPAGES_PORT}`;
300
+ const runtimeOrigin = params.runtimeOrigin ?? resolveServeRuntimeOrigin(params.serveOptions);
301
+ const previewHost = params.previewHost ?? new NodeStaticPreviewHost();
302
+ const requestBridge = params.requestBridge ?? new NodeHttpRequestBridge();
303
+ const devRuntimeFactory = params.devRuntimeFactory ?? new DefaultNodeServerDevRuntimeFactory();
377
304
  const adapter = new NodeServerAdapter({
378
305
  ...params,
379
- runtimeOrigin
306
+ runtimeOrigin,
307
+ previewHost,
308
+ requestBridge,
309
+ devRuntimeFactory
380
310
  });
381
311
  return adapter.createAdapter();
382
312
  }
@@ -0,0 +1,55 @@
1
+ import type { AddressInfo } from 'node:net';
2
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
3
+ import type { StaticPreviewHost, StaticPreviewHostStartOptions } from '../shared/static-preview-host.js';
4
+ type NodeStaticPreviewServer = {
5
+ start(): Promise<{
6
+ address(): AddressInfo | string | null;
7
+ }>;
8
+ stop(force?: boolean): Promise<void>;
9
+ };
10
+ type NodeStaticPreviewServerFactory = new (args: {
11
+ appConfig: EcoPagesAppConfig;
12
+ options?: {
13
+ hostname?: string;
14
+ port?: number;
15
+ };
16
+ }) => NodeStaticPreviewServer;
17
+ /**
18
+ * Node preview-host wrapper that manages the lifecycle of the static preview
19
+ * server used after a build.
20
+ *
21
+ * @remarks
22
+ * The host separates preview-server construction from lifecycle control so the
23
+ * Node server adapter can restart preview serving safely across repeated build
24
+ * and preview flows.
25
+ */
26
+ export declare class NodeStaticPreviewHost implements StaticPreviewHost {
27
+ private readonly previewServerFactory;
28
+ private previewServer;
29
+ private stopPromise;
30
+ /**
31
+ * Creates the preview host with an injectable preview-server constructor.
32
+ */
33
+ constructor(previewServerFactory?: NodeStaticPreviewServerFactory);
34
+ /**
35
+ * Starts the Node preview host after fully draining any previous preview server
36
+ * shutdown that may still be in flight.
37
+ *
38
+ * @remarks
39
+ * The host only publishes a new preview server after `start()` succeeds. That
40
+ * keeps failed startup attempts from replacing the last known-good server with a
41
+ * half-initialized instance.
42
+ */
43
+ start(options: StaticPreviewHostStartOptions): Promise<number | null>;
44
+ /**
45
+ * Stops the active preview server and coalesces overlapping stop requests onto
46
+ * the same shutdown promise.
47
+ *
48
+ * @remarks
49
+ * The host keeps the server reference until shutdown succeeds. If shutdown
50
+ * fails, callers can still retry `stop()` or inspect the same live instance
51
+ * instead of losing the handle during a rejected async stop.
52
+ */
53
+ stop(force?: boolean): Promise<void>;
54
+ }
55
+ export {};
@@ -0,0 +1,68 @@
1
+ import { NodeStaticContentServer } from "./static-content-server.js";
2
+ class NodeStaticPreviewHost {
3
+ /**
4
+ * Creates the preview host with an injectable preview-server constructor.
5
+ */
6
+ constructor(previewServerFactory = NodeStaticContentServer) {
7
+ this.previewServerFactory = previewServerFactory;
8
+ }
9
+ previewServerFactory;
10
+ previewServer = null;
11
+ stopPromise = null;
12
+ /**
13
+ * Starts the Node preview host after fully draining any previous preview server
14
+ * shutdown that may still be in flight.
15
+ *
16
+ * @remarks
17
+ * The host only publishes a new preview server after `start()` succeeds. That
18
+ * keeps failed startup attempts from replacing the last known-good server with a
19
+ * half-initialized instance.
20
+ */
21
+ async start(options) {
22
+ await this.stop();
23
+ const previewServer = new this.previewServerFactory({
24
+ appConfig: options.appConfig,
25
+ options: {
26
+ hostname: options.hostname,
27
+ port: options.port
28
+ }
29
+ });
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;
35
+ }
36
+ return options.port;
37
+ }
38
+ /**
39
+ * Stops the active preview server and coalesces overlapping stop requests onto
40
+ * the same shutdown promise.
41
+ *
42
+ * @remarks
43
+ * The host keeps the server reference until shutdown succeeds. If shutdown
44
+ * fails, callers can still retry `stop()` or inspect the same live instance
45
+ * instead of losing the handle during a rejected async stop.
46
+ */
47
+ async stop(force = true) {
48
+ if (this.stopPromise) {
49
+ await this.stopPromise;
50
+ return;
51
+ }
52
+ if (!this.previewServer) {
53
+ return;
54
+ }
55
+ const activePreviewServer = this.previewServer;
56
+ this.stopPromise = activePreviewServer.stop(force).then(() => {
57
+ if (this.previewServer === activePreviewServer) {
58
+ this.previewServer = null;
59
+ }
60
+ }).finally(() => {
61
+ this.stopPromise = null;
62
+ });
63
+ await this.stopPromise;
64
+ }
65
+ }
66
+ export {
67
+ NodeStaticPreviewHost
68
+ };
@@ -0,0 +1,26 @@
1
+ import type { ReturnParseCliArgs } from '../../utils/parse-cli-args.js';
2
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
3
+ export type RuntimeBinding = {
4
+ preferredPort: number;
5
+ preferredHostname: string;
6
+ runtimeOrigin: string;
7
+ serveOptions: Record<string, unknown>;
8
+ watch: boolean;
9
+ };
10
+ export type StaticRuntimeMode = {
11
+ requiresFetchRuntime: boolean;
12
+ canBuildWithoutRuntimeServer: boolean;
13
+ };
14
+ export declare function resolveServeRuntimeOrigin(serveOptions: {
15
+ hostname?: string;
16
+ port?: number | string;
17
+ }): string;
18
+ export declare function resolveRuntimeBinding(options: {
19
+ cliArgs: ReturnParseCliArgs;
20
+ serverOptions?: Record<string, unknown>;
21
+ env?: NodeJS.ProcessEnv;
22
+ }): RuntimeBinding;
23
+ export declare function resolveStaticRuntimeMode(options: {
24
+ appConfig: EcoPagesAppConfig;
25
+ cliArgs: ReturnParseCliArgs;
26
+ }): StaticRuntimeMode;