update: add local-proxy library

This commit is contained in:
熊潇 2025-06-20 12:07:09 +08:00
parent 3c0cb64478
commit 574317a42d
14 changed files with 361 additions and 133 deletions

View File

@ -1,7 +1,7 @@
import http from 'node:http';
import send from 'send';
import fs from 'node:fs';
import path from 'path';
import path from 'node:path';
import { ProxyInfo } from './proxy.ts';
import { checkFileExists } from '../file/index.ts';
import { log } from '@/module/logger.ts';

View File

@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@ -0,0 +1,20 @@
import fs from 'node:fs';
import path from 'node:path';
/**
*
* @param filePath
* @param checkIsFile default: false
* @returns
*/
export const checkFileExists = (filePath: string, checkIsFile = false) => {
try {
fs.accessSync(filePath);
if (checkIsFile) {
return fs.statSync(filePath).isFile();
}
return true;
} catch (error) {
return false;
}
};

View File

@ -0,0 +1,132 @@
import fs from 'node:fs';
// import { AssistantConfig, checkFileExists } from '@/module/assistant/index.ts';
import { checkFileExists } from './file.ts';
import path from 'node:path';
type AssistantConfig = {
configPath?: {
pagesDir?: string;
};
getCacheAssistantConfig?: () => {
watch?: {
enabled?: boolean;
};
};
};
type ProxyType = {
user: string;
key: string;
path: string;
indexPath: string;
absolutePath?: string;
};
export type LocalProxyOpts = {
watch?: boolean; // 是否监听文件变化
pagesDir?: string; // 前端应用路径
init?: boolean;
};
export class LocalProxy {
localProxyProxyList: ProxyType[] = [];
pagesDir: string;
watch?: boolean;
watching?: boolean;
initing?: boolean;
constructor(opts?: LocalProxyOpts) {
this.pagesDir = opts?.pagesDir || '';
this.watch = opts?.watch ?? false;
if (opts.init) {
this.init();
if (this.watch) {
this.onWatch();
}
}
}
initFromAssistantConfig(assistantConfig?: AssistantConfig) {
if (!assistantConfig) return;
this.pagesDir = assistantConfig.configPath?.pagesDir || '';
this.watch = !!assistantConfig.getCacheAssistantConfig()?.watch.enabled;
this.init();
if (this.watch) {
this.onWatch();
}
}
init() {
const frontAppDir = this.pagesDir;
if (frontAppDir) {
if (this.initing) {
return;
}
if (!checkFileExists(frontAppDir)) {
fs.mkdirSync(frontAppDir, { recursive: true });
}
this.initing = true;
const userList = fs.readdirSync(frontAppDir);
const localProxyProxyList: ProxyType[] = [];
userList.forEach((user) => {
const userPath = path.join(frontAppDir, user);
const stat = fs.statSync(userPath);
if (stat.isDirectory()) {
const appList = fs.readdirSync(userPath);
appList.forEach((app) => {
const appPath = path.join(userPath, app);
const indexPath = path.join(appPath, 'index.html');
if (!checkFileExists(indexPath, true)) {
return;
}
// const appPath = `${appPath}/index.html`;
if (checkFileExists(indexPath, true)) {
localProxyProxyList.push({
user: user,
key: app,
path: `/${user}/${app}/`,
indexPath: `${user}/${app}/index.html`,
absolutePath: appPath,
});
}
});
}
});
this.localProxyProxyList = localProxyProxyList;
this.initing = false;
}
}
onWatch() {
// 监听文件变化
const frontAppDir = this.pagesDir;
const that = this;
if (!this.watch && !frontAppDir) {
return;
}
if (this.watching) {
return;
}
that.watching = true;
let timer: NodeJS.Timeout;
const debounce = (fn: () => void, delay: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
}, delay);
};
fs.watch(frontAppDir, { recursive: true }, (eventType, filename) => {
if (eventType === 'rename' || eventType === 'change') {
const filePath = path.join(frontAppDir, filename);
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory() || filename.endsWith('.html')) {
// 重新加载
debounce(that.init.bind(that), 5 * 1000);
}
} catch (error) {}
}
});
}
getLocalProxyList() {
return this.localProxyProxyList;
}
reload() {
this.init();
}
}

View File

