@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
|
@@ -736,6 +736,8 @@ var HostFunctions = class {
|
|
|
736
736
|
this.pendingHttpCall = null;
|
|
737
737
|
this.httpCallResponse = null;
|
|
738
738
|
this.streamClosed = false;
|
|
739
|
+
// Local response state (from proxy_send_local_response / send_http_response)
|
|
740
|
+
this.localResponse = null;
|
|
739
741
|
this.memory = memory;
|
|
740
742
|
this.propertyResolver = propertyResolver;
|
|
741
743
|
this.propertyAccessControl = propertyAccessControl;
|
|
@@ -790,6 +792,16 @@ var HostFunctions = class {
|
|
|
790
792
|
resetStreamClosed() {
|
|
791
793
|
this.streamClosed = false;
|
|
792
794
|
}
|
|
795
|
+
// Local response accessors (called by ProxyWasmRunner after callHook)
|
|
796
|
+
hasLocalResponse() {
|
|
797
|
+
return this.localResponse !== null;
|
|
798
|
+
}
|
|
799
|
+
getLocalResponse() {
|
|
800
|
+
return this.localResponse;
|
|
801
|
+
}
|
|
802
|
+
resetLocalResponse() {
|
|
803
|
+
this.localResponse = null;
|
|
804
|
+
}
|
|
793
805
|
getRequestHeaders() {
|
|
794
806
|
return this.requestHeaders;
|
|
795
807
|
}
|
|
@@ -1050,15 +1062,21 @@ var HostFunctions = class {
|
|
|
1050
1062
|
this.currentContextId = contextId || this.currentContextId;
|
|
1051
1063
|
return 0 /* Ok */;
|
|
1052
1064
|
},
|
|
1053
|
-
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, grpcStatus) => {
|
|
1065
|
+
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, headerPairsPtr, headerPairsLen, grpcStatus) => {
|
|
1054
1066
|
this.setLastHostCall(
|
|
1055
1067
|
`proxy_send_local_response status=${statusCode} bodyLen=${bodyLen}`
|
|
1056
1068
|
);
|
|
1057
1069
|
const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
|
|
1058
|
-
const body = this.memory.
|
|
1070
|
+
const body = this.memory.readBytes(bodyPtr, bodyLen);
|
|
1071
|
+
if (headerPairsLen > 0) {
|
|
1072
|
+
const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
|
|
1073
|
+
const headers = HeaderManager.deserializeBinary(headerBytes);
|
|
1074
|
+
this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
|
|
1075
|
+
}
|
|
1076
|
+
this.localResponse = { statusCode, statusText, body };
|
|
1059
1077
|
this.logs.push({
|
|
1060
1078
|
level: 1,
|
|
1061
|
-
message: `local_response status=${statusCode} ${statusText}
|
|
1079
|
+
message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
|
|
1062
1080
|
});
|
|
1063
1081
|
return 0 /* Ok */;
|
|
1064
1082
|
},
|
|
@@ -1638,7 +1656,6 @@ async function loadDotenvFiles(dotenvPath = ".") {
|
|
|
1638
1656
|
// server/runner/ProxyWasmRunner.ts
|
|
1639
1657
|
var textEncoder4 = new TextEncoder();
|
|
1640
1658
|
var ProxyWasmRunner = class {
|
|
1641
|
-
// Default to enabled
|
|
1642
1659
|
constructor(fastEdgeConfig, dotenvEnabled = true) {
|
|
1643
1660
|
this.module = null;
|
|
1644
1661
|
// Compiled module (reused)
|
|
@@ -1652,6 +1669,8 @@ var ProxyWasmRunner = class {
|
|
|
1652
1669
|
this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1653
1670
|
this.stateManager = null;
|
|
1654
1671
|
this.dotenvEnabled = true;
|
|
1672
|
+
// Default to enabled
|
|
1673
|
+
this.dotenvPath = ".";
|
|
1655
1674
|
this.memory = new MemoryManager();
|
|
1656
1675
|
this.propertyResolver = new PropertyResolver();
|
|
1657
1676
|
this.propertyAccessControl = new PropertyAccessControl();
|
|
@@ -1686,7 +1705,7 @@ var ProxyWasmRunner = class {
|
|
|
1686
1705
|
return;
|
|
1687
1706
|
}
|
|
1688
1707
|
try {
|
|
1689
|
-
const dotenvConfig = await loadDotenvFiles(
|
|
1708
|
+
const dotenvConfig = await loadDotenvFiles(this.dotenvPath);
|
|
1690
1709
|
if (dotenvConfig.secrets && Object.keys(dotenvConfig.secrets).length > 0) {
|
|
1691
1710
|
const existingSecrets = this.secretStore.getAll();
|
|
1692
1711
|
this.secretStore = new SecretStore({
|
|
@@ -1718,9 +1737,37 @@ var ProxyWasmRunner = class {
|
|
|
1718
1737
|
console.error("Failed to load dotenv files:", error);
|
|
1719
1738
|
}
|
|
1720
1739
|
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Apply dotenv settings to the running module without recompiling.
|
|
1742
|
+
* Resets SecretStore/Dictionary to empty, then re-loads dotenv files
|
|
1743
|
+
* using the new settings. The compiled WASM module is untouched.
|
|
1744
|
+
*/
|
|
1745
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
1746
|
+
this.dotenvEnabled = enabled;
|
|
1747
|
+
if (dotenvPath !== void 0) {
|
|
1748
|
+
this.dotenvPath = dotenvPath;
|
|
1749
|
+
}
|
|
1750
|
+
this.secretStore = new SecretStore();
|
|
1751
|
+
this.dictionary = new Dictionary();
|
|
1752
|
+
await this.loadDotenvIfEnabled();
|
|
1753
|
+
if (!enabled) {
|
|
1754
|
+
this.hostFunctions = new HostFunctions(
|
|
1755
|
+
this.memory,
|
|
1756
|
+
this.propertyResolver,
|
|
1757
|
+
this.propertyAccessControl,
|
|
1758
|
+
() => this.currentHook,
|
|
1759
|
+
this.debug,
|
|
1760
|
+
this.secretStore,
|
|
1761
|
+
this.dictionary
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1721
1765
|
async load(bufferOrPath, config) {
|
|
1722
|
-
if (config?.
|
|
1723
|
-
this.dotenvEnabled = config.
|
|
1766
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
1767
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
1768
|
+
}
|
|
1769
|
+
if (config?.dotenv?.path !== void 0) {
|
|
1770
|
+
this.dotenvPath = config.dotenv.path;
|
|
1724
1771
|
}
|
|
1725
1772
|
this.resetState();
|
|
1726
1773
|
let buffer;
|
|
@@ -1791,6 +1838,25 @@ var ProxyWasmRunner = class {
|
|
|
1791
1838
|
"system"
|
|
1792
1839
|
);
|
|
1793
1840
|
}
|
|
1841
|
+
if (results.onRequestHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1842
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1843
|
+
const responseHeaders = results.onRequestHeaders.output.response.headers;
|
|
1844
|
+
this.hostFunctions.resetLocalResponse();
|
|
1845
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1846
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1847
|
+
return {
|
|
1848
|
+
hookResults: results,
|
|
1849
|
+
finalResponse: {
|
|
1850
|
+
status: local.statusCode,
|
|
1851
|
+
statusText: local.statusText,
|
|
1852
|
+
headers: responseHeaders,
|
|
1853
|
+
body,
|
|
1854
|
+
contentType,
|
|
1855
|
+
isBase64
|
|
1856
|
+
},
|
|
1857
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1794
1860
|
const headersAfterRequestHeaders = results.onRequestHeaders.output.request.headers;
|
|
1795
1861
|
const propertiesAfterRequestHeaders = results.onRequestHeaders.properties;
|
|
1796
1862
|
this.logDebug(
|
|
@@ -1815,6 +1881,25 @@ var ProxyWasmRunner = class {
|
|
|
1815
1881
|
"system"
|
|
1816
1882
|
);
|
|
1817
1883
|
}
|
|
1884
|
+
if (results.onRequestBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1885
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1886
|
+
const responseHeaders = results.onRequestBody.output.response.headers;
|
|
1887
|
+
this.hostFunctions.resetLocalResponse();
|
|
1888
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1889
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1890
|
+
return {
|
|
1891
|
+
hookResults: results,
|
|
1892
|
+
finalResponse: {
|
|
1893
|
+
status: local.statusCode,
|
|
1894
|
+
statusText: local.statusText,
|
|
1895
|
+
headers: responseHeaders,
|
|
1896
|
+
body,
|
|
1897
|
+
contentType,
|
|
1898
|
+
isBase64
|
|
1899
|
+
},
|
|
1900
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1818
1903
|
const modifiedRequestHeaders = results.onRequestBody.output.request.headers;
|
|
1819
1904
|
const modifiedRequestBody = results.onRequestBody.output.request.body;
|
|
1820
1905
|
const propertiesAfterRequestBody = results.onRequestBody.properties;
|
|
@@ -2003,6 +2088,7 @@ var ProxyWasmRunner = class {
|
|
|
2003
2088
|
throw new Error("WASM module not loaded");
|
|
2004
2089
|
}
|
|
2005
2090
|
this.currentHook = this.getHookContext(call.hook);
|
|
2091
|
+
this.hostFunctions.resetLocalResponse();
|
|
2006
2092
|
const imports = this.createImports();
|
|
2007
2093
|
this.instance = await WebAssembly.instantiate(this.module, imports);
|
|
2008
2094
|
const memory = this.instance.exports.memory;
|
|
@@ -2401,7 +2487,7 @@ var ProxyWasmRunner = class {
|
|
|
2401
2487
|
/**
|
|
2402
2488
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
2403
2489
|
*/
|
|
2404
|
-
async execute(
|
|
2490
|
+
async execute(request) {
|
|
2405
2491
|
throw new Error(
|
|
2406
2492
|
"execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
|
|
2407
2493
|
);
|
|
@@ -2436,16 +2522,24 @@ function ensureNullTerminated(value) {
|
|
|
2436
2522
|
}
|
|
2437
2523
|
return value.endsWith("\0") ? value : `${value}\0`;
|
|
2438
2524
|
}
|
|
2525
|
+
function encodeLocalResponseBody(body, contentType) {
|
|
2526
|
+
const isBinary = contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("application/zip");
|
|
2527
|
+
if (isBinary) {
|
|
2528
|
+
return { body: Buffer.from(body).toString("base64"), isBase64: true };
|
|
2529
|
+
}
|
|
2530
|
+
return { body: new TextDecoder().decode(body), isBase64: false };
|
|
2531
|
+
}
|
|
2439
2532
|
|
|
2440
2533
|
// server/runner/HttpWasmRunner.ts
|
|
2441
|
-
import { spawn } from "child_process";
|
|
2442
|
-
import * as http from "http";
|
|
2534
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
2443
2535
|
|
|
2444
2536
|
// server/utils/fastedge-cli.ts
|
|
2445
2537
|
import { execSync } from "child_process";
|
|
2446
|
-
import { existsSync } from "fs";
|
|
2447
|
-
import { join } from "path";
|
|
2538
|
+
import { existsSync, chmodSync } from "fs";
|
|
2539
|
+
import { join, dirname } from "path";
|
|
2540
|
+
import { fileURLToPath } from "url";
|
|
2448
2541
|
import os from "os";
|
|
2542
|
+
var _currentDir = dirname(fileURLToPath(import.meta.url));
|
|
2449
2543
|
function getCliBinaryName() {
|
|
2450
2544
|
switch (os.platform()) {
|
|
2451
2545
|
case "win32":
|
|
@@ -2461,19 +2555,30 @@ function getCliBinaryName() {
|
|
|
2461
2555
|
function getBundledCliPaths() {
|
|
2462
2556
|
const binaryName = getCliBinaryName();
|
|
2463
2557
|
return [
|
|
2464
|
-
//
|
|
2465
|
-
join(
|
|
2558
|
+
// Installed npm package: dist/lib/index.js → dist/fastedge-cli/
|
|
2559
|
+
join(_currentDir, "..", "fastedge-cli", binaryName),
|
|
2560
|
+
// Production: bundled server at dist/server.js → dist/fastedge-cli/
|
|
2561
|
+
join(_currentDir, "fastedge-cli", binaryName),
|
|
2466
2562
|
// Development/Tests: running from source
|
|
2467
|
-
//
|
|
2468
|
-
join(
|
|
2469
|
-
// Alternative: if
|
|
2470
|
-
join(
|
|
2563
|
+
// _currentDir might be server/utils/, so go up to project root
|
|
2564
|
+
join(_currentDir, "..", "..", "fastedge-run", binaryName),
|
|
2565
|
+
// Alternative: if _currentDir is already at project root
|
|
2566
|
+
join(_currentDir, "fastedge-run", binaryName)
|
|
2471
2567
|
];
|
|
2472
2568
|
}
|
|
2569
|
+
function ensureExecutable(binaryPath) {
|
|
2570
|
+
if (process.platform !== "win32") {
|
|
2571
|
+
try {
|
|
2572
|
+
chmodSync(binaryPath, 493);
|
|
2573
|
+
} catch {
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2473
2577
|
async function findFastEdgeRunCli() {
|
|
2474
2578
|
const envPath = process.env.FASTEDGE_RUN_PATH;
|
|
2475
2579
|
if (envPath) {
|
|
2476
2580
|
if (existsSync(envPath)) {
|
|
2581
|
+
ensureExecutable(envPath);
|
|
2477
2582
|
return envPath;
|
|
2478
2583
|
} else {
|
|
2479
2584
|
throw new Error(
|
|
@@ -2483,6 +2588,7 @@ async function findFastEdgeRunCli() {
|
|
|
2483
2588
|
}
|
|
2484
2589
|
for (const bundledPath of getBundledCliPaths()) {
|
|
2485
2590
|
if (existsSync(bundledPath)) {
|
|
2591
|
+
ensureExecutable(bundledPath);
|
|
2486
2592
|
return bundledPath;
|
|
2487
2593
|
}
|
|
2488
2594
|
}
|
|
@@ -2530,9 +2636,12 @@ var HttpWasmRunner = class {
|
|
|
2530
2636
|
this.port = null;
|
|
2531
2637
|
this.cliPath = null;
|
|
2532
2638
|
this.tempWasmPath = null;
|
|
2639
|
+
this.currentWasmPath = null;
|
|
2640
|
+
// resolved path used when spawning
|
|
2533
2641
|
this.logs = [];
|
|
2534
2642
|
this.stateManager = null;
|
|
2535
2643
|
this.dotenvEnabled = true;
|
|
2644
|
+
this.dotenvPath = null;
|
|
2536
2645
|
this.portManager = portManager;
|
|
2537
2646
|
this.dotenvEnabled = dotenvEnabled;
|
|
2538
2647
|
}
|
|
@@ -2540,8 +2649,11 @@ var HttpWasmRunner = class {
|
|
|
2540
2649
|
* Load WASM binary and spawn fastedge-run process
|
|
2541
2650
|
*/
|
|
2542
2651
|
async load(bufferOrPath, config) {
|
|
2543
|
-
if (config?.
|
|
2544
|
-
this.dotenvEnabled = config.
|
|
2652
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
2653
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
2654
|
+
}
|
|
2655
|
+
if (config?.dotenv?.path !== void 0) {
|
|
2656
|
+
this.dotenvPath = config.dotenv.path;
|
|
2545
2657
|
}
|
|
2546
2658
|
await this.cleanup();
|
|
2547
2659
|
this.cliPath = await findFastEdgeRunCli();
|
|
@@ -2553,7 +2665,7 @@ var HttpWasmRunner = class {
|
|
|
2553
2665
|
wasmPath = await writeTempWasmFile(bufferOrPath);
|
|
2554
2666
|
this.tempWasmPath = wasmPath;
|
|
2555
2667
|
}
|
|
2556
|
-
this.port = this.portManager.allocate();
|
|
2668
|
+
this.port = await this.portManager.allocate();
|
|
2557
2669
|
const args = [
|
|
2558
2670
|
"http",
|
|
2559
2671
|
"-p",
|
|
@@ -2564,8 +2676,13 @@ var HttpWasmRunner = class {
|
|
|
2564
2676
|
"true"
|
|
2565
2677
|
];
|
|
2566
2678
|
if (this.dotenvEnabled) {
|
|
2567
|
-
|
|
2679
|
+
if (this.dotenvPath) {
|
|
2680
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2681
|
+
} else {
|
|
2682
|
+
args.push("--dotenv");
|
|
2683
|
+
}
|
|
2568
2684
|
}
|
|
2685
|
+
this.currentWasmPath = wasmPath;
|
|
2569
2686
|
this.process = spawn(this.cliPath, args, {
|
|
2570
2687
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2571
2688
|
env: {
|
|
@@ -2581,17 +2698,17 @@ var HttpWasmRunner = class {
|
|
|
2581
2698
|
/**
|
|
2582
2699
|
* Execute an HTTP request through the WASM module
|
|
2583
2700
|
*/
|
|
2584
|
-
async execute(
|
|
2701
|
+
async execute(request) {
|
|
2585
2702
|
if (!this.port || !this.process) {
|
|
2586
2703
|
throw new Error("HttpWasmRunner not loaded. Call load() first.");
|
|
2587
2704
|
}
|
|
2588
2705
|
this.logs = [];
|
|
2589
2706
|
try {
|
|
2590
|
-
const url = `http://localhost:${this.port}${
|
|
2707
|
+
const url = `http://localhost:${this.port}${request.path}`;
|
|
2591
2708
|
const response = await fetch(url, {
|
|
2592
|
-
method:
|
|
2593
|
-
headers:
|
|
2594
|
-
body:
|
|
2709
|
+
method: request.method,
|
|
2710
|
+
headers: request.headers,
|
|
2711
|
+
body: request.body || void 0,
|
|
2595
2712
|
signal: AbortSignal.timeout(3e4)
|
|
2596
2713
|
// 30 second timeout
|
|
2597
2714
|
});
|
|
@@ -2619,7 +2736,7 @@ var HttpWasmRunner = class {
|
|
|
2619
2736
|
/**
|
|
2620
2737
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2621
2738
|
*/
|
|
2622
|
-
async callHook(
|
|
2739
|
+
async callHook(_hookCall) {
|
|
2623
2740
|
throw new Error(
|
|
2624
2741
|
"callHook() is not supported for HTTP WASM. Use execute() instead."
|
|
2625
2742
|
);
|
|
@@ -2627,11 +2744,53 @@ var HttpWasmRunner = class {
|
|
|
2627
2744
|
/**
|
|
2628
2745
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2629
2746
|
*/
|
|
2630
|
-
async callFullFlow(
|
|
2747
|
+
async callFullFlow(_url, _method, _headers, _body, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
|
|
2631
2748
|
throw new Error(
|
|
2632
2749
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2633
2750
|
);
|
|
2634
2751
|
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Apply dotenv settings by restarting the fastedge-run process.
|
|
2754
|
+
* The WASM file is not re-read; only the --dotenv flag changes.
|
|
2755
|
+
*/
|
|
2756
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
2757
|
+
this.dotenvEnabled = enabled;
|
|
2758
|
+
if (dotenvPath !== void 0) {
|
|
2759
|
+
this.dotenvPath = dotenvPath;
|
|
2760
|
+
}
|
|
2761
|
+
if (!this.process || !this.currentWasmPath || !this.cliPath || this.port === null) {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
await this.killProcess();
|
|
2765
|
+
this.process = null;
|
|
2766
|
+
const args = [
|
|
2767
|
+
"http",
|
|
2768
|
+
"-p",
|
|
2769
|
+
this.port.toString(),
|
|
2770
|
+
"-w",
|
|
2771
|
+
this.currentWasmPath,
|
|
2772
|
+
"--wasi-http",
|
|
2773
|
+
"true"
|
|
2774
|
+
];
|
|
2775
|
+
if (this.dotenvEnabled) {
|
|
2776
|
+
if (this.dotenvPath) {
|
|
2777
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2778
|
+
} else {
|
|
2779
|
+
args.push("--dotenv");
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
this.process = spawn(this.cliPath, args, {
|
|
2783
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2784
|
+
env: {
|
|
2785
|
+
RUST_LOG: "info",
|
|
2786
|
+
...process.env
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
this.setupLogCapture();
|
|
2790
|
+
this.setupErrorHandlers();
|
|
2791
|
+
const timeout = process.env.NODE_ENV === "test" || process.env.VITEST ? 2e4 : 1e4;
|
|
2792
|
+
await this.waitForServerReady(this.port, timeout);
|
|
2793
|
+
}
|
|
2635
2794
|
/**
|
|
2636
2795
|
* Clean up resources
|
|
2637
2796
|
*/
|
|
@@ -2656,12 +2815,41 @@ var HttpWasmRunner = class {
|
|
|
2656
2815
|
getType() {
|
|
2657
2816
|
return "http-wasm";
|
|
2658
2817
|
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Get the port the fastedge-run HTTP server is listening on
|
|
2820
|
+
*/
|
|
2821
|
+
getPort() {
|
|
2822
|
+
return this.port;
|
|
2823
|
+
}
|
|
2659
2824
|
/**
|
|
2660
2825
|
* Set state manager
|
|
2661
2826
|
*/
|
|
2662
2827
|
setStateManager(stateManager) {
|
|
2663
2828
|
this.stateManager = stateManager;
|
|
2664
2829
|
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Parse log level from a process output line.
|
|
2832
|
+
* Matches bare prefixes (e.g. "INFO target > msg") and bracketed prefixes (e.g. "[INFO] msg").
|
|
2833
|
+
* Falls back to the provided default if no known level prefix is found.
|
|
2834
|
+
*/
|
|
2835
|
+
parseLogLevel(message, fallback) {
|
|
2836
|
+
const match = message.trimStart().match(/^\[?(\w+)\]?/);
|
|
2837
|
+
const prefix = match?.[1]?.toUpperCase();
|
|
2838
|
+
switch (prefix) {
|
|
2839
|
+
case "TRACE":
|
|
2840
|
+
return 0;
|
|
2841
|
+
case "DEBUG":
|
|
2842
|
+
return 1;
|
|
2843
|
+
case "INFO":
|
|
2844
|
+
return 2;
|
|
2845
|
+
case "WARN":
|
|
2846
|
+
return 3;
|
|
2847
|
+
case "ERROR":
|
|
2848
|
+
return 4;
|
|
2849
|
+
default:
|
|
2850
|
+
return fallback;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2665
2853
|
/**
|
|
2666
2854
|
* Setup log capture from process stdout/stderr
|
|
2667
2855
|
*/
|
|
@@ -2670,13 +2858,17 @@ var HttpWasmRunner = class {
|
|
|
2670
2858
|
this.process.stdout?.on("data", (data) => {
|
|
2671
2859
|
const message = data.toString().trim();
|
|
2672
2860
|
if (message) {
|
|
2673
|
-
|
|
2861
|
+
const log = { level: this.parseLogLevel(message, 2), message };
|
|
2862
|
+
this.logs.push(log);
|
|
2863
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2674
2864
|
}
|
|
2675
2865
|
});
|
|
2676
2866
|
this.process.stderr?.on("data", (data) => {
|
|
2677
2867
|
const message = data.toString().trim();
|
|
2678
2868
|
if (message) {
|
|
2679
|
-
|
|
2869
|
+
const log = { level: this.parseLogLevel(message, 4), message };
|
|
2870
|
+
this.logs.push(log);
|
|
2871
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2680
2872
|
}
|
|
2681
2873
|
});
|
|
2682
2874
|
}
|
|
@@ -2706,66 +2898,54 @@ var HttpWasmRunner = class {
|
|
|
2706
2898
|
});
|
|
2707
2899
|
}
|
|
2708
2900
|
/**
|
|
2709
|
-
*
|
|
2710
|
-
*
|
|
2711
|
-
*
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
*
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
{
|
|
2720
|
-
hostname: "localhost",
|
|
2721
|
-
port,
|
|
2722
|
-
path: "/",
|
|
2723
|
-
method: "GET",
|
|
2724
|
-
headers: {
|
|
2725
|
-
"content-length": "0",
|
|
2726
|
-
connection: "close"
|
|
2727
|
-
}
|
|
2728
|
-
},
|
|
2729
|
-
(res) => {
|
|
2730
|
-
res.resume();
|
|
2731
|
-
resolve(true);
|
|
2732
|
-
}
|
|
2733
|
-
);
|
|
2734
|
-
req.setTimeout(timeoutMs, () => {
|
|
2735
|
-
req.destroy();
|
|
2736
|
-
resolve(false);
|
|
2737
|
-
});
|
|
2738
|
-
req.on("error", () => resolve(false));
|
|
2739
|
-
req.end();
|
|
2740
|
-
});
|
|
2741
|
-
}
|
|
2742
|
-
/**
|
|
2743
|
-
* Wait for server to be ready by polling
|
|
2901
|
+
* Wait for the fastedge-run HTTP server to be ready by watching process logs
|
|
2902
|
+
* for the "Listening on" message.
|
|
2903
|
+
*
|
|
2904
|
+
* We intentionally avoid HTTP probing here. HTTP probes trigger WASM execution
|
|
2905
|
+
* (the app calls event.request.text() to read the body), which can hang for
|
|
2906
|
+
* many seconds in CI due to WASM JIT compilation on the first request, or
|
|
2907
|
+
* because newer fastedge-run builds hold the body stream open regardless of
|
|
2908
|
+
* content-length. Watching logs avoids all of this: fastedge-run emits
|
|
2909
|
+
* "Listening on http://127.0.0.1:<port>" as soon as the HTTP listener is bound,
|
|
2910
|
+
* before any WASM execution occurs.
|
|
2744
2911
|
*/
|
|
2745
|
-
|
|
2912
|
+
waitForServerReady(port, timeoutMs) {
|
|
2746
2913
|
const startTime = Date.now();
|
|
2747
|
-
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2914
|
+
return new Promise((resolve, reject) => {
|
|
2915
|
+
const check = () => {
|
|
2916
|
+
if (this.process && this.process.exitCode !== null) {
|
|
2917
|
+
return reject(
|
|
2918
|
+
new Error(
|
|
2919
|
+
`FastEdge-run process exited with code ${this.process.exitCode} before server started`
|
|
2920
|
+
)
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
if (this.logs.some((l) => l.message.includes("Listening on"))) {
|
|
2924
|
+
return resolve();
|
|
2925
|
+
}
|
|
2926
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
2927
|
+
const processInfo = this.process ? `Process state: exitCode=${this.process.exitCode}, killed=${this.process.killed}, pid=${this.process.pid}` : "Process is null";
|
|
2928
|
+
const recentLogs = this.logs.slice(-5).map((l) => `[${l.level}] ${l.message}`).join("\n");
|
|
2929
|
+
return reject(
|
|
2930
|
+
new Error(
|
|
2931
|
+
`FastEdge-run server did not start within ${timeoutMs}ms on port ${port}
|
|
2761
2932
|
${processInfo}
|
|
2762
2933
|
Recent logs:
|
|
2763
2934
|
${recentLogs || "(no logs)"}`
|
|
2764
|
-
|
|
2935
|
+
)
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
setTimeout(check, 50);
|
|
2939
|
+
};
|
|
2940
|
+
check();
|
|
2941
|
+
});
|
|
2765
2942
|
}
|
|
2766
2943
|
/**
|
|
2767
|
-
* Kill the process gracefully (SIGINT) with
|
|
2768
|
-
*
|
|
2944
|
+
* Kill the process gracefully (SIGINT) with platform-specific force-kill fallback.
|
|
2945
|
+
* SIGINT is sent first on all platforms — Node.js translates it for Windows.
|
|
2946
|
+
* If the process does not exit within 2 seconds:
|
|
2947
|
+
* - Windows: taskkill /F /T to terminate the process tree
|
|
2948
|
+
* - Unix: SIGKILL
|
|
2769
2949
|
*/
|
|
2770
2950
|
async killProcess() {
|
|
2771
2951
|
if (!this.process) return;
|
|
@@ -2776,8 +2956,18 @@ ${recentLogs || "(no logs)"}`
|
|
|
2776
2956
|
}
|
|
2777
2957
|
this.process.kill("SIGINT");
|
|
2778
2958
|
const timeout = setTimeout(() => {
|
|
2779
|
-
if (this.process &&
|
|
2780
|
-
|
|
2959
|
+
if (this.process && this.process.exitCode === null && this.process.signalCode === null) {
|
|
2960
|
+
if (process.platform === "win32") {
|
|
2961
|
+
const pid = this.process.pid;
|
|
2962
|
+
if (pid) {
|
|
2963
|
+
try {
|
|
2964
|
+
execSync2(`taskkill /F /T /PID ${pid}`);
|
|
2965
|
+
} catch {
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
this.process.kill("SIGKILL");
|
|
2970
|
+
}
|
|
2781
2971
|
}
|
|
2782
2972
|
resolve();
|
|
2783
2973
|
}, 2e3);
|
|
@@ -2800,9 +2990,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2800
2990
|
"application/zip",
|
|
2801
2991
|
"application/gzip"
|
|
2802
2992
|
];
|
|
2803
|
-
return binaryTypes.some(
|
|
2804
|
-
(type) => contentType.toLowerCase().includes(type)
|
|
2805
|
-
);
|
|
2993
|
+
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
2806
2994
|
}
|
|
2807
2995
|
/**
|
|
2808
2996
|
* Parse headers from fetch Headers object
|
|
@@ -2817,6 +3005,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2817
3005
|
};
|
|
2818
3006
|
|
|
2819
3007
|
// server/runner/PortManager.ts
|
|
3008
|
+
import { createServer } from "net";
|
|
2820
3009
|
var PortManager = class {
|
|
2821
3010
|
constructor() {
|
|
2822
3011
|
this.minPort = 8100;
|
|
@@ -2824,18 +3013,31 @@ var PortManager = class {
|
|
|
2824
3013
|
this.allocatedPorts = /* @__PURE__ */ new Set();
|
|
2825
3014
|
this.lastAllocatedPort = this.minPort - 1;
|
|
2826
3015
|
}
|
|
2827
|
-
// Track last allocated port for sequential allocation
|
|
2828
3016
|
/**
|
|
2829
|
-
*
|
|
2830
|
-
*
|
|
2831
|
-
*
|
|
3017
|
+
* Check whether a port is actually free at the OS level.
|
|
3018
|
+
* This is necessary when multiple server processes run simultaneously —
|
|
3019
|
+
* each has its own PortManager with independent in-memory state, so
|
|
3020
|
+
* in-memory tracking alone is not enough to prevent cross-process conflicts.
|
|
3021
|
+
*/
|
|
3022
|
+
isPortFree(port) {
|
|
3023
|
+
return new Promise((resolve) => {
|
|
3024
|
+
const server = createServer();
|
|
3025
|
+
server.once("error", () => resolve(false));
|
|
3026
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
3027
|
+
server.listen(port, "127.0.0.1");
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Allocate an available port from the pool.
|
|
3032
|
+
* Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
|
|
3033
|
+
* with an OS-level check (avoids cross-process collisions).
|
|
2832
3034
|
* @returns The allocated port number
|
|
2833
3035
|
* @throws Error if no ports are available
|
|
2834
3036
|
*/
|
|
2835
|
-
allocate() {
|
|
3037
|
+
async allocate() {
|
|
2836
3038
|
for (let offset = 1; offset <= this.maxPort - this.minPort + 1; offset++) {
|
|
2837
3039
|
const port = this.minPort + (this.lastAllocatedPort - this.minPort + offset) % (this.maxPort - this.minPort + 1);
|
|
2838
|
-
if (!this.allocatedPorts.has(port)) {
|
|
3040
|
+
if (!this.allocatedPorts.has(port) && await this.isPortFree(port)) {
|
|
2839
3041
|
this.allocatedPorts.add(port);
|
|
2840
3042
|
this.lastAllocatedPort = port;
|
|
2841
3043
|
return port;
|
|
@@ -2847,32 +3049,19 @@ var PortManager = class {
|
|
|
2847
3049
|
}
|
|
2848
3050
|
/**
|
|
2849
3051
|
* Release a previously allocated port back to the pool
|
|
2850
|
-
* @param port The port number to release
|
|
2851
3052
|
*/
|
|
2852
3053
|
release(port) {
|
|
2853
3054
|
this.allocatedPorts.delete(port);
|
|
2854
3055
|
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Get the number of currently allocated ports
|
|
2857
|
-
*/
|
|
2858
3056
|
getAllocatedCount() {
|
|
2859
3057
|
return this.allocatedPorts.size;
|
|
2860
3058
|
}
|
|
2861
|
-
/**
|
|
2862
|
-
* Get the number of available ports
|
|
2863
|
-
*/
|
|
2864
3059
|
getAvailableCount() {
|
|
2865
3060
|
return this.maxPort - this.minPort + 1 - this.allocatedPorts.size;
|
|
2866
3061
|
}
|
|
2867
|
-
/**
|
|
2868
|
-
* Check if a specific port is allocated
|
|
2869
|
-
*/
|
|
2870
3062
|
isAllocated(port) {
|
|
2871
3063
|
return this.allocatedPorts.has(port);
|
|
2872
3064
|
}
|
|
2873
|
-
/**
|
|
2874
|
-
* Reset all allocations (useful for testing)
|
|
2875
|
-
*/
|
|
2876
3065
|
reset() {
|
|
2877
3066
|
this.allocatedPorts.clear();
|
|
2878
3067
|
}
|
|
@@ -2917,9 +3106,9 @@ async function createRunnerFromBuffer(buffer, config) {
|
|
|
2917
3106
|
const wasmType = config?.runnerType ?? await detectWasmType(buffer);
|
|
2918
3107
|
let runner;
|
|
2919
3108
|
if (wasmType === "http-wasm") {
|
|
2920
|
-
runner = new HttpWasmRunner(new PortManager(), config?.
|
|
3109
|
+
runner = new HttpWasmRunner(new PortManager(), config?.dotenv?.enabled ?? false);
|
|
2921
3110
|
} else {
|
|
2922
|
-
runner = new ProxyWasmRunner(void 0, config?.
|
|
3111
|
+
runner = new ProxyWasmRunner(void 0, config?.dotenv?.enabled ?? false);
|
|
2923
3112
|
}
|
|
2924
3113
|
await runner.load(buffer, config);
|
|
2925
3114
|
return runner;
|
|
@@ -2948,7 +3137,10 @@ var TestConfigSchema = z.object({
|
|
|
2948
3137
|
request: RequestConfigSchema,
|
|
2949
3138
|
response: ResponseConfigSchema.optional(),
|
|
2950
3139
|
properties: z.record(z.string(), z.unknown()).optional().default({}),
|
|
2951
|
-
|
|
3140
|
+
dotenv: z.object({
|
|
3141
|
+
enabled: z.boolean().optional(),
|
|
3142
|
+
path: z.string().optional()
|
|
3143
|
+
}).optional()
|
|
2952
3144
|
});
|
|
2953
3145
|
|
|
2954
3146
|
// server/test-framework/suite-runner.ts
|