diff --git a/.changeset/config.json b/.changeset/config.json index 7fcf973..0ac7b73 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": ["webview", "lsp-yapi-to-code-server"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 11217e9..f8e60f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,53 +3,64 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "attach", - "name": "Attach to Server", - "port": 6011, - "restart": true, - "outFiles": ["${workspaceFolder}/dist/**/server.js"], - "env": { + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach to Server", + "port": 6011, + "restart": true, + "outFiles": ["${workspaceFolder}/dist/server.js"], + "sourceMaps": true, + "sourceMapPathOverrides": { + "./src/*": "${workspaceFolder}/src/server/src/*" + }, + "skipFiles": [ + "/**", + "${workspaceFolder}/node_modules/**" + ], + "env": { "DEBUG": "*" } - }, - { - "name": "Launch Client", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/dist/**/extension.js" - ], - "preLaunchTask": "${defaultBuildTask}", - "env": { - "DEBUG": "*" + }, + { + "name": "Launch Client", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "sourceMaps": true, + "sourceMapPathOverrides": { + "./src/*": "${workspaceFolder}/src/*" }, - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - ], - "preLaunchTask": "tasks: watch-tests" - } - ], - "compounds": [ - { - "name": "Client + Server", - "configurations": ["Launch Client", "Attach to Server"] - } - ] + "outFiles": [ + "${workspaceFolder}/dist/extension.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "DEBUG": "*" + } + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "tasks: watch-tests" + } + ], + "compounds": [ + { + "name": "Client + Server", + "configurations": ["Launch Client", "Attach to Server"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 36e36b0..01ef6e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,52 +1,56 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - // Disable the default formatter, use eslint instead - "prettier.enable": false, - "editor.formatOnSave": false, + "typescript.tsdk": "node_modules/typescript/lib", + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, - // Auto fix - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", - "source.organizeImports": "never", - "source.fixAll.markdownlint": "explicit" - }, + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never", + "source.fixAll.markdownlint": "explicit" + }, - // Silent the stylistic rules in you IDE, but still auto fix them - "eslint.rules.customizations": [ - { "rule": "style/*", "severity": "off", "fixable": true }, - { "rule": "format/*", "severity": "off", "fixable": true }, - { "rule": "*-indent", "severity": "off", "fixable": true }, - { "rule": "*-spacing", "severity": "off", "fixable": true }, - { "rule": "*-spaces", "severity": "off", "fixable": true }, - { "rule": "*-order", "severity": "off", "fixable": true }, - { "rule": "*-dangle", "severity": "off", "fixable": true }, - { "rule": "*-newline", "severity": "off", "fixable": true }, - { "rule": "*quotes", "severity": "off", "fixable": true }, - { "rule": "*semi", "severity": "off", "fixable": true } - ], + // Silent the stylistic rules in you IDE, but still auto fix them + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } + ], - // Enable eslint for all supported languages - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - "html", - "markdown", - "json", - "jsonc", - "yaml", - "toml", - "xml", - "gql", - "graphql", - "astro", - "svelte", - "css", - "less", - "scss", - "pcss", - "postcss" - ] + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ], + "cSpell.words": [ + "rspack", + "yapi" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f3f805..fbb8c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.0.36 + +### Patch Changes + +- 统一配置读取,升级支持关闭api校验 + ## 0.0.34 ### Patch Changes diff --git a/eslint.config.mjs b/eslint.config.mjs index 7df19f0..2d9b22c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,4 +17,9 @@ export default antfu({ 'unused-imports/no-unused-vars': 1, 'no-eval': 1, }, +}, { + files: ['package.json'], + rules: { + 'jsonc/sort-keys': 0, + }, }) diff --git a/package.json b/package.json index 5cfda83..805a208 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "yapi-to-code", "displayName": "Yapi to Code", - "version": "0.0.34", - "private": true, + "version": "0.0.36", "description": "基于 YApi 生成 ts/dart/...", "categories": [ "Snippets", @@ -20,6 +19,7 @@ "publisher": "zhixiaoqiang", "main": "./dist/extension.js", "workspaces": [ + "./", "src/server", "src/webview" ], @@ -125,11 +125,13 @@ "type": "string", "enum": [ "outerFunction", - "fetchMethodGeneric" + "fetchMethodGeneric", + "null" ], "enumDescriptions": [ "类型将会放置在外层函数:Promise", - "类型将会放置在请求方法的泛型中:post" + "类型将会放置在请求方法的泛型中:post", + "不进行校验" ], "default": "outerFunction", "markdownDescription": "`resType` 放置的位置是外层的 `Promise` 还是作为请求方法的泛型 `post`", @@ -237,7 +239,7 @@ "@types/glob": "^9.0.0", "@types/mocha": "^10.0.1", "@types/node": "^22.17.0", - "@types/vscode": "^1.102.0", + "@types/vscode": "^1.99.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", "@vscode/test-electron": "^2.5.2", @@ -267,7 +269,7 @@ "engines": { "node": ">=16.17.0", "npm": ">=8.15.0", - "vscode": "^1.102.0" + "vscode": "^1.99.0" }, "icon": "dist/assets/logo.png" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf1184a..9b1883a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: specifier: ^22.17.0 version: 22.17.0 '@types/vscode': - specifier: ^1.102.0 + specifier: ^1.99.0 version: 1.102.0 '@typescript-eslint/eslint-plugin': specifier: ^8.38.0 @@ -931,25 +931,21 @@ packages: resolution: {integrity: sha512-C5LR+/na47/ZlQXvrQP96GfzRnh34fa4cT3wD7C+BOwPrPrCdhf8Z3GNzv2J6Rs/ACBZjpD2DE+ARrWsBAv33w==} cpu: [arm64] os: [linux] - libc: [glibc] '@rspack/binding-linux-arm64-musl@0.2.12': resolution: {integrity: sha512-UbLGMUOSPaivO6TbGVt1JKeOXTfj1DFjIBTy2CKxIF5+B6xvg+ns9BhZobtZmjDlJ9GvkkHFoawFyl6UG+XLpg==} cpu: [arm64] os: [linux] - libc: [musl] '@rspack/binding-linux-x64-gnu@0.2.12': resolution: {integrity: sha512-I5W6WaD9llJjFR+Z1aJj0Ml/cheyHDH4eyTfEZXiOCEeTFJlA5NhMn97fDWoFKVYh5wq6uwCrxnaOSehYlsbYw==} cpu: [x64] os: [linux] - libc: [glibc] '@rspack/binding-linux-x64-musl@0.2.12': resolution: {integrity: sha512-JuJKw502u/1FroIfR5iwoZ9pfx/iPpFEQmA6TseIKnu8eHM1jPKlPWAY4geBvzgiz04EdTs4uJ8o1ItoQLsddg==} cpu: [x64] os: [linux] - libc: [musl] '@rspack/binding-win32-arm64-msvc@0.2.12': resolution: {integrity: sha512-f5npfQkXP8uHDSwiT1cXAhrdPwr7hrCz3EVKfwsB5Y1ny17YAH4ztm5Pk7oBB8H8SjQfn2Af8C3YEz1SUyk5/g==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ad42c8d..6af4207 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - . - src/server - src/webview diff --git a/rspack.config.js b/rspack.config.js index 486f27e..1bd6630 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -1,40 +1,40 @@ -const path = require('path') +const path = require('node:path') /** @type {import('@rspack/cli').Configuration} */ module.exports = { - target: 'node', - mode: 'production', - entry: './src/index.ts', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'extension.js', - library: { - type: 'commonjs2' - } - }, - externals: { - vscode: 'commonjs vscode' - }, - resolve: { - extensions: ['.ts', '.js', '.tsx', '.jsx'], - modules: [path.resolve(__dirname, 'node_modules')], - alias: { - '@': path.resolve(__dirname, './src') - } - }, - builtins: { - copy: { - patterns: [ - { - from: path.resolve('src', 'assets'), - to: path.resolve('dist', 'assets') - } - ] - } - }, - watchOptions: { - ignored: /node_modules/, - poll: true - }, - devtool: false + target: 'node', + mode: 'production', + entry: './src/index.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + library: { + type: 'commonjs2', + }, + }, + externals: { + vscode: 'commonjs vscode', + }, + resolve: { + extensions: ['.ts', '.js', '.tsx', '.jsx'], + modules: [path.resolve(__dirname, 'node_modules')], + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + builtins: { + copy: { + patterns: [ + { + from: path.resolve('src', 'assets'), + to: path.resolve('dist', 'assets'), + }, + ], + }, + }, + watchOptions: { + ignored: /node_modules/, + poll: true, + }, + devtool: 'source-map', } diff --git a/src/client/index.ts b/src/client/index.ts index e398fb5..6631cd5 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -11,7 +11,7 @@ import { } from 'vscode-languageclient/node' import { debugVscodeApi } from '@/debug' -import { MAIN_MSG } from '../constant/msg' +import { MAIN_MSG, MsgType } from '../constant/msg' import { getApiDetail, getDir, searchApi } from '../services/api' import Dove from '../utils/dove' import { Client_Server_MsgTYpe } from '../utils/types' @@ -66,6 +66,7 @@ function clientRun(): [LanguageClient, Dove] { // 启动客户端,也会启动服务器 client.start().then(async () => { debugVscodeApi('client ready') + console.log('yapi2code client ready') client.onNotification(MAIN_MSG, (data: any) => { dove.receiveMessage(data) @@ -79,6 +80,17 @@ function clientRun(): [LanguageClient, Dove] { }) }, ) + + // 推送一次合并配置,并监听配置变化自动推送 + async function pushMergedConfig() { + const { getConfiguration } = await import('../common/vscodeapi') + const config = await getConfiguration() + dove.sendMessage(MsgType.CONFIG_UPDATED, config) + } + await pushMergedConfig() + + const { watchYapiConfiguration } = await import('../common/vscodeapi') + watchYapiConfiguration(pushMergedConfig) }) return [client, dove] diff --git a/src/common/vscodeapi.ts b/src/common/vscodeapi.ts index 8584f3a..aa43611 100644 --- a/src/common/vscodeapi.ts +++ b/src/common/vscodeapi.ts @@ -1,109 +1,139 @@ +import type { + Disposable, + LogOutputChannel, + WorkspaceFolder, +} from 'vscode' + import path from 'node:path' +import process from 'node:process' import { pathExists } from 'fs-extra' - import { - commands, - Disposable, - LogOutputChannel, - Uri, - window, - workspace, - WorkspaceFolder + commands, + RelativePattern, + Uri, + window, + workspace, } from 'vscode' -import { Command } from '../constant/vscode' -import { CONFIG_FILE_NAME, DEFAULT_CONFIG, IConfig } from '../constant/config' import { debugVscodeApi } from '@/debug' +import { CONFIG_FILE_NAME, DEFAULT_CONFIG } from '../constant/config' +import { Command } from '../constant/vscode' export function createOutputChannel(name: string): LogOutputChannel { - return window.createOutputChannel(name, { log: true }) + return window.createOutputChannel(name, { log: true }) } export async function getConfiguration() { - const mergeConfig: IConfig = { - ...DEFAULT_CONFIG, - ...workspace.getConfiguration('yapi') - } - try { - const projectConfig = await getProjectConfig() + const mergeConfig: YapiConfig = { + ...DEFAULT_CONFIG, + ...workspace.getConfiguration('yapi'), + } + try { + const projectConfig = await getProjectConfig() - Object.assign(mergeConfig, projectConfig) - } catch (error) { - debugVscodeApi('get config error', error) - } + Object.assign(mergeConfig, projectConfig) + } + catch (error) { + debugVscodeApi('get config error', error) + } - return mergeConfig + return mergeConfig } export function registerCommand( - command: string, - callback: (...args: any[]) => any, - thisArg?: any + command: string, + callback: (...args: any[]) => any, + thisArg?: any, ): Disposable { - return commands.registerCommand(command, callback, thisArg) + return commands.registerCommand(command, callback, thisArg) } export const { onDidChangeConfiguration } = workspace +export function watchYapiConfiguration(onChange: () => any) { + const disposes: Disposable[] = [] + disposes.push( + workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('yapi')) { + onChange() + } + }), + ) + getProjectRoot().then((root) => { + const pattern = new RelativePattern(root, CONFIG_FILE_NAME) + const watcher = workspace.createFileSystemWatcher(pattern) + disposes.push(watcher) + watcher.onDidCreate(onChange) + watcher.onDidChange(onChange) + watcher.onDidDelete(onChange) + }) + return { + dispose() { + disposes.forEach(d => d.dispose()) + }, + } +} + export function isVirtualWorkspace(): boolean { - const isVirtual = - workspace.workspaceFolders && - workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file') - return !!isVirtual + const isVirtual = workspace.workspaceFolders && workspace.workspaceFolders.every(f => f.uri.scheme !== 'file') + return !!isVirtual } export function getWorkspaceFolders(): readonly WorkspaceFolder[] { - return workspace.workspaceFolders ?? [] + return workspace.workspaceFolders ?? [] } export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { - return workspace.getWorkspaceFolder(uri) + return workspace.getWorkspaceFolder(uri) } export async function getProjectRoot(): Promise { - const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() - if (workspaces.length === 0) { - return { - uri: Uri.file(process.cwd()), - name: path.basename(process.cwd()), - index: 0 - } - } else if (workspaces.length === 1) { - return workspaces[0] - } else { - let rootWorkspace = workspaces[0] + const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() + if (workspaces.length === 0) { + return { + uri: Uri.file(process.cwd()), + name: path.basename(process.cwd()), + index: 0, + } + } + else if (workspaces.length === 1) { + return workspaces[0] + } + else { + let rootWorkspace = workspaces[0] - for (const w of workspaces) { - if (await pathExists(w.uri.fsPath)) { - rootWorkspace = w - } - } - return rootWorkspace - } + for (const w of workspaces) { + if (await pathExists(w.uri.fsPath)) { + rootWorkspace = w + } + } + return rootWorkspace + } } export async function getProjectConfig() { - const workspaceFolder = await getProjectRoot() - let config - if (workspaceFolder) { - return workspace.fs - .readFile(Uri.joinPath(workspaceFolder.uri, CONFIG_FILE_NAME)) - .then( - (res) => { - try { - config = eval(res.toString())?.() - return config - } catch (error) { - debugVscodeApi('配置文件异常,请检查配置项', error) - commands.executeCommand( - Command.WARN_TOAST, - `配置异常,请检查配置项 ${error}` - ) - } - }, - (err) => { - debugVscodeApi('getProjectConfig error', err) - } - ) - } - return config + const workspaceFolder = await getProjectRoot() + let config + if (workspaceFolder) { + return workspace.fs + .readFile(Uri.joinPath(workspaceFolder.uri, CONFIG_FILE_NAME)) + .then( + (res) => { + try { + config = eval(res.toString())?.() + return config + } + catch (error) { + debugVscodeApi('配置文件异常,请检查配置项', error) + commands.executeCommand( + Command.WARN_TOAST, + `配置异常,请检查配置项 ${error}`, + ) + } + }, + (err) => { + debugVscodeApi('getProjectConfig error', err) + }, + ) + } + return config } diff --git a/src/constant/config.ts b/src/constant/config.ts index 4acb9e0..903e98d 100644 --- a/src/constant/config.ts +++ b/src/constant/config.ts @@ -24,6 +24,8 @@ export enum ResponseTypePositionEnum { OUTER_FUNCTION = 'outerFunction', /** 类型将会放置在请求方法的泛型中:post */ FETCH_METHOD_GENERIC = 'fetchMethodGeneric', + /** 不进行校验 */ + NULL = 'null', } export enum ConfigKeyEnum { @@ -69,58 +71,59 @@ export const DEFAULT_CONFIG: IConfig = { [ConfigKeyEnum.RESPONSE_KEY]: ResponseKeyEnum.ALL, [ConfigKeyEnum.RESPONSE_CUSTOM_KEY]: ResponseKeyEnum.DATA, [ConfigKeyEnum.RESPONSE_TYPE_POSITION]: - ResponseTypePositionEnum.OUTER_FUNCTION, + ResponseTypePositionEnum.OUTER_FUNCTION, } export const YAPI_RESPONSE_NAME = 'YapiResponse' export function genConfigTemplate(config: IConfig = DEFAULT_CONFIG) { const configTemplate = `module.exports = () => { - return { - /** 域名:优先取工作区缓存的域名(登录成功的域名) */ - host: '${config.host}', - /** banner 头部内容,可以填写导入的请求实例等 */ - banner: '${config.banner}', - /** 生成 res 包含的属性,默认 all, 可指定为 data、custom - * 'all' | 'data' | 'custom' - */ - responseKey: '${config.responseKey}', - /** 生成 res 指定的属性值,仅当 responseKey 选择 custom 是有效,默认 data, 可指定为任意 key(支持链式:data.result) */ - responseCustomKey: '${config.responseCustomKey}', - /** resDataTypeContent 放置的位置是外层的 Promise 还是作为请求方法的泛型 post - * 'outerFunction' | 'fetchMethodGeneric' - */ - responseTypePosition: '${config.responseTypePosition}', - /** 开启自动格式化 */ - format: ${config.format}, - /** 缩进使用 tab,或者 双空格 */ - useTab: ${config.useTab}, - /** 自定义生成 request 方法 */${ - config.genRequest - ? ` + return { + /** 域名:优先取工作区缓存的域名(登录成功的域名) */ + host: '${config.host}', + /** banner 头部内容,可以填写导入的请求实例等 */ + banner: '${config.banner}', + /** 生成 res 包含的属性,默认 all, 可指定为 data、custom + * 'all' | 'data' | 'custom' + */ + responseKey: '${config.responseKey}', + /** 生成 res 指定的属性值,仅当 responseKey 选择 custom 是有效,默认 data, 可指定为任意 key(支持链式:data.result) */ + responseCustomKey: '${config.responseCustomKey}', + /** resDataTypeContent 放置的位置是外层的 Promise 还是作为请求方法的泛型 post + * null 不进行校验 + * 'outerFunction' | 'fetchMethodGeneric' | 'null' + */ + responseTypePosition: '${config.responseTypePosition}', + /** 开启自动格式化 */ + format: ${config.format}, + /** 缩进使用 tab,或者 双空格 */ + useTab: ${config.useTab}, + /** 自定义生成 request 方法 */${ + config.genRequest + ? ` ${config.genRequest?.toString()}` - : ` + : ` genRequest( - { - comment, - fnName, - IReqTypeName, - IResTypeName, - requestMethod, - apiPath, - }, - data - ) { + { + comment, + fnName, + IReqTypeName, + IResTypeName, + requestMethod, + apiPath, + }, + data + ) { const params = IReqTypeName ? \`data: \${IReqTypeName}\` : '' const dataContent = IResTypeName ? 'data' : '' - return ( - \`\\n\${comment}\\n + return ( + \`\\n\${comment}\\n export async function \${fnName}(\${params}) { return request.\${requestMethod}<\${IResTypeName}>('\${apiPath}', '\${dataContent}') }\` - ) - }` - }} + ) + }` + }} }` return configTemplate diff --git a/src/constant/msg.ts b/src/constant/msg.ts index 0b2a782..aae763b 100644 --- a/src/constant/msg.ts +++ b/src/constant/msg.ts @@ -1,31 +1,33 @@ /** 消息类型 */ export enum MsgType { - /** slidebar和vscode通信 */ - COMMAND = 'COMMAND', - LOGIN_STATUS = 'LOGIN_STATUS', - LOGIN_NOW = 'LOGIN_NOW', - FETCH_GROUP = 'FETCH_GROUP', - FETCH_PROJECT = 'FETCH_PROJECT', - FETCH_DIR = 'FETCH_DIR', - FETCH_DIR_AND_ITEM = 'FETCH_DIR_AND_ITEM', - FETCH_ITEM = 'FETCH_ITEM', - FETCH_DETAIL = 'FETCH_DETAIL', - WEBVIEW_DONE = 'WEBVIEW_DONE', - SERVER_URL = 'SERVER_URL', - FRESH_DATA = 'FRESH_DATA', - /** panel和vscode通信 */ - PANEL_KEY_CHANGE = 'PANEL_KEY_CHANGE', - FETCH_CURRENT_KEY = 'FETCH_CURRENT_KEY', - /** LSP和client通信 */ - FIX_ALL = 'FIX_ALL', - /** 获取API文件 */ - API_FILE_HANDLER = 'API_FILE_HANDLER', - /** 导航到文件 */ - OPEN_FILE = 'OPEN_FILE', - LSP_DONE = 'LSP_DONE', - LOGIN_BY_LDAP = 'LOGIN_BY_LDAP', - /** 初始配置 */ - INIT_CONFIG = 'INIT_CONFIG' + /** slidebar和vscode通信 */ + COMMAND = 'COMMAND', + LOGIN_STATUS = 'LOGIN_STATUS', + LOGIN_NOW = 'LOGIN_NOW', + FETCH_GROUP = 'FETCH_GROUP', + FETCH_PROJECT = 'FETCH_PROJECT', + FETCH_DIR = 'FETCH_DIR', + FETCH_DIR_AND_ITEM = 'FETCH_DIR_AND_ITEM', + FETCH_ITEM = 'FETCH_ITEM', + FETCH_DETAIL = 'FETCH_DETAIL', + WEBVIEW_DONE = 'WEBVIEW_DONE', + SERVER_URL = 'SERVER_URL', + FRESH_DATA = 'FRESH_DATA', + /** panel和vscode通信 */ + PANEL_KEY_CHANGE = 'PANEL_KEY_CHANGE', + FETCH_CURRENT_KEY = 'FETCH_CURRENT_KEY', + /** LSP和client通信 */ + FIX_ALL = 'FIX_ALL', + /** 获取API文件 */ + API_FILE_HANDLER = 'API_FILE_HANDLER', + /** 导航到文件 */ + OPEN_FILE = 'OPEN_FILE', + LSP_DONE = 'LSP_DONE', + LOGIN_BY_LDAP = 'LOGIN_BY_LDAP', + /** 初始配置 */ + INIT_CONFIG = 'INIT_CONFIG', + /** 客户端合并后的配置推送到服务端 */ + CONFIG_UPDATED = 'CONFIG_UPDATED', } // 主消息类型 用于客户端和服务器通信 diff --git a/src/server/package.json b/src/server/package.json index f9ccc5e..5d754b7 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,18 +1,18 @@ { - "name": "lsp-yapi-to-code-server", - "version": "1.0.0", - "description": "yapi to code of a language server in node.", - "license": "MIT", - "scripts": { - "build": "rspack build", - "start": "rspack build --watch true" - }, - "dependencies": { - "typescript": "^5.8.3", - "vscode-languageserver": "^9.0.1", - "vscode-languageserver-textdocument": "^1.0.12" - }, - "engines": { - "node": "*" - } + "name": "lsp-yapi-to-code-server", + "version": "1.0.0", + "description": "yapi to code of a language server in node.", + "license": "MIT", + "scripts": { + "build": "rspack build", + "start": "rspack build --watch true" + }, + "dependencies": { + "typescript": "^5.8.3", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12" + }, + "engines": { + "node": "*" + } } diff --git a/src/server/rspack.config.js b/src/server/rspack.config.js index 40b3a2c..f48e1a2 100644 --- a/src/server/rspack.config.js +++ b/src/server/rspack.config.js @@ -1,22 +1,23 @@ -const path = require('path') +const path = require('node:path') module.exports = { - target: 'node', - mode: 'production', - entry: './src/index.ts', - output: { - path: path.resolve(__dirname, '..', '..', 'dist'), - filename: 'server.js', - library: { - type: 'commonjs2' - } - }, - resolve: { - extensions: ['.ts', '.js'] - }, - watchOptions: { - ignored: /node_modules/, - poll: true - }, - devtool: false + target: 'node', + mode: 'production', + entry: './src/index.ts', + output: { + path: path.resolve(__dirname, '..', '..', 'dist'), + filename: 'server.js', + library: { + type: 'commonjs2', + }, + }, + resolve: { + extensions: ['.ts', '.js'], + }, + watchOptions: { + ignored: /node_modules/, + poll: true, + }, + // 使用适合调试的 source map 格式 + devtool: 'source-map', } diff --git a/src/server/src/index.ts b/src/server/src/index.ts index 949af43..0659f6a 100644 --- a/src/server/src/index.ts +++ b/src/server/src/index.ts @@ -1,34 +1,35 @@ -import { SourceFile } from 'typescript' +import type { SourceFile } from 'typescript' +import type { + CodeAction, + CodeActionParams, + Diagnostic, + InitializeParams, + InitializeResult, +} from 'vscode-languageserver/node' + +import type { ApiFunctionStruct, ImportPositionInfo } from './types' import { TextDocument } from 'vscode-languageserver-textdocument' import { - CodeAction, - CodeActionKind, - CodeActionParams, - createConnection, - Diagnostic, - DiagnosticSeverity, - DidChangeConfigurationNotification, - InitializeParams, - InitializeResult, - ProposedFeatures, - TextDocuments, - TextDocumentSyncKind + CodeActionKind, + createConnection, + DiagnosticSeverity, + DidChangeConfigurationNotification, + ProposedFeatures, + TextDocuments, + TextDocumentSyncKind, } from 'vscode-languageserver/node' - import { API_NOT_DEFINED, MAIN_MSG, MsgType } from '../../constant/msg' +import { Command } from '../../constant/vscode' import Dove from '../../utils/dove' -import { YapiVSCodeConfig } from '../../utils/types' import { getAST } from './astree' import { loadFile } from './fileSys' import { - getApiPositionList, - getImportTypePosition, - getYapiResponseInfo + getApiPositionList, + getImportTypePosition, + getYapiResponseInfo, } from './parseAST' import { quickfix } from './quickfix' import store from './store' -import type { ApiFunctionStruct, ImportPositionInfo } from './types' -import { Command } from '../../constant/vscode' // 初始化 LSP 连接对象 export const connection = createConnection(ProposedFeatures.all) @@ -36,46 +37,46 @@ console.log('server init') // 创建文档集合对象,用于映射到实际文档 const documents: TextDocuments = new TextDocuments(TextDocument) - +/** @deprecated 计划废弃, 统一采用从client读取配置 */ let hasConfigurationCapability = false connection.onInitialize((params: InitializeParams) => { - const capabilities = params.capabilities - - hasConfigurationCapability = !!( - capabilities.workspace && !!capabilities.workspace.configuration - ) - - const hasCodeActionLiteralsCapability = !!( - capabilities.textDocument && - capabilities.textDocument.codeAction && - capabilities.textDocument.codeAction.codeActionLiteralSupport - ) - // 明确声明插件支持的语言特性 - const result: InitializeResult = { - capabilities: { - // 启用文档增量更新同步 - textDocumentSync: TextDocumentSyncKind.Incremental - } - } - - if (hasCodeActionLiteralsCapability) { - result.capabilities.codeActionProvider = { - codeActionKinds: [CodeActionKind.QuickFix] - } - } - return result + const capabilities = params.capabilities + + hasConfigurationCapability = !!( + capabilities.workspace && !!capabilities.workspace.configuration + ) + + const hasCodeActionLiteralsCapability = !!( + capabilities.textDocument + && capabilities.textDocument.codeAction + && capabilities.textDocument.codeAction.codeActionLiteralSupport + ) + // 明确声明插件支持的语言特性 + const result: InitializeResult = { + capabilities: { + // 启用文档增量更新同步 + textDocumentSync: TextDocumentSyncKind.Incremental, + }, + } + + if (hasCodeActionLiteralsCapability) { + result.capabilities.codeActionProvider = { + codeActionKinds: [CodeActionKind.QuickFix], + } + } + return result }) connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register( - DidChangeConfigurationNotification.type, - undefined - ) - } - dove.sendMessage(MsgType.LSP_DONE, {}) + if (hasConfigurationCapability) { + // Register for all configuration changes. + connection.client.register( + DidChangeConfigurationNotification.type, + undefined, + ) + } + dove.sendMessage(MsgType.LSP_DONE, {}) }) // Make the text document manager listen on the connection @@ -87,235 +88,252 @@ connection.listen() // 初始化通信器 const dove = new Dove((msg: any) => { - connection.sendNotification(MAIN_MSG, msg) + connection.sendNotification(MAIN_MSG, msg) }) // 收到消息 connection.onNotification(MAIN_MSG, (data: any) => { - dove.receiveMessage(data) + dove.receiveMessage(data) }) // 监听消息 dove.subscribe(MsgType.FIX_ALL, async (uri: string) => { - const fixs = await provideCodeActions(store.get(uri).detail, true).catch( - () => { - dove.sendMessage(MsgType.COMMAND, { - command: Command.WARN_TOAST, - data: 'Yapi请求失败' - }) - return [] - } - ) - console.log('请求结束') - if (!fixs.length) { - return - } - return fixs + const fixs = await provideCodeActions(store.get(uri).detail, true).catch( + () => { + dove.sendMessage(MsgType.COMMAND, { + command: Command.WARN_TOAST, + data: 'Yapi请求失败', + }) + return [] + }, + ) + console.log('请求结束') + if (!fixs.length) { + return + } + return fixs }) -//关闭文件时清空储存 +// 关闭文件时清空储存 documents.onDidClose((change) => { - console.log('关闭文件时清空储存') - const uri = change.document.uri - store.remove(uri) + console.log('关闭文件时清空储存') + const uri = change.document.uri + store.remove(uri) }) // 增量错误诊断 documents.onDidChangeContent((change) => { - validateTextDocument(change.document) + validateTextDocument(change.document) }) -// 设置 -const defaultSettings: YapiVSCodeConfig = { responseType: '' } -let globalSettings: YapiVSCodeConfig = defaultSettings +/** @deprecated 计划废弃, 统一采用从client读取配置 */ +const defaultSettings: YapiConfig = { responseType: '' } +/** @deprecated 计划废弃, 统一采用从client读取配置 */ +let globalSettings: YapiConfig = defaultSettings +/** 客户端推送的合并配置 */ +let mergedSettings: YapiConfig | null = null // Cache the settings of all open documents -const documentSettings: Map> = new Map() - -connection.onDidChangeConfiguration((change) => { - if (hasConfigurationCapability) { - // Reset all cached document settings - documentSettings.clear() - } else { - console.log(change.settings, 'change.settings') +const documentSettings: Map> = new Map() - globalSettings = (change.settings.yapi || defaultSettings) - } +/** 接收客户端推送的合并配置 */ +dove.subscribe(MsgType.CONFIG_UPDATED, (cfg: YapiConfig) => { + mergedSettings = cfg + documents.all().forEach(validateTextDocument) +}) - // Revalidate all open text documents - documents.all().forEach(validateTextDocument) +connection.onDidChangeConfiguration((change) => { + if (hasConfigurationCapability) { + // Reset all cached document settings + documentSettings.clear() + } + else { + console.log(change.settings, 'change.settings') + + globalSettings = (change.settings.yapi || defaultSettings) + } + + // Revalidate all open text documents + documents.all().forEach(validateTextDocument) }) -function getDocumentSettings(resource: string): Thenable { - if (!hasConfigurationCapability) { - return Promise.resolve(globalSettings) - } - let result = documentSettings.get(resource) - - if (!result) { - result = connection.workspace.getConfiguration({ - scopeUri: resource, - section: 'yapi' - }) - documentSettings.set(resource, result) - } - return result +function getDocumentSettings(resource: string): Thenable { + // 优先使用客户端推送的合并配置 + if (mergedSettings) { + return Promise.resolve(mergedSettings) + } + if (!hasConfigurationCapability) { + return Promise.resolve(globalSettings) + } + let result = documentSettings.get(resource) + + if (!result) { + result = connection.workspace.getConfiguration({ + scopeUri: resource, + section: 'yapi', + }) + // result = getConfiguration() + documentSettings.set(resource, result) + } + return result } // Only keep settings for open documents documents.onDidClose((e) => { - documentSettings.delete(e.document.uri) + documentSettings.delete(e.document.uri) }) async function validateTextDocument(textDocument: TextDocument): Promise { - // 获取文档纯文本 - const text = textDocument.getText() - - const diagStore = store.get(textDocument.uri) - diagStore.clear() - - // 获取项目yapi配置 - const yapiConfig = await getDocumentSettings(textDocument.uri) - // 生成抽象语法树 AST - const ast: SourceFile = getAST(text) - // 遍历 AST 筛选符合标记的节点位置列表 - const apiFnList: ApiFunctionStruct[] = getApiPositionList(ast, yapiConfig) - - // 获取import type的位置,已存在则用现在,没有则获取最后import插入位置 - const importInfo = await getImportTypePosition(ast) - - // 获取yapiResponse类型的位置 - const yapiResponseTypeInfo = getYapiResponseInfo(ast) - // 插入点 - const importPos: ImportPositionInfo = { - type: 'useOld', - position: { - line: 0, - character: 0 - }, - nameList: [] - } - if (importInfo.type === 'useOld') { - // 使用已有的 - importPos.position = textDocument.positionAt(importInfo.position) - importPos.nameList = importInfo.nameList || [] - } else { - // 新注入 - importPos.type = 'useNew' - const pos = textDocument.positionAt(importInfo.position) - importPos.position = - importInfo.position === 0 - ? { line: 0, character: 0 } - : { - line: pos.line, - character: pos.character - } - } - - const diagnostics: Diagnostic[] = [] - - for (const apiFn of apiFnList) { - const mayTipsMessage = `${apiFn.apiPath} ${API_NOT_DEFINED} ${ - Number.isFinite(apiFn.paramTypeInsertPosition) ? '未定义参数类型' : '' - } ${ - Number.isFinite(apiFn.fnRespTypeInsertPosition) ? '未定义返回值类型' : '' - } + // 获取文档纯文本 + const text = textDocument.getText() + + const diagStore = store.get(textDocument.uri) + diagStore.clear() + + // 获取项目yapi配置 + const yapiConfig = await getDocumentSettings(textDocument.uri) + // 生成抽象语法树 AST + const ast: SourceFile = getAST(text) + // 遍历 AST 筛选符合标记的节点位置列表 + const apiFnList: ApiFunctionStruct[] = getApiPositionList(ast, yapiConfig) + + // 获取import type的位置,已存在则用现在,没有则获取最后import插入位置 + const importInfo = await getImportTypePosition(ast) + + // 获取yapiResponse类型的位置 + const yapiResponseTypeInfo = getYapiResponseInfo(ast) + // 插入点 + const importPos: ImportPositionInfo = { + type: 'useOld', + position: { + line: 0, + character: 0, + }, + nameList: [], + } + if (importInfo.type === 'useOld') { + // 使用已有的 + importPos.position = textDocument.positionAt(importInfo.position) + importPos.nameList = importInfo.nameList || [] + } + else { + // 新注入 + importPos.type = 'useNew' + const pos = textDocument.positionAt(importInfo.position) + importPos.position = importInfo.position === 0 + ? { line: 0, character: 0 } + : { + line: pos.line, + character: pos.character, + } + } + + const diagnostics: Diagnostic[] = [] + + for (const apiFn of apiFnList) { + const mayTipsMessage = `${apiFn.apiPath} ${API_NOT_DEFINED} ${ + Number.isFinite(apiFn.paramTypeInsertPosition) ? '未定义参数类型' : '' + } ${ + Number.isFinite(apiFn.fnRespTypeInsertPosition) ? '未定义返回值类型' : '' + } ${ - Number.isFinite(apiFn.methodGenericInsertPosition) - ? '未定义返回值类型' - : '' - }` - - const tipsMessage = getSubName(diagStore.diagnosticMap, mayTipsMessage) - - const diagnostic: Diagnostic = { - severity: DiagnosticSeverity.Information, - range: { - start: textDocument.positionAt(apiFn.pos + 1), - end: textDocument.positionAt(apiFn.end) - }, - message: tipsMessage, - source: 'yapi to code' - } - diagStore.setCurDiagnostic(tipsMessage, { - paramTypeInsertPosition: Number.isFinite(apiFn.paramTypeInsertPosition) - ? textDocument.positionAt(apiFn.paramTypeInsertPosition!) - : undefined, - fnRespTypeInsertPosition: Number.isFinite(apiFn.fnRespTypeInsertPosition) - ? textDocument.positionAt(apiFn.fnRespTypeInsertPosition!) - : undefined, - methodGenericInsertPosition: Number.isFinite( - apiFn.methodGenericInsertPosition - ) - ? textDocument.positionAt(apiFn.methodGenericInsertPosition!) - : undefined, - apiPath: apiFn.apiPath, - requestFn: apiFn.requestFn - }) - diagStore.importPositionInfo.set(tipsMessage, importPos) - - if (yapiResponseTypeInfo.type === 'useNew') { - // 插入 YapiResponse - const pos = textDocument.positionAt(yapiResponseTypeInfo.position) - diagStore.yapiResponseNameInfo.set(tipsMessage, { - line: pos.line + 1, - character: 0 - }) - } else { - diagStore.yapiResponseNameInfo.delete(tipsMessage) - } - diagnostics.push(diagnostic) - } - - diagStore.detail = { - textDocument, - context: { - diagnostics - }, - diagnostics - } - - // 发送诊断信息 - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) + Number.isFinite(apiFn.methodGenericInsertPosition) + ? '未定义返回值类型' + : '' + }` + + const tipsMessage = getSubName(diagStore.diagnosticMap, mayTipsMessage) + + const diagnostic: Diagnostic = { + severity: DiagnosticSeverity.Information, + range: { + start: textDocument.positionAt(apiFn.pos + 1), + end: textDocument.positionAt(apiFn.end), + }, + message: tipsMessage, + source: 'yapi to code', + } + diagStore.setCurDiagnostic(tipsMessage, { + paramTypeInsertPosition: Number.isFinite(apiFn.paramTypeInsertPosition) + ? textDocument.positionAt(apiFn.paramTypeInsertPosition!) + : undefined, + fnRespTypeInsertPosition: Number.isFinite(apiFn.fnRespTypeInsertPosition) + ? textDocument.positionAt(apiFn.fnRespTypeInsertPosition!) + : undefined, + methodGenericInsertPosition: Number.isFinite( + apiFn.methodGenericInsertPosition, + ) + ? textDocument.positionAt(apiFn.methodGenericInsertPosition!) + : undefined, + apiPath: apiFn.apiPath, + requestFn: apiFn.requestFn, + }) + diagStore.importPositionInfo.set(tipsMessage, importPos) + + if (yapiResponseTypeInfo.type === 'useNew') { + // 插入 YapiResponse + const pos = textDocument.positionAt(yapiResponseTypeInfo.position) + diagStore.yapiResponseNameInfo.set(tipsMessage, { + line: pos.line + 1, + character: 0, + }) + } + else { + diagStore.yapiResponseNameInfo.delete(tipsMessage) + } + diagnostics.push(diagnostic) + } + + diagStore.detail = { + textDocument, + context: { + diagnostics, + }, + diagnostics, + } + + // 发送诊断信息 + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) } -connection.onCodeAction((params) => provideCodeActions(params)) +connection.onCodeAction(params => provideCodeActions(params)) async function provideCodeActions( - params: CodeActionParams, - isFixAll?: boolean + params: CodeActionParams, + isFixAll?: boolean, ): Promise { - if (!params.context.diagnostics.length) { - return [] - } - const document = params.textDocument - if (!document) { - return [] - } - return quickfix(dove, params, isFixAll) + if (!params.context.diagnostics.length) { + return [] + } + const document = params.textDocument + if (!document) { + return [] + } + return quickfix(dove, params) } /** 获取副本名字 */ function getSubName(diagMap: any, name: string, init = 1): string { - return diagMap.get(name) ? getSubName(diagMap, name + init, init + 1) : name + return diagMap.get(name) ? getSubName(diagMap, name + init, init + 1) : name } /** 保存获取接口列表进行校验 */ -dove.subscribe(MsgType.API_FILE_HANDLER, async (apiFileList: any) => { - console.log('保存获取接口列表进行校验') - const results = await Promise.all( - apiFileList?.map(async (file: string) => { - // 获取项目yapi配置 - const yapiConfig = await getDocumentSettings(file) - const text = await loadFile(file?.replace('file://', '')) - - // 生成抽象语法树 AST - const ast: SourceFile = getAST(text) - // 遍历 AST 筛选符合标记的节点位置列表 - const apiFnList: ApiFunctionStruct[] = getApiPositionList(ast, yapiConfig) - return { - uri: file, - apiFnList: apiFnList - } - }) - ) - return results +dove.subscribe(MsgType.API_FILE_HANDLER, async (apiFileList: string[]) => { + console.log('保存获取接口列表进行校验') + const results = await Promise.all( + apiFileList?.map(async (file) => { + // 获取项目yapi配置 + const yapiConfig = await getDocumentSettings(file) + console.log('=======> yapiConfig API_FILE_HANDLER:', file, yapiConfig) + const text = await loadFile(file?.replace('file://', '')) + + // 生成抽象语法树 AST + const ast: SourceFile = getAST(text) + // 遍历 AST 筛选符合标记的节点位置列表 + const apiFnList: ApiFunctionStruct[] = getApiPositionList(ast, yapiConfig) + return { + uri: file, + apiFnList, + } + }), + ) + return results }) diff --git a/src/server/src/parseAST.ts b/src/server/src/parseAST.ts index ad333f0..ec732ba 100644 --- a/src/server/src/parseAST.ts +++ b/src/server/src/parseAST.ts @@ -1,380 +1,381 @@ -import { - visitEachChild, - transform, - isParameter, - isCallExpression, - isArrowFunction, - isFunctionDeclaration, - isFunctionExpression, - isBlock, - isReturnStatement, - isToken, - isStringLiteral, - isTemplateExpression, - isTemplateTail, - isImportDeclaration, - isImportSpecifier, - isTypeAliasDeclaration, - isInterfaceDeclaration, - isPropertyAccessExpression, - isIdentifier -} from 'typescript' import type { - ArrowFunction, - FunctionExpression, - FunctionDeclaration, - SourceFile, - TransformerFactory, - Node, - TransformationContext, - CallExpression, - ParameterDeclaration, - StringLiteral, - ExpressionStatement, - ImportDeclaration, - ImportClause, - ImportSpecifier, - TypeAliasDeclaration, - InterfaceDeclaration, - PropertyAccessExpression, - Identifier, - NamedImports + ArrowFunction, + CallExpression, + ExpressionStatement, + FunctionDeclaration, + FunctionExpression, + Identifier, + ImportClause, + ImportDeclaration, + ImportSpecifier, + InterfaceDeclaration, + NamedImports, + Node, + ParameterDeclaration, + PropertyAccessExpression, + SourceFile, + StringLiteral, + TransformationContext, + TransformerFactory, + TypeAliasDeclaration, } from 'typescript' +import type { ApiFunctionStruct, FunctionStruct } from './types' -import type { FunctionStruct, ApiFunctionStruct } from './types' -import { YAPI_RESPONSE_NAME, YAPI_RESPONSE_TYPE } from '../../constant/config' +import { + isArrowFunction, + isBlock, + isCallExpression, + isFunctionDeclaration, + isFunctionExpression, + isIdentifier, + isImportDeclaration, + isImportSpecifier, + isInterfaceDeclaration, + isParameter, + isPropertyAccessExpression, + isReturnStatement, + isStringLiteral, + isTemplateExpression, + isTemplateTail, + isToken, + isTypeAliasDeclaration, + transform, + visitEachChild, +} from 'typescript' +import { ResponseTypePositionEnum, YAPI_RESPONSE_NAME, YAPI_RESPONSE_TYPE } from '../../constant/config' type FunctionNode = ArrowFunction | FunctionExpression | FunctionDeclaration -export function getApiPositionList(ast: SourceFile, config?: any) { - const result: ApiFunctionStruct[] = [] - // 遍历所有节点 获取位置 - const transformer: TransformerFactory = ( - context: TransformationContext - ) => { - return function visitor(node: Node | CallExpression): Node { - if (isFunction(node)) { - const apiNode = getApiNode(node as FunctionNode, config) - if (apiNode) { - result.push({ - ...apiNode, - pos: node.pos, - end: node.end - }) - } - } +export function getApiPositionList(ast: SourceFile, config?: YapiConfig) { + const result: ApiFunctionStruct[] = [] + // 遍历所有节点 获取位置 + const transformer: TransformerFactory = ( + context: TransformationContext, + ) => { + return function visitor(node: Node | CallExpression): Node { + if (isFunction(node)) { + const apiNode = getApiNode(node as FunctionNode, config) + if (apiNode) { + result.push({ + ...apiNode, + pos: node.pos, + end: node.end, + }) + } + } - return visitEachChild(node, visitor, context) - } - } - transform(ast, [transformer]) // 记录节点位置 + return visitEachChild(node, visitor, context) + } + } + transform(ast, [transformer]) // 记录节点位置 - return result + return result } -function getApiNode(node: FunctionNode, config?: any): false | FunctionStruct { - const result: FunctionStruct = { apiPath: '', requestFn: 'any' } - // 获取参数节点 - const paramNodes = getTheChildNode(node, isParameter) +/** 获取apiNode位置信息和判断结果,用于分析是否需要进行标记 */ +function getApiNode(node: FunctionNode, config?: YapiConfig): false | FunctionStruct { + const result: FunctionStruct = { apiPath: '', requestFn: 'any' } + // 获取参数节点 + const paramNodes = getTheChildNode(node, isParameter) - // 参数大于一的时候不是个正常的接口传参,所以直接返回 - if (paramNodes.length > 1) { - return false - } - // 参数是否进行类型定义 - const paramIsTyped = - paramNodes.length === 0 - ? true - : getIsParamType(paramNodes[0] as ParameterDeclaration) + // 参数大于一的时候不是个正常的接口传参,所以直接返回 + if (paramNodes.length > 1) { + return false + } + // 参数是否进行类型定义 + const paramIsTyped = paramNodes.length === 0 ? true : getIsParamType(paramNodes[0] as ParameterDeclaration) - if (!paramIsTyped) { - result.paramTypeInsertPosition = paramNodes[0].end - } - // 函数是否有返回类型 - const respIsTyped = getIsReturnType(node as ArrowFunction) + if (!paramIsTyped) { + result.paramTypeInsertPosition = paramNodes[0].end + } + // 函数是否有返回类型 + const respIsTyped = getIsReturnType(node as ArrowFunction) - // 若参数和返回值均定义了类型,则不进行标记 - if (respIsTyped && paramIsTyped) { - return false - } - // 存在参数和返回值未定义类型,读取函数体 - // isCallExpression ()=>request.get('xxx') - const [callExpressionNode] = getTheChildNode( - node, - isCallExpression - ) + // 若参数和返回值均定义了类型,则不进行标记 + if ((respIsTyped && paramIsTyped) || config?.responseTypePosition === ResponseTypePositionEnum.NULL) { + return false + } + // 存在参数和返回值未定义类型,读取函数体 + // isCallExpression ()=>request.get('xxx') + const [callExpressionNode] = getTheChildNode( + node, + isCallExpression, + ) - const hasResGeneric = - callExpressionNode && - callExpressionNode.typeArguments && - callExpressionNode.typeArguments.length > 0 - if (hasResGeneric && paramIsTyped) { - return false - } + const hasResGeneric = callExpressionNode + && callExpressionNode.typeArguments + && callExpressionNode.typeArguments.length > 0 + if (hasResGeneric && paramIsTyped) { + return false + } - if (config && config.responseType === YAPI_RESPONSE_TYPE.GENERIC) { - if (!hasResGeneric) { - result.methodGenericInsertPosition = - getResponseGenericInsertPosition(callExpressionNode) - } - } else { - if (!respIsTyped) { - result.fnRespTypeInsertPosition = getFnRespInsertPosition(node)! - } - } + if (config && config.responseType === YAPI_RESPONSE_TYPE.GENERIC) { + if (!hasResGeneric) { + result.methodGenericInsertPosition = getResponseGenericInsertPosition(callExpressionNode) + } + } + else { + if (!respIsTyped) { + result.fnRespTypeInsertPosition = getFnRespInsertPosition(node)! + } + } - if (callExpressionNode) { - const apiPath = getApiUrlFromExpressionNode(callExpressionNode) - if (apiPath) { - result.apiPath = apiPath - result.requestFn = getFnName(callExpressionNode) - return result - } - } - // ()=>{ return request.get('xxx') } - const [bodyNode] = getTheChildNode(node, isBlock) + if (callExpressionNode) { + const apiPath = getApiUrlFromExpressionNode(callExpressionNode) + if (apiPath) { + result.apiPath = apiPath + result.requestFn = getFnName(callExpressionNode) + return result + } + } + // ()=>{ return request.get('xxx') } + const [bodyNode] = getTheChildNode(node, isBlock) - if (bodyNode) { - const [returnNode] = getTheChildNode(bodyNode, isReturnStatement) - if (returnNode) { - const [callExpressionNode] = getTheChildNode( - returnNode, - isCallExpression - ) - if (callExpressionNode) { - const apiPath = getApiUrlFromExpressionNode(callExpressionNode) - if (apiPath) { - result.apiPath = apiPath - result.requestFn = getFnName(callExpressionNode) - return result - } - } - } - } + if (bodyNode) { + const [returnNode] = getTheChildNode(bodyNode, isReturnStatement) + if (returnNode) { + const [callExpressionNode] = getTheChildNode( + returnNode, + isCallExpression, + ) + if (callExpressionNode) { + const apiPath = getApiUrlFromExpressionNode(callExpressionNode) + if (apiPath) { + result.apiPath = apiPath + result.requestFn = getFnName(callExpressionNode) + return result + } + } + } + } - return false + return false } function isFunction(node: Node): boolean { - return ( - isArrowFunction(node) || - isFunctionExpression(node) || - isFunctionDeclaration(node) - ) + return ( + isArrowFunction(node) + || isFunctionExpression(node) + || isFunctionDeclaration(node) + ) } /** * @description 获取指定子节点 */ function getTheChildNode( - parentNode: Node, - jugeFn: (node: Node) => any + parentNode: Node, + jugeFn: (node: Node) => any, ): T[] { - const gather: T[] = [] - parentNode?.forEachChild((node) => { - if (jugeFn(node)) { - gather.push(node as unknown as T) - } - }) - return gather + const gather: T[] = [] + parentNode?.forEachChild((node) => { + if (jugeFn(node)) { + gather.push(node as unknown as T) + } + }) + return gather } /** * @description 获取接口链接 */ function getUrlFromStringNode(node: StringLiteral) { - return node?.text || '' + return node?.text || '' } /** * @description 获取api URL */ function getApiUrlFromExpressionNode( - node: ExpressionStatement | CallExpression + node: ExpressionStatement | CallExpression, ) { - // isStringLiteral - const strNodes: any[] = [] - const identifierNode = getTheChildNode(node, isToken) - if (identifierNode) { - strNodes.push( - ...identifierNode.map((node) => - getUrlFromStringNode(node as StringLiteral) - ) - ) - } - const strNode = getTheChildNode(node, isStringLiteral) - if (strNode) { - strNodes.push( - ...strNode.map((node) => getUrlFromStringNode(node as StringLiteral)) - ) - } - // 携带变量 - const [variableNode] = getTheChildNode( - node, - isTemplateExpression - ) - if (variableNode) { - const nodes = variableNode?.templateSpans?.map( - (node) => getTheChildNode(node, isTemplateTail)?.[0] - ) - strNodes.push( - ...(nodes?.map((node: any) => - getUrlFromStringNode(node as StringLiteral) - ) || []) - ) - } + // isStringLiteral + const strNodes: any[] = [] + const identifierNode = getTheChildNode(node, isToken) + if (identifierNode) { + strNodes.push( + ...identifierNode.map(node => + getUrlFromStringNode(node as StringLiteral), + ), + ) + } + const strNode = getTheChildNode(node, isStringLiteral) + if (strNode) { + strNodes.push( + ...strNode.map(node => getUrlFromStringNode(node as StringLiteral)), + ) + } + // 携带变量 + const [variableNode] = getTheChildNode( + node, + isTemplateExpression, + ) + if (variableNode) { + const nodes = variableNode?.templateSpans?.map( + node => getTheChildNode(node, isTemplateTail)?.[0], + ) + strNodes.push( + ...(nodes?.map((node: any) => + getUrlFromStringNode(node as StringLiteral), + ) || []), + ) + } - const apiPathList = strNodes?.filter((str) => /^\/.+\/.+\/.+/.test(str)) + const apiPathList = strNodes?.filter(str => /^\/.+\/.+\/.+/.test(str)) - if (apiPathList?.length) { - return apiPathList[0] - } else { - return false - } + if (apiPathList?.length) { + return apiPathList[0] + } + else { + return false + } } /** * @description 判断函数是否有返回类型 */ function getIsReturnType(node: ArrowFunction) { - return node.type + return node.type } /** * @description 判断参数是否有定义类型 */ function getIsParamType(node: ParameterDeclaration) { - return node.type + return node.type } /** * @description 获取函数的返回类型插入点 */ function getFnRespInsertPosition(node: FunctionNode) { - if (isArrowFunction(node)) { - return (node as ArrowFunction).equalsGreaterThanToken.pos - } else if (isFunctionDeclaration(node) || isFunctionExpression(node)) { - return (node as FunctionExpression).body.pos - } + if (isArrowFunction(node)) { + return (node as ArrowFunction).equalsGreaterThanToken.pos + } + else if (isFunctionDeclaration(node) || isFunctionExpression(node)) { + return (node as FunctionExpression).body.pos + } } /** * @description 查找import的位置 */ export async function getImportTypePosition(ast: SourceFile) { - const importNodes = getTheChildNode( - ast, - isImportDeclaration - ) + const importNodes = getTheChildNode( + ast, + isImportDeclaration, + ) - for (const node of importNodes) { - const path = (node.moduleSpecifier as StringLiteral)?.text - if (path === './types') { - return { - type: 'useOld', - position: node.importClause!.end - 1, - nameList: getNameListFromImportClause(node.importClause!) - } - } - } - const { length, [length - 1]: last } = importNodes - return { - type: 'useNew', - position: last?.end || 0 - } + for (const node of importNodes) { + const path = (node.moduleSpecifier as StringLiteral)?.text + if (path === './types') { + return { + type: 'useOld', + position: node.importClause!.end - 1, + nameList: getNameListFromImportClause(node.importClause!), + } + } + } + const { length, [length - 1]: last } = importNodes + return { + type: 'useNew', + position: last?.end || 0, + } } /** * @description 查找YapiResponseName */ export function getYapiResponseInfo(ast: SourceFile) { - // 查找import - const importNodes = getTheChildNode( - ast, - isImportDeclaration - ) - for (const node of importNodes) { - const path = node.importClause as ImportClause - const bindingNode = path?.namedBindings - const name = path?.name - if (bindingNode) { - for (const node of getTheChildNode( - bindingNode, - isImportSpecifier - )) { - if (node.name.escapedText === YAPI_RESPONSE_NAME) { - return { - type: 'useOld', - position: 0 - } - } - } - } - if (name?.escapedText === YAPI_RESPONSE_NAME) { - return { - type: 'useOld', - position: 0 - } - } - } - // 查找命名变量 - const typeAliasNode = getTheChildNode( - ast, - isTypeAliasDeclaration - ) - const interfaceNode = getTheChildNode( - ast, - isInterfaceDeclaration - ) - for (const node of [...typeAliasNode, ...interfaceNode]) { - if (node.name.escapedText === YAPI_RESPONSE_NAME) { - return { - type: 'useOld', - position: 0 - } - } - } - // 需要自定义一个YapiResponseName - const { length, [length - 1]: last } = importNodes - return { - type: 'useNew', - position: last?.end || 0 - } + // 查找import + const importNodes = getTheChildNode( + ast, + isImportDeclaration, + ) + for (const node of importNodes) { + const path = node.importClause as ImportClause + const bindingNode = path?.namedBindings + const name = path?.name + if (bindingNode) { + for (const node of getTheChildNode( + bindingNode, + isImportSpecifier, + )) { + if (node.name.escapedText === YAPI_RESPONSE_NAME) { + return { + type: 'useOld', + position: 0, + } + } + } + } + if (name?.escapedText === YAPI_RESPONSE_NAME) { + return { + type: 'useOld', + position: 0, + } + } + } + // 查找命名变量 + const typeAliasNode = getTheChildNode( + ast, + isTypeAliasDeclaration, + ) + const interfaceNode = getTheChildNode( + ast, + isInterfaceDeclaration, + ) + for (const node of [...typeAliasNode, ...interfaceNode]) { + if (node.name.escapedText === YAPI_RESPONSE_NAME) { + return { + type: 'useOld', + position: 0, + } + } + } + // 需要自定义一个YapiResponseName + const { length, [length - 1]: last } = importNodes + return { + type: 'useNew', + position: last?.end || 0, + } } /** * @description 获取函数名字 */ function getFnName(node: CallExpression): string { - const [pNode] = getTheChildNode( - node, - isPropertyAccessExpression - ) - if (pNode) { - // 形式 request.get - return ( - (pNode.expression as Identifier).escapedText.toString() + - '.' + - (pNode.name as Identifier).escapedText.toString() - ) - } else { - // 形式 request - const [iNode] = getTheChildNode(node, isIdentifier) - return iNode.escapedText.toString() - } + const [pNode] = getTheChildNode( + node, + isPropertyAccessExpression, + ) + if (pNode) { + // 形式 request.get + return ( + `${(pNode.expression as Identifier).escapedText.toString() + }.${ + (pNode.name as Identifier).escapedText.toString()}` + ) + } + else { + // 形式 request + const [iNode] = getTheChildNode(node, isIdentifier) + return iNode.escapedText.toString() + } } function getResponseGenericInsertPosition(node: CallExpression) { - const [pNode] = getTheChildNode( - node, - isPropertyAccessExpression - ) - // http expression.end http.get 取下级name.end - if (pNode) { - return pNode.name.end - } else { - return node?.expression?.end - } + const [pNode] = getTheChildNode( + node, + isPropertyAccessExpression, + ) + // http expression.end http.get 取下级name.end + if (pNode) { + return pNode.name.end + } + else { + return node?.expression?.end + } } /** @@ -383,18 +384,19 @@ function getResponseGenericInsertPosition(node: CallExpression) { * @returns { string[] } [a, b, c, d] */ function getNameListFromImportClause(node: ImportClause): string[] { - try { - if (!node) { - return [] - } - const nameBindingsNode = node.namedBindings as NamedImports - if (!nameBindingsNode) { - return [] - } - return nameBindingsNode.elements.map((elem) => { - return elem.name.escapedText.toString() - }) - } catch (error) { - return [] - } + try { + if (!node) { + return [] + } + const nameBindingsNode = node.namedBindings as NamedImports + if (!nameBindingsNode) { + return [] + } + return nameBindingsNode.elements.map((elem) => { + return elem.name.escapedText.toString() + }) + } + catch (error) { + return [] + } } diff --git a/src/server/src/types/YapiVSCodeConfig.d.ts b/src/server/src/types/YapiVSCodeConfig.d.ts new file mode 100644 index 0000000..a9b5c79 --- /dev/null +++ b/src/server/src/types/YapiVSCodeConfig.d.ts @@ -0,0 +1,11 @@ +/** Yapi2Code配置文件类型 */ +type YapiConfig = { + /** 补全返回类型 */ + responseType?: 'methodGeneric' | ''; + /** + * @param outerFunction: 放置的位置是外层的 Promise + * @param fetchMethodGeneric: 作为请求方法的泛型 post + * @param null: 不进行校验 + */ + responseTypePosition?: 'outerFunction' | 'fetchMethodGeneric' | 'null' + } \ No newline at end of file diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index f4f9b66..f52a74d 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,12 +1,13 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": ["ESNext"], - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "strict": true, - }, - "include": ["src", "../utils/"], - "exclude": ["node_modules", ".vscode-test"] + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext"], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "sourceMap": true, + "esModuleInterop": true + }, + "include": ["src", "../utils/"], + "exclude": ["node_modules", ".vscode-test"] } diff --git a/src/utils/types.ts b/src/utils/types.ts index e398eaa..804a599 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,18 +1,14 @@ export enum Client_Server_MsgTYpe { - GIVE_INFO_FROM_PATH__SERVER = 'GIVE_INFO_FROM_PATH__SERVER' -} - -export interface YapiVSCodeConfig { - responseType?: 'methodGeneric' | '' + GIVE_INFO_FROM_PATH__SERVER = 'GIVE_INFO_FROM_PATH__SERVER', } export type ApiTypeList = { - uri: string - apiFnList: { - apiPath: string - requestFn: string - }[] - path?: string + uri: string + apiFnList: { + apiPath: string + requestFn: string + }[] + path?: string }[] -export type ErrorMessage = { message: string; apiPath: string }[] +export type ErrorMessage = { message: string, apiPath: string }[] diff --git a/tsconfig.json b/tsconfig.json index 7953f67..e432270 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,25 @@ { - "compilerOptions": { + "compilerOptions": { + "target": "ESNext", "jsx": "react", - "module": "ESNext", - "target": "ESNext", - "outDir": "dist", - "moduleResolution": "Node", - "lib": [ - "ESNext", - "dom" - ], - "sourceMap": true, - "rootDir": "src", - "allowSyntheticDefaultImports": true, - "strict": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "exclude": [ - "node_modules", - ".vscode-test" - ] + "lib": [ + "ESNext", + "dom" + ], + "baseUrl": ".", + "rootDir": "src", + "module": "ESNext", + "moduleResolution": "Node", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true, + "outDir": "dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] }