@ -0,0 +1,17 @@
{
"name": "@kevisual/local-proxy",
"version": "0.0.6",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"publishConfig": {
"access": "public"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.11.0",
"type": "module"
}

View File

@ -0,0 +1,40 @@
import { LocalProxy, LocalProxyOpts } from './index.ts';
import http from 'node:http';
import { fileProxy } from './proxy/file-proxy.ts';
const localProxy = new LocalProxy({});
let home = '/root/center';
export const initProxy = (data: LocalProxyOpts & { home?: string }) => {
localProxy.pagesDir = data.pagesDir || '';
localProxy.watch = data.watch ?? false;
localProxy.init();
home = data.home;
};
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const url = new URL(req.url, 'http://localhost');
const pathname = decodeURIComponent(url.pathname);
if (pathname === '/') {
res.writeHead(302, { Location: home + '/' });
return res.end();
}
if (pathname.startsWith('/favicon.ico')) {
res.statusCode = 404;
res.end('Not Found Favicon');
return;
}
if (pathname.startsWith('/client') || pathname.startsWith('/api') || pathname.startsWith('/v1') || pathname.startsWith('/serve')) {
console.debug('handle by router');
return;
}
const localProxyProxyList = localProxy.getLocalProxyList();
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
if (localProxyProxy) {
return fileProxy(req, res, {
path: localProxyProxy.path,
rootPath: localProxy.pagesDir,
indexPath: localProxyProxy.indexPath,
});
}
res.statusCode = 404;
res.end('Not Found Proxy');
};

View File

@ -0,0 +1,54 @@
import http from 'node:http';
import send from 'send';
import fs from 'node:fs';
import path from 'node:path';
import { ProxyInfo } from './proxy.ts';
import { checkFileExists } from '../file.ts';
// import { log } from '@/module/logger.ts';
const log = console;
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
// url开头的文件
const url = new URL(req.url, 'http://localhost');
const [user, key, _info] = url.pathname.split('/');
const pathname = url.pathname.slice(1);
const { indexPath = '', target = '', rootPath = process.cwd() } = proxyApi;
if (!indexPath) {
return res.end('Not Found indexPath');
}
try {
// 检测文件是否存在如果文件不存在则返回404
let filePath = '';
let exist = false;
if (_info) {
filePath = path.join(rootPath, target, pathname);
exist = checkFileExists(filePath, true);
}
if (!exist) {
filePath = path.join(rootPath, target, indexPath);
exist = checkFileExists(filePath, true);
}
log.debug('filePath', { filePath, exist });
if (!exist) {
res.statusCode = 404;
res.end('Not Found File');
return;
}
const ext = path.extname(filePath);
let maxAge = 24 * 60 * 60 * 1000; // 24小时
if (ext === '.html') {
maxAge = 0;
}
let sendFilePath = path.relative(rootPath, filePath);
const file = send(req, sendFilePath, {
root: rootPath,
maxAge,
});
file.pipe(res);
} catch (error) {
res.statusCode = 404;
res.end('Error:Not Found File');
return;
}
};

View File

@ -0,0 +1,60 @@
export type ProxyInfo = {
/**
* , /root/center,
*/
path?: string;
/**
*
*/
target?: string;
/**
*
*/
type?: 'file' | 'dynamic' | 'minio' | 'http';
/**
* pathname url.pathname, pathname使pathname作为请求的url.pathname
* @default undefined
* @example /api/v1/user
*/
pathname?: string;
/**
* 使websocket, http
* @default false
*/
ws?: boolean;
/**
* index.html type为fileProxy代理有用 访
*/
indexPath?: string;
/**
* , process.cwd(), type为fileProxy代理有用
*/
rootPath?: string;
};
export type ApiList = {
path: string;
/**
* url或者相对路径
*/
target: string;
/**
*
*/
ws?: boolean;
/**
*
*/
type?: 'static' | 'dynamic' | 'minio';
}[];
/**
[
{
path: '/api/v1/user',
target: 'http://localhost:3000/api/v1/user',
type: 'dynamic',
},
]
*/

View File

@ -0,0 +1,15 @@
```ts
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
initProxy({
pagesDir: './demo',
watch: true,
});
import { App } from '../app.ts';
const app = new App();
app.listen(2233, () => {
console.log('Server is running on http://localhost:2233');
});
app.onServerRequest(proxyRoute);
```

View File

