@ecopages/core 0.2.0-beta.1 → 0.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +107 -3
- package/src/adapters/abstract/application-adapter.d.ts +73 -1
- package/src/adapters/abstract/application-adapter.js +89 -0
- package/src/adapters/abstract/server-adapter.d.ts +3 -0
- package/src/adapters/abstract/ws-pattern-matcher.d.ts +60 -0
- package/src/adapters/abstract/ws-pattern-matcher.js +61 -0
- package/src/adapters/bun/client-bridge.d.ts +1 -0
- package/src/adapters/bun/client-bridge.js +3 -0
- package/src/adapters/bun/create-app.d.ts +5 -3
- package/src/adapters/bun/create-app.js +44 -39
- package/src/adapters/bun/hmr-manager.d.ts +2 -1
- package/src/adapters/bun/hmr-manager.js +2 -2
- package/src/adapters/bun/server-adapter.d.ts +81 -20
- package/src/adapters/bun/server-adapter.js +220 -78
- package/src/adapters/bun/static-preview-host.js +25 -20
- package/src/adapters/create-app.d.ts +13 -0
- package/src/adapters/create-app.js +10 -1
- package/src/adapters/node/create-app.d.ts +4 -0
- package/src/adapters/node/create-app.js +38 -9
- package/src/adapters/node/node-client-bridge.d.ts +1 -0
- package/src/adapters/node/node-client-bridge.js +3 -0
- package/src/adapters/node/node-hmr-manager.d.ts +2 -1
- package/src/adapters/node/node-hmr-manager.js +2 -2
- package/src/adapters/node/server-adapter-dependencies.js +2 -0
- package/src/adapters/node/server-adapter.d.ts +55 -19
- package/src/adapters/node/server-adapter.js +140 -132
- package/src/adapters/node/static-content-server.d.ts +1 -0
- package/src/adapters/node/static-content-server.js +10 -2
- package/src/adapters/node/static-preview-host.js +27 -12
- package/src/adapters/shared/bun-user-websocket-lifecycle.d.ts +22 -0
- package/src/adapters/shared/bun-user-websocket-lifecycle.js +96 -0
- package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
- package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
- package/src/adapters/shared/hmr-html-response.d.ts +1 -1
- package/src/adapters/shared/hmr-html-response.js +4 -1
- package/src/adapters/shared/node-http-websocket-upgrades.d.ts +21 -0
- package/src/adapters/shared/node-http-websocket-upgrades.js +100 -0
- package/src/adapters/shared/port-manager.d.ts +116 -0
- package/src/adapters/shared/port-manager.js +196 -0
- package/src/adapters/shared/runtime-app-bootstrap.d.ts +2 -1
- package/src/adapters/shared/runtime-app-bootstrap.js +3 -6
- package/src/adapters/shared/runtime-server-lifecycle.d.ts +38 -0
- package/src/adapters/shared/runtime-server-lifecycle.js +39 -0
- package/src/adapters/shared/server-adapter.d.ts +2 -1
- package/src/adapters/shared/server-adapter.js +12 -4
- package/src/adapters/shared/server-route-handler.d.ts +4 -1
- package/src/adapters/shared/server-route-handler.js +5 -2
- package/src/adapters/shared/server-static-builder.d.ts +14 -29
- package/src/adapters/shared/server-static-builder.js +91 -70
- package/src/adapters/shared/shared-hmr-manager.d.ts +12 -0
- package/src/adapters/shared/shared-hmr-manager.js +64 -7
- package/src/adapters/shared/static-preview-host.d.ts +5 -0
- package/src/adapters/shared/websocket-lifecycle.d.ts +24 -0
- package/src/adapters/shared/websocket-lifecycle.js +55 -0
- package/src/build/README.md +77 -14
- package/src/build/app-build-manifest-runtime.d.ts +10 -0
- package/src/build/app-build-manifest-runtime.js +90 -0
- package/src/build/build-adapter.d.ts +17 -319
- package/src/build/build-adapter.js +28 -94
- package/src/build/build-contracts.d.ts +97 -0
- package/src/build/build-contracts.js +0 -0
- package/src/build/build-input-fingerprint.d.ts +28 -0
- package/src/build/build-input-fingerprint.js +31 -0
- package/src/build/build-profile-options.d.ts +7 -0
- package/src/build/build-profile-options.js +37 -0
- package/src/build/build-runtime.d.ts +24 -0
- package/src/build/build-runtime.js +65 -0
- package/src/build/deduping-build-executor.d.ts +28 -0
- package/src/build/deduping-build-executor.js +56 -0
- package/src/build/jsx-ownership-plugins.d.ts +24 -0
- package/src/build/jsx-ownership-plugins.js +41 -0
- package/src/build/pages-unified-graph-build.d.ts +31 -0
- package/src/build/pages-unified-graph-build.js +206 -0
- package/src/build/parallel-build-executor.d.ts +21 -0
- package/src/build/parallel-build-executor.js +52 -0
- package/src/build/production-build-cache.d.ts +20 -0
- package/src/build/production-build-cache.js +63 -0
- package/src/build/rolldown-adapter-helpers.d.ts +3 -6
- package/src/build/rolldown-adapter-helpers.js +71 -6
- package/src/build/rolldown-build-adapter.d.ts +2 -6
- package/src/build/rolldown-build-adapter.js +3 -3
- package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
- package/src/build/rolldown-build-invocation-metrics.js +43 -0
- package/src/build/rolldown-plugin-bridge.d.ts +15 -1
- package/src/build/rolldown-plugin-bridge.js +20 -5
- package/src/build/rolldown-source-transform-pass.d.ts +15 -0
- package/src/build/rolldown-source-transform-pass.js +58 -0
- package/src/build/runtime-build-executor.d.ts +8 -15
- package/src/build/runtime-build-executor.js +12 -15
- package/src/build/runtime-build-output-normalizer.d.ts +7 -0
- package/src/build/runtime-build-output-normalizer.js +94 -2
- package/src/build/server-entry-build-cache.d.ts +44 -0
- package/src/build/server-entry-build-cache.js +161 -0
- package/src/config/config-builder.d.ts +12 -7
- package/src/config/config-builder.js +12 -9
- package/src/dev/client-bridge-registry.d.ts +5 -0
- package/src/dev/client-bridge-registry.js +15 -0
- package/src/dev/dev-client-ownership.d.ts +7 -0
- package/src/dev/dev-client-ownership.js +6 -0
- package/src/dev/host-runtime.d.ts +3 -0
- package/src/dev/host-runtime.js +7 -0
- package/src/hmr/client/hmr-runtime.js +5 -2
- package/src/hmr/strategies/server-rendered-template-hmr-strategy.d.ts +17 -0
- package/src/hmr/strategies/server-rendered-template-hmr-strategy.js +29 -0
- package/src/integrations/ghtml/ghtml-renderer.d.ts +2 -6
- package/src/integrations/ghtml/ghtml-renderer.js +2 -51
- package/src/integrations/ghtml/ghtml.plugin.d.ts +3 -16
- package/src/integrations/ghtml/ghtml.plugin.js +7 -14
- package/src/plugins/define-integration.d.ts +26 -0
- package/src/plugins/define-integration.js +19 -0
- package/src/plugins/eco-component-meta-plugin.js +5 -8
- package/src/plugins/foreign-jsx-override-plugin.js +4 -6
- package/src/plugins/integration-plugin.d.ts +19 -9
- package/src/plugins/integration-plugin.js +7 -4
- package/src/plugins/jsx-import-source.utils.d.ts +8 -0
- package/src/plugins/jsx-import-source.utils.js +26 -0
- package/src/plugins/processor.d.ts +19 -4
- package/src/plugins/processor.js +22 -4
- package/src/plugins/source-transform.d.ts +31 -0
- package/src/plugins/source-transform.js +23 -1
- package/src/route-renderer/GRAPH.md +8 -8
- package/src/route-renderer/README.md +30 -28
- package/src/route-renderer/orchestration/component-graph-collectors.d.ts +10 -0
- package/src/route-renderer/orchestration/component-graph-collectors.js +143 -0
- package/src/route-renderer/orchestration/component-graph.d.ts +36 -0
- package/src/route-renderer/orchestration/component-graph.js +90 -0
- package/src/route-renderer/orchestration/component-render-context.d.ts +3 -10
- package/src/route-renderer/orchestration/component-render-context.js +3 -1
- package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +1 -18
- package/src/route-renderer/orchestration/declared-ownership-graph.js +5 -31
- package/src/route-renderer/orchestration/document-shell-render.service.d.ts +54 -0
- package/src/route-renderer/orchestration/document-shell-render.service.js +54 -0
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +59 -29
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +164 -35
- package/src/route-renderer/orchestration/global-injector-assets.service.d.ts +7 -0
- package/src/route-renderer/orchestration/global-injector-assets.service.js +32 -0
- package/src/route-renderer/orchestration/integration-renderer.d.ts +18 -54
- package/src/route-renderer/orchestration/integration-renderer.js +108 -194
- package/src/route-renderer/orchestration/integration-route-render-adapter.d.ts +38 -0
- package/src/route-renderer/orchestration/integration-route-render-adapter.js +27 -0
- package/src/route-renderer/orchestration/ownership-validation.service.d.ts +4 -0
- package/src/route-renderer/orchestration/ownership-validation.service.js +11 -1
- package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.d.ts +9 -0
- package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.js +7 -0
- package/src/route-renderer/orchestration/page-browser-graph.service.d.ts +25 -0
- package/src/route-renderer/orchestration/page-browser-graph.service.js +174 -0
- package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
- package/src/route-renderer/orchestration/render-output.utils.js +4 -0
- package/src/route-renderer/orchestration/route-html-finalization.service.d.ts +17 -0
- package/src/route-renderer/orchestration/route-html-finalization.service.js +26 -0
- package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +24 -0
- package/src/route-renderer/orchestration/route-prepared-options.builder.js +52 -0
- package/src/route-renderer/orchestration/route-prepared-options.utils.d.ts +4 -0
- package/src/route-renderer/orchestration/route-prepared-options.utils.js +33 -0
- package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +5 -38
- package/src/route-renderer/orchestration/route-render-orchestrator.js +51 -459
- package/src/route-renderer/orchestration/string-markup-renderer.d.ts +14 -0
- package/src/route-renderer/orchestration/string-markup-renderer.js +54 -0
- package/src/router/client/navigation-coordinator.d.ts +1 -1
- package/src/router/client/navigation-coordinator.js +12 -2
- package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
- package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +32 -14
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
- package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +0 -3
- package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -31
- package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +0 -3
- package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +2 -30
- package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.d.ts +2 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.js +43 -0
- package/src/services/assets/browser-bundle.service.d.ts +3 -1
- package/src/services/assets/browser-bundle.service.js +17 -5
- package/src/services/html/html-transformer.service.d.ts +1 -1
- package/src/services/html/html-transformer.service.js +4 -4
- package/src/services/invalidation/development-invalidation.service.d.ts +10 -1
- package/src/services/invalidation/development-invalidation.service.js +31 -2
- package/src/services/module-loading/README.md +49 -0
- package/src/services/module-loading/app-server-module-transpiler.service.js +5 -17
- package/src/services/module-loading/page-module-import.service.d.ts +4 -1
- package/src/services/module-loading/page-module-import.service.js +61 -41
- package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
- package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
- package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
- package/src/services/module-loading/route-module-build-cache.js +38 -0
- package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
- package/src/services/module-loading/route-module-build-cache.store.js +207 -0
- package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
- package/src/services/module-loading/route-module-build-manifest.js +116 -0
- package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
- package/src/services/module-loading/route-module-dependency-hasher.js +121 -0
- package/src/services/module-loading/server-module-transpiler.service.js +1 -1
- package/src/static-site-generator/README.md +33 -0
- package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
- package/src/static-site-generator/static-build-invalidation.js +59 -0
- package/src/static-site-generator/static-export-context.d.ts +22 -0
- package/src/static-site-generator/static-export-context.js +0 -0
- package/src/static-site-generator/static-site-generator.d.ts +17 -20
- package/src/static-site-generator/static-site-generator.js +188 -85
- package/src/types/internal-types.d.ts +12 -9
- package/src/types/public-types.d.ts +87 -22
- package/src/utils/parse-cli-args.d.ts +1 -0
- package/src/utils/parse-cli-args.js +3 -0
- package/src/watchers/project-watcher.d.ts +14 -2
- package/src/watchers/project-watcher.js +50 -20
- package/src/build/rolldown-dev-build-adapter.d.ts +0 -82
- package/src/build/rolldown-dev-build-adapter.js +0 -166
- package/src/hmr/hmr.postcss.test.e2e.d.ts +0 -1
- package/src/hmr/hmr.postcss.test.e2e.js +0 -31
- package/src/hmr/hmr.test.e2e.d.ts +0 -1
- package/src/hmr/hmr.test.e2e.js +0 -43
- package/src/route-renderer/orchestration/ownership-planning.service.d.ts +0 -24
- package/src/route-renderer/orchestration/ownership-planning.service.js +0 -63
- package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +0 -91
- package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.js +0 -170
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { appLogger } from "../../global/app-logger.js";
|
|
2
|
+
import { invokeWebSocketHandlerHook, toWebSocketCloseInfo } from "./websocket-lifecycle.js";
|
|
3
|
+
function toUpgradeRequest(ws, runtimeOrigin) {
|
|
4
|
+
const upgradeUrl = ws.data?.upgradeUrl;
|
|
5
|
+
if (upgradeUrl) {
|
|
6
|
+
return new Request(upgradeUrl);
|
|
7
|
+
}
|
|
8
|
+
return new Request(runtimeOrigin);
|
|
9
|
+
}
|
|
10
|
+
async function runUserWebSocketOpen(ws, kind, deps) {
|
|
11
|
+
const handler = deps.userHandlers.get(kind);
|
|
12
|
+
if (!handler) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const params = ws.data?.params ?? {};
|
|
16
|
+
const search = ws.data?.search ?? {};
|
|
17
|
+
const request = toUpgradeRequest(ws, deps.runtimeOrigin);
|
|
18
|
+
let context;
|
|
19
|
+
try {
|
|
20
|
+
context = await deps.resolveContext(request, handler, kind, params, search);
|
|
21
|
+
} catch {
|
|
22
|
+
ws.close(1011, "context initialization failed");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ws.data.context = context;
|
|
26
|
+
const socket = deps.adaptSocket(ws, kind, params, search, context);
|
|
27
|
+
try {
|
|
28
|
+
await handler.onConnect?.(socket);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
appLogger.error(`[WS:${kind}] onConnect failed:`, error);
|
|
31
|
+
ws.close(1011, "onConnect failed");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function createBunUserWebSocketLifecycle(deps) {
|
|
35
|
+
const resolveSocketForEvent = (ws) => {
|
|
36
|
+
const kind = ws.data?.kind;
|
|
37
|
+
if (!kind) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
const handler = deps.userHandlers.get(kind);
|
|
41
|
+
if (!handler) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
const params = ws.data?.params ?? {};
|
|
45
|
+
const search = ws.data?.search ?? {};
|
|
46
|
+
const socket = deps.adaptSocket(ws, kind, params, search, ws.data?.context);
|
|
47
|
+
return { kind, handler, socket };
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
open(ws) {
|
|
51
|
+
const kind = ws.data?.kind;
|
|
52
|
+
if (!kind) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
void runUserWebSocketOpen(ws, kind, deps).catch((error) => {
|
|
56
|
+
appLogger.error(`[WS:${kind}] open failed:`, error);
|
|
57
|
+
ws.close(1011, "internal error");
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
message(ws, msg) {
|
|
61
|
+
const resolved = resolveSocketForEvent(ws);
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (ws.data?.context === void 0 && resolved.handler.context) {
|
|
66
|
+
appLogger.warn(`[WS:${resolved.kind}] message received before context resolved; dropping.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const message = typeof msg === "string" ? { kind: "text", text: msg } : { kind: "binary", data: new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength) };
|
|
70
|
+
invokeWebSocketHandlerHook(
|
|
71
|
+
resolved.kind,
|
|
72
|
+
"onMessage",
|
|
73
|
+
resolved.handler.onMessage?.(resolved.socket, message)
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
close(ws, code, reason) {
|
|
77
|
+
const resolved = resolveSocketForEvent(ws);
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const event = toWebSocketCloseInfo(code, reason);
|
|
82
|
+
invokeWebSocketHandlerHook(resolved.kind, "onClose", resolved.handler.onClose?.(resolved.socket, event));
|
|
83
|
+
},
|
|
84
|
+
error(ws, error) {
|
|
85
|
+
const resolved = resolveSocketForEvent(ws);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
appLogger.error(`[WS:${resolved.kind}] error:`, error);
|
|
90
|
+
invokeWebSocketHandlerHook(resolved.kind, "onError", resolved.handler.onError?.(resolved.socket, error));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
createBunUserWebSocketLifecycle
|
|
96
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies a source public directory into the runtime dist dir, skipping unchanged files.
|
|
3
|
+
*
|
|
4
|
+
* Dev restarts call this on every boot; byte comparison avoids rewriting identical assets.
|
|
5
|
+
*/
|
|
6
|
+
export declare function copyRuntimePublicDirIfChanged(sourceDir: string, destinationDir: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
4
|
+
function copyRuntimePublicDirIfChanged(sourceDir, destinationDir) {
|
|
5
|
+
fileSystem.ensureDir(destinationDir);
|
|
6
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
7
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
8
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
9
|
+
if (entry.isDirectory()) {
|
|
10
|
+
copyRuntimePublicDirIfChanged(sourcePath, destinationPath);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (!entry.isFile()) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (fileSystem.exists(destinationPath) && Buffer.compare(fileSystem.readFileAsBuffer(sourcePath), fileSystem.readFileAsBuffer(destinationPath)) === 0) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
fileSystem.copyFile(sourcePath, destinationPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
copyRuntimePublicDirIfChanged
|
|
24
|
+
};
|
|
@@ -11,7 +11,7 @@ export declare function isHtmlResponse(response: Response): boolean;
|
|
|
11
11
|
* responses flow through different layers, but both need identical injection
|
|
12
12
|
* behavior in watch mode.
|
|
13
13
|
*/
|
|
14
|
-
export declare function shouldInjectHmrHtmlResponse(watch: boolean, hmrManager?: Pick<IHmrManager, 'isEnabled'
|
|
14
|
+
export declare function shouldInjectHmrHtmlResponse(watch: boolean, hmrManager?: Pick<IHmrManager, 'isEnabled'>, hostOwnsDevClient?: boolean): boolean;
|
|
15
15
|
/**
|
|
16
16
|
* Injects the development HMR runtime script into an HTML response if it is not
|
|
17
17
|
* already present.
|
|
@@ -4,7 +4,10 @@ function isHtmlResponse(response) {
|
|
|
4
4
|
const contentType = response.headers.get("Content-Type");
|
|
5
5
|
return contentType !== null && contentType.startsWith("text/html");
|
|
6
6
|
}
|
|
7
|
-
function shouldInjectHmrHtmlResponse(watch, hmrManager) {
|
|
7
|
+
function shouldInjectHmrHtmlResponse(watch, hmrManager, hostOwnsDevClient = false) {
|
|
8
|
+
if (hostOwnsDevClient) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
8
11
|
return watch && hmrManager?.isEnabled() === true;
|
|
9
12
|
}
|
|
10
13
|
async function injectHmrRuntimeIntoHtmlResponse(response) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Duplex } from 'node:stream';
|
|
2
|
+
import type { Server as NodeHttpServer, IncomingMessage } from 'node:http';
|
|
3
|
+
import type { EcopagesWebSocketHandler } from '../../types/public-types.js';
|
|
4
|
+
export type NodeHttpWebSocketUpgradePreflight = (req: IncomingMessage, socket: Duplex, head: Buffer) => boolean;
|
|
5
|
+
export type AttachNodeHttpWebSocketUpgradesOptions = {
|
|
6
|
+
runtimeOrigin: string;
|
|
7
|
+
websocketHandlers: Map<string, EcopagesWebSocketHandler<any, any>>;
|
|
8
|
+
/**
|
|
9
|
+
* When true, unmatched upgrade requests are left for other listeners (e.g.
|
|
10
|
+
* Vite HMR). When false, unmatched upgrades are destroyed immediately.
|
|
11
|
+
*/
|
|
12
|
+
passthroughUnmatched?: boolean;
|
|
13
|
+
preflight?: NodeHttpWebSocketUpgradePreflight;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Wires Ecopages user WebSocket routes onto a Node HTTP server's `upgrade` event.
|
|
17
|
+
*
|
|
18
|
+
* Used by the Node adapter in standalone mode and by host integrations such as
|
|
19
|
+
* the Vite plugin that embed Ecopages behind a foreign HTTP server.
|
|
20
|
+
*/
|
|
21
|
+
export declare function attachNodeHttpWebSocketUpgrades(server: NodeHttpServer, options: AttachNodeHttpWebSocketUpgradesOptions): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { appLogger } from "../../global/app-logger.js";
|
|
3
|
+
import { findWebSocketRoute } from "../abstract/ws-pattern-matcher.js";
|
|
4
|
+
import { createEcopagesSocket, invokeWebSocketHandlerHook, toWebSocketCloseInfo } from "./websocket-lifecycle.js";
|
|
5
|
+
const attachedUpgradeServers = /* @__PURE__ */ new WeakSet();
|
|
6
|
+
function adaptNodeWebSocket(ws, kind, params, search, context) {
|
|
7
|
+
return createEcopagesSocket(
|
|
8
|
+
{
|
|
9
|
+
send: (data) => ws.send(data),
|
|
10
|
+
close: (code, reason) => ws.close(code, reason)
|
|
11
|
+
},
|
|
12
|
+
{ kind, params, search, context }
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function toUpgradeRequest(runtimeOrigin, req) {
|
|
16
|
+
const upgradeUrl = new URL(req.url ?? "/", runtimeOrigin);
|
|
17
|
+
return new Request(upgradeUrl, {
|
|
18
|
+
method: req.method,
|
|
19
|
+
headers: req.headers
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async function resolveNodeContext(request, handler, kind, params, search) {
|
|
23
|
+
if (!handler.context) {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await handler.context({ request, kind, params, search });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
appLogger.error(`[WS:${kind}] context() failed; closing connection.`, error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function setupNodeWebSocketConnection(ws, wsMatch, req, runtimeOrigin, search) {
|
|
34
|
+
const handler = wsMatch.handler;
|
|
35
|
+
const baseRequest = toUpgradeRequest(runtimeOrigin, req);
|
|
36
|
+
let context;
|
|
37
|
+
try {
|
|
38
|
+
context = await resolveNodeContext(baseRequest, handler, wsMatch.kind, wsMatch.params, search);
|
|
39
|
+
} catch {
|
|
40
|
+
ws.close(1011, "context initialization failed");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const socket = adaptNodeWebSocket(ws, wsMatch.kind, wsMatch.params, search, context);
|
|
44
|
+
try {
|
|
45
|
+
await handler.onConnect?.(socket);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
appLogger.error(`[WS:${wsMatch.kind}] onConnect failed:`, error);
|
|
48
|
+
ws.close(1011, "onConnect failed");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
ws.on("message", (msg, isBinary) => {
|
|
52
|
+
const message = isBinary ? {
|
|
53
|
+
kind: "binary",
|
|
54
|
+
data: msg instanceof ArrayBuffer ? new Uint8Array(msg) : Array.isArray(msg) ? new Uint8Array(Buffer.concat(msg)) : new Uint8Array(msg)
|
|
55
|
+
} : { kind: "text", text: msg.toString() };
|
|
56
|
+
invokeWebSocketHandlerHook(wsMatch.kind, "onMessage", handler.onMessage?.(socket, message));
|
|
57
|
+
});
|
|
58
|
+
ws.on("close", (code, reason) => {
|
|
59
|
+
const event = toWebSocketCloseInfo(code, reason.toString());
|
|
60
|
+
invokeWebSocketHandlerHook(wsMatch.kind, "onClose", handler.onClose?.(socket, event));
|
|
61
|
+
});
|
|
62
|
+
ws.on("error", (err) => {
|
|
63
|
+
appLogger.error(`[WS:${wsMatch.kind}] error:`, err);
|
|
64
|
+
invokeWebSocketHandlerHook(wsMatch.kind, "onError", handler.onError?.(socket, err));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function attachNodeHttpWebSocketUpgrades(server, options) {
|
|
68
|
+
if (options.websocketHandlers.size === 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (attachedUpgradeServers.has(server)) {
|
|
72
|
+
appLogger.warn("[WS] WebSocket upgrades already attached to this HTTP server; skipping duplicate attach.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
attachedUpgradeServers.add(server);
|
|
76
|
+
const userWss = new WebSocketServer({ noServer: true });
|
|
77
|
+
server.on("upgrade", (req, socket, head) => {
|
|
78
|
+
if (options.preflight?.(req, socket, head)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const url = new URL(req.url ?? "/", options.runtimeOrigin);
|
|
82
|
+
const wsMatch = findWebSocketRoute(options.websocketHandlers, url.pathname);
|
|
83
|
+
if (!wsMatch) {
|
|
84
|
+
if (!options.passthroughUnmatched) {
|
|
85
|
+
socket.destroy();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
userWss.handleUpgrade(req, socket, head, (ws) => {
|
|
90
|
+
const search = Object.fromEntries(url.searchParams.entries());
|
|
91
|
+
void setupNodeWebSocketConnection(ws, wsMatch, req, options.runtimeOrigin, search).catch((error) => {
|
|
92
|
+
appLogger.error(`[WS:${wsMatch.kind}] unexpected error:`, error);
|
|
93
|
+
ws.close(1011, "internal error");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
attachNodeHttpWebSocketUpgrades
|
|
100
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether a thrown error indicates the requested port is already in
|
|
3
|
+
* use.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Matches Node's `code: 'EADDRINUSE'` and Bun's surface, where the error
|
|
7
|
+
* carries the same `code` plus a human-readable message such as
|
|
8
|
+
* `"Failed to start server. Is port 3000 in use?"`. Older Bun builds that do
|
|
9
|
+
* not set `.code` are covered by a fallback match on `syscall === 'listen'`
|
|
10
|
+
* plus the message shape.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isPortInUseError(error: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Probes whether a TCP port is currently bindable from this process. Uses a
|
|
15
|
+
* short-lived Node `net.Server` so both Node and Bun can run it via their
|
|
16
|
+
* Node-compatible core.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* A successful probe only guarantees the port was free at probe time; callers
|
|
20
|
+
* must still handle EADDRINUSE when they actually bind, since release races
|
|
21
|
+
* can flip the answer between probe and bind.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPortAvailable(port: number, hostname?: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Default interactive prompt. Asks a yes/no question on stdin/stdout and
|
|
26
|
+
* auto-approves (returns `true`) after `timeoutMs` with no input.
|
|
27
|
+
*
|
|
28
|
+
* @returns `true` when the user accepted (or timed out), `false` when they declined.
|
|
29
|
+
*/
|
|
30
|
+
export type PromptFunction = (message: string, timeoutMs: number) => Promise<boolean>;
|
|
31
|
+
export interface PortManagerOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Bind factory. Should throw on EADDRINUSE (or return `null`/`undefined`
|
|
34
|
+
* when it declines without a port collision). Returning a non-null port
|
|
35
|
+
* marks the bind as successful.
|
|
36
|
+
*/
|
|
37
|
+
startOnPort: (port: number) => Promise<number | null | undefined>;
|
|
38
|
+
/** Emits the "moved to port X" warning when the bound port differs from the preferred port. */
|
|
39
|
+
warn?: (message: string) => void;
|
|
40
|
+
/** Infers a free port to display in the prompt. Defaults to {@link isPortAvailable}. */
|
|
41
|
+
probePort?: (port: number) => Promise<boolean>;
|
|
42
|
+
/** Custom prompter. Defaults to a readline-based yes/no prompt on the process stdio. */
|
|
43
|
+
prompt?: PromptFunction;
|
|
44
|
+
/** Enables the prompt flow at all. Defaults to `process.stdin.isTTY && process.stdout.isTTY`. */
|
|
45
|
+
interactive?: boolean;
|
|
46
|
+
/** Auto-approve timeout (ms) for the prompt. Defaults to 10000. */
|
|
47
|
+
autoApproveMs?: number;
|
|
48
|
+
/** Maximum port offset to scan when falling back. Defaults to 20. */
|
|
49
|
+
maxPortOffset?: number;
|
|
50
|
+
/** Retry attempts for the same port before declaring it taken. Defaults to 2. */
|
|
51
|
+
releaseRaceRetries?: number;
|
|
52
|
+
/** Backoff between same-port retries. Defaults to 100 ms. */
|
|
53
|
+
releaseRaceDelayMs?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface BindPortOptions {
|
|
56
|
+
/** Port the caller would prefer to bind. */
|
|
57
|
+
preferredPort: number;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the caller permits moving to the next free port automatically when
|
|
60
|
+
* the preferred port is busy and no interactive prompt is available
|
|
61
|
+
* (non-TTY / CI). Explicit ports that fall back silently in CI would surprise
|
|
62
|
+
* operators, so this is honoured by the non-interactive path only.
|
|
63
|
+
*/
|
|
64
|
+
allowPortFallback: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Owns preview-port assignment: probing, race-release retries, fallback, and
|
|
68
|
+
* an optional interactive auto-approve prompt.
|
|
69
|
+
*
|
|
70
|
+
* @remarks
|
|
71
|
+
* Single-process API. The manager does not cache ports across calls; each
|
|
72
|
+
* `bind()` runs fresh from the preferred port.
|
|
73
|
+
*/
|
|
74
|
+
export declare class PortManager {
|
|
75
|
+
private readonly startOnPort;
|
|
76
|
+
private readonly warn;
|
|
77
|
+
private readonly probePort;
|
|
78
|
+
private readonly prompt;
|
|
79
|
+
private readonly interactive;
|
|
80
|
+
private readonly autoApproveMs;
|
|
81
|
+
private readonly maxPortOffset;
|
|
82
|
+
private readonly releaseRaceRetries;
|
|
83
|
+
private readonly releaseRaceDelayMs;
|
|
84
|
+
constructor(options: PortManagerOptions);
|
|
85
|
+
/**
|
|
86
|
+
* Binds the preview server to the preferred port.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* When the preferred port is free, binds it directly — no prompt, no
|
|
90
|
+
* fallback. The interactive prompt only fires on a genuine EADDRINUSE
|
|
91
|
+
* collision (TTY runs), and auto-approves after
|
|
92
|
+
* {@link PortManagerOptions.autoApproveMs}. In non-interactive (CI) runs,
|
|
93
|
+
* the manager falls back silently only when
|
|
94
|
+
* {@link BindPortOptions.allowPortFallback} is true (the default-port
|
|
95
|
+
* path); pinned ports rethrow EADDRINUSE so Docker-style fixed-port
|
|
96
|
+
* deployments fail loudly instead of silently moving.
|
|
97
|
+
*/
|
|
98
|
+
bind(options: BindPortOptions): Promise<number | null>;
|
|
99
|
+
private shouldFallback;
|
|
100
|
+
private fallForward;
|
|
101
|
+
/**
|
|
102
|
+
* Attempts to bind via {@link PortManagerOptions.startOnPort} on `port`,
|
|
103
|
+
* retrying EADDRINUSE collisions up to
|
|
104
|
+
* {@link PortManagerOptions.releaseRaceRetries} to ride through TIME_WAIT
|
|
105
|
+
* race release.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* Only EADDRINUSE is retryable. Any other error is rethrown synchronously
|
|
109
|
+
* rather than swallowed as a soft "port taken" result, so a misconfigured
|
|
110
|
+
* factory surfaces loudly instead of causing a silent fallback. A `null`
|
|
111
|
+
* return from the factory (no throw) is reported as `factory-refused`, not
|
|
112
|
+
* conflated with a real collision.
|
|
113
|
+
*/
|
|
114
|
+
private tryPort;
|
|
115
|
+
private findNextFreePort;
|
|
116
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createServer as createNetServer } from "node:net";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
function isPortInUseError(error) {
|
|
4
|
+
if (!(error instanceof Error) && (typeof error !== "object" || error === null)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const maybeErr = error;
|
|
8
|
+
const code = typeof maybeErr.code === "string" || typeof maybeErr.code === "number" ? String(maybeErr.code) : void 0;
|
|
9
|
+
const syscall = typeof maybeErr.syscall === "string" ? maybeErr.syscall : void 0;
|
|
10
|
+
const message = error instanceof Error ? error.message : String(maybeErr.message ?? "");
|
|
11
|
+
if (code === "EADDRINUSE") {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (message.includes("EADDRINUSE")) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (syscall === "listen" && /is port \d+ in use\??/i.test(message)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
function isPortAvailable(port, hostname = "127.0.0.1") {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const tester = createNetServer();
|
|
28
|
+
tester.once("error", () => resolve(false));
|
|
29
|
+
tester.once("listening", () => {
|
|
30
|
+
tester.close(() => resolve(true));
|
|
31
|
+
});
|
|
32
|
+
tester.listen(port, hostname);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function createDefaultPrompt(stdin, stdout) {
|
|
36
|
+
return async (message, timeoutMs) => {
|
|
37
|
+
stdout.write(`${message} `);
|
|
38
|
+
return await new Promise((resolve) => {
|
|
39
|
+
const rl = createInterface({ input: stdin, output: void 0, terminal: false });
|
|
40
|
+
let settled = false;
|
|
41
|
+
const finish = (value) => {
|
|
42
|
+
if (settled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
rl.close();
|
|
48
|
+
stdout.write("\n");
|
|
49
|
+
resolve(value);
|
|
50
|
+
};
|
|
51
|
+
const timer = setTimeout(() => finish(true), timeoutMs);
|
|
52
|
+
rl.once("line", (line) => {
|
|
53
|
+
const answer = line.trim().toLowerCase();
|
|
54
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
55
|
+
finish(true);
|
|
56
|
+
} else {
|
|
57
|
+
finish(false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
rl.once("close", () => finish(true));
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const PREVIEW_PORT_RELEASE_RACE_RETRIES = 2;
|
|
65
|
+
const PREVIEW_PORT_FALLBACK_ATTEMPTS = 20;
|
|
66
|
+
const PREVIEW_PORT_RELEASE_RACE_DELAY_MS = 100;
|
|
67
|
+
const PREVIEW_PORT_AUTO_APPROVE_MS = 1e4;
|
|
68
|
+
class PortManager {
|
|
69
|
+
startOnPort;
|
|
70
|
+
warn;
|
|
71
|
+
probePort;
|
|
72
|
+
prompt;
|
|
73
|
+
interactive;
|
|
74
|
+
autoApproveMs;
|
|
75
|
+
maxPortOffset;
|
|
76
|
+
releaseRaceRetries;
|
|
77
|
+
releaseRaceDelayMs;
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.startOnPort = options.startOnPort;
|
|
80
|
+
this.warn = options.warn;
|
|
81
|
+
this.probePort = options.probePort ?? isPortAvailable;
|
|
82
|
+
this.maxPortOffset = options.maxPortOffset ?? PREVIEW_PORT_FALLBACK_ATTEMPTS - 1;
|
|
83
|
+
this.releaseRaceRetries = options.releaseRaceRetries ?? PREVIEW_PORT_RELEASE_RACE_RETRIES;
|
|
84
|
+
this.releaseRaceDelayMs = options.releaseRaceDelayMs ?? PREVIEW_PORT_RELEASE_RACE_DELAY_MS;
|
|
85
|
+
this.autoApproveMs = options.autoApproveMs ?? PREVIEW_PORT_AUTO_APPROVE_MS;
|
|
86
|
+
this.interactive = typeof options.interactive === "boolean" ? options.interactive : Boolean(typeof process !== "undefined" && process.stdin?.isTTY && process.stdout?.isTTY);
|
|
87
|
+
if (options.prompt) {
|
|
88
|
+
this.prompt = options.prompt;
|
|
89
|
+
} else if (this.interactive) {
|
|
90
|
+
this.prompt = createDefaultPrompt(process.stdin, process.stdout);
|
|
91
|
+
} else {
|
|
92
|
+
this.prompt = void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Binds the preview server to the preferred port.
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* When the preferred port is free, binds it directly — no prompt, no
|
|
100
|
+
* fallback. The interactive prompt only fires on a genuine EADDRINUSE
|
|
101
|
+
* collision (TTY runs), and auto-approves after
|
|
102
|
+
* {@link PortManagerOptions.autoApproveMs}. In non-interactive (CI) runs,
|
|
103
|
+
* the manager falls back silently only when
|
|
104
|
+
* {@link BindPortOptions.allowPortFallback} is true (the default-port
|
|
105
|
+
* path); pinned ports rethrow EADDRINUSE so Docker-style fixed-port
|
|
106
|
+
* deployments fail loudly instead of silently moving.
|
|
107
|
+
*/
|
|
108
|
+
async bind(options) {
|
|
109
|
+
const preferredPort = options.preferredPort;
|
|
110
|
+
const firstAttempt = await this.tryPort(preferredPort);
|
|
111
|
+
if (firstAttempt.status === "bound") {
|
|
112
|
+
return firstAttempt.boundPort;
|
|
113
|
+
}
|
|
114
|
+
if (firstAttempt.status === "factory-refused") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
await this.shouldFallback(preferredPort, options.allowPortFallback, firstAttempt.error);
|
|
118
|
+
return await this.fallForward(preferredPort);
|
|
119
|
+
}
|
|
120
|
+
async shouldFallback(preferredPort, allowPortFallback, error) {
|
|
121
|
+
if (this.interactive && this.prompt) {
|
|
122
|
+
const nextFree = await this.findNextFreePort(preferredPort);
|
|
123
|
+
const nextFreeSuffix = typeof nextFree === "number" ? ` (next free: ${nextFree})` : "";
|
|
124
|
+
const autoApproveHint = `auto-yes in ${Math.round(this.autoApproveMs / 1e3)}s`;
|
|
125
|
+
const message = `[@ecopages/core] Port ${preferredPort} is already in use${nextFreeSuffix}. Bind there instead? [Y/n] (${autoApproveHint})`;
|
|
126
|
+
const accepted = await this.prompt(message, this.autoApproveMs);
|
|
127
|
+
if (!accepted) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!allowPortFallback) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async fallForward(preferredPort) {
|
|
137
|
+
for (let offset = 1; offset <= this.maxPortOffset; offset += 1) {
|
|
138
|
+
const candidatePort = preferredPort + offset;
|
|
139
|
+
const attempt = await this.tryPort(candidatePort);
|
|
140
|
+
if (attempt.status === "bound") {
|
|
141
|
+
this.warn?.(
|
|
142
|
+
`Port ${preferredPort} is in use; preview serving on port ${attempt.boundPort} instead. Pin the preview port with --port or ECOPAGES_PORT when a fixed binding is required.`
|
|
143
|
+
);
|
|
144
|
+
return attempt.boundPort;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Attempts to bind via {@link PortManagerOptions.startOnPort} on `port`,
|
|
151
|
+
* retrying EADDRINUSE collisions up to
|
|
152
|
+
* {@link PortManagerOptions.releaseRaceRetries} to ride through TIME_WAIT
|
|
153
|
+
* race release.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* Only EADDRINUSE is retryable. Any other error is rethrown synchronously
|
|
157
|
+
* rather than swallowed as a soft "port taken" result, so a misconfigured
|
|
158
|
+
* factory surfaces loudly instead of causing a silent fallback. A `null`
|
|
159
|
+
* return from the factory (no throw) is reported as `factory-refused`, not
|
|
160
|
+
* conflated with a real collision.
|
|
161
|
+
*/
|
|
162
|
+
async tryPort(port) {
|
|
163
|
+
for (let attempt = 0; attempt < this.releaseRaceRetries; attempt += 1) {
|
|
164
|
+
try {
|
|
165
|
+
const bound = await this.startOnPort(port);
|
|
166
|
+
if (bound) {
|
|
167
|
+
return { status: "bound", boundPort: bound };
|
|
168
|
+
}
|
|
169
|
+
return { status: "factory-refused" };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (!isPortInUseError(error)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (attempt === this.releaseRaceRetries - 1) {
|
|
175
|
+
return { status: "port-in-use", error };
|
|
176
|
+
}
|
|
177
|
+
await sleep(this.releaseRaceDelayMs);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { status: "factory-refused" };
|
|
181
|
+
}
|
|
182
|
+
async findNextFreePort(startPort) {
|
|
183
|
+
for (let offset = 1; offset <= this.maxPortOffset; offset += 1) {
|
|
184
|
+
const candidate = startPort + offset;
|
|
185
|
+
if (await this.probePort(candidate)) {
|
|
186
|
+
return candidate;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
PortManager,
|
|
194
|
+
isPortAvailable,
|
|
195
|
+
isPortInUseError
|
|
196
|
+
};
|
|
@@ -6,9 +6,10 @@ export type RuntimeBinding = {
|
|
|
6
6
|
runtimeOrigin: string;
|
|
7
7
|
serveOptions: Record<string, unknown>;
|
|
8
8
|
watch: boolean;
|
|
9
|
+
/** Whether preview may bind to the next free port when the preferred port is busy. */
|
|
10
|
+
allowPortFallback: boolean;
|
|
9
11
|
};
|
|
10
12
|
export type StaticRuntimeMode = {
|
|
11
|
-
requiresFetchRuntime: boolean;
|
|
12
13
|
canBuildWithoutRuntimeServer: boolean;
|
|
13
14
|
};
|
|
14
15
|
export declare function resolveServeRuntimeOrigin(serveOptions: {
|
|
@@ -12,11 +12,13 @@ function resolveServeRuntimeOrigin(serveOptions) {
|
|
|
12
12
|
}
|
|
13
13
|
function resolveRuntimeBinding(options) {
|
|
14
14
|
const env = options.env ?? process.env;
|
|
15
|
+
const portExplicitlyConfigured = options.cliArgs.port !== void 0 || Boolean(env.ECOPAGES_PORT) || options.serverOptions?.port !== void 0;
|
|
15
16
|
const preferredPort = options.cliArgs.port ?? (env.ECOPAGES_PORT ? Number(env.ECOPAGES_PORT) : void 0) ?? DEFAULT_ECOPAGES_PORT;
|
|
16
17
|
const preferredHostname = options.cliArgs.hostname ?? env.ECOPAGES_HOSTNAME ?? DEFAULT_ECOPAGES_HOSTNAME;
|
|
17
18
|
return {
|
|
18
19
|
preferredPort,
|
|
19
20
|
preferredHostname,
|
|
21
|
+
allowPortFallback: !portExplicitlyConfigured,
|
|
20
22
|
runtimeOrigin: resolveServeRuntimeOrigin({
|
|
21
23
|
hostname: preferredHostname,
|
|
22
24
|
port: preferredPort
|
|
@@ -30,13 +32,8 @@ function resolveRuntimeBinding(options) {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
function resolveStaticRuntimeMode(options) {
|
|
33
|
-
const requiresFetchRuntime = options.appConfig.integrations.some(
|
|
34
|
-
(integration) => integration.staticBuildStep === "fetch"
|
|
35
|
-
);
|
|
36
|
-
const canBuildWithoutRuntimeServer = (options.cliArgs.build || options.cliArgs.preview) && !requiresFetchRuntime;
|
|
37
35
|
return {
|
|
38
|
-
|
|
39
|
-
canBuildWithoutRuntimeServer
|
|
36
|
+
canBuildWithoutRuntimeServer: options.cliArgs.build || options.cliArgs.preview
|
|
40
37
|
};
|
|
41
38
|
}
|
|
42
39
|
export {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
2
|
+
import type { IHmrManager } from '../../types/public-types.js';
|
|
3
|
+
import type { ProjectWatcher } from '../../watchers/project-watcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Copies source `public/` into dist and ensures the resolved assets directory exists.
|
|
6
|
+
*/
|
|
7
|
+
export declare function prepareRuntimePublicDir(appConfig: EcoPagesAppConfig): void;
|
|
8
|
+
/**
|
|
9
|
+
* Propagates browser build plugins and the shared HMR manager into integrations.
|
|
10
|
+
*/
|
|
11
|
+
export declare function wireIntegrationHmrManagers(appConfig: EcoPagesAppConfig, hmrManager: IHmrManager): void;
|
|
12
|
+
/**
|
|
13
|
+
* Injects the dev HMR runtime into adapter-level HTML responses when watch mode is active.
|
|
14
|
+
*/
|
|
15
|
+
export declare function maybeInjectAdapterHmrHtmlResponse(response: Response, options: {
|
|
16
|
+
watch: boolean;
|
|
17
|
+
hmrManager?: Pick<IHmrManager, 'isEnabled'>;
|
|
18
|
+
hostOwnsDevClient: boolean;
|
|
19
|
+
}): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Releases shared dev resources in a consistent order across server adapters.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Bun calls `buildRuntime()` during `initialize()` when watch is enabled; Node defers
|
|
25
|
+
* runtime creation to `completeInitialization()`. Transport-specific setup stays in
|
|
26
|
+
* each adapter — only shared teardown lives here.
|
|
27
|
+
*/
|
|
28
|
+
export declare function disposeDevResources(options: {
|
|
29
|
+
projectWatcher: ProjectWatcher | null | undefined;
|
|
30
|
+
appConfig: EcoPagesAppConfig;
|
|
31
|
+
hmrManager: IHmrManager | null | undefined;
|
|
32
|
+
bridge: {
|
|
33
|
+
destroy(): void;
|
|
34
|
+
} | null | undefined;
|
|
35
|
+
previewHost: {
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<void>;
|