@faapi/faapi 1.0.0-canary.d7438c8 → 1.0.1-canary.218585b

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/dist/index.js CHANGED
@@ -1150,14 +1150,14 @@ var ValidationError = class extends FaapiError {
1150
1150
  issues;
1151
1151
  };
1152
1152
  var RouteNotFoundError = class extends FaapiError {
1153
- constructor(path10) {
1154
- super("ROUTE_NOT_FOUND", `Route not found: ${path10}`, 404);
1153
+ constructor(path11) {
1154
+ super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
1155
1155
  this.name = "RouteNotFoundError";
1156
1156
  }
1157
1157
  };
1158
1158
  var MethodNotAllowedError = class extends FaapiError {
1159
- constructor(method, path10, allowedMethods) {
1160
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path10}`, 405);
1159
+ constructor(method, path11, allowedMethods) {
1160
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
1161
1161
  this.allowedMethods = allowedMethods;
1162
1162
  this.name = "MethodNotAllowedError";
1163
1163
  }
@@ -1634,1007 +1634,1222 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1634
1634
  return await compose(middlewares, ctx, finalHandler);
1635
1635
  }
1636
1636
 
1637
- // src/cli/createAppCore.ts
1638
- import fs5 from "fs";
1637
+ // src/testServer.ts
1639
1638
  import path8 from "path";
1640
- import { PassThrough } from "stream";
1639
+ import os from "os";
1640
+ import fs5 from "fs/promises";
1641
1641
 
1642
- // src/router/sortRoutes.ts
1643
- function sortRoutes(routes) {
1644
- return [...routes].sort((a, b) => {
1645
- if (a.isDynamic !== b.isDynamic) {
1646
- return a.isDynamic ? 1 : -1;
1647
- }
1648
- if (a.isCatchAll !== b.isCatchAll) {
1649
- return a.isCatchAll ? 1 : -1;
1650
- }
1651
- const aSegments = a.urlPath.split("/").filter(Boolean).length;
1652
- const bSegments = b.urlPath.split("/").filter(Boolean).length;
1653
- if (aSegments !== bSegments) {
1654
- return aSegments - bSegments;
1655
- }
1656
- return a.urlPath.localeCompare(b.urlPath);
1657
- });
1642
+ // src/router/scanRoutes.ts
1643
+ import fg from "fast-glob";
1644
+ import path4 from "path";
1645
+ import fs3 from "fs";
1646
+
1647
+ // src/router/constants.ts
1648
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1649
+ var HTTP_METHOD_SET = new Set(HTTP_METHODS);
1650
+ function isHttpMethod(value) {
1651
+ return HTTP_METHOD_SET.has(value);
1658
1652
  }
1659
1653
 
1660
- // src/router/detectRouteConflicts.ts
1661
- function detectRouteConflicts(routes) {
1662
- const map = /* @__PURE__ */ new Map();
1663
- for (const route of routes) {
1664
- const key = `${route.method} ${route.urlPath}`;
1665
- const existing = map.get(key);
1666
- if (existing) {
1667
- existing.files.push(route.filePath);
1668
- } else {
1669
- map.set(key, {
1670
- method: route.method,
1671
- urlPath: route.urlPath,
1672
- files: [route.filePath]
1673
- });
1674
- }
1654
+ // src/utils/normalizePath.ts
1655
+ function normalizePath(path11) {
1656
+ if (!path11) return "";
1657
+ let result = path11.replace(/\\/g, "/");
1658
+ result = result.replace(/\/+/g, "/");
1659
+ result = result.replace(/\/+$/, "");
1660
+ if (result && !result.startsWith("/")) {
1661
+ result = "/" + result;
1675
1662
  }
1676
- const conflicts = [];
1677
- for (const conflict of map.values()) {
1678
- if (conflict.files.length > 1) {
1679
- conflicts.push(conflict);
1663
+ return result;
1664
+ }
1665
+
1666
+ // src/router/parseRouteFile.ts
1667
+ function dynamicSegmentToParam(segment) {
1668
+ const match = segment.match(/^\[(.+)\]$/);
1669
+ if (match) {
1670
+ return ":" + match[1];
1671
+ }
1672
+ return segment;
1673
+ }
1674
+ function extractParamNames(urlPath) {
1675
+ const params = [];
1676
+ const segments = urlPath.split("/");
1677
+ for (const segment of segments) {
1678
+ if (segment.startsWith(":...")) {
1679
+ params.push(segment.slice(4));
1680
+ } else if (segment.startsWith(":")) {
1681
+ params.push(segment.slice(1));
1680
1682
  }
1681
1683
  }
1682
- return conflicts;
1684
+ return params;
1685
+ }
1686
+ function isCatchAllSegment(segment) {
1687
+ return /^\[\.\.\..+\]$/.test(segment);
1688
+ }
1689
+ function isRouteGroup(segment) {
1690
+ return /^\(.+\)$/.test(segment);
1691
+ }
1692
+ function filePathToUrlPath(filePath) {
1693
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
1694
+ const lastSlashIndex = withoutPrefix.lastIndexOf("/");
1695
+ const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
1696
+ if (!dirPath) {
1697
+ return "";
1698
+ }
1699
+ const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
1700
+ return normalizePath(segments.join("/"));
1683
1701
  }
1684
1702
 
1685
- // src/server/createServer.ts
1686
- import {
1687
- createServer as createHttpServer
1688
- } from "http";
1689
- import { createSecureServer as createHttp2SecureServer } from "http2";
1690
- import { readFileSync } from "fs";
1691
- import { Readable as Readable2 } from "stream";
1692
- import path6 from "path";
1693
-
1694
- // src/router/matchRoute.ts
1695
- function matchRoute(routes, method, path10) {
1696
- for (const route of routes) {
1697
- if (route.method !== method) {
1698
- continue;
1703
+ // src/middleware/loadMiddlewares.ts
1704
+ var middlewareCache = /* @__PURE__ */ new Map();
1705
+ function invalidateMiddlewareCache() {
1706
+ middlewareCache.clear();
1707
+ }
1708
+ function getCachedMiddlewares(absPath) {
1709
+ return middlewareCache.get(absPath);
1710
+ }
1711
+ function setCachedMiddlewares(absPath, bundle) {
1712
+ middlewareCache.set(absPath, bundle);
1713
+ }
1714
+ async function loadMiddlewaresFile(filePath) {
1715
+ try {
1716
+ const module = await importWithCacheBust(filePath);
1717
+ const middlewares = module.default ?? module.middlewares ?? [];
1718
+ if (!Array.isArray(middlewares)) {
1719
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
1720
+ return { middlewares: [], injectors: {} };
1699
1721
  }
1700
- if (!route.isDynamic) {
1701
- if (route.urlPath === path10) {
1702
- return { route, params: {} };
1722
+ const validMiddlewares = middlewares.filter((m) => {
1723
+ if (typeof m !== "function") {
1724
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
1725
+ return false;
1703
1726
  }
1704
- continue;
1727
+ return true;
1728
+ });
1729
+ const injectors = module.injectors ?? {};
1730
+ if (typeof injectors !== "object" || injectors === null) {
1731
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
1732
+ return { middlewares: validMiddlewares, injectors: {} };
1705
1733
  }
1706
- const params = matchDynamicPath(route.urlPath, path10, route.paramNames, route.isCatchAll);
1707
- if (params !== null) {
1708
- return { route, params };
1734
+ const validInjectors = {};
1735
+ for (const [name, injector] of Object.entries(injectors)) {
1736
+ if (typeof injector !== "function") {
1737
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
1738
+ continue;
1739
+ }
1740
+ validInjectors[name] = injector;
1709
1741
  }
1742
+ return { middlewares: validMiddlewares, injectors: validInjectors };
1743
+ } catch {
1744
+ return { middlewares: [], injectors: {} };
1710
1745
  }
1711
- return null;
1712
1746
  }
1713
- function matchWsRoute(wsRoutes, path10) {
1714
- for (const route of wsRoutes) {
1715
- if (!route.isDynamic) {
1716
- if (route.urlPath === path10) {
1717
- return { route, params: {} };
1718
- }
1719
- continue;
1720
- }
1721
- const params = matchDynamicPath(route.urlPath, path10, route.paramNames, route.isCatchAll);
1722
- if (params !== null) {
1723
- return { route, params };
1724
- }
1747
+
1748
+ // src/router/scanRoutes.ts
1749
+ var APP_DIR = "src";
1750
+ function toProdAbsPath(sourceAbsPath, rootDir, dist) {
1751
+ let rel = path4.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
1752
+ if (rel.startsWith(`${APP_DIR}/`)) {
1753
+ rel = rel.slice(APP_DIR.length + 1);
1725
1754
  }
1726
- return null;
1755
+ const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
1756
+ return path4.resolve(rootDir, prodRel);
1727
1757
  }
1728
- function matchDynamicPath(pattern, path10, paramNames, isCatchAll) {
1729
- const patternSegments = pattern.split("/").filter(Boolean);
1730
- const pathSegments = path10.split("/").filter(Boolean);
1731
- if (isCatchAll) {
1732
- const nonCatchAllCount = patternSegments.length - 1;
1733
- if (pathSegments.length <= nonCatchAllCount) {
1734
- return null;
1735
- }
1736
- const params2 = {};
1737
- for (let i = 0; i < nonCatchAllCount; i++) {
1738
- const patternSeg = patternSegments[i];
1739
- const pathSeg = pathSegments[i];
1740
- if (patternSeg.startsWith(":")) {
1741
- const paramName = patternSeg.slice(1);
1742
- params2[paramName] = pathSeg;
1743
- } else if (patternSeg !== pathSeg) {
1744
- return null;
1758
+ async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
1759
+ const routeDir = path4.dirname(routeFilePath);
1760
+ const resolvedRoot = path4.resolve(rootDir);
1761
+ const mwPaths = [];
1762
+ let currentDir = path4.resolve(rootDir, routeDir);
1763
+ while (true) {
1764
+ if (dist) {
1765
+ const mwPath = path4.join(currentDir, "middlewares.js");
1766
+ const absMwPath = path4.resolve(rootDir, mwPath);
1767
+ const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
1768
+ if (fs3.existsSync(prodAbsMwPath)) {
1769
+ mwPaths.push(prodAbsMwPath);
1770
+ }
1771
+ } else {
1772
+ for (const ext of [".ts", ".js"]) {
1773
+ const mwPath = path4.join(currentDir, `middlewares${ext}`);
1774
+ const absMwPath = path4.resolve(rootDir, mwPath);
1775
+ if (fs3.existsSync(absMwPath)) {
1776
+ mwPaths.push(absMwPath);
1777
+ break;
1778
+ }
1745
1779
  }
1746
1780
  }
1747
- const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
1748
- const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
1749
- params2[catchAllParamName] = catchAllValue;
1750
- if (Object.keys(params2).length !== paramNames.length) {
1751
- return null;
1752
- }
1753
- return params2;
1754
- }
1755
- if (patternSegments.length !== pathSegments.length) {
1756
- return null;
1781
+ if (currentDir === resolvedRoot) break;
1782
+ const parentDir = path4.dirname(currentDir);
1783
+ if (parentDir === currentDir) break;
1784
+ currentDir = parentDir;
1757
1785
  }
1758
- const params = {};
1759
- for (let i = 0; i < patternSegments.length; i++) {
1760
- const patternSeg = patternSegments[i];
1761
- const pathSeg = pathSegments[i];
1762
- if (patternSeg.startsWith(":")) {
1763
- const paramName = patternSeg.slice(1);
1764
- params[paramName] = pathSeg;
1765
- } else if (patternSeg !== pathSeg) {
1766
- return null;
1786
+ if (mwPaths.length === 0) return void 0;
1787
+ mwPaths.reverse();
1788
+ const mergedMiddlewares = [];
1789
+ const mergedInjectors = {};
1790
+ for (const absMwPath of mwPaths) {
1791
+ let bundle = getCachedMiddlewares(absMwPath);
1792
+ if (bundle === void 0) {
1793
+ bundle = await loadMiddlewaresFile(absMwPath);
1794
+ setCachedMiddlewares(absMwPath, bundle);
1795
+ }
1796
+ mergedMiddlewares.push(...bundle.middlewares);
1797
+ for (const [name, injector] of Object.entries(bundle.injectors)) {
1798
+ mergedInjectors[name] = injector;
1767
1799
  }
1768
1800
  }
1769
- if (Object.keys(params).length !== paramNames.length) {
1770
- return null;
1801
+ if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
1802
+ return void 0;
1771
1803
  }
1772
- return params;
1804
+ return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
1773
1805
  }
1774
-
1775
- // src/loader/resolveExports.ts
1776
- function resolveExport(module, exportName) {
1777
- if (exportName in module && typeof module[exportName] !== "undefined") {
1778
- return module[exportName];
1779
- }
1780
- const defaultExport = module.default;
1781
- if (defaultExport !== null && typeof defaultExport === "object") {
1782
- const value = defaultExport[exportName];
1783
- if (value !== void 0) {
1784
- return value;
1806
+ async function extractMethodsFromHandler(absPath) {
1807
+ try {
1808
+ const module = await importWithCacheBust(absPath);
1809
+ const methods = [];
1810
+ for (const key of Object.keys(module)) {
1811
+ if (isHttpMethod(key) && typeof module[key] === "function") {
1812
+ methods.push(key);
1813
+ }
1785
1814
  }
1815
+ return methods;
1816
+ } catch (err) {
1817
+ const reason = err instanceof Error ? err.message : String(err);
1818
+ console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25 ${absPath}: ${reason}`);
1819
+ return [];
1786
1820
  }
