@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
package/dist/lib/index.cjs
CHANGED
|
@@ -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;
|
|
@@ -771,6 +772,8 @@ var HostFunctions = class {
|
|
|
771
772
|
this.pendingHttpCall = null;
|
|
772
773
|
this.httpCallResponse = null;
|
|
773
774
|
this.streamClosed = false;
|
|
775
|
+
// Local response state (from proxy_send_local_response / send_http_response)
|
|
776
|
+
this.localResponse = null;
|
|
774
777
|
this.memory = memory;
|
|
775
778
|
this.propertyResolver = propertyResolver;
|
|
776
779
|
this.propertyAccessControl = propertyAccessControl;
|
|
@@ -825,6 +828,16 @@ var HostFunctions = class {
|
|
|
825
828
|
resetStreamClosed() {
|
|
826
829
|
this.streamClosed = false;
|
|
827
830
|
}
|
|
831
|
+
// Local response accessors (called by ProxyWasmRunner after callHook)
|
|
832
|
+
hasLocalResponse() {
|
|
833
|
+
return this.localResponse !== null;
|
|
834
|
+
}
|
|
835
|
+
getLocalResponse() {
|
|
836
|
+
return this.localResponse;
|
|
837
|
+
}
|
|
838
|
+
resetLocalResponse() {
|
|
839
|
+
this.localResponse = null;
|
|
840
|
+
}
|
|
828
841
|
getRequestHeaders() {
|
|
829
842
|
return this.requestHeaders;
|
|
830
843
|
}
|
|
@@ -1085,15 +1098,21 @@ var HostFunctions = class {
|
|
|
1085
1098
|
this.currentContextId = contextId || this.currentContextId;
|
|
1086
1099
|
return 0 /* Ok */;
|
|
1087
1100
|
},
|
|
1088
|
-
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, grpcStatus) => {
|
|
1101
|
+
proxy_send_local_response: (statusCode, statusCodePtr, statusCodeLen, bodyPtr, bodyLen, headerPairsPtr, headerPairsLen, grpcStatus) => {
|
|
1089
1102
|
this.setLastHostCall(
|
|
1090
1103
|
`proxy_send_local_response status=${statusCode} bodyLen=${bodyLen}`
|
|
1091
1104
|
);
|
|
1092
1105
|
const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
|
|
1093
|
-
const body = this.memory.
|
|
1106
|
+
const body = this.memory.readBytes(bodyPtr, bodyLen);
|
|
1107
|
+
if (headerPairsLen > 0) {
|
|
1108
|
+
const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
|
|
1109
|
+
const headers = HeaderManager.deserializeBinary(headerBytes);
|
|
1110
|
+
this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
|
|
1111
|
+
}
|
|
1112
|
+
this.localResponse = { statusCode, statusText, body };
|
|
1094
1113
|
this.logs.push({
|
|
1095
1114
|
level: 1,
|
|
1096
|
-
message: `local_response status=${statusCode} ${statusText}
|
|
1115
|
+
message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
|
|
1097
1116
|
});
|
|
1098
1117
|
return 0 /* Ok */;
|
|
1099
1118
|
},
|
|
@@ -1673,7 +1692,6 @@ async function loadDotenvFiles(dotenvPath = ".") {
|
|
|
1673
1692
|
// server/runner/ProxyWasmRunner.ts
|
|
1674
1693
|
var textEncoder4 = new TextEncoder();
|
|
1675
1694
|
var ProxyWasmRunner = class {
|
|
1676
|
-
// Default to enabled
|
|
1677
1695
|
constructor(fastEdgeConfig, dotenvEnabled = true) {
|
|
1678
1696
|
this.module = null;
|
|
1679
1697
|
// Compiled module (reused)
|
|
@@ -1687,6 +1705,8 @@ var ProxyWasmRunner = class {
|
|
|
1687
1705
|
this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1688
1706
|
this.stateManager = null;
|
|
1689
1707
|
this.dotenvEnabled = true;
|
|
1708
|
+
// Default to enabled
|
|
1709
|
+
this.dotenvPath = ".";
|
|
1690
1710
|
this.memory = new MemoryManager();
|
|
1691
1711
|
this.propertyResolver = new PropertyResolver();
|
|
1692
1712
|
this.propertyAccessControl = new PropertyAccessControl();
|
|
@@ -1721,7 +1741,7 @@ var ProxyWasmRunner = class {
|
|
|
1721
1741
|
return;
|
|
1722
1742
|
}
|
|
1723
1743
|
try {
|
|
1724
|
-
const dotenvConfig = await loadDotenvFiles(
|
|
1744
|
+
const dotenvConfig = await loadDotenvFiles(this.dotenvPath);
|
|
1725
1745
|
if (dotenvConfig.secrets && Object.keys(dotenvConfig.secrets).length > 0) {
|
|
1726
1746
|
const existingSecrets = this.secretStore.getAll();
|
|
1727
1747
|
this.secretStore = new SecretStore({
|
|
@@ -1753,9 +1773,37 @@ var ProxyWasmRunner = class {
|
|
|
1753
1773
|
console.error("Failed to load dotenv files:", error);
|
|
1754
1774
|
}
|
|
1755
1775
|
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Apply dotenv settings to the running module without recompiling.
|
|
1778
|
+
* Resets SecretStore/Dictionary to empty, then re-loads dotenv files
|
|
1779
|
+
* using the new settings. The compiled WASM module is untouched.
|
|
1780
|
+
*/
|
|
1781
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
1782
|
+
this.dotenvEnabled = enabled;
|
|
1783
|
+
if (dotenvPath !== void 0) {
|
|
1784
|
+
this.dotenvPath = dotenvPath;
|
|
1785
|
+
}
|
|
1786
|
+
this.secretStore = new SecretStore();
|
|
1787
|
+
this.dictionary = new Dictionary();
|
|
1788
|
+
await this.loadDotenvIfEnabled();
|
|
1789
|
+
if (!enabled) {
|
|
1790
|
+
this.hostFunctions = new HostFunctions(
|
|
1791
|
+
this.memory,
|
|
1792
|
+
this.propertyResolver,
|
|
1793
|
+
this.propertyAccessControl,
|
|
1794
|
+
() => this.currentHook,
|
|
1795
|
+
this.debug,
|
|
1796
|
+
this.secretStore,
|
|
1797
|
+
this.dictionary
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1756
1801
|
async load(bufferOrPath, config) {
|
|
1757
|
-
if (config?.
|
|
1758
|
-
this.dotenvEnabled = config.
|
|
1802
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
1803
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
1804
|
+
}
|
|
1805
|
+
if (config?.dotenv?.path !== void 0) {
|
|
1806
|
+
this.dotenvPath = config.dotenv.path;
|
|
1759
1807
|
}
|
|
1760
1808
|
this.resetState();
|
|
1761
1809
|
let buffer;
|
|
@@ -1826,6 +1874,25 @@ var ProxyWasmRunner = class {
|
|
|
1826
1874
|
"system"
|
|
1827
1875
|
);
|
|
1828
1876
|
}
|
|
1877
|
+
if (results.onRequestHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1878
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1879
|
+
const responseHeaders = results.onRequestHeaders.output.response.headers;
|
|
1880
|
+
this.hostFunctions.resetLocalResponse();
|
|
1881
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1882
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1883
|
+
return {
|
|
1884
|
+
hookResults: results,
|
|
1885
|
+
finalResponse: {
|
|
1886
|
+
status: local.statusCode,
|
|
1887
|
+
statusText: local.statusText,
|
|
1888
|
+
headers: responseHeaders,
|
|
1889
|
+
body,
|
|
1890
|
+
contentType,
|
|
1891
|
+
isBase64
|
|
1892
|
+
},
|
|
1893
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1829
1896
|
const headersAfterRequestHeaders = results.onRequestHeaders.output.request.headers;
|
|
1830
1897
|
const propertiesAfterRequestHeaders = results.onRequestHeaders.properties;
|
|
1831
1898
|
this.logDebug(
|
|
@@ -1850,6 +1917,25 @@ var ProxyWasmRunner = class {
|
|
|
1850
1917
|
"system"
|
|
1851
1918
|
);
|
|
1852
1919
|
}
|
|
1920
|
+
if (results.onRequestBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
1921
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
1922
|
+
const responseHeaders = results.onRequestBody.output.response.headers;
|
|
1923
|
+
this.hostFunctions.resetLocalResponse();
|
|
1924
|
+
const contentType = responseHeaders["content-type"] || "text/plain";
|
|
1925
|
+
const { body, isBase64 } = encodeLocalResponseBody(local.body, contentType);
|
|
1926
|
+
return {
|
|
1927
|
+
hookResults: results,
|
|
1928
|
+
finalResponse: {
|
|
1929
|
+
status: local.statusCode,
|
|
1930
|
+
statusText: local.statusText,
|
|
1931
|
+
headers: responseHeaders,
|
|
1932
|
+
body,
|
|
1933
|
+
contentType,
|
|
1934
|
+
isBase64
|
|
1935
|
+
},
|
|
1936
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1853
1939
|
const modifiedRequestHeaders = results.onRequestBody.output.request.headers;
|
|
1854
1940
|
const modifiedRequestBody = results.onRequestBody.output.request.body;
|
|
1855
1941
|
const propertiesAfterRequestBody = results.onRequestBody.properties;
|
|
@@ -2038,6 +2124,7 @@ var ProxyWasmRunner = class {
|
|
|
2038
2124
|
throw new Error("WASM module not loaded");
|
|
2039
2125
|
}
|
|
2040
2126
|
this.currentHook = this.getHookContext(call.hook);
|
|
2127
|
+
this.hostFunctions.resetLocalResponse();
|
|
2041
2128
|
const imports = this.createImports();
|
|
2042
2129
|
this.instance = await WebAssembly.instantiate(this.module, imports);
|
|
2043
2130
|
const memory = this.instance.exports.memory;
|
|
@@ -2436,7 +2523,7 @@ var ProxyWasmRunner = class {
|
|
|
2436
2523
|
/**
|
|
2437
2524
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
2438
2525
|
*/
|
|
2439
|
-
async execute(
|
|
2526
|
+
async execute(request) {
|
|
2440
2527
|
throw new Error(
|
|
2441
2528
|
"execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
|
|
2442
2529
|
);
|
|
@@ -2471,16 +2558,24 @@ function ensureNullTerminated(value) {
|
|
|
2471
2558
|
}
|
|
2472
2559
|
return value.endsWith("\0") ? value : `${value}\0`;
|
|
2473
2560
|
}
|
|
2561
|
+
function encodeLocalResponseBody(body, contentType) {
|
|
2562
|
+
const isBinary = contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("application/zip");
|
|
2563
|
+
if (isBinary) {
|
|
2564
|
+
return { body: Buffer.from(body).toString("base64"), isBase64: true };
|
|
2565
|
+
}
|
|
2566
|
+
return { body: new TextDecoder().decode(body), isBase64: false };
|
|
2567
|
+
}
|
|
2474
2568
|
|
|
2475
2569
|
// server/runner/HttpWasmRunner.ts
|
|
2476
2570
|
var import_child_process2 = require("child_process");
|
|
2477
|
-
var http = __toESM(require("http"));
|
|
2478
2571
|
|
|
2479
2572
|
// server/utils/fastedge-cli.ts
|
|
2480
2573
|
var import_child_process = require("child_process");
|
|
2481
2574
|
var import_fs = require("fs");
|
|
2482
2575
|
var import_path2 = require("path");
|
|
2576
|
+
var import_url = require("url");
|
|
2483
2577
|
var import_os = __toESM(require("os"));
|
|
2578
|
+
var _currentDir = (0, import_path2.dirname)((0, import_url.fileURLToPath)(__importMetaUrl));
|
|
2484
2579
|
function getCliBinaryName() {
|
|
2485
2580
|
switch (import_os.default.platform()) {
|
|
2486
2581
|
case "win32":
|
|
@@ -2496,19 +2591,30 @@ function getCliBinaryName() {
|
|
|
2496
2591
|
function getBundledCliPaths() {
|
|
2497
2592
|
const binaryName = getCliBinaryName();
|
|
2498
2593
|
return [
|
|
2499
|
-
//
|
|
2500
|
-
(0, import_path2.join)(
|
|
2594
|
+
// Installed npm package: dist/lib/index.js → dist/fastedge-cli/
|
|
2595
|
+
(0, import_path2.join)(_currentDir, "..", "fastedge-cli", binaryName),
|
|
2596
|
+
// Production: bundled server at dist/server.js → dist/fastedge-cli/
|
|
2597
|
+
(0, import_path2.join)(_currentDir, "fastedge-cli", binaryName),
|
|
2501
2598
|
// Development/Tests: running from source
|
|
2502
|
-
//
|
|
2503
|
-
(0, import_path2.join)(
|
|
2504
|
-
// Alternative: if
|
|
2505
|
-
(0, import_path2.join)(
|
|
2599
|
+
// _currentDir might be server/utils/, so go up to project root
|
|
2600
|
+
(0, import_path2.join)(_currentDir, "..", "..", "fastedge-run", binaryName),
|
|
2601
|
+
// Alternative: if _currentDir is already at project root
|
|
2602
|
+
(0, import_path2.join)(_currentDir, "fastedge-run", binaryName)
|
|
2506
2603
|
];
|
|
2507
2604
|
}
|
|
2605
|
+
function ensureExecutable(binaryPath) {
|
|
2606
|
+
if (process.platform !== "win32") {
|
|
2607
|
+
try {
|
|
2608
|
+
(0, import_fs.chmodSync)(binaryPath, 493);
|
|
2609
|
+
} catch {
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2508
2613
|
async function findFastEdgeRunCli() {
|
|
2509
2614
|
const envPath = process.env.FASTEDGE_RUN_PATH;
|
|
2510
2615
|
if (envPath) {
|
|
2511
2616
|
if ((0, import_fs.existsSync)(envPath)) {
|
|
2617
|
+
ensureExecutable(envPath);
|
|
2512
2618
|
return envPath;
|
|
2513
2619
|
} else {
|
|
2514
2620
|
throw new Error(
|
|
@@ -2518,6 +2624,7 @@ async function findFastEdgeRunCli() {
|
|
|
2518
2624
|
}
|
|
2519
2625
|
for (const bundledPath of getBundledCliPaths()) {
|
|
2520
2626
|
if ((0, import_fs.existsSync)(bundledPath)) {
|
|
2627
|
+
ensureExecutable(bundledPath);
|
|
2521
2628
|
return bundledPath;
|
|
2522
2629
|
}
|
|
2523
2630
|
}
|
|
@@ -2565,9 +2672,12 @@ var HttpWasmRunner = class {
|
|
|
2565
2672
|
this.port = null;
|
|
2566
2673
|
this.cliPath = null;
|
|
2567
2674
|
this.tempWasmPath = null;
|
|
2675
|
+
this.currentWasmPath = null;
|
|
2676
|
+
// resolved path used when spawning
|
|
2568
2677
|
this.logs = [];
|
|
2569
2678
|
this.stateManager = null;
|
|
2570
2679
|
this.dotenvEnabled = true;
|
|
2680
|
+
this.dotenvPath = null;
|
|
2571
2681
|
this.portManager = portManager;
|
|
2572
2682
|
this.dotenvEnabled = dotenvEnabled;
|
|
2573
2683
|
}
|
|
@@ -2575,8 +2685,11 @@ var HttpWasmRunner = class {
|
|
|
2575
2685
|
* Load WASM binary and spawn fastedge-run process
|
|
2576
2686
|
*/
|
|
2577
2687
|
async load(bufferOrPath, config) {
|
|
2578
|
-
if (config?.
|
|
2579
|
-
this.dotenvEnabled = config.
|
|
2688
|
+
if (config?.dotenv?.enabled !== void 0) {
|
|
2689
|
+
this.dotenvEnabled = config.dotenv.enabled;
|
|
2690
|
+
}
|
|
2691
|
+
if (config?.dotenv?.path !== void 0) {
|
|
2692
|
+
this.dotenvPath = config.dotenv.path;
|
|
2580
2693
|
}
|
|
2581
2694
|
await this.cleanup();
|
|
2582
2695
|
this.cliPath = await findFastEdgeRunCli();
|
|
@@ -2588,7 +2701,7 @@ var HttpWasmRunner = class {
|
|
|
2588
2701
|
wasmPath = await writeTempWasmFile(bufferOrPath);
|
|
2589
2702
|
this.tempWasmPath = wasmPath;
|
|
2590
2703
|
}
|
|
2591
|
-
this.port = this.portManager.allocate();
|
|
2704
|
+
this.port = await this.portManager.allocate();
|
|
2592
2705
|
const args = [
|
|
2593
2706
|
"http",
|
|
2594
2707
|
"-p",
|
|
@@ -2599,8 +2712,13 @@ var HttpWasmRunner = class {
|
|
|
2599
2712
|
"true"
|
|
2600
2713
|
];
|
|
2601
2714
|
if (this.dotenvEnabled) {
|
|
2602
|
-
|
|
2715
|
+
if (this.dotenvPath) {
|
|
2716
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2717
|
+
} else {
|
|
2718
|
+
args.push("--dotenv");
|
|
2719
|
+
}
|
|
2603
2720
|
}
|
|
2721
|
+
this.currentWasmPath = wasmPath;
|
|
2604
2722
|
this.process = (0, import_child_process2.spawn)(this.cliPath, args, {
|
|
2605
2723
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2606
2724
|
env: {
|
|
@@ -2616,17 +2734,17 @@ var HttpWasmRunner = class {
|
|
|
2616
2734
|
/**
|
|
2617
2735
|
* Execute an HTTP request through the WASM module
|
|
2618
2736
|
*/
|
|
2619
|
-
async execute(
|
|
2737
|
+
async execute(request) {
|
|
2620
2738
|
if (!this.port || !this.process) {
|
|
2621
2739
|
throw new Error("HttpWasmRunner not loaded. Call load() first.");
|
|
2622
2740
|
}
|
|
2623
2741
|
this.logs = [];
|
|
2624
2742
|
try {
|
|
2625
|
-
const url = `http://localhost:${this.port}${
|
|
2743
|
+
const url = `http://localhost:${this.port}${request.path}`;
|
|
2626
2744
|
const response = await fetch(url, {
|
|
2627
|
-
method:
|
|
2628
|
-
headers:
|
|
2629
|
-
body:
|
|
2745
|
+
method: request.method,
|
|
2746
|
+
headers: request.headers,
|
|
2747
|
+
body: request.body || void 0,
|
|
2630
2748
|
signal: AbortSignal.timeout(3e4)
|
|
2631
2749
|
// 30 second timeout
|
|
2632
2750
|
});
|
|
@@ -2654,7 +2772,7 @@ var HttpWasmRunner = class {
|
|
|
2654
2772
|
/**
|
|
2655
2773
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2656
2774
|
*/
|
|
2657
|
-
async callHook(
|
|
2775
|
+
async callHook(_hookCall) {
|
|
2658
2776
|
throw new Error(
|
|
2659
2777
|
"callHook() is not supported for HTTP WASM. Use execute() instead."
|
|
2660
2778
|
);
|
|
@@ -2662,11 +2780,53 @@ var HttpWasmRunner = class {
|
|
|
2662
2780
|
/**
|
|
2663
2781
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2664
2782
|
*/
|
|
2665
|
-
async callFullFlow(
|
|
2783
|
+
async callFullFlow(_url, _method, _headers, _body, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
|
|
2666
2784
|
throw new Error(
|
|
2667
2785
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2668
2786
|
);
|
|
2669
2787
|
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Apply dotenv settings by restarting the fastedge-run process.
|
|
2790
|
+
* The WASM file is not re-read; only the --dotenv flag changes.
|
|
2791
|
+
*/
|
|
2792
|
+
async applyDotenv(enabled, dotenvPath) {
|
|
2793
|
+
this.dotenvEnabled = enabled;
|
|
2794
|
+
if (dotenvPath !== void 0) {
|
|
2795
|
+
this.dotenvPath = dotenvPath;
|
|
2796
|
+
}
|
|
2797
|
+
if (!this.process || !this.currentWasmPath || !this.cliPath || this.port === null) {
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
await this.killProcess();
|
|
2801
|
+
this.process = null;
|
|
2802
|
+
const args = [
|
|
2803
|
+
"http",
|
|
2804
|
+
"-p",
|
|
2805
|
+
this.port.toString(),
|
|
2806
|
+
"-w",
|
|
2807
|
+
this.currentWasmPath,
|
|
2808
|
+
"--wasi-http",
|
|
2809
|
+
"true"
|
|
2810
|
+
];
|
|
2811
|
+
if (this.dotenvEnabled) {
|
|
2812
|
+
if (this.dotenvPath) {
|
|
2813
|
+
args.push("--dotenv", this.dotenvPath);
|
|
2814
|
+
} else {
|
|
2815
|
+
args.push("--dotenv");
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
this.process = (0, import_child_process2.spawn)(this.cliPath, args, {
|
|
2819
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2820
|
+
env: {
|
|
2821
|
+
RUST_LOG: "info",
|
|
2822
|
+
...process.env
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
this.setupLogCapture();
|
|
2826
|
+
this.setupErrorHandlers();
|
|
2827
|
+
const timeout = process.env.NODE_ENV === "test" || process.env.VITEST ? 2e4 : 1e4;
|
|
2828
|
+
await this.waitForServerReady(this.port, timeout);
|
|
2829
|
+
}
|
|
2670
2830
|
/**
|
|
2671
2831
|
* Clean up resources
|
|
2672
2832
|
*/
|
|
@@ -2691,12 +2851,41 @@ var HttpWasmRunner = class {
|
|
|
2691
2851
|
getType() {
|
|
2692
2852
|
return "http-wasm";
|
|
2693
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Get the port the fastedge-run HTTP server is listening on
|
|
2856
|
+
*/
|
|
2857
|
+
getPort() {
|
|
2858
|
+
return this.port;
|
|
2859
|
+
}
|
|
2694
2860
|
/**
|
|
2695
2861
|
* Set state manager
|
|
2696
2862
|
*/
|
|
2697
2863
|
setStateManager(stateManager) {
|
|
2698
2864
|
this.stateManager = stateManager;
|
|
2699
2865
|
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Parse log level from a process output line.
|
|
2868
|
+
* Matches bare prefixes (e.g. "INFO target > msg") and bracketed prefixes (e.g. "[INFO] msg").
|
|
2869
|
+
* Falls back to the provided default if no known level prefix is found.
|
|
2870
|
+
*/
|
|
2871
|
+
parseLogLevel(message, fallback) {
|
|
2872
|
+
const match = message.trimStart().match(/^\[?(\w+)\]?/);
|
|
2873
|
+
const prefix = match?.[1]?.toUpperCase();
|
|
2874
|
+
switch (prefix) {
|
|
2875
|
+
case "TRACE":
|
|
2876
|
+
return 0;
|
|
2877
|
+
case "DEBUG":
|
|
2878
|
+
return 1;
|
|
2879
|
+
case "INFO":
|
|
2880
|
+
return 2;
|
|
2881
|
+
case "WARN":
|
|
2882
|
+
return 3;
|
|
2883
|
+
case "ERROR":
|
|
2884
|
+
return 4;
|
|
2885
|
+
default:
|
|
2886
|
+
return fallback;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2700
2889
|
/**
|
|
2701
2890
|
* Setup log capture from process stdout/stderr
|
|
2702
2891
|
*/
|
|
@@ -2705,13 +2894,17 @@ var HttpWasmRunner = class {
|
|
|
2705
2894
|
this.process.stdout?.on("data", (data) => {
|
|
2706
2895
|
const message = data.toString().trim();
|
|
2707
2896
|
if (message) {
|
|
2708
|
-
|
|
2897
|
+
const log = { level: this.parseLogLevel(message, 2), message };
|
|
2898
|
+
this.logs.push(log);
|
|
2899
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2709
2900
|
}
|
|
2710
2901
|
});
|
|
2711
2902
|
this.process.stderr?.on("data", (data) => {
|
|
2712
2903
|
const message = data.toString().trim();
|
|
2713
2904
|
if (message) {
|
|
2714
|
-
|
|
2905
|
+
const log = { level: this.parseLogLevel(message, 4), message };
|
|
2906
|
+
this.logs.push(log);
|
|
2907
|
+
this.stateManager?.emitHttpWasmLog(log);
|
|
2715
2908
|
}
|
|
2716
2909
|
});
|
|
2717
2910
|
}
|
|
@@ -2741,66 +2934,54 @@ var HttpWasmRunner = class {
|
|
|
2741
2934
|
});
|
|
2742
2935
|
}
|
|
2743
2936
|
/**
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
{
|
|
2755
|
-
hostname: "localhost",
|
|
2756
|
-
port,
|
|
2757
|
-
path: "/",
|
|
2758
|
-
method: "GET",
|
|
2759
|
-
headers: {
|
|
2760
|
-
"content-length": "0",
|
|
2761
|
-
connection: "close"
|
|
2762
|
-
}
|
|
2763
|
-
},
|
|
2764
|
-
(res) => {
|
|
2765
|
-
res.resume();
|
|
2766
|
-
resolve(true);
|
|
2767
|
-
}
|
|
2768
|
-
);
|
|
2769
|
-
req.setTimeout(timeoutMs, () => {
|
|
2770
|
-
req.destroy();
|
|
2771
|
-
resolve(false);
|
|
2772
|
-
});
|
|
2773
|
-
req.on("error", () => resolve(false));
|
|
2774
|
-
req.end();
|
|
2775
|
-
});
|
|
2776
|
-
}
|
|
2777
|
-
/**
|
|
2778
|
-
* Wait for server to be ready by polling
|
|
2937
|
+
* Wait for the fastedge-run HTTP server to be ready by watching process logs
|
|
2938
|
+
* for the "Listening on" message.
|
|
2939
|
+
*
|
|
2940
|
+
* We intentionally avoid HTTP probing here. HTTP probes trigger WASM execution
|
|
2941
|
+
* (the app calls event.request.text() to read the body), which can hang for
|
|
2942
|
+
* many seconds in CI due to WASM JIT compilation on the first request, or
|
|
2943
|
+
* because newer fastedge-run builds hold the body stream open regardless of
|
|
2944
|
+
* content-length. Watching logs avoids all of this: fastedge-run emits
|
|
2945
|
+
* "Listening on http://127.0.0.1:<port>" as soon as the HTTP listener is bound,
|
|
2946
|
+
* before any WASM execution occurs.
|
|
2779
2947
|
*/
|
|
2780
|
-
|
|
2948
|
+
waitForServerReady(port, timeoutMs) {
|
|
2781
2949
|
const startTime = Date.now();
|
|
2782
|
-
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2950
|
+
return new Promise((resolve, reject) => {
|
|
2951
|
+
const check = () => {
|
|
2952
|
+
if (this.process && this.process.exitCode !== null) {
|
|
2953
|
+
return reject(
|
|
2954
|
+
new Error(
|
|
2955
|
+
`FastEdge-run process exited with code ${this.process.exitCode} before server started`
|
|
2956
|
+
)
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
if (this.logs.some((l) => l.message.includes("Listening on"))) {
|
|
2960
|
+
return resolve();
|
|
2961
|
+
}
|
|
2962
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
2963
|
+
const processInfo = this.process ? `Process state: exitCode=${this.process.exitCode}, killed=${this.process.killed}, pid=${this.process.pid}` : "Process is null";
|
|
2964
|
+
const recentLogs = this.logs.slice(-5).map((l) => `[${l.level}] ${l.message}`).join("\n");
|
|
2965
|
+
return reject(
|
|
2966
|
+
new Error(
|
|
2967
|
+
`FastEdge-run server did not start within ${timeoutMs}ms on port ${port}
|
|
2796
2968
|
${processInfo}
|
|
2797
2969
|
Recent logs:
|
|
2798
2970
|
${recentLogs || "(no logs)"}`
|
|
2799
|
-
|
|
2971
|
+
)
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
setTimeout(check, 50);
|
|
2975
|
+
};
|
|
2976
|
+
check();
|
|
2977
|
+
});
|
|
2800
2978
|
}
|
|
2801
2979
|
/**
|
|
2802
|
-
* Kill the process gracefully (SIGINT) with
|
|
2803
|
-
*
|
|
2980
|
+
* Kill the process gracefully (SIGINT) with platform-specific force-kill fallback.
|
|
2981
|
+
* SIGINT is sent first on all platforms — Node.js translates it for Windows.
|
|
2982
|
+
* If the process does not exit within 2 seconds:
|
|
2983
|
+
* - Windows: taskkill /F /T to terminate the process tree
|
|
2984
|
+
* - Unix: SIGKILL
|
|
2804
2985
|
*/
|
|
2805
2986
|
async killProcess() {
|
|
2806
2987
|
if (!this.process) return;
|
|
@@ -2811,8 +2992,18 @@ ${recentLogs || "(no logs)"}`
|
|
|
2811
2992
|
}
|
|
2812
2993
|
this.process.kill("SIGINT");
|
|
2813
2994
|
const timeout = setTimeout(() => {
|
|
2814
|
-
if (this.process &&
|
|
2815
|
-
|
|
2995
|
+
if (this.process && this.process.exitCode === null && this.process.signalCode === null) {
|
|
2996
|
+
if (process.platform === "win32") {
|
|
2997
|
+
const pid = this.process.pid;
|
|
2998
|
+
if (pid) {
|
|
2999
|
+
try {
|
|
3000
|
+
(0, import_child_process2.execSync)(`taskkill /F /T /PID ${pid}`);
|
|
3001
|
+
} catch {
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
} else {
|
|
3005
|
+
this.process.kill("SIGKILL");
|
|
3006
|
+
}
|
|
2816
3007
|
}
|
|
2817
3008
|
resolve();
|
|
2818
3009
|
}, 2e3);
|
|
@@ -2835,9 +3026,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2835
3026
|
"application/zip",
|
|
2836
3027
|
"application/gzip"
|
|
2837
3028
|
];
|
|
2838
|
-
return binaryTypes.some(
|
|
2839
|
-
(type) => contentType.toLowerCase().includes(type)
|
|
2840
|
-
);
|
|
3029
|
+
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
2841
3030
|
}
|
|
2842
3031
|
/**
|
|
2843
3032
|
* Parse headers from fetch Headers object
|
|
@@ -2852,6 +3041,7 @@ ${recentLogs || "(no logs)"}`
|
|
|
2852
3041
|
};
|
|
2853
3042
|
|
|
2854
3043
|
// server/runner/PortManager.ts
|
|
3044
|
+
var import_net = require("net");
|
|
2855
3045
|
var PortManager = class {
|
|
2856
3046
|
constructor() {
|
|
2857
3047
|
this.minPort = 8100;
|
|
@@ -2859,18 +3049,31 @@ var PortManager = class {
|
|
|
2859
3049
|
this.allocatedPorts = /* @__PURE__ */ new Set();
|
|
2860
3050
|
this.lastAllocatedPort = this.minPort - 1;
|
|
2861
3051
|
}
|
|
2862
|
-
// Track last allocated port for sequential allocation
|
|
2863
3052
|
/**
|
|
2864
|
-
*
|
|
2865
|
-
*
|
|
2866
|
-
*
|
|
3053
|
+
* Check whether a port is actually free at the OS level.
|
|
3054
|
+
* This is necessary when multiple server processes run simultaneously —
|
|
3055
|
+
* each has its own PortManager with independent in-memory state, so
|
|
3056
|
+
* in-memory tracking alone is not enough to prevent cross-process conflicts.
|
|
3057
|
+
*/
|
|
3058
|
+
isPortFree(port) {
|
|
3059
|
+
return new Promise((resolve) => {
|
|
3060
|
+
const server = (0, import_net.createServer)();
|
|
3061
|
+
server.once("error", () => resolve(false));
|
|
3062
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
3063
|
+
server.listen(port, "127.0.0.1");
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Allocate an available port from the pool.
|
|
3068
|
+
* Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
|
|
3069
|
+
* with an OS-level check (avoids cross-process collisions).
|
|
2867
3070
|
* @returns The allocated port number
|
|
2868
3071
|
* @throws Error if no ports are available
|
|
2869
3072
|
*/
|
|
2870
|
-
allocate() {
|
|
3073
|
+
async allocate() {
|
|
2871
3074
|
for (let offset = 1; offset <= this.maxPort - this.minPort + 1; offset++) {
|
|
2872
3075
|
const port = this.minPort + (this.lastAllocatedPort - this.minPort + offset) % (this.maxPort - this.minPort + 1);
|
|
2873
|
-
if (!this.allocatedPorts.has(port)) {
|
|
3076
|
+
if (!this.allocatedPorts.has(port) && await this.isPortFree(port)) {
|
|
2874
3077
|
this.allocatedPorts.add(port);
|
|
2875
3078
|
this.lastAllocatedPort = port;
|
|
2876
3079
|
return port;
|
|
@@ -2882,32 +3085,19 @@ var PortManager = class {
|
|
|
2882
3085
|
}
|
|
2883
3086
|
/**
|
|
2884
3087
|
* Release a previously allocated port back to the pool
|
|
2885
|
-
* @param port The port number to release
|
|
2886
3088
|
*/
|
|
2887
3089
|
release(port) {
|
|
2888
3090
|
this.allocatedPorts.delete(port);
|
|
2889
3091
|
}
|
|
2890
|
-
/**
|
|
2891
|
-
* Get the number of currently allocated ports
|
|
2892
|
-
*/
|
|
2893
3092
|
getAllocatedCount() {
|
|
2894
3093
|
return this.allocatedPorts.size;
|
|
2895
3094
|
}
|
|
2896
|
-
/**
|
|
2897
|
-
* Get the number of available ports
|
|
2898
|
-
*/
|
|
2899
3095
|
getAvailableCount() {
|
|
2900
3096
|
return this.maxPort - this.minPort + 1 - this.allocatedPorts.size;
|
|
2901
3097
|
}
|
|
2902
|
-
/**
|
|
2903
|
-
* Check if a specific port is allocated
|
|
2904
|
-
*/
|
|
2905
3098
|
isAllocated(port) {
|
|
2906
3099
|
return this.allocatedPorts.has(port);
|
|
2907
3100
|
}
|
|
2908
|
-
/**
|
|
2909
|
-
* Reset all allocations (useful for testing)
|
|
2910
|
-
*/
|
|
2911
3101
|
reset() {
|
|
2912
3102
|
this.allocatedPorts.clear();
|
|
2913
3103
|
}
|
|
@@ -2957,6 +3147,8 @@ var NullStateManager = class {
|
|
|
2957
3147
|
}
|
|
2958
3148
|
emitHttpWasmRequestCompleted() {
|
|
2959
3149
|
}
|
|
3150
|
+
emitHttpWasmLog() {
|
|
3151
|
+
}
|
|
2960
3152
|
emitReloadWorkspaceWasm() {
|
|
2961
3153
|
}
|
|
2962
3154
|
};
|
|
@@ -3003,9 +3195,9 @@ async function createRunnerFromBuffer(buffer, config) {
|
|
|
3003
3195
|
const wasmType = config?.runnerType ?? await detectWasmType(buffer);
|
|
3004
3196
|
let runner;
|
|
3005
3197
|
if (wasmType === "http-wasm") {
|
|
3006
|
-
runner = new HttpWasmRunner(new PortManager(), config?.
|
|
3198
|
+
runner = new HttpWasmRunner(new PortManager(), config?.dotenv?.enabled ?? false);
|
|
3007
3199
|
} else {
|
|
3008
|
-
runner = new ProxyWasmRunner(void 0, config?.
|
|
3200
|
+
runner = new ProxyWasmRunner(void 0, config?.dotenv?.enabled ?? false);
|
|
3009
3201
|
}
|
|
3010
3202
|
await runner.load(buffer, config);
|
|
3011
3203
|
return runner;
|