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