@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
@@ -1,36 +1,44 @@
1
+ import path from "node:path";
1
2
  import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
3
+ import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
2
4
  import { appLogger } from "../../global/app-logger.js";
3
5
  import { HttpError } from "../../errors/http-error.js";
4
6
  import { createRequire } from "../../utils/locals-utils.js";
5
7
  import { fileSystem } from "@ecopages/file-system";
8
+ import { getAppBrowserBuildPlugins, setupAppRuntimePlugins } from "../../build/build-adapter.js";
9
+ import { installAppRuntimeBuildExecutor } from "../../build/runtime-build-executor.js";
10
+ import { StaticSiteGenerator } from "../../static-site-generator/static-site-generator.js";
11
+ import { ProjectWatcher } from "../../watchers/project-watcher.js";
6
12
  import { SharedServerAdapter } from "../shared/server-adapter.js";
7
13
  import { ApiResponseBuilder } from "../shared/api-response.js";
8
- import { installSharedRuntimeBuildExecutor } from "../shared/runtime-bootstrap.js";
9
- import { StaticContentServer } from "../../dev/sc-server.js";
10
- import { ServerRouteHandler } from "../shared/server-route-handler.js";
11
14
  import { ServerStaticBuilder } from "../shared/server-static-builder.js";
12
15
  import {
13
16
  injectHmrRuntimeIntoHtmlResponse,
14
17
  isHtmlResponse,
15
18
  shouldInjectHmrHtmlResponse
16
19
  } from "../shared/hmr-html-response.js";
20
+ import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
17
21
  import { ClientBridge } from "./client-bridge.js";
18
22
  import { HmrManager } from "./hmr-manager.js";
