@faapi/faapi 1.4.0 → 2.0.0

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
@@ -1219,8 +1219,8 @@ __export(generateSchemaFiles_exports, {
1219
1219
  getRuntimeSchemaPath: () => getRuntimeSchemaPath,
1220
1220
  getSchemaOutputPath: () => getSchemaOutputPath
1221
1221
  });
1222
- import path5 from "path";
1223
- import fs4 from "fs/promises";
1222
+ import path7 from "path";
1223
+ import fs6 from "fs/promises";
1224
1224
  function getSchemaOutputPath(sourceFile, dist, rootDir) {
1225
1225
  let rel = sourceFile.replace(/\\/g, "/");
1226
1226
  if (rel.startsWith("src/")) {
@@ -1228,7 +1228,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
1228
1228
  }
1229
1229
  const idx = rel.lastIndexOf("/");
1230
1230
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1231
- return path5.resolve(rootDir, dist, relDir, "zod.js");
1231
+ return path7.resolve(rootDir, dist, relDir, "zod.js");
1232
1232
  }
1233
1233
  function getRuntimeSchemaPath(filePath, dist, rootDir) {
1234
1234
  let rel = filePath.replace(/\\/g, "/");
@@ -1239,7 +1239,7 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
1239
1239
  }
1240
1240
  const idx = rel.lastIndexOf("/");
1241
1241
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1242
- return path5.resolve(rootDir, dist, relDir, "zod.js");
1242
+ return path7.resolve(rootDir, dist, relDir, "zod.js");
1243
1243
  }
1244
1244
  function getHelpersImportPath(relDir) {
1245
1245
  if (!relDir) return `./${HELPERS_FILENAME}`;
@@ -1289,7 +1289,7 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1289
1289
  }
1290
1290
  const fileEntries = [];
1291
1291
  for (const [filePath, fileSources] of sourcesByFile) {
1292
- const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
1292
+ const relFile = path7.relative(rootDir, filePath).replace(/\\/g, "/");
1293
1293
  const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
1294
1294
  const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
1295
1295
  let relForDir = relFile;
@@ -1304,7 +1304,7 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1304
1304
  }
1305
1305
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
1306
1306
  if (usesCoerceHelpers(allSourceCode)) {
1307
- const helpersPath = path5.resolve(rootDir, dist, HELPERS_FILENAME);
1307
+ const helpersPath = path7.resolve(rootDir, dist, HELPERS_FILENAME);
1308
1308
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
1309
1309
  }
1310
1310
  await Promise.all(
@@ -1312,8 +1312,8 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1312
1312
  );
1313
1313
  }
1314
1314
  async function writeSchemaFile(outputPath, source) {
1315
- await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
1316
- await fs4.writeFile(outputPath, source, "utf-8");
1315
+ await fs6.mkdir(path7.dirname(outputPath), { recursive: true });
1316
+ await fs6.writeFile(outputPath, source, "utf-8");
1317
1317
  }
