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