@faapi/faapi 1.5.0 → 2.0.1-canary.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,1358 +1672,800 @@ 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);
1839
+
1840
+ // src/cli/compileOnDemand.ts
1841
+ import path8 from "path";
1842
+ import fs7 from "fs";
1843
+
1844
+ // src/cli/compileDevRoutes.ts
1845
+ import path6 from "path";
1846
+ import fs5 from "fs";
1847
+ import fg from "fast-glob";
1848
+
1849
+ // src/cli/aliasPlugin.ts
1850
+ import path5 from "path";
1851
+ import fs4 from "fs";
1852
+
1853
+ // src/utils/resolveAlias.ts
1854
+ function resolveAlias(specifier, config) {
1855
+ const candidates = [];
1856
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1857
+ const wildcardIndex = pattern.indexOf("*");
1858
+ if (wildcardIndex === -1) {
1859
+ if (specifier === pattern) {
1860
+ candidates.push(...targets);
1861
+ }
1862
+ continue;
1863
+ }
1864
+ const prefix = pattern.slice(0, wildcardIndex);
1865
+ const suffix = pattern.slice(wildcardIndex + 1);
1866
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
1867
+ const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
1868
+ for (const target of targets) {
1869
+ candidates.push(target.replace("*", captured));
1870
+ }
1911
1871
  }
1912
- };
1913
- const extend = config?.extendContext;
1914
- if (typeof extend === "function") {
1915
- extend(ctx);
1916
1872
  }
1917
- return ctx;
1873
+ return candidates;
1918
1874
  }
1919
1875
 
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;
1876
+ // src/utils/readTsconfig.ts
1877
+ import ts6 from "typescript";
1878
+ import path4 from "path";
1879
+ import fs3 from "fs";
1880
+ function readTsconfig(rootDir) {
1881
+ const tsconfigPath = path4.resolve(rootDir, "tsconfig.json");
1882
+ if (!fs3.existsSync(tsconfigPath)) return null;
1883
+ const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
1884
+ if (configFile.error || !configFile.config) return null;
1885
+ const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
1886
+ const baseUrl = parsed.options.baseUrl ?? rootDir;
1887
+ const rawPaths = parsed.options.paths;
1888
+ if (!rawPaths) return null;
1889
+ const paths = {};
1890
+ for (const [pattern, targets] of Object.entries(rawPaths)) {
1891
+ paths[pattern] = targets.map((t) => path4.resolve(baseUrl, t));
1927
1892
  }
1928
- const proto = Object.getPrototypeOf(value);
1929
- return proto === null || proto === Object.prototype;
1893
+ return { baseUrl, paths };
1930
1894
  }
