recorver code
This commit is contained in:
20
auto.ts
Normal file
20
auto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { loadTS, getMatchFiles } from './src/auto/load-ts.ts';
|
||||
import { listenSocket } from './src/auto/listen-sock.ts';
|
||||
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
|
||||
|
||||
export { Route, QueryRouter, QueryRouterServer };
|
||||
|
||||
export const App = QueryRouterServer;
|
||||
|
||||
export { createSchema } from './src/validator/index.ts';
|
||||
export type { Rule } from './src/validator/rule.ts';
|
||||
export type { Schema } from 'zod';
|
||||
export type { RouteContext, RouteOpts } from './src/route.ts';
|
||||
|
||||
export type { Run } from './src/route.ts';
|
||||
|
||||
export { CustomError } from './src/result/error.ts';
|
||||
|
||||
export { listenSocket, loadTS, getMatchFiles };
|
||||
|
||||
export { autoCall } from './src/auto/call-sock.ts';
|
164
src/auto/call-sock.ts
Normal file
164
src/auto/call-sock.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createConnection } from 'node:net';
|
||||
|
||||
type QueryData = {
|
||||
path?: string;
|
||||
key?: string;
|
||||
payload?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type CallSockOptions = {
|
||||
socketPath?: string;
|
||||
timeout?: number;
|
||||
method?: 'GET' | 'POST';
|
||||
};
|
||||
|
||||
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
|
||||
const { socketPath = './app.sock', timeout = 10000, method = 'POST' } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = createConnection(socketPath);
|
||||
let responseData = '';
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
// 设置超时
|
||||
if (timeout > 0) {
|
||||
timer = setTimeout(() => {
|
||||
client.destroy();
|
||||
reject(new Error(`Socket call timeout after ${timeout}ms`));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
client.on('connect', () => {
|
||||
try {
|
||||
let request: string;
|
||||
|
||||
if (method === 'GET') {
|
||||
// GET 请求:参数放在 URL 中
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (key === 'payload' && typeof value === 'object') {
|
||||
searchParams.append(key, JSON.stringify(value));
|
||||
} else {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `/?${queryString}` : '/';
|
||||
|
||||
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
|
||||
} else {
|
||||
// POST 请求:数据放在 body 中
|
||||
const body = JSON.stringify(data);
|
||||
const contentLength = Buffer.byteLength(body, 'utf8');
|
||||
|
||||
request = [
|
||||
'POST / HTTP/1.1',
|
||||
'Host: localhost',
|
||||
'Content-Type: application/json',
|
||||
`Content-Length: ${contentLength}`,
|
||||
'Connection: close',
|
||||
'',
|
||||
body,
|
||||
].join('\r\n');
|
||||
}
|
||||
|
||||
client.write(request);
|
||||
} catch (error) {
|
||||
if (timer) clearTimeout(timer);
|
||||
client.destroy();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('data', (chunk) => {
|
||||
responseData += chunk.toString();
|
||||
|
||||
// 检查是否收到完整的HTTP响应
|
||||
if (responseData.includes('\r\n\r\n')) {
|
||||
const [headerSection] = responseData.split('\r\n\r\n');
|
||||
const contentLengthMatch = headerSection.match(/content-length:\s*(\d+)/i);
|
||||
|
||||
if (contentLengthMatch) {
|
||||
const expectedLength = parseInt(contentLengthMatch[1]);
|
||||
const bodyStart = responseData.indexOf('\r\n\r\n') + 4;
|
||||
const currentBodyLength = Buffer.byteLength(responseData.slice(bodyStart), 'utf8');
|
||||
|
||||
// 如果收到了完整的响应,主动关闭连接
|
||||
if (currentBodyLength >= expectedLength) {
|
||||
client.end();
|
||||
}
|
||||
} else if (responseData.includes('\r\n0\r\n\r\n')) {
|
||||
// 检查 chunked 编码结束标记
|
||||
client.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on('end', () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
try {
|
||||
// 解析 HTTP 响应
|
||||
const response = parseHttpResponse(responseData);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试解析 JSON 响应
|
||||
try {
|
||||
const result = JSON.parse(response.body);
|
||||
resolve(result);
|
||||
} catch {
|
||||
// 如果不是 JSON,直接返回文本
|
||||
resolve(response.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
client.on('timeout', () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
client.destroy();
|
||||
reject(new Error('Socket connection timeout'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 解析 HTTP 响应的辅助函数
|
||||
function parseHttpResponse(responseData: string) {
|
||||
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
|
||||
const body = bodyParts.join('\r\n\r\n');
|
||||
|
||||
const lines = headerSection.split('\r\n');
|
||||
const statusLine = lines[0];
|
||||
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
|
||||
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const [key, ...valueParts] = lines[i].split(':');
|
||||
if (key && valueParts.length > 0) {
|
||||
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode,
|
||||
headers,
|
||||
body: body || '',
|
||||
};
|
||||
}
|
||||
|
||||
export const autoCall = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
||||
return callSock(data, { ...options, method: 'POST' });
|
||||
};
|
274
src/auto/listen-sock.ts
Normal file
274
src/auto/listen-sock.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import type { IncomingMessage } from 'http';
|
||||
import { QueryRouterServer } from '../route.ts';
|
||||
import { getRuntime } from './runtime.ts';
|
||||
import { runFirstCheck } from './listen/run-check.ts';
|
||||
import { cleanup } from './listen/cleanup.ts';
|
||||
import { ServerTimer } from './listen/server-time.ts';
|
||||
|
||||
type ListenSocketOptions = {
|
||||
/**
|
||||
* Unix socket path, defaults to './app.sock'
|
||||
*/
|
||||
path?: string;
|
||||
app?: QueryRouterServer;
|
||||
/**
|
||||
* Unix socket path, defaults to './app.pid'
|
||||
*/
|
||||
pidPath?: string;
|
||||
/**
|
||||
* Timeout for the server, defaults to 15 minutes.
|
||||
* If the server is not responsive for this duration, it will be terminated
|
||||
*/
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
const server = async (req, app: QueryRouterServer) => {
|
||||
const runtime = getRuntime();
|
||||
let data;
|
||||
if (!runtime.isNode) {
|
||||
data = await getRequestParams(req);
|
||||
} else {
|
||||
data = await parseBody(req);
|
||||
}
|
||||
// @ts-ignore
|
||||
const serverTimer = app.serverTimer;
|
||||
if (serverTimer) {
|
||||
serverTimer?.run?.();
|
||||
}
|
||||
const result = await app.queryRoute(data as any);
|
||||
const response = new Response(JSON.stringify(result));
|
||||
response.headers.set('Content-Type', 'application/json');
|
||||
return response;
|
||||
};
|
||||
export const closeListenSocket = () => {
|
||||
console.log('Closing listen socket');
|
||||
process.emit('SIGINT');
|
||||
};
|
||||
export const serverTimer = new ServerTimer();
|
||||
export const listenSocket = async (options?: ListenSocketOptions) => {
|
||||
const path = options?.path || './app.sock';
|
||||
const pidPath = options?.pidPath || './app.pid';
|
||||
const timeout = options?.timeout || 24 * 60 * 60 * 1000; // 24 hours
|
||||
const runtime = getRuntime();
|
||||
|
||||
serverTimer.timeout = timeout;
|
||||
serverTimer.startTimer();
|
||||
serverTimer.onTimeout = closeListenSocket;
|
||||
|
||||
let app = options?.app || globalThis.context?.app;
|
||||
if (!app) {
|
||||
app = new QueryRouterServer();
|
||||
}
|
||||
app.serverTimer = serverTimer;
|
||||
await runFirstCheck(path, pidPath);
|
||||
let close = async () => {};
|
||||
cleanup({ path, close });
|
||||
if (runtime.isDeno) {
|
||||
// 检查 Deno 版本是否支持 Unix domain socket
|
||||
try {
|
||||
// @ts-ignore
|
||||
const listener = Deno.listen({
|
||||
transport: 'unix',
|
||||
path: path,
|
||||
});
|
||||
|
||||
// 处理连接
|
||||
(async () => {
|
||||
for await (const conn of listener) {
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
const httpConn = Deno.serveHttp(conn);
|
||||
for await (const requestEvent of httpConn) {
|
||||
try {
|
||||
const response = await server(requestEvent.request, app);
|
||||
await requestEvent.respondWith(response);
|
||||
} catch (error) {
|
||||
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
})();
|
||||
close = async () => {
|
||||
listener.close();
|
||||
};
|
||||
return listener;
|
||||
} catch (error) {
|
||||
// 如果 Unix socket 不支持,回退到 HTTP 服务器
|
||||
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
|
||||
|
||||
// @ts-ignore
|
||||
const listener = Deno.listen({ port: 0 }); // 使用随机端口
|
||||
|
||||
// @ts-ignore
|
||||
console.log(`Deno server listening on port ${listener.addr.port}`);
|
||||
|
||||
(async () => {
|
||||
for await (const conn of listener) {
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
const httpConn = Deno.serveHttp(conn);
|
||||
for await (const requestEvent of httpConn) {
|
||||
try {
|
||||
const response = await server(requestEvent.request, app);
|
||||
await requestEvent.respondWith(response);
|
||||
} catch (error) {
|
||||
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
})();
|
||||
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime.isBun) {
|
||||
// @ts-ignore
|
||||
const bunServer = Bun.serve({
|
||||
unix: path,
|
||||
fetch(req) {
|
||||
return server(req, app);
|
||||
},
|
||||
});
|
||||
close = async () => {
|
||||
await bunServer.stop();
|
||||
};
|
||||
return bunServer;
|
||||
}
|
||||
|
||||
// Node.js 环境
|
||||
const http = await import('http');
|
||||
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const response = await server(req, app);
|
||||
|
||||
// 设置响应头
|
||||
response.headers.forEach((value, key) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
|
||||
// 设置状态码
|
||||
res.statusCode = response.status;
|
||||
|
||||
// 读取响应体并写入
|
||||
const body = await response.text();
|
||||
res.end(body);
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
httpServer.listen(path);
|
||||
close = async () => {
|
||||
httpServer.close();
|
||||
};
|
||||
return httpServer;
|
||||
};
|
||||
|
||||
export const getRequestParams = async (req: Request) => {
|
||||
let urlParams: Record<string, any> = {};
|
||||
let bodyParams: Record<string, any> = {};
|
||||
|
||||
// 获取URL参数
|
||||
const url = new URL(req.url);
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
// 尝试解析JSON payload
|
||||
if (key === 'payload') {
|
||||
try {
|
||||
urlParams[key] = JSON.parse(value);
|
||||
} catch {
|
||||
urlParams[key] = value;
|
||||
}
|
||||
} else {
|
||||
urlParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取body参数
|
||||
if (req.method.toLowerCase() === 'post' && req.body) {
|
||||
const contentType = req.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
try {
|
||||
bodyParams = await req.json();
|
||||
} catch {
|
||||
// 如果解析失败,保持空对象
|
||||
}
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const formData = await req.text();
|
||||
const params = new URLSearchParams(formData);
|
||||
for (const [key, value] of params.entries()) {
|
||||
bodyParams[key] = value;
|
||||
}
|
||||
} else if (contentType.includes('multipart/form-data')) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
for (const [key, value] of formData.entries()) {
|
||||
// @ts-ignore
|
||||
bodyParams[key] = value instanceof File ? value : value.toString();
|
||||
}
|
||||
} catch {
|
||||
// 如果解析失败,保持空对象
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// body参数优先,合并数据
|
||||
return {
|
||||
...urlParams,
|
||||
...bodyParams,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const arr: any[] = [];
|
||||
req.on('data', (chunk) => {
|
||||
arr.push(chunk);
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(arr).toString();
|
||||
|
||||
// 获取 Content-Type 头信息
|
||||
const contentType = req.headers['content-type'] || '';
|
||||
|
||||
// 处理 application/json
|
||||
if (contentType.includes('application/json')) {
|
||||
resolve(JSON.parse(body) as T);
|
||||
return;
|
||||
}
|
||||
// 处理 application/x-www-form-urlencoded
|
||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const formData = new URLSearchParams(body);
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
||||
try {
|
||||
result[key] = JSON.parse(value);
|
||||
} catch {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
resolve(result as T);
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认尝试 JSON 解析
|
||||
try {
|
||||
resolve(JSON.parse(body) as T);
|
||||
} catch {
|
||||
resolve({} as T);
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({} as T);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
102
src/auto/listen/cleanup.ts
Normal file
102
src/auto/listen/cleanup.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { getRuntime } from '../runtime.ts';
|
||||
|
||||
let isClean = false;
|
||||
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
|
||||
const runtime = getRuntime();
|
||||
if (runtime.isDeno) {
|
||||
// Deno 实现 - 启动后不等待结果
|
||||
const process = new Deno.Command('sh', {
|
||||
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
|
||||
stdout: 'null',
|
||||
stderr: 'null',
|
||||
});
|
||||
process.spawn(); // 不等待结果
|
||||
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||
return;
|
||||
}
|
||||
const { spawn } = await import('node:child_process');
|
||||
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
child.unref(); // 完全分离
|
||||
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||
};
|
||||
|
||||
type CleanupOptions = {
|
||||
path: string;
|
||||
close?: () => Promise<void>;
|
||||
pidPath?: string;
|
||||
};
|
||||
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
|
||||
const runtime = getRuntime();
|
||||
|
||||
// 检查文件是否存在并删除
|
||||
const cleanupFile = async () => {
|
||||
if (isClean) return;
|
||||
isClean = true;
|
||||
if (runtime.isDeno) {
|
||||
await deleteFileDetached(path, pidPath);
|
||||
}
|
||||
await close();
|
||||
if (!runtime.isDeno) {
|
||||
await deleteFileDetached(path, pidPath);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据运行时环境注册不同的退出监听器
|
||||
if (runtime.isDeno) {
|
||||
// Deno 环境
|
||||
const handleSignal = () => {
|
||||
cleanupFile();
|
||||
Deno.exit(0);
|
||||
};
|
||||
|
||||
try {
|
||||
Deno.addSignalListener('SIGINT', handleSignal);
|
||||
Deno.addSignalListener('SIGTERM', handleSignal);
|
||||
} catch (error) {
|
||||
console.warn('[DEBUG] Failed to add signal listeners:', error);
|
||||
}
|
||||
|
||||
// 对于 beforeunload 和 unload,使用异步清理
|
||||
const handleUnload = () => {
|
||||
cleanupFile();
|
||||
};
|
||||
|
||||
globalThis.addEventListener('beforeunload', handleUnload);
|
||||
globalThis.addEventListener('unload', handleUnload);
|
||||
} else if (runtime.isNode || runtime.isBun) {
|
||||
// Node.js 和 Bun 环境
|
||||
import('process').then(({ default: process }) => {
|
||||
// 信号处理使用同步清理,然后退出
|
||||
const signalHandler = async (signal: string) => {
|
||||
await cleanupFile();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => signalHandler('SIGINT'));
|
||||
process.on('SIGTERM', () => signalHandler('SIGTERM'));
|
||||
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
|
||||
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
|
||||
|
||||
process.on('exit', async () => {
|
||||
await cleanupFile();
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught Exception:', error);
|
||||
await cleanupFile();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
await cleanupFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 返回手动清理函数,以便需要时主动调用
|
||||
return cleanupFile;
|
||||
};
|
51
src/auto/listen/run-check.ts
Normal file
51
src/auto/listen/run-check.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { getRuntime } from '../runtime.ts';
|
||||
|
||||
export const getPid = async () => {
|
||||
const runtime = getRuntime();
|
||||
|
||||
let pid = 0;
|
||||
if (runtime.isDeno) {
|
||||
// @ts-ignore
|
||||
pid = Deno.pid;
|
||||
} else {
|
||||
pid = process.pid;
|
||||
}
|
||||
return pid;
|
||||
};
|
||||
export const writeAppid = async (pidPath = './app.pid') => {
|
||||
const fs = await import('node:fs');
|
||||
const pid = await getPid();
|
||||
fs.writeFileSync(pidPath, pid + '');
|
||||
};
|
||||
|
||||
export const getPidFromFileAndStop = async () => {
|
||||
const fs = await import('node:fs');
|
||||
if (fs.existsSync('./app.pid')) {
|
||||
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
|
||||
if (!isNaN(pid)) {
|
||||
if (pid === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
process.kill(pid);
|
||||
console.log(`Stopped process with PID ${pid}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop process with PID ${pid}:`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const runFirstCheck = async (path: string, pidPath: string) => {
|
||||
await getPidFromFileAndStop();
|
||||
await writeAppid(pidPath);
|
||||
try {
|
||||
const fs = await import('node:fs');
|
||||
if (fs.existsSync(path)) {
|
||||
fs.unlinkSync(path);
|
||||
console.log(`Socket file ${path} cleaned up during first check`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to clean up socket file ${path} during first check:`, error);
|
||||
}
|
||||
};
|
33
src/auto/listen/server-time.ts
Normal file
33
src/auto/listen/server-time.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export class ServerTimer {
|
||||
updatedAt: number;
|
||||
timer: any;
|
||||
timeout: number;
|
||||
onTimeout: any;
|
||||
interval = 10 * 1000;
|
||||
constructor(opts?: { timeout?: number }) {
|
||||
this.timeout = opts?.timeout || 15 * 60 * 1000;
|
||||
this.run();
|
||||
}
|
||||
startTimer() {
|
||||
const that = this;
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
this.timer = setInterval(() => {
|
||||
const updatedAt = Date.now();
|
||||
const timeout = that.timeout;
|
||||
const onTimeout = that.onTimeout;
|
||||
const isExpired = updatedAt - that.updatedAt > timeout;
|
||||
if (isExpired) {
|
||||
onTimeout?.();
|
||||
clearInterval(that.timer);
|
||||
that.timer = null;
|
||||
}
|
||||
}, that.interval);
|
||||
}
|
||||
|
||||
run(): number {
|
||||
this.updatedAt = Date.now();
|
||||
return this.updatedAt;
|
||||
}
|
||||
}
|
38
src/auto/load-ts.ts
Normal file
38
src/auto/load-ts.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { getRuntime } from './runtime.ts';
|
||||
import { glob } from './utils/glob.ts';
|
||||
type GlobOptions = {
|
||||
cwd?: string;
|
||||
load?: (args?: any) => Promise<any>;
|
||||
};
|
||||
|
||||
export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}): Promise<string[]> => {
|
||||
const runtime = getRuntime();
|
||||
if (runtime.isNode) {
|
||||
console.error(`Node.js is not supported`);
|
||||
return [];
|
||||
}
|
||||
if (runtime.isDeno) {
|
||||
// Deno 环境下
|
||||
return await glob(match);
|
||||
}
|
||||
if (runtime.isBun) {
|
||||
// Bun 环境下
|
||||
// @ts-ignore
|
||||
const { Glob } = await import('bun');
|
||||
const path = await import('path');
|
||||
// @ts-ignore
|
||||
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
|
||||
const files: string[] = [];
|
||||
for await (const file of glob.scan('.')) {
|
||||
files.push(path.join(cwd, file));
|
||||
}
|
||||
// @ts-ignore
|
||||
return Array.from(files);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => {
|
||||
const files = await getMatchFiles(match, { cwd });
|
||||
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
|
||||
};
|
19
src/auto/runtime.ts
Normal file
19
src/auto/runtime.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
type RuntimeEngine = 'node' | 'deno' | 'bun';
|
||||
|
||||
type Runtime = {
|
||||
isNode?: boolean;
|
||||
isDeno?: boolean;
|
||||
isBun?: boolean;
|
||||
engine: RuntimeEngine;
|
||||
};
|
||||
export const getRuntime = (): Runtime => {
|
||||
// @ts-ignore
|
||||
if (typeof Deno !== 'undefined') {
|
||||
return { isDeno: true, engine: 'deno' };
|
||||
}
|
||||
// @ts-ignore
|
||||
if (typeof Bun !== 'undefined') {
|
||||
return { isBun: true, engine: 'bun' };
|
||||
}
|
||||
return { isNode: true, engine: 'node' };
|
||||
};
|
83
src/auto/utils/glob.ts
Normal file
83
src/auto/utils/glob.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
type GlobOptions = {
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
export const glob = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}) => {
|
||||
const fs = await import('node:fs');
|
||||
const path = await import('node:path');
|
||||
|
||||
// 将 glob 模式转换为正则表达式
|
||||
const globToRegex = (pattern: string): RegExp => {
|
||||
const escaped = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*\*/g, '__DOUBLE_STAR__') // 临时替换 **
|
||||
.replace(/\*/g, '[^/]*') // * 匹配除 / 外的任意字符
|
||||
.replace(/__DOUBLE_STAR__/g, '.*') // ** 匹配任意字符包括 /
|
||||
.replace(/\?/g, '[^/]'); // ? 匹配除 / 外的单个字符
|
||||
return new RegExp(`^${escaped}$`);
|
||||
};
|
||||
|
||||
// 递归读取目录
|
||||
const readDirRecursive = async (dir: string): Promise<string[]> => {
|
||||
const files: string[] = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
} else if (entry.isDirectory()) {
|
||||
// 递归搜索子目录
|
||||
const subFiles = await readDirRecursive(fullPath);
|
||||
files.push(...subFiles);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略无法访问的目录
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
// 解析模式是否包含递归搜索
|
||||
const hasRecursive = match.includes('**');
|
||||
|
||||
try {
|
||||
let allFiles: string[] = [];
|
||||
|
||||
if (hasRecursive) {
|
||||
// 处理递归模式
|
||||
const basePath = match.split('**')[0];
|
||||
const startDir = path.resolve(cwd, basePath || '.');
|
||||
allFiles = await readDirRecursive(startDir);
|
||||
} else {
|
||||
// 处理非递归模式
|
||||
const dir = path.resolve(cwd, path.dirname(match));
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile()) {
|
||||
allFiles.push(path.join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建相对于 cwd 的匹配模式
|
||||
const normalizedMatch = path.resolve(cwd, match);
|
||||
const regex = globToRegex(normalizedMatch);
|
||||
|
||||
// 过滤匹配的文件
|
||||
const matchedFiles = allFiles.filter(file => {
|
||||
const normalizedFile = path.resolve(file);
|
||||
return regex.test(normalizedFile);
|
||||
});
|
||||
|
||||
return matchedFiles;
|
||||
} catch (error) {
|
||||
console.error(`Error in glob pattern "${match}":`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
@@ -1,2 +1,6 @@
|
||||
export type { Rule, Schema } from './rule.ts';
|
||||
import { z } from 'zod';
|
||||
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
|
||||
|
||||
export type { Rule } from './rule.ts';
|
||||
|
||||
export type Schema = z.ZodType<any, any, any>;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { z, ZodError, Schema } from 'zod';
|
||||
export { Schema };
|
||||
import { z, ZodError } from 'zod';
|
||||
type BaseRule = {
|
||||
value?: any;
|
||||
required?: boolean;
|
||||
@@ -64,7 +63,7 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
||||
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
||||
}
|
||||
};
|
||||
export const createSchema = (rule: Rule): Schema => {
|
||||
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
|
||||
try {
|
||||
rule.required = rule.required ?? false;
|
||||
if (!rule.required) {
|
||||
|
Reference in New Issue
Block a user