1318
1318
  var init_generateSchemaFiles = __esm({
1319
1319
  "src/cli/generateSchemaFiles.ts"() {
@@ -1646,14 +1646,14 @@ var ValidationError = class extends FaapiError {
1646
1646
  issues;
1647
1647
  };
1648
1648
  var RouteNotFoundError = class extends FaapiError {
1649
- constructor(path15) {
1650
- super("ROUTE_NOT_FOUND", `Route not found: ${path15}`, 404);
1649
+ constructor(path14) {
1650
+ super("ROUTE_NOT_FOUND", `Route not found: ${path14}`, 404);
1651
1651
  this.name = "RouteNotFoundError";
1652
1652
  }
1653
1653
  };
1654
1654
  var MethodNotAllowedError = class extends FaapiError {
1655
- constructor(method, path15, allowedMethods) {
1656
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path15}`, 405);
1655
+ constructor(method, path14, allowedMethods) {
1656
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path14}`, 405);
1657
1657
  this.allowedMethods = allowedMethods;
1658
1658
  this.name = "MethodNotAllowedError";
1659
1659
  }
@@ -1672,791 +1672,183 @@ var ModuleLoadError = class extends FaapiError {
1672
1672
  }
1673
1673
  };
1674
1674
 
1675
- // src/runtime/sse.ts
1676
- function encodeSseEvent(event) {
1677
- let out = "";
1678
- if (event.comment !== void 0) {
1679
- out += `: ${event.comment}
1680
- `;
1681
- }
1682
- if (event.event !== void 0) {
1683
- out += `event: ${event.event}
1684
- `;
1685
- }
1686
- if (event.id !== void 0) {
1687
- out += `id: ${event.id}
1688
- `;
1689
- }
1690
- if (event.retry !== void 0) {
1691
- out += `retry: ${event.retry}
1692
- `;
1693
- }
1694
- if (event.data !== void 0) {
1695
- let dataStr;
1696
- if (typeof event.data === "string") {
1697
- dataStr = event.data;
1698
- } else if (event.data === null) {
1699
- dataStr = "null";
1675
+ // src/cli/createAppCore.ts
1676
+ import fs11 from "fs";
1677
+ import path12 from "path";
1678
+ import { PassThrough, Readable as Readable3 } from "stream";
1679
+
1680
+ // src/router/sortRoutes.ts
1681
+ function sortRoutes(routes) {
1682
+ return [...routes].sort((a, b) => {
1683
+ if (a.isDynamic !== b.isDynamic) {
1684
+ return a.isDynamic ? 1 : -1;
1685
+ }
1686
+ if (a.isCatchAll !== b.isCatchAll) {
1687
+ return a.isCatchAll ? 1 : -1;
1688
+ }
1689
+ const aSegments = a.urlPath.split("/").filter(Boolean).length;
1690
+ const bSegments = b.urlPath.split("/").filter(Boolean).length;
1691
+ if (aSegments !== bSegments) {
1692
+ return aSegments - bSegments;
1693
+ }
1694
+ return a.urlPath.localeCompare(b.urlPath);
1695
+ });
1696
+ }
1697
+
1698
+ // src/router/detectRouteConflicts.ts
1699
+ function detectRouteConflicts(routes) {
1700
+ const map = /* @__PURE__ */ new Map();
1701
+ for (const route of routes) {
1702
+ const key = `${route.method} ${route.urlPath}`;
1703
+ const existing = map.get(key);
1704
+ if (existing) {
1705
+ existing.files.push(route.filePath);
1700
1706
  } else {
1701
- dataStr = JSON.stringify(event.data);
1707
+ map.set(key, {
1708
+ method: route.method,
1709
+ urlPath: route.urlPath,
1710
+ files: [route.filePath]
1711
+ });
1702
1712
  }
1703
- const lines = dataStr.split("\n");
1704
- for (const line of lines) {
1705
- out += `data: ${line}
1706
- `;
1713
+ }
1714
+ const conflicts = [];
1715
+ for (const conflict of map.values()) {
1716
+ if (conflict.files.length > 1) {
1717
+ conflicts.push(conflict);
1707
1718
  }
1708
1719
  }
1709
- out += "\n";
1710
- return out;
1720
+ return conflicts;
1711
1721
  }
1712
- function createSseWriter() {
1713
- const encoder = new TextEncoder();
1714
- let controller = null;
1715
- let closed = false;
1716
- let aborted = false;
1717
- const stream = new ReadableStream({
1718
- start(c) {
1719
- controller = c;
1720
- },
1721
- cancel() {
1722
- aborted = true;
1723
- closed = true;
1724
- controller = null;
1722
+
1723
+ // src/server/createServer.ts
1724
+ import {
1725
+ createServer as createHttpServer
1726
+ } from "http";
1727
+ import { createSecureServer as createHttp2SecureServer } from "http2";
1728
+ import { readFileSync } from "fs";
1729
+ import { Readable as Readable2 } from "stream";
1730
+ import path10 from "path";
1731
+
1732
+ // src/router/matchRoute.ts
1733
+ function matchRoute(routes, method, path14) {
1734
+ for (const route of routes) {
1735
+ if (route.method !== method) {
1736
+ continue;
1725
1737
  }
1726
- });
1727
- const response = new Response(stream, {
1728
- status: 200,
1729
- headers: {
1730
- "Content-Type": "text/event-stream",
1731
- "Cache-Control": "no-cache",
1732
- Connection: "keep-alive"
1738
+ if (!route.isDynamic) {
1739
+ if (route.urlPath === path14) {
1740
+ return { route, params: {} };
1741
+ }
1742
+ continue;
1733
1743
  }
1734
- });
1735
- const writer = {
1736
- send(event) {
1737
- if (closed || !controller) return;
1738
- const text = encodeSseEvent(event);
1739
- controller.enqueue(encoder.encode(text));
1740
- },
1741
- sendRaw(chunk) {
1742
- if (closed || !controller) return;
1743
- const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
1744
- controller.enqueue(bytes);
1745
- },
1746
- sendError(error) {
1747
- if (closed || !controller) return;
1748
- const message = error instanceof Error ? error.message : String(error);
1749
- const text = encodeSseEvent({ event: "error", data: message });
1750
- try {
1751
- controller.enqueue(encoder.encode(text));
1752
- } finally {
1753
- writer.close();
1744
+ const params = matchDynamicPath(route.urlPath, path14, route.paramNames, route.isCatchAll);
1745
+ if (params !== null) {
1746
+ return { route, params };
1747
+ }
1748
+ }
1749
+ return null;
1750
+ }
1751
+ function matchWsRoute(wsRoutes, path14) {
1752
+ for (const route of wsRoutes) {
1753
+ if (!route.isDynamic) {
1754
+ if (route.urlPath === path14) {
1755
+ return { route, params: {} };
1754
1756
  }
1755
- },
1756
- close() {
1757
- if (closed) return;
1758
- closed = true;
1759
- if (controller) {
1760
- try {
1761
- controller.close();
1762
- } catch {
1763
- }
1764
- controller = null;
1757
+ continue;
1758
+ }
1759
+ const params = matchDynamicPath(route.urlPath, path14, route.paramNames, route.isCatchAll);
1760
+ if (params !== null) {
1761
+ return { route, params };
1762
+ }
1763
+ }
1764
+ return null;
1765
+ }
1766
+ function matchDynamicPath(pattern, path14, paramNames, isCatchAll) {
1767
+ const patternSegments = pattern.split("/").filter(Boolean);
1768
+ const pathSegments = path14.split("/").filter(Boolean);
1769
+ if (isCatchAll) {
1770
+ const nonCatchAllCount = patternSegments.length - 1;
1771
+ if (pathSegments.length <= nonCatchAllCount) {
1772
+ return null;
1773
+ }
1774
+ const params2 = {};
1775
+ for (let i = 0; i < nonCatchAllCount; i++) {
1776
+ const patternSeg = patternSegments[i];
1777
+ const pathSeg = pathSegments[i];
1778
+ if (patternSeg.startsWith(":")) {
1779
+ const paramName = patternSeg.slice(1);
1780
+ params2[paramName] = pathSeg;
1781
+ } else if (patternSeg !== pathSeg) {
1782
+ return null;
1765
1783
  }
1766
- },
1767
- get closed() {
1768
- return closed;
1769
- },
1770
- get aborted() {
1771
- return aborted;
1772
- },
1773
- get response() {
1774
- return response;
1775
1784
  }
1776
- };
1777
- return writer;
1785
+ const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
1786
+ const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
1787
+ params2[catchAllParamName] = catchAllValue;
1788
+ if (Object.keys(params2).length !== paramNames.length) {
1789
+ return null;
1790
+ }
1791
+ return params2;
1792
+ }
1793
+ if (patternSegments.length !== pathSegments.length) {
1794
+ return null;
1795
+ }
1796
+ const params = {};
1797
+ for (let i = 0; i < patternSegments.length; i++) {
1798
+ const patternSeg = patternSegments[i];
1799
+ const pathSeg = pathSegments[i];
1800
+ if (patternSeg.startsWith(":")) {
1801
+ const paramName = patternSeg.slice(1);
1802
+ params[paramName] = pathSeg;
1803
+ } else if (patternSeg !== pathSeg) {
1804
+ return null;
1805
+ }
1806
+ }
1807
+ if (Object.keys(params).length !== paramNames.length) {
1808
+ return null;
1809
+ }
1810
+ return params;
1778
1811
  }
1779
1812
 
1780
- // src/runtime/createContext.ts
1781
- function parseCookies(cookieHeader) {
1782
- const cookies = /* @__PURE__ */ new Map();
1783
- if (!cookieHeader) return cookies;
1784
- for (const pair of cookieHeader.split(";")) {
1785
- const [name, ...rest] = pair.split("=");
1786
- const trimmed = name?.trim();
1787
- if (trimmed) {
1788
- cookies.set(trimmed, rest.join("=").trim());
1813
+ // src/loader/loadRouteModule.ts
1814
+ import fs8 from "fs";
1815
+
1816
+ // src/loader/resolveExports.ts
1817
+ function resolveExport(module, exportName) {
1818
+ if (exportName in module && typeof module[exportName] !== "undefined") {
1819
+ return module[exportName];
1820
+ }
1821
+ const defaultExport = module.default;
1822
+ if (defaultExport !== null && typeof defaultExport === "object") {
1823
+ const value = defaultExport[exportName];
1824
+ if (value !== void 0) {
1825
+ return value;
1789
1826
  }
1790
1827
  }
1791
- return cookies;
1828
+ return void 0;
1792
1829
  }
1793
- function formatSetCookie(name, value, options) {
1794
- let cookie = `${name}=${value}`;
1795
- if (options?.domain) cookie += `; Domain=${options.domain}`;
1796
- if (options?.path) cookie += `; Path=${options.path}`;
1797
- if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
1798
- if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
1799
- if (options?.httpOnly) cookie += `; HttpOnly`;
1800
- if (options?.secure) cookie += `; Secure`;
1801
- if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
1802
- return cookie;
1830
+
1831
+ // src/loader/validateRouteModule.ts
1832
+ function validateRouteModule(value, method, filePath) {
1833
+ if (typeof value !== "function") {
1834
+ throw new Error(
1835
+ `Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
1836
+ );
1837
+ }
1803
1838
  }
1804
- function createContext(request, params, config = {}, ip = "") {
1805
- const url = new URL(request.url);
1806
- const meta = { headers: {}, setCookies: [] };
1807
- const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
1808
- const cookiesObj = {};
1809
- for (const [key, val] of parsedCookies) {
1810
- cookiesObj[key] = val;
1811
- }
1812
- const ctx = {
1813
- request,
1814
- params,
1815
- query: url.searchParams,
1816
- headers: request.headers,
1817
- method: request.method,
1818
- path: url.pathname,
1819
- ip,
1820
- ua: request.headers.get("user-agent") ?? "",
1821
- cookies: cookiesObj,
1822
- config,
1823
- meta,
1824
- setStatus(status) {
1825
- meta.status = status;
1826
- },
1827
- setHeader(key, value) {
1828
- meta.headers[key] = value;
1829
- },
1830
- setETag(value) {
1831
- meta.headers["etag"] = value;
1832
- },
1833
- redirect(url2, status = 302) {
1834
- return new Response(null, {
1835
- status,
1836
- headers: { Location: url2 }
1837
- });
1838
- },
1839
- json(data, status) {
1840
- const headers = { "Content-Type": "application/json" };
1841
- return new Response(JSON.stringify(data), {
1842
- status: status ?? 200,
1843
- headers
1844
- });
1845
- },
1846
- html(html, status) {
1847
- const headers = { "Content-Type": "text/html; charset=utf-8" };
1848
- return new Response(html, {
1849
- status: status ?? 200,
1850
- headers
1851
- });
1852
- },
1853
- getCookie(name) {
1854
- return parsedCookies.get(name);
1855
- },
1856
- setCookie(name, value, options) {
1857
- meta.setCookies.push(formatSetCookie(name, value, options));
1858
- },
1859
- deleteCookie(name) {
1860
- meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
1861
- },
1862
- /**
1863
- * 创建 SSE writer,用于流式推送事件
1864
- *
1865
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
1866
- * 作为 HTTP 响应(Content-Type: text/event-stream)。
1867
- *
1868
- * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
1869
- */
1870
- sse() {
1871
- const writer = createSseWriter();
1872
- const ctxWithSse = ctx;
1873
- ctxWithSse.__sseResponse = writer.response;
1874
- ctxWithSse.__sseWriter = writer;
1875
- return writer;
1876
- },
1877
- /**
1878
- * 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
1879
- *
1880
- * 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
1881
- * handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
1882
- */
1883
- ok(data) {
1884
- const responseConfig = config.response;
1885
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
1886
- const body = okFn(data);
1887
- return ctx.json(body);
1888
- },
1889
- /**
1890
- * 返回错误响应(对象形式参数,status 和 code 均可省略)
1891
- *
1892
- * - status 省略时 HTTP 状态码默认 500
1893
- * - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
1894
- * - status 和 code 独立无关联
1895
- *
1896
- * body 用 config.response.fail(或默认实现)包装。
1897
- */
1898
- fail(options) {
1899
- const responseConfig = config.response;
1900
- const failFn = responseConfig?.fail ?? ((e) => {
1901
- const error = { message: e.message };
1902
- if (e.code !== void 0) error.code = e.code;
1903
- return { error };
1904
- });
1905
- const body = failFn({
1906
- status: options.status,
1907
- code: options.code,
1908
- message: options.message
1909
- });
1910
- return ctx.json(body, options.status ?? 500);
1911
- }
1912
- };
1913
- const extend = config?.extendContext;
1914
- if (typeof extend === "function") {
1915
- extend(ctx);
1916
- }
1917
- return ctx;
1918
- }
1919
-
1920
- // src/utils/isPlainObject.ts
1921
- function isPlainObject(value) {
1922
- if (value === null || typeof value !== "object") {
1923
- return false;
1924
- }
1925
- if (Array.isArray(value)) {
1926
- return false;
1927
- }
1928
- const proto = Object.getPrototypeOf(value);
1929
- return proto === null || proto === Object.prototype;
1930
- }
1931
-
1932
- // src/response/toResponse.ts
1933
- async function toResponse(value, meta) {
1934
- if (value instanceof Promise) {
1935
- return toResponse(await value, meta);
1936
- }
1937
- const applyMeta = (headers2) => {
1938
- if (!meta) return;
1939
- for (const [key, val] of Object.entries(meta.headers)) {
1940
- headers2.set(key, val);
1941
- }
1942
- for (const cookie of meta.setCookies ?? []) {
1943
- headers2.append("set-cookie", cookie);
1944
- }
1945
- };
1946
- if (value instanceof Response) {
1947
- if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
1948
- const headers2 = new Headers(value.headers);
1949
- applyMeta(headers2);
1950
- return new Response(value.body, {
1951
- status: meta.status ?? value.status,
1952
- headers: headers2
1953
- });
1954
- }
1955
- return value;
1956
- }
1957
- if (value === null || value === void 0) {
1958
- const status = meta?.status ?? 204;
1959
- const headers2 = new Headers();
1960
- applyMeta(headers2);
1961
- return new Response(null, { status, headers: headers2 });
1962
- }
1963
- if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
1964
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1965
- applyMeta(headers2);
1966
- return new Response(value, {
1967
- status: meta?.status ?? 200,
1968
- headers: headers2
1969
- });
1970
- }
1971
- if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
1972
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1973
- applyMeta(headers2);
1974
- return new Response(value, {
1975
- status: meta?.status ?? 200,
1976
- headers: headers2
1977
- });
1978
- }
1979
- if (value instanceof Uint8Array) {
1980
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1981
- applyMeta(headers2);
1982
- return new Response(value, {
1983
- status: meta?.status ?? 200,
1984
- headers: headers2
1985
- });
1986
- }
1987
- if (isPlainObject(value) || Array.isArray(value)) {
1988
- const body2 = JSON.stringify(value);
1989
- const headers2 = new Headers({ "Content-Type": "application/json" });
1990
- applyMeta(headers2);
1991
- return new Response(body2, {
1992
- status: meta?.status ?? 200,
1993
- headers: headers2
1994
- });
1995
- }
1996
- if (typeof value === "string") {
1997
- const headers2 = new Headers({ "Content-Type": "text/plain" });
1998
- applyMeta(headers2);
1999
- return new Response(value, {
2000
- status: meta?.status ?? 200,
2001
- headers: headers2
2002
- });
2003
- }
2004
- if (typeof value === "number" || typeof value === "boolean") {
2005
- const headers2 = new Headers({ "Content-Type": "text/plain" });
2006
- applyMeta(headers2);
2007
- return new Response(String(value), {
2008
- status: meta?.status ?? 200,
2009
- headers: headers2
2010
- });
2011
- }
2012
- const body = JSON.stringify(value);
2013
- const headers = new Headers({ "Content-Type": "application/json" });
2014
- applyMeta(headers);
2015
- return new Response(body, {
2016
- status: meta?.status ?? 200,
2017
- headers
2018
- });
2019
- }
2020
-
2021
- // src/injection/injectParams.ts
2022
- init_resolveInjection();
2023
-
2024
- // src/utils/queryToObject.ts
2025
- function queryToObject(params) {
2026
- const result = {};
2027
- for (const [key, value] of params) {
2028
- result[key] = value;
2029
- }
2030
- return result;
2031
- }
2032
-
2033
- // src/injection/injectParams.ts
2034
- function getBuiltinInjectionValue(type, ctx, body) {
2035
- switch (type) {
2036
- case "query":
2037
- return queryToObject(ctx.query);
2038
- case "params":
2039
- return ctx.params;
2040
- case "headers":
2041
- return ctx.headers;
2042
- case "context":
2043
- return ctx;
2044
- case "cookies":
2045
- return ctx.cookies;
2046
- case "ip":
2047
- return ctx.ip;
2048
- case "ua":
2049
- return ctx.ua;
2050
- case "body":
2051
- return body;
2052
- // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
2053
- // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
2054
- case "form":
2055
- return body;
2056
- case "files":
2057
- if (body && typeof body === "object" && "files" in body) {
2058
- return body.files;
2059
- }
2060
- return [];
2061
- case "fields":
2062
- if (body && typeof body === "object" && "fields" in body) {
2063
- return body.fields;
2064
- }
2065
- return {};
2066
- default:
2067
- return void 0;
2068
- }
2069
- }
2070
- async function injectParamsAsync(handler, ctx, body, injectors) {
2071
- const injections = resolveInjection(handler);
2072
- if (injections.length === 0) {
2073
- return await handler();
2074
- }
2075
- const args = await Promise.all(
2076
- injections.map(async (injection) => {
2077
- if (injection.type !== "unknown") {
2078
- return getBuiltinInjectionValue(injection.type, ctx, body);
2079
- }
2080
- if (injectors && injection.name in injectors) {
2081
- return await injectors[injection.name](ctx);
2082
- }
2083
- return void 0;
2084
- })
2085
- );
2086
- return await handler(...args);
2087
- }
2088
-
2089
- // src/runtime/invokeHandler.ts
2090
- function wrapResult(result, ctx) {
2091
- if (result instanceof Response) return result;
2092
- const responseConfig = ctx.config.response;
2093
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
2094
- return okFn(result);
2095
- }
2096
- function mergeMeta(response, meta) {
2097
- const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
2098
- if (!hasMeta) return response;
2099
- const headers = new Headers(response.headers);
2100
- for (const [key, value] of Object.entries(meta.headers)) {
2101
- headers.set(key, value);
2102
- }
2103
- for (const cookie of meta.setCookies) {
2104
- headers.append("set-cookie", cookie);
2105
- }
2106
- return new Response(response.body, {
2107
- status: meta.status ?? response.status,
2108
- headers
2109
- });
2110
- }
2111
- async function compose(middlewares, ctx, finalHandler) {
2112
- const meta = ctx.meta;
2113
- let index = -1;
2114
- async function dispatch(i) {
2115
- if (i <= index) {
2116
- throw new Error("next() called multiple times");
2117
- }
2118
- index = i;
2119
- if (i >= middlewares.length) {
2120
- return await finalHandler();
2121
- }
2122
- const mw = middlewares[i];
2123
- let innerResponse;
2124
- const next = async () => {
2125
- innerResponse = await dispatch(i + 1);
2126
- return innerResponse;
2127
- };
2128
- const result = await mw(ctx, next);
2129
- if (result instanceof Response) {
2130
- return mergeMeta(result, meta);
2131
- }
2132
- if (innerResponse !== void 0) {
2133
- return innerResponse;
2134
- }
2135
- throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
2136
- }
2137
- return await dispatch(0);
2138
- }
2139
- async function invokeHandler(handler, ctx, body, middlewares, injectors) {
2140
- const meta = ctx.meta;
2141
- const pickSseAndAutoClose = () => {
2142
- const sseWriter = ctx.__sseWriter;
2143
- if (!sseWriter) return null;
2144
- if (!sseWriter.closed && !sseWriter.aborted) {
2145
- sseWriter.close();
2146
- }
2147
- return mergeMeta(sseWriter.response, meta);
2148
- };
2149
- const autoCloseSseOnError = () => {
2150
- const sseWriter = ctx.__sseWriter;
2151
- if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
2152
- sseWriter.close();
2153
- }
2154
- };
2155
- if (!middlewares || middlewares.length === 0) {
2156
- try {
2157
- const result = await injectParamsAsync(handler, ctx, body, injectors);
2158
- const sseResponse = pickSseAndAutoClose();
2159
- if (sseResponse) return sseResponse;
2160
- return toResponse(wrapResult(result, ctx), meta);
2161
- } catch (err) {
2162
- autoCloseSseOnError();
2163
- throw err;
2164
- }
2165
- }
2166
- const finalHandler = async () => {
2167
- try {
2168
- const result = await injectParamsAsync(handler, ctx, body, injectors);
2169
- const sseResponse = pickSseAndAutoClose();
2170
- if (sseResponse) return sseResponse;
2171
- return toResponse(wrapResult(result, ctx), meta);
2172
- } catch (err) {
2173
- autoCloseSseOnError();
2174
- throw err;
2175
- }
2176
- };
2177
- return await compose(middlewares, ctx, finalHandler);
2178
- }
2179
-
2180
- // src/testServer.ts
2181
- import path12 from "path";
2182
- import os from "os";
2183
- import fs11 from "fs/promises";
2184
-
2185
- // src/router/scanRoutes.ts
2186
- import fg from "fast-glob";
2187
- import path4 from "path";
2188
- import fs3 from "fs";
2189
-
2190
- // src/router/constants.ts
2191
- var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
2192
- var HTTP_METHOD_SET = new Set(HTTP_METHODS);
2193
-
2194
- // src/utils/normalizePath.ts
2195
- function normalizePath(path15) {
2196
- if (!path15) return "";
2197
- let result = path15.replace(/\\/g, "/");
2198
- result = result.replace(/\/+/g, "/");
2199
- result = result.replace(/\/+$/, "");
2200
- if (result && !result.startsWith("/")) {
2201
- result = "/" + result;
2202
- }
2203
- return result;
2204
- }
2205
-
2206
- // src/router/parseRouteFile.ts
2207
- function dynamicSegmentToParam(segment) {
2208
- const match = segment.match(/^\[(.+)\]$/);
2209
- if (match) {
2210
- return ":" + match[1];
2211
- }
2212
- return segment;
2213
- }
2214
- function extractParamNames(urlPath) {
2215
- const params = [];
2216
- const segments = urlPath.split("/");
2217
- for (const segment of segments) {
2218
- if (segment.startsWith(":...")) {
2219
- params.push(segment.slice(4));
2220
- } else if (segment.startsWith(":")) {
2221
- params.push(segment.slice(1));
2222
- }
2223
- }
2224
- return params;
2225
- }
2226
- function isCatchAllSegment(segment) {
2227
- return /^\[\.\.\..+\]$/.test(segment);
2228
- }
2229
- function isRouteGroup(segment) {
2230
- return /^\(.+\)$/.test(segment);
2231
- }
2232
- function filePathToUrlPath(filePath) {
2233
- const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
2234
- const lastSlashIndex = withoutPrefix.lastIndexOf("/");
2235
- const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
2236
- if (!dirPath) {
2237
- return "";
2238
- }
2239
- const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
2240
- return normalizePath(segments.join("/"));
2241
- }
2242
-
2243
- // src/middleware/loadMiddlewares.ts
2244
- var middlewareCache = /* @__PURE__ */ new Map();
2245
- function invalidateMiddlewareCache() {
2246
- middlewareCache.clear();
2247
- }
2248
- function getCachedMiddlewares(absPath) {
2249
- return middlewareCache.get(absPath);
2250
- }
2251
- function setCachedMiddlewares(absPath, bundle) {
2252
- middlewareCache.set(absPath, bundle);
2253
- }
2254
- async function loadMiddlewaresFile(filePath) {
2255
- try {
2256
- const module = await importWithCacheBust(filePath);
2257
- const middlewares = module.default ?? module.middlewares ?? [];
2258
- if (!Array.isArray(middlewares)) {
2259
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2260
- return { middlewares: [], injectors: {} };
2261
- }
2262
- const validMiddlewares = middlewares.filter((m) => {
2263
- if (typeof m !== "function") {
2264
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
2265
- return false;
2266
- }
2267
- return true;
2268
- });
2269
- const injectors = module.injectors ?? {};
2270
- if (typeof injectors !== "object" || injectors === null) {
2271
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2272
- return { middlewares: validMiddlewares, injectors: {} };
2273
- }
2274
- const validInjectors = {};
2275
- for (const [name, injector] of Object.entries(injectors)) {
2276
- if (typeof injector !== "function") {
2277
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
2278
- continue;
2279
- }
2280
- validInjectors[name] = injector;
2281
- }
2282
- return { middlewares: validMiddlewares, injectors: validInjectors };
2283
- } catch {
2284
- return { middlewares: [], injectors: {} };
2285
- }
2286
- }
2287
- async function loadMergedMiddlewares(middlewarePaths) {
2288
- if (middlewarePaths.length === 0) return void 0;
2289
- const mergedMiddlewares = [];
2290
- const mergedInjectors = {};
2291
- for (const absMwPath of middlewarePaths) {
2292
- let bundle = getCachedMiddlewares(absMwPath);
2293
- if (bundle === void 0) {
2294
- bundle = await loadMiddlewaresFile(absMwPath);
2295
- setCachedMiddlewares(absMwPath, bundle);
2296
- }
2297
- mergedMiddlewares.push(...bundle.middlewares);
2298
- for (const [name, injector] of Object.entries(bundle.injectors)) {
2299
- mergedInjectors[name] = injector;
2300
- }
2301
- }
2302
- if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
2303
- return void 0;
2304
- }
2305
- return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
2306
- }
2307
-
2308
- // src/router/scanRoutes.ts
2309
- var HTTP_OR_WS_EXPORT_RE = new RegExp(
2310
- String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
2311
- "g"
2312
- );
2313
- function extractExportsFromSource(source) {
2314
- const names = /* @__PURE__ */ new Set();
2315
- let match;
2316
- HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
2317
- while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
2318
- names.add(match[1]);
2319
- }
2320
- return names;
2321
- }
2322
- function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
2323
- const routeDir = path4.dirname(routeFilePath);
2324
- const resolvedRoot = path4.resolve(rootDir);
2325
- const paths = [];
2326
- let currentDir = path4.resolve(rootDir, routeDir);
2327
- while (true) {
2328
- if (dist) {
2329
- const mwTsPath = path4.join(currentDir, "middlewares.ts");
2330
- const mwJsPath = path4.join(currentDir, "middlewares.js");
2331
- const absTsPath = path4.resolve(rootDir, mwTsPath);
2332
- const absJsPath = path4.resolve(rootDir, mwJsPath);
2333
- const absMwPath = fs3.existsSync(absTsPath) ? absTsPath : fs3.existsSync(absJsPath) ? absJsPath : null;
2334
- if (absMwPath) {
2335
- const relMwPath = path4.relative(rootDir, absMwPath);
2336
- const prodAbsPath = path4.resolve(rootDir, toProdFilePath(relMwPath, dist));
2337
- paths.push(prodAbsPath);
2338
- }
2339
- } else {
2340
- for (const ext of [".ts", ".js"]) {
2341
- const mwPath = path4.join(currentDir, `middlewares${ext}`);
2342
- const absMwPath = path4.resolve(rootDir, mwPath);
2343
- if (fs3.existsSync(absMwPath)) {
2344
- paths.push(absMwPath);
2345
- break;
2346
- }
2347
- }
2348
- }
2349
- if (currentDir === resolvedRoot) break;
2350
- const parentDir = path4.dirname(currentDir);
2351
- if (parentDir === currentDir) break;
2352
- currentDir = parentDir;
2353
- }
2354
- paths.reverse();
2355
- return paths;
2356
- }
2357
- function toProdFilePath(filePath, dist) {
2358
- let rel = filePath.replace(/\\/g, "/");
2359
- if (rel.startsWith("src/")) {
2360
- rel = rel.slice(4);
2361
- }
2362
- const jsPath = rel.replace(/\.ts$/, ".js");
2363
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
2364
- }
2365
- async function scanRoutes(rootDir, patterns, dist) {
2366
- const files = await fg(patterns, {
2367
- cwd: rootDir,
2368
- onlyFiles: true,
2369
- absolute: false
2370
- });
2371
- const routes = [];
2372
- const wsRoutes = [];
2373
- for (const file of files) {
2374
- const normalizedFile = file.replace(/\\/g, "/");
2375
- const fileName = normalizedFile.split("/").pop();
2376
- if (fileName === "handler.ts" || fileName === "handler.js") {
2377
- const absPath = path4.resolve(rootDir, normalizedFile);
2378
- const urlPath = filePathToUrlPath(normalizedFile);
2379
- const paramNames = extractParamNames(urlPath);
2380
- const isDynamic = paramNames.length > 0;
2381
- const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
2382
- let middlewarePaths;
2383
- let middlewareBundle;
2384
- if (dist) {
2385
- middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
2386
- } else {
2387
- const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
2388
- middlewareBundle = await loadMergedMiddlewares(mwPaths);
2389
- }
2390
- const source = await fs3.promises.readFile(absPath, "utf8").catch(() => "");
2391
- const exportNames = extractExportsFromSource(source);
2392
- const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
2393
- for (const method of methods) {
2394
- routes.push({
2395
- method,
2396
- urlPath,
2397
- filePath: normalizedFile,
2398
- paramNames,
2399
- isDynamic,
2400
- isCatchAll: isCatchAll || void 0,
2401
- middlewarePaths,
2402
- middlewares: middlewareBundle?.middlewares,
2403
- injectors: middlewareBundle?.injectors
2404
- });
2405
- }
2406
- if (exportNames.has("WS")) {
2407
- wsRoutes.push({
2408
- urlPath,
2409
- filePath: normalizedFile,
2410
- paramNames,
2411
- isDynamic,
2412
- isCatchAll: isCatchAll || void 0,
2413
- middlewarePaths,
2414
- middlewares: middlewareBundle?.middlewares,
2415
- injectors: middlewareBundle?.injectors
2416
- });
2417
- }
2418
- continue;
2419
- }
2420
- }
2421
- return { routes, wsRoutes };
2422
- }
2423
-
2424
- // src/router/sortRoutes.ts
2425
- function sortRoutes(routes) {
2426
- return [...routes].sort((a, b) => {
2427
- if (a.isDynamic !== b.isDynamic) {
2428
- return a.isDynamic ? 1 : -1;
2429
- }
2430
- if (a.isCatchAll !== b.isCatchAll) {
2431
- return a.isCatchAll ? 1 : -1;
2432
- }
2433
- const aSegments = a.urlPath.split("/").filter(Boolean).length;
2434
- const bSegments = b.urlPath.split("/").filter(Boolean).length;
2435
- if (aSegments !== bSegments) {
2436
- return aSegments - bSegments;
2437
- }
2438
- return a.urlPath.localeCompare(b.urlPath);
2439
- });
2440
- }
2441
-
2442
- // src/testServer.ts
2443
- init_generateSchemaFiles();
2444
-
2445
- // src/validator/validateInput.ts
2446
- init_schemaName();
2447
1839
 
2448
1840
  // src/cli/compileOnDemand.ts
2449
- import path9 from "path";
2450
- import fs8 from "fs";
2451
-
2452
- // src/cli/compileDevRoutes.ts
2453
1841
  import path8 from "path";
2454
1842
  import fs7 from "fs";
2455
- import fg2 from "fast-glob";
1843
+
1844
+ // src/cli/compileDevRoutes.ts
1845
+ import path6 from "path";
1846
+ import fs5 from "fs";
1847
+ import fg from "fast-glob";
2456
1848
 
2457
1849
  // src/cli/aliasPlugin.ts
2458
- import path7 from "path";
2459
- import fs6 from "fs";
1850
+ import path5 from "path";
1851
+ import fs4 from "fs";
2460
1852
 
2461
1853
  // src/utils/resolveAlias.ts
2462
1854
  function resolveAlias(specifier, config) {
@@ -2483,11 +1875,11 @@ function resolveAlias(specifier, config) {
2483
1875
 
2484
1876
  // src/utils/readTsconfig.ts
2485
1877
  import ts6 from "typescript";
2486
- import path6 from "path";
2487
- import fs5 from "fs";
1878
+ import path4 from "path";
1879
+ import fs3 from "fs";
2488
1880
  function readTsconfig(rootDir) {
2489
- const tsconfigPath = path6.resolve(rootDir, "tsconfig.json");
2490
- if (!fs5.existsSync(tsconfigPath)) return null;
1881
+ const tsconfigPath = path4.resolve(rootDir, "tsconfig.json");
1882
+ if (!fs3.existsSync(tsconfigPath)) return null;
2491
1883
  const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2492
1884
  if (configFile.error || !configFile.config) return null;
2493
1885
  const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
@@ -2496,7 +1888,7 @@ function readTsconfig(rootDir) {
2496
1888
  if (!rawPaths) return null;
2497
1889
  const paths = {};
2498
1890
  for (const [pattern, targets] of Object.entries(rawPaths)) {
2499
- paths[pattern] = targets.map((t) => path6.resolve(baseUrl, t));
1891
+ paths[pattern] = targets.map((t) => path4.resolve(baseUrl, t));
2500
1892
  }
2501
1893
  return { baseUrl, paths };
2502
1894
  }
@@ -2509,29 +1901,29 @@ function toProdExtension(filePath) {
2509
1901
  return filePath;
2510
1902
  }
2511
1903
  function toProdImportPath(sourceFile, importer) {
2512
- const importerDir = path7.dirname(importer);
2513
- let rel = path7.relative(importerDir, sourceFile);
2514
- rel = rel.split(path7.sep).join("/");
1904
+ const importerDir = path5.dirname(importer);
1905
+ let rel = path5.relative(importerDir, sourceFile);
1906
+ rel = rel.split(path5.sep).join("/");
2515
1907
  if (!rel.startsWith(".")) rel = "./" + rel;
2516
1908
  return toProdExtension(rel);
2517
1909
  }
2518
1910
  function toRealPath(p) {
2519
1911
  try {
2520
- return fs6.realpathSync(p);
1912
+ return fs4.realpathSync(p);
2521
1913
  } catch {
2522
1914
  return p;
2523
1915
  }
2524
1916
  }
2525
1917
  function isInsideDir(filePath, dir) {
2526
- const rel = path7.relative(dir, filePath);
2527
- return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1918
+ const rel = path5.relative(dir, filePath);
1919
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
2528
1920
  }
2529
1921
  var APP_DIR = "src";
2530
1922
  function toStrippedProdImportPath(sourceFile, rootDir) {
2531
- const appDirAbs = toRealPath(path7.resolve(rootDir, APP_DIR));
1923
+ const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
2532
1924
  const sourceReal = toRealPath(sourceFile);
2533
- let rel = path7.relative(appDirAbs, sourceReal);
2534
- rel = rel.split(path7.sep).join("/");
1925
+ let rel = path5.relative(appDirAbs, sourceReal);
1926
+ rel = rel.split(path5.sep).join("/");
2535
1927
  if (!rel.startsWith(".")) rel = "./" + rel;
2536
1928
  return toProdExtension(rel);
2537
1929
  }
@@ -2546,34 +1938,34 @@ var INDEX_EXTS = [
2546
1938
  "/index.cjs"
2547
1939
  ];
2548
1940
  function resolveRelativeSpecifier(importer, specifier) {
2549
- const importerDir = path7.dirname(importer);
2550
- const base = path7.resolve(importerDir, specifier);
1941
+ const importerDir = path5.dirname(importer);
1942
+ const base = path5.resolve(importerDir, specifier);
2551
1943
  if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2552
- return fs6.existsSync(base) ? base : null;
1944
+ return fs4.existsSync(base) ? base : null;
2553
1945
  }
2554
1946
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2555
- return fs6.existsSync(base) ? base : null;
1947
+ return fs4.existsSync(base) ? base : null;
2556
1948
  }
2557
1949
  for (const ext of SOURCE_EXTS) {
2558
1950
  const file = base + ext;
2559
- if (fs6.existsSync(file)) return file;
1951
+ if (fs4.existsSync(file)) return file;
2560
1952
  }
2561
1953
  for (const indexExt of INDEX_EXTS) {
2562
1954
  const file = base + indexExt;
2563
- if (fs6.existsSync(file)) return file;
1955
+ if (fs4.existsSync(file)) return file;
2564
1956
  }
2565
1957
  return null;
2566
1958
  }
2567
1959
  function createAliasPlugin(config, options) {
2568
1960
  const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2569
- const appDirAbs = options?.rootDir ? toRealPath(path7.resolve(options.rootDir, APP_DIR)) : null;
1961
+ const appDirAbs = options?.rootDir ? toRealPath(path5.resolve(options.rootDir, APP_DIR)) : null;
2570
1962
  return {
2571
1963
  name: "faapi-alias",
2572
1964
  setup(build) {
2573
1965
  build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2574
1966
  let source;
2575
1967
  try {
2576
- source = fs6.readFileSync(args.path, "utf8");
1968
+ source = fs4.readFileSync(args.path, "utf8");
2577
1969
  } catch {
2578
1970
  return void 0;
2579
1971
  }
@@ -2606,7 +1998,7 @@ function createAliasPlugin(config, options) {
2606
1998
  for (const candidate of candidates) {
2607
1999
  for (const ext of SOURCE_EXTS) {
2608
2000
  const file = candidate + ext;
2609
- if (fs6.existsSync(file)) {
2001
+ if (fs4.existsSync(file)) {
2610
2002
  modified = true;
2611
2003
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2612
2004
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2619,7 +2011,7 @@ function createAliasPlugin(config, options) {
2619
2011
  }
2620
2012
  for (const indexExt of INDEX_EXTS) {
2621
2013
  const file = candidate + indexExt;
2622
- if (fs6.existsSync(file)) {
2014
+ if (fs4.existsSync(file)) {
2623
2015
  modified = true;
2624
2016
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2625
2017
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2648,7 +2040,7 @@ function buildAliasPlugins(rootDir) {
2648
2040
  var APP_DIR2 = "src";
2649
2041
  async function compileDevRoutes(options) {
2650
2042
  const { rootDir, dist, files, logLevel = "silent" } = options;
2651
- const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2043
+ const entryPoints = files ?? await fg([`${APP_DIR2}/**/*.ts`], {
2652
2044
  cwd: rootDir,
2653
2045
  onlyFiles: true,
2654
2046
  absolute: true,
@@ -2657,11 +2049,11 @@ async function compileDevRoutes(options) {
2657
2049
  if (entryPoints.length === 0) {
2658
2050
  return { compiledFiles: [] };
2659
2051
  }
2660
- const absDist = path8.resolve(rootDir, dist);
2661
- await fs7.promises.mkdir(absDist, { recursive: true });
2052
+ const absDist = path6.resolve(rootDir, dist);
2053
+ await fs5.promises.mkdir(absDist, { recursive: true });
2662
2054
  const plugins = buildAliasPlugins(rootDir);
2663
2055
  const esbuild = await import("esbuild");
2664
- const outbase = path8.resolve(rootDir, APP_DIR2);
2056
+ const outbase = path6.resolve(rootDir, APP_DIR2);
2665
2057
  const result = await esbuild.build({
2666
2058
  entryPoints,
2667
2059
  outdir: absDist,
@@ -2678,10 +2070,10 @@ async function compileDevRoutes(options) {
2678
2070
  if (result.outputFiles) {
2679
2071
  await Promise.all(
2680
2072
  result.outputFiles.map(async (file) => {
2681
- await fs7.promises.mkdir(path8.dirname(file.path), { recursive: true });
2073
+ await fs5.promises.mkdir(path6.dirname(file.path), { recursive: true });
2682
2074
  const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2683
- await fs7.promises.writeFile(tmp, file.contents);
2684
- await fs7.promises.rename(tmp, file.path);
2075
+ await fs5.promises.writeFile(tmp, file.contents);
2076
+ await fs5.promises.rename(tmp, file.path);
2685
2077
  })
2686
2078
  );
2687
2079
  }
@@ -2693,8 +2085,8 @@ init_generateSchemaFiles();
2693
2085
  init_generateSchemaFiles();
2694
2086
  function isProductFresh(sourceAbsPath, productAbsPath) {
2695
2087
  try {
2696
- const srcStat = fs8.statSync(sourceAbsPath);
2697
- const prodStat = fs8.statSync(productAbsPath);
2088
+ const srcStat = fs7.statSync(sourceAbsPath);
2089
+ const prodStat = fs7.statSync(productAbsPath);
2698
2090
  return prodStat.mtimeMs >= srcStat.mtimeMs;
2699
2091
  } catch {
2700
2092
  return false;
@@ -2708,7 +2100,7 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2708
2100
  if (compiledFiles.has(sourceAbsPath)) {
2709
2101
  return false;
2710
2102
  }
2711
- if (!fs8.existsSync(sourceAbsPath)) {
2103
+ if (!fs7.existsSync(sourceAbsPath)) {
2712
2104
  return false;
2713
2105
  }
2714
2106
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
@@ -2726,11 +2118,11 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2726
2118
  return true;
2727
2119
  }
2728
2120
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2729
- const rel = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2121
+ const rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2730
2122
  if (!rel.startsWith("src/")) return null;
2731
2123
  const relWithoutSrc = rel.slice(4);
2732
2124
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2733
- return path9.resolve(rootDir, dist, jsRel);
2125
+ return path8.resolve(rootDir, dist, jsRel);
2734
2126
  }
2735
2127
  var generatedSchemas = /* @__PURE__ */ new Set();
2736
2128
  function clearGeneratedSchemas() {
@@ -2740,9 +2132,9 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2740
2132
  if (generatedSchemas.has(schemaPath)) {
2741
2133
  return false;
2742
2134
  }
2743
- const prodAbsPath = path9.resolve(rootDir, routeFilePath);
2135
+ const prodAbsPath = path8.resolve(rootDir, routeFilePath);
2744
2136
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2745
- if (!fs8.existsSync(sourceAbsPath)) {
2137
+ if (!fs7.existsSync(sourceAbsPath)) {
2746
2138
  return false;
2747
2139
  }
2748
2140
  if (isProductFresh(sourceAbsPath, schemaPath)) {
@@ -2753,7 +2145,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2753
2145
  if (fileRoutes.length === 0) {
2754
2146
  return false;
2755
2147
  }
2756
- const sourceRelPath = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2148
+ const sourceRelPath = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2757
2149
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2758
2150
  await generateSchemaFiles(sourceRoutes, rootDir, dist);
2759
2151
  generatedSchemas.add(schemaPath);
@@ -2766,235 +2158,31 @@ async function deleteSchemaFiles(routes, rootDir, dist) {
2766
2158
  if (deleted.has(schemaPath)) continue;
2767
2159
  deleted.add(schemaPath);
2768
2160
  try {
2769
- await fs8.promises.unlink(schemaPath);
2161
+ await fs7.promises.unlink(schemaPath);
2770
2162
  } catch {
2771
2163
  }
2772
2164
  }
2773
2165
  }
2774
- function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2775
- const rel = path9.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2776
- let relWithoutDist = rel;
2777
- if (relWithoutDist.startsWith(`${dist}/`)) {
2778
- relWithoutDist = relWithoutDist.slice(dist.length + 1);
2779
- }
2780
- const srcRel = `src/${relWithoutDist}`;
2781
- const tsRel = srcRel.replace(/\.js$/, ".ts");
2782
- const tsAbs = path9.resolve(rootDir, tsRel);
2783
- if (fs8.existsSync(tsAbs)) return tsAbs;
2784
- return path9.resolve(rootDir, srcRel);
2785
- }
2786
- var devOnDemandEnabled = false;
2787
- function isDevOnDemandEnabled() {
2788
- return devOnDemandEnabled;
2789
- }
2790
- var devDistDir;
2791
- function getDevDist() {
2792
- return devDistDir;
2793
- }
2794
-
2795
- // src/validator/validateInput.ts
2796
- var moduleCache = /* @__PURE__ */ new Map();
2797
- function invalidateSchemaCache() {
2798
- moduleCache.clear();
2799
- }
2800
- async function loadSchemaModule(schemaPath) {
2801
- let mod = moduleCache.get(schemaPath);
2802
- if (!mod) {
2803
- mod = await importWithCacheBust(schemaPath, isDevOnDemandEnabled());
2804
- moduleCache.set(schemaPath, mod);
2805
- }
2806
- return mod;
2807
- }
2808
- async function validateInput(schemaPath, method, inputType, input) {
2809
- const schemaName = getSchemaName(method, inputType);
2810
- const schemaKey = `${schemaName}Schema`;
2811
- let mod;
2812
- try {
2813
- mod = await loadSchemaModule(schemaPath);
2814
- } catch (err) {
2815
- const reason = err instanceof Error ? err.message : String(err);
2816
- throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
2817
- }
2818
- const schema = mod[schemaKey];
2819
- if (schema === void 0 || schema === null) {
2820
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2821
- return { valid: true, issues: [], data };
2822
- }
2823
- if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2824
- throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2825
- }
2826
- const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2827
- const zodSchema = schema;
2828
- const result = zodSchema.safeParse(inputObj);
2829
- if (result.success) {
2830
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2831
- return { valid: true, issues: [], data };
2832
- }
2833
- const issues = mapZodIssues(result.error);
2834
- return { valid: false, issues, data: inputObj };
2835
- }
2836
- function mapZodIssues(error) {
2837
- return error.issues.map((issue) => {
2838
- const code = mapZodCode(issue.code, issue.message);
2839
- const path15 = issue.path.map(String).join(".") || "";
2840
- return {
2841
- path: path15,
2842
- code,
2843
- expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2844
- received: issue.received ?? mapReceivedFromMessage(issue.message),
2845
- message: issue.message
2846
- };
2847
- });
2848
- }
2849
- function mapZodCode(zodCode, message) {
2850
- switch (zodCode) {
2851
- case "invalid_type":
2852
- case "invalid_union":
2853
- case "invalid_union_discriminator":
2854
- return "TYPE_MISMATCH";
2855
- case "unrecognized_keys":
2856
- return "INVALID_FORMAT";
2857
- case "invalid_value":
2858
- case "invalid_string":
2859
- case "too_small":
2860
- case "too_big":
2861
- case "invalid_intersection_types":
2862
- case "not_multiple_of":
2863
- return "INVALID_VALUE";
2864
- case "custom":
2865
- return "INVALID_VALUE";
2866
- default:
2867
- if (message.includes("Required") || message.includes("required")) {
2868
- return "MISSING_FIELD";
2869
- }
2870
- return "INVALID_VALUE";
2871
- }
2872
- }
2873
- function mapExpectedFromMessage(message) {
2874
- const match = message.match(/Expected\s+(\w+)/i);
2875
- return match ? match[1].toLowerCase() : "unknown";
2876
- }
2877
- function mapReceivedFromMessage(message) {
2878
- const match = message.match(/received\s+(\w+)/i);
2879
- return match ? match[1].toLowerCase() : "unknown";
2880
- }
2881
-
2882
- // src/server/createServer.ts
2883
- import {
2884
- createServer as createHttpServer
2885
- } from "http";
2886
- import { createSecureServer as createHttp2SecureServer } from "http2";
2887
- import { readFileSync } from "fs";
2888
- import { Readable as Readable2 } from "stream";
2889
- import path11 from "path";
2890
-
2891
- // src/router/matchRoute.ts
2892
- function matchRoute(routes, method, path15) {
2893
- for (const route of routes) {
2894
- if (route.method !== method) {
2895
- continue;
2896
- }
2897
- if (!route.isDynamic) {
2898
- if (route.urlPath === path15) {
2899
- return { route, params: {} };
2900
- }
2901
- continue;
2902
- }
2903
- const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2904
- if (params !== null) {
2905
- return { route, params };
2906
- }
2907
- }
2908
- return null;
2909
- }
2910
- function matchWsRoute(wsRoutes, path15) {
2911
- for (const route of wsRoutes) {
2912
- if (!route.isDynamic) {
2913
- if (route.urlPath === path15) {
2914
- return { route, params: {} };
2915
- }
2916
- continue;
2917
- }
2918
- const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2919
- if (params !== null) {
2920
- return { route, params };
2921
- }
2922
- }
2923
- return null;
2924
- }
2925
- function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
2926
- const patternSegments = pattern.split("/").filter(Boolean);
2927
- const pathSegments = path15.split("/").filter(Boolean);
2928
- if (isCatchAll) {
2929
- const nonCatchAllCount = patternSegments.length - 1;
2930
- if (pathSegments.length <= nonCatchAllCount) {
2931
- return null;
2932
- }
2933
- const params2 = {};
2934
- for (let i = 0; i < nonCatchAllCount; i++) {
2935
- const patternSeg = patternSegments[i];
2936
- const pathSeg = pathSegments[i];
2937
- if (patternSeg.startsWith(":")) {
2938
- const paramName = patternSeg.slice(1);
2939
- params2[paramName] = pathSeg;
2940
- } else if (patternSeg !== pathSeg) {
2941
- return null;
2942
- }
2943
- }
2944
- const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
2945
- const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
2946
- params2[catchAllParamName] = catchAllValue;
2947
- if (Object.keys(params2).length !== paramNames.length) {
2948
- return null;
2949
- }
2950
- return params2;
2951
- }
2952
- if (patternSegments.length !== pathSegments.length) {
2953
- return null;
2954
- }
2955
- const params = {};
2956
- for (let i = 0; i < patternSegments.length; i++) {
2957
- const patternSeg = patternSegments[i];
2958
- const pathSeg = pathSegments[i];
2959
- if (patternSeg.startsWith(":")) {
2960
- const paramName = patternSeg.slice(1);
2961
- params[paramName] = pathSeg;
2962
- } else if (patternSeg !== pathSeg) {
2963
- return null;
2964
- }
2965
- }
2966
- if (Object.keys(params).length !== paramNames.length) {
2967
- return null;
2968
- }
2969
- return params;
2970
- }
2971
-
2972
- // src/loader/loadRouteModule.ts
2973
- import fs9 from "fs";
2974
-
2975
- // src/loader/resolveExports.ts
2976
- function resolveExport(module, exportName) {
2977
- if (exportName in module && typeof module[exportName] !== "undefined") {
2978
- return module[exportName];
2979
- }
2980
- const defaultExport = module.default;
2981
- if (defaultExport !== null && typeof defaultExport === "object") {
2982
- const value = defaultExport[exportName];
2983
- if (value !== void 0) {
2984
- return value;
2985
- }
2986
- }
2987
- return void 0;
2988
- }
2989
-
2990
- // src/loader/validateRouteModule.ts
2991
- function validateRouteModule(value, method, filePath) {
2992
- if (typeof value !== "function") {
2993
- throw new Error(
2994
- `Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
2995
- );
2996
- }
2997
- }
2166
+ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2167
+ const rel = path8.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2168
+ let relWithoutDist = rel;
2169
+ if (relWithoutDist.startsWith(`${dist}/`)) {
2170
+ relWithoutDist = relWithoutDist.slice(dist.length + 1);
2171
+ }
2172
+ const srcRel = `src/${relWithoutDist}`;
2173
+ const tsRel = srcRel.replace(/\.js$/, ".ts");
2174
+ const tsAbs = path8.resolve(rootDir, tsRel);
2175
+ if (fs7.existsSync(tsAbs)) return tsAbs;
2176
+ return path8.resolve(rootDir, srcRel);
2177
+ }
2178
+ var devOnDemandEnabled = false;
2179
+ function isDevOnDemandEnabled() {
2180
+ return devOnDemandEnabled;
2181
+ }
2182
+ var devDistDir;
2183
+ function getDevDist() {
2184
+ return devDistDir;
2185
+ }
2998
2186
 
2999
2187
  // src/loader/loadRouteModule.ts
3000
2188
  async function loadRouteModule(filePath, method, rootDir) {
@@ -3002,7 +2190,7 @@ async function loadRouteModule(filePath, method, rootDir) {
3002
2190
  const dist = getDevDist();
3003
2191
  if (dist) {
3004
2192
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3005
- if (sourcePath && fs9.existsSync(sourcePath)) {
2193
+ if (sourcePath && fs8.existsSync(sourcePath)) {
3006
2194
  try {
3007
2195
  await ensureCompiled(sourcePath, rootDir, dist);
3008
2196
  } catch (compileErr) {
@@ -3014,16 +2202,270 @@ async function loadRouteModule(filePath, method, rootDir) {
3014
2202
  }
3015
2203
  }
3016
2204
  }
3017
- let module;
3018
- try {
3019
- module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
3020
- } catch (err) {
3021
- const reason = err instanceof Error ? err.message : String(err);
3022
- throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
2205
+ let module;
2206
+ try {
2207
+ module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
2208
+ } catch (err) {
2209
+ const reason = err instanceof Error ? err.message : String(err);
2210
+ throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
2211
+ }
2212
+ const handler = resolveExport(module, method);
2213
+ validateRouteModule(handler, method, filePath);
2214
+ return { handler, method };
2215
+ }
2216
+
2217
+ // src/runtime/sse.ts
2218
+ function encodeSseEvent(event) {
2219
+ let out = "";
2220
+ if (event.comment !== void 0) {
2221
+ out += `: ${event.comment}
2222
+ `;
2223
+ }
2224
+ if (event.event !== void 0) {
2225
+ out += `event: ${event.event}
2226
+ `;
2227
+ }
2228
+ if (event.id !== void 0) {
2229
+ out += `id: ${event.id}
2230
+ `;
2231
+ }
2232
+ if (event.retry !== void 0) {
2233
+ out += `retry: ${event.retry}
2234
+ `;
2235
+ }
2236
+ if (event.data !== void 0) {
2237
+ let dataStr;
2238
+ if (typeof event.data === "string") {
2239
+ dataStr = event.data;
2240
+ } else if (event.data === null) {
2241
+ dataStr = "null";
2242
+ } else {
2243
+ dataStr = JSON.stringify(event.data);
2244
+ }
2245
+ const lines = dataStr.split("\n");
2246
+ for (const line of lines) {
2247
+ out += `data: ${line}
2248
+ `;
2249
+ }
2250
+ }
2251
+ out += "\n";
2252
+ return out;
2253
+ }
2254
+ function createSseWriter() {
2255
+ const encoder = new TextEncoder();
2256
+ let controller = null;
2257
+ let closed = false;
2258
+ let aborted = false;
2259
+ const stream = new ReadableStream({
2260
+ start(c) {
2261
+ controller = c;
2262
+ },
2263
+ cancel() {
2264
+ aborted = true;
2265
+ closed = true;
2266
+ controller = null;
2267
+ }
2268
+ });
2269
+ const response = new Response(stream, {
2270
+ status: 200,
2271
+ headers: {
2272
+ "Content-Type": "text/event-stream",
2273
+ "Cache-Control": "no-cache",
2274
+ Connection: "keep-alive"
2275
+ }
2276
+ });
2277
+ const writer = {
2278
+ send(event) {
2279
+ if (closed || !controller) return;
2280
+ const text = encodeSseEvent(event);
2281
+ controller.enqueue(encoder.encode(text));
2282
+ },
2283
+ sendRaw(chunk) {
2284
+ if (closed || !controller) return;
2285
+ const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
2286
+ controller.enqueue(bytes);
2287
+ },
2288
+ sendError(error) {
2289
+ if (closed || !controller) return;
2290
+ const message = error instanceof Error ? error.message : String(error);
2291
+ const text = encodeSseEvent({ event: "error", data: message });
2292
+ try {
2293
+ controller.enqueue(encoder.encode(text));
2294
+ } finally {
2295
+ writer.close();
2296
+ }
2297
+ },
2298
+ close() {
2299
+ if (closed) return;
2300
+ closed = true;
2301
+ if (controller) {
2302
+ try {
2303
+ controller.close();
2304
+ } catch {
2305
+ }
2306
+ controller = null;
2307
+ }
2308
+ },
2309
+ get closed() {
2310
+ return closed;
2311
+ },
2312
+ get aborted() {
2313
+ return aborted;
2314
+ },
2315
+ get response() {
2316
+ return response;
2317
+ }
2318
+ };
2319
+ return writer;
2320
+ }
2321
+
2322
+ // src/runtime/createContext.ts
2323
+ function parseCookies(cookieHeader) {
2324
+ const cookies = /* @__PURE__ */ new Map();
2325
+ if (!cookieHeader) return cookies;
2326
+ for (const pair of cookieHeader.split(";")) {
2327
+ const [name, ...rest] = pair.split("=");
2328
+ const trimmed = name?.trim();
2329
+ if (trimmed) {
2330
+ cookies.set(trimmed, rest.join("=").trim());
2331
+ }
2332
+ }
2333
+ return cookies;
2334
+ }
2335
+ function formatSetCookie(name, value, options) {
2336
+ let cookie = `${name}=${value}`;
2337
+ if (options?.domain) cookie += `; Domain=${options.domain}`;
2338
+ if (options?.path) cookie += `; Path=${options.path}`;
2339
+ if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
2340
+ if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
2341
+ if (options?.httpOnly) cookie += `; HttpOnly`;
2342
+ if (options?.secure) cookie += `; Secure`;
2343
+ if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
2344
+ return cookie;
2345
+ }
2346
+ function createContext(request, params, config = {}, ip = "") {
2347
+ const url = new URL(request.url);
2348
+ const meta = { headers: {}, setCookies: [] };
2349
+ const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
2350
+ const cookiesObj = {};
2351
+ for (const [key, val] of parsedCookies) {
2352
+ cookiesObj[key] = val;
2353
+ }
2354
+ const ctx = {
2355
+ request,
2356
+ params,
2357
+ query: url.searchParams,
2358
+ headers: request.headers,
2359
+ method: request.method,
2360
+ path: url.pathname,
2361
+ ip,
2362
+ ua: request.headers.get("user-agent") ?? "",
2363
+ cookies: cookiesObj,
2364
+ config,
2365
+ meta,
2366
+ setStatus(status) {
2367
+ meta.status = status;
2368
+ },
2369
+ setHeader(key, value) {
2370
+ meta.headers[key] = value;
2371
+ },
2372
+ setETag(value) {
2373
+ meta.headers["etag"] = value;
2374
+ },
2375
+ redirect(url2, status = 302) {
2376
+ return new Response(null, {
2377
+ status,
2378
+ headers: { Location: url2 }
2379
+ });
2380
+ },
2381
+ json(data, status) {
2382
+ const headers = { "Content-Type": "application/json" };
2383
+ return new Response(JSON.stringify(data), {
2384
+ status: status ?? 200,
2385
+ headers
2386
+ });
2387
+ },
2388
+ html(html, status) {
2389
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
2390
+ return new Response(html, {
2391
+ status: status ?? 200,
2392
+ headers
2393
+ });
2394
+ },
2395
+ getCookie(name) {
2396
+ return parsedCookies.get(name);
2397
+ },
2398
+ setCookie(name, value, options) {
2399
+ meta.setCookies.push(formatSetCookie(name, value, options));
2400
+ },
2401
+ deleteCookie(name) {
2402
+ meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
2403
+ },
2404
+ /**
2405
+ * 创建 SSE writer,用于流式推送事件
2406
+ *
2407
+ * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
2408
+ * 作为 HTTP 响应(Content-Type: text/event-stream)。
2409
+ *
2410
+ * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
2411
+ */
2412
+ sse() {
2413
+ const writer = createSseWriter();
2414
+ const ctxWithSse = ctx;
2415
+ ctxWithSse.__sseResponse = writer.response;
2416
+ ctxWithSse.__sseWriter = writer;
2417
+ return writer;
2418
+ },
2419
+ /**
2420
+ * 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
2421
+ *
2422
+ * 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
2423
+ * handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
2424
+ */
2425
+ ok(data) {
2426
+ const responseConfig = config.response;
2427
+ const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
2428
+ const body = okFn(data);
2429
+ return ctx.json(body);
2430
+ },
2431
+ /**
2432
+ * 返回错误响应(对象形式参数,status 和 code 均可省略)
2433
+ *
2434
+ * - status 省略时 HTTP 状态码默认 500
2435
+ * - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
2436
+ * - status 和 code 独立无关联
2437
+ *
2438
+ * body 用 config.response.fail(或默认实现)包装。
2439
+ */
2440
+ fail(options) {
2441
+ const responseConfig = config.response;
2442
+ const failFn = responseConfig?.fail ?? ((e) => {
2443
+ const error = { message: e.message };
2444
+ if (e.code !== void 0) error.code = e.code;
2445
+ return { error };
2446
+ });
2447
+ const body = failFn({
2448
+ status: options.status,
2449
+ code: options.code,
2450
+ message: options.message
2451
+ });
2452
+ return ctx.json(body, options.status ?? 500);
2453
+ }
2454
+ };
2455
+ const extend = config?.extendContext;
2456
+ if (typeof extend === "function") {
2457
+ extend(ctx);
2458
+ }
2459
+ return ctx;
2460
+ }
2461
+
2462
+ // src/utils/queryToObject.ts
2463
+ function queryToObject(params) {
2464
+ const result = {};
2465
+ for (const [key, value] of params) {
2466
+ result[key] = value;
3023
2467
  }
3024
- const handler = resolveExport(module, method);
3025
- validateRouteModule(handler, method, filePath);
3026
- return { handler, method };
2468
+ return result;
3027
2469
  }
3028
2470
 
3029
2471
  // src/utils/parseJsonBody.ts
@@ -3063,48 +2505,297 @@ async function parseMultipart(request) {
3063
2505
  }
3064
2506
  }
3065
2507
  }
3066
- return { fields, files };
2508
+ return { fields, files };
2509
+ }
2510
+
2511
+ // src/runtime/resolveInput.ts
2512
+ init_inputType();
2513
+ async function resolveInput(method, request) {
2514
+ const inputType = getInputTypeForMethod(method);
2515
+ if (inputType === "body") {
2516
+ const contentType = request.headers.get("content-type") ?? "";
2517
+ if (contentType.includes("multipart/form-data")) {
2518
+ return parseMultipart(request);
2519
+ }
2520
+ if (contentType.includes("application/x-www-form-urlencoded")) {
2521
+ const text2 = await request.text();
2522
+ if (text2.trim() === "") return null;
2523
+ const params = new URLSearchParams(text2);
2524
+ const obj = {};
2525
+ for (const [key, value] of params) {
2526
+ obj[key] = value;
2527
+ }
2528
+ return obj;
2529
+ }
2530
+ const text = await request.text();
2531
+ if (text.trim() === "") {
2532
+ return null;
2533
+ }
2534
+ const result = parseJsonBody(text);
2535
+ if (!result.success) {
2536
+ throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
2537
+ {
2538
+ path: "body",
2539
+ code: "INVALID_FORMAT",
2540
+ expected: "JSON",
2541
+ received: "text",
2542
+ message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
2543
+ }
2544
+ ]);
2545
+ }
2546
+ return result.data;
2547
+ }
2548
+ const url = new URL(request.url);
2549
+ return queryToObject(url.searchParams);
2550
+ }
2551
+
2552
+ // src/utils/isPlainObject.ts
2553
+ function isPlainObject(value) {
2554
+ if (value === null || typeof value !== "object") {
2555
+ return false;
2556
+ }
2557
+ if (Array.isArray(value)) {
2558
+ return false;
2559
+ }
2560
+ const proto = Object.getPrototypeOf(value);
2561
+ return proto === null || proto === Object.prototype;
2562
+ }
2563
+
2564
+ // src/response/toResponse.ts
2565
+ async function toResponse(value, meta) {
2566
+ if (value instanceof Promise) {
2567
+ return toResponse(await value, meta);
2568
+ }
2569
+ const applyMeta = (headers2) => {
2570
+ if (!meta) return;
2571
+ for (const [key, val] of Object.entries(meta.headers)) {
2572
+ headers2.set(key, val);
2573
+ }
2574
+ for (const cookie of meta.setCookies ?? []) {
2575
+ headers2.append("set-cookie", cookie);
2576
+ }
2577
+ };
2578
+ if (value instanceof Response) {
2579
+ if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
2580
+ const headers2 = new Headers(value.headers);
2581
+ applyMeta(headers2);
2582
+ return new Response(value.body, {
2583
+ status: meta.status ?? value.status,
2584
+ headers: headers2
2585
+ });
2586
+ }
2587
+ return value;
2588
+ }
2589
+ if (value === null || value === void 0) {
2590
+ const status = meta?.status ?? 204;
2591
+ const headers2 = new Headers();
2592
+ applyMeta(headers2);
2593
+ return new Response(null, { status, headers: headers2 });
2594
+ }
2595
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
2596
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
2597
+ applyMeta(headers2);
2598
+ return new Response(value, {
2599
+ status: meta?.status ?? 200,
2600
+ headers: headers2
2601
+ });
2602
+ }
2603
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
2604
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
2605
+ applyMeta(headers2);
2606
+ return new Response(value, {
2607
+ status: meta?.status ?? 200,
2608
+ headers: headers2
2609
+ });
2610
+ }
2611
+ if (value instanceof Uint8Array) {
2612
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
2613
+ applyMeta(headers2);
2614
+ return new Response(value, {
2615
+ status: meta?.status ?? 200,
2616
+ headers: headers2
2617
+ });
2618
+ }
2619
+ if (isPlainObject(value) || Array.isArray(value)) {
2620
+ const body2 = JSON.stringify(value);
2621
+ const headers2 = new Headers({ "Content-Type": "application/json" });
2622
+ applyMeta(headers2);
2623
+ return new Response(body2, {
2624
+ status: meta?.status ?? 200,
2625
+ headers: headers2
2626
+ });
2627
+ }
2628
+ if (typeof value === "string") {
2629
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
2630
+ applyMeta(headers2);
2631
+ return new Response(value, {
2632
+ status: meta?.status ?? 200,
2633
+ headers: headers2
2634
+ });
2635
+ }
2636
+ if (typeof value === "number" || typeof value === "boolean") {
2637
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
2638
+ applyMeta(headers2);
2639
+ return new Response(String(value), {
2640
+ status: meta?.status ?? 200,
2641
+ headers: headers2
2642
+ });
2643
+ }
2644
+ const body = JSON.stringify(value);
2645
+ const headers = new Headers({ "Content-Type": "application/json" });
2646
+ applyMeta(headers);
2647
+ return new Response(body, {
2648
+ status: meta?.status ?? 200,
2649
+ headers
2650
+ });
2651
+ }
2652
+
2653
+ // src/injection/injectParams.ts
2654
+ init_resolveInjection();
2655
+ function getBuiltinInjectionValue(type, ctx, body) {
2656
+ switch (type) {
2657
+ case "query":
2658
+ return queryToObject(ctx.query);
2659
+ case "params":
2660
+ return ctx.params;
2661
+ case "headers":
2662
+ return ctx.headers;
2663
+ case "context":
2664
+ return ctx;
2665
+ case "cookies":
2666
+ return ctx.cookies;
2667
+ case "ip":
2668
+ return ctx.ip;
2669
+ case "ua":
2670
+ return ctx.ua;
2671
+ case "body":
2672
+ return body;
2673
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
2674
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
2675
+ case "form":
2676
+ return body;
2677
+ case "files":
2678
+ if (body && typeof body === "object" && "files" in body) {
2679
+ return body.files;
2680
+ }
2681
+ return [];
2682
+ case "fields":
2683
+ if (body && typeof body === "object" && "fields" in body) {
2684
+ return body.fields;
2685
+ }
2686
+ return {};
2687
+ default:
2688
+ return void 0;
2689
+ }
2690
+ }
2691
+ async function injectParamsAsync(handler, ctx, body, injectors) {
2692
+ const injections = resolveInjection(handler);
2693
+ if (injections.length === 0) {
2694
+ return await handler();
2695
+ }
2696
+ const args = await Promise.all(
2697
+ injections.map(async (injection) => {
2698
+ if (injection.type !== "unknown") {
2699
+ return getBuiltinInjectionValue(injection.type, ctx, body);
2700
+ }
2701
+ if (injectors && injection.name in injectors) {
2702
+ return await injectors[injection.name](ctx);
2703
+ }
2704
+ return void 0;
2705
+ })
2706
+ );
2707
+ return await handler(...args);
2708
+ }
2709
+
2710
+ // src/runtime/invokeHandler.ts
2711
+ function wrapResult(result, ctx) {
2712
+ if (result instanceof Response) return result;
2713
+ const responseConfig = ctx.config.response;
2714
+ const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
2715
+ return okFn(result);
2716
+ }
2717
+ function mergeMeta(response, meta) {
2718
+ const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
2719
+ if (!hasMeta) return response;
2720
+ const headers = new Headers(response.headers);
2721
+ for (const [key, value] of Object.entries(meta.headers)) {
2722
+ headers.set(key, value);
2723
+ }
2724
+ for (const cookie of meta.setCookies) {
2725
+ headers.append("set-cookie", cookie);
2726
+ }
2727
+ return new Response(response.body, {
2728
+ status: meta.status ?? response.status,
2729
+ headers
2730
+ });
2731
+ }
2732
+ async function compose(middlewares, ctx, finalHandler) {
2733
+ const meta = ctx.meta;
2734
+ let index = -1;
2735
+ async function dispatch(i) {
2736
+ if (i <= index) {
2737
+ throw new Error("next() called multiple times");
2738
+ }
2739
+ index = i;
2740
+ if (i >= middlewares.length) {
2741
+ return await finalHandler();
2742
+ }
2743
+ const mw = middlewares[i];
2744
+ let innerResponse;
2745
+ const next = async () => {
2746
+ innerResponse = await dispatch(i + 1);
2747
+ return innerResponse;
2748
+ };
2749
+ const result = await mw(ctx, next);
2750
+ if (result instanceof Response) {
2751
+ return mergeMeta(result, meta);
2752
+ }
2753
+ if (innerResponse !== void 0) {
2754
+ return innerResponse;
2755
+ }
2756
+ throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
2757
+ }
2758
+ return await dispatch(0);
3067
2759
  }
3068
-
3069
- // src/runtime/resolveInput.ts
3070
- init_inputType();
3071
- async function resolveInput(method, request) {
3072
- const inputType = getInputTypeForMethod(method);
3073
- if (inputType === "body") {
3074
- const contentType = request.headers.get("content-type") ?? "";
3075
- if (contentType.includes("multipart/form-data")) {
3076
- return parseMultipart(request);
3077
- }
3078
- if (contentType.includes("application/x-www-form-urlencoded")) {
3079
- const text2 = await request.text();
3080
- if (text2.trim() === "") return null;
3081
- const params = new URLSearchParams(text2);
3082
- const obj = {};
3083
- for (const [key, value] of params) {
3084
- obj[key] = value;
3085
- }
3086
- return obj;
2760
+ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
2761
+ const meta = ctx.meta;
2762
+ const pickSseAndAutoClose = () => {
2763
+ const sseWriter = ctx.__sseWriter;
2764
+ if (!sseWriter) return null;
2765
+ if (!sseWriter.closed && !sseWriter.aborted) {
2766
+ sseWriter.close();
3087
2767
  }
3088
- const text = await request.text();
3089
- if (text.trim() === "") {
3090
- return null;
2768
+ return mergeMeta(sseWriter.response, meta);
2769
+ };
2770
+ const autoCloseSseOnError = () => {
2771
+ const sseWriter = ctx.__sseWriter;
2772
+ if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
2773
+ sseWriter.close();
3091
2774
  }
3092
- const result = parseJsonBody(text);
3093
- if (!result.success) {
3094
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
3095
- {
3096
- path: "body",
3097
- code: "INVALID_FORMAT",
3098
- expected: "JSON",
3099
- received: "text",
3100
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
3101
- }
3102
- ]);
2775
+ };
2776
+ if (!middlewares || middlewares.length === 0) {
2777
+ try {
2778
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
2779
+ const sseResponse = pickSseAndAutoClose();
2780
+ if (sseResponse) return sseResponse;
2781
+ return toResponse(wrapResult(result, ctx), meta);
2782
+ } catch (err) {
2783
+ autoCloseSseOnError();
2784
+ throw err;
3103
2785
  }
3104
- return result.data;
3105
2786
  }
3106
- const url = new URL(request.url);
3107
- return queryToObject(url.searchParams);
2787
+ const finalHandler = async () => {
2788
+ try {
2789
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
2790
+ const sseResponse = pickSseAndAutoClose();
2791
+ if (sseResponse) return sseResponse;
2792
+ return toResponse(wrapResult(result, ctx), meta);
2793
+ } catch (err) {
2794
+ autoCloseSseOnError();
2795
+ throw err;
2796
+ }
2797
+ };
2798
+ return await compose(middlewares, ctx, finalHandler);
3108
2799
  }
3109
2800
 
3110
2801
  // src/response/sendNodeResponse.ts
@@ -3131,6 +2822,94 @@ async function sendNodeResponse(response, res) {
3131
2822
  res.end();
3132
2823
  }
3133
2824
 
2825
+ // src/validator/validateInput.ts
2826
+ init_schemaName();
2827
+ var moduleCache = /* @__PURE__ */ new Map();
2828
+ function invalidateSchemaCache() {
2829
+ moduleCache.clear();
2830
+ }
2831
+ async function loadSchemaModule(schemaPath) {
2832
+ let mod = moduleCache.get(schemaPath);
2833
+ if (!mod) {
2834
+ mod = await importWithCacheBust(schemaPath, isDevOnDemandEnabled());
2835
+ moduleCache.set(schemaPath, mod);
2836
+ }
2837
+ return mod;
2838
+ }
2839
+ async function validateInput(schemaPath, method, inputType, input) {
2840
+ const schemaName = getSchemaName(method, inputType);
2841
+ const schemaKey = `${schemaName}Schema`;
2842
+ let mod;
2843
+ try {
2844
+ mod = await loadSchemaModule(schemaPath);
2845
+ } catch (err) {
2846
+ const reason = err instanceof Error ? err.message : String(err);
2847
+ throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
2848
+ }
2849
+ const schema = mod[schemaKey];
2850
+ if (schema === void 0 || schema === null) {
2851
+ const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2852
+ return { valid: true, issues: [], data };
2853
+ }
2854
+ if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2855
+ throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2856
+ }
2857
+ const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2858
+ const zodSchema = schema;
2859
+ const result = zodSchema.safeParse(inputObj);
2860
+ if (result.success) {
2861
+ const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2862
+ return { valid: true, issues: [], data };
2863
+ }
2864
+ const issues = mapZodIssues(result.error);
2865
+ return { valid: false, issues, data: inputObj };
2866
+ }
2867
+ function mapZodIssues(error) {
2868
+ return error.issues.map((issue) => {
2869
+ const code = mapZodCode(issue.code, issue.message);
2870
+ const path14 = issue.path.map(String).join(".") || "";
2871
+ return {
2872
+ path: path14,
2873
+ code,
2874
+ expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2875
+ received: issue.received ?? mapReceivedFromMessage(issue.message),
2876
+ message: issue.message
2877
+ };
2878
+ });
2879
+ }
2880
+ function mapZodCode(zodCode, message) {
2881
+ switch (zodCode) {
2882
+ case "invalid_type":
2883
+ case "invalid_union":
2884
+ case "invalid_union_discriminator":
2885
+ return "TYPE_MISMATCH";
2886
+ case "unrecognized_keys":
2887
+ return "INVALID_FORMAT";
2888
+ case "invalid_value":
2889
+ case "invalid_string":
2890
+ case "too_small":
2891
+ case "too_big":
2892
+ case "invalid_intersection_types":
2893
+ case "not_multiple_of":
2894
+ return "INVALID_VALUE";
2895
+ case "custom":
2896
+ return "INVALID_VALUE";
2897
+ default:
2898
+ if (message.includes("Required") || message.includes("required")) {
2899
+ return "MISSING_FIELD";
2900
+ }
2901
+ return "INVALID_VALUE";
2902
+ }
2903
+ }
2904
+ function mapExpectedFromMessage(message) {
2905
+ const match = message.match(/Expected\s+(\w+)/i);
2906
+ return match ? match[1].toLowerCase() : "unknown";
2907
+ }
2908
+ function mapReceivedFromMessage(message) {
2909
+ const match = message.match(/received\s+(\w+)/i);
2910
+ return match ? match[1].toLowerCase() : "unknown";
2911
+ }
2912
+
3134
2913
  // src/server/createServer.ts
3135
2914
  init_inputType();
3136
2915
 
@@ -3152,9 +2931,9 @@ function getClientIp(req) {
3152
2931
  }
3153
2932
 
3154
2933
  // src/server/handleWsUpgrade.ts
3155
- import fs10 from "fs";
2934
+ import fs9 from "fs";
3156
2935
  import { WebSocketServer, WebSocket } from "ws";
3157
- import path10 from "path";
2936
+ import path9 from "path";
3158
2937
 
3159
2938
  // src/errors/formatErrorResponse.ts
3160
2939
  function formatErrorResponse(error) {
@@ -3229,6 +3008,71 @@ function buildErrorResponse(err) {
3229
3008
  }
3230
3009
  }
3231
3010
 
3011
+ // src/middleware/loadMiddlewares.ts
3012
+ var middlewareCache = /* @__PURE__ */ new Map();
3013
+ function invalidateMiddlewareCache() {
3014
+ middlewareCache.clear();
3015
+ }
3016
+ function getCachedMiddlewares(absPath) {
3017
+ return middlewareCache.get(absPath);
3018
+ }
3019
+ function setCachedMiddlewares(absPath, bundle) {
3020
+ middlewareCache.set(absPath, bundle);
3021
+ }
3022
+ async function loadMiddlewaresFile(filePath) {
3023
+ try {
3024
+ const module = await importWithCacheBust(filePath);
3025
+ const middlewares = module.default ?? module.middlewares ?? [];
3026
+ if (!Array.isArray(middlewares)) {
3027
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
3028
+ return { middlewares: [], injectors: {} };
3029
+ }
3030
+ const validMiddlewares = middlewares.filter((m) => {
3031
+ if (typeof m !== "function") {
3032
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
3033
+ return false;
3034
+ }
3035
+ return true;
3036
+ });
3037
+ const injectors = module.injectors ?? {};
3038
+ if (typeof injectors !== "object" || injectors === null) {
3039
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
3040
+ return { middlewares: validMiddlewares, injectors: {} };
3041
+ }
3042
+ const validInjectors = {};
3043
+ for (const [name, injector] of Object.entries(injectors)) {
3044
+ if (typeof injector !== "function") {
3045
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
3046
+ continue;
3047
+ }
3048
+ validInjectors[name] = injector;
3049
+ }
3050
+ return { middlewares: validMiddlewares, injectors: validInjectors };
3051
+ } catch {
3052
+ return { middlewares: [], injectors: {} };
3053
+ }
3054
+ }
3055
+ async function loadMergedMiddlewares(middlewarePaths) {
3056
+ if (middlewarePaths.length === 0) return void 0;
3057
+ const mergedMiddlewares = [];
3058
+ const mergedInjectors = {};
3059
+ for (const absMwPath of middlewarePaths) {
3060
+ let bundle = getCachedMiddlewares(absMwPath);
3061
+ if (bundle === void 0) {
3062
+ bundle = await loadMiddlewaresFile(absMwPath);
3063
+ setCachedMiddlewares(absMwPath, bundle);
3064
+ }
3065
+ mergedMiddlewares.push(...bundle.middlewares);
3066
+ for (const [name, injector] of Object.entries(bundle.injectors)) {
3067
+ mergedInjectors[name] = injector;
3068
+ }
3069
+ }
3070
+ if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
3071
+ return void 0;
3072
+ }
3073
+ return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
3074
+ }
3075
+
3232
3076
  // src/runtime/wsHandler.ts
3233
3077
  function wrapWsSocket(rawSocket) {
3234
3078
  return {
@@ -3256,7 +3100,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
3256
3100
  const dist = getDevDist();
3257
3101
  if (dist) {
3258
3102
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3259
- if (sourcePath && fs10.existsSync(sourcePath)) {
3103
+ if (sourcePath && fs9.existsSync(sourcePath)) {
3260
3104
  await ensureCompiled(sourcePath, rootDir, dist);
3261
3105
  }
3262
3106
  }
@@ -3336,7 +3180,7 @@ function attachWebSocket(options) {
3336
3180
  const finalHandler = async () => {
3337
3181
  let handlers;
3338
3182
  try {
3339
- const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3183
+ const absoluteFilePath = path9.resolve(rootDir, route.filePath);
3340
3184
  handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
3341
3185
  } catch (err) {
3342
3186
  const reason = err instanceof Error ? err.message : String(err);
@@ -3435,15 +3279,15 @@ function limitStreamSize(stream, maxSize) {
3435
3279
  }
3436
3280
  });
3437
3281
  }
3438
- function findAllowedMethods(routes, path15) {
3282
+ function findAllowedMethods(routes, path14) {
3439
3283
  const methods = /* @__PURE__ */ new Set();
3440
3284
  for (const route of routes) {
3441
- if (route.urlPath === path15) {
3285
+ if (route.urlPath === path14) {
3442
3286
  methods.add(route.method);
3443
3287
  continue;
3444
3288
  }
3445
3289
  if (route.isDynamic) {
3446
- const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
3290
+ const params = matchDynamicPath(route.urlPath, path14, route.paramNames, route.isCatchAll);
3447
3291
  if (params !== null) {
3448
3292
  methods.add(route.method);
3449
3293
  }
@@ -3529,7 +3373,7 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3529
3373
  }
3530
3374
  ctx.params = match.params;
3531
3375
  const { route } = match;
3532
- const absoluteFilePath = path11.resolve(rootDir, route.filePath);
3376
+ const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3533
3377
  const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
3534
3378
  const input = await resolveInput(route.method, request);
3535
3379
  const inputType = getInputTypeForMethod(route.method);
@@ -3590,229 +3434,6 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3590
3434
  }
3591
3435
  }
3592
3436
 
3593
- // src/testServer.ts
3594
- var DEFAULT_PATTERNS = ["src/api/**/*.ts"];
3595
- var DEFAULT_BODY_LIMIT2 = 10 * 1024 * 1024;
3596
- async function createTestServer(options) {
3597
- const {
3598
- rootDir,
3599
- patterns = DEFAULT_PATTERNS,
3600
- dist,
3601
- cors: cors2 = false,
3602
- helmet: helmet2 = false,
3603
- logger: logger2 = false,
3604
- middlewares,
3605
- injectors,
3606
- onError,
3607
- config,
3608
- bodyLimit = DEFAULT_BODY_LIMIT2
3609
- } = options;
3610
- const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3611
- const sorted = sortRoutes(routes);
3612
- const schemaDist = dist ? path12.isAbsolute(dist) ? dist : path12.resolve(rootDir, dist) : await fs11.mkdtemp(path12.join(os.tmpdir(), "faapi-test-schema-"));
3613
- await generateSchemaFiles(sorted, rootDir, schemaDist);
3614
- const { server } = createServer({
3615
- routes: sorted,
3616
- rootDir,
3617
- dist: schemaDist,
3618
- cors: cors2,
3619
- helmet: helmet2,
3620
- logger: logger2,
3621
- middlewares,
3622
- injectors,
3623
- onError,
3624
- config,
3625
- wsRoutes,
3626
- bodyLimit
3627
- });
3628
- const baseUrl = await listenOnRandomPort(server);
3629
- let closed = false;
3630
- const testServer = {
3631
- server,
3632
- baseUrl,
3633
- routes: sorted,
3634
- wsRoutes,
3635
- schemaDist,
3636
- async close() {
3637
- if (closed) return;
3638
- closed = true;
3639
- const s = server;
3640
- s.closeAllConnections?.();
3641
- s.closeIdleConnections?.();
3642
- await new Promise((resolve) => {
3643
- server.close(() => resolve());
3644
- });
3645
- await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3646
- });
3647
- invalidateSchemaCache();
3648
- }
3649
- };
3650
- return testServer;
3651
- }
3652
- function listenOnRandomPort(server) {
3653
- return new Promise((resolve, reject) => {
3654
- server.listen(0, () => {
3655
- const addr = server.address();
3656
- if (typeof addr === "object" && addr !== null) {
3657
- resolve(`http://localhost:${addr.port}`);
3658
- } else {
3659
- reject(new Error("Failed to get server address"));
3660
- }
3661
- });
3662
- server.on("error", (err) => {
3663
- reject(err);
3664
- });
3665
- });
3666
- }
3667
-
3668
- // src/wsTestClient.ts
3669
- import { WebSocket as WebSocket2 } from "ws";
3670
- var MessageQueue = class {
3671
- queue = [];
3672
- waiters = [];
3673
- listener;
3674
- constructor(ws) {
3675
- this.listener = (data) => {
3676
- const msg = normalizeRawData(data);
3677
- const waiter = this.waiters.shift();
3678
- if (waiter) {
3679
- waiter(msg);
3680
- } else {
3681
- this.queue.push(msg);
3682
- }
3683
- };
3684
- ws.on("message", this.listener);
3685
- }
3686
- /**
3687
- * 取下一条消息
3688
- *
3689
- * 队列有则立即 resolve,无则注册 waiter 等待下一条 'message' 事件。
3690
- * 超时未到 → reject('WebSocket message timeout'),waiter 被清理。
3691
- *
3692
- * @param timeout 超时毫秒,默认 2000
3693
- */
3694
- next(timeout = 2e3) {
3695
- return new Promise((resolve, reject) => {
3696
- const wrapped = (msg2) => {
3697
- clearTimeout(timer);
3698
- resolve(msg2);
3699
- };
3700
- const timer = setTimeout(() => {
3701
- const idx = this.waiters.indexOf(wrapped);
3702
- if (idx >= 0) this.waiters.splice(idx, 1);
3703
- reject(new Error("WebSocket message timeout"));
3704
- }, timeout);
3705
- const msg = this.queue.shift();
3706
- if (msg !== void 0) {
3707
- wrapped(msg);
3708
- } else {
3709
- this.waiters.push(wrapped);
3710
- }
3711
- });
3712
- }
3713
- };
3714
- function normalizeRawData(data) {
3715
- if (Buffer.isBuffer(data)) {
3716
- return data.toString("utf8");
3717
- }
3718
- if (Array.isArray(data)) {
3719
- return Buffer.concat(data).toString("utf8");
3720
- }
3721
- return Buffer.from(data).toString("utf8");
3722
- }
3723
- function waitForWsOpen(ws, timeout = 2e3) {
3724
- return new Promise((resolve, reject) => {
3725
- const timer = setTimeout(() => {
3726
- reject(new Error("WebSocket open timeout"));
3727
- }, timeout);
3728
- const cleanup = () => {
3729
- clearTimeout(timer);
3730
- ws.removeListener("open", onOpen);
3731
- ws.removeListener("error", onError);
3732
- ws.removeListener("close", onClose);
3733
- };
3734
- const onOpen = () => {
3735
- cleanup();
3736
- resolve();
3737
- };
3738
- const onError = (err) => {
3739
- cleanup();
3740
- reject(err);
3741
- };
3742
- const onClose = () => {
3743
- cleanup();
3744
- reject(new Error("WebSocket closed before open"));
3745
- };
3746
- ws.once("open", onOpen);
3747
- ws.once("error", onError);
3748
- ws.once("close", onClose);
3749
- });
3750
- }
3751
- async function connectWs(baseUrl, pathname, options = {}) {
3752
- const { timeout = 2e3, headers, protocols } = options;
3753
- const wsBaseUrl = baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
3754
- const url = `${wsBaseUrl}${pathname}`;
3755
- const ws = new WebSocket2(url, protocols, headers ? { headers } : void 0);
3756
- const queue = new MessageQueue(ws);
3757
- try {
3758
- await waitForWsOpen(ws, timeout);
3759
- } catch (err) {
3760
- if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3761
- ws.close();
3762
- }
3763
- throw err;
3764
- }
3765
- let closed = false;
3766
- return {
3767
- ws,
3768
- queue,
3769
- async close() {
3770
- if (closed) return;
3771
- closed = true;
3772
- if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3773
- ws.close();
3774
- }
3775
- await new Promise((resolve) => {
3776
- const timer = setTimeout(resolve, 1e3);
3777
- ws.once("close", () => {
3778
- clearTimeout(timer);
3779
- resolve();
3780
- });
3781
- });
3782
- }
3783
- };
3784
- }
3785
-
3786
- // src/cli/createAppCore.ts
3787
- import fs13 from "fs";
3788
- import path14 from "path";
3789
- import { PassThrough } from "stream";
3790
-
3791
- // src/router/detectRouteConflicts.ts
3792
- function detectRouteConflicts(routes) {
3793
- const map = /* @__PURE__ */ new Map();
3794
- for (const route of routes) {
3795
- const key = `${route.method} ${route.urlPath}`;
3796
- const existing = map.get(key);
3797
- if (existing) {
3798
- existing.files.push(route.filePath);
3799
- } else {
3800
- map.set(key, {
3801
- method: route.method,
3802
- urlPath: route.urlPath,
3803
- files: [route.filePath]
3804
- });
3805
- }
3806
- }
3807
- const conflicts = [];
3808
- for (const conflict of map.values()) {
3809
- if (conflict.files.length > 1) {
3810
- conflicts.push(conflict);
3811
- }
3812
- }
3813
- return conflicts;
3814
- }
3815
-
3816
3437
  // src/server/startServer.ts
3817
3438
  function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
3818
3439
  if (handlerWrappers.length > 0) {
@@ -3842,8 +3463,8 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
3842
3463
  }
3843
3464
 
3844
3465
  // src/cli/generateRoutes.ts
3845
- import fs12 from "fs";
3846
- import path13 from "path";
3466
+ import fs10 from "fs";
3467
+ import path11 from "path";
3847
3468
  async function hydrateRoutes(manifest) {
3848
3469
  const hydrateRoute = (serialized) => ({
3849
3470
  method: serialized.method,
@@ -3931,6 +3552,15 @@ var DEFAULT_DIST = "dist";
3931
3552
  var DEFAULT_PORT = 3e3;
3932
3553
  var ROUTES_FILE = "faapi-routes.js";
3933
3554
  var PATTERNS = ["src/api/**/*.ts"];
3555
+ var currentApp = null;
3556
+ function getApp() {
3557
+ if (!currentApp) {
3558
+ throw new Error(
3559
+ "[faapi] No app instance. Call createProdApp() / createDevApp() first, or run `faapi dev` / `node dist/main`."
3560
+ );
3561
+ }
3562
+ return currentApp;
3563
+ }
3934
3564
  var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
3935
3565
  "cors",
3936
3566
  "lifecycle",
@@ -3950,8 +3580,8 @@ function isFaapiConfigKey(key) {
3950
3580
  async function createAppBase(options) {
3951
3581
  const rootDir = options?.rootDir ?? process.cwd();
3952
3582
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
3953
- const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE);
3954
- if (!fs13.existsSync(routesPath)) {
3583
+ const routesPath = path12.resolve(rootDir, dist, ROUTES_FILE);
3584
+ if (!fs11.existsSync(routesPath)) {
3955
3585
  throw new Error(
3956
3586
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
3957
3587
  );
@@ -4088,11 +3718,7 @@ async function createAppBase(options) {
4088
3718
  reject(new Error("No request handler found"));
4089
3719
  return;
4090
3720
  }
4091
- const mockReq = new PassThrough({
4092
- read() {
4093
- this.push(null);
4094
- }
4095
- });
3721
+ const mockReq = Readable3.from(body !== void 0 ? [Buffer.from(JSON.stringify(body))] : []);
4096
3722
  mockReq.method = method;
4097
3723
  mockReq.url = `${reqPath}${queryStr}`;
4098
3724
  mockReq.headers = {
@@ -4101,9 +3727,6 @@ async function createAppBase(options) {
4101
3727
  "content-type": body !== void 0 ? "application/json" : void 0
4102
3728
  };
4103
3729
  mockReq.socket = { remoteAddress: "127.0.0.1" };
4104
- if (body !== void 0) {
4105
- mockReq.push(JSON.stringify(body));
4106
- }
4107
3730
  handler(
4108
3731
  mockReq,
4109
3732
  mockRes
@@ -4125,17 +3748,20 @@ async function createAppBase(options) {
4125
3748
  }
4126
3749
  if (!server.listening) {
4127
3750
  app.server = null;
3751
+ if (currentApp === app) currentApp = null;
4128
3752
  return;
4129
3753
  }
4130
3754
  return new Promise((resolve) => {
4131
3755
  server.close((err) => {
4132
3756
  if (err) console.error("Error closing server:", err);
4133
3757
  app.server = null;
3758
+ if (currentApp === app) currentApp = null;
4134
3759
  resolve();
4135
3760
  });
4136
3761
  });
4137
3762
  }
4138
3763
  };
3764
+ currentApp = app;
4139
3765
  const ctx = {
4140
3766
  rootDir,
4141
3767
  dist,
@@ -4155,6 +3781,180 @@ async function createAppBase(options) {
4155
3781
  return { app, ctx };
4156
3782
  }
4157
3783
 
3784
+ // src/router/scanRoutes.ts
3785
+ import fg2 from "fast-glob";
3786
+ import path13 from "path";
3787
+ import fs12 from "fs";
3788
+
3789
+ // src/router/constants.ts
3790
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
3791
+ var HTTP_METHOD_SET = new Set(HTTP_METHODS);
3792
+
3793
+ // src/utils/normalizePath.ts
3794
+ function normalizePath(path14) {
3795
+ if (!path14) return "";
3796
+ let result = path14.replace(/\\/g, "/");
3797
+ result = result.replace(/\/+/g, "/");
3798
+ result = result.replace(/\/+$/, "");
3799
+ if (result && !result.startsWith("/")) {
3800
+ result = "/" + result;
3801
+ }
3802
+ return result;
3803
+ }
3804
+
3805
+ // src/router/parseRouteFile.ts
3806
+ function dynamicSegmentToParam(segment) {
3807
+ const match = segment.match(/^\[(.+)\]$/);
3808
+ if (match) {
3809
+ return ":" + match[1];
3810
+ }
3811
+ return segment;
3812
+ }
3813
+ function extractParamNames(urlPath) {
3814
+ const params = [];
3815
+ const segments = urlPath.split("/");
3816
+ for (const segment of segments) {
3817
+ if (segment.startsWith(":...")) {
3818
+ params.push(segment.slice(4));
3819
+ } else if (segment.startsWith(":")) {
3820
+ params.push(segment.slice(1));
3821
+ }
3822
+ }
3823
+ return params;
3824
+ }
3825
+ function isCatchAllSegment(segment) {
3826
+ return /^\[\.\.\..+\]$/.test(segment);
3827
+ }
3828
+ function isRouteGroup(segment) {
3829
+ return /^\(.+\)$/.test(segment);
3830
+ }
3831
+ function filePathToUrlPath(filePath) {
3832
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
3833
+ const lastSlashIndex = withoutPrefix.lastIndexOf("/");
3834
+ const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
3835
+ if (!dirPath) {
3836
+ return "";
3837
+ }
3838
+ const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
3839
+ return normalizePath(segments.join("/"));
3840
+ }
3841
+
3842
+ // src/router/scanRoutes.ts
3843
+ var HTTP_OR_WS_EXPORT_RE = new RegExp(
3844
+ String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
3845
+ "g"
3846
+ );
3847
+ function extractExportsFromSource(source) {
3848
+ const names = /* @__PURE__ */ new Set();
3849
+ let match;
3850
+ HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
3851
+ while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
3852
+ names.add(match[1]);
3853
+ }
3854
+ return names;
3855
+ }
3856
+ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
3857
+ const routeDir = path13.dirname(routeFilePath);
3858
+ const resolvedRoot = path13.resolve(rootDir);
3859
+ const paths = [];
3860
+ let currentDir = path13.resolve(rootDir, routeDir);
3861
+ while (true) {
3862
+ if (dist) {
3863
+ const mwTsPath = path13.join(currentDir, "middlewares.ts");
3864
+ const mwJsPath = path13.join(currentDir, "middlewares.js");
3865
+ const absTsPath = path13.resolve(rootDir, mwTsPath);
3866
+ const absJsPath = path13.resolve(rootDir, mwJsPath);
3867
+ const absMwPath = fs12.existsSync(absTsPath) ? absTsPath : fs12.existsSync(absJsPath) ? absJsPath : null;
3868
+ if (absMwPath) {
3869
+ const relMwPath = path13.relative(rootDir, absMwPath);
3870
+ const prodAbsPath = path13.resolve(rootDir, toProdFilePath(relMwPath, dist));
3871
+ paths.push(prodAbsPath);
3872
+ }
3873
+ } else {
3874
+ for (const ext of [".ts", ".js"]) {
3875
+ const mwPath = path13.join(currentDir, `middlewares${ext}`);
3876
+ const absMwPath = path13.resolve(rootDir, mwPath);
3877
+ if (fs12.existsSync(absMwPath)) {
3878
+ paths.push(absMwPath);
3879
+ break;
3880
+ }
3881
+ }
3882
+ }
3883
+ if (currentDir === resolvedRoot) break;
3884
+ const parentDir = path13.dirname(currentDir);
3885
+ if (parentDir === currentDir) break;
3886
+ currentDir = parentDir;
3887
+ }
3888
+ paths.reverse();
3889
+ return paths;
3890
+ }
3891
+ function toProdFilePath(filePath, dist) {
3892
+ let rel = filePath.replace(/\\/g, "/");
3893
+ if (rel.startsWith("src/")) {
3894
+ rel = rel.slice(4);
3895
+ }
3896
+ const jsPath = rel.replace(/\.ts$/, ".js");
3897
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
3898
+ }
3899
+ async function scanRoutes(rootDir, patterns, dist) {
3900
+ const files = await fg2(patterns, {
3901
+ cwd: rootDir,
3902
+ onlyFiles: true,
3903
+ absolute: false
3904
+ });
3905
+ const routes = [];
3906
+ const wsRoutes = [];
3907
+ for (const file of files) {
3908
+ const normalizedFile = file.replace(/\\/g, "/");
3909
+ const fileName = normalizedFile.split("/").pop();
3910
+ if (fileName === "handler.ts" || fileName === "handler.js") {
3911
+ const absPath = path13.resolve(rootDir, normalizedFile);
3912
+ const urlPath = filePathToUrlPath(normalizedFile);
3913
+ const paramNames = extractParamNames(urlPath);
3914
+ const isDynamic = paramNames.length > 0;
3915
+ const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
3916
+ let middlewarePaths;
3917
+ let middlewareBundle;
3918
+ if (dist) {
3919
+ middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
3920
+ } else {
3921
+ const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
3922
+ middlewareBundle = await loadMergedMiddlewares(mwPaths);
3923
+ }
3924
+ const source = await fs12.promises.readFile(absPath, "utf8").catch(() => "");
3925
+ const exportNames = extractExportsFromSource(source);
3926
+ const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
3927
+ for (const method of methods) {
3928
+ routes.push({
3929
+ method,
3930
+ urlPath,
3931
+ filePath: normalizedFile,
3932
+ paramNames,
3933
+ isDynamic,
3934
+ isCatchAll: isCatchAll || void 0,
3935
+ middlewarePaths,
3936
+ middlewares: middlewareBundle?.middlewares,
3937
+ injectors: middlewareBundle?.injectors
3938
+ });
3939
+ }
3940
+ if (exportNames.has("WS")) {
3941
+ wsRoutes.push({
3942
+ urlPath,
3943
+ filePath: normalizedFile,
3944
+ paramNames,
3945
+ isDynamic,
3946
+ isCatchAll: isCatchAll || void 0,
3947
+ middlewarePaths,
3948
+ middlewares: middlewareBundle?.middlewares,
3949
+ injectors: middlewareBundle?.injectors
3950
+ });
3951
+ }
3952
+ continue;
3953
+ }
3954
+ }
3955
+ return { routes, wsRoutes };
3956
+ }
3957
+
4158
3958
  // src/cli/createDevApp.ts
4159
3959
  init_createProgram();
4160
3960
  async function createDevApp(options) {
@@ -4188,30 +3988,25 @@ async function createProdApp(options) {
4188
3988
  export {
4189
3989
  FaapiError,
4190
3990
  InternalError,
4191
- MessageQueue,
4192
3991
  MethodNotAllowedError,
4193
3992
  ModuleLoadError,
4194
3993
  RouteNotFoundError,
4195
3994
  SchemaExtractionError,
4196
3995
  ValidationError,
4197
3996
  collectRouteSchemaSources,
4198
- connectWs,
4199
3997
  cors,
4200
3998
  createProdApp as createApp,
4201
- createContext,
4202
3999
  createDevApp,
4203
4000
  createProdApp,
4204
4001
  createProgram,
4205
- createTestServer,
4206
4002
  extractTypeInfo,
4003
+ getApp,
4207
4004
  getInputTypeForMethod,
4208
4005
  helmet,
4209
4006
  invalidateProgramCache,
4210
- invokeHandler,
4211
4007
  loadConfig,
4212
4008
  loadEnv,
4213
4009
  logger,
4214
- resolveTypeNode,
4215
- waitForWsOpen
4010
+ resolveTypeNode
4216
4011
  };
4217
4012
  //# sourceMappingURL=index.js.map