1931
1895
 
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;
1896
+ // src/cli/aliasPlugin.ts
1897
+ function toProdExtension(filePath) {
1898
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
1899
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
1900
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
1901
+ return filePath;
1902
+ }
1903
+ function toProdImportPath(sourceFile, importer) {
1904
+ const importerDir = path5.dirname(importer);
1905
+ let rel = path5.relative(importerDir, sourceFile);
1906
+ rel = rel.split(path5.sep).join("/");
1907
+ if (!rel.startsWith(".")) rel = "./" + rel;
1908
+ return toProdExtension(rel);
1909
+ }
1910
+ function toRealPath(p) {
1911
+ try {
1912
+ return fs4.realpathSync(p);
1913
+ } catch {
1914
+ return p;
1956
1915
  }
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 });
1916
+ }
1917
+ function isInsideDir(filePath, dir) {
1918
+ const rel = path5.relative(dir, filePath);
1919
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
1920
+ }
1921
+ var APP_DIR = "src";
1922
+ function toStrippedProdImportPath(sourceFile, rootDir) {
1923
+ const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
1924
+ const sourceReal = toRealPath(sourceFile);
1925
+ let rel = path5.relative(appDirAbs, sourceReal);
1926
+ rel = rel.split(path5.sep).join("/");
1927
+ if (!rel.startsWith(".")) rel = "./" + rel;
1928
+ return toProdExtension(rel);
1929
+ }
1930
+ var PROD_EXTS = [".js", ".mjs", ".cjs"];
1931
+ var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
1932
+ var INDEX_EXTS = [
1933
+ "/index.ts",
1934
+ "/index.tsx",
1935
+ "/index.js",
1936
+ "/index.jsx",
1937
+ "/index.mjs",
1938
+ "/index.cjs"
1939
+ ];
1940
+ function resolveRelativeSpecifier(importer, specifier) {
1941
+ const importerDir = path5.dirname(importer);
1942
+ const base = path5.resolve(importerDir, specifier);
1943
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
1944
+ return fs4.existsSync(base) ? base : null;
1962
1945
  }
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
- });
1946
+ if (/\.(ts|tsx|jsx)$/.test(specifier)) {
1947
+ return fs4.existsSync(base) ? base : null;
1970
1948
  }
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
- });
1949
+ for (const ext of SOURCE_EXTS) {
1950
+ const file = base + ext;
1951
+ if (fs4.existsSync(file)) return file;
1978
1952
  }
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
- });
1953
+ for (const indexExt of INDEX_EXTS) {
1954
+ const file = base + indexExt;
1955
+ if (fs4.existsSync(file)) return file;
1986
1956
  }
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
- });
1957
+ return null;
2019
1958
  }
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;
1959
+ function createAliasPlugin(config, options) {
1960
+ const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
1961
+ const appDirAbs = options?.rootDir ? toRealPath(path5.resolve(options.rootDir, APP_DIR)) : null;
1962
+ return {
1963
+ name: "faapi-alias",
1964
+ setup(build) {
1965
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
1966
+ let source;
1967
+ try {
1968
+ source = fs4.readFileSync(args.path, "utf8");
1969
+ } catch {
1970
+ return void 0;
1971
+ }
1972
+ const importer = args.path;
1973
+ const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
1974
+ let modified = false;
1975
+ const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
1976
+ if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
1977
+ return full;
1978
+ }
1979
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
1980
+ const resolved = resolveRelativeSpecifier(importer, specifier);
1981
+ if (resolved) {
1982
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
1983
+ return full;
1984
+ }
1985
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
1986
+ modified = true;
1987
+ return `${prefix}${quote}${toStrippedProdImportPath(
1988
+ resolved,
1989
+ options.rootDir
1990
+ )}${quote}`;
1991
+ }
1992
+ modified = true;
1993
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
1994
+ }
1995
+ return full;
1996
+ }
1997
+ const candidates = resolveAlias(specifier, config);
1998
+ for (const candidate of candidates) {
1999
+ for (const ext of SOURCE_EXTS) {
2000
+ const file = candidate + ext;
2001
+ if (fs4.existsSync(file)) {
2002
+ modified = true;
2003
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2004
+ return `${prefix}${quote}${toStrippedProdImportPath(
2005
+ file,
2006
+ options.rootDir
2007
+ )}${quote}`;
2008
+ }
2009
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2010
+ }
2011
+ }
2012
+ for (const indexExt of INDEX_EXTS) {
2013
+ const file = candidate + indexExt;
2014
+ if (fs4.existsSync(file)) {
2015
+ modified = true;
2016
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2017
+ return `${prefix}${quote}${toStrippedProdImportPath(
2018
+ file,
2019
+ options.rootDir
2020
+ )}${quote}`;
2021
+ }
2022
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2023
+ }
2024
+ }
2025
+ }
2026
+ return full;
2027
+ });
2028
+ if (!modified) return void 0;
2029
+ return { contents: newSource, loader: "default" };
2030
+ });
2031
+ }
2032
+ };
2033
+ }
2034
+ function buildAliasPlugins(rootDir) {
2035
+ const tsconfig = readTsconfig(rootDir);
2036
+ return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2031
2037
  }
2032
2038
 
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;
2039
+ // src/cli/compileDevRoutes.ts
2040
+ var APP_DIR2 = "src";
2041
+ async function compileDevRoutes(options) {
2042
+ const { rootDir, dist, files, logLevel = "silent" } = options;
2043
+ const entryPoints = files ?? await fg([`${APP_DIR2}/**/*.ts`], {
2044
+ cwd: rootDir,
2045
+ onlyFiles: true,
2046
+ absolute: true,
2047
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
2048
+ });
2049
+ if (entryPoints.length === 0) {
2050
+ return { compiledFiles: [] };
2068
2051
  }
2069
- }
2070
- async function injectParamsAsync(handler, ctx, body, injectors) {
2071
- const injections = resolveInjection(handler);
2072
- if (injections.length === 0) {
2073
- return await handler();
2052
+ const absDist = path6.resolve(rootDir, dist);
2053
+ await fs5.promises.mkdir(absDist, { recursive: true });
2054
+ const plugins = buildAliasPlugins(rootDir);
2055
+ const esbuild = await import("esbuild");
2056
+ const outbase = path6.resolve(rootDir, APP_DIR2);
2057
+ const result = await esbuild.build({
2058
+ entryPoints,
2059
+ outdir: absDist,
2060
+ outbase,
2061
+ bundle: false,
2062
+ platform: "node",
2063
+ format: "esm",
2064
+ sourcemap: true,
2065
+ packages: "external",
2066
+ plugins,
2067
+ logLevel,
2068
+ write: false
2069
+ });
2070
+ if (result.outputFiles) {
2071
+ await Promise.all(
2072
+ result.outputFiles.map(async (file) => {
2073
+ await fs5.promises.mkdir(path6.dirname(file.path), { recursive: true });
2074
+ const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2075
+ await fs5.promises.writeFile(tmp, file.contents);
2076
+ await fs5.promises.rename(tmp, file.path);
2077
+ })
2078
+ );
2074
2079
  }
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);
2080
+ return { compiledFiles: entryPoints };
2087
2081
  }
