From 574317a42d54832fccf44a5c7940f04dda9c8a33 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Fri, 20 Jun 2025 12:07:09 +0800 Subject: [PATCH] update: add local-proxy library --- .../src/module/assistant/proxy/file-proxy.ts | 2 +- assistant/src/module/local-proxy/.npmrc | 2 + assistant/src/module/local-proxy/file.ts | 20 +++ assistant/src/module/local-proxy/index.ts | 132 ++++++++++++++++++ assistant/src/module/local-proxy/package.json | 17 +++ assistant/src/module/local-proxy/proxy.ts | 40 ++++++ .../module/local-proxy/proxy/file-proxy.ts | 54 +++++++ .../src/module/local-proxy/proxy/proxy.ts | 60 ++++++++ assistant/src/module/local-proxy/readme.md | 15 ++ assistant/src/services/proxy/local-proxy.ts | 112 +-------------- .../src/services/proxy/proxy-page-index.ts | 13 +- package.json | 4 +- src/command/deploy.ts | 22 ++- test/deploy-test/index.html | 1 + 14 files changed, 361 insertions(+), 133 deletions(-) create mode 100644 assistant/src/module/local-proxy/.npmrc create mode 100644 assistant/src/module/local-proxy/file.ts create mode 100644 assistant/src/module/local-proxy/index.ts create mode 100644 assistant/src/module/local-proxy/package.json create mode 100644 assistant/src/module/local-proxy/proxy.ts create mode 100644 assistant/src/module/local-proxy/proxy/file-proxy.ts create mode 100644 assistant/src/module/local-proxy/proxy/proxy.ts create mode 100644 assistant/src/module/local-proxy/readme.md create mode 100644 test/deploy-test/index.html diff --git a/assistant/src/module/assistant/proxy/file-proxy.ts b/assistant/src/module/assistant/proxy/file-proxy.ts index 344c5ac..1aa2f03 100644 --- a/assistant/src/module/assistant/proxy/file-proxy.ts +++ b/assistant/src/module/assistant/proxy/file-proxy.ts @@ -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'; diff --git a/assistant/src/module/local-proxy/.npmrc b/assistant/src/module/local-proxy/.npmrc new file mode 100644 index 0000000..7446745 --- /dev/null +++ b/assistant/src/module/local-proxy/.npmrc @@ -0,0 +1,2 @@ +//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} +//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/assistant/src/module/local-proxy/file.ts b/assistant/src/module/local-proxy/file.ts new file mode 100644 index 0000000..031447c --- /dev/null +++ b/assistant/src/module/local-proxy/file.ts @@ -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; + } +}; \ No newline at end of file diff --git a/assistant/src/module/local-proxy/index.ts b/assistant/src/module/local-proxy/index.ts new file mode 100644 index 0000000..0e8b2f9 --- /dev/null +++ b/assistant/src/module/local-proxy/index.ts @@ -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(); + } +} diff --git a/assistant/src/module/local-proxy/package.json b/assistant/src/module/local-proxy/package.json new file mode 100644 index 0000000..a9c9900 --- /dev/null +++ b/assistant/src/module/local-proxy/package.json @@ -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 (https://www.xiongxiao.me)", + "license": "MIT", + "packageManager": "pnpm@10.11.0", + "type": "module" +} diff --git a/assistant/src/module/local-proxy/proxy.ts b/assistant/src/module/local-proxy/proxy.ts new file mode 100644 index 0000000..3af27d9 --- /dev/null +++ b/assistant/src/module/local-proxy/proxy.ts @@ -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'); +}; diff --git a/assistant/src/module/local-proxy/proxy/file-proxy.ts b/assistant/src/module/local-proxy/proxy/file-proxy.ts new file mode 100644 index 0000000..ea04cac --- /dev/null +++ b/assistant/src/module/local-proxy/proxy/file-proxy.ts @@ -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; + } +}; diff --git a/assistant/src/module/local-proxy/proxy/proxy.ts b/assistant/src/module/local-proxy/proxy/proxy.ts new file mode 100644 index 0000000..bec35a1 --- /dev/null +++ b/assistant/src/module/local-proxy/proxy/proxy.ts @@ -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', + }, +] + */ diff --git a/assistant/src/module/local-proxy/readme.md b/assistant/src/module/local-proxy/readme.md new file mode 100644 index 0000000..818e407 --- /dev/null +++ b/assistant/src/module/local-proxy/readme.md @@ -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); +``` diff --git a/assistant/src/services/proxy/local-proxy.ts b/assistant/src/services/proxy/local-proxy.ts index a5591f0..e93c344 100644 --- a/assistant/src/services/proxy/local-proxy.ts +++ b/assistant/src/services/proxy/local-proxy.ts @@ -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'; diff --git a/assistant/src/services/proxy/proxy-page-index.ts b/assistant/src/services/proxy/proxy-page-index.ts index 5ccaef9..8d94603 100644 --- a/assistant/src/services/proxy/proxy-page-index.ts +++ b/assistant/src/services/proxy/proxy-page-index.ts @@ -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, }); } diff --git a/package.json b/package.json index 8540bc5..18cf26f 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/command/deploy.ts b/src/command/deploy.ts index ffe7593..3e16532 100644 --- a/src/command/deploy.ts +++ b/src/command/deploy.ts @@ -192,19 +192,17 @@ 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) => { - try { - if (typeof res === 'string') { - return JSON.parse(res); - } else { - return res; - } - } catch (error) { - return typeof res === 'string' ? {} : 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); + } else { + return res; } - }); + } catch (error) { + return typeof res === 'string' ? {} : res; + } + }); const checkData: { path: string; isUpload: boolean }[] = res.data; if (res.code !== 200) { console.error('check failed', res); diff --git a/test/deploy-test/index.html b/test/deploy-test/index.html new file mode 100644 index 0000000..f2ba8f8 --- /dev/null +++ b/test/deploy-test/index.html @@ -0,0 +1 @@ +abc \ No newline at end of file