1787
- return void 0;
1788
1821
  }
1789
-
1790
- // src/loader/validateRouteModule.ts
1791
- function validateRouteModule(value, method, filePath) {
1792
- if (typeof value !== "function") {
1793
- throw new Error(
1794
- `Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
1795
- );
1796
- }
1797
- }
1798
-
1799
- // src/loader/loadRouteModule.ts
1800
- async function loadRouteModule(filePath, method) {
1801
- let module;
1822
+ async function hasWsExport(absPath) {
1802
1823
  try {
1803
- module = await importWithCacheBust(filePath);
1824
+ const module = await importWithCacheBust(absPath);
1825
+ return typeof module["WS"] === "function";
1804
1826
  } catch (err) {
1805
1827
  const reason = err instanceof Error ? err.message : String(err);
1806
- throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
1807
- }
1808
- const handler = resolveExport(module, method);
1809
- validateRouteModule(handler, method, filePath);
1810
- return { handler, method };
1811
- }
1812
-
1813
- // src/utils/parseJsonBody.ts
1814
- function parseJsonBody(text) {
1815
- try {
1816
- const data = JSON.parse(text);
1817
- return { success: true, data };
1818
- } catch {
1819
- return { success: false, error: "Invalid JSON body" };
1828
+ console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25\uFF08WS \u68C0\u6D4B\uFF09${absPath}: ${reason}`);
1829
+ return false;
1820
1830
  }
1821
1831
  }
