@gcoredev/fastedge-test 0.0.1-beta.4 → 0.1.0-beta.3
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/README.md +6 -6
- package/dist/fastedge-cli/METADATA.json +1 -3
- package/dist/fastedge-cli/{fastedge-run-linux-x64-unkown → fastedge-run-darwin-arm64} +0 -0
- package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
- package/dist/fastedge-cli/fastedge-run.exe +0 -0
- package/dist/frontend/assets/index-CEFjsU8e.js +35 -0
- package/dist/frontend/assets/index-DdlINQc_.css +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/lib/index.cjs +299 -107
- package/dist/lib/index.js +301 -110
- package/dist/lib/runner/HostFunctions.d.ts +8 -0
- package/dist/lib/runner/HttpWasmRunner.d.ts +34 -14
- package/dist/lib/runner/IStateManager.d.ts +3 -2
- package/dist/lib/runner/IWasmRunner.d.ts +16 -1
- package/dist/lib/runner/NullStateManager.d.ts +1 -0
- package/dist/lib/runner/PortManager.d.ts +17 -19
- package/dist/lib/runner/ProxyWasmRunner.d.ts +7 -0
- package/dist/lib/schemas/api.d.ts +8 -2
- package/dist/lib/schemas/config.d.ts +4 -1
- package/dist/lib/test-framework/index.cjs +301 -108
- package/dist/lib/test-framework/index.js +303 -111
- package/dist/lib/test-framework/suite-runner.d.ts +1 -1
- package/dist/server.js +30 -29
- package/docs/API.md +758 -360
- package/docs/DEBUGGER.md +151 -0
- package/docs/INDEX.md +111 -0
- package/docs/RUNNER.md +582 -0
- package/docs/TEST_CONFIG.md +242 -0
- package/docs/TEST_FRAMEWORK.md +384 -284
- package/docs/WEBSOCKET.md +499 -0
- package/docs/quickstart.md +171 -0
- package/llms.txt +72 -14
- package/package.json +15 -5
- package/schemas/api-config.schema.json +12 -5
- package/schemas/api-load.schema.json +11 -6
- package/schemas/{test-config.schema.json → fastedge-config.test.schema.json} +12 -5
- package/dist/fastedge-cli/.gitkeep +0 -0
- package/dist/frontend/assets/index-CnXStFTd.css +0 -1
- package/dist/frontend/assets/index-FR9Oqsow.js +0 -37
- package/docs/HYBRID_LOADING.md +0 -546
- package/docs/LOCAL_SERVER.md +0 -153
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var __importMetaUrl = require('url').pathToFileURL(__filename).href;
|
|
1
2
|
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -789,6 +790,8 @@ var HostFunctions = class {
|
|
|
789
790
|
this.pendingHttpCall = null;
|
|
790
791
|
this.httpCallResponse = null;
|
|
791
792
|
this.streamClosed = false;
|
|
793
|
+
// Local response state (from proxy_send_local_response / send_http_response)
|
|
794
|
+
this.localResponse = null;
|
|
792
795
|
this.memory = memory;
|
|
793
796
|
this.propertyResolver = propertyResolver;
|
|
794
797
|
this.propertyAccessControl = propertyAccessControl;
|
|
@@ -843,6 +846,16 @@ var HostFunctions = class {
|
|
|
843
846
|
resetStreamClosed() {
|
|
844
847
|
this.streamClosed = false;
|
|
845
848
|
}
|
|
849
|
+
// Local response accessors (called by ProxyWasmRunner after callHook)
|
|
850
|
+
hasLocalResponse() {
|
|
851
|
+
return this.localResponse !== null;
|
|
852
|
+
}
|
|
853
|
+
getLocalResponse() {
|
|
854
|
+
return this.localResponse;
|
|
855
|
+
}
|
|
856
|
+
resetLocalResponse() {
|
|
857
|
+
this.localResponse = null;
|
|
858
|
+
}
|
|
846
859
|
getRequestHeaders() {
|
|
847
860
|
return this.requestHeaders;
|
|
848
861
|
}
|
|
@@ -1103,15 +1116,21 @@ var HostFunctions = class {
|
|
|
1103
1116
|
this.currentContextId = contextId || this.currentContextId;
|
|
1104
1117
|
return 0 /* Ok */;
|
|
1105
1118
|
},
|
|
1106
|
-
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, grpcStatus) => {
|
|
1119
|
+
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, headerPairsPtr, headerPairsLen, grpcStatus) => {
|
|
1107
1120
|
this.setLastHostCall(
|
|
1108
1121
|
`proxy_send_local_response status=${statusCode} bodyLen=${bodyLen}`
|
|
1109
1122
|
);
|
|
1110
1123
|
const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
|
|
1111
|
-
const body = this.memory.
|
|
1124
|
+
const body = this.memory.readBytes(bodyPtr, bodyLen);
|
|
1125
|
+
if (headerPairsLen > 0) {
|
|
1126
|
+
const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
|
|
1127
|
+
const headers = HeaderManager.deserializeBinary(headerBytes);
|
|
1128
|
+
this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
|
|
1129
|
+
}
|
|
1130
|
+
this.localResponse = { statusCode, statusText, body };
|
|
1112
1131
|
this.logs.push({
|
|
1113
1132
|
level: 1,
|
|
1114
|
-
message: `local_response status=${statusCode} ${statusText}
|
|
1133
|
+
message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
|
|
1115
1134
|
});
|
|
1116
1135
|
return 0 /* Ok */;
|
|
1117
1136
|
},
|
|
@@ -1691,7 +1710,6 @@ async function loadDotenvFiles(dotenvPath = ".") {
|
|
|
1691
1710
|
// server/runner/ProxyWasmRunner.ts
|
|
1692
1711
|
var textEncoder4 = new TextEncoder();
|
|
1693
1712
|
var ProxyWasmRunner = class {
|
|
1694
|
-
// Default to enabled
|
|
1695
1713
|
constructor(fastEdgeConfig, dotenvEnabled = true) {
|
|
1696
1714
|
this.module = null;
|
|
1697
1715
|
// Compiled module (reused)
|
|
@@ -1705,6 +1723,8 @@ var ProxyWasmRunner = class {
|
|
|
1705
1723
|
this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1706
1724
|
this.stateManager = null;
|
|
1707
1725
|
this.dotenvEnabled = true;
|
|
1726
|
+
// Default to enabled
|
|
1727
|
+
this.dotenvPath = ".";
|
|
1708
1728
|
this.memory = new MemoryManager();
|
|
1709
1729
|
this.propertyResolver = new PropertyResolver();
|
|
1710
1730
|
this.propertyAccessControl = new PropertyAccessControl();
|
|
@@ -1739,7 +1759,7 @@ var ProxyWasmRunner = class {
|
|
|
1739
1759
|
return;
|
|
1740
1760
|
}
|
|
1741
1761
|
try {
|
|
1742
|
-
const dotenvConfig = await loadDotenvFiles(
|
|
1762
|
+
const dotenvConfig = await loadDotenvFiles(this.dotenvPath);
|
|
1743
1763
|
if (dotenvConfig.secrets && Object.keys(dotenvConfig.secrets).length > 0) {
|
|
1744
1764
|
const existingSecrets = this.secretStore.getAll();
|
|
1745
1765
|
this.secretStore = new SecretStore({
|
|
@@ -1771,9 +1791,37 @@ var ProxyWasmRunner = class {
|
|
|
1771
1791
|
console.error("Failed to load dotenv files:", error);
|
|
1772
1792
|
}
|
|
1773
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Apply dotenv settings to the running module without recompiling.
|
|
1796
|
+
* Resets SecretStore/Dictionary to empty, then re-loads dotenv files
|
|
1797
|
+
* using the new settings. The compiled WASM module is untouched.
|
|
1798
|
+
*/
|
|
1799
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
1800
|
+
this.dotenvEnabled = enabled;
|
|
1801
|
+
if (dotenvPath !== void 0) {
|
|
1802
|
+
this.dotenvPath = dotenvPath;
|
|
1803
|
+
}
|
|
1804
|
+
this.secretStore = new SecretStore();
|
|
1805
|
+
this.dictionary = new Dictionary();
|
|
1806
|
+
await this.loadDotenvIfEnabled();
|
|
1807
|
+
if (!enabled) {
|
|
1808
|
+
this.hostFunctions = new HostFunctions(
|
|
1809
|
+
this.memory,
|
|
1810
|
+
this.propertyResolver,
|
|
1811
|
+
this.propertyAccessControl,
|
|
1812
|
+
() => this.currentHook,
|
|
1813
|
+
this.debug,
|
|
1814
|
+
this.secretStore,
|
|
1815
|
+
this.dictionary
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1774
1819
|
async load(bufferOrPath, config) {
|
|
1775
|
-
if (config?.
|
|
1776
|
-
this.dotenvEnabled = config.
|
|
1820
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
1821
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
1822
|
+
}
|
|
1823
|
+
if (config?.dotenv?.path !== void 0) {
|
|
1824
|
+
this.dotenvPath = config.dotenv.path;
|
|
1777
1825
|
}
|
|
1778
1826
|
this.resetState();
|
|
1779
1827
|
let buffer;
|
|
@@ -1844,6 +1892,25 @@ var ProxyWasmRunner = class {
|
|
|
1844
1892
|
"system"
|
|
1845
1893
|
);
|
|
1846
1894
|
}
|
|
1895
|
+
if (results.onRequestHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1896
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1897
|
+
const responseHeaders = results.onRequestHeaders.output.response.headers;
|
|
1898
|
+
this.hostFunctions.resetLocalResponse();
|
|
1899
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1900
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1901
|
+
return {
|
|
1902
|
+
hookResults: results,
|
|
1903
|
+
finalResponse: {
|
|
1904
|
+
status: local.statusCode,
|
|
1905
|
+
statusText: local.statusText,
|
|
1906
|
+
headers: responseHeaders,
|
|
1907
|
+
body,
|
|
1908
|
+
contentType,
|
|
1909
|
+
isBase64
|
|
1910
|
+
},
|
|
1911
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1847
1914
|
const headersAfterRequestHeaders = results.onRequestHeaders.output.request.headers;
|
|
1848
1915
|
const propertiesAfterRequestHeaders = results.onRequestHeaders.properties;
|
|
1849
1916
|
this.logDebug(
|
|
@@ -1868,6 +1935,25 @@ var ProxyWasmRunner = class {
|
|
|
1868
1935
|
"system"
|
|
1869
1936
|
);
|
|
1870
1937
|
}
|
|
1938
|
+
if (results.onRequestBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1939
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1940
|
+
const responseHeaders = results.onRequestBody.output.response.headers;
|
|
1941
|
+
this.hostFunctions.resetLocalResponse();
|
|
1942
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1943
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1944
|
+
return {
|
|
1945
|
+
hookResults: results,
|
|
1946
|
+
finalResponse: {
|
|
1947
|
+
status: local.statusCode,
|
|
1948
|
+
statusText: local.statusText,
|
|
1949
|
+
headers: responseHeaders,
|
|
1950
|
+
body,
|
|
1951
|
+
contentType,
|
|
1952
|
+
isBase64
|
|
1953
|
+
},
|
|
1954
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1871
1957
|
const modifiedRequestHeaders = results.onRequestBody.output.request.headers;
|
|
1872
1958
|
const modifiedRequestBody = results.onRequestBody.output.request.body;
|
|
1873
1959
|
const propertiesAfterRequestBody = results.onRequestBody.properties;
|
|
@@ -2056,6 +2142,7 @@ var ProxyWasmRunner = class {
|
|
|
2056
2142
|
throw new Error("WASM module not loaded");
|
|
2057
2143
|
}
|
|
2058
2144
|
this.currentHook = this.getHookContext(call.hook);
|
|
2145
|
+
this.hostFunctions.resetLocalResponse();
|
|
2059
2146
|
const imports = this.createImports();
|
|
2060
2147
|
this.instance = await WebAssembly.instantiate(this.module, imports);
|
|
2061
2148
|
const memory = this.instance.exports.memory;
|
|
@@ -2454,7 +2541,7 @@ var ProxyWasmRunner = class {
|
|
|
2454
2541
|
/**
|
|
2455
2542
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
2456
2543
|
*/
|
|
2457
|
-
async execute(
|
|
2544
|
+
async execute(request) {
|
|
2458
2545
|
throw new Error(
|
|
2459
2546
|
"execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
|
|
2460
2547
|
);
|
|
@@ -2489,16 +2576,24 @@ function ensureNullTerminated(value) {
|
|
|
2489
2576
|
}
|
|
2490
2577
|
return value.endsWith("\0") ? value : `${value}\0`;
|
|
2491
2578
|
}
|
|
2579
|
+
function encodeLocalResponseBody(body, contentType) {
|
|
2580
|
+
const isBinary = contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("application/zip");
|
|
2581
|
+
if (isBinary) {
|
|
2582
|
+
return { body: Buffer.from(body).toString("base64"), isBase64: true };
|
|
2583
|
+
}
|
|
2584
|
+
return { body: new TextDecoder().decode(body), isBase64: false };
|
|
2585
|
+
}
|
|
2492
2586
|
|
|
2493
2587
|
// server/runner/HttpWasmRunner.ts
|
|
2494
2588
|
var import_child_process2 = require("child_process");
|
|
2495
|
-
var http = __toESM(require("http"));
|
|
2496
2589
|
|
|
2497
2590
|
// server/utils/fastedge-cli.ts
|
|
2498
2591
|
var import_child_process = require("child_process");
|
|
2499
2592
|
var import_fs = require("fs");
|
|
2500
2593
|
var import_path2 = require("path");
|
|
2594
|
+
var import_url = require("url");
|
|
2501
2595
|
var import_os = __toESM(require("os"));
|
|
2596
|
+
var _currentDir = (0, import_path2.dirname)((0, import_url.fileURLToPath)(__importMetaUrl));
|
|
2502
2597
|
function getCliBinaryName() {
|
|
2503
2598
|
switch (import_os.default.platform()) {
|
|
2504
2599
|
case "win32":
|
|
@@ -2514,19 +2609,30 @@ function getCliBinaryName() {
|
|
|
2514
2609
|
function getBundledCliPaths() {
|
|
2515
2610
|
const binaryName = getCliBinaryName();
|
|
2516
2611
|
return [
|
|
2517
|
-
//
|
|
2518
|
-
(0, import_path2.join)(
|
|
2612
|
+
// Installed npm package: dist/lib/index.js → dist/fastedge-cli/
|
|
2613
|
+
(0, import_path2.join)(_currentDir, "..", "fastedge-cli", binaryName),
|
|
2614
|
+
// Production: bundled server at dist/server.js → dist/fastedge-cli/
|
|
2615
|
+
(0, import_path2.join)(_currentDir, "fastedge-cli", binaryName),
|
|
2519
2616
|
// Development/Tests: running from source
|
|
2520
|
-
//
|
|
2521
|
-
(0, import_path2.join)(
|
|
2522
|
-
// Alternative: if
|
|
2523
|
-
(0, import_path2.join)(
|
|
2617
|
+
// _currentDir might be server/utils/, so go up to project root
|
|
2618
|
+
(0, import_path2.join)(_currentDir, "..", "..", "fastedge-run", binaryName),
|
|
2619
|
+
// Alternative: if _currentDir is already at project root
|
|
2620
|
+
(0, import_path2.join)(_currentDir, "fastedge-run", binaryName)
|
|
2524
2621
|
];
|
|
2525
2622
|
}
|
|
2623
|
+
function ensureExecutable(binaryPath) {
|
|
2624
|
+
if (process.platform !== "win32") {
|
|
2625
|
+
try {
|
|
2626
|
+
(0, import_fs.chmodSync)(binaryPath, 493);
|
|
2627
|
+
} catch {
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2526
2631
|
async function findFastEdgeRunCli() {
|
|
2527
2632
|
const envPath = process.env.FASTEDGE_RUN_PATH;
|
|
2528
2633
|
if (envPath) {
|
|
2529
2634
|
if ((0, import_fs.existsSync)(envPath)) {
|
|
2635
|
+
ensureExecutable(envPath);
|
|
2530
2636
|
return envPath;
|
|
2531
2637
|
} else {
|
|
2532
2638
|
throw new Error(
|
|
@@ -2536,6 +2642,7 @@ async function findFastEdgeRunCli() {
|
|
|
2536
2642
|
}
|
|
2537
2643
|
for (const bundledPath of getBundledCliPaths()) {
|
|
2538
2644
|
if ((0, import_fs.existsSync)(bundledPath)) {
|
|
2645
|
+
ensureExecutable(bundledPath);
|
|
2539
2646
|
return bundledPath;
|
|
2540
2647
|
}
|
|
2541
2648
|
}
|
|
@@ -2583,9 +2690,12 @@ var HttpWasmRunner = class {
|
|
|
2583
2690
|
this.port = null;
|
|
2584
2691
|
this.cliPath = null;
|
|
2585
2692
|
this.tempWasmPath = null;
|
|
2693
|
+
this.currentWasmPath = null;
|
|
2694
|
+
// resolved path used when spawning
|
|
2586
2695
|
this.logs = [];
|
|
2587
2696
|
this.stateManager = null;
|
|
2588
2697
|
this.dotenvEnabled = true;
|
|
2698
|
+
this.dotenvPath = null;
|
|
2589
2699
|
this.portManager = portManager;
|
|
2590
2700
|
this.dotenvEnabled = dotenvEnabled;
|
|
2591
2701
|
}
|
|
@@ -2593,8 +2703,11 @@ var HttpWasmRunner = class {
|
|
|
2593
2703
|
* Load WASM binary and spawn fastedge-run process
|
|
2594
2704
|
*/
|
|
2595
2705
|
async load(bufferOrPath, config) {
|
|
2596
|
-
if (config?.
|
|
2597
|
-
this.dotenvEnabled = config.
|
|
2706
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
2707
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
2708
|
+
}
|
|
2709
|
+
if (config?.dotenv?.path !== void 0) {
|
|
2710
|
+
this.dotenvPath = config.dotenv.path;
|
|
2598
2711
|
}
|
|
2599
2712
|
await this.cleanup();
|
|
2600
2713
|
this.cliPath = await findFastEdgeRunCli();
|
|
@@ -2606,7 +2719,7 @@ var HttpWasmRunner = class {
|
|
|
2606
2719
|
wasmPath = await writeTempWasmFile(bufferOrPath);
|
|
2607
2720
|
this.tempWasmPath = wasmPath;
|
|
2608
2721
|
}
|
|
2609
|
-
this.port = this.portManager.allocate();
|
|
2722
|
+
this.port = await this.portManager.allocate();
|
|
2610
2723
|
const args = [
|
|
2611
2724
|
"http",
|
|
2612
2725
|
"-p",
|
|
@@ -2617,8 +2730,13 @@ var HttpWasmRunner = class {
|
|
|
2617
2730
|
"true"
|
|
2618
2731
|
];
|
|
2619
2732
|
if (this.dotenvEnabled) {
|
|
2620
|
-
|
|
2733
|
+
if (this.dotenvPath) {
|
|
2734
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2735
|
+
} else {
|
|
2736
|
+
args.push("--dotenv");
|
|
2737
|
+
}
|
|
2621
2738
|
}
|
|
2739
|
+
this.currentWasmPath = wasmPath;
|
|
2622
2740
|
this.process = (0, import_child_process2.spawn)(this.cliPath, args, {
|
|
2623
2741
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2624
2742
|
env: {
|
|
@@ -2634,17 +2752,17 @@ var HttpWasmRunner = class {
|
|
|
2634
2752
|
/**
|
|
2635
2753
|
* Execute an HTTP request through the WASM module
|
|
2636
2754
|
*/
|
|
2637
|
-
async execute(
|
|
2755
|
+
async execute(request) {
|
|
2638
2756
|
if (!this.port || !this.process) {
|
|
2639
2757
|
throw new Error("HttpWasmRunner not loaded. Call load() first.");
|
|
2640
2758
|
}
|
|
2641
2759
|
this.logs = [];
|
|
2642
2760
|
try {
|
|
2643
|
-
const url = `http://localhost:${this.port}${
|
|
2761
|
+
const url = `http://localhost:${this.port}${request.path}`;
|
|
2644
2762
|
const response = await fetch(url, {
|
|
2645
|
-
method:
|
|
2646
|
-
headers:
|
|
2647
|
-
body:
|
|
2763
|
+
method: request.method,
|
|
2764
|
+
headers: request.headers,
|
|
2765
|
+
body: request.body || void 0,
|
|
2648
2766
|
signal: AbortSignal.timeout(3e4)
|
|
2649
2767
|
// 30 second timeout
|
|
2650
2768
|
});
|
|
@@ -2672,7 +2790,7 @@ var HttpWasmRunner = class {
|
|
|
2672
2790
|
/**
|
|
2673
2791
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2674
2792
|
*/
|
|
2675
|
-
async callHook(
|
|
2793
|
+
async callHook(_hookCall) {
|
|
2676
2794
|
throw new Error(
|
|
2677
2795
|
"callHook() is not supported for HTTP WASM. Use execute() instead."
|
|
2678
2796
|
);
|
|
@@ -2680,11 +2798,53 @@ var HttpWasmRunner = class {
|
|
|
2680
2798
|
/**
|
|
2681
2799
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2682
2800
|
*/
|
|
2683
|
-
async callFullFlow(
|
|
2801
|
+
async callFullFlow(_url, _method, _headers, _body, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
|
|
2684
2802
|
throw new Error(
|
|
2685
2803
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2686
2804
|
);
|
|
2687
2805
|
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Apply dotenv settings by restarting the fastedge-run process.
|
|
2808
|
+
* The WASM file is not re-read; only the --dotenv flag changes.
|
|
2809
|
+
*/
|
|
2810
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
2811
|
+
this.dotenvEnabled = enabled;
|
|
2812
|
+
if (dotenvPath !== void 0) {
|
|
2813
|
+
this.dotenvPath = dotenvPath;
|
|
2814
|
+
}
|
|
2815
|
+
if (!this.process || !this.currentWasmPath || !this.cliPath || this.port === null) {
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
await this.killProcess();
|
|
2819
|
+
this.process = null;
|
|
2820
|
+
const args = [
|
|
2821
|
+
"http",
|
|
2822
|
+
"-p",
|
|
2823
|
+
this.port.toString(),
|
|
2824
|
+
"-w",
|
|
2825
|
+
this.currentWasmPath,
|
|
2826
|
+
"--wasi-http",
|
|
2827
|
+
"true"
|
|
2828
|
+
];
|
|
2829
|
+
if (this.dotenvEnabled) {
|
|
2830
|
+
if (this.dotenvPath) {
|
|
2831
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2832
|
+
} else {
|
|
2833
|
+
args.push("--dotenv");
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
this.process = (0, import_child_process2.spawn)(this.cliPath, args, {
|
|
2837
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2838
|
+
env: {
|
|
2839
|
+
RUST_LOG: "info",
|
|
2840
|
+
...process.env
|
|
2841
|
+
}
|
|
2842
|
+
});
|
|
2843
|
+
this.setupLogCapture();
|
|
2844
|
+
this.setupErrorHandlers();
|
|
2845
|
+
const timeout = process.env.NODE_ENV === "test" || process.env.VITEST ? 2e4 : 1e4;
|
|
2846
|
+
await this.waitForServerReady(this.port, timeout);
|
|
2847
|
+
}
|
|
2688
2848
|
/**
|
|
2689
2849
|
* Clean up resources
|
|
2690
2850
|
*/
|
|
@@ -2709,12 +2869,41 @@ var HttpWasmRunner = class {
|
|
|
2709
2869
|
getType() {
|
|
2710
2870
|
return "http-wasm";
|
|
2711
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Get the port the fastedge-run HTTP server is listening on
|
|
2874
|
+
*/
|
|
2875
|
+
getPort() {
|
|
2876
|
+
return this.port;
|
|
2877
|
+
}
|
|
2712
2878
|
/**
|
|
2713
2879
|
* Set state manager
|
|
2714
2880
|
*/
|
|
2715
2881
|
setStateManager(stateManager) {
|
|
2716
2882
|
this.stateManager = stateManager;
|
|
2717
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Parse log level from a process output line.
|
|
2886
|
+
* Matches bare prefixes (e.g. "INFO target > msg") and bracketed prefixes (e.g. "[INFO] msg").
|
|
2887
|
+
* Falls back to the provided default if no known level prefix is found.
|
|
2888
|
+
*/
|
|
2889
|
+
parseLogLevel(message, fallback) {
|
|
2890
|
+
const match = message.trimStart().match(/^\[?(\w+)\]?/);
|
|
2891
|
+
const prefix = match?.[1]?.toUpperCase();
|
|
2892
|
+
switch (prefix) {
|
|
2893
|
+
case "TRACE":
|
|
2894
|
+
return 0;
|
|
2895
|
+
case "DEBUG":
|
|
2896
|
+
return 1;
|
|
2897
|
+
case "INFO":
|
|
2898
|
+
return 2;
|
|
2899
|
+
case "WARN":
|
|
2900
|
+
return 3;
|
|
2901
|
+
case "ERROR":
|
|
2902
|
+
return 4;
|
|
2903
|
+
default:
|
|
2904
|
+
return fallback;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2718
2907
|
/**
|
|
2719
2908
|
* Setup log capture from process stdout/stderr
|
|
2720
2909
|
*/
|
|
@@ -2723,13 +2912,17 @@ var HttpWasmRunner = class {
|
|
|
2723
2912
|
this.process.stdout?.on("data", (data) => {
|
|
2724
2913
|
const message = data.toString().trim();
|
|
2725
2914
|
if (message) {
|
|
2726
|
-
|
|
2915
|
+
const log = { level: this.parseLogLevel(message, 2), message };
|
|
2916
|
+
this.logs.push(log);
|
|
2917
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2727
2918
|
}
|
|
2728
2919
|
});
|
|
2729
2920
|
this.process.stderr?.on("data", (data) => {
|
|
2730
2921
|
const message = data.toString().trim();
|
|
2731
2922
|
if (message) {
|
|
2732
|
-
|
|
2923
|
+
const log = { level: this.parseLogLevel(message, 4), message };
|
|
2924
|
+
this.logs.push(log);
|
|
2925
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2733
2926
|
}
|
|
2734
2927
|
});
|
|
2735
2928
|
}
|
|
@@ -2759,66 +2952,54 @@ var HttpWasmRunner = class {
|
|
|
2759
2952
|
});
|
|
2760
2953
|
}
|
|
2761
2954
|
/**
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
{
|
|
2773
|
-
hostname: "localhost",
|
|
2774
|
-
port,
|
|
2775
|
-
path: "/",
|
|
2776
|
-
method: "GET",
|
|
2777
|
-
headers: {
|
|
2778
|
-
"content-length": "0",
|
|
2779
|
-
connection: "close"
|
|
2780
|
-
}
|
|
2781
|
-
},
|
|
2782
|
-
(res) => {
|
|
2783
|
-
res.resume();
|
|
2784
|
-
resolve(true);
|
|
2785
|
-
}
|
|
2786
|
-
);
|
|
2787
|
-
req.setTimeout(timeoutMs, () => {
|
|
2788
|
-
req.destroy();
|
|
2789
|
-
resolve(false);
|
|
2790
|
-
});
|
|
2791
|
-
req.on("error", () => resolve(false));
|
|
2792
|
-
req.end();
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
/**
|
|
2796
|
-
* Wait for server to be ready by polling
|
|
2955
|
+
* Wait for the fastedge-run HTTP server to be ready by watching process logs
|
|
2956
|
+
* for the "Listening on" message.
|
|
2957
|
+
*
|
|
2958
|
+
* We intentionally avoid HTTP probing here. HTTP probes trigger WASM execution
|
|
2959
|
+
* (the app calls event.request.text() to read the body), which can hang for
|
|
2960
|
+
* many seconds in CI due to WASM JIT compilation on the first request, or
|
|
2961
|
+
* because newer fastedge-run builds hold the body stream open regardless of
|
|
2962
|
+
* content-length. Watching logs avoids all of this: fastedge-run emits
|
|
2963
|
+
* "Listening on http://127.0.0.1:<port>" as soon as the HTTP listener is bound,
|
|
2964
|
+
* before any WASM execution occurs.
|
|
2797
2965
|
*/
|
|
2798
|
-
|
|
2966
|
+
waitForServerReady(port, timeoutMs) {
|
|
2799
2967
|
const startTime = Date.now();
|
|
2800
|
-
|
|
2801
|
-
const
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2968
|
+
return new Promise((resolve, reject) => {
|
|
2969
|
+
const check = () => {
|
|
2970
|
+
if (this.process && this.process.exitCode !== null) {
|
|
2971
|
+
return reject(
|
|
2972
|
+
new Error(
|
|
2973
|
+
`FastEdge-run process exited with code ${this.process.exitCode} before server started`
|
|
2974
|
+
)
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
if (this.logs.some((l) => l.message.includes("Listening on"))) {
|
|
2978
|
+
return resolve();
|
|
2979
|
+
}
|
|
2980
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
2981
|
+
const processInfo = this.process ? `Process state: exitCode=${this.process.exitCode}, killed=${this.process.killed}, pid=${this.process.pid}` : "Process is null";
|
|
2982
|
+
const recentLogs = this.logs.slice(-5).map((l) => `[${l.level}] ${l.message}`).join("\n");
|
|
2983
|
+
return reject(
|
|
2984
|
+
new Error(
|
|
2985
|
+
`FastEdge-run server did not start within ${timeoutMs}ms on port ${port}
|
|
2814
2986
|
${processInfo}
|
|
2815
2987
|
Recent logs:
|
|
2816
2988
|
${recentLogs || "(no logs)"}`
|
|
2817
|
-
|
|
2989
|
+
)
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
setTimeout(check, 50);
|
|
2993
|
+
};
|
|
2994
|
+
check();
|
|
2995
|
+
});
|
|
2818
2996
|
}
|
|
2819
2997
|
/**
|
|
2820
|
-
* Kill the process gracefully (SIGINT) with
|
|
2821
|
-
*
|
|
2998
|
+
* Kill the process gracefully (SIGINT) with platform-specific force-kill fallback.
|
|
2999
|
+
* SIGINT is sent first on all platforms — Node.js translates it for Windows.
|
|
3000
|
+
* If the process does not exit within 2 seconds:
|
|
3001
|
+
* - Windows: taskkill /F /T to terminate the process tree
|
|
3002
|
+
* - Unix: SIGKILL
|
|
2822
3003
|
*/
|
|
2823
3004
|
async killProcess() {
|
|
2824
3005
|
if (!this.process) return;
|
|
@@ -2829,8 +3010,18 @@ ${recentLogs || "(no logs)"}`
|
|
|
2829
3010
|
}
|
|
2830
3011
|
this.process.kill("SIGINT");
|
|
2831
3012
|
const timeout = setTimeout(() => {
|
|
2832
|
-
if (this.process &&
|
|
2833
|
-
|
|
3013
|
+
if (this.process && this.process.exitCode === null && this.process.signalCode === null) {
|
|
3014
|
+
if (process.platform === "win32") {
|
|
3015
|
+
const pid = this.process.pid;
|
|
3016
|
+
if (pid) {
|
|
3017
|
+
try {
|
|
3018
|
+
(0, import_child_process2.execSync)(`taskkill /F /T /PID ${pid}`);
|
|
3019
|
+
} catch {
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
} else {
|
|
3023
|
+
this.process.kill("SIGKILL");
|
|
3024
|
+
}
|
|
2834
3025
|
}
|
|
2835
3026
|
resolve();
|
|
2836
3027
|
}, 2e3);
|
|
@@ -2853,9 +3044,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2853
3044
|
"application/zip",
|
|
2854
3045
|
"application/gzip"
|
|
2855
3046
|
];
|
|
2856
|
-
return binaryTypes.some(
|
|
2857
|
-
(type) => contentType.toLowerCase().includes(type)
|
|
2858
|
-
);
|
|
3047
|
+
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
2859
3048
|
}
|
|
2860
3049
|
/**
|
|
2861
3050
|
* Parse headers from fetch Headers object
|
|
@@ -2870,6 +3059,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2870
3059
|
};
|
|
2871
3060
|
|
|
2872
3061
|
// server/runner/PortManager.ts
|
|
3062
|
+
var import_net = require("net");
|
|
2873
3063
|
var PortManager = class {
|
|
2874
3064
|
constructor() {
|
|
2875
3065
|
this.minPort = 8100;
|
|
@@ -2877,18 +3067,31 @@ var PortManager = class {
|
|
|
2877
3067
|
this.allocatedPorts = /* @__PURE__ */ new Set();
|
|
2878
3068
|
this.lastAllocatedPort = this.minPort - 1;
|
|
2879
3069
|
}
|
|
2880
|
-
// Track last allocated port for sequential allocation
|
|
2881
3070
|
/**
|
|
2882
|
-
*
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
3071
|
+
* Check whether a port is actually free at the OS level.
|
|
3072
|
+
* This is necessary when multiple server processes run simultaneously —
|
|
3073
|
+
* each has its own PortManager with independent in-memory state, so
|
|
3074
|
+
* in-memory tracking alone is not enough to prevent cross-process conflicts.
|
|
3075
|
+
*/
|
|
3076
|
+
isPortFree(port) {
|
|
3077
|
+
return new Promise((resolve) => {
|
|
3078
|
+
const server = (0, import_net.createServer)();
|
|
3079
|
+
server.once("error", () => resolve(false));
|
|
3080
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
3081
|
+
server.listen(port, "127.0.0.1");
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Allocate an available port from the pool.
|
|
3086
|
+
* Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
|
|
3087
|
+
* with an OS-level check (avoids cross-process collisions).
|
|
2885
3088
|
* @returns The allocated port number
|
|
2886
3089
|
* @throws Error if no ports are available
|
|
2887
3090
|
*/
|
|
2888
|
-
allocate() {
|
|
3091
|
+
async allocate() {
|
|
2889
3092
|
for (let offset = 1; offset <= this.maxPort - this.minPort + 1; offset++) {
|
|
2890
3093
|
const port = this.minPort + (this.lastAllocatedPort - this.minPort + offset) % (this.maxPort - this.minPort + 1);
|
|
2891
|
-
if (!this.allocatedPorts.has(port)) {
|
|
3094
|
+
if (!this.allocatedPorts.has(port) && await this.isPortFree(port)) {
|
|
2892
3095
|
this.allocatedPorts.add(port);
|
|
2893
3096
|
this.lastAllocatedPort = port;
|
|
2894
3097
|
return port;
|
|
@@ -2900,32 +3103,19 @@ var PortManager = class {
|
|
|
2900
3103
|
}
|
|
2901
3104
|
/**
|
|
2902
3105
|
* Release a previously allocated port back to the pool
|
|
2903
|
-
* @param port The port number to release
|
|
2904
3106
|
*/
|
|
2905
3107
|
release(port) {
|
|
2906
3108
|
this.allocatedPorts.delete(port);
|
|
2907
3109
|
}
|
|
2908
|
-
/**
|
|
2909
|
-
* Get the number of currently allocated ports
|
|
2910
|
-
*/
|
|
2911
3110
|
getAllocatedCount() {
|
|
2912
3111
|
return this.allocatedPorts.size;
|
|
2913
3112
|
}
|
|
2914
|
-
/**
|
|
2915
|
-
* Get the number of available ports
|
|
2916
|
-
*/
|
|
2917
3113
|
getAvailableCount() {
|
|
2918
3114
|
return this.maxPort - this.minPort + 1 - this.allocatedPorts.size;
|
|
2919
3115
|
}
|
|
2920
|
-
/**
|
|
2921
|
-
* Check if a specific port is allocated
|
|
2922
|
-
*/
|
|
2923
3116
|
isAllocated(port) {
|
|
2924
3117
|
return this.allocatedPorts.has(port);
|
|
2925
3118
|
}
|
|
2926
|
-
/**
|
|
2927
|
-
* Reset all allocations (useful for testing)
|
|
2928
|
-
*/
|
|
2929
3119
|
reset() {
|
|
2930
3120
|
this.allocatedPorts.clear();
|
|
2931
3121
|
}
|
|
@@ -2970,9 +3160,9 @@ async function createRunnerFromBuffer(buffer, config) {
|
|
|
2970
3160
|
const wasmType = config?.runnerType ?? await detectWasmType(buffer);
|
|
2971
3161
|
let runner;
|
|
2972
3162
|
if (wasmType === "http-wasm") {
|
|
2973
|
-
runner = new HttpWasmRunner(new PortManager(), config?.
|
|
3163
|
+
runner = new HttpWasmRunner(new PortManager(), config?.dotenv?.enabled ?? false);
|
|
2974
3164
|
} else {
|
|
2975
|
-
runner = new ProxyWasmRunner(void 0, config?.
|
|
3165
|
+
runner = new ProxyWasmRunner(void 0, config?.dotenv?.enabled ?? false);
|
|
2976
3166
|
}
|
|
2977
3167
|
await runner.load(buffer, config);
|
|
2978
3168
|
return runner;
|
|
@@ -3001,7 +3191,10 @@ var TestConfigSchema = import_zod.z.object({
|
|
|
3001
3191
|
request: RequestConfigSchema,
|
|
3002
3192
|
response: ResponseConfigSchema.optional(),
|
|
3003
3193
|
properties: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional().default({}),
|
|
3004
|
-
|
|
3194
|
+
dotenv: import_zod.z.object({
|
|
3195
|
+
enabled: import_zod.z.boolean().optional(),
|
|
3196
|
+
path: import_zod.z.string().optional()
|
|
3197
|
+
}).optional()
|
|
3005
3198
|
});
|
|
3006
3199
|
|
|
3007
3200
|
// server/test-framework/suite-runner.ts
|