19
- import { ServerLifecycle } from "./server-lifecycle.js";
23
+ import { BunStaticPreviewHost } from "./static-preview-host.js";
20
24
  class BunServerAdapter extends SharedServerAdapter {
21
25
  apiHandlers;
22
26
  staticRoutes;
23
27
  errorHandler;
24
28
  bridge;
25
- lifecycle;
26
29
  hmrManager;
27
30
  initializationPromise = null;
28
31
  fullyInitialized = false;
29
- lifecycleFactory;
30
- staticBuilderFactory;
31
- routeHandlerFactory;
32
- hmrManagerFactory;
33
- bridgeFactory;
32
+ previewHost;
33
+ /**
34
+ * Creates a Bun server adapter with already-resolved runtime collaborators.
35
+ *
36
+ * @remarks
37
+ * The public params interface keeps `hmrManager`, `bridge`, and `previewHost`
38
+ * optional so factory callers can omit them. By the time the concrete adapter
39
+ * is constructed, those collaborators are mandatory because the adapter cannot
40
+ * initialize Bun HMR or preview flows without them.
41
+ */
34
42
  constructor({
35
43
  appConfig,
36
44
  runtimeOrigin,
@@ -39,31 +47,32 @@ class BunServerAdapter extends SharedServerAdapter {
39
47
  staticRoutes,
40
48
  errorHandler,
41
49
  options,
42
- lifecycle,
43
- staticBuilderFactory,
44
- routeHandlerFactory,
45
50
  hmrManager,
46
- bridge
51
+ bridge,
52
+ previewHost
47
53
  }) {
48
54
  super({ appConfig, runtimeOrigin, serveOptions, options });
49
55
  this.apiHandlers = apiHandlers || [];
50
56
  this.staticRoutes = staticRoutes || [];
51
57
  this.errorHandler = errorHandler;
52
- this.lifecycleFactory = lifecycle;
53
- this.staticBuilderFactory = staticBuilderFactory;
54
- this.routeHandlerFactory = routeHandlerFactory;
55
- this.hmrManagerFactory = hmrManager;
56
- this.bridgeFactory = bridge;
58
+ this.bridge = bridge;
59
+ this.hmrManager = hmrManager;
60
+ this.previewHost = previewHost;
57
61
  }
58
62
  /**
59
- * Determines if HMR script should be injected.
60
- * Only injects in watch mode when HMR manager is enabled.
63
+ * Returns whether adapter-level HTML responses still need HMR runtime injection.
64
+ *
65
+ * @remarks
66
+ * Filesystem-routed pages are wrapped later in the shared route layer. This
67
+ * adapter-level check exists for explicit API handlers that return HTML and
68
+ * would otherwise bypass the route wrapper entirely.
61
69
  */
62
70
  shouldInjectHmrScript() {
63
71
  return shouldInjectHmrHtmlResponse(this.options?.watch === true, this.hmrManager);
64
72
  }
65
73
  /**
66
- * Checks if a response contains HTML content.
74
+ * Delegates the HTML-response test to the shared response helper used by both
75
+ * adapters.
67
76
  */
68
77
  isHtmlResponse(response) {
69
78
  return isHtmlResponse(response);
@@ -79,33 +88,68 @@ class BunServerAdapter extends SharedServerAdapter {
79
88
  return response;
80
89
  }
81
90
  /**
82
- * Initializes the server adapter's core components.
83
- * Delegates to ServerLifecycle for setup.
91
+ * Initializes the server adapter's core runtime components.
84
92
  */
85
93
  async initialize() {
86
- installSharedRuntimeBuildExecutor(this.appConfig, {
94
+ installAppRuntimeBuildExecutor(this.appConfig, {
87
95
  development: this.options?.watch === true
88
96
  });
89
- this.bridge = this.bridgeFactory ?? new ClientBridge();
90
- this.hmrManager = this.hmrManagerFactory ?? new HmrManager({ appConfig: this.appConfig, bridge: this.bridge });
91
- this.lifecycle = this.lifecycleFactory ?? new ServerLifecycle({
92
- appConfig: this.appConfig,
93
- runtimeOrigin: this.runtimeOrigin,
94
- hmrManager: this.hmrManager,
95
- bridge: this.bridge
96
- });
97
- this.staticSiteGenerator = await this.lifecycle.initialize();
97
+ this.staticSiteGenerator = new StaticSiteGenerator({ appConfig: this.appConfig });
98
+ await this.hmrManager.buildRuntime();
99
+ this.prepareRuntimePublicDir();
98
100
  const staticBuilderOptions = {
99
101
  appConfig: this.appConfig,
100
102
  staticSiteGenerator: this.staticSiteGenerator,
101
103
  serveOptions: this.serveOptions,
102
104
  apiHandlers: this.apiHandlers
103
105
  };
104
- this.staticBuilder = this.staticBuilderFactory ? this.staticBuilderFactory(staticBuilderOptions) : new ServerStaticBuilder(staticBuilderOptions);
105
- await this.lifecycle.initializePlugins({ watch: this.options?.watch });
106
+ this.staticBuilder = new ServerStaticBuilder(staticBuilderOptions);
107
+ await this.initializeRuntimePlugins({ watch: this.options?.watch });
108
+ }
109
+ /**
110
+ * Copies the source `public` directory into the runtime output and ensures the
111
+ * HMR assets directory exists before any runtime bundles are emitted.
112
+ */
113
+ prepareRuntimePublicDir() {
114
+ const srcPublicDir = path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.publicDir);
115
+ if (fileSystem.exists(srcPublicDir)) {
116
+ fileSystem.copyDir(srcPublicDir, path.join(this.appConfig.rootDir, this.appConfig.distDir));
117
+ }
118
+ fileSystem.ensureDir(path.join(this.appConfig.absolutePaths.distDir, RESOLVED_ASSETS_DIR));
106
119
  }
107
120
  /**
108
- * Refreshes the router routes during watch mode.
121
+ * Registers runtime plugins and propagates the final HMR manager into each
122
+ * integration.
123
+ *
124
+ * @remarks
125
+ * This is where Bun's runtime-plugin registration path meets the integration
126
+ * lifecycle. A failure here leaves the runtime partially bootstrapped, so the
127
+ * method logs the underlying error and rethrows instead of trying to limp on.
128
+ */
129
+ async initializeRuntimePlugins(options) {
130
+ try {
131
+ this.hmrManager.setEnabled(Boolean(options?.watch));
132
+ await setupAppRuntimePlugins({
133
+ appConfig: this.appConfig,
134
+ runtimeOrigin: this.runtimeOrigin,
135
+ hmrManager: this.hmrManager,
136
+ onRuntimePlugin: (plugin) => {
137
+ Bun.plugin(plugin);
138
+ }
139
+ });
140
+ const browserBuildPlugins = getAppBrowserBuildPlugins(this.appConfig);
141
+ this.hmrManager.setPlugins(browserBuildPlugins);
142
+ for (const integration of this.appConfig.integrations) {
143
+ integration.setHmrManager(this.hmrManager);
144
+ }
145
+ } catch (error) {
146
+ appLogger.error(`Failed to initialize plugins: ${error instanceof Error ? error.message : String(error)}`);
147
+ throw error;
148
+ }
149
+ }
150
+ /**
151
+ * Rebuilds the shared routing state and hot-reloads the live Bun server when a
152
+ * watched route file changes.
109
153
  */
110
154
  async refreshRouterRoutes() {
111
155
  if (!this.serverInstance || typeof this.serverInstance.reload !== "function") {
@@ -127,15 +171,21 @@ class BunServerAdapter extends SharedServerAdapter {
127
171
  })();
128
172
  }
129
173
  async watch() {
130
- await this.lifecycle.startWatching({
131
- refreshRouterRoutesCallback: this.refreshRouterRoutes.bind(this)
174
+ const watcher = new ProjectWatcher({
175
+ config: this.appConfig,
176
+ refreshRouterRoutesCallback: this.refreshRouterRoutes.bind(this),
177
+ hmrManager: this.hmrManager,
178
+ bridge: this.bridge
132
179
  });
180
+ await watcher.createWatcherSubscription();
133
181
  }
134
182
  /**
135
- * Retrieves the current server options, optionally enabling HMR.
136
- * If HMR is enabled, modifies fetch to handle WebSocket upgrades and serve HMR runtime.
137
- * Ensures original fetch logic is preserved and called for non-HMR requests.
138
- * @param options.enableHmr Whether to enable Hot Module Replacement
183
+ * Builds the `Bun.serve()` options for the current adapter state.
184
+ *
185
+ * @remarks
186
+ * The HMR-enabled variant wraps the base fetch handler so one Bun server can
187
+ * serve normal requests, accept HMR websocket upgrades, and expose the HMR
188
+ * runtime asset without splitting responsibility across separate listeners.
139
189
  */
140
190
  getServerOptions({ enableHmr = false } = {}) {
141
191
  appLogger.debug(`[BunServerAdapter] getServerOptions called with enableHmr: ${enableHmr}`);
@@ -191,8 +241,12 @@ class BunServerAdapter extends SharedServerAdapter {
191
241
  return void 0;
192
242
  }
193
243
  /**
194
- * Creates complete server configuration with request handling.
195
- * @returns Server options ready for Bun.serve()
244
+ * Composes the base Bun server settings that all runtime modes build from.
245
+ *
246
+ * @remarks
247
+ * This method centralizes the Bun-specific error boundary. It preserves the
248
+ * shared route pipeline while still allowing adapter-level custom error-handler
249
+ * execution and `HttpError` passthrough.
196
250
  */
197
251
  buildServerSettings() {
198
252
  const serverOptions = { ...this.serveOptions };
@@ -268,15 +322,15 @@ class BunServerAdapter extends SharedServerAdapter {
268
322
  }
269
323
  const previewHostname = this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME;
270
324
  const previewPort = Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT);
271
- const previewServer = StaticContentServer.createServer({
325
+ const activePreviewPort = await this.previewHost.start({
272
326
  appConfig: this.appConfig,
273
- options: { port: previewPort }
327
+ hostname: String(previewHostname),
328
+ port: previewPort
274
329
  });
275
- if (previewServer.server?.port) {
276
- appLogger.info(`Preview running at http://${previewHostname}:${previewServer.server.port}`);
330
+ if (activePreviewPort) {
331
+ appLogger.info(`Preview running at http://${previewHostname}:${activePreviewPort}`);
277
332
  return;
278
333
  }
279
- appLogger.error("Failed to start preview server");
280
334
  }
281
335
  /**
282
336
  * Initializes the server with dynamic routes after server creation.
@@ -296,7 +350,12 @@ class BunServerAdapter extends SharedServerAdapter {
296
350
  return this.initializationPromise;
297
351
  }
298
352
  /**
299
- * Performs complete server setup including routing, watchers, and HMR.
353
+ * Performs the one-time post-bind initialization path for Bun servers.
354
+ *
355
+ * @remarks
356
+ * This is intentionally split from `initialize()` because shared route handling
357
+ * and file watching need the live server instance to exist before Bun can
358
+ * reload updated route handlers in place.
300
359
  */
301
360
  async _performInitialization(server) {
302
361
  this.serverInstance = server;
@@ -367,23 +426,16 @@ class BunServerAdapter extends SharedServerAdapter {
367
426
  }
368
427
  }
369
428
  async function createBunServerAdapter(params) {
370
- const runtimeOrigin = params.runtimeOrigin ?? `http://${params.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${params.serveOptions.port || DEFAULT_ECOPAGES_PORT}`;
429
+ const runtimeOrigin = params.runtimeOrigin ?? resolveServeRuntimeOrigin(params.serveOptions);
371
430
  const bridge = params.bridge ?? new ClientBridge();
372
431
  const hmrManager = params.hmrManager ?? new HmrManager({ appConfig: params.appConfig, bridge });
373
- const lifecycle = params.lifecycle ?? new ServerLifecycle({
374
- appConfig: params.appConfig,
375
- runtimeOrigin,
376
- hmrManager,
377
- bridge
378
- });
432
+ const previewHost = params.previewHost ?? new BunStaticPreviewHost();
379
433
  const adapter = new BunServerAdapter({
380
434
  ...params,
381
435
  runtimeOrigin,
382
436
  bridge,
383
437
  hmrManager,
384
- lifecycle,
385
- staticBuilderFactory: params.staticBuilderFactory ?? ((opts) => new ServerStaticBuilder(opts)),
386
- routeHandlerFactory: params.routeHandlerFactory ?? ((p) => new ServerRouteHandler(p))
438
+ previewHost
387
439
  });
388
440
  return adapter.createAdapter();
389
441
  }
@@ -0,0 +1,28 @@
1
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
+ import type { StaticPreviewHost, StaticPreviewHostStartOptions } from '../shared/static-preview-host.js';
3
+ type BunStaticPreviewServer = {
4
+ server: {
5
+ port?: number;
6
+ } | null;
7
+ stop(): void;
8
+ };
9
+ type BunStaticPreviewServerFactory = {
10
+ createServer(args: {
11
+ appConfig: EcoPagesAppConfig;
12
+ options: {
13
+ port: number;
14
+ };
15
+ }): BunStaticPreviewServer;
16
+ };
17
+ type BunStaticPreviewLogger = {
18
+ error(message: string): unknown;
19
+ };
20
+ export declare class BunStaticPreviewHost implements StaticPreviewHost {
21
+ private readonly previewServerFactory;
22
+ private readonly logger;
23
+ private previewServer;
24
+ constructor(previewServerFactory?: BunStaticPreviewServerFactory, logger?: BunStaticPreviewLogger);
25
+ start(options: StaticPreviewHostStartOptions): Promise<number | null>;
26
+ stop(): Promise<void>;
27
+ }
28
+ export {};
@@ -0,0 +1,45 @@
1
+ import { StaticContentServer } from "../../dev/sc-server.js";
2
+ import { appLogger } from "../../global/app-logger.js";
3
+ class BunStaticPreviewHost {
4
+ constructor(previewServerFactory = StaticContentServer, logger = appLogger) {
5
+ this.previewServerFactory = previewServerFactory;
6
+ this.logger = logger;
7
+ }
8
+ previewServerFactory;
9
+ logger;
10
+ previewServer = null;
11
+ async start(options) {
12
+ 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({
17
+ appConfig: options.appConfig,
18
+ options: { port: options.port }
19
+ });
20
+ const previewPort = this.previewServer.server?.port;
21
+ if (previewPort) {
22
+ return previewPort;
23
+ }
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
+ }
34
+ }
35
+ this.logger.error("Failed to start preview server");
36
+ return null;
37
+ }
38
+ async stop() {
39
+ this.previewServer?.stop();
40
+ this.previewServer = null;
41
+ }
42
+ }
43
+ export {
44
+ BunStaticPreviewHost
45
+ };
@@ -1,17 +1,23 @@
1
- import { type Server as NodeServerInstance } from 'node:http';
2
1
  import { SharedApplicationAdapter } from '../shared/application-adapter.js';
2
+ import type { RuntimeHost } from '../shared/runtime-host.js';
3
3
  import type { EcopagesAppOptions } from '../create-app.js';
4
4
  import { type NodeServerAdapterResult, createNodeServerAdapter } from './server-adapter.js';
5
+ import type { NodeServerInstance } from './server-adapter.js';
5
6
  export declare class NodeEcopagesApp extends SharedApplicationAdapter<EcopagesAppOptions, NodeServerInstance, Request> {
6
7
  serverAdapter: NodeServerAdapterResult | undefined;
7
8
  private server;
8
9
  private runtimeOrigin;
10
+ private readonly runtimeHost;
11
+ constructor(options: EcopagesAppOptions, dependencies: {
12
+ runtimeHost: RuntimeHost<NodeServerInstance, {
13
+ port?: number;
14
+ hostname?: string;
15
+ }>;
16
+ });
9
17
  protected createServerAdapter(params: Parameters<typeof createNodeServerAdapter>[0]): Promise<NodeServerAdapterResult>;
10
18
  stop(force?: boolean): Promise<void>;
11
19
  protected initializeServerAdapter(): Promise<NodeServerAdapterResult>;
12
20
  start(): Promise<NodeServerInstance | void>;
13
- private createWebRequest;
14
- private sendNodeResponse;
15
21
  fetch(request: Request): Promise<Response>;
16
22
  }
17
23
  export declare function createNodeApp(options: EcopagesAppOptions): Promise<NodeEcopagesApp>;
@@ -1,13 +1,18 @@
1
- import { createServer } from "node:http";
2
- import { Readable } from "node:stream";
3
- import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
4
1
  import { appLogger } from "../../global/app-logger.js";
5
2
  import { SharedApplicationAdapter } from "../shared/application-adapter.js";
3
+ import { resolveRuntimeBinding } from "../shared/runtime-app-bootstrap.js";
6
4
  import { createNodeServerAdapter } from "./server-adapter.js";
5
+ import { NodeHttpRequestBridge } from "./http-request-bridge.js";
6
+ import { NodeRuntimeHost } from "./runtime-host.js";
7
7
  class NodeEcopagesApp extends SharedApplicationAdapter {
8
8
  serverAdapter;
9
9
  server = null;
10
10
  runtimeOrigin = "";
11
+ runtimeHost;
12
+ constructor(options, dependencies) {
13
+ super(options);
14
+ this.runtimeHost = dependencies.runtimeHost;
15
+ }
11
16
  createServerAdapter(params) {
12
17
  return createNodeServerAdapter(params);
13
18
  }
@@ -17,39 +22,22 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
17
22
  }
18
23
  const activeServer = this.server;
19
24
  this.server = null;
20
- await new Promise((resolve, reject) => {
21
- activeServer.close((error) => {
22
- if (error) {
23
- reject(error);
24
- return;
25
- }
26
- resolve();
27
- });
28
- if (force) {
29
- activeServer.closeAllConnections();
30
- }
31
- });
25
+ await this.runtimeHost.stop(activeServer, { force });
32
26
  }
33
27
  async initializeServerAdapter() {
34
- const { dev } = this.cliArgs;
35
- const { port: cliPort, hostname: cliHostname } = this.cliArgs;
36
- const envPort = process.env.ECOPAGES_PORT;
37
- const envHostname = process.env.ECOPAGES_HOSTNAME;
38
- const preferredPort = cliPort ?? (envPort ? Number(envPort) : void 0) ?? DEFAULT_ECOPAGES_PORT;
39
- const preferredHostname = cliHostname ?? envHostname ?? DEFAULT_ECOPAGES_HOSTNAME;
40
- this.runtimeOrigin = `http://${preferredHostname}:${preferredPort}`;
28
+ const binding = resolveRuntimeBinding({
29
+ cliArgs: this.cliArgs,
30
+ serverOptions: this.serverOptions
31
+ });
32
+ this.runtimeOrigin = binding.runtimeOrigin;
41
33
  return this.createServerAdapter({
42
34
  runtimeOrigin: this.runtimeOrigin,
43
35
  appConfig: this.appConfig,
44
36
  apiHandlers: this.apiHandlers,
45
37
  staticRoutes: this.staticRoutes,
46
38
  errorHandler: this.errorHandler,
47
- options: { watch: dev },
48
- serveOptions: {
49
- port: preferredPort,
50
- hostname: preferredHostname,
51
- ...this.serverOptions
52
- }
39
+ options: { watch: binding.watch },
40
+ serveOptions: binding.serveOptions
53
41
  });
54
42
  }
55
43
  async start() {
@@ -71,64 +59,17 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
71
59
  return;
72
60
  }
73
61
  const serveOptions = this.serverAdapter.getServerOptions();
74
- const hostname = String(serveOptions.hostname ?? DEFAULT_ECOPAGES_HOSTNAME);
75
- const port = Number(serveOptions.port ?? DEFAULT_ECOPAGES_PORT);
76
- this.runtimeOrigin = `http://${hostname}:${port}`;
77
- this.server = createServer(async (req, res) => {
78
- try {
79
- const webRequest = this.createWebRequest(req);
80
- const response = await this.serverAdapter.handleRequest(webRequest);
81
- await this.sendNodeResponse(res, response);
82
- } catch (error) {
83
- appLogger.error("Node server adapter request failed", error);
84
- res.statusCode = 500;
85
- res.end("Internal Server Error");
62
+ this.server = await this.runtimeHost.start({
63
+ serveOptions,
64
+ handleRequest: async (request) => await this.serverAdapter.handleRequest(request),
65
+ onError: async () => {
86
66
  }
87
67
  });
88
- await new Promise((resolve) => {
89
- this.server.listen(port, hostname, () => resolve());
90
- });
68
+ this.runtimeOrigin = this.runtimeHost.getOrigin(this.server, serveOptions);
91
69
  await this.serverAdapter.completeInitialization(this.server);
92
70
  appLogger.info(`Node server running at ${this.runtimeOrigin}`);
93
71
  return this.server;
94
72
  }
95
- createWebRequest(req) {
96
- const url = new URL(req.url ?? "/", this.runtimeOrigin);
97
- const headers = new Headers();
98
- for (const [key, value] of Object.entries(req.headers)) {
99
- if (Array.isArray(value)) {
100
- for (const item of value) {
101
- headers.append(key, item);
102
- }
103
- continue;
104
- }
105
- if (value !== void 0) {
106
- headers.set(key, value);
107
- }
108
- }
109
- const method = (req.method ?? "GET").toUpperCase();
110
- const requestInit = {
111
- method,
112
- headers
113
- };
114
- if (method !== "GET" && method !== "HEAD") {
115
- requestInit.body = Readable.toWeb(req);
116
- requestInit.duplex = "half";
117
- }
118
- return new Request(url, requestInit);
119
- }
120
- async sendNodeResponse(res, response) {
121
- res.statusCode = response.status;
122
- response.headers.forEach((value, key) => {
123
- res.setHeader(key, value);
124
- });
125
- if (!response.body) {
126
- res.end();
127
- return;
128
- }
129
- const body = Buffer.from(await response.arrayBuffer());
130
- res.end(body);
131
- }
132
73
  async fetch(request) {
133
74
  if (!this.serverAdapter) {
134
75
  this.serverAdapter = await this.initializeServerAdapter();
@@ -137,7 +78,9 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
137
78
  }
138
79
  }
139
80
  async function createNodeApp(options) {
140
- return new NodeEcopagesApp(options);
81
+ return new NodeEcopagesApp(options, {
82
+ runtimeHost: new NodeRuntimeHost(new NodeHttpRequestBridge())
83
+ });
141
84
  }
142
85
  async function createApp(options) {
143
86
  return createNodeApp(options);
@@ -0,0 +1,57 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ /**
3
+ * Signals that the remote client closed the HTTP exchange before the Node
4
+ * adapter finished reading or writing the body.
5
+ *
6
+ * @remarks
7
+ * This error represents a transport-level disconnect, not an application
8
+ * failure. Higher layers may safely suppress logging for this error when the
9
+ * client socket is already gone and no meaningful response can still be sent.
10
+ */
11
+ export declare class NodeClientAbortError extends Error {
12
+ /**
13
+ * Creates the canonical Node transport abort error used across request-body
14
+ * reads and response-body writes.
15
+ */
16
+ constructor();
17
+ }
18
+ /**
19
+ * Type guard for the canonical client-abort error used by the Node adapter.
20
+ *
21
+ * @remarks
22
+ * Runtime hosts use this guard before logging so only explicitly classified
23
+ * client disconnects are muted. All other failures continue through the normal
24
+ * error-reporting path.
25
+ */
26
+ export declare function isNodeClientAbortError(error: unknown): error is NodeClientAbortError;
27
+ /**
28
+ * Bridges Node's `IncomingMessage` / `ServerResponse` pair to the Web
29
+ * `Request` / `Response` contract used by the shared routing pipeline.
30
+ *
31
+ * @remarks
32
+ * This class is the Node transport boundary. It owns the translation of request
33
+ * headers and bodies into Web primitives and the reverse translation of Web
34
+ * responses back onto Node streams, including client-abort normalization.
35
+ */
36
+ export declare class NodeHttpRequestBridge {
37
+ /**
38
+ * Converts one incoming Node request into the Web `Request` shape consumed by
39
+ * the shared server adapter.
40
+ *
41
+ * @remarks
42
+ * Non-GET/HEAD requests retain streaming semantics via a `ReadableStream` so
43
+ * large request bodies do not need to be buffered before route handling begins.
44
+ */
45
+ createWebRequest(req: IncomingMessage, runtimeOrigin: string): Request;
46
+ /**
47
+ * Sends a Web `Response` through Node's `ServerResponse` without buffering the
48
+ * full body in memory first.
49
+ *
50
+ * @remarks
51
+ * Streaming responses matter for large payloads and long-lived transports such
52
+ * as server-sent events. The bridge therefore forwards the `ReadableStream`
53
+ * directly into the Node writable response instead of materializing an
54
+ * intermediate `ArrayBuffer`.
55
+ */
56
+ sendNodeResponse(res: ServerResponse, response: Response): Promise<void>;
57
+ }