2088
2082
 
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);
2083
+ // src/cli/compileOnDemand.ts
2084
+ init_generateSchemaFiles();
2085
+ init_generateSchemaFiles();
2086
+ function isProductFresh(sourceAbsPath, productAbsPath) {
2087
+ try {
2088
+ const srcStat = fs7.statSync(sourceAbsPath);
2089
+ const prodStat = fs7.statSync(productAbsPath);
2090
+ return prodStat.mtimeMs >= srcStat.mtimeMs;
2091
+ } catch {
2092
+ return false;
2093
+ }
2095
2094
  }
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);
2095
+ var compiledFiles = /* @__PURE__ */ new Set();
2096
+ function clearCompiledFiles() {
2097
+ compiledFiles.clear();
2098
+ }
2099
+ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2100
+ if (compiledFiles.has(sourceAbsPath)) {
2101
+ return false;
2102
2102
  }
2103
- for (const cookie of meta.setCookies) {
2104
- headers.append("set-cookie", cookie);
2103
+ if (!fs7.existsSync(sourceAbsPath)) {
2104
+ return false;
2105
2105
  }
2106
- return new Response(response.body, {
2107
- status: meta.status ?? response.status,
2108
- headers
2106
+ const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2107
+ if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2108
+ compiledFiles.add(sourceAbsPath);
2109
+ return false;
2110
+ }
2111
+ await compileDevRoutes({
2112
+ rootDir,
2113
+ dist,
2114
+ files: [sourceAbsPath],
2115
+ logLevel: "silent"
2109
2116
  });
2117
+ compiledFiles.add(sourceAbsPath);
2118
+ return true;
2110
2119
  }
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);
2120
+ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2121
+ const rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2122
+ if (!rel.startsWith("src/")) return null;
2123
+ const relWithoutSrc = rel.slice(4);
2124
+ const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2125
+ return path8.resolve(rootDir, dist, jsRel);
2138
2126
  }
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);
2127
+ var generatedSchemas = /* @__PURE__ */ new Set();
2128
+ function clearGeneratedSchemas() {
2129
+ generatedSchemas.clear();
2178
2130
  }
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;
2131
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2132
+ if (generatedSchemas.has(schemaPath)) {
2133
+ return false;
2202
2134
  }
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];
2135
+ const prodAbsPath = path8.resolve(rootDir, routeFilePath);
2136
+ const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2137
+ if (!fs7.existsSync(sourceAbsPath)) {
2138
+ return false;
2211
2139
  }
2212
- return segment;
2140
+ if (isProductFresh(sourceAbsPath, schemaPath)) {
2141
+ generatedSchemas.add(schemaPath);
2142
+ return false;
2143
+ }
2144
+ const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
2145
+ if (fileRoutes.length === 0) {
2146
+ return false;
2147
+ }
2148
+ const sourceRelPath = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2149
+ const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2150
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2151
+ generatedSchemas.add(schemaPath);
2152
+ return true;
2213
2153
  }
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));
2154
+ async function deleteSchemaFiles(routes, rootDir, dist) {
2155
+ const deleted = /* @__PURE__ */ new Set();
2156
+ for (const route of routes) {
2157
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
2158
+ if (deleted.has(schemaPath)) continue;
2159
+ deleted.add(schemaPath);
2160
+ try {
2161
+ await fs7.promises.unlink(schemaPath);
2162
+ } catch {
2222
2163
  }
2223
2164
  }
2224
- return params;
2225
- }
2226
- function isCatchAllSegment(segment) {
2227
- return /^\[\.\.\..+\]$/.test(segment);
2228
- }
2229
- function isRouteGroup(segment) {
2230
- return /^\(.+\)$/.test(segment);
2231
2165
  }
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 "";
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);
2238
2171
  }
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();
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);
2247
2177
  }
2248
- function getCachedMiddlewares(absPath) {
2249
- return middlewareCache.get(absPath);
2178
+ var devOnDemandEnabled = false;
2179
+ function isDevOnDemandEnabled() {
2180
+ return devOnDemandEnabled;
2250
2181
  }
2251
- function setCachedMiddlewares(absPath, bundle) {
2252
- middlewareCache.set(absPath, bundle);
2182
+ var devDistDir;
2183
+ function getDevDist() {
2184
+ return devDistDir;
2253
2185
  }
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;
2186
+
2187
+ // src/loader/loadRouteModule.ts
2188
+ async function loadRouteModule(filePath, method, rootDir) {
2189
+ if (isDevOnDemandEnabled() && rootDir) {
2190
+ const dist = getDevDist();
2191
+ if (dist) {
2192
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2193
+ if (sourcePath && fs8.existsSync(sourcePath)) {
2194
+ try {
2195
+ await ensureCompiled(sourcePath, rootDir, dist);
2196
+ } catch (compileErr) {
2197
+ const reason = compileErr instanceof Error ? compileErr.message : String(compileErr);
2198
+ throw new Error(`Failed to compile route module "${sourcePath}": ${reason}`, {
2199
+ cause: compileErr
2200
+ });
2201
+ }
2279
2202
  }
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
2203
  }
2301
2204
  }
2302
- if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
2303
- return void 0;
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 });
2304
2211
  }
2305
- return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
2212
+ const handler = resolveExport(module, method);
2213
+ validateRouteModule(handler, method, filePath);
2214
+ return { handler, method };
2306
2215
  }
