前言#
前几天公司的一个前端大佬用js写了一个库,基于通义千问的api将公司某项目中的js文件给配置上了d.ts。
然后我想着这玩意儿用rust写一个应该挺舒服,毕竟是工具。
代码:
git clone https://github.com/1714080902120/auto_gen_d_ts.git通义千问#
不过我们这里是基于api来实现的,所以我们实际要去到的网站是:如何使用通义千问API_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
创建API-KEY#
我们要请求它的接口,第一步就是要有请求的权限,官方要求请求得加上DashScope API-KEY:如何开通DashScope并创建API-KEY_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
这里就不展示创建流程了,官方的文档已经说的很清楚了。
选择模型#
我们这里没有特殊要求,所以直接用最简单的大语言模型就好:通义千问模型简介_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
接口#
我们要的接口:POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation
然后请求的时候把API-KEY传入header里即可
配置#
我们是基于接口请求来实现而不是SDK(问题是也没有呀,虽然也可以通过interface的方式去接入Python的SDK),所以我们这里需要用到:
- reqwest:不用多说
- serde_json:也是不用多说
- tokio:怎么还是不用多说?!
实现#
初始化#
cargo new auto_gen_d_ts
cd auto_gen_d_ts
cargo add reqwest
cargo add tokio
cargo add serde_jsonCargo.toml#
[package]
name = "rust_auto_gen_d_ts"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.12.3", features = ["json"] }
serde_json = "1.0.116"
tokio = { version = "1", features = ["full"] }demo#
我们先搞一个请求的demo,确保流程跑的通
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = env::var(Q_WEN_KEY_NAME)?;
let client = Client::new();
let url = String::from(
"https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
);
let file_str = fs::read_to_string("./test/jump.js").await?;
let msg = format!(
"我有一组JavaScript函数:
```javscript
{}我不需要你解释,只需要给出类型代码即可,不要你任何的描述,也不要注释。
我希望你可以给出这些函数的typescript类型。
我只要类型,并且可以放到d.ts文件中作为这些函数的类型提示。
",
file_str
);
let payload = json!({
"input": {
"prompt": msg
},
"parameters": {
"temperature": 0.9,
"top_p": 0.9,
"max_tokens": 1024,
"stream": false,
"result_format": "message",
},
"model": "qwen-plus",
});
let res = client
.post(url)
.header("Authorization", format!("Bearer {}", key))
.json(&payload)
.send()
.await?;
let res: serde_json::Value = res.json().await?;
dbg(&res);
Ok(())}
我们这里通过环境变量`Q_WEN_KEY`来获取`API-KEY`。
比如:
```bash
$ENV:Q_WEN_KEY='XXX'然后我们在根目录创建一个test文件夹,往里放一个jump.js(不要问为啥叫这名字,从公司项目里随便找了个文件copy过来的。。里面代码我删掉了,毕竟公司的资产)
export function test(a, b) {
return a + b;
}
export const test2 = () => {
return "haha";
};然后我们cargo run
[src\main.rs:99] &res = "```typescript\nexport function test(a: number, b: number): number {\n}\n\nexport const test2: () => string = () => {\n return \"haha\";\n};\n```\n这段代码定义了`test`和`test2`的类型,可以放 入`.d.ts`文件中为JavaScript函数提供TypeScript类型提示。`test`函数接受两个数字参数并返回一个数字,`test2`是一个无参数函数,返回一个字符串。"
把多余的话去掉:
export function test(a: number, b: number): number {}
export const test2: () => string = () => { return "haha"; }不是很对,这说明我们的prompt没写好,不过我们现在的目的不是这个,而是实现请求并且有反馈。目前我们做到了这点。
需求#
前面我们确认了这种方案是走得通的,那么我们就基于这个方案做一下拓展,将它变成一个工具:
- 要有输入框,用户可以指定要处理的文件(夹)和存放输出的位置
- 支持批量处理
js,只针对js文件里export的函数(包括箭头函数) - 生成对应的
typescript类型 - 批量写入
d.ts文件
需求分析(简单分析)#
- 我们需要有三个参数,一个要处理的目标路径
input_path,一个要存放处理后文件的路径output_path,其中input_path可以是文件或者文件夹。两个路径需要兼顾相对路径和绝对路径。还需要有个deep♂的参数用来自定义是否需要递归子文件夹 - 将相对路径转换成绝对路径,然后根据路径收集文件名和文件内容
- 只处理
export function和export const xx = () => {}这两种,所以我们需要对源码进行过滤,这里就需要将js解析成AST - 将源码发送给通义千问,让它返回给对应类型
- 根据目标路径的文件夹结构创建同名文件夹和文件名.d.ts
那么就先简单分析到这里。
代码架构#
根据上面我们分析之后的几点,我们先完成代码的架构
- 有一个
InputArgs,用来获取三个参数和转换成绝对路径 - 有一个
read,用来获取文件的名字和内容,转换成File结构 - 要有
compile,即将js源码转换成AST,并且从里面拿到符合要求的源码 - 要有一个
ask,用来访问通义千问 - 要有一个
write,将访问得到的内容转换成对应的文件
整理一下:
src
args // 获取输入的参数
compile
parse // 将`js`解析成`AST`
transform // `visit`节点,拿到需要的源码
gen // 理论上应该是整理transform之后代码,但是我这里是基于`ask`得到的数据再生成`file_io write`需要的代码结构
constants // 存放一些静态数据
file_io
read // 读取文件
write // 生成文件
ask
client // 请求相关逻辑
main.rs // 入口说一下为啥要这么拆分,因为这么做我们才好做单元测试
获取参数#
我们在src下创建一个args的文件夹和一个mod.rs,我们直接把代码放在这个mod.rs(别忘了引入到main.rs中)就好,当然,如果你还想细分,也可以再搞其他的文件。
首先我们来设计下存放输入数据的结构和方法:
#[derive(Debug)]
pub struct InputArgs {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub deep: bool,
}
impl InputArgs {
pub async fn init(&self) -> Result<Self, Box<dyn std::error::Error>> {}
async fn get_input(&self) -> Result<(String, String, bool), Box<dyn std::error::Error>> {}
async fn absoluted_path(
&self,
input_args: (String, String, bool),
) -> Result<Self, Box<dyn std::error::Error>> {}
}init作为入口get_input作为数据获取的主要部分absoluted_path将代码转换成绝对路径
然后我们先来实现get_input的部分:
这一部分我们需要获取三个参数input_path、output_path和deep,这里我们就需要借助stdout和stdin,来等待用户输入的内容
use std::io::{self, stdin, stdout};
fn get_path_from_command_line(path: &mut String) -> Result<(), std::io::Error> {
stdout().flush()?;
stdin().read_line(path)?;
path.trim_end_matches('\n');
Ok(())
}
impl InputArgs {
// ...
async fn get_input(&self) -> Result<(String, String, bool), Box<dyn std::error::Error>> {
print!("请输入目标文件(夹)地址: ");
let mut input_file_path = String::new();
get_path_from_command_line(&mut input_file_path)?;
print!("请输入输出文件夹地址(注意是文件夹): ");
let mut output_file_path = String::new();
get_path_from_command_line(&mut output_file_path)?;
print!("要深度♂吗?即递归子文件夹(y/n): ");
let mut need_deep = String::new();
get_path_from_command_line(&mut need_deep)?;
Ok((
input_file_path,
output_file_path,
need_deep.replace("\r\n", "").to_lowercase().eq("y"),
))
}
// ...
}
- flush,刷新输出流,确保所有中间缓冲的内容能被我们拿到
- read_line,这个不用多说,将拿到的数据写入
path中 - trim_end_matches,移除
path末尾可能存在的\n
get_input主体没啥好说的,最后一个deep参数写起来有些怪,如果你有更好的方案,可以评论区说下~
在开始实现下一个方法之前,我们需要设计下测试用例,并做单元测试(后面我就不写那么详细的测试用例了,麻烦,而且占篇幅挺大的,其实就是懒,不想写):
#[tokio::test]
async fn test_get_command_line_data () {
let mut path = String::new();
let result = get_path_from_command_line(&mut path).expect("get path error");
assert_eq!(result, ());
assert_eq!(path, String::from("1\r\n"));
}对应的输出(不要在意\r\n为啥不一起处理):
running 1 test
1
test args::tests::test_get_command_line_data ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 8.73s
* Terminal will be reused by tasks, press any key to close it. 然后是get_input的:
#[tokio::test]
async fn test_get_args() {
let mut input_args = InputArgs {
input_path: PathBuf::new(),
output_path: PathBuf::new(),
deep: false
};
let target_inputs = (String::from("1\r\n"), String::from("2\r\n"), false);
// 我们需要自行控制输入内容
let res = input_args.get_input().await.unwrap();
assert_eq!(res, target_inputs);
}对应的输出:
running 1 test
请输入目标文件(夹)地址: 1
请输入输出文件夹地址(注意是文件夹): 2
要深度♂吗?即递归子文件夹(y/n): n
test args::tests::test_get_args ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 5.67s
* Terminal will be reused by tasks, press any key to close it. ok,没问题,我们来实现第二个方法absouluted_path,即将内容转换成绝对路径
fn trans_2_absolute(path: &str, is_input: bool) -> Result<PathBuf, std::io::Error> {
let is_absolute = Path::new(&path).is_absolute();
match is_absolute {
false => match canonicalize(path) {
Ok(path) => Ok(path),
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
if is_input {
println!("找不到目标文件或者文件夹:{}", &path);
return Err(e);
}
// 创建文件夹
let msg = format!("文件路径找不到,准备创建文件夹:{}", &path);
println!("{}", msg);
std::fs::create_dir_all(&path)?;
let target_path = canonicalize(&path).expect("获取绝对路径失败");
Ok(target_path)
}
_ => Err(e),
},
},
_ => {
let target_path = PathBuf::from(&path);
if !target_path.exists() {
if is_input {
println!("找不到目标文件或者文件夹:{}", &path);
return Err(io::Error::new(io::ErrorKind::NotFound, "啧啧啧"));
}
println!("路径并不存在:{}", &path);
println!("正在尝试创建文件夹");
match fs::create_dir_all(&target_path) {
Ok(_) => {
println!("创建成功:{}", &path);
}
Err(e) => {
println!("创建失败:{}", e);
return Err(e);
}
}
}
Ok(PathBuf::from(path))
}
}
}
impl InputArgs {
// ...
// ...
async fn absoluted_path(
&self,
input_args: (String, String, bool),
) -> Result<Self, Box<dyn std::error::Error>> {
let (input_file_path, output_file_path, deep) = input_args;
let input_path =
trans_2_absolute(&input_file_path.replace("\r", "").replace("\n", ""), true)?;
let output_path =
trans_2_absolute(&output_file_path.replace("\r", "").replace("\n", ""), false)?;
Ok(Self {
input_path,
output_path,
deep,
})
}
}代码稍微长了些,因为我们需要判断这个路径是否是绝对路径,如果不是,我们需要转换成绝对路径,另外我们还需要确认路径是否存在,如果不存在,那么我们还需要帮他创建这个文件路径(当然你也可以直接报错不执行后续的)
- canonicalize,这个方法可以把当前的相对路径转换成绝对路径,不过如果当前系统中不存在这个文件夹,那么会直接报错,所以我们还需要处理下错误的场景,帮忙创建文件夹,然后再重新将路径转换成绝对路径。
- 当然,要处理的目标路径这个就不需要我们帮忙创建了,如果没找到直接报错。
其它没啥好说的了,测试用例就不写了,前面说过原因了。
然后我们来实现InputArgs最后一个方法:这个方法简单,只需要组装前面两个方法即可
impl InputArgs {
pub async fn init(&self) -> Result<Self, Box<dyn std::error::Error>> {
self.absoluted_path(self.get_input().await?).await
}
// ...
// ...
}这个方法没啥好说的
那么到这,获取参数这一块的就完成了。
最后我们可以搞一个整体的测试用例:
#[tokio::test]
async fn test_init() {
// 创建临时文件夹和文件
let temp_dir = fs::create_dir_all("/tmp/test_dir").await.unwrap();
let mut temp_file = fs::OpenOptions::new()
.write(true)
.create(true)
.open("/tmp/test_dir/test_file.txt")
.await
.unwrap();
let _ = temp_file.write_all(b"test input data").await.unwrap();
// 设置输入参数
let input_path = Path::new("/tmp/test_dir/test_file.txt");
let output_path = Path::new("/tmp/test_dir/test_output.txt");
let input_args = InputArgs {
input_path: input_path.to_path_buf(),
output_path: output_path.to_path_buf(),
deep: true,
};
// 执行初始化函数
let result = input_args.init().await.unwrap();
// 验证结果是否正确
assert_eq!(result.input_path, canonicalize(input_path.to_path_buf()).unwrap());
assert_eq!(result.output_path, canonicalize(output_path.to_path_buf()).unwrap());
}这里使用了tempfile,可以创建临时文件夹
running 1 test
/tmp/test_dir/test_file.txt
/tmp/test_dir/test_output.txt
y
test args::tests::test_init ... ok
successes:
---- args::tests::test_init stdout ----
请输入目标文件(夹)地址: 请输入输出文件夹地址(注意是文件夹): 要深度♂吗?即递归子文件夹(y/n):
successes:
args::tests::test_init
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 18 filtered out; finished in 11.14s
* Terminal will be reused by tasks, press any key to close it. 读取文件内容#
我们在src下创建一个file_io文件夹,里面创建read、write和mod.rs(另外两个文件记得在mod.rs中导出)三个文件。
我们现在暂时不涉及write,我们先来实现read的逻辑。我们需要读取文件内容、文件的名字,另外如果输入的目标地址是一个文件夹,并且deep为true,那么这个时候我们就需要深度递归这个文件夹,并且记录这个过程中子文件夹的名字和路径。
这一点有些复杂,需要用到递归的逻辑,另外子文件夹的路径我们可以使用字符串拼接,然后传递给下次递归,这样可以保证路径是对的,相当于维护一个单调栈,不过是字符串的形式。
我们先设计下输出的数据的结构
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct File {
pub name: String,
pub content: String,
pub relative_name: String,
}name:文件名字content:文件内容relative_name:如果deep为true的话,那么我们还需要记录文件的相对路径,这样在生成文件的时候也能保证相对位置是正确的。
然后就是实现读取的部分,我直接贴代码了,递归相信大家都知道怎么写:
// 要递归处理下了,毕竟可能文件夹里还有文件夹
// 怎么这么讨厌呢
pub async fn get_files_contents(path: &Path, deep: bool) -> io::Result<(Vec<File>, Vec<String>)> {
let mut contents = vec![];
let mut dirs = vec![];
fn recur_dir(
prefix: String,
deep: bool,
path: &Path,
dirs: &mut Vec<String>,
contents: &mut Vec<File>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = std::fs::metadata(path)?;
if metadata.is_dir() {
if !prefix.is_empty() {
dirs.push(prefix.clone());
}
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let file_path = entry.path();
let entry = entry.file_type()?;
let mut name = file_path
.file_name()
.expect(&format!("获取文件名失败: {}", file_path.display()))
.to_string_lossy()
.to_string();
// 过滤掉测试用例文件夹
if name.contains("__test__") {
continue;
}
if entry.is_file() {
// 只针对js吧。。
if name.ends_with(".js") {
name = name.replace(".js", "");
let content = std::fs::read_to_string(file_path)?;
contents.push(File {
relative_name: format!(
"{}{}{}",
&prefix,
if prefix.is_empty() { "" } else { "/" },
name
),
name,
content,
});
}
} else if entry.is_dir() {
if !deep {
continue;
} else {
recur_dir(
format!(
"{}{}{}",
prefix,
if prefix.is_empty() { "" } else { "/" },
name
),
deep,
&file_path,
dirs,
contents,
)?;
}
}
}
Ok(())
} else if metadata.is_file() {
let mut name = path
.file_name()
.expect(&format!("获取文件名失败: {}", &path.display()))
.to_string_lossy()
.to_string();
// 只针对js吧。。
if name.ends_with(".js") {
let content = std::fs::read_to_string(path)?;
name = name.replace(".js", "");
contents.push(File {
relative_name: format!(
"{}{}{}",
&prefix,
if prefix.is_empty() { "" } else { "/" },
name
),
name,
content,
});
Ok(())
} else {
Err(format!("非js文件: {}", path.display()).into())
}
} else {
Err(format!("你这是什么路径?!: {}", path.display()).into())
}
}
recur_dir(String::new(), deep, path, &mut dirs, &mut contents);
Ok((contents, dirs))
}
- metadata,通过路径读取文件(夹)的系统信息
我们这里还单独记录了每个子文件夹的路径,这么做是为了保证输出文件在创建的过程中可以正常创建,因为最终都是绝对路径。
然后就没啥好说的了,接着也是简单的一个测试用例
#[tokio::test]
async fn test_get_files_contents() -> io::Result<()> {
let files = get_files_contents(path::Path::new("./test"), true).await?;
dbg!(files);
Ok(())
}然后我们把原本test/jump.js文件迁移到新建的test/haha/jump.js
就不写target_input了,直接肉眼看下是否正常:
running 1 test
test file_io::read::tests::test_get_files_contents ... ok
successes:
---- file_io::read::tests::test_get_files_contents stdout ----
[src\file_io\read.rs:146] files = (
[
File {
name: "jump",
content: "export function test(a, b) {\r\n return a + b;\r\n}\r\n\r\nexport const test2 = () => {\r\n return \"haha\";\r\n};\r\n",
relative_name: "haha/jump",
},
],
[
"haha",
],
)
successes:
file_io::read::tests::test_get_files_contents
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 18 filtered out; finished in 0.00s
* Terminal will be reused by tasks, press any key to close it. 肉眼peek正常
Compile#
这部分稍微复杂一些,因为swc,这个包迭代非常非常频繁,半年前的代码现在已经不能用了(官方自己的example也有的没有更新),然后文档里面不介绍内容,需要一堆子包配合,所以比较复杂。
我们在src下创建一个compile文件夹,里面创建parse.rs、transform.rs、gen.rs、mod.rs四个文件。
在开始写代码之前,我们需要引入几个crate:
swc = "0.273.27"
swc_common = "0.33.25"
swc_ecma_ast = "0.112.8"
swc_ecma_parser = "0.143.16"
swc_ecma_preset_env = "0.206.18"
swc_ecma_transforms_base = "0.137.21"
swc_ecma_utils = "0.127.20"
swc_ecma_visit = "0.98.7"要注意包的版本,因为更新很频繁!
我们先处理parse:
use swc_common::errors::{EmitterWriter, Handler};
use swc_common::input::SourceFileInput;
use swc_common::FileName;
use swc_common::{sync::Lrc, SourceMap};
use swc_common::{Mark, GLOBALS};
use swc_ecma_ast::Module;
use swc_ecma_parser::{lexer::Lexer, Parser};
use swc_ecma_parser::{Capturing, EsConfig, Syntax};
use swc_ecma_preset_env::preset_env;
use swc_ecma_transforms_base::resolver;
use swc_ecma_visit::FoldWith;
pub fn parse_js_2_ast(file_name: &str, code: &str) -> (Module, Arc<String>) {
return GLOBALS.set(&Default::default(), || {
// code = code.trim().replace("\n", "").replace("\r", "");
let source_map = Lrc::new(SourceMap::default());
let handler = Handler::with_emitter(
true,
false,
Box::new(EmitterWriter::new(
Box::new(std::io::stderr()),
Some(source_map.clone()),
false,
false,
)),
);
let fm = source_map.new_source_file(FileName::Custom(file_name.into()), code.into());
let lexer = Lexer::new(
Syntax::Es(EsConfig::default()),
Default::default(),
SourceFileInput::from(&*fm),
None,
);
let capturing = Capturing::new(lexer);
let mut parser = Parser::new_from(capturing);
for e in parser.take_errors() {
e.into_diagnostic(&handler).emit();
}
(
parser
.parse_module()
.map_err(|e| e.into_diagnostic(&handler).emit())
.expect("Failed to parse module."),
source_map
.get_source_file(&FileName::Custom(file_name.into()))
.expect(&format!("获取源代码失败{}", &file_name))
.src
.clone(),
)
});
}这里swc依赖了一个包:scoped-tls,这个包提供了(旧)标准库的scoped_thread_local!(当前标准库的文档中没看到,只有一个thread_local!, 它用来声明一个拥有其内容的线程本地存储密钥。)的功能,可以让我们自己去impl它。
GLOBALS:来看下相关的源码:
pub struct Globals {
hygiene_data: Mutex<hygiene::HygieneData>,
#[allow(unused)]
dummy_cnt: AtomicU32,
#[allow(unused)]
marks: Mutex<Vec<MarkData>>,
}
// ...
better_scoped_tls::scoped_tls!(
pub static GLOBALS: Globals
);
#[macro_export]
macro_rules! scoped_tls {
($(#[$attrs:meta])* $vis:vis static $name:ident: $ty:ty) => {
$crate::scoped_tls::scoped_thread_local!(
static INNER: $ty
);
$(#[$attrs])*
$vis static $name: $crate::ScopedKey<$ty> = $crate::ScopedKey {
inner: &INNER,
#[cfg(debug_assertions)]
module_path: module_path!(),
#[cfg(debug_assertions)]
name: stringify!($name),
};
};
}也就是给GLOBALS创建一个ScopedKey,有点类似前端localStorage的用法。现在GLOBALS有一个全局的唯一本地线程秘钥和空间。
- ScopedKey set:前面实现了
ScopedKey之后拥有的方法,它接收一个参数和一个闭包,参数会在闭包运行的过程中存在,可以调用with拿到。我们这里用不到这个数据,我们用到只是因为swc官方要求的。。。 Lrc://!Lrcis an alias of either Rc or Arc.SourceMap:span(节点最小单位),作为span的中间器(interner)。这里面存储的span都只是指针([BytePos])指向数据的位置。简单地说,我们可以通过它拿到节点的信息。
pub struct SourceMap {
pub(super) files: Lock<SourceMapFiles>,
start_pos: AtomicUsize,
file_loader: Box<dyn FileLoader + Sync + Send>,
// This is used to apply the file path remapping as specified via
// --remap-path-prefix to all SourceFiles allocated within this SourceMap.
path_mapping: FilePathMapping,
/// In case we are in a doctest, replace all file names with the PathBuf,
/// and add the given offsets to the line info
doctest_offset: Option<(FileName, isize)>,
}Handler:这个就不多说了(相关的比如EmitterWriter也不多说了,会用即可),用来处理、收集错误的。new_source_file:顾名思义,创建一个source_file,一个source_map可以存储多个source_file。Lexer:官方没有给这货和Capturing任何的注释,但是看名字就知道是一个词法分析器,看它的配置项比如target支持设置不同的ES版本,比如ES5、ES6也可以推测出。我们暂时就不去深入了解了。Parser:解析器get_source_file:顾名思义,就是用来获取文件对象信息的,src即是源码,是一个Arc<String>。注意我们不能使用我们自己的源码,它内部应该是处理过的,我们后面根据节点的start和end是根据它这个处理过的源码进行定位的,所以如果我们用自己的源码容易出现偏移。
其实我也不太了解怎么用,官方没有文档介绍怎么用,迭代又非常频繁导致别人的demo很容易过期,跟着过期攻略 + 官方example自己组装的。。。以后有机会可以来分析下源码。
然后我们随便搞个测试用例:
#[cfg(test)]
mod tests {
use super::*;
use swc_ecma_ast::*;
#[test]
fn test_get_js_ast() {
let file_name = "function.js";
let mut code = "export function add(a, b)
{ return a + b; }
export const test =
(a) => { return a }";
let module = parse_js_2_ast(file_name, &mut code);
}
#[test]
fn test_get_async_js_ast() {
let file_name = "function.js";
let mut code = "
export async function add(a, b) { return a + b; }
export const test = async (a) => { return a }";
dbg!(&code);
let module = parse_js_2_ast(file_name, &mut code);
dbg!(&module);
}
}输出结果:
---- compile::parse::tests::test_get_async_js_ast stdout ----
[src\compile\parse.rs:84] &code = "\n export async function add(a, b) { return a + b; }\n export const test = async (a) => { return a }"
[src\compile\parse.rs:86] &module = (
Module {
span: 10..113#0,
body: [
ModuleDecl(
ExportDecl(
ExportDecl {
span: 10..59#0,
decl: Fn(
FnDecl {
ident: Ident {
span: 32..35#0,
sym: "add",
optional: false,
},
declare: false,
function: Function {
params: [
Param {
span: 36..37#0,
decorators: [],
pat: Ident(
BindingIdent {
id: Ident {
span: 36..37#0,
sym: "a",
optional: false,
},
type_ann: None,
},
),
},
Param {
span: 39..40#0,
decorators: [],
pat: Ident(
BindingIdent {
id: Ident {
span: 39..40#0,
sym: "b",
optional: false,
},
type_ann: None,
},
),
},
],
decorators: [],
span: 17..59#0,
body: Some(
BlockStmt {
span: 42..59#0,
stmts: [
Return(
ReturnStmt {
span: 44..57#0,
arg: Some(
Bin(
BinExpr {
span: 51..56#0,
op: "+",
left: Ident(
Ident {
span: 51..52#0,
sym: "a",
optional: false,
},
),
right: Ident(
Ident {
span: 55..56#0,
sym: "b",
optional: false,
},
),
},
),
),
},
),
],
},
),
is_generator: false,
is_async: true,
type_params: None,
return_type: None,
},
},
),
},
),
),
ModuleDecl(
ExportDecl(
ExportDecl {
span: 68..113#0,
decl: Var(
VarDecl {
span: 75..113#0,
kind: "const",
declare: false,
decls: [
VarDeclarator {
span: 81..113#0,
name: Ident(
BindingIdent {
id: Ident {
span: 81..85#0,
sym: "test",
optional: false,
},
type_ann: None,
},
),
init: Some(
Arrow(
ArrowExpr {
span: 88..113#0,
params: [
Ident(
BindingIdent {
id: Ident {
span: 95..96#0,
sym: "a",
optional: false,
},
type_ann: None,
},
),
],
body: BlockStmt(
BlockStmt {
span: 101..113#0,
stmts: [
Return(
ReturnStmt {
span: 103..111#0,
arg: Some(
Ident(
Ident {
span: 110..111#0,
sym: "a",
optional: false,
},
),
),
},
),
],
},
),
is_async: true,
is_generator: false,
type_params: None,
return_type: None,
},
),
),
definite: false,
},
],
},
),
},
),
),
],
shebang: None,
},
"\n export async function add(a, b) { return a + b; }\n export const test = async (a) => { return a }",
)
successes:
compile::parse::tests::test_get_async_js_ast
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 18 filtered out; finished in 0.00s
* Terminal will be reused by tasks, press any key to close it. 可以看到和babel的AST类似,毕竟解析逻辑是一致的。
然后我们来到transform阶段,我们需要的是export function和export const xx = () => {},所以我们需要写一个visitor用来visit所有的节点:
#[derive(Default, Debug)]
struct ExportFnCollector<'a> {
collected_codes: Vec<Vec<String>>,
source_code: &'a str,
}然后我们可以实现官方提供的Visit in swc_ecma_visit对象,它内部支持实现对不同类型节点的visit。这一点和babel的类似:
impl<'a> Visit for ExportFnCollector<'a> {
fn visit_export_decl(&mut self, n: &ExportDecl) {
self.match_export_function_or_arrow_expr(n);
}
}- visit_export_decl:这个方法看名字就知道是用来访问
export节点的,然后我们再根据它进行分析:
impl<'a> ExportFnCollector<'a> {
pub fn match_export_function_or_arrow_expr(&mut self, n: &ExportDecl) {
match &n.decl {
Decl::Fn(fn_decl) => {
let span = &n.span;
let start = span.lo();
let end = span.hi();
self.get_source_code(start, end, &fn_decl.ident.sym);
}
Decl::Var(var_decl) => {
if !var_decl.decls.is_empty() {
let fir_node = &var_decl.decls[0];
match &fir_node.init {
Some(expr) => match **expr {
Expr::Arrow(_) => {
let span = &n.span;
let start = span.lo();
let end = span.hi();
self.get_source_code(start, end, &fir_node.name.as_ident().expect("获取箭头函数名字失败").id.sym);
}
_ => (),
},
_ => (),
}
}
}
_ => (),
}
}
pub fn get_source_code(&mut self, start: BytePos, end: BytePos, fn_name: &str) {
let code = self.source_code[start.to_usize() - 1..end.to_usize() - 1].to_string();
// 由于每次请求的token个数有上限限制,所以将整个文件内容进行切块处理
let code_len = code.len();
// 但是对于当个函数超过限制那就无法处理了
if code_len > FUNCTION_CHUNK_MAX_SIZE {
println!("函数“{}”太长,忽略", fn_name);
}
if self.collected_codes.is_empty() {
self.collected_codes.push(vec![code]);
return;
}
let mut last_v = self.collected_codes.pop().unwrap();
let last_size = last_v.iter().fold(0, |pre, cur| pre + cur.len());
if last_size + code_len > FUNCTION_CHUNK_MAX_SIZE {
self.collected_codes.push(vec![code]);
} else {
last_v.push(code);
self.collected_codes.push(last_v);
}
}
}
impl<'a> Visit for ExportFnCollector<'a> {
fn visit_export_decl(&mut self, n: &ExportDecl) {
self.match_export_function_or_arrow_expr(n);
}
}这里的逻辑就多说了,相信大家都看得懂。
visitor写完了,我们准备来实现接入visit的过程:
use swc_common::comments::SingleThreadedComments;
use swc_common::source_map::Pos;
use swc_common::{BytePos, Mark, GLOBALS};
use swc_ecma_ast::{ExportDecl, Module};
use swc_ecma_preset_env::preset_env;
use swc_ecma_transforms_base::helpers::HELPERS;
use swc_ecma_transforms_base::resolver;
use swc_ecma_visit::{FoldWith, Visit, VisitWith};
pub fn visit_ast(mut module: Module, source_code: &str) -> (Module, Vec<Vec<String>>) {
return GLOBALS.set(&Default::default(), || {
return HELPERS.set(&Default::default(), || {
let mut resolver = resolver(Default::default(), Default::default(), Default::default());
let resolved_module = module.fold_with(&mut resolver);
let mut my_visitor = ExportFnCollector {
collected_codes: vec![],
source_code,
};
resolved_module.visit_with(&mut my_visitor);
let mut transformer = preset_env(
Mark::fresh(Mark::root()),
Some(SingleThreadedComments::default()),
Default::default(),
Default::default(),
&mut Default::default(),
);
let transformed_module = resolved_module.fold_with(&mut transformer);
(transformed_module, my_visitor.collected_codes)
});
});
}HELPERS:和Globals一样,我们需要这个HELPERS包裹,这样才能处理js中比如async这种语法。resolver:看下官方的描述:
何时运行#
解析器需要“干净”的 ast。您可以通过解析或通过 删除 AST 节点中的所有语法上下文。
它有什么作用#
首先,所有作用域(fn、block)都有自己的 SyntaxContext。 Resolver 访问模块中的所有标识符,并在作用域中查找绑定标识。这些标识符现在具有范围 (fn, block) 的 SyntaxContext。执行此操作时,解析程序会尝试将普通标识符(无卫生(
hygiene)信息)解析为对范围标识符的引用。如果解析器找到合适的变量,则标识符引用将与变量具有相同的上下文。
简单地说我们可以通过它去修改节点,类似babel里的transform阶段。
preset_env:这个我也不清楚干啥的,后面这段transformer的应该是不需要的,我是参考的过期攻略。
不管怎么样,代码是写完了,照例搞一个测试用例:
use super::super::parse::parse_js_2_ast;
use super::*;
#[test]
fn test_transform() {
let file_name = "function.js";
let mut code = "export function add(a, b)
{ return a + b; }
export const test =
(a) => { return a }";
let mut parse_res = parse_js_2_ast(file_name, &mut code);
let module = visit_ast(parse_res.0, &parse_res.1);
dbg!(module);
}对应输出结果:
running 1 test
test compile::transform::tests::test_transform ... ok
successes:
---- compile::transform::tests::test_transform stdout ----
[src\compile\transform.rs:127] module = (
Module {
span: 1..113#0,
body: [
ModuleDecl(
ExportDecl(
ExportDecl {
span: 1..56#0,
decl: Fn(
FnDecl {
ident: Ident {
span: 17..20#2,
sym: "add",
optional: false,
},
declare: false,
function: Function {
params: [
Param {
span: 21..22#0,
decorators: [],
pat: Ident(
BindingIdent {
id: Ident {
span: 21..22#3,
sym: "a",
optional: false,
},
type_ann: None,
},
),
},
Param {
span: 24..25#0,
decorators: [],
pat: Ident(
BindingIdent {
id: Ident {
span: 24..25#3,
sym: "b",
optional: false,
},
type_ann: None,
},
),
},
],
decorators: [],
span: 8..56#3,
body: Some(
BlockStmt {
span: 39..56#3,
stmts: [
Return(
ReturnStmt {
span: 41..54#0,
arg: Some(
Bin(
BinExpr {
span: 48..53#0,
op: "+",
left: Ident(
Ident {
span: 48..49#3,
sym: "a",
optional: false,
},
),
right: Ident(
Ident {
span: 52..53#3,
sym: "b",
optional: false,
},
),
},
),
),
},
),
],
},
),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
},
},
),
},
),
),
ModuleDecl(
ExportDecl(
ExportDecl {
span: 65..113#0,
decl: Var(
VarDecl {
span: 72..113#0,
kind: "var",
declare: false,
decls: [
VarDeclarator {
span: 78..113#0,
name: Ident(
BindingIdent {
id: Ident {
span: 78..82#2,
sym: "test",
optional: false,
},
type_ann: None,
},
),
init: Some(
Fn(
FnExpr {
ident: None,
function: Function {
params: [
Param {
span: 0..0#0,
decorators: [],
pat: Ident(
BindingIdent {
id: Ident {
span: 95..96#4,
sym: "a",
optional: false,
},
type_ann: None,
},
),
},
],
decorators: [],
span: 94..113#0,
body: Some(
BlockStmt {
span: 101..113#4,
stmts: [
Return(
ReturnStmt {
span: 103..111#0,
arg: Some(
Ident(
Ident {
span: 110..111#4,
sym: "a",
optional: false,
},
),
),
},
),
],
},
),
is_generator: false,
is_async: false,
type_params: None,
return_type: None,
},
},
),
),
definite: false,
},
],
},
),
},
),
),
],
shebang: None,
},
[
[
"export function add(a, b)\n { return a + b; }",
"export const test = \n (a) => { return a }",
],
],
)
successes:
compile::transform::tests::test_transform
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 18 filtered out; finished in 0.01s
* Terminal will be reused by tasks, press any key to close it. 可以看到正确的拿到了我们的函数。
最后还有个gen.rs,这个实际上不是ast阶段的,只是为了好看我给放到这里了。。
哦,忘了说了,因为官方api每次请求都是有token长度限制的,所以我对每个文件还进行了二次拆分,具体限制可以自行看官方文档:
通义千问如何计量计费和查看账单_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
你可以通过这个接口去确认是否超了:
https://dashscope.aliyuncs.com/api/v1/tokenizerToken计算API详情_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
请求通义千问api#
接下来就是请求阶段了,这块是我耗时最久的,不过原因则是因为通义千问官方的api的QPM(每分钟最多请求)< 200(实际更少),所以我这里要调整每秒的请求频率,很麻烦。
我们同样在src下创建一个ask文件夹,里面放client.rs和mod.rs。
代码没啥好说的,我直接贴出来了:
use std::collections::HashMap;
use crate::{
compile::{
gen::{self, CodeBlock},
Transformed,
},
constants::{get_q_wen_key, Q_WEN_TEXT_GEN_API},
};
use reqwest::Client;
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
pub fn create_client() -> Client {
reqwest::Client::new()
}
// #[derive(Debug, Default)]
// struct SendData {
// state: bool,
// data: CodeBlock
// }
// 这里只能通过通信的方案去获取response的数据
// 动态错误不是线程安全的,所以这里搞不了动态错误,写起来有点恶心
pub async fn batch_ask(
files: Vec<Transformed>,
) -> Result<Vec<CodeBlock>, Box<dyn std::error::Error>> {
let files_len = files.len();
let (tx, mut rx) = mpsc::channel(100);
let mut tasks = Vec::with_capacity(files_len);
// 因为QPS只有20,所以需要限制下
// 貌似还没有20.。。。日了狗
// 看来得想个方案
// 原来是1分钟200次。。。
// 也不是200啊,骗子
// 测试下来59就炸了
// 算了,改成每秒的,200次 / 60 = 3
// 3秒也不行,1秒2个吧
let max_requests_per_second = 3;
let mut num_requests = 0;
for file in files {
for funcs in file.filter_code {
if num_requests >= max_requests_per_second {
println!("----------超速被抓了,等待一秒-------------");
sleep(Duration::from_secs_f32(1.5)).await;
num_requests = 0;
}
num_requests += 1;
let new_tx = tx.clone();
let file_name = file.file.relative_name.clone();
let task = tokio::spawn(async move {
let res = get_qwen_code_block(file_name, funcs).await;
match res {
Ok(r) => match new_tx.send(r).await {
Err(e) => {
let msg = format!("线程发送失败:{}", e);
println!("{}", msg);
drop(new_tx);
}
_ => {
drop(new_tx);
},
},
Err(_) => {
println!("请求失败");
drop(new_tx);
}
};
});
tasks.push(task);
}
}
drop(tx);
let mut code_block_map = HashMap::new();
loop {
match rx.recv().await {
Some(message) => {
code_block_map
.entry(message.file_name.clone())
.or_insert(vec![])
.push(message);
}
None => {
// 数据结构设计的不太行,待优化TODO
println!("----------请求结束--罚款200,扣两分---------");
return Ok(code_block_map
.values()
.map(|v| {
let mut code_block = CodeBlock::default();
code_block.file_name = v[0].file_name.clone();
code_block.language = v[0].language.clone();
v.iter().fold(code_block, |mut code_block, curr| {
code_block.content += &format!("\n{}", &curr.content);
code_block
})
})
.collect());
}
}
}
}
pub async fn get_qwen_code_block(file_name: String, vs: Vec<String>) -> Result<CodeBlock, ()> {
let key = get_q_wen_key().expect("获取key失败");
let cli = create_client();
let payload = gen_pay_load(vs.join("\n"));
let res = cli
.post(Q_WEN_TEXT_GEN_API)
.header("Authorization", format!("Bearer {}", key))
.json(&payload)
.send()
.await
.expect(&format!("请求失败:{}", &payload));
let res: Value = res
.json()
.await
.expect(&format!("数据json化失败:{:#?}", &payload));
let res = res["output"]["choices"][0]["message"]["content"]
.as_str()
.expect(&format!("获取字段失败:{:#?}", &res));
match gen::CodeBlock::find_code_block(file_name, res) {
Some(code_block) => Ok(code_block),
_ => {
let msg = format!("获取code block失败:{:#?}", &res);
println!("{}", msg);
Err(())
}
}
}
// 直接生成类型的精度太低了,还是得考虑大佬的生成json然后自己组装的方案
fn gen_pay_load(source_code: String) -> Value {
let promot = format!(
"作为一名前端工程师的 AI 辅助,我的任务是将提取 JS 源代码中的函数类型描述,生成符合typescript的d.ts类型文件格式的类型
示例1:
Q: 源代码如下:
```javascript
{} A: 我返回的结果为:
```typescript
{}
```
源代码如下: {}
",
"
export function test (a) { return a + 1 }
export const test2 = () => { return \"haha\" }
export async function test3 (b: string) { return b }
export const test4 = async (c: number, d: number) => { return c + d }
",
"
export declare function test (a: number): number;
export declare const test2: () => string;
export declare function test3 (b: string | undefined): string | undefined;
export declare const test4: (c: number, d: number) => number;
",
source_code,
);
json!({
"input": {
"prompt": promot
},
"parameters": {
"temperature": 0.8,
"result_format": "message",
},
"model": "qwen-plus", // 还是选这个吧,plus的QPS太低了
})}
哦,我这里还将代码`gen`了下:
```rust
use crate::constants::RES_TEXT_RANGE_REG;
use regex::Regex;
#[derive(Debug, Default)]
pub struct CodeBlock {
pub file_name: String,
pub language: String,
pub content: String,
}
impl CodeBlock {
pub fn find_code_block(file_name: String, input: &str) -> Option<Self> {
let re = Regex::new(RES_TEXT_RANGE_REG).unwrap();
let captures = re.captures(input)?;
Some(Self {
file_name,
language: captures["language"].to_owned(),
content: captures["content"].to_owned(),
})
}
}
因为大模型有时候话很多,所以我只能是去捕捉````typescript`里的代码,注意这里用到了regex的包。
我这里没写测试用例,主要是忘了。
不过没关系,到这里我们就基本差最后一步了,可以直接在main中组装。
main中组装阶段性结果#
mod args;
mod ask;
mod compile;
mod constants;
mod file_io;
use args::InputArgs;
use ask::client;
use file_io::{read::get_files_contents, write::generate_dts_files};
use std::env;
use crate::compile::{parse::parse_js_2_ast, transform::visit_ast, Transformed};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// get args
let args = InputArgs {
input_path: env::current_dir()?,
output_path: env::current_dir()?,
deep: false
}
.init()
.await?;
// read files content
let (files, dirs) = get_files_contents(&args.input_path, args.deep).await?;
println!("-------------一共需要处理 {} 个文件(可能包含空文件)--------------", files.len());
// compile
let transformed_files = files
.into_iter()
.map(|mut file| {
let parse_res = parse_js_2_ast(&file.name, &mut file.content);
let res = visit_ast(parse_res.0, &parse_res.1);
Transformed {
file,
filter_code: res.1,
}
})
.collect::<Vec<Transformed>>();
// request
println!("---------------------正在处理中,请耐心等待---------------------");
let code_blocks = client::batch_ask(transformed_files).await?;
println!("---------------------处理完毕,共{}个block,准备生成文件---------------------", code_blocks.len());
// write file
// generate_dts_files(&args.output_path, &code_blocks, dirs).await?;
// println!("---------------------生成文件完毕,感谢使用---------------------");
Ok(())
}我这里就不跑了,你们可以自行运行下
批量性写入#
没啥好说的,直接上代码:
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use tokio::{fs::File as AsyncFile, io::AsyncWriteExt};
use crate::compile::gen::CodeBlock;
pub async fn generate_dts_files(
output_path: &Path,
file_name_contents: &Vec<CodeBlock>,
dirs: Vec<String>,
) -> io::Result<()> {
let path: PathBuf = output_path.into();
create_sub_dir(&path, dirs)?;
let metadata = tokio::fs::metadata(output_path).await?;
match (metadata.is_dir(), metadata.is_file()) {
(true, false) => {
for block in file_name_contents {
let dts_file_name = format!("{}.d.ts", block.file_name);
let dts_file_path = path.join(PathBuf::from(dts_file_name));
println!("{}", &dts_file_path.display());
create_or_overwrite_file(&dts_file_path, &block.content).await?;
}
}
(false, true) => {
let block = file_name_contents
.first()
.expect("File name contents list must not be empty");
let dts_file_name = format!("{}.d.ts", &block.file_name);
let dts_file_path = output_path.with_file_name(dts_file_name);
create_or_overwrite_file(&dts_file_path, &block.content).await?;
}
_ => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Unsupported entry type at path: {}", output_path.display()),
))
}
};
Ok(())
}
async fn create_or_overwrite_file(path: &Path, content: &str) -> io::Result<()> {
let mut file = AsyncFile::create(path).await?;
file.write_all(content.as_bytes()).await?;
Ok(())
}
fn create_sub_dir(output_path: &Path, dirs: Vec<String>) -> io::Result<()> {
for dir in dirs {
fs::create_dir_all(output_path.join(PathBuf::from(dir)))?;
}
Ok(())
}还记得前面read时收集的子文件夹相对路径集合么?我们在这里先根据这个集合创建相对文件夹,然后再创建文件,这样就不会报错了。
这里简单搞一个测试用例:
#[tokio::test]
async fn test_generate_dts_files() {
// Create a temporary directory for testing
let temp_dir = tempfile::Builder::new()
.prefix("test_output")
.tempdir()
.unwrap();
// Define file name contents for testing
let file_name_contents = vec![
CodeBlock {
file_name: "file1".to_string(),
language: "js".to_string(),
content: "content1".to_string(),
},
CodeBlock {
file_name: "file2".to_string(),
language: "js".to_string(),
content: "content2".to_string(),
},
];
// Call the function under test
generate_dts_files(temp_dir.path(), &file_name_contents, vec![])
.await
.unwrap();
// Check if the expected files are created with the correct contents
let file1_path = temp_dir.path().join("file1.d.ts");
let file2_path = temp_dir.path().join("file2.d.ts");
assert!(fs::metadata(&file1_path).is_ok());
assert!(fs::metadata(&file2_path).is_ok());
let file1_content = fs::read_to_string(&file1_path).unwrap();
let file2_content = fs::read_to_string(&file2_path).unwrap();
assert_eq!("content1", file1_content);
assert_eq!("content2", file2_content);
// Clean up the temporary directory
temp_dir.close().unwrap();
}对应输出:
running 1 test
test file_io::write::tests::test_generate_dts_files ... ok
successes:
---- file_io::write::tests::test_generate_dts_files stdout ----
C:\Users\LIUDAN~1\AppData\Local\Temp\test_outputwgRjPZ\file1.d.ts
C:\Users\LIUDAN~1\AppData\Local\Temp\test_outputwgRjPZ\file2.d.ts
successes:
file_io::write::tests::test_generate_dts_files
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 18 filtered out; finished in 0.01s
* Terminal will be reused by tasks, press any key to close it. ok
最终组装#
mod args;
mod ask;
mod compile;
mod constants;
mod file_io;
use args::InputArgs;
use ask::client;
use file_io::{read::get_files_contents, write::generate_dts_files};
use std::env;
use crate::compile::{parse::parse_js_2_ast, transform::visit_ast, Transformed};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// get args
let args = InputArgs {
input_path: env::current_dir()?,
output_path: env::current_dir()?,
deep: false
}
.init()
.await?;
// read files content
let (files, dirs) = get_files_contents(&args.input_path, args.deep).await?;
println!("-------------一共需要处理 {} 个文件(可能包含空文件)--------------", files.len());
// compile
let transformed_files = files
.into_iter()
.map(|mut file| {
let parse_res = parse_js_2_ast(&file.name, &mut file.content);
let res = visit_ast(parse_res.0, &parse_res.1);
Transformed {
file,
filter_code: res.1,
}
})
.collect::<Vec<Transformed>>();
// request
println!("---------------------正在处理中,请耐心等待---------------------");
let code_blocks = client::batch_ask(transformed_files).await?;
println!("---------------------处理完毕,共{}个block,准备生成文件---------------------", code_blocks.len());
// write file
generate_dts_files(&args.output_path, &code_blocks, dirs).await?;
println!("---------------------生成文件完毕,感谢使用---------------------");
Ok(())
}然后我们直接运行,不搞测试用例了。(记得先设置你的API-KEY)
cargo run对应输出:
请输入目标文件(夹)地址: C:\xxx\xx\xx\utils
请输入输出文件夹地址(注意是文件夹): C:\test_rust_auto_gen_d_ts
要深度♂吗?即递归子文件夹(y/n): y
-------------一共需要处理 95 个文件(可能包含空文件)--------------
函数“checkModuleData”太长,忽略
函数“getSaveParam”太长,忽略
---------------------正在处理中,请耐心等待---------------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------超速被抓了,等待一秒-------------
----------请求结束--罚款200,扣两分---------
---------------------处理完毕,共60个block,准备生成文件---------------------
C:\test_rust_auto_gen_d_ts\tools/pure/str.d.ts
C:\test_rust_auto_gen_d_ts\editor/features/image.d.ts
C:\test_rust_auto_gen_d_ts\def/BusDef.d.ts
C:\test_rust_auto_gen_d_ts\bgUtil.d.ts
C:\test_rust_auto_gen_d_ts\editor/common.d.ts
C:\test_rust_auto_gen_d_ts\editor/page.d.ts
C:\test_rust_auto_gen_d_ts\editor/business.d.ts
C:\test_rust_auto_gen_d_ts\editor/getModulesData.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/parse.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/typer.d.ts
C:\test_rust_auto_gen_d_ts\tools/lock.d.ts
C:\test_rust_auto_gen_d_ts\editPanel.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/func.d.ts
C:\test_rust_auto_gen_d_ts\editor/productGroup.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/browser/file.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/cache.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/DialogCreator.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/date.d.ts
C:\test_rust_auto_gen_d_ts\editor/modules/initial.d.ts
C:\test_rust_auto_gen_d_ts\editor/init.d.ts
C:\test_rust_auto_gen_d_ts\color.d.ts
C:\test_rust_auto_gen_d_ts\editor/manage.d.ts
C:\test_rust_auto_gen_d_ts\editor/contextMenu/index.d.ts
C:\test_rust_auto_gen_d_ts\editor/modules/productCategory.d.ts
C:\test_rust_auto_gen_d_ts\JqConvertFunc.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/confirm.d.ts
C:\test_rust_auto_gen_d_ts\fai/tool.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/options.d.ts
C:\test_rust_auto_gen_d_ts\editor/jump.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/async.d.ts
C:\test_rust_auto_gen_d_ts\fai/bussiness.d.ts
C:\test_rust_auto_gen_d_ts\editor/save.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/page.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/url.d.ts
C:\test_rust_auto_gen_d_ts\editor/features/coupon.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/dom.d.ts
C:\test_rust_auto_gen_d_ts\def/FdpNameMapDef.d.ts
C:\test_rust_auto_gen_d_ts\data.d.ts
C:\test_rust_auto_gen_d_ts\editor/designer.d.ts
C:\test_rust_auto_gen_d_ts\tools/dialog/adapter.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/arr.d.ts
C:\test_rust_auto_gen_d_ts\initData.d.ts
C:\test_rust_auto_gen_d_ts\editor/modules/ProductList.d.ts
C:\test_rust_auto_gen_d_ts\url.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/browser/dom.d.ts
C:\test_rust_auto_gen_d_ts\tools/modal.d.ts
C:\test_rust_auto_gen_d_ts\editor/contextMenu/util.d.ts
C:\test_rust_auto_gen_d_ts\editor/contextMenu/log.d.ts
C:\test_rust_auto_gen_d_ts\editor/modulesHandler.d.ts
C:\test_rust_auto_gen_d_ts\colorpicker/colorMatch.d.ts
C:\test_rust_auto_gen_d_ts\editor/log.d.ts
C:\test_rust_auto_gen_d_ts\editor/tool.d.ts
C:\test_rust_auto_gen_d_ts\bizData.d.ts
C:\test_rust_auto_gen_d_ts\editor/product.d.ts
C:\test_rust_auto_gen_d_ts\editor/material/index.d.ts
C:\test_rust_auto_gen_d_ts\def/data/index.d.ts
C:\test_rust_auto_gen_d_ts\dom.d.ts
C:\test_rust_auto_gen_d_ts\fai/popupWindowVersionTwo.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/num.d.ts
C:\test_rust_auto_gen_d_ts\tools/pure/obj.d.ts
---------------------生成文件完毕,感谢使用---------------------这就完成了,随便找个文件看下:color.d.ts
export declare const genLinearGradient: (options?: {
rotate?: number;
startColor?: string;
startOpacity?: number;
startPercentage?: number;
endColor?: string;
endOpacity?: number;
endPercentage?: number;
}) => string;
export declare const hexToRgb: (hex: string, alp?: number) => string;
export declare const hexToRgbObj: (hex: string, alpha?: number) => {
red: number;
green: number;
blue: number;
alpha: number;
};
export declare function HextoRgba(hex: string, alp?: number): string;
export declare const isColorString: (str: string) => boolean;
export declare const getRgbaWithOpacity: (color: string, opacity: number) => string;
export declare const getThemeMatchingColor: (themeColor: string) => {
secondaryColor: string;
mainTextColor: string;
secondaryTextColor: string;
};表现比较正常(当然,有时候也很抽风)
最后我们运行cargo build --release生成最终产物exe文件,这样就拿到立即执行文件了。
总结#
中间漏了一些代码,具体可以去github上看下,这里就不多说了。
这个工具比较简单,中间的一些过程可以抽离变成一个通用的结构,这样以后写一些工具就可以灵活变通了。
