@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.
- package/package.json +2 -2
- package/src/adapters/bun/create-app.d.ts +8 -1
- package/src/adapters/bun/create-app.js +52 -65
- package/src/adapters/bun/hmr-manager.d.ts +19 -103
- package/src/adapters/bun/hmr-manager.js +26 -280
- package/src/adapters/bun/runtime-host.d.ts +52 -0
- package/src/adapters/bun/runtime-host.js +56 -0
- package/src/adapters/bun/server-adapter.d.ts +89 -28
- package/src/adapters/bun/server-adapter.js +113 -61
- package/src/adapters/bun/static-preview-host.d.ts +28 -0
- package/src/adapters/bun/static-preview-host.js +45 -0
- package/src/adapters/node/create-app.d.ts +9 -3
- package/src/adapters/node/create-app.js +24 -81
- package/src/adapters/node/http-request-bridge.d.ts +57 -0
- package/src/adapters/node/http-request-bridge.js +118 -0
- package/src/adapters/node/node-hmr-manager.d.ts +22 -91
- package/src/adapters/node/node-hmr-manager.js +26 -272
- package/src/adapters/node/runtime-host.d.ts +57 -0
- package/src/adapters/node/runtime-host.js +92 -0
- package/src/adapters/node/server-adapter-dependencies.d.ts +19 -0
- package/src/adapters/node/server-adapter-dependencies.js +18 -0
- package/src/adapters/node/server-adapter.d.ts +10 -37
- package/src/adapters/node/server-adapter.js +55 -125
- package/src/adapters/node/static-preview-host.d.ts +55 -0
- package/src/adapters/node/static-preview-host.js +68 -0
- package/src/adapters/shared/runtime-app-bootstrap.d.ts +26 -0
- package/src/adapters/shared/runtime-app-bootstrap.js +46 -0
- package/src/adapters/shared/runtime-host.d.ts +12 -0
- package/src/adapters/shared/runtime-host.js +0 -0
- package/src/adapters/shared/shared-hmr-manager.d.ts +59 -0
- package/src/adapters/shared/shared-hmr-manager.js +239 -0
- package/src/adapters/shared/static-preview-host.d.ts +10 -0
- package/src/adapters/shared/static-preview-host.js +0 -0
- package/src/build/build-adapter.js +12 -1
- package/src/build/esbuild-build-adapter.d.ts +1 -0
- package/src/build/esbuild-build-adapter.js +13 -0
- package/src/hmr/strategies/js-hmr-strategy.js +0 -4
- package/src/plugins/integration-plugin.d.ts +6 -1
- package/src/route-renderer/orchestration/integration-renderer.d.ts +32 -14
- package/src/route-renderer/orchestration/integration-renderer.js +80 -14
- package/src/route-renderer/orchestration/processed-asset-dedupe.d.ts +1 -0
- package/src/route-renderer/orchestration/processed-asset-dedupe.js +15 -11
- package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +22 -8
- package/src/route-renderer/orchestration/route-render-orchestrator.js +59 -10
- package/src/services/assets/asset-processing-service/page-package.d.ts +4 -1
- package/src/services/assets/asset-processing-service/page-package.js +11 -5
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +3 -1
- package/src/services/html/html-rewriter-provider.service.d.ts +3 -0
- package/src/services/html/html-transformer.service.d.ts +10 -1
- package/src/services/html/html-transformer.service.js +80 -9
- package/src/services/module-loading/page-module-import.service.js +2 -2
- package/src/types/public-types.d.ts +24 -7
- package/src/adapters/bun/server-lifecycle.d.ts +0 -63
- package/src/adapters/bun/server-lifecycle.js +0 -92
- package/src/adapters/shared/runtime-bootstrap.d.ts +0 -38
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
71
|
+
installAppRuntimeBuildExecutor(this.appConfig, {
|
|
71
72
|
development: this.options?.watch === true
|
|
72
73
|
});
|
|
73
|
-
|
|
74
|
-
await
|
|
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
|
-
|
|
119
|
-
await this.previewServer.stop();
|
|
120
|
-
}
|
|
121
|
-
this.previewServer = new NodeStaticContentServer({
|
|
126
|
+
await this.previewHost.start({
|
|
122
127
|
appConfig: this.appConfig,
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
339
|
-
|
|
340
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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 ??
|
|
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;
|