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"
+ ]
+}