@ -1,111 +1 @@
import fs from 'node:fs';
import { AssistantConfig, checkFileExists } from '@/module/assistant/index.ts';
import path from 'node:path';
import { logger } from '@/module/logger.ts';
type ProxyType = {
user: string;
key: string;
path: string;
indexPath: string;
absolutePath?: string;
};
export type LocalProxyOpts = {
assistantConfig?: AssistantConfig; // 前端应用路径
// watch?: boolean; // 是否监听文件变化
};
export class LocalProxy {
localProxyProxyList: ProxyType[] = [];
assistantConfig?: AssistantConfig;
watch?: boolean;
watching?: boolean;
initing?: boolean;
constructor(opts?: LocalProxyOpts) {
this.assistantConfig = opts?.assistantConfig;
if (this.assistantConfig) {
this.watch = !!this.assistantConfig.getCacheAssistantConfig()?.watch.enabled;
this.init();
console.log('init local proxy', this.assistantConfig.getAppList);
if (this.watch) {
this.onWatch();
}
}
}
init() {
const frontAppDir = this.assistantConfig.configPath?.pagesDir;
if (frontAppDir) {
if (this.initing) {
return;
}
this.initing = true;
const userList = fs.readdirSync(frontAppDir);
const localProxyProxyList: ProxyType[] = [];
userList.forEach((user) => {
const userPath = path.join(frontAppDir, user);
const stat = fs.statSync(userPath);
if (stat.isDirectory()) {
const appList = fs.readdirSync(userPath);
appList.forEach((app) => {
const appPath = path.join(userPath, app);
const indexPath = path.join(appPath, 'index.html');
if (!checkFileExists(indexPath, true)) {
return;
}
// const appPath = `${appPath}/index.html`;
if (checkFileExists(indexPath, true)) {
localProxyProxyList.push({
user: user,
key: app,
path: `/${user}/${app}/`,
indexPath: `${user}/${app}/index.html`,
absolutePath: appPath,
});
}
});
}
});
this.localProxyProxyList = localProxyProxyList;
this.initing = false;
}
}
onWatch() {
// 监听文件变化
const frontAppDir = this.assistantConfig.configPath?.pagesDir;
const that = this;
if (!this.watch && !frontAppDir) {
return;
}
if (this.watching) {
return;
}
that.watching = true;
let timer: NodeJS.Timeout;
const debounce = (fn: () => void, delay: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
logger.info('reload local proxy');
}, delay);
};
fs.watch(frontAppDir, { recursive: true }, (eventType, filename) => {
if (eventType === 'rename' || eventType === 'change') {
const filePath = path.join(frontAppDir, filename);
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory() || filename.endsWith('.html')) {
// 重新加载
debounce(that.init.bind(that), 5 * 1000);
}
} catch (error) {}
}
});
}
getLocalProxyList() {
return this.localProxyProxyList;
}
reload() {
this.init();
}
}
export * from '@/module/local-proxy/index.ts';

View File

@ -1,16 +1,15 @@
import { fileProxy, httpProxy, createApiProxy, wsProxy } from '@/module/assistant/index.ts';
import http from 'http';
import http from 'node:http';
import { LocalProxy } from './local-proxy.ts';
import { assistantConfig, app } from '@/app.ts';
import { log, logger } from '@/module/logger.ts';
const localProxy = new LocalProxy({
assistantConfig,
});
const localProxy = new LocalProxy({});
localProxy.initFromAssistantConfig(assistantConfig);
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const _assistantConfig = assistantConfig.getCacheAssistantConfig();
const appDir = assistantConfig.configPath?.pagesDir;
const url = new URL(req.url, 'http://localhost');
const pathname = url.pathname;
const pathname = decodeURIComponent(url.pathname);
if (pathname === '/' && _assistantConfig?.home) {
res.writeHead(302, { Location: `${_assistantConfig?.home}/` });
return res.end();
@ -71,7 +70,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
log.log('localProxyProxy', { localProxyProxy, url: req.url });
return fileProxy(req, res, {
path: localProxyProxy.path,
rootPath: appDir,
rootPath: localProxy.pagesDir,
indexPath: localProxyProxy.indexPath,
});
}

View File

@ -1,6 +1,6 @@
{
"name": "@kevisual/cli",
"version": "0.0.57-beta.1",
"version": "0.0.57-beta.2",
"description": "envision command tools",
"main": "dist/app.mjs",
"type": "module",
@ -64,7 +64,7 @@
"ignore": "^7.0.5",
"inquirer": "^12.6.3",
"jsonwebtoken": "^9.0.2",
"rollup": "^4.43.0",
"rollup": "^4.44.0",
"rollup-plugin-dts": "^6.2.1",
"tar": "^7.4.3",
"zustand": "^5.0.5"

View File

@ -192,9 +192,7 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
}
const token = await storage.getItem('token');
const checkUrl = new URL('/api/s1/resources/upload/check', getBaseURL());
const res = await query
.adapter({ url: checkUrl.toString(), method: 'POST', body: data, headers: { Authorization: 'Bearer ' + token, contentType: 'application/json' } })
.then((res) => {
const res = await query.adapter({ url: checkUrl.toString(), method: 'POST', body: data, headers: { Authorization: 'Bearer ' + token } }).then((res) => {
try {
if (typeof res === 'string') {
return JSON.parse(res);

View File

@ -0,0 +1 @@
abc