diff --git a/.eslintignore b/.eslintignore index b8e7c7a..1e10c31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,7 @@ node_modules/ coverage/ test/fixtures/**/app/proxy +dist/ +astparser/ +bin/libs/ +**/*.d.ts diff --git a/.gitignore b/.gitignore index f63cf18..fa00e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ proxy/ package-lock.json assembly/ -mocks_data/proxy/**/__* \ No newline at end of file +mocks_data/proxy/**/__* + +# TypeScript build output +dist/ +*.tsbuildinfo \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..76e32f6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +*.log +astparser +bin/libs +.git diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..32a2397 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..e08a9f6 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,111 @@ +# Upgrade Guide - v2.0.0 + +## 重大变更 + +### TypeScript 迁移 +项目已完全迁移到 TypeScript,提供更好的类型安全和开发体验。 + +### 依赖更新 +所有依赖已更新到最新版本: + +- **commander**: 2.19.0 → 12.1.0 (最新 CLI 框架) +- **globby**: 8.0.1 → 13.2.2 (现代文件匹配) +- **debug**: 4.1.0 → 4.3.7 (最新调试工具) +- **urllib**: 2.31.1 → 4.5.1 (最新 HTTP 客户端) +- **nunjucks**: 3.1.3 → 3.2.4 (最新模板引擎) +- **eslint**: 5.5.0 → 9.17.0 (最新代码检查) +- **移除 mkdirp** - 使用 Node.js 内置 `fs.mkdir({ recursive: true })` + +### Node.js 版本要求 +- **最低要求**: Node.js >= 18.0.0 +- 推荐使用 Node.js 18 LTS 或更高版本 + +### 工具链改进 +新增开发工具: +- **TypeScript 5.7** - 类型安全 +- **Prettier 3.4** - 代码格式化 +- **ESLint 9** - 代码质量检查 +- **tsx** - TypeScript 执行器 + +## 新功能 + +### 类型定义 +现在提供完整的 TypeScript 类型定义: +```typescript +import Jar2proxy from 'jar2proxy'; +import type { Jar2proxyOptions, ProxyConfig } from 'jar2proxy'; +``` + +### 改进的脚本命令 +```json +{ + "build": "tsc", // 编译 TypeScript + "dev": "tsc --watch", // 监听模式开发 + "lint": "eslint . --fix", // 代码检查 + "format": "prettier --write ...", // 代码格式化 + "typecheck": "tsc --noEmit", // 类型检查 + "build:java": "..." // 单独构建 Java 部分 +} +``` + +## 迁移指南 + +### 使用方式 +基本使用方式保持不变,但现在支持更好的类型提示: + +```typescript +// 使用 TypeScript +import Jar2proxy from 'jar2proxy'; + +const jar2proxy = new Jar2proxy({ + baseDir: '/path/to/project', + proxyConfigPath: 'config/proxy.js', +}); + +await jar2proxy.run(); +``` + +### 配置文件 +配置文件格式保持向后兼容,无需修改。 + +## 开发者说明 + +### 构建项目 +```bash +# 安装依赖 +npm install + +# 构建 TypeScript +npm run build + +# 可选:构建 Java 部分(需要 JDK 8) +npm run build:java +``` + +### 代码质量 +```bash +# 格式化代码 +npm run format + +# 代码检查 +npm run lint + +# 类型检查 +npm run typecheck +``` + +## 已知问题 + +### Java 构建 +- Java 代码需要 JDK 8 +- 使用较新 JDK 版本时,Java 构建可能需要调整 +- 如果遇到 `com.sun.tools.javadoc` 相关错误,请确保使用 JDK 8 + +## 向后兼容性 + +v2.0.0 保持了与 v1.x 的 API 兼容性: +- 配置文件格式不变 +- CLI 命令不变 +- 生成的代理文件格式不变 + +主要变化在于内部实现和开发体验的提升。 diff --git a/astparser/build.xml b/astparser/build.xml index c1090af..06020a6 100644 --- a/astparser/build.xml +++ b/astparser/build.xml @@ -22,7 +22,7 @@ - + diff --git a/bin/jar2proxy.ts b/bin/jar2proxy.ts new file mode 100644 index 0000000..394e09a --- /dev/null +++ b/bin/jar2proxy.ts @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import Jar2proxy from '../lib/jar2proxy'; +import createDebug from 'debug'; + +const debug = createDebug('jar2proxy:bin'); + +const program = new Command(); + +program + .option('-b, --base ', 'the base directory of the project') + .option('-t, --tpl ', 'path to template') + .option('-c, --config ', 'appoint the proxy config file path') + .allowUnknownOption() + .parse(process.argv); + +const options = program.opts(); + +const opts = { + baseDir: options.base, + defaultTpl: options.tpl, + proxyConfigPath: options.config, + isProduction: process.env.NODE_ENV === 'production', +}; + +debug('%j', opts); + +const jar2proxy = new Jar2proxy(opts); + +jar2proxy + .run() + .then(() => { + console.log('[jar2proxy] Generated completed.'); + console.log( + '[jar2proxy] You can see detail at %s/logs/jar2proxy-*.log', + jar2proxy['config'].baseDir + ); + setTimeout(() => { + process.exit(); + }, 1000); + }) + .catch((err) => { + console.error('[jar2proxy] Error:', err); + process.exit(1); + }); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..88b93bf --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,60 @@ +const js = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsparser = require('@typescript-eslint/parser'); +const prettier = require('eslint-plugin-prettier'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + js.configs.recommended, + prettierConfig, + { + files: ['**/*.ts', '**/*.js'], + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + 'astparser/**', + 'bin/libs/**', + '**/*.d.ts', + ], + plugins: { + '@typescript-eslint': tseslint, + prettier, + }, + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'no-console': 'off', + 'prettier/prettier': 'error', + }, + }, + { + files: ['test/**/*.js', 'test/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, + }, +]; diff --git a/lib/jar2proxy.ts b/lib/jar2proxy.ts new file mode 100644 index 0000000..da68fe4 --- /dev/null +++ b/lib/jar2proxy.ts @@ -0,0 +1,239 @@ +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import assert from 'assert'; +import JarProcessor from './java/jar'; +import createDebug from 'debug'; +import ProxyConfig from './proxy_config'; +import Dependency from './maven/dependency'; +import { globby } from 'globby'; +import nunjucks from './nunjucks'; +import createLogger from './logger'; +import type { + Jar2proxyOptions, + Jar2proxyConfig, + Logger, + ProxyConfig as ProxyConfigType, + ASTJson, + ApiConfig, +} from './types'; + +const debug = createDebug('jar2proxy:jar2proxy'); + +interface DefaultApiOptions { + tpl: string; + group: string; + version: string; + port: number; + responseTimeout: number; +} + +const DEFAULT_API_OPTIONS: DefaultApiOptions = { + tpl: 'proxy.js.tpl', + group: 'HSF', + version: '1.0', + port: 12200, + responseTimeout: 3000, +}; + +class Jar2proxy { + private config: Jar2proxyConfig; + private logger: Logger; + private jarDir: string; + private proxyConfig!: ProxyConfigType; + private astjson!: ASTJson; + private proxyDir!: string; + private proxyClassDir!: string; + + constructor(options: Jar2proxyOptions) { + const { + proxyConfigPath = 'config/proxy.js', + baseDir = process.cwd(), + defaultTpl = 'proxy.js.tpl', + isProduction = false, + } = options; + + this.config = { + baseDir, + proxyConfigPath, + defaultTpl, + isProduction, + }; + this.logger = createLogger(this.config.baseDir); + debug('constructor %j', this.config); + + assert(fs.existsSync(baseDir), `Application baseDir not exists! ${baseDir}`); + + const configPath = path.join(baseDir, proxyConfigPath); + assert(fs.existsSync(configPath), `Config file config/proxy.js not found! ${configPath}`); + + this.config.proxyConfigPath = configPath; + this.jarDir = path.join(os.tmpdir(), Date.now().toString()); + } + + loadConfig(): ProxyConfigType { + const { baseDir, proxyConfigPath } = this.config; + const proxyConfig = new ProxyConfig({ + baseDir, + proxyConfigPath, + logger: this.logger, + }); + const config = proxyConfig.readConfig(); + return config; + } + + async run(): Promise { + try { + const { baseDir } = this.config; + const config = this.loadConfig(); + this.logger.info('%j', config); + this.proxyConfig = config; + + const depd = new Dependency({ + baseDir, + directoryToJar: config.directoryToJar, + mavenRepository: config.mavenRepository, + dependencies: config.dependencies, + logger: this.logger, + jarDir: this.jarDir, + }); + + await depd.download(); + await this.genAST(config); + await this.renderByAST(); + } catch (err) { + this.logger.error(err); + if (this.logger.flush) { + this.logger.flush(); + } + console.log((err as Error).stack); + } + } + + private async genAST(proxyConfig: ProxyConfigType): Promise { + const jar = new JarProcessor({ + logger: this.logger, + }); + + const arr = await globby(['*-sources.jar'], { + cwd: this.jarDir, + }); + + jar.extract( + arr.map((jarName) => path.join(this.jarDir, jarName)), + this.jarDir + ); + + const astfile = jar.parse(this.jarDir, proxyConfig); + this.logger.info('[jar2proxy] astfile: %s', astfile); + + if (!fs.existsSync(astfile)) { + throw new Error("Ast result file can't be found"); + } + + this.astjson = require(astfile); + } + + private async renderByAST(): Promise { + this.proxyDir = path.join(this.config.baseDir, 'app/proxy'); + this.proxyClassDir = path.join(this.config.baseDir, 'app/proxy_class'); + + fs.mkdirSync(this.proxyDir, { recursive: true }); + fs.mkdirSync(this.proxyClassDir, { recursive: true }); + + fs.copyFileSync( + path.join(__dirname, './tpls/proxy_class.js'), + path.join(this.proxyClassDir, './index.js') + ); + + this.renderProxy(); + this.renderClass(); + } + + private renderProxy(): void { + const defaultOptions: any = { ...DEFAULT_API_OPTIONS }; + defaultOptions.responseTimeout = + this.proxyConfig.responseTimeout || defaultOptions.responseTimeout; + defaultOptions.version = this.proxyConfig.version || defaultOptions.version; + defaultOptions.group = this.proxyConfig.group || defaultOptions.group; + defaultOptions.tpl = this.config.defaultTpl || defaultOptions.tpl; + + for (const appService of this.proxyConfig.services) { + for (const apiName in appService.api) { + const apiConfigValue = appService.api[apiName]; + const apiConfig: ApiConfig = + typeof apiConfigValue === 'string' + ? { interfaceName: apiConfigValue } + : apiConfigValue; + + this.logger.info('[jar2proxy] render proxy %s: %s', apiName, apiConfig.interfaceName); + + const proxyAST = this.astjson.proxyMap[apiConfig.interfaceName]; + if (!proxyAST) { + this.logger.info('[jar2proxy] proxy %s not found.', apiConfig.interfaceName); + continue; + } + + const proxyName = apiName.substring(0, 1).toLowerCase() + apiName.substring(1); + const proxyModel = { + ...defaultOptions, + ...appService, + ...apiConfig, + proxyName, + proxyProfile: proxyAST, + }; + + this.logger.info('[jar2proxy] render proxy with model: %j', proxyModel); + + const content = nunjucks.renderString(this.getTemplateStr(proxyModel.tpl), proxyModel); + const proxyfile = path.join(this.proxyDir, proxyName) + '.js'; + + this.logger.info('[jar2proxy] write proxy name to %s', proxyfile); + fs.writeFileSync(proxyfile, content); + } + } + } + + private renderClass(): void { + for (const className in this.astjson.classMap) { + const classfile = this.initClassPath(className); + const classAST = this.astjson.classMap[className]; + this.logger.info('[jar2proxy] render class %s %j', className, classAST); + + const content = nunjucks.renderString(this.getTemplateStr('class.js.tpl'), { + class: classAST, + }); + fs.writeFileSync(classfile, content); + } + + for (const className in this.astjson.enumMap) { + const classfile = this.initClassPath(className); + const enumAST = this.astjson.enumMap[className]; + this.logger.info('[jar2proxy] render enum %s %j', className, enumAST); + + const content = nunjucks.renderString(this.getTemplateStr('enum.js.tpl'), enumAST); + fs.writeFileSync(classfile, content); + } + } + + private initClassPath(className: string): string { + const args = className.split('.'); + args.unshift(this.proxyClassDir); + args[args.length - 1] = args[args.length - 1] + '.js'; + + const classfile = path.join(...args); + fs.mkdirSync(path.dirname(classfile), { recursive: true }); + return classfile; + } + + private getTemplateStr(name: string): string { + if (path.isAbsolute(name)) { + return fs.readFileSync(name, 'utf8'); + } + + const str = fs.readFileSync(path.join(__dirname, './tpls', name), 'utf8'); + return str; + } +} + +export default Jar2proxy; diff --git a/lib/java/jar.ts b/lib/java/jar.ts new file mode 100644 index 0000000..fcc2a7f --- /dev/null +++ b/lib/java/jar.ts @@ -0,0 +1,111 @@ +import { execSync, spawnSync } from 'child_process'; +import platform from './platform'; +import { javahome, toolsPath } from './javahome'; +import { join, delimiter, basename } from 'path'; +import * as fs from 'fs'; +import createDebug from 'debug'; +import type { JarProcessorOptions, Logger, ProxyConfig } from '../types'; + +const debug = createDebug('jar2proxy:jar'); + +class JarProcessor { + private logger: Logger; + + constructor(options: JarProcessorOptions) { + this.logger = options.logger; + } + + copyTo(jarFile: string, distdir: string): void { + fs.copyFileSync(jarFile, join(distdir, basename(jarFile))); + } + + /** + * extract all jar files to target dir + * @param jarFiles all fullpath jar + * @param distdir target dir + */ + extract(jarFiles: string[], distdir: string): void { + debug('extract javahome: %s', javahome); + const winprefix = + platform === 'win' && /^\w:/.test(distdir) ? distdir.substring(0, 2) + ' && ' : ''; + const binjar = join(javahome, 'bin/jar'); + + for (const jarFile of jarFiles) { + if (!fs.existsSync(jarFile)) { + console.log('[jar2proxy:extract] jar file not exists: %s.', jarFile); + continue; + } + + // #37 compatible with windows cross drive cd, d:/tmp + const cmd = `${winprefix} cd ${distdir} && "${binjar}" -xf ${jarFile}`; + debug('exec: %s', cmd); + execSync(cmd, { encoding: 'utf8' }); + } + } + + /** + * parse + * @param sourcesDir source code dir + * @param proxyConfig proxy config + * @return ast file path + */ + parse(sourcesDir: string, proxyConfig: ProxyConfig): string { + const astfile = join(sourcesDir, 'proxy-ast.json'); + const classpath: string[] = []; + + this.logger.info('[jar2proxy] javahome: %s', javahome); + this.logger.info('[jar2proxy] toolsPath: %s', toolsPath); + + classpath.push(toolsPath); + classpath.push(join(__dirname, '../../bin/libs/fastjson-1.2.48.jar')); + classpath.push(join(__dirname, '../../bin/libs/log4j-core-2.11.1.jar')); + classpath.push(join(__dirname, '../../bin/libs/log4j-api-2.11.1.jar')); + classpath.push(join(__dirname, '../../bin/libs/astparser.jar')); + + const cmd: string[] = []; + // Running the container sometimes initializes less than 256m of memory. + cmd.push('-Xmx1024m'); + cmd.push('-Dfile.encoding=UTF-8'); + cmd.push('-Djava.awt.headless=true'); + cmd.push('-classpath'); + cmd.push(classpath.join(delimiter)); + cmd.push('com.ali.jar2proxy.astparser.AstParser'); + cmd.push('-source'); + cmd.push(sourcesDir); + cmd.push('-output'); + cmd.push(astfile); + + const interfaceNames: string[] = []; + proxyConfig.services.forEach((service) => { + for (const facadeName in service.api) { + const facade = service.api[facadeName]; + const interfaceName = + typeof facade === 'string' ? facade.split(':')[0] : facade.interfaceName; + + if (!interfaceName || interfaceName.indexOf('.') === -1) { + throw new Error( + `Please config interface name [${facadeName}:${interfaceName}] at proxy.js with right package name` + ); + } + interfaceNames.push(interfaceName); + } + }); + + cmd.push('-proxy'); + cmd.push(interfaceNames.join(':')); + + const bin = join(javahome, 'bin/java'); + this.logger.info('[jar2proxy] parse: %s %s', bin, cmd.join(' ')); + + const result = spawnSync(bin, cmd, { + stdio: 'pipe', + }); + + this.logger.info('%s', result.stdout); + this.logger.info('%s', result.stderr); + + return astfile; + } +} + +export default JarProcessor; diff --git a/lib/java/javahome.ts b/lib/java/javahome.ts new file mode 100644 index 0000000..7f2ad3d --- /dev/null +++ b/lib/java/javahome.ts @@ -0,0 +1,114 @@ +import platform from './platform'; +import { join } from 'path'; +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import createDebug from 'debug'; + +const debug = createDebug('jar2proxy:javahome'); + +interface JavaHome { + javahome: string; + toolsPath: string; +} + +function exec(cmd: string): string { + return (execSync(cmd) || '').toString().replace('\n', ''); +} + +function osx(): JavaHome { + let toolsPath = ''; + let javahome = (process.env.JAVA_HOME || '').trim(); + + if (!javahome) { + javahome = exec('/usr/libexec/java_home'); + debug('osx.libexec', javahome); + } + + if (javahome) { + if (fs.existsSync(javahome)) { + javahome = fs.realpathSync(javahome); + debug('osx.realpath', javahome); + } + + // jdk1.6 toolsPath = join(javahome, '../Classes/classes.jar'); + // jdk1.7, jdk1.8 has different dir with mac + // only support jdk1.8 + toolsPath = join(javahome, 'lib/tools.jar'); + + if (!fs.existsSync(toolsPath)) { + throw new Error( + 'lib/tools.jar not found in JAVA_HOME! Only support jdk1.8, javahome: ' + javahome + ); + } + } + + return { + javahome, + toolsPath, + }; +} + +function linux(): JavaHome { + let toolsPath = ''; + let javahome = (process.env.JAVA_HOME || '').trim(); + + if (fs.existsSync(javahome)) { + javahome = fs.realpathSync(javahome); + } + + toolsPath = join(javahome, 'lib/tools.jar'); + + return { + javahome, + toolsPath, + }; +} + +function win(): JavaHome | null { + let javahome = (process.env.JAVA_HOME || '').trim(); + + if (javahome) { + javahome = javahome.replace(/[\r\n]/g, ''); + return { + javahome, + toolsPath: join(javahome, 'lib/tools.jar'), + }; + } + + const roots = ['C:/Program Files/Java', 'C:/Program Files (x86)/Java']; + + for (const root of roots) { + if (fs.existsSync(root)) { + const files = fs.readdirSync(root); + for (const file of files) { + if (/^jdk/.test(file)) { + return { + javahome: join(root, file), + toolsPath: join(root, file, 'lib/tools.jar'), + }; + } + } + } + } + + return null; +} + +let javaHomeConfig: JavaHome | null = null; + +if (platform === 'osx') { + javaHomeConfig = osx(); +} else if (platform === 'linux') { + javaHomeConfig = linux(); +} else if (platform === 'win') { + javaHomeConfig = win(); +} + +if (!javaHomeConfig || !fs.existsSync(javaHomeConfig.toolsPath)) { + throw new Error( + (javaHomeConfig?.toolsPath || 'JAVA_HOME') + " Can't be found or not correct!" + ); +} + +export const { javahome, toolsPath } = javaHomeConfig; +export default javaHomeConfig; diff --git a/lib/java/platform.ts b/lib/java/platform.ts new file mode 100644 index 0000000..7c5e4fb --- /dev/null +++ b/lib/java/platform.ts @@ -0,0 +1,22 @@ +import * as os from 'os'; + +const osPlatform = os.platform(); +let platform: 'osx' | 'linux' | 'win' | undefined; + +switch (osPlatform) { + case 'darwin': + platform = 'osx'; + break; + case 'linux': + case 'freebsd': + platform = 'linux'; + break; + case 'win32': + case 'cygwin': + platform = 'win'; + break; + default: + break; +} + +export default platform; diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..3babf09 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import Logger from 'mini-logger'; +import type { Logger as LoggerType } from './types'; + +/** + * log all runtime info to jar2proxy-info.log + * @param baseDir application dir + * @return return logger instance + */ +export default function createLogger(baseDir: string): LoggerType { + const logdir = path.join(baseDir, 'logs'); + fs.mkdirSync(logdir, { recursive: true }); + + return Logger({ + dir: logdir, + categories: ['info'], + format: '[jar2proxy-{category}.]YYYY-MM-DD[.log]', + flushInterval: '1ms', + timestamp: true, + seperator: '\n', + }); +} diff --git a/lib/maven/dependency.ts b/lib/maven/dependency.ts new file mode 100644 index 0000000..b07381f --- /dev/null +++ b/lib/maven/dependency.ts @@ -0,0 +1,210 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { request, RequestOptions } from 'urllib'; +import copy from 'copy-to'; +import xml2map from 'xml2map'; +import type { DependencyConfig, Dependency as DependencyType, Logger } from '../types'; + +interface JarUrlHelper extends DependencyType { + mavenRepository?: string | null; + timestamp?: number; + jarVersion?: string; + sources?: boolean; +} + +class Dependency { + private config: DependencyConfig; + private directoryToJar: string; + private jarDir: string; + private logger: Logger; + + constructor(config: DependencyConfig) { + this.config = config; + const { baseDir, directoryToJar, logger, jarDir } = this.config; + + this.directoryToJar = directoryToJar || path.join(baseDir, 'assembly'); + this.jarDir = jarDir; + fs.mkdirSync(this.jarDir, { recursive: true }); + this.logger = logger; + } + + async download(): Promise { + await this.execDownload(); + } + + private async execDownload(): Promise { + const jarUrls: Record = {}; + const { mavenRepository, dependencies } = this.config; + + for (const dependency of dependencies) { + if (!dependency) { + continue; + } + + const key = dependency.artifactId + '-' + dependency.version; + const helper: JarUrlHelper = { + groupId: dependency.groupId, + artifactId: dependency.artifactId, + version: dependency.version, + mavenRepository, + timestamp: Date.now(), + ignoreSources: dependency.ignoreSources, + }; + + // if mavenRepository is empty only try read file from local libs dir + if (mavenRepository && /-SNAPSHOT$/i.test(helper.version)) { + const metadataUrl = `${mavenRepository}/${helper.groupId.replace(/\./g, '/')}/${helper.artifactId}/${helper.version}/maven-metadata.xml`; + this.logger.info('[jar2proxy] metadata %s', metadataUrl); + + const rvData = await this.request( + metadataUrl, + { + method: 'GET', + headers: { + accept: '*/*', + 'accept-language': 'zh-CN,zh;', + }, + timeout: 30000, + }, + 3 + ); + + if (rvData.status === 200) { + const dataStr = rvData.data?.toString() || ''; + (rvData as any).data = xml2map.tojson(dataStr); + } + + this.logger.info('[jar2proxy] SNAPSHOT metadata: %j', rvData.data); + + if (!(rvData.data as any).metadata || !(rvData.data as any).metadata.versioning) { + throw new Error( + `Jar Not Found SNAPSHOT: ${helper.groupId}/${helper.artifactId}/${helper.version}` + ); + } + + const versioning = (rvData.data as any).metadata.versioning; + + if (!versioning.snapshotVersions) { + const snapshot = versioning.snapshot; + const version = `${helper.version.replace('-SNAPSHOT', '')}-${snapshot.timestamp}-${snapshot.buildNumber}`; + jarUrls[key + '.jar'] = copy({ jarVersion: version }).and(helper).to(); + if (!helper.ignoreSources) { + jarUrls[key + '-sources.jar'] = copy({ jarVersion: `${version}-sources` }) + .and(helper) + .to({ sources: true }); + } + } else { + const versions = versioning.snapshotVersions.snapshotVersion; + const sources = versions.filter((v: any) => v.classifier === 'sources')[0]; + + if (!sources) { + throw new Error( + `${key}-sources.jar is missing, Please contact the package administrator.` + ); + } + + const jar = versions.filter((v: any) => !v.classifier)[0]; + jarUrls[key + '.jar'] = copy({ jarVersion: sources.value }).and(helper).to(); + if (!helper.ignoreSources) { + jarUrls[key + '-sources.jar'] = copy({ jarVersion: `${jar.value}-sources` }) + .and(helper) + .to({ sources: true }); + } + } + } else { + jarUrls[key + '.jar'] = helper; + if (!helper.ignoreSources) { + jarUrls[key + '-sources.jar'] = copy({ jarVersion: `${helper.version}-sources` }) + .and(helper) + .to({ sources: true }); + } + } + } + + await Promise.all( + Object.keys(jarUrls).map((fileName) => this.createTask(jarUrls[fileName], fileName)) + ); + } + + private async createTask(helper: JarUrlHelper, fileName: string): Promise { + this.logger.info(this.jarDir, fileName); + + if (!helper.mavenRepository) { + await this.createCopyTask(helper, fileName); + return; + } + + const startTime = Date.now(); + const filepath = path.join(this.jarDir, fileName); + const jarUrl = `${helper.mavenRepository}/${helper.groupId.replace(/\./g, '/')}/${helper.artifactId}/${helper.version}/${helper.artifactId}-${helper.jarVersion || helper.version}.jar`; + + this.logger.info('downloading: %j %s', helper, jarUrl); + + const writeStream = fs.createWriteStream(filepath); + const rvData = await this.request( + jarUrl, + { + method: 'GET', + headers: { + accept: '*/*', + 'accept-language': 'zh-CN,zh;', + }, + writeStream, + timeout: 600 * 1000, + }, + 3 + ); + + if (rvData.status !== 200) { + throw new Error('Jar Not Found ' + jarUrl); + } + + this.logger.info( + '[jar2proxy] Downloaded %s %s %sms', + jarUrl, + rvData.status, + Date.now() - startTime + ); + } + + // try copy jar file from jarDir if mavenRepository empty + private async createCopyTask(_helper: JarUrlHelper, fileName: string): Promise { + const filepath = path.join(this.jarDir, fileName); + fs.copyFileSync(path.join(this.directoryToJar, fileName), filepath); + } + + private async request( + url: string, + args: RequestOptions, + retry: number + ): Promise<{ status: number; data: any }> { + retry--; + + try { + const result = await request(url, args); + return { + status: result.status, + data: result.data, + }; + } catch (err: any) { + if (retry <= 0) { + throw err; + } + + if (err.code === 'ENOTFOUND' || err.code === 'ECONNRESET' || err.code === 'ENETRESET') { + console.warn( + '[jar2proxy] request %s error: %s, retry after 100ms, left: %s', + url, + err, + retry + ); + console.error(err); + return await this.request(url, args, retry); + } + + throw err; + } + } +} + +export default Dependency; diff --git a/lib/nunjucks.ts b/lib/nunjucks.ts new file mode 100644 index 0000000..40c4843 --- /dev/null +++ b/lib/nunjucks.ts @@ -0,0 +1,166 @@ +import * as nunjucks from 'nunjucks'; +import { stringify } from 'cassandra-map'; + +interface Field { + type?: string; + generic?: Field[]; + arrayType?: boolean; + arrayDepth?: number; +} + +function getType(str: string, field?: Field, onlyBasis?: boolean): string { + const generic = field?.generic; + + switch (str) { + case 'boolean': + case 'Boolean': + return 'boolean'; + case 'String': + return 'string'; + case 'short': + case 'Short': + case 'long': + case 'Long': + case 'Int': + case 'int': + case 'Integer': + case 'float': + case 'Float': + case 'double': + case 'Double': + case 'BigDecimal': + return 'number'; + case 'Void': + case 'void': + return 'void'; + case 'Date': + return 'Date'; + case 'Map': + case 'HashMap': + case 'Properties': + case 'Currency': + if (Array.isArray(generic) && generic.length === 2) { + const key = getType(splitLast(generic[0].type || '', '.'), generic[0], onlyBasis); + const val = getType(splitLast(generic[1].type || '', '.'), generic[1], onlyBasis); + return `{ [key: ${key === 'number' ? key : 'string'}]: ${val} }`; + } + return 'Object'; + case 'List': + case 'ArrayList': + case 'Collection': + if (generic && generic.length && generic[0].type) { + const val = getType(splitLast(generic[0].type, '.'), generic[0], onlyBasis); + return `${val}[]`; + } + return 'any[]'; + case 'Set': + if (generic && generic.length && generic[0].type) { + const val = getType(splitLast(generic[0].type, '.'), generic[0], onlyBasis); + return `Set<${val}>`; + } + return 'Set'; + default: + // T, K, V generic type + return onlyBasis === true ? 'any' : str.length === 1 ? 'any' : str; + } +} + +function splitLast(str: string, sep: string): string { + if (!str || typeof str !== 'string') { + return ''; + } + + const items = str.split(sep); + return items[items.length - 1]; +} + +const filters = { + stringify, + toURL(val: string): string { + return val.replace(/\./g, '/'); + }, + comment(text: string, indent?: number): string { + text = (text || '').trim(); + + if (!text) { + return ''; + } + + indent = indent || 0; // spaces of indent + const indentStr = new Array(indent + 2).join(' '); // indent string + + return ( + '/**\n' + + indentStr + + '* ' + + text.replace(/\n/g, '\n' + indentStr + '*') + + '\n' + + indentStr + + '*/' + ); + }, + antx(val: string): string { + val = val || ''; + const match = val.match(/^\${(.*)}$/); + if (match && match[1]) { + return "app.config['" + match[1] + "']"; + } + return "'" + val + "'"; + }, + + formatParams(str: string): string { + return (str || '') + .trim() + .split(',') + .filter((s) => !!s.trim()) + .map((s) => s.trim()) + .join(', '); + }, + + // return first param of arguments + firstParam(str: string): string { + return (str || '') + .trim() + .split(',') + .filter((s) => !!s.trim()) + .map((s) => s.trim())[0]; + }, + + arrayParam(str: string): string[] { + return (str || '') + .trim() + .split(',') + .filter((s) => !!s.trim()) + .map((s) => s.trim()); + }, + + // split and return the last element + splitLast, + + upperFirst(str: string): string { + return str[0].toUpperCase() + str.substring(1); + }, + + lowerFirst(str: string): string { + return str[0].toLowerCase() + str.substring(1); + }, + + getType(str: string, field?: Field, onlyBasis?: boolean): string { + const type = getType(str, field, onlyBasis); + let suffix = ''; + if (field?.arrayType) { + for (let i = 0; i < (field.arrayDepth || 0); i++) { + suffix += '[]'; + } + } + return type + suffix; + }, +}; + +const engine = nunjucks.configure({ autoescape: false, watch: false }); + +for (const k in filters) { + engine.addFilter(k, (filters as any)[k]); +} + +export default engine; diff --git a/lib/proxy_config.ts b/lib/proxy_config.ts new file mode 100644 index 0000000..a27bea1 --- /dev/null +++ b/lib/proxy_config.ts @@ -0,0 +1,312 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { getPlugins } from 'egg-utils'; +import assert from 'assert'; +import type { ProxyConfig as ProxyConfigType, Logger, Service, Dependency } from './types'; + +interface Plugin { + enable: boolean; + path: string; + name: string; + version?: string; +} + +interface ProxyConfigOptions { + baseDir: string; + proxyConfigPath: string; + logger: Logger; +} + +interface DependencyWithPlugin extends Dependency { + __fromPlugin?: string; +} + +interface ServiceWithDependency extends Service { + dependency: DependencyWithPlugin[]; +} + +interface ProxyConfigWithPlugin extends Partial { + __fromPlugin?: string; + services: ServiceWithDependency[]; +} + +class ProxyConfig { + private baseDir: string; + private proxyConfigPath: string; + private logger: Logger; + private appProxyConfig!: ProxyConfigWithPlugin; + private pluginProxyConfigs!: ProxyConfigWithPlugin[]; + private proxyConfig!: ProxyConfigType; + + constructor(options: ProxyConfigOptions) { + const { baseDir, proxyConfigPath, logger } = options; + assert(logger, "property logger can't be empty"); + assert(baseDir, "property baseDir can't be empty"); + assert(proxyConfigPath, "property proxyConfigPath can't be empty"); + this.baseDir = baseDir; + this.proxyConfigPath = proxyConfigPath; + this.logger = logger; + } + + readConfig(): ProxyConfigType { + const proxyConfig = require(this.proxyConfigPath) as ProxyConfigWithPlugin; + const { eggFramework = 'egg' } = proxyConfig; + this.logger.info('getPlugins %s %s', this.baseDir, eggFramework); + + let plugins: Record = {}; + + try { + plugins = getPlugins({ + baseDir: this.baseDir, + framework: path.join(this.baseDir, 'node_modules', eggFramework), + }); + } catch (err) { + plugins = {}; + this.logger.info('getPlugins failed.'); + this.logger.error(err); + } + + this.logger.info('getPlugins: %j', plugins); + this.appProxyConfig = proxyConfig; + this.pluginProxyConfigs = this.readPluginProxyConfig(plugins); + this.proxyConfig = this.mergeProxyConfig(); + return this.proxyConfig; + } + + private readPluginProxyConfig(plugins: Record): ProxyConfigWithPlugin[] { + const proxyConfigs: ProxyConfigWithPlugin[] = []; + + for (const pluginName in plugins) { + const plugin = plugins[pluginName]; + if (!plugin.enable) { + continue; + } + + const proxyPath = path.join(plugin.path, 'app/proxy'); + const configPath = path.join(plugin.path, 'config/proxy.js'); + + // If plugin author pushed jar2proxy result files, jar2proxy will skip process when exec in application + if (!fs.existsSync(proxyPath) && fs.existsSync(configPath)) { + const pluginConfig = require(configPath) as ProxyConfigWithPlugin; + pluginConfig.__fromPlugin = plugin.path; + proxyConfigs.push(pluginConfig); + } + } + + return proxyConfigs; + } + + // merge all plugin proxy config to app/config/proxy.js + private mergeProxyConfig(): ProxyConfigType { + // app/config/proxy.js + let appConfig = this.appProxyConfig; + + // ${plugins}/config/proxy.js + this.pluginProxyConfigs.forEach((pluginConfig) => { + appConfig = this.mergePluginConfigToAppConfig(this.appProxyConfig, pluginConfig); + }); + + if (!Array.isArray(appConfig.services)) { + throw new Error("proxyConfig.services can't be empty."); + } + + const dependencies = this.mergeDependencies(appConfig); + + return { + ...appConfig, + dependencies, + } as ProxyConfigType; + } + + private mergePluginConfigToAppConfig( + appConfig: ProxyConfigWithPlugin, + pluginConfig: ProxyConfigWithPlugin + ): ProxyConfigWithPlugin { + const mergedConfig = { ...appConfig }; + const keys = ['group', 'responseTimeout', 'errorAsNull']; + + const mergeService = (pluginService: ServiceWithDependency) => { + // 打标签,方便后面合并 jar 处理 + pluginService.dependency = Array.isArray(pluginService.dependency) + ? pluginService.dependency + : [pluginService.dependency as any]; + + pluginService.dependency.forEach((item) => { + if (!item) { + return; + } + item.__fromPlugin = pluginConfig.__fromPlugin; + }); + + const targetService = mergedConfig.services.find( + (mergedService) => mergedService.appName === pluginService.appName + ); + + if (targetService) { + targetService.api = { ...targetService.api, ...pluginService.api }; + const diffDependency = pluginService.dependency.filter((pluginDepd) => { + if (!pluginDepd) { + return false; + } + const isSameDep = targetService.dependency.some( + (appDepd) => + pluginDepd.groupId === appDepd.groupId && + pluginDepd.artifactId === appDepd.artifactId && + pluginDepd.version === appDepd.version + ); + return !isSameDep; + }); + targetService.dependency = targetService.dependency.concat(diffDependency); + } else { + mergedConfig.services.push(pluginService); + } + }; + + for (const keyName in pluginConfig) { + if ( + keys.some((item) => item === keyName) && + !Object.prototype.hasOwnProperty.call(mergedConfig, keyName) + ) { + (mergedConfig as any)[keyName] = (pluginConfig as any)[keyName]; + console.log( + '[jar2proxy] `%s` not found in config/proxy.js use plugin config first [%s]', + keyName, + (mergedConfig as any)[keyName] + ); + } + + if (keyName !== 'services') { + continue; + } + + pluginConfig.services.forEach(mergeService); + } + + return mergedConfig; + } + + private mergeDependencies(appConfig: ProxyConfigWithPlugin): DependencyWithPlugin[] { + const tmpAppDependencies: DependencyWithPlugin[] = []; + const appDependencies: DependencyWithPlugin[] = []; + const pluginDependencies: DependencyWithPlugin[] = []; + const services = appConfig.services; + + for (const service of services) { + const depds = Array.isArray(service.dependency) + ? service.dependency + : [service.dependency as any]; + + depds.forEach((depd) => { + if (!depd) { + return; + } + if (depd.__fromPlugin) { + pluginDependencies.push(depd); + } else { + tmpAppDependencies.push(depd); + } + }); + } + + // check if app config had conflict depd config + for (const depd of tmpAppDependencies) { + if ( + appDependencies.find( + (item) => item.groupId === depd.groupId && item.artifactId === depd.artifactId + ) + ) { + continue; + } + + const result = tmpAppDependencies.filter( + (item) => item.artifactId === depd.artifactId && item.groupId === depd.groupId + ); + + if (result.length >= 2) { + const message = ` + App dependency "groupId:${depd.groupId}, artifactId:${depd.artifactId}" appeared twice, you should delete the old one! + Problem dependencies: ${JSON.stringify(result, null, 2)} + `; + this.dependencyConflictCheck(result, message); + appDependencies.push(result[0]); + } else { + appDependencies.push(depd); + } + } + + const pluginHasChecked: DependencyWithPlugin[] = []; + + pluginDependencies.forEach((pluginDepd) => { + if (!pluginDepd) { + return; + } + + if ( + pluginHasChecked.find( + (item) => item.groupId === pluginDepd.groupId && item.artifactId === pluginDepd.artifactId + ) + ) { + return; + } + + const result = pluginDependencies.filter((item) => { + if (!item || !pluginDepd) { + return false; + } + return item.artifactId === pluginDepd.artifactId && item.groupId === pluginDepd.groupId; + }); + + const existInApp = appDependencies.find( + (appDepd) => + appDepd.artifactId === pluginDepd.artifactId && appDepd.groupId === pluginDepd.groupId + ); + + if (result.length >= 2 && !existInApp) { + const message = ` + Plugin dependency "groupId:${pluginDepd.groupId}, artifactId:${pluginDepd.artifactId}" appeared twice but not found in app, you can override plugin config with assign newer one in app! + Problem dependencies: ${JSON.stringify(result, null, 2)} + `; + this.dependencyConflictCheck(result, message); + appDependencies.push(result[0]); + } else if (result.length === 1) { + if (existInApp) { + this.logger.info( + '[jar2proxy] App and Plugin dependency conflict, default use app dependency first!' + ); + this.logger.info(`Plugin dependency: ${JSON.stringify(pluginDepd, null, 2)}`); + this.logger.info(`App dependency: ${JSON.stringify(existInApp, null, 2)}`); + } else { + appDependencies.push(pluginDepd); + } + } + + pluginHasChecked.push(pluginDepd); + }); + + return appDependencies; + } + + private dependencyConflictCheck(dependencies: DependencyWithPlugin[], message: string): void { + if (dependencies.length === 1) { + return; + } + + const versions: string[] = []; + + dependencies.forEach((depd) => { + if (!versions.some((ver) => ver === depd.version)) { + versions.push(depd.version); + } + }); + + if (versions.length === 1) { + // print the same depd, but do not stop task + console.warn(message); + } else { + // multiple versions, break the task. + throw new Error(message); + } + } +} + +export default ProxyConfig; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..00cb749 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,105 @@ +export interface ProxyConfig { + eggFramework?: string; + directoryToJar?: string; + mavenRepository?: string | null; + group?: string; + version?: string; + responseTimeout?: number; + dependencies: Dependency[]; + services: Service[]; +} + +export interface Service { + appName: string; + group?: string; + version?: string; + responseTimeout?: number; + api: Record; +} + +export interface ApiConfig { + interfaceName: string; + version?: string; + group?: string; + port?: number; + responseTimeout?: number; + tpl?: string; +} + +export interface Dependency { + groupId: string; + artifactId: string; + version: string; + ignoreSources?: boolean; +} + +export interface Jar2proxyOptions { + proxyConfigPath?: string; + baseDir?: string; + defaultTpl?: string; + isProduction?: boolean; +} + +export interface Jar2proxyConfig { + baseDir: string; + proxyConfigPath: string; + defaultTpl: string; + isProduction: boolean; +} + +export interface Logger { + info: (...args: any[]) => void; + error: (...args: any[]) => void; + warn?: (...args: any[]) => void; + flush?: () => void; +} + +export interface ASTJson { + proxyMap: Record; + classMap: Record; + enumMap: Record; +} + +export interface ProxyAST { + interfaceName: string; + methods: MethodAST[]; +} + +export interface MethodAST { + name: string; + returnType: string; + parameters: ParameterAST[]; +} + +export interface ParameterAST { + name: string; + type: string; +} + +export interface ClassAST { + className: string; + fields: FieldAST[]; +} + +export interface FieldAST { + name: string; + type: string; +} + +export interface EnumAST { + className: string; + values: string[]; +} + +export interface DependencyConfig { + baseDir: string; + directoryToJar?: string; + mavenRepository?: string | null; + dependencies: Dependency[]; + logger: Logger; + jarDir: string; +} + +export interface JarProcessorOptions { + logger: Logger; +} diff --git a/lib/types/cassandra-map.d.ts b/lib/types/cassandra-map.d.ts new file mode 100644 index 0000000..5c42ce6 --- /dev/null +++ b/lib/types/cassandra-map.d.ts @@ -0,0 +1,3 @@ +declare module 'cassandra-map' { + export function stringify(obj: any): string; +} diff --git a/lib/types/copy-to.d.ts b/lib/types/copy-to.d.ts new file mode 100644 index 0000000..a0e49a5 --- /dev/null +++ b/lib/types/copy-to.d.ts @@ -0,0 +1,9 @@ +declare module 'copy-to' { + function copy>(obj: T): { + and: >(target: U) => { + to: >(extra?: V) => T & U & V; + }; + }; + + export = copy; +} diff --git a/lib/types/egg-utils.d.ts b/lib/types/egg-utils.d.ts new file mode 100644 index 0000000..b46b062 --- /dev/null +++ b/lib/types/egg-utils.d.ts @@ -0,0 +1,16 @@ +declare module 'egg-utils' { + interface Plugin { + enable: boolean; + path: string; + name: string; + version?: string; + package?: string; + } + + interface GetPluginsOptions { + baseDir: string; + framework?: string; + } + + export function getPlugins(options: GetPluginsOptions): Record; +} diff --git a/lib/types/mini-logger.d.ts b/lib/types/mini-logger.d.ts new file mode 100644 index 0000000..0f882fc --- /dev/null +++ b/lib/types/mini-logger.d.ts @@ -0,0 +1,21 @@ +declare module 'mini-logger' { + interface LoggerOptions { + dir: string; + categories?: string[]; + format?: string; + flushInterval?: string; + timestamp?: boolean; + seperator?: string; + } + + interface Logger { + info: (...args: any[]) => void; + error: (...args: any[]) => void; + warn?: (...args: any[]) => void; + flush?: () => void; + } + + function MiniLogger(options: LoggerOptions): Logger; + + export = MiniLogger; +} diff --git a/lib/types/xml2map.d.ts b/lib/types/xml2map.d.ts new file mode 100644 index 0000000..42cc94f --- /dev/null +++ b/lib/types/xml2map.d.ts @@ -0,0 +1,8 @@ +declare module 'xml2map' { + interface Xml2Map { + tojson: (xml: string) => any; + } + + const xml2map: Xml2Map; + export = xml2map; +} diff --git a/package.json b/package.json index 22439c7..dade7e9 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,34 @@ { "name": "jar2proxy", - "version": "1.0.7", + "version": "2.0.0", "author": "coolme200", "keywords": [ "jar", - "proxy" + "proxy", + "typescript", + "egg" ], "description": "transfer java facade with jar to js", - "main": "index.js", + "main": "dist/lib/jar2proxy.js", + "types": "dist/lib/jar2proxy.d.ts", "bin": { - "jar2proxy": "bin/jar2proxy.js" + "jar2proxy": "dist/bin/jar2proxy.js" }, "files": [ "bin", - "lib" + "lib", + "dist" ], + "engines": { + "node": ">=18.0.0" + }, "scripts": { + "build:java": "./node_modules/mirant/bin/ant -f astparser/build.xml", + "build": "tsc", + "dev": "tsc --watch", "test": "npm run build && egg-bin test", + "format": "prettier --write \"**/*.{ts,js,json,md}\"", + "typecheck": "tsc --noEmit", "lint": "eslint .", "cov": "npm run build && egg-bin cov", "ci": "npm run lint && npm run cov", @@ -26,29 +38,36 @@ }, "dependencies": { "cassandra-map": "^0.1.7", - "commander": "^2.19.0", + "commander": "^12.1.0", "copy-to": "^2.0.1", - "debug": "^4.1.0", + "debug": "^4.3.7", "egg-utils": "^2.4.1", - "extend2": "^1.0.0", - "globby": "^8.0.1", + "globby": "^13.2.2", "mini-logger": "^1.1.3", - "mkdirp": "^0.5.1", - "nunjucks": "^3.1.3", - "urllib": "^2.31.1", + "nunjucks": "^3.2.4", + "urllib": "^4.5.1", "xml2map": "^1.0.2" }, "devDependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^22.10.2", + "@types/nunjucks": "^3.2.6", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", "autod": "^3.0.1", - "rimraf": "^2.6.2", - "fs-extra": "^7.0.0", - "egg-bin": "^4.8.5", - "mirant": "1.9.6", - "coffee": "^5.1.0", "await-event": "^2.1.0", - "enums": "1.0.1", - "eslint": "^5.5.0", - "eslint-config-egg": "^7.1.0" + "coffee": "^5.5.0", + "egg-bin": "^4.8.5", + "enums": "^1.0.1", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "fs-extra": "^11.2.0", + "mirant": "^1.9.6", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" }, "license": "MIT" } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..84fc306 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "types": ["node"], + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "lib/**/*", + "bin/**/*", + "test/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "astparser" + ] +}