import http from 'http'; import connect from 'connect'; import queryString from 'querystring'; import bodyParser from 'body-parser'; import httpProxy from 'http-proxy'; import chokidar from 'chokidar'; import { defaults } from 'lodash'; import path from 'path'; import decache from 'decache'; import { guid, requireLocalFunction } from './utils'; import invokeLocalFunction from './invokeLocalFunction'; class LocalFunctionServer { options; proxy = httpProxy.createProxyServer({}); app = connect(); req; res; constructor(options) { this.options = defaults(options, { port: 3636, endpoint: 'http://api.bspapp.com', }); this.options.port = Number(this.options.port); this.init(); } private async invokeLocalCloudFunction() { const { endpoint, project, appId, privateKey } = this.options; const { req, res } = this; const { requestId, spaceId, token, params } = req.body; const mpSource = req.url === '/function' ? 'function' : 'client'; req['x-egg-trace-id'] = requestId; req['x-basement-endpoint'] = endpoint; req['x-basement-trace-id'] = requestId; res.setHeader('content-type', 'application/json; charset=utf-8'); res.setHeader('request-id', requestId); try { const functionResult = await invokeLocalFunction({ appId, spaceId, project, name: params.functionTarget, functionArgs: params.functionArgs, token, userAgent: req.headers['user-agent'], clientIp: '127.0.0.1', mpSource, serverSecret: privateKey, }); const result = { success: true, result: functionResult, }; const endResult = JSON.stringify(result); res.end(endResult); } catch (e) { const errorResult = { success: false, error: { code: e.code || 'FunctionCommonError', message: e.message, }, }; res.end(JSON.stringify(errorResult)); } } private init() { const { endpoint } = this.options; const { app, proxy } = this; //restream parsed body before proxying proxy.on('proxyReq', function (proxyReq, req) { let bodyData; const contentType = proxyReq.getHeader('Content-Type'); // TS MODIFY if (!req['body'] || !Object.keys(req['body']).length) return; if (contentType === 'application/json') bodyData = JSON.stringify(req['body']); if (contentType === 'application/x-www-form-urlencoded') bodyData = queryString.stringify(req['body']); if (bodyData) { proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); proxyReq.write(bodyData); } }); app.use(bodyParser.json()); //json parser app.use(bodyParser.urlencoded({ extended: false })); app.use((req: any, res) => { if (req.url === '/debugger-proxy?role=chrome') return; const params = JSON.parse(req.body.params); this.req = req; this.res = res; if ( req.body.method === 'serverless.function.runtime.invoke' && params.functionTarget && params.functionTarget !== 'alipay-openapi' ) { req.body.params = params; req.body.requestId = guid(); this.invokeLocalCloudFunction(); return; } proxy.web(req, res, { target: endpoint, changeOrigin: true, }); }); } private watchFileChange() { const { project } = this.options; //监听本地云函数修改 const watcher = chokidar.watch(project, { // 不进行node_modules文件的监听 ignored: /node_modules/, persistent: true, }); watcher.on('change', (filepath) => { // 只监听js和json文件 if (!/\.(js|json)$/.test(filepath)) return; const functionName = path.relative(project, filepath).split(path.sep)[0]; const entry = path.join(project, functionName, 'index.js'); // 删除当前云函数的require缓存 decache(entry); // 重新require当前云函数 requireLocalFunction(project, functionName); }); } /** * 启动本地服务 */ async createServer(): Promise { const { port } = this.options; const server = http.createServer(this.app); this.watchFileChange(); await new Promise((resolve) => { server.listen(port, resolve); }); return server; } } export default LocalFunctionServer;