diff --git a/runtimes/nodejs/Dockerfile b/runtimes/nodejs/Dockerfile index f899bf8014..c1ededacf9 100644 --- a/runtimes/nodejs/Dockerfile +++ b/runtimes/nodejs/Dockerfile @@ -17,6 +17,7 @@ COPY . /app # COPY --chown=node:node . /app RUN mkdir /app/data || true RUN chown node:node /app/data +RUN chown node:node /app/functions # RUN npm install # RUN npm run build RUN chown -R node:node /app/node_modules diff --git a/runtimes/nodejs/README.md b/runtimes/nodejs/README.md index 8a0955a4e6..bb2ca06d1e 100644 --- a/runtimes/nodejs/README.md +++ b/runtimes/nodejs/README.md @@ -1,4 +1,3 @@ - # Intro `runtime-nodejs` is the application service engine of `laf`, responsible for: diff --git a/runtimes/nodejs/functions/global.d.ts b/runtimes/nodejs/functions/global.d.ts new file mode 100644 index 0000000000..808ec118f6 --- /dev/null +++ b/runtimes/nodejs/functions/global.d.ts @@ -0,0 +1,79 @@ +/** + * The input parameters of cloud function calls + */ +declare interface FunctionContext { + __function_name: string + + /** + * This object is parsed from JWT Token Payload + */ + user?: { + [key: string]: any + } + + /** + * Uploaded file, the file object array + */ + files?: File[] + + /** + * HTTP headers + */ + headers?: IncomingHttpHeaders + + /** + * HTTP Query parameter (URL parameter), JSON object + */ + query?: any + + /** + * HTTP Body + */ + body?: any + + /** + * + */ + params?: any + + /** + * HTTP Request ID + */ + requestId?: string + + /** + * HTTP Method + */ + method?: string + + /** + * Express request object + */ + request?: HttpRequest + + /** + * Express response object + */ + response?: HttpResponse + + /** + * WebSocket object + */ + socket?: WebSocket + + [key: string]: any +} + +interface IModule { + exports: any +} + +interface IProcess { + /** + * Environment + */ + env: any +} + +declare const module: IModule +declare const process: IProcess diff --git a/runtimes/nodejs/functions/tsconfig.json b/runtimes/nodejs/functions/tsconfig.json new file mode 100644 index 0000000000..b12affb72f --- /dev/null +++ b/runtimes/nodejs/functions/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false, + "baseUrl": "./", + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "./*" + ] +} \ No newline at end of file diff --git a/runtimes/nodejs/package-lock.json b/runtimes/nodejs/package-lock.json index 6a81334285..fe901cb09d 100644 --- a/runtimes/nodejs/package-lock.json +++ b/runtimes/nodejs/package-lock.json @@ -34,7 +34,10 @@ "node-modules-utils": "^1.0.0-beta.14", "nodemailer": "^6.6.3", "pako": "^2.1.0", + "typescript-language-server": "^3.3.2", "validator": "^13.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-ws-jsonrpc-cjs": "^3.0.0", "ws": "^8.11.0", "zlib": "^1.0.5" }, @@ -1445,6 +1448,23 @@ "node": ">=14.0.0" } }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.0.0.tgz", + "integrity": "sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.0.1.tgz", + "integrity": "sha512-N2oCZRglhWKm7iMBu7S6wDzXirjAofi7tAd26cxmgibRYOBS4D3hGfmkwCpHdASZzwZDD8rluh0Rcqw1JeZDRw==", + "dependencies": { + "@smithy/util-base64": "^2.0.1", + "tslib": "^2.5.0" + } + }, "node_modules/@smithy/config-resolver": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.22.tgz", @@ -5505,6 +5525,17 @@ "node": ">=14.17" } }, + "node_modules/typescript-language-server": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-3.3.2.tgz", + "integrity": "sha512-jzun53CIkTbpAki0nP+hk5baGW+86SNNlVhyIj2ZUy45zUkCnmoetWuAtfRRQYrlIr8x4QB3ymGJPuwDQSd/ew==", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/underscore": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", @@ -5610,6 +5641,59 @@ "extsprintf": "^1.2.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/vscode-ws-jsonrpc-cjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc-cjs/-/vscode-ws-jsonrpc-cjs-3.0.0.tgz", + "integrity": "sha512-4ZomHJ+UczRPvBEhCqPx8Qp2phqSYEXawyAqtnzrmVp4DNGQErUelj+hh7iGl0Y7mn0PHJOrppx/Nce5LH9JZw==", + "dependencies": { + "vscode-jsonrpc": "~8.1.0" + }, + "engines": { + "node": ">=16.11.0", + "npm": ">=8.0.0" + } + }, "node_modules/web-encoding": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", diff --git a/runtimes/nodejs/package.json b/runtimes/nodejs/package.json index 9b1559be5d..c7eba0ff06 100644 --- a/runtimes/nodejs/package.json +++ b/runtimes/nodejs/package.json @@ -51,7 +51,10 @@ "node-modules-utils": "^1.0.0-beta.14", "nodemailer": "^6.6.3", "pako": "^2.1.0", + "typescript-language-server": "^3.3.2", "validator": "^13.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-ws-jsonrpc-cjs": "^3.0.0", "ws": "^8.11.0", "zlib": "^1.0.5" }, @@ -83,4 +86,4 @@ "lint-staged": { "*.{ts,js}": "eslint --fix" } -} +} \ No newline at end of file diff --git a/runtimes/nodejs/src/handler/typings.ts b/runtimes/nodejs/src/handler/typings.ts index 7e0a47494a..74eb1b05f5 100644 --- a/runtimes/nodejs/src/handler/typings.ts +++ b/runtimes/nodejs/src/handler/typings.ts @@ -21,7 +21,6 @@ const nodeModulesRoot = path.resolve(__dirname, '../../node_modules') * Gets declaration files of a dependency package */ export async function handlePackageTypings(req: IRequest, res: Response) { - // verify the debug token const token = req.get('x-laf-develop-token') if (!token) { @@ -87,9 +86,12 @@ export async function handlePackageTypings(req: IRequest, res: Response) { } } - - -async function getThreePartyPackageTypings(req: IRequest, res: Response, basePath: string, packageName: string) { +async function getThreePartyPackageTypings( + req: IRequest, + res: Response, + basePath: string, + packageName: string, +) { const requestId = req['requestId'] try { // Gets other three-party package types @@ -106,4 +108,4 @@ async function getThreePartyPackageTypings(req: IRequest, res: Response, basePat error: error.toString(), }) } -} \ No newline at end of file +} diff --git a/runtimes/nodejs/src/index.ts b/runtimes/nodejs/src/index.ts index 1090d2fb91..17e31635e9 100644 --- a/runtimes/nodejs/src/index.ts +++ b/runtimes/nodejs/src/index.ts @@ -21,6 +21,9 @@ import xmlparser from 'express-xml-bodyparser' import './support/cloud-sdk' import storageServer from './storage-server' import { DatabaseChangeStream } from './support/database-change-stream' +import url from 'url' + +import { LspWebSocket } from './support/lsp' import { createCloudSdk } from './support/cloud-sdk' // hack: set createCloudSdk to global object to make it available in @lafjs/cloud package @@ -97,6 +100,12 @@ const server = app.listen(Config.PORT, () => * WebSocket upgrade & connect */ server.on('upgrade', (req, socket, head) => { + const pathname = req.url ? url.parse(req.url).pathname : undefined + if (pathname === '/_/lsp') { + LspWebSocket.handleUpgrade(req, socket, head) + return + } + WebSocketAgent.server.handleUpgrade(req, socket as any, head, (client) => { WebSocketAgent.server.emit('connection', client, req) }) diff --git a/runtimes/nodejs/src/support/lsp.ts b/runtimes/nodejs/src/support/lsp.ts new file mode 100644 index 0000000000..f33b198d3c --- /dev/null +++ b/runtimes/nodejs/src/support/lsp.ts @@ -0,0 +1,178 @@ +import path from 'path' +import fs from 'fs' +import * as rpc from 'vscode-ws-jsonrpc-cjs' +import * as lsp from 'vscode-languageserver' +import * as server from 'vscode-ws-jsonrpc-cjs/server' +import * as ws from 'ws' +import { IncomingMessage } from 'http' +import { Duplex } from 'stream' +import { uniqueId } from 'lodash' +import { logger } from './logger' +import { parseToken } from './token' + +const WORKSPACE_PATH = path.join(__dirname, '../../functions') + +export class LspWebSocket { + private static lspWS = new ws.WebSocketServer({ + noServer: true, + perMessageDeflate: false, + }) + + static handleUpgrade( + request: IncomingMessage, + socket: Duplex, + upgradeHead: Buffer, + ) { + // verify the debug token + { + const token = request.headers['sec-websocket-protocol'] as string + if (!token) { + request.destroy(new Error('x-laf-develop-token is needed')) + return + } + + const auth = parseToken(token) || null + if (auth?.type !== 'develop') { + request.destroy(new Error('permission denied: invalid develop token')) + return + } + } + + this.lspWS.handleUpgrade(request, socket, upgradeHead, (webSocket) => { + const socket = { + send: (content) => + webSocket.send(content, (error) => { + if (error) { + throw error + } + }), + onMessage: (cb) => webSocket.on('message', cb), + onError: (cb) => webSocket.on('error', cb), + onClose: (cb) => webSocket.on('close', cb), + dispose: () => webSocket.close(), + } + + if (webSocket.readyState === webSocket.OPEN) { + const uid = uniqueId() + this.launchLsp(socket, uid) + logger.info(`Launched ts lsp server ${uid}`) + } + }) + } + + static launchLsp(socket: rpc.IWebSocket, uid: string) { + if (!fs.existsSync(WORKSPACE_PATH)) { + fs.mkdirSync(WORKSPACE_PATH) + } + + const reader = new rpc.WebSocketMessageReader(socket) + const writer = new rpc.WebSocketMessageWriter(socket) + + // start the language server as an external process + const tlsPath = path.join( + __dirname, + '../../node_modules/typescript-language-server/lib/cli.mjs', + ) + + const socketConnection = server.createConnection(reader, writer, () => + socket.dispose(), + ) + const serverConnection = server.createServerProcess( + 'typescript-language-server', + 'node', + [ + tlsPath, + '--stdio', + '--log-level', + '4', + '--tsserver-log-verbosity', + 'off', + ], + ) + + server.forward( + socketConnection, + serverConnection, + (message: lsp.Message & { method: string }) => { + if ( + message.method === 'window/logMessage' || + message.method === 'initialize' + ) { + return message + } + + // @ts-ignore + if (message?.error?.code === -32601) { + // suppress error messages that cause lsp server to shut down + return '' as any + } + + if (lsp.Message.isRequest(message)) { + if (message.method === lsp.InitializeRequest.type.method) { + const initializeParams = message.params as lsp.InitializeParams + initializeParams.processId = process.pid + initializeParams.workspaceFolders = [ + { + name: 'statics', + uri: `file://${WORKSPACE_PATH}`, + }, + ] + } + } + + if (lsp.Message.isNotification(message)) { + if ( + message.method === lsp.DidOpenTextDocumentNotification.type.method + ) { + const params = message.params as lsp.DidOpenTextDocumentParams + const [, fullPath] = params.textDocument.uri.split('//') + + if (!fullPath.includes('node_modules')) { + // write file if not exists + if (!fs.existsSync(fullPath)) { + const dir = path.dirname(fullPath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { + recursive: true, + }) + } + fs.writeFileSync(fullPath, params.textDocument.text) + } + } + } + + if ( + message.method === lsp.DidCloseTextDocumentNotification.type.method + ) { + const params = message.params as lsp.DidCloseTextDocumentParams + const [, fullPath] = params.textDocument.uri.split('//') + + if (!fullPath.includes('node_modules')) { + // delete file if exists + if (fs.existsSync(fullPath)) { + fs.unlinkSync(fullPath) + } + + let dir = path.dirname(fullPath) + + // remove empty dir + if (dir.startsWith(WORKSPACE_PATH)) { + while (dir !== WORKSPACE_PATH) { + if (fs.readdirSync(dir).length === 0) { + fs.rmdirSync(dir) + dir = path.dirname(dir) + } + } + } + } + } + } + return message + }, + ) + + socketConnection.onClose(() => { + logger.info(`lsp server ${uid} closed`) + }) + } +} diff --git a/runtimes/nodejs/start.sh b/runtimes/nodejs/start.sh index 2051d50b80..f2ac8d0427 100644 --- a/runtimes/nodejs/start.sh +++ b/runtimes/nodejs/start.sh @@ -1,5 +1,7 @@ #!/bin/sh +ln -s $CUSTOM_DEPENDENCY_BASE_PATH/node_modules $PWD/functions/node_modules > /dev/null 2>&1 + # source .env echo "****** start service: node $FLAGS --experimental-vm-modules --experimental-fetch ./dist/index.js *******" exec node $FLAGS --experimental-vm-modules --experimental-fetch ./dist/index.js \ No newline at end of file diff --git a/server/src/function/dto/create-function.dto.ts b/server/src/function/dto/create-function.dto.ts index 79aa4aa7db..69983fedd8 100644 --- a/server/src/function/dto/create-function.dto.ts +++ b/server/src/function/dto/create-function.dto.ts @@ -43,11 +43,9 @@ export class CreateFunctionDto { if (this.tags?.length >= 8) { return 'tags length must less than 8' } - - if (this.name.includes('./') || this.name.includes('../')) { + if (this.name.includes('./')) { return 'the relative path is not allowed in function name' } - return null } }