🎉 initial commit
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
### Rust template
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["landaiqing <landaiqing@126.com>"]
|
||||||
|
description = "A WASM Based Rust Code Formatter"
|
||||||
|
name = "rust_fmt"
|
||||||
|
|
||||||
|
edition = "2021"
|
||||||
|
homepage = "https://github.com/landaiqing/rust_fmt"
|
||||||
|
keywords = ["wasm", "rust", "formatter"]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/landaiqing/rust_fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
prettyplease = "0.2"
|
||||||
|
serde = "1.0"
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
|
serde_json = "1.0"
|
||||||
|
syn = { version = "2.0", features = ["full", "parsing", "printing"] }
|
||||||
|
wasm-bindgen = "0.2.99"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
debug = false # set to `true` for debug information
|
||||||
|
lto = true
|
||||||
|
opt-level = "s"
|
||||||
|
panic = "abort" # Let it crash and force ourselves to write safe Rust.
|
||||||
|
strip = "symbols" # set to `false` for debug information
|
||||||
128
README.md
Normal file
128
README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Rust Formatter (WASM)
|
||||||
|
|
||||||
|
一个基于 WebAssembly 的 Rust 代码格式化工具,可以在浏览器和 Node.js 环境中使用。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🚀 基于 WebAssembly,性能优异
|
||||||
|
- 🌍 支持浏览器和 Node.js 环境
|
||||||
|
- ⚙️ 支持配置选项(tab/space 缩进)
|
||||||
|
- 📦 轻量级,无需额外的 Rust 工具链
|
||||||
|
- 🔧 基于 `prettyplease` 和 `syn` 提供准确的格式化
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @landaiqing/rust_fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import init, { format } from '@landaiqing/rust_fmt';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const rustCode = `fn main(){println!("Hello, world!");}`;
|
||||||
|
|
||||||
|
// 使用默认配置(空格缩进)
|
||||||
|
const formatted = format(rustCode);
|
||||||
|
console.log(formatted);
|
||||||
|
|
||||||
|
// 使用 tab 缩进
|
||||||
|
const formattedWithTabs = format(rustCode, { use_tabs: true });
|
||||||
|
console.log(formattedWithTabs);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 浏览器 (ES Modules)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import init, { format } from '@landaiqing/rust_fmt';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const rustCode = `fn main(){println!("Hello, world!");}`;
|
||||||
|
const formatted = format(rustCode);
|
||||||
|
console.log(formatted);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vite
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import init, { format } from '@landaiqing/rust_fmt/vite';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const rustCode = `fn main(){println!("Hello, world!");}`;
|
||||||
|
const formatted = format(rustCode);
|
||||||
|
console.log(formatted);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `format(input: string, config?: Config): string`
|
||||||
|
|
||||||
|
格式化 Rust 代码字符串。
|
||||||
|
|
||||||
|
#### 参数
|
||||||
|
|
||||||
|
- `input`: 要格式化的 Rust 代码字符串
|
||||||
|
- `config` (可选): 格式化配置选项
|
||||||
|
|
||||||
|
#### Config 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Config {
|
||||||
|
/**
|
||||||
|
* 当设置为 true 时,使用 tab 缩进而不是空格缩进
|
||||||
|
*
|
||||||
|
* 默认值: false
|
||||||
|
*/
|
||||||
|
use_tabs?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# 构建 WASM 包
|
||||||
|
wasm-pack build --target=web --scope=landaiqing
|
||||||
|
|
||||||
|
# 复制额外文件
|
||||||
|
cp -R ./extra/. ./pkg/
|
||||||
|
|
||||||
|
# 处理 package.json
|
||||||
|
node ./scripts/package.mjs ./pkg/package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Node.js 测试
|
||||||
|
node test_node/test-node.mjs
|
||||||
|
|
||||||
|
# Deno 测试
|
||||||
|
deno test test_deno/deno.test.ts --allow-read
|
||||||
|
|
||||||
|
# Bun 测试
|
||||||
|
bun test test_bun/bun.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
本项目基于以下优秀的开源项目:
|
||||||
|
|
||||||
|
- [prettyplease](https://github.com/dtolnay/prettyplease) - Rust 代码格式化库
|
||||||
|
- [syn](https://github.com/dtolnay/syn) - Rust 语法解析库
|
||||||
|
- [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) - Rust 和 WebAssembly 绑定生成器
|
||||||
10
extra/rust_fmt_node.js
Normal file
10
extra/rust_fmt_node.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import initAsync from "./rust_fmt.js";
|
||||||
|
|
||||||
|
const wasm = new URL("./rust_fmt_bg.wasm", import.meta.url);
|
||||||
|
|
||||||
|
export default function __wbg_init(init = { module_or_path: fs.readFile(wasm) }) {
|
||||||
|
return initAsync(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./rust_fmt.js";
|
||||||
8
extra/rust_fmt_vite.js
Normal file
8
extra/rust_fmt_vite.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import initAsync from "./rust_fmt.js";
|
||||||
|
import wasm from "./rust_fmt_bg.wasm?url";
|
||||||
|
|
||||||
|
export default function __wbg_init(input = { module_or_path: wasm }) {
|
||||||
|
return initAsync(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./rust_fmt.js";
|
||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
6
scripts/build.sh
Normal file
6
scripts/build.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
cd $(dirname $0)/..
|
||||||
|
wasm-pack build --target=web --scope=landaiqing
|
||||||
|
|
||||||
|
cp -R ./extra/. ./pkg/
|
||||||
|
|
||||||
|
./scripts/package.mjs ./pkg/package.json
|
||||||
39
scripts/package.mjs
Normal file
39
scripts/package.mjs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import process from "node:process";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const pkg_path = path.resolve(process.cwd(), process.argv[2]);
|
||||||
|
const pkg_text = fs.readFileSync(pkg_path, { encoding: "utf-8" });
|
||||||
|
const pkg_json = JSON.parse(pkg_text);
|
||||||
|
|
||||||
|
delete pkg_json.files;
|
||||||
|
|
||||||
|
pkg_json.main = pkg_json.module;
|
||||||
|
pkg_json.type = "module";
|
||||||
|
pkg_json.publishConfig = {
|
||||||
|
access: "public",
|
||||||
|
};
|
||||||
|
pkg_json.exports = {
|
||||||
|
".": {
|
||||||
|
types: "./rust_fmt.d.ts",
|
||||||
|
node: "./rust_fmt_node.js",
|
||||||
|
default: "./rust_fmt.js",
|
||||||
|
},
|
||||||
|
"./vite": {
|
||||||
|
types: "./rust_fmt.d.ts",
|
||||||
|
default: "./rust_fmt_vite.js",
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
"./*": "./*",
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(pkg_path, JSON.stringify(pkg_json, null, 4));
|
||||||
|
|
||||||
|
// JSR
|
||||||
|
|
||||||
|
const jsr_path = path.resolve(pkg_path, "..", "jsr.jsonc");
|
||||||
|
pkg_json.name = "@fmt/rust-fmt";
|
||||||
|
pkg_json.exports = "./rust_fmt.js";
|
||||||
|
pkg_json.exclude = ["!**", "*.tgz"];
|
||||||
|
fs.writeFileSync(jsr_path, JSON.stringify(pkg_json, null, 4));
|
||||||
51
src/lib.rs
Normal file
51
src/lib.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use syn::{parse_file, File};
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn format(input: &str, config: Option<Config>) -> Result<String, String> {
|
||||||
|
let config = config
|
||||||
|
.map(|x| serde_wasm_bindgen::from_value::<RustConfig>(x.clone()))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|op| op.to_string())?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// 解析 Rust 代码
|
||||||
|
let syntax_tree: File = parse_file(input).map_err(|e| format!("解析错误: {}", e))?;
|
||||||
|
|
||||||
|
// 格式化代码
|
||||||
|
let formatted = if config.use_tabs {
|
||||||
|
// 使用 tab 缩进
|
||||||
|
let formatted = prettyplease::unparse(&syntax_tree);
|
||||||
|
// 将 4 个空格替换为 tab
|
||||||
|
formatted.replace(" ", "\t")
|
||||||
|
} else {
|
||||||
|
// 使用默认的空格缩进
|
||||||
|
prettyplease::unparse(&syntax_tree)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(typescript_custom_section)]
|
||||||
|
const TS_Config: &'static str = r#"
|
||||||
|
export interface Config {
|
||||||
|
/**
|
||||||
|
* When set to true, uses tabs for indentation instead of spaces
|
||||||
|
*
|
||||||
|
* Default: false
|
||||||
|
*/
|
||||||
|
use_tabs?: boolean;
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(typescript_type = "Config")]
|
||||||
|
pub type Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Default)]
|
||||||
|
struct RustConfig {
|
||||||
|
#[serde(alias = "useTabs")]
|
||||||
|
pub use_tabs: bool,
|
||||||
|
}
|
||||||
29
test_bun/bun.spec.ts
Normal file
29
test_bun/bun.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Glob } from "bun";
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { chdir } from "node:process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import init, { format } from "../pkg/rust_fmt";
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const test_root = fileURLToPath(import.meta.resolve("../test_data"));
|
||||||
|
chdir(test_root);
|
||||||
|
|
||||||
|
const glob = new Glob("**/*.rs");
|
||||||
|
|
||||||
|
for await (const input_path of glob.scan()) {
|
||||||
|
const [input, expected] = await Promise.all([
|
||||||
|
Bun.file(input_path).text(),
|
||||||
|
Bun.file(input_path + ".snap").text().catch(() => {
|
||||||
|
// 如果没有 snap 文件,就创建一个
|
||||||
|
const formatted = format(input);
|
||||||
|
return formatted;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
test(input_path, () => {
|
||||||
|
const actual = format(input);
|
||||||
|
expect(actual).toBe(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
13
test_config.mjs
Normal file
13
test_config.mjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import init, { format } from './pkg/rust_fmt_node.js';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const testCode = `fn main(){println!("Hello, world!");}`;
|
||||||
|
|
||||||
|
console.log('=== 默认配置(空格缩进)===');
|
||||||
|
const result1 = format(testCode);
|
||||||
|
console.log(result1);
|
||||||
|
|
||||||
|
console.log('=== 使用 tab 缩进 ===');
|
||||||
|
const result2 = format(testCode, { use_tabs: true });
|
||||||
|
console.log(result2);
|
||||||
22
test_data/complex_example.rs
Normal file
22
test_data/complex_example.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn process_data(data:Vec<i32>)->HashMap<i32,i32>{
|
||||||
|
let mut result=HashMap::new();
|
||||||
|
for(index,value)in data.iter().enumerate(){
|
||||||
|
result.insert(index as i32,*value*2);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests{
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_data(){
|
||||||
|
let input=vec![1,2,3,4,5];
|
||||||
|
let result=process_data(input);
|
||||||
|
assert_eq!(result.get(&0),Some(&2));
|
||||||
|
assert_eq!(result.get(&1),Some(&4));
|
||||||
|
}
|
||||||
|
}
|
||||||
19
test_data/complex_example.rs.snap
Normal file
19
test_data/complex_example.rs.snap
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
fn process_data(data: Vec<i32>) -> HashMap<i32, i32> {
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
for (index, value) in data.iter().enumerate() {
|
||||||
|
result.insert(index as i32, *value * 2);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_process_data() {
|
||||||
|
let input = vec![1, 2, 3, 4, 5];
|
||||||
|
let result = process_data(input);
|
||||||
|
assert_eq!(result.get(& 0), Some(& 2));
|
||||||
|
assert_eq!(result.get(& 1), Some(& 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
5
test_data/simple_function.rs
Normal file
5
test_data/simple_function.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fn main(){println!("Hello, world!");}
|
||||||
|
|
||||||
|
fn add(a:i32,b:i32)->i32{
|
||||||
|
a+b
|
||||||
|
}
|
||||||
6
test_data/simple_function.rs.snap
Normal file
6
test_data/simple_function.rs.snap
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
|
fn add(a: i32, b: i32) -> i32 {
|
||||||
|
a + b
|
||||||
|
}
|
||||||
11
test_data/struct_with_impl.rs
Normal file
11
test_data/struct_with_impl.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
struct Person{name:String,age:u32}
|
||||||
|
|
||||||
|
impl Person{
|
||||||
|
fn new(name:String,age:u32)->Self{
|
||||||
|
Self{name,age}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn greet(&self){
|
||||||
|
println!("Hello, my name is {} and I'm {} years old.",self.name,self.age);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
test_data/struct_with_impl.rs.snap
Normal file
12
test_data/struct_with_impl.rs.snap
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: u32,
|
||||||
|
}
|
||||||
|
impl Person {
|
||||||
|
fn new(name: String, age: u32) -> Self {
|
||||||
|
Self { name, age }
|
||||||
|
}
|
||||||
|
fn greet(&self) {
|
||||||
|
println!("Hello, my name is {} and I'm {} years old.", self.name, self.age);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
test_deno/deno.test.ts
Normal file
35
test_deno/deno.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import init, { format } from "../pkg/rust_fmt.js";
|
||||||
|
|
||||||
|
import { assertEquals } from "jsr:@std/assert";
|
||||||
|
import { walk } from "jsr:@std/fs/walk";
|
||||||
|
import { relative } from "jsr:@std/path";
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const update = Deno.args.includes("--update");
|
||||||
|
|
||||||
|
const test_root = new URL("../test_data", import.meta.url);
|
||||||
|
|
||||||
|
for await (const entry of walk(test_root, {
|
||||||
|
includeDirs: false,
|
||||||
|
exts: ["rs"],
|
||||||
|
})) {
|
||||||
|
if (entry.name.startsWith(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Deno.readTextFileSync(entry.path);
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
const actual = format(input);
|
||||||
|
Deno.writeTextFileSync(entry.path + ".snap", actual);
|
||||||
|
} else {
|
||||||
|
const test_name = relative(test_root.pathname, entry.path);
|
||||||
|
const expected = Deno.readTextFileSync(entry.path + ".snap");
|
||||||
|
|
||||||
|
Deno.test(test_name, () => {
|
||||||
|
const actual = format(input);
|
||||||
|
assertEquals(actual, expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
35
test_node/test-node.mjs
Normal file
35
test_node/test-node.mjs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { basename } from "node:path";
|
||||||
|
import { chdir } from "node:process";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import init, { format } from "../pkg/rust_fmt_node.js";
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const test_root = fileURLToPath(import.meta.resolve("../test_data"));
|
||||||
|
chdir(test_root);
|
||||||
|
|
||||||
|
for await (const input_path of fs.glob("**/*.rs")) {
|
||||||
|
if (basename(input_path).startsWith(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect_path = input_path + ".snap";
|
||||||
|
|
||||||
|
const [input, expected] = await Promise.all([
|
||||||
|
fs.readFile(input_path, "utf-8"),
|
||||||
|
fs.readFile(expect_path, "utf-8").catch(() => {
|
||||||
|
// 如果没有 snap 文件,就创建一个
|
||||||
|
const formatted = format(input);
|
||||||
|
return formatted;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
test(input_path, () => {
|
||||||
|
const actual = format(input);
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user