@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,118 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
class NodeClientAbortError extends Error {
|
|
4
|
+
/**
|
|
5
|
+
* Creates the canonical Node transport abort error used across request-body
|
|
6
|
+
* reads and response-body writes.
|
|
7
|
+
*/
|
|
8
|
+
constructor() {
|
|
9
|
+
super("Client closed the request");
|
|
10
|
+
this.name = "ClientAbortError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isNodeClientDisconnectCode(code) {
|
|
14
|
+
return [
|
|
15
|
+
"ECONNRESET",
|
|
16
|
+
"EPIPE",
|
|
17
|
+
"ERR_STREAM_DESTROYED",
|
|
18
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
19
|
+
"ERR_STREAM_UNABLE_TO_PIPE"
|
|
20
|
+
].includes(code ?? "");
|
|
21
|
+
}
|
|
22
|
+
function isNodeClientAbortError(error) {
|
|
23
|
+
return error instanceof NodeClientAbortError;
|
|
24
|
+
}
|
|
25
|
+
function toNodeClientAbortError(error) {
|
|
26
|
+
if (error instanceof NodeClientAbortError) {
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
30
|
+
const code = typeof error.code === "string" ? error.code : void 0;
|
|
31
|
+
if (isNodeClientDisconnectCode(code)) {
|
|
32
|
+
return new NodeClientAbortError();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
class NodeHttpRequestBridge {
|
|
38
|
+
/**
|
|
39
|
+
* Converts one incoming Node request into the Web `Request` shape consumed by
|
|
40
|
+
* the shared server adapter.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Non-GET/HEAD requests retain streaming semantics via a `ReadableStream` so
|
|
44
|
+
* large request bodies do not need to be buffered before route handling begins.
|
|
45
|
+
*/
|
|
46
|
+
createWebRequest(req, runtimeOrigin) {
|
|
47
|
+
const url = new URL(req.url ?? "/", runtimeOrigin);
|
|
48
|
+
const headers = new Headers();
|
|
49
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
for (const item of value) {
|
|
52
|
+
headers.append(key, item);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (value !== void 0) {
|
|
57
|
+
headers.set(key, value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
61
|
+
const requestInit = {
|
|
62
|
+
method,
|
|
63
|
+
headers
|
|
64
|
+
};
|
|
65
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
66
|
+
const body = new ReadableStream({
|
|
67
|
+
start(controller) {
|
|
68
|
+
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
69
|
+
req.once("end", () => controller.close());
|
|
70
|
+
req.once("aborted", () => {
|
|
71
|
+
controller.error(new NodeClientAbortError());
|
|
72
|
+
});
|
|
73
|
+
req.once("error", (err) => {
|
|
74
|
+
const isClientAbort = err.code === "ECONNRESET";
|
|
75
|
+
controller.error(isClientAbort ? new NodeClientAbortError() : err);
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
cancel() {
|
|
79
|
+
req.destroy();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
requestInit.body = body;
|
|
83
|
+
requestInit.duplex = "half";
|
|
84
|
+
}
|
|
85
|
+
return new Request(url, requestInit);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Sends a Web `Response` through Node's `ServerResponse` without buffering the
|
|
89
|
+
* full body in memory first.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* Streaming responses matter for large payloads and long-lived transports such
|
|
93
|
+
* as server-sent events. The bridge therefore forwards the `ReadableStream`
|
|
94
|
+
* directly into the Node writable response instead of materializing an
|
|
95
|
+
* intermediate `ArrayBuffer`.
|
|
96
|
+
*/
|
|
97
|
+
async sendNodeResponse(res, response) {
|
|
98
|
+
res.statusCode = response.status;
|
|
99
|
+
response.headers.forEach((value, key) => {
|
|
100
|
+
res.setHeader(key, value);
|
|
101
|
+
});
|
|
102
|
+
if (!response.body) {
|
|
103
|
+
res.end();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const responseBody = response.body;
|
|
107
|
+
try {
|
|
108
|
+
await pipeline(Readable.fromWeb(responseBody), res);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw toNodeClientAbortError(error) ?? error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
NodeClientAbortError,
|
|
116
|
+
NodeHttpRequestBridge,
|
|
117
|
+
isNodeClientAbortError
|
|
118
|
+
};
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
4
|
-
import type { ClientBridgeEvent } from '../../types/public-types.js';
|
|
1
|
+
import type { EcoPagesAppConfig, IClientBridge } from '../../types/internal-types.js';
|
|
2
|
+
import { InMemoryEntrypointDependencyGraph, type EntrypointDependencyGraph } from '../../services/runtime-state/entrypoint-dependency-graph.service.js';
|
|
3
|
+
import { SharedHmrManager } from '../shared/shared-hmr-manager.js';
|
|
5
4
|
export interface NodeHmrManagerParams {
|
|
6
5
|
appConfig: EcoPagesAppConfig;
|
|
7
6
|
bridge: IClientBridge;
|
|
8
7
|
}
|
|
9
|
-
type HandleFileChangeOptions = {
|
|
10
|
-
broadcast?: boolean;
|
|
11
|
-
};
|
|
12
8
|
/**
|
|
13
9
|
* Node development HMR manager.
|
|
14
10
|
*
|
|
@@ -22,100 +18,35 @@ type HandleFileChangeOptions = {
|
|
|
22
18
|
* reserved for integration-owned page bundles, while generic script assets must
|
|
23
19
|
* go through `registerScriptEntrypoint()`.
|
|
24
20
|
*/
|
|
25
|
-
export declare class NodeHmrManager
|
|
26
|
-
private static readonly entrypointRegistrationTimeoutMs;
|
|
27
|
-
readonly appConfig: EcoPagesAppConfig;
|
|
28
|
-
private readonly bridge;
|
|
29
|
-
private watchers;
|
|
30
|
-
private watchedFiles;
|
|
31
|
-
private entrypointRegistrations;
|
|
32
|
-
private distDir;
|
|
33
|
-
private plugins;
|
|
34
|
-
private enabled;
|
|
35
|
-
private strategies;
|
|
36
|
-
private readonly entrypointRegistrar;
|
|
37
|
-
private readonly browserBundleService;
|
|
38
|
-
private readonly entrypointDependencyGraph;
|
|
39
|
-
private readonly serverModuleTranspiler;
|
|
40
|
-
constructor({ appConfig, bridge }: NodeHmrManagerParams);
|
|
21
|
+
export declare class NodeHmrManager extends SharedHmrManager {
|
|
41
22
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* This must not remove the directory because multiple app processes
|
|
45
|
-
* can share the same dist path during e2e runs.
|
|
46
|
-
*/
|
|
47
|
-
private cleanDistDir;
|
|
48
|
-
/**
|
|
49
|
-
* Returns whether the generic JS strategy is allowed to rebuild an entrypoint.
|
|
23
|
+
* Creates the Node HMR manager over the shared coordination pipeline.
|
|
50
24
|
*
|
|
51
25
|
* @remarks
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
26
|
+
* Unlike Bun, Node keeps websocket subscription wiring in the server adapter,
|
|
27
|
+
* so this manager only specializes the shared manager where runtime behavior
|
|
28
|
+
* actually differs: dependency-graph storage, missing-file tolerance, and how
|
|
29
|
+
* runtime bundle failures disable HMR.
|
|
55
30
|
*/
|
|
56
|
-
|
|
57
|
-
private initializeStrategies;
|
|
58
|
-
registerStrategy(strategy: HmrStrategy): void;
|
|
59
|
-
setPlugins(plugins: EcoBuildPlugin[]): void;
|
|
60
|
-
setEnabled(enabled: boolean): void;
|
|
61
|
-
isEnabled(): boolean;
|
|
62
|
-
buildRuntime(): Promise<void>;
|
|
63
|
-
getRuntimePath(): string;
|
|
64
|
-
broadcast(event: ClientBridgeEvent): void;
|
|
65
|
-
handleFileChange(filePath: string, options?: HandleFileChangeOptions): Promise<void>;
|
|
66
|
-
getOutputUrl(entrypointPath: string): string | undefined;
|
|
67
|
-
getWatchedFiles(): Map<string, string>;
|
|
68
|
-
getDistDir(): string;
|
|
69
|
-
getPlugins(): EcoBuildPlugin[];
|
|
70
|
-
getDefaultContext(): DefaultHmrContext;
|
|
71
|
-
private clearFailedEntrypointRegistration;
|
|
72
|
-
/**
|
|
73
|
-
* Registers one integration-owned page entrypoint.
|
|
74
|
-
*
|
|
75
|
-
* @remarks
|
|
76
|
-
* Concurrent callers share one in-flight registration. The registration is
|
|
77
|
-
* removed from the dedupe map once it resolves or fails so later requests do
|
|
78
|
-
* not inherit stale state.
|
|
79
|
-
*/
|
|
80
|
-
registerEntrypoint(entrypointPath: string): Promise<string>;
|
|
81
|
-
/**
|
|
82
|
-
* Registers one generic script entrypoint.
|
|
83
|
-
*
|
|
84
|
-
* @remarks
|
|
85
|
-
* This path is intentionally separate from page entrypoints so non-framework
|
|
86
|
-
* scripts can still use the generic build fallback without weakening the page
|
|
87
|
-
* ownership contract.
|
|
88
|
-
*/
|
|
89
|
-
registerScriptEntrypoint(entrypointPath: string): Promise<string>;
|
|
31
|
+
constructor({ appConfig, bridge }: NodeHmrManagerParams);
|
|
90
32
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @remarks
|
|
94
|
-
* The flow is:
|
|
95
|
-
* 1. Reserve the output URL in the watched map.
|
|
96
|
-
* 2. Remove any stale emitted file from an earlier process or failed build.
|
|
97
|
-
* 3. Let the strategy chain try to emit the entrypoint without broadcasting.
|
|
98
|
-
* 4. Fail if the owning integration did not emit the expected output.
|
|
33
|
+
* Reuses the shared in-memory dependency graph when the app already has one and
|
|
34
|
+
* otherwise creates the default Node development graph.
|
|
99
35
|
*/
|
|
100
|
-
|
|
36
|
+
protected createEntrypointDependencyGraph(existingEntrypointDependencyGraph: EntrypointDependencyGraph): InMemoryEntrypointDependencyGraph;
|
|
101
37
|
/**
|
|
102
|
-
*
|
|
38
|
+
* Tells the shared HMR manager to ignore missing-file events during Node watch
|
|
39
|
+
* mode.
|
|
103
40
|
*
|
|
104
41
|
* @remarks
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
42
|
+
* Node file watching can briefly report remove-and-recreate sequences while a
|
|
43
|
+
* file is being rewritten. Treating those transient gaps as fatal would disable
|
|
44
|
+
* otherwise healthy development sessions.
|
|
108
45
|
*/
|
|
109
|
-
|
|
46
|
+
protected shouldSkipMissingFileChange(_filePath: string): boolean;
|
|
110
47
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @remarks
|
|
114
|
-
* The manager intentionally does not remove emitted `_hmr` files from disk
|
|
115
|
-
* because multiple app processes may share the same dist directory during test
|
|
116
|
-
* runs. It does clear in-memory indexes so old entrypoints and dependencies
|
|
117
|
-
* cannot leak across a reused manager instance.
|
|
48
|
+
* Disables HMR after a runtime bundle failure so the Node dev server can keep
|
|
49
|
+
* serving requests without repeatedly emitting broken updates.
|
|
118
50
|
*/
|
|
119
|
-
|
|
51
|
+
protected onRuntimeBundleFailure(error: unknown): void;
|
|
120
52
|
}
|
|
121
|
-
export {};
|
|
@@ -1,293 +1,47 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
|
|
4
|
-
import { getAppBuildExecutor } from "../../build/build-adapter.js";
|
|
5
|
-
import { fileSystem } from "@ecopages/file-system";
|
|
6
|
-
import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
|
|
7
|
-
import { DefaultHmrStrategy } from "../../hmr/strategies/default-hmr-strategy.js";
|
|
8
|
-
import { JsHmrStrategy } from "../../hmr/strategies/js-hmr-strategy.js";
|
|
9
1
|
import { appLogger } from "../../global/app-logger.js";
|
|
10
|
-
import { HmrEntrypointRegistrar } from "../shared/hmr-entrypoint-registrar.js";
|
|
11
|
-
import { BrowserBundleService } from "../../services/assets/browser-bundle.service.js";
|
|
12
|
-
import { getAppServerModuleTranspiler } from "../../services/module-loading/app-server-module-transpiler.service.js";
|
|
13
2
|
import {
|
|
14
|
-
|
|
15
|
-
InMemoryEntrypointDependencyGraph,
|
|
16
|
-
setAppEntrypointDependencyGraph
|
|
3
|
+
InMemoryEntrypointDependencyGraph
|
|
17
4
|
} from "../../services/runtime-state/entrypoint-dependency-graph.service.js";
|
|
18
|
-
import {
|
|
19
|
-
class NodeHmrManager {
|
|
20
|
-
static entrypointRegistrationTimeoutMs = 4e3;
|
|
21
|
-
appConfig;
|
|
22
|
-
bridge;
|
|
23
|
-
watchers = /* @__PURE__ */ new Map();
|
|
24
|
-
watchedFiles = /* @__PURE__ */ new Map();
|
|
25
|
-
entrypointRegistrations = /* @__PURE__ */ new Map();
|
|
26
|
-
distDir;
|
|
27
|
-
plugins = [];
|
|
28
|
-
enabled = true;
|
|
29
|
-
strategies = [];
|
|
30
|
-
entrypointRegistrar;
|
|
31
|
-
browserBundleService;
|
|
32
|
-
entrypointDependencyGraph;
|
|
33
|
-
serverModuleTranspiler;
|
|
34
|
-
constructor({ appConfig, bridge }) {
|
|
35
|
-
this.appConfig = appConfig;
|
|
36
|
-
this.bridge = bridge;
|
|
37
|
-
this.distDir = path.join(resolveInternalWorkDir(this.appConfig), RESOLVED_ASSETS_DIR, "_hmr");
|
|
38
|
-
this.entrypointRegistrar = new HmrEntrypointRegistrar({
|
|
39
|
-
srcDir: this.appConfig.absolutePaths.srcDir,
|
|
40
|
-
distDir: this.distDir,
|
|
41
|
-
entrypointRegistrations: this.entrypointRegistrations,
|
|
42
|
-
watchedFiles: this.watchedFiles,
|
|
43
|
-
clearFailedRegistration: (entrypointPath) => this.clearFailedEntrypointRegistration(entrypointPath),
|
|
44
|
-
registrationTimeoutMs: NodeHmrManager.entrypointRegistrationTimeoutMs
|
|
45
|
-
});
|
|
46
|
-
this.browserBundleService = new BrowserBundleService(appConfig);
|
|
47
|
-
const existingEntrypointDependencyGraph = getAppEntrypointDependencyGraph(appConfig);
|
|
48
|
-
this.entrypointDependencyGraph = existingEntrypointDependencyGraph instanceof InMemoryEntrypointDependencyGraph ? existingEntrypointDependencyGraph : new InMemoryEntrypointDependencyGraph();
|
|
49
|
-
setAppEntrypointDependencyGraph(this.appConfig, this.entrypointDependencyGraph);
|
|
50
|
-
this.serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
|
|
51
|
-
this.cleanDistDir();
|
|
52
|
-
this.initializeStrategies();
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Ensures the HMR output directory exists.
|
|
56
|
-
*
|
|
57
|
-
* This must not remove the directory because multiple app processes
|
|
58
|
-
* can share the same dist path during e2e runs.
|
|
59
|
-
*/
|
|
60
|
-
cleanDistDir() {
|
|
61
|
-
fileSystem.ensureDir(this.distDir);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Returns whether the generic JS strategy is allowed to rebuild an entrypoint.
|
|
65
|
-
*
|
|
66
|
-
* @remarks
|
|
67
|
-
* Higher-priority integration strategies own framework page entrypoints. When
|
|
68
|
-
* one of them matches, the generic JS strategy must stay out of the way so a
|
|
69
|
-
* shared dependency invalidation does not overwrite framework-specific output.
|
|
70
|
-
*/
|
|
71
|
-
shouldJsStrategyProcessEntrypoint(entrypointPath) {
|
|
72
|
-
return !this.strategies.some((strategy) => {
|
|
73
|
-
if (strategy.type !== HmrStrategyType.INTEGRATION || strategy.priority <= HmrStrategyType.SCRIPT) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
return strategy.matches(entrypointPath);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
appLogger.error(error);
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
initializeStrategies() {
|
|
85
|
-
const jsContext = {
|
|
86
|
-
getWatchedFiles: () => this.watchedFiles,
|
|
87
|
-
getDistDir: () => this.distDir,
|
|
88
|
-
getPlugins: () => this.plugins,
|
|
89
|
-
getSrcDir: () => this.appConfig.absolutePaths.srcDir,
|
|
90
|
-
getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
|
|
91
|
-
getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
|
|
92
|
-
getTemplateExtensions: () => this.appConfig.templatesExt,
|
|
93
|
-
getBrowserBundleService: () => this.browserBundleService,
|
|
94
|
-
getEntrypointDependencyGraph: () => this.entrypointDependencyGraph,
|
|
95
|
-
shouldProcessEntrypoint: (entrypointPath) => this.shouldJsStrategyProcessEntrypoint(entrypointPath)
|
|
96
|
-
};
|
|
97
|
-
this.strategies = [new JsHmrStrategy(jsContext), new DefaultHmrStrategy()];
|
|
98
|
-
}
|
|
99
|
-
registerStrategy(strategy) {
|
|
100
|
-
this.strategies.push(strategy);
|
|
101
|
-
}
|
|
102
|
-
setPlugins(plugins) {
|
|
103
|
-
this.plugins = [...plugins];
|
|
104
|
-
}
|
|
105
|
-
setEnabled(enabled) {
|
|
106
|
-
this.enabled = enabled;
|
|
107
|
-
}
|
|
108
|
-
isEnabled() {
|
|
109
|
-
return this.enabled;
|
|
110
|
-
}
|
|
111
|
-
async buildRuntime() {
|
|
112
|
-
const runtimeSource = path.resolve(import.meta.dirname, "../../hmr/client/hmr-runtime.js");
|
|
113
|
-
try {
|
|
114
|
-
const result = await this.browserBundleService.bundle({
|
|
115
|
-
profile: "hmr-runtime",
|
|
116
|
-
entrypoints: [runtimeSource],
|
|
117
|
-
outdir: this.distDir,
|
|
118
|
-
naming: "_hmr_runtime.js",
|
|
119
|
-
minify: false,
|
|
120
|
-
plugins: this.plugins
|
|
121
|
-
});
|
|
122
|
-
if (!result.success) {
|
|
123
|
-
this.enabled = false;
|
|
124
|
-
appLogger.error("[HMR] Failed to build runtime script; continuing with HMR disabled.", result.logs);
|
|
125
|
-
}
|
|
126
|
-
} catch (error) {
|
|
127
|
-
this.enabled = false;
|
|
128
|
-
appLogger.error("[HMR] Failed to build runtime script; continuing with HMR disabled.", error);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
getRuntimePath() {
|
|
132
|
-
return path.join(this.distDir, "_hmr_runtime.js");
|
|
133
|
-
}
|
|
134
|
-
broadcast(event) {
|
|
135
|
-
appLogger.debug(
|
|
136
|
-
`[HMR] Broadcasting ${event.type} event, path=${event.path || "all"}, subscribers=${this.bridge.subscriberCount}`
|
|
137
|
-
);
|
|
138
|
-
this.bridge.broadcast(event);
|
|
139
|
-
}
|
|
140
|
-
async handleFileChange(filePath, options = {}) {
|
|
141
|
-
if (!fileSystem.exists(filePath)) {
|
|
142
|
-
appLogger.debug(`[NodeHmrManager] Skipping missing file change: ${filePath}`);
|
|
143
|
-
this.clearFailedEntrypointRegistration(filePath);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
|
|
147
|
-
const strategy = sorted.find((s) => {
|
|
148
|
-
try {
|
|
149
|
-
return s.matches(filePath);
|
|
150
|
-
} catch (err) {
|
|
151
|
-
appLogger.error(err);
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
if (!strategy) {
|
|
156
|
-
appLogger.warn(`[HMR] No strategy found for ${filePath}`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
appLogger.debug(`[NodeHmrManager] Selected strategy: ${strategy.constructor.name}`);
|
|
160
|
-
const action = await strategy.process(filePath);
|
|
161
|
-
const shouldBroadcast = options.broadcast ?? true;
|
|
162
|
-
if (shouldBroadcast && action.type === "broadcast") {
|
|
163
|
-
if (action.events) {
|
|
164
|
-
for (const event of action.events) {
|
|
165
|
-
this.broadcast(event);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
getOutputUrl(entrypointPath) {
|
|
171
|
-
return this.watchedFiles.get(entrypointPath);
|
|
172
|
-
}
|
|
173
|
-
getWatchedFiles() {
|
|
174
|
-
return this.watchedFiles;
|
|
175
|
-
}
|
|
176
|
-
getDistDir() {
|
|
177
|
-
return this.distDir;
|
|
178
|
-
}
|
|
179
|
-
getPlugins() {
|
|
180
|
-
return this.plugins;
|
|
181
|
-
}
|
|
182
|
-
getDefaultContext() {
|
|
183
|
-
return {
|
|
184
|
-
getWatchedFiles: () => this.watchedFiles,
|
|
185
|
-
getDistDir: () => this.distDir,
|
|
186
|
-
getPlugins: () => this.plugins,
|
|
187
|
-
getSrcDir: () => this.appConfig.absolutePaths.srcDir,
|
|
188
|
-
getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
|
|
189
|
-
getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
|
|
190
|
-
getBuildExecutor: () => getAppBuildExecutor(this.appConfig),
|
|
191
|
-
getBrowserBundleService: () => this.browserBundleService,
|
|
192
|
-
importServerModule: async (filePath) => await this.serverModuleTranspiler.importModule({
|
|
193
|
-
filePath,
|
|
194
|
-
outdir: path.join(resolveInternalExecutionDir(this.appConfig), ".server-modules"),
|
|
195
|
-
externalPackages: true
|
|
196
|
-
})
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
clearFailedEntrypointRegistration(entrypointPath) {
|
|
200
|
-
this.watchedFiles.delete(entrypointPath);
|
|
201
|
-
this.entrypointDependencyGraph.clearEntrypointDependencies(entrypointPath);
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Registers one integration-owned page entrypoint.
|
|
205
|
-
*
|
|
206
|
-
* @remarks
|
|
207
|
-
* Concurrent callers share one in-flight registration. The registration is
|
|
208
|
-
* removed from the dedupe map once it resolves or fails so later requests do
|
|
209
|
-
* not inherit stale state.
|
|
210
|
-
*/
|
|
211
|
-
async registerEntrypoint(entrypointPath) {
|
|
212
|
-
return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
|
|
213
|
-
emit: async (normalizedEntrypoint) => await this.emitStrictEntrypoint(normalizedEntrypoint),
|
|
214
|
-
getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(
|
|
215
|
-
`[HMR] Integration failed to emit entrypoint ${normalizedEntrypoint} to ${outputPath}. Page entrypoints must be produced by their owning integration.`
|
|
216
|
-
)
|
|
217
|
-
});
|
|
218
|
-
}
|
|
5
|
+
import { SharedHmrManager } from "../shared/shared-hmr-manager.js";
|
|
6
|
+
class NodeHmrManager extends SharedHmrManager {
|
|
219
7
|
/**
|
|
220
|
-
*
|
|
8
|
+
* Creates the Node HMR manager over the shared coordination pipeline.
|
|
221
9
|
*
|
|
222
10
|
* @remarks
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
11
|
+
* Unlike Bun, Node keeps websocket subscription wiring in the server adapter,
|
|
12
|
+
* so this manager only specializes the shared manager where runtime behavior
|
|
13
|
+
* actually differs: dependency-graph storage, missing-file tolerance, and how
|
|
14
|
+
* runtime bundle failures disable HMR.
|
|
226
15
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
emit: async (normalizedEntrypoint, outputPath) => await this.emitScriptEntrypoint(normalizedEntrypoint, outputPath),
|
|
230
|
-
getMissingOutputError: (normalizedEntrypoint) => new Error(`[HMR] Failed to register script entrypoint: ${normalizedEntrypoint}`)
|
|
231
|
-
});
|
|
16
|
+
constructor({ appConfig, bridge }) {
|
|
17
|
+
super({ appConfig, bridge });
|
|
232
18
|
}
|
|
233
19
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* @remarks
|
|
237
|
-
* The flow is:
|
|
238
|
-
* 1. Reserve the output URL in the watched map.
|
|
239
|
-
* 2. Remove any stale emitted file from an earlier process or failed build.
|
|
240
|
-
* 3. Let the strategy chain try to emit the entrypoint without broadcasting.
|
|
241
|
-
* 4. Fail if the owning integration did not emit the expected output.
|
|
20
|
+
* Reuses the shared in-memory dependency graph when the app already has one and
|
|
21
|
+
* otherwise creates the default Node development graph.
|
|
242
22
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
23
|
+
createEntrypointDependencyGraph(existingEntrypointDependencyGraph) {
|
|
24
|
+
return existingEntrypointDependencyGraph instanceof InMemoryEntrypointDependencyGraph ? existingEntrypointDependencyGraph : new InMemoryEntrypointDependencyGraph();
|
|
245
25
|
}
|
|
246
26
|
/**
|
|
247
|
-
*
|
|
27
|
+
* Tells the shared HMR manager to ignore missing-file events during Node watch
|
|
28
|
+
* mode.
|
|
248
29
|
*
|
|
249
30
|
* @remarks
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
31
|
+
* Node file watching can briefly report remove-and-recreate sequences while a
|
|
32
|
+
* file is being rewritten. Treating those transient gaps as fatal would disable
|
|
33
|
+
* otherwise healthy development sessions.
|
|
253
34
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const buildResult = await this.browserBundleService.bundle({
|
|
257
|
-
profile: "hmr-entrypoint",
|
|
258
|
-
entrypoints: [entrypointPath],
|
|
259
|
-
outdir: this.distDir,
|
|
260
|
-
naming,
|
|
261
|
-
minify: false,
|
|
262
|
-
plugins: this.plugins
|
|
263
|
-
});
|
|
264
|
-
if (!buildResult.success) {
|
|
265
|
-
appLogger.error(`[HMR] Generic script entrypoint build failed for ${entrypointPath}:`, buildResult.logs);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const entrypointDependencies = buildResult.dependencyGraph?.entrypoints?.[entrypointPath];
|
|
269
|
-
if (entrypointDependencies) {
|
|
270
|
-
this.entrypointDependencyGraph.setEntrypointDependencies(entrypointPath, entrypointDependencies);
|
|
271
|
-
}
|
|
35
|
+
shouldSkipMissingFileChange(_filePath) {
|
|
36
|
+
return true;
|
|
272
37
|
}
|
|
273
38
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* @remarks
|
|
277
|
-
* The manager intentionally does not remove emitted `_hmr` files from disk
|
|
278
|
-
* because multiple app processes may share the same dist directory during test
|
|
279
|
-
* runs. It does clear in-memory indexes so old entrypoints and dependencies
|
|
280
|
-
* cannot leak across a reused manager instance.
|
|
39
|
+
* Disables HMR after a runtime bundle failure so the Node dev server can keep
|
|
40
|
+
* serving requests without repeatedly emitting broken updates.
|
|
281
41
|
*/
|
|
282
|
-
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
watcher.close();
|
|
286
|
-
}
|
|
287
|
-
this.watchers.clear();
|
|
288
|
-
this.watchedFiles.clear();
|
|
289
|
-
this.entrypointDependencyGraph.reset();
|
|
290
|
-
this.plugins = [];
|
|
42
|
+
onRuntimeBundleFailure(error) {
|
|
43
|
+
this.enabled = false;
|
|
44
|
+
appLogger.error("[HMR] Failed to build runtime script; continuing with HMR disabled.", error);
|
|
291
45
|
}
|
|
292
46
|
}
|
|
293
47
|
export {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createServer, type Server as NodeServerInstance } from 'node:http';
|
|
2
|
+
import type { RuntimeHost, RuntimeHostStartOptions } from '../shared/runtime-host.js';
|
|
3
|
+
import { NodeHttpRequestBridge } from './http-request-bridge.js';
|
|
4
|
+
type NodeServerFactory = typeof createServer;
|
|
5
|
+
/**
|
|
6
|
+
* Node runtime host that binds the shared Web-request pipeline onto a concrete
|
|
7
|
+
* Node HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The host owns only transport concerns: creating the Node listener, converting
|
|
11
|
+
* requests through `NodeHttpRequestBridge`, and handling socket-level failures.
|
|
12
|
+
* Routing and rendering remain in the shared server adapter.
|
|
13
|
+
*/
|
|
14
|
+
export declare class NodeRuntimeHost implements RuntimeHost<NodeServerInstance, {
|
|
15
|
+
port?: number;
|
|
16
|
+
hostname?: string;
|
|
17
|
+
}> {
|
|
18
|
+
private readonly requestBridge;
|
|
19
|
+
private readonly serverFactory;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a Node runtime host with injectable request bridging and server
|
|
22
|
+
* creation seams for tests and alternate hosts.
|
|
23
|
+
*/
|
|
24
|
+
constructor(requestBridge: NodeHttpRequestBridge, serverFactory?: NodeServerFactory);
|
|
25
|
+
/**
|
|
26
|
+
* Starts the Node HTTP server and wires each request through the shared Web
|
|
27
|
+
* request pipeline.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Client disconnects are treated as normal aborts and do not trigger adapter
|
|
31
|
+
* error logging or the runtime host's `onError` callback.
|
|
32
|
+
*/
|
|
33
|
+
start(options: RuntimeHostStartOptions<{
|
|
34
|
+
port?: number;
|
|
35
|
+
hostname?: string;
|
|
36
|
+
}>): Promise<NodeServerInstance>;
|
|
37
|
+
/**
|
|
38
|
+
* Stops the Node HTTP server and, by default, force-closes any remaining open
|
|
39
|
+
* connections.
|
|
40
|
+
*/
|
|
41
|
+
stop(server: NodeServerInstance, options?: {
|
|
42
|
+
force?: boolean;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the public runtime origin from the bound Node listener.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* The host preserves the configured hostname rather than echoing the raw socket
|
|
49
|
+
* address because users care about the requested host contract, not the local
|
|
50
|
+
* bind interface that Node chose internally.
|
|
51
|
+
*/
|
|
52
|
+
getOrigin(server: NodeServerInstance, fallbackServeOptions: {
|
|
53
|
+
port?: number;
|
|
54
|
+
hostname?: string;
|
|
55
|
+
}): string;
|
|
56
|
+
}
|
|
57
|
+
export {};
|