1822
-
1823
- // src/utils/parseMultipart.ts
1824
- async function parseMultipart(request) {
1825
- const formData = await request.formData();
1826
- const fields = {};
1827
- const files = [];
1828
- for (const [key, value] of formData.entries()) {
1829
- if (value instanceof File) {
1830
- files.push({
1831
- name: key,
1832
- filename: value.name,
1833
- type: value.type,
1834
- size: value.size,
1835
- arrayBuffer: () => value.arrayBuffer()
1836
- });
1837
- } else {
1838
- if (key in fields) {
1839
- const existing = fields[key];
1840
- if (Array.isArray(existing)) {
1841
- existing.push(value);
1842
- } else {
1843
- fields[key] = [existing, value];
1844
- }
1845
- } else {
1846
- fields[key] = value;
1832
+ async function scanRoutes(rootDir, patterns, dist) {
1833
+ const files = await fg(patterns, {
1834
+ cwd: rootDir,
1835
+ onlyFiles: true,
1836
+ absolute: false
1837
+ });
1838
+ const routes = [];
1839
+ const wsRoutes = [];
1840
+ for (const file of files) {
1841
+ const normalizedFile = file.replace(/\\/g, "/");
1842
+ const fileName = normalizedFile.split("/").pop();
1843
+ if (fileName === "handler.ts" || fileName === "handler.js") {
1844
+ const absPath = path4.resolve(rootDir, normalizedFile);
1845
+ const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
1846
+ const urlPath = filePathToUrlPath(normalizedFile);
1847
+ const paramNames = extractParamNames(urlPath);
1848
+ const isDynamic = paramNames.length > 0;
1849
+ const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
1850
+ const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
1851
+ const methods = await extractMethodsFromHandler(importPath);
1852
+ for (const method of methods) {
1853
+ routes.push({
1854
+ method,
1855
+ urlPath,
1856
+ filePath: normalizedFile,
1857
+ paramNames,
1858
+ isDynamic,
1859
+ isCatchAll: isCatchAll || void 0,
1860
+ middlewares: middlewareBundle?.middlewares,
1861
+ injectors: middlewareBundle?.injectors
1862
+ });
1863
+ }
1864
+ const hasWs = await hasWsExport(importPath);
1865
+ if (hasWs) {
1866
+ wsRoutes.push({
1867
+ urlPath,
1868
+ filePath: normalizedFile,
1869
+ paramNames,
1870
+ isDynamic,
1871
+ isCatchAll: isCatchAll || void 0,
1872
+ middlewares: middlewareBundle?.middlewares,
1873
+ injectors: middlewareBundle?.injectors
1874
+ });
1847
1875
  }
1876
+ continue;
1848
1877
  }
1849
1878
  }
1850
- return { fields, files };
1879
+ return { routes, wsRoutes };
1851
1880
  }
1852
1881
 
1853
- // src/runtime/resolveInput.ts
1854
- async function resolveInput(method, request) {
1855
- const inputType = getInputTypeForMethod(method);
1856
- if (inputType === "body") {
1857
- const contentType = request.headers.get("content-type") ?? "";
1858
- if (contentType.includes("multipart/form-data")) {
1859
- return parseMultipart(request);
1860
- }
1861
- if (contentType.includes("application/x-www-form-urlencoded")) {
1862
- const text2 = await request.text();
1863
- if (text2.trim() === "") return null;
1864
- const params = new URLSearchParams(text2);
1865
- const obj = {};
1866
- for (const [key, value] of params) {
1867
- obj[key] = value;
1868
- }
1869
- return obj;
1882
+ // src/router/sortRoutes.ts
1883
+ function sortRoutes(routes) {
1884
+ return [...routes].sort((a, b) => {
1885
+ if (a.isDynamic !== b.isDynamic) {
1886
+ return a.isDynamic ? 1 : -1;
1870
1887
  }
1871
- const text = await request.text();
1872
- if (text.trim() === "") {
1873
- return null;
1888
+ if (a.isCatchAll !== b.isCatchAll) {
1889
+ return a.isCatchAll ? 1 : -1;
1874
1890
  }
1875
- const result = parseJsonBody(text);
1876
- if (!result.success) {
1877
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
1878
- {
1879
- path: "body",
1880
- code: "INVALID_FORMAT",
1881
- expected: "JSON",
1882
- received: "text",
1883
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
1884
- }
1885
- ]);
1891
+ const aSegments = a.urlPath.split("/").filter(Boolean).length;
1892
+ const bSegments = b.urlPath.split("/").filter(Boolean).length;
1893
+ if (aSegments !== bSegments) {
1894
+ return aSegments - bSegments;
1886
1895
  }
1887
- return result.data;
1888
- }
1889
- const url = new URL(request.url);
1890
- return queryToObject(url.searchParams);
1896
+ return a.urlPath.localeCompare(b.urlPath);
1897
+ });
1891
1898
  }
1892
1899
 
1893
- // src/response/sendNodeResponse.ts
1894
- import { Readable } from "stream";
1895
- async function sendNodeResponse(response, res) {
1896
- res.statusCode = response.status;
1897
- for (const [key, value] of response.headers) {
1898
- if (key.toLowerCase() === "set-cookie") {
1899
- res.appendHeader(key, value);
1900
- } else {
1901
- res.setHeader(key, value);
1900
+ // src/cli/generateSchemaFiles.ts
1901
+ import path5 from "path";
1902
+ import fs4 from "fs/promises";
1903
+
1904
+ // src/ast/generateZodSchema.ts
1905
+ var CodeGenContext = class {
1906
+ /** 命名类型集合:name → RuntimeType */
1907
+ namedTypes = /* @__PURE__ */ new Map();
1908
+ /** 类型解析器(用于解析 ref 的实际类型) */
1909
+ resolveType;
1910
+ /** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
1911
+ entryTypeName = "";
1912
+ /** 入口类型导出名(exportName,自引用时用此名生成变量名) */
1913
+ entryExportName = "";
1914
+ /**
1915
+ * 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
1916
+ *
1917
+ * true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
1918
+ * 嵌套类型(array/object/tuple/union 等)的元素递归处理。
1919
+ */
1920
+ coerce = false;
1921
+ constructor(resolveType) {
1922
+ this.resolveType = resolveType;
1923
+ }
1924
+ };
1925
+ function collectNamedTypes(type, ctx) {
1926
+ switch (type.kind) {
1927
+ case "string":
1928
+ case "number":
1929
+ case "boolean":
1930
+ case "bigint":
1931
+ case "null":
1932
+ case "undefined":
1933
+ case "any":
1934
+ case "unknown":
1935
+ case "literal":
1936
+ case "date":
1937
+ return;
1938
+ case "array":
1939
+ collectNamedTypes(type.element, ctx);
1940
+ return;
1941
+ case "tuple":
1942
+ for (const el of type.elements) {
1943
+ collectNamedTypes(el.type, ctx);
1944
+ }
1945
+ return;
1946
+ case "object":
1947
+ for (const prop of type.properties) {
1948
+ collectNamedTypes(prop.type, ctx);
1949
+ }
1950
+ return;
1951
+ case "union":
1952
+ for (const member of type.members) {
1953
+ collectNamedTypes(member, ctx);
1954
+ }
1955
+ return;
1956
+ case "record":
1957
+ collectNamedTypes(type.key, ctx);
1958
+ collectNamedTypes(type.value, ctx);
1959
+ return;
1960
+ case "map":
1961
+ collectNamedTypes(type.key, ctx);
1962
+ collectNamedTypes(type.value, ctx);
1963
+ return;
1964
+ case "set":
1965
+ collectNamedTypes(type.element, ctx);
1966
+ return;
1967
+ case "ref": {
1968
+ if (ctx.namedTypes.has(type.name)) return;
1969
+ ctx.namedTypes.set(type.name, { kind: "any" });
1970
+ const resolved = ctx.resolveType(type.name);
1971
+ if (resolved) {
1972
+ ctx.namedTypes.set(type.name, resolved);
1973
+ collectNamedTypes(resolved, ctx);
1974
+ }
1975
+ return;
1902
1976
  }
1903
1977
  }
1904
- if (response.body) {
1905
- const nodeStream = Readable.fromWeb(response.body);
1906
- await new Promise((resolve, reject) => {
1907
- nodeStream.on("error", reject);
1908
- res.on("error", reject);
1909
- res.on("finish", resolve);
1910
- nodeStream.pipe(res);
1911
- });
1912
- return;
1978
+ }
1979
+ function runtimeTypeToZodExpression(type, ctx, constraints) {
1980
+ const expr = baseExpression(type, ctx);
1981
+ const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
1982
+ if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
1983
+ return wrapCoercePreprocess(type.kind, withConstraints);
1913
1984
  }
1914
- res.end();
1985
+ return withConstraints;
1915
1986
  }
1916
-
1917
- // src/validator/validateInput.ts
1918
- var moduleCache = /* @__PURE__ */ new Map();
1919
- function invalidateSchemaCache() {
1920
- moduleCache.clear();
1921
- }
1922
- async function loadSchemaModule(schemaPath) {
1923
- let mod = moduleCache.get(schemaPath);
1924
- if (!mod) {
1925
- mod = await importWithCacheBust(schemaPath);
1926
- moduleCache.set(schemaPath, mod);
1927
- }
1928
- return mod;
1987
+ function applyConstraints(baseExpr, constraints, typeKind) {
1988
+ const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
1989
+ return `${baseExpr}${suffix}`;
1929
1990
  }
1930
- async function validateInput(schemaPath, method, inputType, input) {
1931
- const schemaName = getSchemaName(method, inputType);
1932
- const schemaKey = `${schemaName}Schema`;
1933
- let mod;
1934
- try {
1935
- mod = await loadSchemaModule(schemaPath);
1936
- } catch (err) {
1937
- const reason = err instanceof Error ? err.message : String(err);
1938
- throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
1939
- }
1940
- const schema = mod[schemaKey];
1941
- if (schema === void 0 || schema === null) {
1942
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
1943
- return { valid: true, issues: [], data };
1944
- }
1945
- if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
1946
- throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
1947
- }
1948
- const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
1949
- const zodSchema = schema;
1950
- const result = zodSchema.safeParse(inputObj);
1951
- if (result.success) {
1952
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
1953
- return { valid: true, issues: [], data };
1991
+ function constraintToZodChain(constraint, _typeKind) {
1992
+ switch (constraint.kind) {
1993
+ case "max":
1994
+ return `.max(${constraint.value})`;
1995
+ case "min":
1996
+ return `.min(${constraint.value})`;
1997
+ case "int":
1998
+ return ".int()";
1999
+ case "positive":
2000
+ return ".positive()";
2001
+ case "negative":
2002
+ return ".negative()";
2003
+ case "nonnegative":
2004
+ return ".nonnegative()";
2005
+ case "nonpositive":
2006
+ return ".nonpositive()";
2007
+ case "maxLength":
2008
+ return `.max(${constraint.value})`;
2009
+ case "minLength":
2010
+ return `.min(${constraint.value})`;
2011
+ case "length":
2012
+ return `.length(${constraint.value})`;
2013
+ case "regex": {
2014
+ const flags = constraint.flags ?? "";
2015
+ return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
2016
+ }
2017
+ case "email":
2018
+ return ".email()";
2019
+ case "url":
2020
+ return ".url()";
2021
+ case "uuid":
2022
+ return ".uuid()";
1954
2023
  }
1955
- const issues = mapZodIssues(result.error);
1956
- return { valid: false, issues, data: inputObj };
1957
- }
1958
- function mapZodIssues(error) {
1959
- return error.issues.map((issue) => {
1960
- const code = mapZodCode(issue.code, issue.message);
1961
- const path10 = issue.path.map(String).join(".") || "";
1962
- return {
1963
- path: path10,
1964
- code,
1965
- expected: issue.expected ?? mapExpectedFromMessage(issue.message),
1966
- received: issue.received ?? mapReceivedFromMessage(issue.message),
1967
- message: issue.message
1968
- };
1969
- });
1970
2024
  }
1971
- function mapZodCode(zodCode, message) {
1972
- switch (zodCode) {
1973
- case "invalid_type":
1974
- case "invalid_union":
1975
- case "invalid_union_discriminator":
1976
- return "TYPE_MISMATCH";
1977
- case "unrecognized_keys":
1978
- return "INVALID_FORMAT";
1979
- case "invalid_value":
1980
- case "invalid_string":
1981
- case "too_small":
1982
- case "too_big":
1983
- case "invalid_intersection_types":
1984
- case "not_multiple_of":
1985
- return "INVALID_VALUE";
1986
- case "custom":
1987
- return "INVALID_VALUE";
1988
- default:
1989
- if (message.includes("Required") || message.includes("required")) {
1990
- return "MISSING_FIELD";
2025
+ function baseExpression(type, ctx) {
2026
+ switch (type.kind) {
2027
+ case "string":
2028
+ return "z.string()";
2029
+ case "number":
2030
+ return "z.number()";
2031
+ case "boolean":
2032
+ return "z.boolean()";
2033
+ case "bigint":
2034
+ return "z.never()";
2035
+ case "null":
2036
+ return "z.null()";
2037
+ case "undefined":
2038
+ return "z.undefined()";
2039
+ case "any":
2040
+ case "unknown":
2041
+ return "z.unknown()";
2042
+ case "literal":
2043
+ return `z.literal(${JSON.stringify(type.value)})`;
2044
+ case "array":
2045
+ return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
2046
+ case "tuple":
2047
+ return generateTupleExpression(type.elements, ctx);
2048
+ case "object":
2049
+ return generateObjectExpression(type.properties, ctx);
2050
+ case "union":
2051
+ return generateUnionExpression(type.members, ctx);
2052
+ case "date":
2053
+ return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
2054
+ case "record":
2055
+ return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
2056
+ case "map":
2057
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
2058
+ case "set":
2059
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
2060
+ case "ref":
2061
+ if (type.name === ctx.entryTypeName) {
2062
+ return `${ctx.entryExportName}Schema`;
1991
2063
  }
1992
- return "INVALID_VALUE";
2064
+ return `${type.name}Schema`;
1993
2065
  }
1994
2066
  }
1995
- function mapExpectedFromMessage(message) {
1996
- const match = message.match(/Expected\s+(\w+)/i);
1997
- return match ? match[1].toLowerCase() : "unknown";
2067
+ var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2068
+ var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
2069
+ var COERCE_MAP_HELPER = 'export const coerceMap = (v) => Array.isArray(v) ? new Map(v) : v instanceof Map ? v : (v && typeof v === "object" ? new Map(Object.entries(v)) : v);';
2070
+ var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2071
+ var HELPERS_FILENAME = "faapi-helpers.js";
2072
+ function generateHelpersFileSource() {
2073
+ return [
2074
+ "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
2075
+ COERCE_NUMBER_HELPER,
2076
+ COERCE_BOOLEAN_HELPER,
2077
+ COERCE_MAP_HELPER,
2078
+ COERCE_SET_HELPER,
2079
+ ""
2080
+ ].join("\n");
1998
2081
  }
1999
- function mapReceivedFromMessage(message) {
2000
- const match = message.match(/received\s+(\w+)/i);
2001
- return match ? match[1].toLowerCase() : "unknown";
2082
+ function usesCoerceHelpers(code) {
2083
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
2002
2084
  }
2003
-
2004
- // src/utils/getClientIp.ts
2005
- function getClientIp(req) {
2006
- const xff = req.headers["x-forwarded-for"];
2007
- if (typeof xff === "string" && xff.length > 0) {
2008
- const first = xff.split(",")[0]?.trim();
2009
- if (first) return first;
2085
+ function wrapCoercePreprocess(kind, inner) {
2086
+ if (kind === "number") {
2087
+ return `z.preprocess(coerceNumber, ${inner})`;
2010
2088
  }
2011
- const remote = req.socket?.remoteAddress;
2012
- if (remote) {
2013
- if (remote.startsWith("::ffff:")) {
2014
- return remote.slice(7);
2089
+ return `z.preprocess(coerceBoolean, ${inner})`;
2090
+ }
2091
+ function generateTupleExpression(elements, ctx) {
2092
+ const fixedExprs = [];
2093
+ const fixedOptional = [];
2094
+ let restExpression = "";
2095
+ let restStarted = false;
2096
+ for (const el of elements) {
2097
+ if (el.rest) {
2098
+ restExpression = runtimeTypeToZodExpression(el.type, ctx);
2099
+ restStarted = true;
2100
+ } else if (!restStarted) {
2101
+ fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
2102
+ fixedOptional.push(el.optional);
2015
2103
  }
2016
- return remote;
2017
2104
  }
2018
- return "";
2019
- }
2020
-
2021
- // src/server/handleWsUpgrade.ts
2022
- import { WebSocketServer, WebSocket } from "ws";
2023
- import path4 from "path";
2024
-
2025
- // src/errors/formatErrorResponse.ts
2026
- function formatErrorResponse(error) {
2027
- if (error instanceof ValidationError) {
2028
- const body2 = {
2029
- code: error.code,
2030
- message: error.message,
2031
- issues: error.issues
2032
- };
2033
- return new Response(JSON.stringify({ error: body2 }), {
2034
- status: error.statusCode,
2035
- headers: { "Content-Type": "application/json" }
2036
- });
2105
+ if (restExpression) {
2106
+ return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
2037
2107
  }
2038
- if (error instanceof MethodNotAllowedError) {
2039
- const body2 = {
2040
- code: error.code,
2041
- message: error.message
2042
- };
2043
- return new Response(JSON.stringify({ error: body2 }), {
2044
- status: error.statusCode,
2045
- headers: {
2046
- "Content-Type": "application/json",
2047
- Allow: error.allowedMethods.join(", ")
2048
- }
2049
- });
2108
+ const hasOptional = fixedOptional.some((o) => o);
2109
+ if (!hasOptional) {
2110
+ return `z.tuple([${fixedExprs.join(", ")}])`;
2050
2111
  }
2051
- if (error instanceof FaapiError) {
2052
- const body2 = {
2053
- code: error.code,
2054
- message: error.message
2055
- };
2056
- return new Response(JSON.stringify({ error: body2 }), {
2057
- status: error.statusCode,
2058
- headers: { "Content-Type": "application/json" }
2059
- });
2112
+ const variants = [];
2113
+ for (let len = fixedExprs.length; len >= 0; len--) {
2114
+ const removed = fixedOptional.slice(len);
2115
+ if (removed.length > 0 && removed.some((o) => !o)) {
2116
+ break;
2117
+ }
2118
+ const subset = fixedExprs.slice(0, len);
2119
+ variants.push(`z.tuple([${subset.join(", ")}])`);
2060
2120
  }
2061
- const body = {
2062
- code: "INTERNAL_ERROR",
2063
- message: error instanceof Error ? error.message : "An unknown error occurred"
2064
- };
2065
- return new Response(JSON.stringify({ error: body }), {
2066
- status: 500,
2067
- headers: { "Content-Type": "application/json" }
2068
- });
2069
- }
2070
-
2071
- // src/server/serverUtils.ts
2072
- function nodeHttpToWebHeaders(req) {
2073
- const headers = new Headers();
2074
- for (const [key, value] of Object.entries(req.headers)) {
2075
- if (value === void 0) continue;
2076
- if (Array.isArray(value)) {
2077
- for (const v of value) headers.append(key, v);
2078
- } else {
2079
- headers.set(key, value);
2080
- }
2121
+ variants.reverse();
2122
+ if (variants.length === 1) {
2123
+ return variants[0];
2081
2124
  }
2082
- return headers;
2125
+ return `z.union([${variants.join(", ")}])`;
2083
2126
  }
2084
- function buildErrorResponse(err) {
2085
- try {
2086
- return formatErrorResponse(err);
2087
- } catch {
2088
- return new Response(
2089
- JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
2090
- {
2091
- status: 500,
2092
- headers: { "Content-Type": "application/json" }
2093
- }
2094
- );
2127
+ function generateObjectExpression(properties, ctx) {
2128
+ const fields = properties.map((prop) => {
2129
+ const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
2130
+ const finalExpr = prop.optional ? `${expr}.optional()` : expr;
2131
+ return `${JSON.stringify(prop.name)}: ${finalExpr}`;
2132
+ });
2133
+ return `z.object({ ${fields.join(", ")} })`;
2134
+ }
2135
+ function generateUnionExpression(members, ctx) {
2136
+ const hasNull = members.some((m) => m.kind === "null");
2137
+ const nonNull = members.filter((m) => m.kind !== "null");
2138
+ if (hasNull && nonNull.length === 1) {
2139
+ return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
2095
2140
  }
2141
+ if (hasNull) {
2142
+ const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2143
+ return `z.union([${unionInner2}]).nullable()`;
2144
+ }
2145
+ const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2146
+ return `z.union([${unionInner}])`;
2096
2147
  }
2097
-
2098
- // src/runtime/wsHandler.ts
2099
- function wrapWsSocket(rawSocket) {
2100
- return {
2101
- send(data) {
2102
- const payload = typeof data === "string" || Buffer.isBuffer(data) ? data : JSON.stringify(data);
2103
- rawSocket.send(payload);
2104
- },
2105
- close(code, reason) {
2106
- rawSocket.close(code, reason);
2107
- },
2108
- get readyState() {
2109
- return rawSocket.readyState;
2110
- }
2111
- };
2148
+ function generateNamedTypeDeclaration(name, type, ctx) {
2149
+ const expr = runtimeTypeToZodExpression(type, ctx);
2150
+ const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
2151
+ if (hasRef) {
2152
+ return `const ${name}Schema = z.lazy(() => ${expr});`;
2153
+ }
2154
+ return `const ${name}Schema = ${expr};`;
2155
+ }
2156
+ function containsRef(type, visited) {
2157
+ switch (type.kind) {
2158
+ case "ref":
2159
+ return visited.has(type.name);
2160
+ case "array":
2161
+ return containsRef(type.element, visited);
2162
+ case "tuple":
2163
+ return type.elements.some((el) => containsRef(el.type, visited));
2164
+ case "object":
2165
+ return type.properties.some((prop) => containsRef(prop.type, visited));
2166
+ case "union":
2167
+ return type.members.some((m) => containsRef(m, visited));
2168
+ case "record":
2169
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
2170
+ case "map":
2171
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
2172
+ case "set":
2173
+ return containsRef(type.element, visited);
2174
+ default:
2175
+ return false;
2176
+ }
2177
+ }
2178
+ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
2179
+ const ctx = new CodeGenContext(resolveType);
2180
+ const name = exportName ?? typeInfo.name;
2181
+ ctx.entryTypeName = typeInfo.name;
2182
+ ctx.entryExportName = name;
2183
+ ctx.coerce = coerce;
2184
+ collectNamedTypes(typeInfo.runtimeType, ctx);
2185
+ ctx.namedTypes.delete(typeInfo.name);
2186
+ const lines = [];
2187
+ lines.push("import { z } from 'zod';");
2188
+ lines.push("");
2189
+ for (const [n, type] of ctx.namedTypes) {
2190
+ lines.push(generateNamedTypeDeclaration(n, type, ctx));
2191
+ }
2192
+ if (ctx.namedTypes.size > 0) lines.push("");
2193
+ const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
2194
+ const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
2195
+ if (hasSelfRef) {
2196
+ lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
2197
+ } else {
2198
+ lines.push(`export const ${name}Schema = ${entryExpr};`);
2199
+ }
2200
+ return lines.join("\n");
2112
2201
  }
2113
2202
 
2114
- // src/server/handleWsUpgrade.ts
2115
- function getPathname(req) {
2116
- const url = req.url ?? "/";
2117
- const idx = url.indexOf("?");
2118
- return idx >= 0 ? url.slice(0, idx) : url;
2203
+ // src/cli/generateSchemaFiles.ts
2204
+ function getSchemaOutputPath(sourceFile, dist, rootDir) {
2205
+ let rel = sourceFile.replace(/\\/g, "/");
2206
+ if (rel.startsWith("src/")) {
2207
+ rel = rel.slice(4);
2208
+ }
2209
+ const idx = rel.lastIndexOf("/");
2210
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2211
+ return path5.resolve(rootDir, dist, relDir, "zod.js");
2119
2212
  }
2120
- async function loadWsHandler(filePath, ctx) {
2121
- const module = await importWithCacheBust(filePath);
2122
- const handler = module["WS"];
2123
- if (typeof handler !== "function") {
2124
- throw new Error(`WS export not found in ${filePath}`);
2213
+ function getRuntimeSchemaPath(filePath, dist, rootDir) {
2214
+ let rel = filePath.replace(/\\/g, "/");
2215
+ if (rel.startsWith("src/")) {
2216
+ rel = rel.slice(4);
2217
+ } else if (rel.startsWith(`${dist}/`)) {
2218
+ rel = rel.slice(dist.length + 1);
2125
2219
  }
2126
- return handler(ctx);
2220
+ const idx = rel.lastIndexOf("/");
2221
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2222
+ return path5.resolve(rootDir, dist, relDir, "zod.js");
2127
2223
  }
2128
- function bindEvents(rawSocket, handlers) {
2129
- if (!handlers) return;
2130
- const ws = wrapWsSocket(rawSocket);
2131
- if (handlers.onOpen) {
2132
- if (rawSocket.readyState === WebSocket.OPEN) {
2133
- handlers.onOpen(ws);
2134
- } else {
2135
- rawSocket.once("open", () => handlers.onOpen(ws));
2224
+ function getHelpersImportPath(relDir) {
2225
+ if (!relDir) return `./${HELPERS_FILENAME}`;
2226
+ const depth = relDir.split("/").filter(Boolean).length;
2227
+ return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2228
+ }
2229
+ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2230
+ const resolveType = (name) => allTypes.get(name)?.runtimeType;
2231
+ const lines = ["import { z } from 'zod';"];
2232
+ const schemaBlocks = [];
2233
+ for (const source of sources) {
2234
+ const { schemaName, typeInfo } = source;
2235
+ if (!typeInfo) {
2236
+ continue;
2136
2237
  }
2238
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2239
+ const block = [`// ${schemaName}`];
2240
+ const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2241
+ /^import \{ z \} from 'zod';\s*\n\s*\n/,
2242
+ ""
2243
+ );
2244
+ block.push(schemaCode);
2245
+ block.push("");
2246
+ schemaBlocks.push(block.join("\n"));
2137
2247
  }
2138
- if (handlers.onMessage) {
2139
- rawSocket.on("message", (data) => {
2140
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
2141
- handlers.onMessage(ws, buf.toString("utf8"));
2142
- });
2248
+ const allSchemaCode = schemaBlocks.join("\n");
2249
+ if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2250
+ lines.push(
2251
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2252
+ );
2143
2253
  }
2144
- if (handlers.onClose) {
2145
- rawSocket.on("close", (code, reason) => {
2146
- handlers.onClose(ws, code, reason.toString("utf8"));
2147
- });
2254
+ lines.push("");
2255
+ lines.push(...schemaBlocks);
2256
+ return lines.join("\n").replace(/\n+$/, "\n");
2257
+ }
2258
+ async function generateSchemaFiles(routes, rootDir, dist) {
2259
+ if (routes.length === 0) return;
2260
+ const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2261
+ const sourcesByFile = /* @__PURE__ */ new Map();
2262
+ for (const source of sources) {
2263
+ let list = sourcesByFile.get(source.filePath);
2264
+ if (!list) {
2265
+ list = [];
2266
+ sourcesByFile.set(source.filePath, list);
2267
+ }
2268
+ list.push(source);
2148
2269
  }
2149
- if (handlers.onError) {
2150
- rawSocket.on("error", (err) => {
2151
- handlers.onError(ws, err);
2152
- });
2270
+ const fileEntries = [];
2271
+ for (const [filePath, fileSources] of sourcesByFile) {
2272
+ const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
2273
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2274
+ const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2275
+ let relForDir = relFile;
2276
+ if (relForDir.startsWith("src/")) {
2277
+ relForDir = relForDir.slice(4);
2278
+ }
2279
+ const dirIdx = relForDir.lastIndexOf("/");
2280
+ const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2281
+ const helpersImportPath = getHelpersImportPath(zodRelDir);
2282
+ const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
2283
+ fileEntries.push({ outputPath, source });
2153
2284
  }
2285
+ const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2286
+ if (usesCoerceHelpers(allSourceCode)) {
2287
+ const helpersPath = path5.resolve(rootDir, dist, HELPERS_FILENAME);
2288
+ await writeSchemaFile(helpersPath, generateHelpersFileSource());
2289
+ }
2290
+ await Promise.all(
2291
+ fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2292
+ );
2154
2293
  }
2155
- async function sendResponseToSocket(socket, response) {
2156
- const body = await response.text().catch(() => "");
2157
- const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
2158
- `;
2159
- const headerLines = [];
2160
- let hasContentLength = false;
2161
- for (const [key, value] of response.headers) {
2162
- if (key.toLowerCase() === "content-length") {
2163
- hasContentLength = true;
2164
- }
2165
- headerLines.push(`${key}: ${value}`);
2294
+ async function writeSchemaFile(outputPath, source) {
2295
+ await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
2296
+ await fs4.writeFile(outputPath, source, "utf-8");
2297
+ }
2298
+
2299
+ // src/validator/validateInput.ts
2300
+ var moduleCache = /* @__PURE__ */ new Map();
2301
+ function invalidateSchemaCache() {
2302
+ moduleCache.clear();
2303
+ }
2304
+ async function loadSchemaModule(schemaPath) {
2305
+ let mod = moduleCache.get(schemaPath);
2306
+ if (!mod) {
2307
+ mod = await importWithCacheBust(schemaPath);
2308
+ moduleCache.set(schemaPath, mod);
2166
2309
  }
2167
- if (!hasContentLength) {
2168
- headerLines.push(`Content-Length: ${Buffer.byteLength(body)}`);
2310
+ return mod;
2311
+ }
2312
+ async function validateInput(schemaPath, method, inputType, input) {
2313
+ const schemaName = getSchemaName(method, inputType);
2314
+ const schemaKey = `${schemaName}Schema`;
2315
+ let mod;
2316
+ try {
2317
+ mod = await loadSchemaModule(schemaPath);
2318
+ } catch (err) {
2319
+ const reason = err instanceof Error ? err.message : String(err);
2320
+ throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
2169
2321
  }
2170
- socket.write(statusLine + headerLines.join("\r\n") + "\r\n\r\n" + body);
2171
- socket.destroy();
2322
+ const schema = mod[schemaKey];
2323
+ if (schema === void 0 || schema === null) {
2324
+ const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2325
+ return { valid: true, issues: [], data };
2326
+ }
2327
+ if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2328
+ throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2329
+ }
2330
+ const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2331
+ const zodSchema = schema;
2332
+ const result = zodSchema.safeParse(inputObj);
2333
+ if (result.success) {
2334
+ const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2335
+ return { valid: true, issues: [], data };
2336
+ }
2337
+ const issues = mapZodIssues(result.error);
2338
+ return { valid: false, issues, data: inputObj };
2172
2339
  }
2173
- function attachWebSocket(options) {
2174
- const { server, routesRef, rootDir, config, globalMiddlewares } = options;
2175
- const wss = new WebSocketServer({ noServer: true });
2176
- server.on("upgrade", async (req, socket, head) => {
2177
- const currentWsRoutes = routesRef.wsCurrent;
2178
- const pathname = getPathname(req);
2179
- const match = matchWsRoute(currentWsRoutes, pathname);
2180
- if (!match) {
2181
- socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
2182
- socket.destroy();
2183
- return;
2184
- }
2185
- const { route, params } = match;
2186
- const headers = nodeHttpToWebHeaders(req);
2187
- const host = req.headers.host ?? "localhost";
2188
- const url = `http://${host}${req.url ?? "/"}`;
2189
- const request = new Request(url, { method: "GET", headers });
2190
- const ctx = createContext(request, params, config, getClientIp(req));
2191
- const meta = ctx.meta;
2192
- let upgraded = false;
2193
- const finalHandler = async () => {
2194
- let handlers;
2195
- try {
2196
- const absoluteFilePath = path4.resolve(rootDir, route.filePath);
2197
- handlers = await loadWsHandler(absoluteFilePath, ctx);
2198
- } catch (err) {
2199
- const reason = err instanceof Error ? err.message : String(err);
2200
- console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
2201
- return new Response("Internal Server Error", { status: 500 });
2202
- }
2203
- await new Promise((resolve, reject) => {
2204
- wss.handleUpgrade(req, socket, head, (rawSocket) => {
2205
- try {
2206
- bindEvents(rawSocket, handlers);
2207
- wss.emit("connection", rawSocket, req);
2208
- upgraded = true;
2209
- resolve();
2210
- } catch (err) {
2211
- reject(err);
2212
- }
2213
- });
2214
- });
2215
- return new Response(null, { status: 200 });
2340
+ function mapZodIssues(error) {
2341
+ return error.issues.map((issue) => {
2342
+ const code = mapZodCode(issue.code, issue.message);
2343
+ const path11 = issue.path.map(String).join(".") || "";
2344
+ return {
2345
+ path: path11,
2346
+ code,
2347
+ expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2348
+ received: issue.received ?? mapReceivedFromMessage(issue.message),
2349
+ message: issue.message
2216
2350
  };
2217
- let response;
2218
- try {
2219
- const dirMiddlewares = route.middlewares ?? [];
2220
- const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
2221
- if (allMiddlewares.length > 0) {
2222
- response = await compose(allMiddlewares, ctx, finalHandler);
2223
- } else {
2224
- response = await finalHandler();
2225
- }
2226
- } catch (err) {
2227
- if (upgraded) {
2228
- console.error("[faapi] WS \u63E1\u624B\u540E\u4E2D\u95F4\u4EF6\u629B\u9519:", err);
2229
- return;
2230
- }
2231
- response = buildErrorResponse(err);
2232
- }
2233
- if (upgraded) {
2234
- return;
2235
- }
2236
- await sendResponseToSocket(socket, mergeMeta(response, meta));
2237
2351
  });
2238
- return wss;
2352
+ }
2353
+ function mapZodCode(zodCode, message) {
2354
+ switch (zodCode) {
2355
+ case "invalid_type":
2356
+ case "invalid_union":
2357
+ case "invalid_union_discriminator":
2358
+ return "TYPE_MISMATCH";
2359
+ case "unrecognized_keys":
2360
+ return "INVALID_FORMAT";
2361
+ case "invalid_value":
2362
+ case "invalid_string":
2363
+ case "too_small":
2364
+ case "too_big":
2365
+ case "invalid_intersection_types":
2366
+ case "not_multiple_of":
2367
+ return "INVALID_VALUE";
2368
+ case "custom":
2369
+ return "INVALID_VALUE";
2370
+ default:
2371
+ if (message.includes("Required") || message.includes("required")) {
2372
+ return "MISSING_FIELD";
2373
+ }
2374
+ return "INVALID_VALUE";
2375
+ }
2376
+ }
2377
+ function mapExpectedFromMessage(message) {
2378
+ const match = message.match(/Expected\s+(\w+)/i);
2379
+ return match ? match[1].toLowerCase() : "unknown";
2380
+ }
2381
+ function mapReceivedFromMessage(message) {
2382
+ const match = message.match(/received\s+(\w+)/i);
2383
+ return match ? match[1].toLowerCase() : "unknown";
2239
2384
  }
2240
2385
 
2241
- // src/cli/generateSchemaFiles.ts
2242
- import path5 from "path";
2243
- import fs3 from "fs/promises";
2386
+ // src/server/createServer.ts
2387
+ import {
2388
+ createServer as createHttpServer
2389
+ } from "http";
2390
+ import { createSecureServer as createHttp2SecureServer } from "http2";
2391
+ import { readFileSync } from "fs";
2392
+ import { Readable as Readable2 } from "stream";
2393
+ import path7 from "path";
2244
2394
 
2245
- // src/ast/generateZodSchema.ts
2246
- var CodeGenContext = class {
2247
- /** 命名类型集合:name RuntimeType */
2248
- namedTypes = /* @__PURE__ */ new Map();
2249
- /** 类型解析器(用于解析 ref 的实际类型) */
2250
- resolveType;
2251
- /** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
2252
- entryTypeName = "";
2253
- /** 入口类型导出名(exportName,自引用时用此名生成变量名) */
2254
- entryExportName = "";
2255
- /**
2256
- * 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
2257
- *
2258
- * true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
2259
- * 嵌套类型(array/object/tuple/union 等)的元素递归处理。
2260
- */
2261
- coerce = false;
2262
- constructor(resolveType) {
2263
- this.resolveType = resolveType;
2264
- }
2265
- };
2266
- function collectNamedTypes(type, ctx) {
2267
- switch (type.kind) {
2268
- case "string":
2269
- case "number":
2270
- case "boolean":
2271
- case "bigint":
2272
- case "null":
2273
- case "undefined":
2274
- case "any":
2275
- case "unknown":
2276
- case "literal":
2277
- case "date":
2278
- return;
2279
- case "array":
2280
- collectNamedTypes(type.element, ctx);
2281
- return;
2282
- case "tuple":
2283
- for (const el of type.elements) {
2284
- collectNamedTypes(el.type, ctx);
2285
- }
2286
- return;
2287
- case "object":
2288
- for (const prop of type.properties) {
2289
- collectNamedTypes(prop.type, ctx);
2290
- }
2291
- return;
2292
- case "union":
2293
- for (const member of type.members) {
2294
- collectNamedTypes(member, ctx);
2295
- }
2296
- return;
2297
- case "record":
2298
- collectNamedTypes(type.key, ctx);
2299
- collectNamedTypes(type.value, ctx);
2300
- return;
2301
- case "map":
2302
- collectNamedTypes(type.key, ctx);
2303
- collectNamedTypes(type.value, ctx);
2304
- return;
2305
- case "set":
2306
- collectNamedTypes(type.element, ctx);
2307
- return;
2308
- case "ref": {
2309
- if (ctx.namedTypes.has(type.name)) return;
2310
- ctx.namedTypes.set(type.name, { kind: "any" });
2311
- const resolved = ctx.resolveType(type.name);
2312
- if (resolved) {
2313
- ctx.namedTypes.set(type.name, resolved);
2314
- collectNamedTypes(resolved, ctx);
2395
+ // src/router/matchRoute.ts
2396
+ function matchRoute(routes, method, path11) {
2397
+ for (const route of routes) {
2398
+ if (route.method !== method) {
2399
+ continue;
2400
+ }
2401
+ if (!route.isDynamic) {
2402
+ if (route.urlPath === path11) {
2403
+ return { route, params: {} };
2315
2404
  }
2316
- return;
2405
+ continue;
2406
+ }
2407
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2408
+ if (params !== null) {
2409
+ return { route, params };
2317
2410
  }
2318
2411
  }
2412
+ return null;
2319
2413
  }
2320
- function runtimeTypeToZodExpression(type, ctx, constraints) {
2321
- const expr = baseExpression(type, ctx);
2322
- const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
2323
- if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
2324
- return wrapCoercePreprocess(type.kind, withConstraints);
2414
+ function matchWsRoute(wsRoutes, path11) {
2415
+ for (const route of wsRoutes) {
2416
+ if (!route.isDynamic) {
2417
+ if (route.urlPath === path11) {
2418
+ return { route, params: {} };
2419
+ }
2420
+ continue;
2421
+ }
2422
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2423
+ if (params !== null) {
2424
+ return { route, params };
2425
+ }
2325
2426
  }
2326
- return withConstraints;
2427
+ return null;
2327
2428
  }
2328
- function applyConstraints(baseExpr, constraints, typeKind) {
2329
- const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
2330
- return `${baseExpr}${suffix}`;
2429
+ function matchDynamicPath(pattern, path11, paramNames, isCatchAll) {
2430
+ const patternSegments = pattern.split("/").filter(Boolean);
2431
+ const pathSegments = path11.split("/").filter(Boolean);
2432
+ if (isCatchAll) {
2433
+ const nonCatchAllCount = patternSegments.length - 1;
2434
+ if (pathSegments.length <= nonCatchAllCount) {
2435
+ return null;
2436
+ }
2437
+ const params2 = {};
2438
+ for (let i = 0; i < nonCatchAllCount; i++) {
2439
+ const patternSeg = patternSegments[i];
2440
+ const pathSeg = pathSegments[i];
2441
+ if (patternSeg.startsWith(":")) {
2442
+ const paramName = patternSeg.slice(1);
2443
+ params2[paramName] = pathSeg;
2444
+ } else if (patternSeg !== pathSeg) {
2445
+ return null;
2446
+ }
2447
+ }
2448
+ const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
2449
+ const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
2450
+ params2[catchAllParamName] = catchAllValue;
2451
+ if (Object.keys(params2).length !== paramNames.length) {
2452
+ return null;
2453
+ }
2454
+ return params2;
2455
+ }
2456
+ if (patternSegments.length !== pathSegments.length) {
2457
+ return null;
2458
+ }
2459
+ const params = {};
2460
+ for (let i = 0; i < patternSegments.length; i++) {
2461
+ const patternSeg = patternSegments[i];
2462
+ const pathSeg = pathSegments[i];
2463
+ if (patternSeg.startsWith(":")) {
2464
+ const paramName = patternSeg.slice(1);
2465
+ params[paramName] = pathSeg;
2466
+ } else if (patternSeg !== pathSeg) {
2467
+ return null;
2468
+ }
2469
+ }
2470
+ if (Object.keys(params).length !== paramNames.length) {
2471
+ return null;
2472
+ }
2473
+ return params;
2331
2474
  }
2332
- function constraintToZodChain(constraint, _typeKind) {
2333
- switch (constraint.kind) {
2334
- case "max":
2335
- return `.max(${constraint.value})`;
2336
- case "min":
2337
- return `.min(${constraint.value})`;
2338
- case "int":
2339
- return ".int()";
2340
- case "positive":
2341
- return ".positive()";
2342
- case "negative":
2343
- return ".negative()";
2344
- case "nonnegative":
2345
- return ".nonnegative()";
2346
- case "nonpositive":
2347
- return ".nonpositive()";
2348
- case "maxLength":
2349
- return `.max(${constraint.value})`;
2350
- case "minLength":
2351
- return `.min(${constraint.value})`;
2352
- case "length":
2353
- return `.length(${constraint.value})`;
2354
- case "regex": {
2355
- const flags = constraint.flags ?? "";
2356
- return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
2475
+
2476
+ // src/loader/resolveExports.ts
2477
+ function resolveExport(module, exportName) {
2478
+ if (exportName in module && typeof module[exportName] !== "undefined") {
2479
+ return module[exportName];
2480
+ }
2481
+ const defaultExport = module.default;
2482
+ if (defaultExport !== null && typeof defaultExport === "object") {
2483
+ const value = defaultExport[exportName];
2484
+ if (value !== void 0) {
2485
+ return value;
2357
2486
  }
2358
- case "email":
2359
- return ".email()";
2360
- case "url":
2361
- return ".url()";
2362
- case "uuid":
2363
- return ".uuid()";
2364
2487
  }
2488
+ return void 0;
2365
2489
  }
2366
- function baseExpression(type, ctx) {
2367
- switch (type.kind) {
2368
- case "string":
2369
- return "z.string()";
2370
- case "number":
2371
- return "z.number()";
2372
- case "boolean":
2373
- return "z.boolean()";
2374
- case "bigint":
2375
- return "z.never()";
2376
- case "null":
2377
- return "z.null()";
2378
- case "undefined":
2379
- return "z.undefined()";
2380
- case "any":
2381
- case "unknown":
2382
- return "z.unknown()";
2383
- case "literal":
2384
- return `z.literal(${JSON.stringify(type.value)})`;
2385
- case "array":
2386
- return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
2387
- case "tuple":
2388
- return generateTupleExpression(type.elements, ctx);
2389
- case "object":
2390
- return generateObjectExpression(type.properties, ctx);
2391
- case "union":
2392
- return generateUnionExpression(type.members, ctx);
2393
- case "date":
2394
- return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
2395
- case "record":
2396
- return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
2397
- case "map":
2398
- return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
2399
- case "set":
2400
- return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
2401
- case "ref":
2402
- if (type.name === ctx.entryTypeName) {
2403
- return `${ctx.entryExportName}Schema`;
2404
- }
2405
- return `${type.name}Schema`;
2490
+
2491
+ // src/loader/validateRouteModule.ts
2492
+ function validateRouteModule(value, method, filePath) {
2493
+ if (typeof value !== "function") {
2494
+ throw new Error(
2495
+ `Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
2496
+ );
2406
2497
  }
2407
2498
  }
2408
- var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2409
- var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
2410
- var COERCE_MAP_HELPER = 'export const coerceMap = (v) => Array.isArray(v) ? new Map(v) : v instanceof Map ? v : (v && typeof v === "object" ? new Map(Object.entries(v)) : v);';
2411
- var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2412
- var HELPERS_FILENAME = "faapi-helpers.js";
2413
- function generateHelpersFileSource() {
2414
- return [
2415
- "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
2416
- COERCE_NUMBER_HELPER,
2417
- COERCE_BOOLEAN_HELPER,
2418
- COERCE_MAP_HELPER,
2419
- COERCE_SET_HELPER,
2420
- ""
2421
- ].join("\n");
2499
+
2500
+ // src/loader/loadRouteModule.ts
2501
+ async function loadRouteModule(filePath, method) {
2502
+ let module;
2503
+ try {
2504
+ module = await importWithCacheBust(filePath);
2505
+ } catch (err) {
2506
+ const reason = err instanceof Error ? err.message : String(err);
2507
+ throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
2508
+ }
2509
+ const handler = resolveExport(module, method);
2510
+ validateRouteModule(handler, method, filePath);
2511
+ return { handler, method };
2422
2512
  }
2423
- function usesCoerceHelpers(code) {
2424
- return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
2513
+
2514
+ // src/utils/parseJsonBody.ts
2515
+ function parseJsonBody(text) {
2516
+ try {
2517
+ const data = JSON.parse(text);
2518
+ return { success: true, data };
2519
+ } catch {
2520
+ return { success: false, error: "Invalid JSON body" };
2521
+ }
2425
2522
  }
2426
- function wrapCoercePreprocess(kind, inner) {
2427
- if (kind === "number") {
2428
- return `z.preprocess(coerceNumber, ${inner})`;
2523
+
2524
+ // src/utils/parseMultipart.ts
2525
+ async function parseMultipart(request) {
2526
+ const formData = await request.formData();
2527
+ const fields = {};
2528
+ const files = [];
2529
+ for (const [key, value] of formData.entries()) {
2530
+ if (value instanceof File) {
2531
+ files.push({
2532
+ name: key,
2533
+ filename: value.name,
2534
+ type: value.type,
2535
+ size: value.size,
2536
+ arrayBuffer: () => value.arrayBuffer()
2537
+ });
2538
+ } else {
2539
+ if (key in fields) {
2540
+ const existing = fields[key];
2541
+ if (Array.isArray(existing)) {
2542
+ existing.push(value);
2543
+ } else {
2544
+ fields[key] = [existing, value];
2545
+ }
2546
+ } else {
2547
+ fields[key] = value;
2548
+ }
2549
+ }
2550
+ }
2551
+ return { fields, files };
2552
+ }
2553
+
2554
+ // src/runtime/resolveInput.ts
2555
+ async function resolveInput(method, request) {
2556
+ const inputType = getInputTypeForMethod(method);
2557
+ if (inputType === "body") {
2558
+ const contentType = request.headers.get("content-type") ?? "";
2559
+ if (contentType.includes("multipart/form-data")) {
2560
+ return parseMultipart(request);
2561
+ }
2562
+ if (contentType.includes("application/x-www-form-urlencoded")) {
2563
+ const text2 = await request.text();
2564
+ if (text2.trim() === "") return null;
2565
+ const params = new URLSearchParams(text2);
2566
+ const obj = {};
2567
+ for (const [key, value] of params) {
2568
+ obj[key] = value;
2569
+ }
2570
+ return obj;
2571
+ }
2572
+ const text = await request.text();
2573
+ if (text.trim() === "") {
2574
+ return null;
2575
+ }
2576
+ const result = parseJsonBody(text);
2577
+ if (!result.success) {
2578
+ throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
2579
+ {
2580
+ path: "body",
2581
+ code: "INVALID_FORMAT",
2582
+ expected: "JSON",
2583
+ received: "text",
2584
+ message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
2585
+ }
2586
+ ]);
2587
+ }
2588
+ return result.data;
2429
2589
  }
2430
- return `z.preprocess(coerceBoolean, ${inner})`;
2590
+ const url = new URL(request.url);
2591
+ return queryToObject(url.searchParams);
2431
2592
  }
2432
- function generateTupleExpression(elements, ctx) {
2433
- const fixedExprs = [];
2434
- const fixedOptional = [];
2435
- let restExpression = "";
2436
- let restStarted = false;
2437
- for (const el of elements) {
2438
- if (el.rest) {
2439
- restExpression = runtimeTypeToZodExpression(el.type, ctx);
2440
- restStarted = true;
2441
- } else if (!restStarted) {
2442
- fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
2443
- fixedOptional.push(el.optional);
2593
+
2594
+ // src/response/sendNodeResponse.ts
2595
+ import { Readable } from "stream";
2596
+ async function sendNodeResponse(response, res) {
2597
+ res.statusCode = response.status;
2598
+ for (const [key, value] of response.headers) {
2599
+ if (key.toLowerCase() === "set-cookie") {
2600
+ res.appendHeader(key, value);
2601
+ } else {
2602
+ res.setHeader(key, value);
2444
2603
  }
2445
2604
  }
2446
- if (restExpression) {
2447
- return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
2605
+ if (response.body) {
2606
+ const nodeStream = Readable.fromWeb(response.body);
2607
+ await new Promise((resolve, reject) => {
2608
+ nodeStream.on("error", reject);
2609
+ res.on("error", reject);
2610
+ res.on("finish", resolve);
2611
+ nodeStream.pipe(res);
2612
+ });
2613
+ return;
2448
2614
  }
2449
- const hasOptional = fixedOptional.some((o) => o);
2450
- if (!hasOptional) {
2451
- return `z.tuple([${fixedExprs.join(", ")}])`;
2615
+ res.end();
2616
+ }
2617
+
2618
+ // src/utils/getClientIp.ts
2619
+ function getClientIp(req) {
2620
+ const xff = req.headers["x-forwarded-for"];
2621
+ if (typeof xff === "string" && xff.length > 0) {
2622
+ const first = xff.split(",")[0]?.trim();
2623
+ if (first) return first;
2452
2624
  }
2453
- const variants = [];
2454
- for (let len = fixedExprs.length; len >= 0; len--) {
2455
- const removed = fixedOptional.slice(len);
2456
- if (removed.length > 0 && removed.some((o) => !o)) {
2457
- break;
2625
+ const remote = req.socket?.remoteAddress;
2626
+ if (remote) {
2627
+ if (remote.startsWith("::ffff:")) {
2628
+ return remote.slice(7);
2458
2629
  }
2459
- const subset = fixedExprs.slice(0, len);
2460
- variants.push(`z.tuple([${subset.join(", ")}])`);
2461
- }
2462
- variants.reverse();
2463
- if (variants.length === 1) {
2464
- return variants[0];
2630
+ return remote;
2465
2631
  }
2466
- return `z.union([${variants.join(", ")}])`;
2467
- }
2468
- function generateObjectExpression(properties, ctx) {
2469
- const fields = properties.map((prop) => {
2470
- const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
2471
- const finalExpr = prop.optional ? `${expr}.optional()` : expr;
2472
- return `${JSON.stringify(prop.name)}: ${finalExpr}`;
2473
- });
2474
- return `z.object({ ${fields.join(", ")} })`;
2632
+ return "";
2475
2633
  }
2476
- function generateUnionExpression(members, ctx) {
2477
- const hasNull = members.some((m) => m.kind === "null");
2478
- const nonNull = members.filter((m) => m.kind !== "null");
2479
- if (hasNull && nonNull.length === 1) {
2480
- return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
2634
+
2635
+ // src/server/handleWsUpgrade.ts
2636
+ import { WebSocketServer, WebSocket } from "ws";
2637
+ import path6 from "path";
2638
+
2639
+ // src/errors/formatErrorResponse.ts
2640
+ function formatErrorResponse(error) {
2641
+ if (error instanceof ValidationError) {
2642
+ const body2 = {
2643
+ code: error.code,
2644
+ message: error.message,
2645
+ issues: error.issues
2646
+ };
2647
+ return new Response(JSON.stringify({ error: body2 }), {
2648
+ status: error.statusCode,
2649
+ headers: { "Content-Type": "application/json" }
2650
+ });
2481
2651
  }
2482
- if (hasNull) {
2483
- const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2484
- return `z.union([${unionInner2}]).nullable()`;
2652
+ if (error instanceof MethodNotAllowedError) {
2653
+ const body2 = {
2654
+ code: error.code,
2655
+ message: error.message
2656
+ };
2657
+ return new Response(JSON.stringify({ error: body2 }), {
2658
+ status: error.statusCode,
2659
+ headers: {
2660
+ "Content-Type": "application/json",
2661
+ Allow: error.allowedMethods.join(", ")
2662
+ }
2663
+ });
2485
2664
  }
2486
- const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2487
- return `z.union([${unionInner}])`;
2488
- }
2489
- function generateNamedTypeDeclaration(name, type, ctx) {
2490
- const expr = runtimeTypeToZodExpression(type, ctx);
2491
- const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
2492
- if (hasRef) {
2493
- return `const ${name}Schema = z.lazy(() => ${expr});`;
2665
+ if (error instanceof FaapiError) {
2666
+ const body2 = {
2667
+ code: error.code,
2668
+ message: error.message
2669
+ };
2670
+ return new Response(JSON.stringify({ error: body2 }), {
2671
+ status: error.statusCode,
2672
+ headers: { "Content-Type": "application/json" }
2673
+ });
2494
2674
  }
2495
- return `const ${name}Schema = ${expr};`;
2675
+ const body = {
2676
+ code: "INTERNAL_ERROR",
2677
+ message: error instanceof Error ? error.message : "An unknown error occurred"
2678
+ };
2679
+ return new Response(JSON.stringify({ error: body }), {
2680
+ status: 500,
2681
+ headers: { "Content-Type": "application/json" }
2682
+ });
2496
2683
  }
2497
- function containsRef(type, visited) {
2498
- switch (type.kind) {
2499
- case "ref":
2500
- return visited.has(type.name);
2501
- case "array":
2502
- return containsRef(type.element, visited);
2503
- case "tuple":
2504
- return type.elements.some((el) => containsRef(el.type, visited));
2505
- case "object":
2506
- return type.properties.some((prop) => containsRef(prop.type, visited));
2507
- case "union":
2508
- return type.members.some((m) => containsRef(m, visited));
2509
- case "record":
2510
- return containsRef(type.key, visited) || containsRef(type.value, visited);
2511
- case "map":
2512
- return containsRef(type.key, visited) || containsRef(type.value, visited);
2513
- case "set":
2514
- return containsRef(type.element, visited);
2515
- default:
2516
- return false;
2684
+
2685
+ // src/server/serverUtils.ts
2686
+ function nodeHttpToWebHeaders(req) {
2687
+ const headers = new Headers();
2688
+ for (const [key, value] of Object.entries(req.headers)) {
2689
+ if (value === void 0) continue;
2690
+ if (Array.isArray(value)) {
2691
+ for (const v of value) headers.append(key, v);
2692
+ } else {
2693
+ headers.set(key, value);
2694
+ }
2517
2695
  }
2696
+ return headers;
2518
2697
  }
2519
- function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
2520
- const ctx = new CodeGenContext(resolveType);
2521
- const name = exportName ?? typeInfo.name;
2522
- ctx.entryTypeName = typeInfo.name;
2523
- ctx.entryExportName = name;
2524
- ctx.coerce = coerce;
2525
- collectNamedTypes(typeInfo.runtimeType, ctx);
2526
- ctx.namedTypes.delete(typeInfo.name);
2527
- const lines = [];
2528
- lines.push("import { z } from 'zod';");
2529
- lines.push("");
2530
- for (const [n, type] of ctx.namedTypes) {
2531
- lines.push(generateNamedTypeDeclaration(n, type, ctx));
2532
- }
2533
- if (ctx.namedTypes.size > 0) lines.push("");
2534
- const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
2535
- const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
2536
- if (hasSelfRef) {
2537
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
2538
- } else {
2539
- lines.push(`export const ${name}Schema = ${entryExpr};`);
2698
+ function buildErrorResponse(err) {
2699
+ try {
2700
+ return formatErrorResponse(err);
2701
+ } catch {
2702
+ return new Response(
2703
+ JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
2704
+ {
2705
+ status: 500,
2706
+ headers: { "Content-Type": "application/json" }
2707
+ }
2708
+ );
2540
2709
  }
2541
- return lines.join("\n");
2542
2710
  }
2543
2711
 
2544
- // src/cli/generateSchemaFiles.ts
2545
- function getSchemaOutputPath(sourceFile, dist, rootDir) {
2546
- let rel = sourceFile.replace(/\\/g, "/");
2547
- if (rel.startsWith("src/")) {
2548
- rel = rel.slice(4);
2549
- }
2550
- const idx = rel.lastIndexOf("/");
2551
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2552
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2712
+ // src/runtime/wsHandler.ts
2713
+ function wrapWsSocket(rawSocket) {
2714
+ return {
2715
+ send(data) {
2716
+ const payload = typeof data === "string" || Buffer.isBuffer(data) ? data : JSON.stringify(data);
2717
+ rawSocket.send(payload);
2718
+ },
2719
+ close(code, reason) {
2720
+ rawSocket.close(code, reason);
2721
+ },
2722
+ get readyState() {
2723
+ return rawSocket.readyState;
2724
+ }
2725
+ };
2553
2726
  }
2554
- function getRuntimeSchemaPath(filePath, dist, rootDir) {
2555
- let rel = filePath.replace(/\\/g, "/");
2556
- if (rel.startsWith("src/")) {
2557
- rel = rel.slice(4);
2558
- } else if (rel.startsWith(`${dist}/`)) {
2559
- rel = rel.slice(dist.length + 1);
2560
- }
2561
- const idx = rel.lastIndexOf("/");
2562
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2563
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2727
+
2728
+ // src/server/handleWsUpgrade.ts
2729
+ function getPathname(req) {
2730
+ const url = req.url ?? "/";
2731
+ const idx = url.indexOf("?");
2732
+ return idx >= 0 ? url.slice(0, idx) : url;
2564
2733
  }
2565
- function getHelpersImportPath(relDir) {
2566
- if (!relDir) return `./${HELPERS_FILENAME}`;
2567
- const depth = relDir.split("/").filter(Boolean).length;
2568
- return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2734
+ async function loadWsHandler(filePath, ctx) {
2735
+ const module = await importWithCacheBust(filePath);
2736
+ const handler = module["WS"];
2737
+ if (typeof handler !== "function") {
2738
+ throw new Error(`WS export not found in ${filePath}`);
2739
+ }
2740
+ return handler(ctx);
2569
2741
  }
2570
- function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2571
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
2572
- const lines = ["import { z } from 'zod';"];
2573
- const schemaBlocks = [];
2574
- for (const source of sources) {
2575
- const { schemaName, typeInfo } = source;
2576
- if (!typeInfo) {
2577
- continue;
2742
+ function bindEvents(rawSocket, handlers) {
2743
+ if (!handlers) return;
2744
+ const ws = wrapWsSocket(rawSocket);
2745
+ if (handlers.onOpen) {
2746
+ if (rawSocket.readyState === WebSocket.OPEN) {
2747
+ handlers.onOpen(ws);
2748
+ } else {
2749
+ rawSocket.once("open", () => handlers.onOpen(ws));
2578
2750
  }
2579
- const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2580
- const block = [`// ${schemaName}`];
2581
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2582
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
2583
- ""
2584
- );
2585
- block.push(schemaCode);
2586
- block.push("");
2587
- schemaBlocks.push(block.join("\n"));
2588
2751
  }
2589
- const allSchemaCode = schemaBlocks.join("\n");
2590
- if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2591
- lines.push(
2592
- `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2593
- );
2752
+ if (handlers.onMessage) {
2753
+ rawSocket.on("message", (data) => {
2754
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
2755
+ handlers.onMessage(ws, buf.toString("utf8"));
2756
+ });
2594
2757
  }
2595
- lines.push("");
2596
- lines.push(...schemaBlocks);
2597
- return lines.join("\n").replace(/\n+$/, "\n");
2598
- }
2599
- async function generateSchemaFiles(routes, rootDir, dist) {
2600
- if (routes.length === 0) return;
2601
- const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2602
- const sourcesByFile = /* @__PURE__ */ new Map();
2603
- for (const source of sources) {
2604
- let list = sourcesByFile.get(source.filePath);
2605
- if (!list) {
2606
- list = [];
2607
- sourcesByFile.set(source.filePath, list);
2608
- }
2609
- list.push(source);
2758
+ if (handlers.onClose) {
2759
+ rawSocket.on("close", (code, reason) => {
2760
+ handlers.onClose(ws, code, reason.toString("utf8"));
2761
+ });
2610
2762
  }
2611
- const fileEntries = [];
2612
- for (const [filePath, fileSources] of sourcesByFile) {
2613
- const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
2614
- const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2615
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2616
- let relForDir = relFile;
2617
- if (relForDir.startsWith("src/")) {
2618
- relForDir = relForDir.slice(4);
2763
+ if (handlers.onError) {
2764
+ rawSocket.on("error", (err) => {
2765
+ handlers.onError(ws, err);
2766
+ });
2767
+ }
2768
+ }
2769
+ async function sendResponseToSocket(socket, response) {
2770
+ const body = await response.text().catch(() => "");
2771
+ const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
2772
+ `;
2773
+ const headerLines = [];
2774
+ let hasContentLength = false;
2775
+ for (const [key, value] of response.headers) {
2776
+ if (key.toLowerCase() === "content-length") {
2777
+ hasContentLength = true;
2619
2778
  }
2620
- const dirIdx = relForDir.lastIndexOf("/");
2621
- const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2622
- const helpersImportPath = getHelpersImportPath(zodRelDir);
2623
- const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
2624
- fileEntries.push({ outputPath, source });
2779
+ headerLines.push(`${key}: ${value}`);
2625
2780
  }
2626
- const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2627
- if (usesCoerceHelpers(allSourceCode)) {
2628
- const helpersPath = path5.resolve(rootDir, dist, HELPERS_FILENAME);
2629
- await writeSchemaFile(helpersPath, generateHelpersFileSource());
2781
+ if (!hasContentLength) {
2782
+ headerLines.push(`Content-Length: ${Buffer.byteLength(body)}`);
2630
2783
  }
2631
- await Promise.all(
2632
- fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2633
- );
2784
+ socket.write(statusLine + headerLines.join("\r\n") + "\r\n\r\n" + body);
2785
+ socket.destroy();
2634
2786
  }
2635
- async function writeSchemaFile(outputPath, source) {
2636
- await fs3.mkdir(path5.dirname(outputPath), { recursive: true });
2637
- await fs3.writeFile(outputPath, source, "utf-8");
2787
+ function attachWebSocket(options) {
2788
+ const { server, routesRef, rootDir, config, globalMiddlewares } = options;
2789
+ const wss = new WebSocketServer({ noServer: true });
2790
+ server.on("upgrade", async (req, socket, head) => {
2791
+ const currentWsRoutes = routesRef.wsCurrent;
2792
+ const pathname = getPathname(req);
2793
+ const match = matchWsRoute(currentWsRoutes, pathname);
2794
+ if (!match) {
2795
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
2796
+ socket.destroy();
2797
+ return;
2798
+ }
2799
+ const { route, params } = match;
2800
+ const headers = nodeHttpToWebHeaders(req);
2801
+ const host = req.headers.host ?? "localhost";
2802
+ const url = `http://${host}${req.url ?? "/"}`;
2803
+ const request = new Request(url, { method: "GET", headers });
2804
+ const ctx = createContext(request, params, config, getClientIp(req));
2805
+ const meta = ctx.meta;
2806
+ let upgraded = false;
2807
+ const finalHandler = async () => {
2808
+ let handlers;
2809
+ try {
2810
+ const absoluteFilePath = path6.resolve(rootDir, route.filePath);
2811
+ handlers = await loadWsHandler(absoluteFilePath, ctx);
2812
+ } catch (err) {
2813
+ const reason = err instanceof Error ? err.message : String(err);
2814
+ console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
2815
+ return new Response("Internal Server Error", { status: 500 });
2816
+ }
2817
+ await new Promise((resolve, reject) => {
2818
+ wss.handleUpgrade(req, socket, head, (rawSocket) => {
2819
+ try {
2820
+ bindEvents(rawSocket, handlers);
2821
+ wss.emit("connection", rawSocket, req);
2822
+ upgraded = true;
2823
+ resolve();
2824
+ } catch (err) {
2825
+ reject(err);
2826
+ }
2827
+ });
2828
+ });
2829
+ return new Response(null, { status: 200 });
2830
+ };
2831
+ let response;
2832
+ try {
2833
+ const dirMiddlewares = route.middlewares ?? [];
2834
+ const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
2835
+ if (allMiddlewares.length > 0) {
2836
+ response = await compose(allMiddlewares, ctx, finalHandler);
2837
+ } else {
2838
+ response = await finalHandler();
2839
+ }
2840
+ } catch (err) {
2841
+ if (upgraded) {
2842
+ console.error("[faapi] WS \u63E1\u624B\u540E\u4E2D\u95F4\u4EF6\u629B\u9519:", err);
2843
+ return;
2844
+ }
2845
+ response = buildErrorResponse(err);
2846
+ }
2847
+ if (upgraded) {
2848
+ return;
2849
+ }
2850
+ await sendResponseToSocket(socket, mergeMeta(response, meta));
2851
+ });
2852
+ return wss;
2638
2853
  }
2639
2854
 
2640
2855
  // src/server/createServer.ts
@@ -2682,15 +2897,15 @@ function limitStreamSize(stream, maxSize) {
2682
2897
  }
2683
2898
  });
2684
2899
  }
2685
- function findAllowedMethods(routes, path10) {
2900
+ function findAllowedMethods(routes, path11) {
2686
2901
  const methods = /* @__PURE__ */ new Set();
2687
2902
  for (const route of routes) {
2688
- if (route.urlPath === path10) {
2903
+ if (route.urlPath === path11) {
2689
2904
  methods.add(route.method);
2690
2905
  continue;
2691
2906
  }
2692
2907
  if (route.isDynamic) {
2693
- const params = matchDynamicPath(route.urlPath, path10, route.paramNames, route.isCatchAll);
2908
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2694
2909
  if (params !== null) {
2695
2910
  methods.add(route.method);
2696
2911
  }
@@ -2776,7 +2991,7 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
2776
2991
  }
2777
2992
  ctx.params = match.params;
2778
2993
  const { route } = match;
2779
- const absoluteFilePath = path6.resolve(rootDir, route.filePath);
2994
+ const absoluteFilePath = path7.resolve(rootDir, route.filePath);
2780
2995
  const routeModule = await loadRouteModule(absoluteFilePath, route.method);
2781
2996
  const input = await resolveInput(route.method, request);
2782
2997
  const inputType = getInputTypeForMethod(route.method);
@@ -2806,19 +3021,242 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
2806
3021
  if (outerMiddlewares.length > 0) {
2807
3022
  response = await compose(outerMiddlewares, ctx, routePipeline);
2808
3023
  } else {
2809
- response = await routePipeline();
3024
+ response = await routePipeline();
3025
+ }
3026
+ await sendNodeResponse(response, res);
3027
+ } catch (err) {
3028
+ const errorResponse = buildErrorResponse(err);
3029
+ await sendNodeResponse(mergeMeta(errorResponse, meta), res);
3030
+ if (onError) {
3031
+ try {
3032
+ await onError(err, ctx);
3033
+ } catch {
3034
+ }
3035
+ }
3036
+ }
3037
+ }
3038
+
3039
+ // src/testServer.ts
3040
+ var DEFAULT_PATTERNS = ["src/api/**/*.ts"];
3041
+ var DEFAULT_BODY_LIMIT2 = 10 * 1024 * 1024;
3042
+ async function createTestServer(options) {
3043
+ const {
3044
+ rootDir,
3045
+ patterns = DEFAULT_PATTERNS,
3046
+ dist,
3047
+ cors: cors2 = false,
3048
+ helmet: helmet2 = false,
3049
+ logger: logger2 = false,
3050
+ middlewares,
3051
+ injectors,
3052
+ onError,
3053
+ config,
3054
+ bodyLimit = DEFAULT_BODY_LIMIT2
3055
+ } = options;
3056
+ const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3057
+ const sorted = sortRoutes(routes);
3058
+ const schemaDist = dist ? path8.isAbsolute(dist) ? dist : path8.resolve(rootDir, dist) : await fs5.mkdtemp(path8.join(os.tmpdir(), "faapi-test-schema-"));
3059
+ await generateSchemaFiles(sorted, rootDir, schemaDist);
3060
+ const { server } = createServer({
3061
+ routes: sorted,
3062
+ rootDir,
3063
+ dist: schemaDist,
3064
+ cors: cors2,
3065
+ helmet: helmet2,
3066
+ logger: logger2,
3067
+ middlewares,
3068
+ injectors,
3069
+ onError,
3070
+ config,
3071
+ wsRoutes,
3072
+ bodyLimit
3073
+ });
3074
+ const baseUrl = await listenOnRandomPort(server);
3075
+ let closed = false;
3076
+ const testServer = {
3077
+ server,
3078
+ baseUrl,
3079
+ routes: sorted,
3080
+ wsRoutes,
3081
+ schemaDist,
3082
+ async close() {
3083
+ if (closed) return;
3084
+ closed = true;
3085
+ const s = server;
3086
+ s.closeAllConnections?.();
3087
+ s.closeIdleConnections?.();
3088
+ await new Promise((resolve) => {
3089
+ server.close(() => resolve());
3090
+ });
3091
+ await fs5.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3092
+ });
3093
+ invalidateSchemaCache();
3094
+ }
3095
+ };
3096
+ return testServer;
3097
+ }
3098
+ function listenOnRandomPort(server) {
3099
+ return new Promise((resolve, reject) => {
3100
+ server.listen(0, () => {
3101
+ const addr = server.address();
3102
+ if (typeof addr === "object" && addr !== null) {
3103
+ resolve(`http://localhost:${addr.port}`);
3104
+ } else {
3105
+ reject(new Error("Failed to get server address"));
3106
+ }
3107
+ });
3108
+ server.on("error", (err) => {
3109
+ reject(err);
3110
+ });
3111
+ });
3112
+ }
3113
+
3114
+ // src/wsTestClient.ts
3115
+ import { WebSocket as WebSocket2 } from "ws";
3116
+ var MessageQueue = class {
3117
+ queue = [];
3118
+ waiters = [];
3119
+ listener;
3120
+ constructor(ws) {
3121
+ this.listener = (data) => {
3122
+ const msg = normalizeRawData(data);
3123
+ const waiter = this.waiters.shift();
3124
+ if (waiter) {
3125
+ waiter(msg);
3126
+ } else {
3127
+ this.queue.push(msg);
3128
+ }
3129
+ };
3130
+ ws.on("message", this.listener);
3131
+ }
3132
+ /**
3133
+ * 取下一条消息
3134
+ *
3135
+ * 队列有则立即 resolve,无则注册 waiter 等待下一条 'message' 事件。
3136
+ * 超时未到 → reject('WebSocket message timeout'),waiter 被清理。
3137
+ *
3138
+ * @param timeout 超时毫秒,默认 2000
3139
+ */
3140
+ next(timeout = 2e3) {
3141
+ return new Promise((resolve, reject) => {
3142
+ const wrapped = (msg2) => {
3143
+ clearTimeout(timer);
3144
+ resolve(msg2);
3145
+ };
3146
+ const timer = setTimeout(() => {
3147
+ const idx = this.waiters.indexOf(wrapped);
3148
+ if (idx >= 0) this.waiters.splice(idx, 1);
3149
+ reject(new Error("WebSocket message timeout"));
3150
+ }, timeout);
3151
+ const msg = this.queue.shift();
3152
+ if (msg !== void 0) {
3153
+ wrapped(msg);
3154
+ } else {
3155
+ this.waiters.push(wrapped);
3156
+ }
3157
+ });
3158
+ }
3159
+ };
3160
+ function normalizeRawData(data) {
3161
+ if (Buffer.isBuffer(data)) {
3162
+ return data.toString("utf8");
3163
+ }
3164
+ if (Array.isArray(data)) {
3165
+ return Buffer.concat(data).toString("utf8");
3166
+ }
3167
+ return Buffer.from(data).toString("utf8");
3168
+ }
3169
+ function waitForWsOpen(ws, timeout = 2e3) {
3170
+ return new Promise((resolve, reject) => {
3171
+ const timer = setTimeout(() => {
3172
+ reject(new Error("WebSocket open timeout"));
3173
+ }, timeout);
3174
+ const cleanup = () => {
3175
+ clearTimeout(timer);
3176
+ ws.removeListener("open", onOpen);
3177
+ ws.removeListener("error", onError);
3178
+ ws.removeListener("close", onClose);
3179
+ };
3180
+ const onOpen = () => {
3181
+ cleanup();
3182
+ resolve();
3183
+ };
3184
+ const onError = (err) => {
3185
+ cleanup();
3186
+ reject(err);
3187
+ };
3188
+ const onClose = () => {
3189
+ cleanup();
3190
+ reject(new Error("WebSocket closed before open"));
3191
+ };
3192
+ ws.once("open", onOpen);
3193
+ ws.once("error", onError);
3194
+ ws.once("close", onClose);
3195
+ });
3196
+ }
3197
+ async function connectWs(baseUrl, pathname, options = {}) {
3198
+ const { timeout = 2e3, headers, protocols } = options;
3199
+ const wsBaseUrl = baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
3200
+ const url = `${wsBaseUrl}${pathname}`;
3201
+ const ws = new WebSocket2(url, protocols, headers ? { headers } : void 0);
3202
+ const queue = new MessageQueue(ws);
3203
+ try {
3204
+ await waitForWsOpen(ws, timeout);
3205
+ } catch (err) {
3206
+ if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3207
+ ws.close();
3208
+ }
3209
+ throw err;
3210
+ }
3211
+ let closed = false;
3212
+ return {
3213
+ ws,
3214
+ queue,
3215
+ async close() {
3216
+ if (closed) return;
3217
+ closed = true;
3218
+ if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3219
+ ws.close();
3220
+ }
3221
+ await new Promise((resolve) => {
3222
+ const timer = setTimeout(resolve, 1e3);
3223
+ ws.once("close", () => {
3224
+ clearTimeout(timer);
3225
+ resolve();
3226
+ });
3227
+ });
3228
+ }
3229
+ };
3230
+ }
3231
+
3232
+ // src/cli/createAppCore.ts
3233
+ import fs7 from "fs";
3234
+ import path10 from "path";
3235
+ import { PassThrough } from "stream";
3236
+
3237
+ // src/router/detectRouteConflicts.ts
3238
+ function detectRouteConflicts(routes) {
3239
+ const map = /* @__PURE__ */ new Map();
3240
+ for (const route of routes) {
3241
+ const key = `${route.method} ${route.urlPath}`;
3242
+ const existing = map.get(key);
3243
+ if (existing) {
3244
+ existing.files.push(route.filePath);
3245
+ } else {
3246
+ map.set(key, {
3247
+ method: route.method,
3248
+ urlPath: route.urlPath,
3249
+ files: [route.filePath]
3250
+ });
2810
3251
  }
2811
- await sendNodeResponse(response, res);
2812
- } catch (err) {
2813
- const errorResponse = buildErrorResponse(err);
2814
- await sendNodeResponse(mergeMeta(errorResponse, meta), res);
2815
- if (onError) {
2816
- try {
2817
- await onError(err, ctx);
2818
- } catch {
2819
- }
3252
+ }
3253
+ const conflicts = [];
3254
+ for (const conflict of map.values()) {
3255
+ if (conflict.files.length > 1) {
3256
+ conflicts.push(conflict);
2820
3257
  }
2821
3258
  }
3259
+ return conflicts;
2822
3260
  }
2823
3261
 
2824
3262
  // src/server/startServer.ts
@@ -2850,55 +3288,8 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
2850
3288
  }
2851
3289
 
2852
3290
  // src/cli/generateRoutes.ts
2853
- import fs4 from "fs";
2854
- import path7 from "path";
2855
-
2856
- // src/middleware/loadMiddlewares.ts
2857
- var middlewareCache = /* @__PURE__ */ new Map();
2858
- function invalidateMiddlewareCache() {
2859
- middlewareCache.clear();
2860
- }
2861
- function getCachedMiddlewares(absPath) {
2862
- return middlewareCache.get(absPath);
2863
- }
2864
- function setCachedMiddlewares(absPath, bundle) {
2865
- middlewareCache.set(absPath, bundle);
2866
- }
2867
- async function loadMiddlewaresFile(filePath) {
2868
- try {
2869
- const module = await importWithCacheBust(filePath);
2870
- const middlewares = module.default ?? module.middlewares ?? [];
2871
- if (!Array.isArray(middlewares)) {
2872
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2873
- return { middlewares: [], injectors: {} };
2874
- }
2875
- const validMiddlewares = middlewares.filter((m) => {
2876
- if (typeof m !== "function") {
2877
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
2878
- return false;
2879
- }
2880
- return true;
2881
- });
2882
- const injectors = module.injectors ?? {};
2883
- if (typeof injectors !== "object" || injectors === null) {
2884
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2885
- return { middlewares: validMiddlewares, injectors: {} };
2886
- }
2887
- const validInjectors = {};
2888
- for (const [name, injector] of Object.entries(injectors)) {
2889
- if (typeof injector !== "function") {
2890
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
2891
- continue;
2892
- }
2893
- validInjectors[name] = injector;
2894
- }
2895
- return { middlewares: validMiddlewares, injectors: validInjectors };
2896
- } catch {
2897
- return { middlewares: [], injectors: {} };
2898
- }
2899
- }
2900
-
2901
- // src/cli/generateRoutes.ts
3291
+ import fs6 from "fs";
3292
+ import path9 from "path";
2902
3293
  async function hydrateRoutes(manifest) {
2903
3294
  const hydrateRoute = async (serialized) => {
2904
3295
  const bundle = await loadMiddlewarePaths(serialized.middlewarePaths);
@@ -3029,8 +3420,8 @@ function isFaapiConfigKey(key) {
3029
3420
  async function createAppBase(options) {
3030
3421
  const rootDir = options?.rootDir ?? process.cwd();
3031
3422
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
3032
- const routesPath = path8.resolve(rootDir, dist, ROUTES_FILE);
3033
- if (!fs5.existsSync(routesPath)) {
3423
+ const routesPath = path10.resolve(rootDir, dist, ROUTES_FILE);
3424
+ if (!fs7.existsSync(routesPath)) {
3034
3425
  throw new Error(
3035
3426
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
3036
3427
  );
@@ -3231,201 +3622,6 @@ async function createAppBase(options) {
3231
3622
  return { app, ctx };
3232
3623
  }
3233
3624
 
3234
- // src/router/scanRoutes.ts
3235
- import fg from "fast-glob";
3236
- import path9 from "path";
3237
- import fs6 from "fs";
3238
-
3239
- // src/router/constants.ts
3240
- var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
3241
- var HTTP_METHOD_SET = new Set(HTTP_METHODS);
3242
- function isHttpMethod(value) {
3243
- return HTTP_METHOD_SET.has(value);
3244
- }
3245
-
3246
- // src/utils/normalizePath.ts
3247
- function normalizePath(path10) {
3248
- if (!path10) return "";
3249
- let result = path10.replace(/\\/g, "/");
3250
- result = result.replace(/\/+/g, "/");
3251
- result = result.replace(/\/+$/, "");
3252
- if (result && !result.startsWith("/")) {
3253
- result = "/" + result;
3254
- }
3255
- return result;
3256
- }
3257
-
3258
- // src/router/parseRouteFile.ts
3259
- function dynamicSegmentToParam(segment) {
3260
- const match = segment.match(/^\[(.+)\]$/);
3261
- if (match) {
3262
- return ":" + match[1];
3263
- }
3264
- return segment;
3265
- }
3266
- function extractParamNames(urlPath) {
3267
- const params = [];
3268
- const segments = urlPath.split("/");
3269
- for (const segment of segments) {
3270
- if (segment.startsWith(":...")) {
3271
- params.push(segment.slice(4));
3272
- } else if (segment.startsWith(":")) {
3273
- params.push(segment.slice(1));
3274
- }
3275
- }
3276
- return params;
3277
- }
3278
- function isCatchAllSegment(segment) {
3279
- return /^\[\.\.\..+\]$/.test(segment);
3280
- }
3281
- function isRouteGroup(segment) {
3282
- return /^\(.+\)$/.test(segment);
3283
- }
3284
- function filePathToUrlPath(filePath) {
3285
- const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
3286
- const lastSlashIndex = withoutPrefix.lastIndexOf("/");
3287
- const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
3288
- if (!dirPath) {
3289
- return "";
3290
- }
3291
- const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
3292
- return normalizePath(segments.join("/"));
3293
- }
3294
-
3295
- // src/router/scanRoutes.ts
3296
- var APP_DIR = "src";
3297
- function toProdAbsPath(sourceAbsPath, rootDir, dist) {
3298
- let rel = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3299
- if (rel.startsWith(`${APP_DIR}/`)) {
3300
- rel = rel.slice(APP_DIR.length + 1);
3301
- }
3302
- const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
3303
- return path9.resolve(rootDir, prodRel);
3304
- }
3305
- async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
3306
- const routeDir = path9.dirname(routeFilePath);
3307
- const resolvedRoot = path9.resolve(rootDir);
3308
- const mwPaths = [];
3309
- let currentDir = path9.resolve(rootDir, routeDir);
3310
- while (true) {
3311
- if (dist) {
3312
- const mwPath = path9.join(currentDir, "middlewares.js");
3313
- const absMwPath = path9.resolve(rootDir, mwPath);
3314
- const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
3315
- if (fs6.existsSync(prodAbsMwPath)) {
3316
- mwPaths.push(prodAbsMwPath);
3317
- }
3318
- } else {
3319
- for (const ext of [".ts", ".js"]) {
3320
- const mwPath = path9.join(currentDir, `middlewares${ext}`);
3321
- const absMwPath = path9.resolve(rootDir, mwPath);
3322
- if (fs6.existsSync(absMwPath)) {
3323
- mwPaths.push(absMwPath);
3324
- break;
3325
- }
3326
- }
3327
- }
3328
- if (currentDir === resolvedRoot) break;
3329
- const parentDir = path9.dirname(currentDir);
3330
- if (parentDir === currentDir) break;
3331
- currentDir = parentDir;
3332
- }
3333
- if (mwPaths.length === 0) return void 0;
3334
- mwPaths.reverse();
3335
- const mergedMiddlewares = [];
3336
- const mergedInjectors = {};
3337
- for (const absMwPath of mwPaths) {
3338
- let bundle = getCachedMiddlewares(absMwPath);
3339
- if (bundle === void 0) {
3340
- bundle = await loadMiddlewaresFile(absMwPath);
3341
- setCachedMiddlewares(absMwPath, bundle);
3342
- }
3343
- mergedMiddlewares.push(...bundle.middlewares);
3344
- for (const [name, injector] of Object.entries(bundle.injectors)) {
3345
- mergedInjectors[name] = injector;
3346
- }
3347
- }
3348
- if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
3349
- return void 0;
3350
- }
3351
- return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
3352
- }
3353
- async function extractMethodsFromHandler(absPath) {
3354
- try {
3355
- const module = await importWithCacheBust(absPath);
3356
- const methods = [];
3357
- for (const key of Object.keys(module)) {
3358
- if (isHttpMethod(key) && typeof module[key] === "function") {
3359
- methods.push(key);
3360
- }
3361
- }
3362
- return methods;
3363
- } catch (err) {
3364
- const reason = err instanceof Error ? err.message : String(err);
3365
- console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25 ${absPath}: ${reason}`);
3366
- return [];
3367
- }
3368
- }
3369
- async function hasWsExport(absPath) {
3370
- try {
3371
- const module = await importWithCacheBust(absPath);
3372
- return typeof module["WS"] === "function";
3373
- } catch (err) {
3374
- const reason = err instanceof Error ? err.message : String(err);
3375
- console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25\uFF08WS \u68C0\u6D4B\uFF09${absPath}: ${reason}`);
3376
- return false;
3377
- }
3378
- }
3379
- async function scanRoutes(rootDir, patterns, dist) {
3380
- const files = await fg(patterns, {
3381
- cwd: rootDir,
3382
- onlyFiles: true,
3383
- absolute: false
3384
- });
3385
- const routes = [];
3386
- const wsRoutes = [];
3387
- for (const file of files) {
3388
- const normalizedFile = file.replace(/\\/g, "/");
3389
- const fileName = normalizedFile.split("/").pop();
3390
- if (fileName === "handler.ts" || fileName === "handler.js") {
3391
- const absPath = path9.resolve(rootDir, normalizedFile);
3392
- const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
3393
- const urlPath = filePathToUrlPath(normalizedFile);
3394
- const paramNames = extractParamNames(urlPath);
3395
- const isDynamic = paramNames.length > 0;
3396
- const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
3397
- const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
3398
- const methods = await extractMethodsFromHandler(importPath);
3399
- for (const method of methods) {
3400
- routes.push({
3401
- method,
3402
- urlPath,
3403
- filePath: normalizedFile,
3404
- paramNames,
3405
- isDynamic,
3406
- isCatchAll: isCatchAll || void 0,
3407
- middlewares: middlewareBundle?.middlewares,
3408
- injectors: middlewareBundle?.injectors
3409
- });
3410
- }
3411
- const hasWs = await hasWsExport(importPath);
3412
- if (hasWs) {
3413
- wsRoutes.push({
3414
- urlPath,
3415
- filePath: normalizedFile,
3416
- paramNames,
3417
- isDynamic,
3418
- isCatchAll: isCatchAll || void 0,
3419
- middlewares: middlewareBundle?.middlewares,
3420
- injectors: middlewareBundle?.injectors
3421
- });
3422
- }
3423
- continue;
3424
- }
3425
- }
3426
- return { routes, wsRoutes };
3427
- }
3428
-
3429
3625
  // src/cli/createDevApp.ts
3430
3626
  async function createDevApp(options) {
3431
3627
  const { app, ctx } = await createAppBase(options);
@@ -3451,18 +3647,21 @@ async function createProdApp(options) {
3451
3647
  export {
3452
3648
  FaapiError,
3453
3649
  InternalError,
3650
+ MessageQueue,
3454
3651
  MethodNotAllowedError,
3455
3652
  ModuleLoadError,
3456
3653
  RouteNotFoundError,
3457
3654
  SchemaExtractionError,
3458
3655
  ValidationError,
3459
3656
  collectRouteSchemaSources,
3657
+ connectWs,
3460
3658
  cors,
3461
3659
  createProdApp as createApp,
3462
3660
  createContext,
3463
3661
  createDevApp,
3464
3662
  createProdApp,
3465
3663
  createProgram,
3664
+ createTestServer,
3466
3665
  extractTypeInfo,
3467
3666
  getInputTypeForMethod,
3468
3667
  helmet,
@@ -3471,6 +3670,7 @@ export {
3471
3670
  loadConfig,
3472
3671
  loadEnv,
3473
3672
  logger,
3474
- resolveTypeNode
3673
+ resolveTypeNode,
3674
+ waitForWsOpen
3475
3675
  };
3476
3676
  //# sourceMappingURL=index.js.map