🎉 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