🎉 initial commit

This commit is contained in:
2025-09-20 20:44:24 +08:00
commit 82538addcc
19 changed files with 483 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

6
scripts/build.sh Normal file
View 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
View 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
View 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
View 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
View 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);

View 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));
}
}

View 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));
}
}

View File

@@ -0,0 +1,5 @@
fn main(){println!("Hello, world!");}
fn add(a:i32,b:i32)->i32{
a+b
}

View File

@@ -0,0 +1,6 @@
fn main() {
println!("Hello, world!");
}
fn add(a: i32, b: i32) -> i32 {
a + b
}

View 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);
}
}

View 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
View 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
View 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);
});
}