2307
2216
 
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]);
2217
+ // src/runtime/sse.ts
2218
+ function encodeSseEvent(event) {
2219
+ let out = "";
2220
+ if (event.comment !== void 0) {
2221
+ out += `: ${event.comment}
2222
+ `;
2319
2223
  }
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;
2224
+ if (event.event !== void 0) {
2225
+ out += `event: ${event.event}
2226
+ `;
2353
2227
  }
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);
2228
+ if (event.id !== void 0) {
2229
+ out += `id: ${event.id}
2230
+ `;
2361
2231
  }
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;
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
+ `;
2419
2249
  }
2420
2250
  }
2421
- return { routes, wsRoutes };
2251
+ out += "\n";
2252
+ return out;
2422
2253
  }
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;
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;
2432
2267
  }
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;
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"
2437
2275
  }
2438
- return a.urlPath.localeCompare(b.urlPath);
2439
2276
  });
2440
- }
2441
-
2442
- // src/testServer.ts
2443
- init_generateSchemaFiles();
2444
-
2445
- // src/validator/validateInput.ts
2446
- init_schemaName();
2447
-
2448
- // src/cli/compileOnDemand.ts
2449
- import path9 from "path";
2450
- import fs8 from "fs";
2451
-
2452
- // src/cli/compileDevRoutes.ts
2453
- import path8 from "path";
2454
- import fs7 from "fs";
2455
- import fg2 from "fast-glob";
2456
-
2457
- // src/cli/aliasPlugin.ts
2458
- import path7 from "path";
2459
- import fs6 from "fs";
2460
-
2461
- // src/utils/resolveAlias.ts
2462
- function resolveAlias(specifier, config) {
2463
- const candidates = [];
2464
- for (const [pattern, targets] of Object.entries(config.paths)) {
2465
- const wildcardIndex = pattern.indexOf("*");
2466
- if (wildcardIndex === -1) {
2467
- if (specifier === pattern) {
2468
- candidates.push(...targets);
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();
2469
2296
  }
2470
- continue;
2471
- }
2472
- const prefix = pattern.slice(0, wildcardIndex);
2473
- const suffix = pattern.slice(wildcardIndex + 1);
2474
- if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
2475
- const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
2476
- for (const target of targets) {
2477
- candidates.push(target.replace("*", captured));
2297
+ },
2298
+ close() {
2299
+ if (closed) return;
2300
+ closed = true;
2301
+ if (controller) {
2302
+ try {
2303
+ controller.close();
2304
+ } catch {
2305
+ }
2306
+ controller = null;
2478
2307
  }
2308
+ },
2309
+ get closed() {
2310
+ return closed;
2311
+ },
2312
+ get aborted() {
2313
+ return aborted;
2314
+ },
2315
+ get response() {
2316
+ return response;
2479
2317
  }
2480
- }
2481
- return candidates;
2318
+ };
2319
+ return writer;
2482
2320
  }
2483
2321
 
2484
- // src/utils/readTsconfig.ts
2485
- import ts6 from "typescript";
2486
- import path6 from "path";
2487
- import fs5 from "fs";
2488
- function readTsconfig(rootDir) {
2489
- const tsconfigPath = path6.resolve(rootDir, "tsconfig.json");
2490
- if (!fs5.existsSync(tsconfigPath)) return null;
2491
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2492
- if (configFile.error || !configFile.config) return null;
2493
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2494
- const baseUrl = parsed.options.baseUrl ?? rootDir;
2495
- const rawPaths = parsed.options.paths;
2496
- if (!rawPaths) return null;
2497
- const paths = {};
2498
- for (const [pattern, targets] of Object.entries(rawPaths)) {
2499
- paths[pattern] = targets.map((t) => path6.resolve(baseUrl, t));
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
+ }
2500
2332
  }
2501
- return { baseUrl, paths };
2333
+ return cookies;
2502
2334
  }
2503
-
2504
- // src/cli/aliasPlugin.ts
2505
- function toProdExtension(filePath) {
2506
- if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2507
- if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2508
- if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2509
- return filePath;
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;
2510
2345
  }
2511
- function toProdImportPath(sourceFile, importer) {
2512
- const importerDir = path7.dirname(importer);
2513
- let rel = path7.relative(importerDir, sourceFile);
2514
- rel = rel.split(path7.sep).join("/");
2515
- if (!rel.startsWith(".")) rel = "./" + rel;
2516
- return toProdExtension(rel);
2517
- }
2518
- function toRealPath(p) {
2519
- try {
2520
- return fs6.realpathSync(p);
2521
- } catch {
2522
- return p;
2523
- }
2524
- }
2525
- function isInsideDir(filePath, dir) {
2526
- const rel = path7.relative(dir, filePath);
2527
- return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
2528
- }
2529
- var APP_DIR = "src";
2530
- function toStrippedProdImportPath(sourceFile, rootDir) {
2531
- const appDirAbs = toRealPath(path7.resolve(rootDir, APP_DIR));
2532
- const sourceReal = toRealPath(sourceFile);
2533
- let rel = path7.relative(appDirAbs, sourceReal);
2534
- rel = rel.split(path7.sep).join("/");
2535
- if (!rel.startsWith(".")) rel = "./" + rel;
2536
- return toProdExtension(rel);
2537
- }
2538
- var PROD_EXTS = [".js", ".mjs", ".cjs"];
2539
- var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2540
- var INDEX_EXTS = [
2541
- "/index.ts",
2542
- "/index.tsx",
2543
- "/index.js",
2544
- "/index.jsx",
2545
- "/index.mjs",
2546
- "/index.cjs"
2547
- ];
2548
- function resolveRelativeSpecifier(importer, specifier) {
2549
- const importerDir = path7.dirname(importer);
2550
- const base = path7.resolve(importerDir, specifier);
2551
- if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2552
- return fs6.existsSync(base) ? base : null;
2553
- }
2554
- if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2555
- return fs6.existsSync(base) ? base : null;
2556
- }
2557
- for (const ext of SOURCE_EXTS) {
2558
- const file = base + ext;
2559
- if (fs6.existsSync(file)) return file;
2560
- }
2561
- for (const indexExt of INDEX_EXTS) {
2562
- const file = base + indexExt;
2563
- if (fs6.existsSync(file)) return file;
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;
2564
2353
  }
2565
- return null;
2566
- }
2567
- function createAliasPlugin(config, options) {
2568
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2569
- const appDirAbs = options?.rootDir ? toRealPath(path7.resolve(options.rootDir, APP_DIR)) : null;
2570
- return {
2571
- name: "faapi-alias",
2572
- setup(build) {
2573
- build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2574
- let source;
2575
- try {
2576
- source = fs6.readFileSync(args.path, "utf8");
2577
- } catch {
2578
- return void 0;
2579
- }
2580
- const importer = args.path;
2581
- const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2582
- let modified = false;
2583
- const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2584
- if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2585
- return full;
2586
- }
2587
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
2588
- const resolved = resolveRelativeSpecifier(importer, specifier);
2589
- if (resolved) {
2590
- if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2591
- return full;
2592
- }
2593
- if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
2594
- modified = true;
2595
- return `${prefix}${quote}${toStrippedProdImportPath(
2596
- resolved,
2597
- options.rootDir
2598
- )}${quote}`;
2599
- }
2600
- modified = true;
2601
- return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
2602
- }
2603
- return full;
2604
- }
2605
- const candidates = resolveAlias(specifier, config);
2606
- for (const candidate of candidates) {
2607
- for (const ext of SOURCE_EXTS) {
2608
- const file = candidate + ext;
2609
- if (fs6.existsSync(file)) {
2610
- modified = true;
2611
- if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2612
- return `${prefix}${quote}${toStrippedProdImportPath(
2613
- file,
2614
- options.rootDir
2615
- )}${quote}`;
2616
- }
2617
- return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2618
- }
2619
- }
2620
- for (const indexExt of INDEX_EXTS) {
2621
- const file = candidate + indexExt;
2622
- if (fs6.existsSync(file)) {
2623
- modified = true;
2624
- if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2625
- return `${prefix}${quote}${toStrippedProdImportPath(
2626
- file,
2627
- options.rootDir
2628
- )}${quote}`;
2629
- }
2630
- return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2631
- }
2632
- }
2633
- }
2634
- return full;
2635
- });
2636
- if (!modified) return void 0;
2637
- return { contents: newSource, loader: "default" };
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
2638
2451
  });
2452
+ return ctx.json(body, options.status ?? 500);
2639
2453
  }
2640
2454
  };
2641
- }
2642
- function buildAliasPlugins(rootDir) {
2643
- const tsconfig = readTsconfig(rootDir);
2644
- return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2645
- }
2646
-
2647
- // src/cli/compileDevRoutes.ts
2648
- var APP_DIR2 = "src";
2649
- async function compileDevRoutes(options) {
2650
- const { rootDir, dist, files, logLevel = "silent" } = options;
2651
- const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2652
- cwd: rootDir,
2653
- onlyFiles: true,
2654
- absolute: true,
2655
- ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
2656
- });
2657
- if (entryPoints.length === 0) {
2658
- return { compiledFiles: [] };
2659
- }
2660
- const absDist = path8.resolve(rootDir, dist);
2661
- await fs7.promises.mkdir(absDist, { recursive: true });
2662
- const plugins = buildAliasPlugins(rootDir);
2663
- const esbuild = await import("esbuild");
2664
- const outbase = path8.resolve(rootDir, APP_DIR2);
2665
- const result = await esbuild.build({
2666
- entryPoints,
2667
- outdir: absDist,
2668
- outbase,
2669
- bundle: false,
2670
- platform: "node",
2671
- format: "esm",
2672
- sourcemap: true,
2673
- packages: "external",
2674
- plugins,
2675
- logLevel,
2676
- write: false
2677
- });
2678
- if (result.outputFiles) {
2679
- await Promise.all(
2680
- result.outputFiles.map(async (file) => {
2681
- await fs7.promises.mkdir(path8.dirname(file.path), { recursive: true });
2682
- 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);
2685
- })
2686
- );
2687
- }
2688
- return { compiledFiles: entryPoints };
2689
- }
2690
-
2691
- // src/cli/compileOnDemand.ts
2692
- init_generateSchemaFiles();
2693
- init_generateSchemaFiles();
2694
- function isProductFresh(sourceAbsPath, productAbsPath) {
2695
- try {
2696
- const srcStat = fs8.statSync(sourceAbsPath);
2697
- const prodStat = fs8.statSync(productAbsPath);
2698
- return prodStat.mtimeMs >= srcStat.mtimeMs;
2699
- } catch {
2700
- return false;
2701
- }
2702
- }
2703
- var compiledFiles = /* @__PURE__ */ new Set();
2704
- function clearCompiledFiles() {
2705
- compiledFiles.clear();
2706
- }
2707
- async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2708
- if (compiledFiles.has(sourceAbsPath)) {
2709
- return false;
2710
- }
2711
- if (!fs8.existsSync(sourceAbsPath)) {
2712
- return false;
2713
- }
2714
- const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2715
- if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2716
- compiledFiles.add(sourceAbsPath);
2717
- return false;
2718
- }
2719
- await compileDevRoutes({
2720
- rootDir,
2721
- dist,
2722
- files: [sourceAbsPath],
2723
- logLevel: "silent"
2724
- });
2725
- compiledFiles.add(sourceAbsPath);
2726
- return true;
2727
- }
2728
- function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2729
- const rel = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2730
- if (!rel.startsWith("src/")) return null;
2731
- const relWithoutSrc = rel.slice(4);
2732
- const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2733
- return path9.resolve(rootDir, dist, jsRel);
2734
- }
2735
- var generatedSchemas = /* @__PURE__ */ new Set();
2736
- function clearGeneratedSchemas() {
2737
- generatedSchemas.clear();
2738
- }
2739
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2740
- if (generatedSchemas.has(schemaPath)) {
2741
- return false;
2742
- }
2743
- const prodAbsPath = path9.resolve(rootDir, routeFilePath);
2744
- const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2745
- if (!fs8.existsSync(sourceAbsPath)) {
2746
- return false;
2747
- }
2748
- if (isProductFresh(sourceAbsPath, schemaPath)) {
2749
- generatedSchemas.add(schemaPath);
2750
- return false;
2751
- }
2752
- const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
2753
- if (fileRoutes.length === 0) {
2754
- return false;
2755
- }
2756
- const sourceRelPath = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2757
- const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2758
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
2759
- generatedSchemas.add(schemaPath);
2760
- return true;
2761
- }
2762
- async function deleteSchemaFiles(routes, rootDir, dist) {
2763
- const deleted = /* @__PURE__ */ new Set();
2764
- for (const route of routes) {
2765
- const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
2766
- if (deleted.has(schemaPath)) continue;
2767
- deleted.add(schemaPath);
2768
- try {
2769
- await fs8.promises.unlink(schemaPath);
2770
- } catch {
2771
- }
2772
- }
2773
- }
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
- );
2455
+ const extend = config?.extendContext;
2456
+ if (typeof extend === "function") {
2457
+ extend(ctx);
2996
2458
  }
2459
+ return ctx;
2997
2460
  }
2998
2461
 
2999
- // src/loader/loadRouteModule.ts
3000
- async function loadRouteModule(filePath, method, rootDir) {
3001
- if (isDevOnDemandEnabled() && rootDir) {
3002
- const dist = getDevDist();
3003
- if (dist) {
3004
- const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3005
- if (sourcePath && fs9.existsSync(sourcePath)) {
3006
- try {
3007
- await ensureCompiled(sourcePath, rootDir, dist);
3008
- } catch (compileErr) {
3009
- const reason = compileErr instanceof Error ? compileErr.message : String(compileErr);
3010
- throw new Error(`Failed to compile route module "${sourcePath}": ${reason}`, {
3011
- cause: compileErr
3012
- });
3013
- }
3014
- }
3015
- }
3016
- }
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 });
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, Readable as Readable3 } 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,14 +3552,25 @@ 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"];
3934
- var currentApp = null;
3555
+ var APP_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("faapi.app.instance");
3556
+ function getCurrentApp() {
3557
+ return globalThis[APP_INSTANCE_KEY] ?? null;
3558
+ }
3559
+ function setCurrentApp(app) {
3560
+ if (app === null) {
3561
+ delete globalThis[APP_INSTANCE_KEY];
3562
+ } else {
3563
+ globalThis[APP_INSTANCE_KEY] = app;
3564
+ }
3565
+ }
3935
3566
  function getApp() {
3936
- if (!currentApp) {
3567
+ const app = getCurrentApp();
3568
+ if (!app) {
3937
3569
  throw new Error(
3938
3570
  "[faapi] No app instance. Call createProdApp() / createDevApp() first, or run `faapi dev` / `node dist/main`."
3939
3571
  );
3940
3572
  }
3941
- return currentApp;
3573
+ return app;
3942
3574
  }
3943
3575
  var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
3944
3576
  "cors",
@@ -3959,8 +3591,8 @@ function isFaapiConfigKey(key) {
3959
3591
  async function createAppBase(options) {
3960
3592
  const rootDir = options?.rootDir ?? process.cwd();
3961
3593
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
3962
- const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE);
3963
- if (!fs13.existsSync(routesPath)) {
3594
+ const routesPath = path12.resolve(rootDir, dist, ROUTES_FILE);
3595
+ if (!fs11.existsSync(routesPath)) {
3964
3596
  throw new Error(
3965
3597
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
3966
3598
  );
@@ -4127,20 +3759,20 @@ async function createAppBase(options) {
4127
3759
  }
4128
3760
  if (!server.listening) {
4129
3761
  app.server = null;
4130
- if (currentApp === app) currentApp = null;
3762
+ if (getCurrentApp() === app) setCurrentApp(null);
4131
3763
  return;
4132
3764
  }
4133
3765
  return new Promise((resolve) => {
4134
3766
  server.close((err) => {
4135
3767
  if (err) console.error("Error closing server:", err);
4136
3768
  app.server = null;
4137
- if (currentApp === app) currentApp = null;
3769
+ if (getCurrentApp() === app) setCurrentApp(null);
4138
3770
  resolve();
4139
3771
  });
4140
3772
  });
4141
3773
  }
4142
3774
  };
4143
- currentApp = app;
3775
+ setCurrentApp(app);
4144
3776
  const ctx = {
4145
3777
  rootDir,
4146
3778
  dist,
@@ -4160,6 +3792,180 @@ async function createAppBase(options) {
4160
3792
  return { app, ctx };
4161
3793
  }
4162
3794
 
3795
+ // src/router/scanRoutes.ts
3796
+ import fg2 from "fast-glob";
3797
+ import path13 from "path";
3798
+ import fs12 from "fs";
3799
+
3800
+ // src/router/constants.ts
3801
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
3802
+ var HTTP_METHOD_SET = new Set(HTTP_METHODS);
3803
+
3804
+ // src/utils/normalizePath.ts
3805
+ function normalizePath(path14) {
3806
+ if (!path14) return "";
3807
+ let result = path14.replace(/\\/g, "/");
3808
+ result = result.replace(/\/+/g, "/");
3809
+ result = result.replace(/\/+$/, "");
3810
+ if (result && !result.startsWith("/")) {
3811
+ result = "/" + result;
3812
+ }
3813
+ return result;
3814
+ }
3815
+
3816
+ // src/router/parseRouteFile.ts
3817
+ function dynamicSegmentToParam(segment) {
3818
+ const match = segment.match(/^\[(.+)\]$/);
3819
+ if (match) {
3820
+ return ":" + match[1];
3821
+ }
3822
+ return segment;
3823
+ }
3824
+ function extractParamNames(urlPath) {
3825
+ const params = [];
3826
+ const segments = urlPath.split("/");
3827
+ for (const segment of segments) {
3828
+ if (segment.startsWith(":...")) {
3829
+ params.push(segment.slice(4));
3830
+ } else if (segment.startsWith(":")) {
3831
+ params.push(segment.slice(1));
3832
+ }
3833
+ }
3834
+ return params;
3835
+ }
3836
+ function isCatchAllSegment(segment) {
3837
+ return /^\[\.\.\..+\]$/.test(segment);
3838
+ }
3839
+ function isRouteGroup(segment) {
3840
+ return /^\(.+\)$/.test(segment);
3841
+ }
3842
+ function filePathToUrlPath(filePath) {
3843
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
3844
+ const lastSlashIndex = withoutPrefix.lastIndexOf("/");
3845
+ const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
3846
+ if (!dirPath) {
3847
+ return "";
3848
+ }
3849
+ const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
3850
+ return normalizePath(segments.join("/"));
3851
+ }
3852
+
3853
+ // src/router/scanRoutes.ts
3854
+ var HTTP_OR_WS_EXPORT_RE = new RegExp(
3855
+ String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
3856
+ "g"
3857
+ );
3858
+ function extractExportsFromSource(source) {
3859
+ const names = /* @__PURE__ */ new Set();
3860
+ let match;
3861
+ HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
3862
+ while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
3863
+ names.add(match[1]);
3864
+ }
3865
+ return names;
3866
+ }
3867
+ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
3868
+ const routeDir = path13.dirname(routeFilePath);
3869
+ const resolvedRoot = path13.resolve(rootDir);
3870
+ const paths = [];
3871
+ let currentDir = path13.resolve(rootDir, routeDir);
3872
+ while (true) {
3873
+ if (dist) {
3874
+ const mwTsPath = path13.join(currentDir, "middlewares.ts");
3875
+ const mwJsPath = path13.join(currentDir, "middlewares.js");
3876
+ const absTsPath = path13.resolve(rootDir, mwTsPath);
3877
+ const absJsPath = path13.resolve(rootDir, mwJsPath);
3878
+ const absMwPath = fs12.existsSync(absTsPath) ? absTsPath : fs12.existsSync(absJsPath) ? absJsPath : null;
3879
+ if (absMwPath) {
3880
+ const relMwPath = path13.relative(rootDir, absMwPath);
3881
+ const prodAbsPath = path13.resolve(rootDir, toProdFilePath(relMwPath, dist));
3882
+ paths.push(prodAbsPath);
3883
+ }
3884
+ } else {
3885
+ for (const ext of [".ts", ".js"]) {
3886
+ const mwPath = path13.join(currentDir, `middlewares${ext}`);
3887
+ const absMwPath = path13.resolve(rootDir, mwPath);
3888
+ if (fs12.existsSync(absMwPath)) {
3889
+ paths.push(absMwPath);
3890
+ break;
3891
+ }
3892
+ }
3893
+ }
3894
+ if (currentDir === resolvedRoot) break;
3895
+ const parentDir = path13.dirname(currentDir);
3896
+ if (parentDir === currentDir) break;
3897
+ currentDir = parentDir;
3898
+ }
3899
+ paths.reverse();
3900
+ return paths;
3901
+ }
3902
+ function toProdFilePath(filePath, dist) {
3903
+ let rel = filePath.replace(/\\/g, "/");
3904
+ if (rel.startsWith("src/")) {
3905
+ rel = rel.slice(4);
3906
+ }
3907
+ const jsPath = rel.replace(/\.ts$/, ".js");
3908
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
3909
+ }
3910
+ async function scanRoutes(rootDir, patterns, dist) {
3911
+ const files = await fg2(patterns, {
3912
+ cwd: rootDir,
3913
+ onlyFiles: true,
3914
+ absolute: false
3915
+ });
3916
+ const routes = [];
3917
+ const wsRoutes = [];
3918
+ for (const file of files) {
3919
+ const normalizedFile = file.replace(/\\/g, "/");
3920
+ const fileName = normalizedFile.split("/").pop();
3921
+ if (fileName === "handler.ts" || fileName === "handler.js") {
3922
+ const absPath = path13.resolve(rootDir, normalizedFile);
3923
+ const urlPath = filePathToUrlPath(normalizedFile);
3924
+ const paramNames = extractParamNames(urlPath);
3925
+ const isDynamic = paramNames.length > 0;
3926
+ const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
3927
+ let middlewarePaths;
3928
+ let middlewareBundle;
3929
+ if (dist) {
3930
+ middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
3931
+ } else {
3932
+ const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
3933
+ middlewareBundle = await loadMergedMiddlewares(mwPaths);
3934
+ }
3935
+ const source = await fs12.promises.readFile(absPath, "utf8").catch(() => "");
3936
+ const exportNames = extractExportsFromSource(source);
3937
+ const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
3938
+ for (const method of methods) {
3939
+ routes.push({
3940
+ method,
3941
+ urlPath,
3942
+ filePath: normalizedFile,
3943
+ paramNames,
3944
+ isDynamic,
3945
+ isCatchAll: isCatchAll || void 0,
3946
+ middlewarePaths,
3947
+ middlewares: middlewareBundle?.middlewares,
3948
+ injectors: middlewareBundle?.injectors
3949
+ });
3950
+ }
3951
+ if (exportNames.has("WS")) {
3952
+ wsRoutes.push({
3953
+ urlPath,
3954
+ filePath: normalizedFile,
3955
+ paramNames,
3956
+ isDynamic,
3957
+ isCatchAll: isCatchAll || void 0,
3958
+ middlewarePaths,
3959
+ middlewares: middlewareBundle?.middlewares,
3960
+ injectors: middlewareBundle?.injectors
3961
+ });
3962
+ }
3963
+ continue;
3964
+ }
3965
+ }
3966
+ return { routes, wsRoutes };
3967
+ }
3968
+
4163
3969
  // src/cli/createDevApp.ts
4164
3970
  init_createProgram();
4165
3971
  async function createDevApp(options) {
@@ -4193,31 +3999,25 @@ async function createProdApp(options) {
4193
3999
  export {
4194
4000
  FaapiError,
4195
4001
  InternalError,
4196
- MessageQueue,
4197
4002
  MethodNotAllowedError,
4198
4003
  ModuleLoadError,
4199
4004
  RouteNotFoundError,
4200
4005
  SchemaExtractionError,
4201
4006
  ValidationError,
4202
4007
  collectRouteSchemaSources,
4203
- connectWs,
4204
4008
  cors,
4205
4009
  createProdApp as createApp,
4206
- createContext,
4207
4010
  createDevApp,
4208
4011
  createProdApp,
4209
4012
  createProgram,
4210
- createTestServer,
4211
4013
  extractTypeInfo,
4212
4014
  getApp,
4213
4015
  getInputTypeForMethod,
4214
4016
  helmet,
4215
4017
  invalidateProgramCache,
4216
- invokeHandler,
4217
4018
  loadConfig,
4218
4019
  loadEnv,
4219
4020
  logger,
4220
- resolveTypeNode,
4221
- waitForWsOpen
4021
+ resolveTypeNode
4222
4022
  };
4223
4023
  //# sourceMappingURL=index.js.map