Skip to content

Commit e673d10

Browse files
committed
fix: use oxfmt for generated toml
1 parent 845aa9e commit e673d10

6 files changed

Lines changed: 98 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040

4141
### Fixed
4242

43+
- **alef-cli/alef-e2e: use `oxfmt` instead of Taplo for generated TOML normalization**:
44+
the default post-generation formatters now call `pnpm dlx oxfmt` for WASM
45+
manifests, FFI/workspace formatting, and Rust e2e `Cargo.toml` normalization,
46+
aligning generated output with Kreuzberg's shared pre-commit hooks.
47+
(`crates/alef-cli/src/pipeline/format.rs`, `crates/alef-e2e/src/format.rs`)
48+
49+
- **alef-backend-ffi: escape generated `cbindgen.toml` `after_includes` safely**:
50+
Doxygen blocks embedded in `after_includes` now render as escaped TOML
51+
multiline strings, so backslash commands such as `\code` and rustdoc examples
52+
containing triple quotes no longer make `cbindgen.toml` invalid.
53+
(`crates/alef-backend-ffi/templates/cbindgen_toml.jinja`,
54+
`crates/alef-backend-ffi/src/gen_bindings/helpers.rs`,
55+
`crates/alef-backend-ffi/src/gen_bindings/mod.rs`)
56+
4357
- **alef-backend-wasm: emit current `getrandom` for wasm target dependencies**:
4458
generated WASM `Cargo.toml` files now use `getrandom` `0.4` with `wasm_js`, so downstream
4559
aggressive dependency upgrades no longer make generated manifests stale.

crates/alef-backend-ffi/src/gen_bindings/helpers.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,30 @@ pub(super) fn gen_cbindgen_toml(prefix: &str, api: &alef_core::ir::ApiSurface) -
393393
})
394394
.collect::<Vec<_>>()
395395
.join("\n");
396+
let after_includes = if forward_decls.is_empty() {
397+
String::new()
398+
} else {
399+
toml_multiline_basic_string(&format!("/* Opaque type forward declarations */\n{forward_decls}\n"))
400+
};
396401

397402
crate::template_env::render(
398403
"cbindgen_toml.jinja",
399404
minijinja::context! {
400405
prefix_upper => &prefix_upper,
401-
forward_decls => &forward_decls,
406+
after_includes => &after_includes,
402407
},
403408
)
404409
}
405410

411+
fn toml_multiline_basic_string(value: &str) -> String {
412+
let escaped = value
413+
.replace('\\', "\\\\")
414+
.replace("\"\"\"", "\\\"\\\"\\\"")
415+
.replace('\u{8}', "\\b")
416+
.replace('\u{c}', "\\f");
417+
format!("\"\"\"\n{escaped}\"\"\"")
418+
}
419+
406420
// ---------------------------------------------------------------------------
407421
// build.rs generation
408422
// ---------------------------------------------------------------------------

crates/alef-backend-ffi/src/gen_bindings/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,7 @@ sources = ["src/lib.rs"]
15831583

15841584
let files = backend.generate_bindings(&api, &config).unwrap();
15851585
let cbindgen = files.iter().find(|f| f.path.ends_with("cbindgen.toml")).unwrap();
1586+
toml::from_str::<toml::Value>(&cbindgen.content).expect("cbindgen.toml must be valid TOML");
15861587

15871588
// Doxygen block precedes the typedef in `forward_decls`. The doc text
15881589
// is lifted from `TypeDef.doc` and rendered as `/** * ... */`.
@@ -1605,6 +1606,38 @@ sources = ["src/lib.rs"]
16051606
);
16061607
}
16071608

1609+
#[test]
1610+
fn test_cbindgen_toml_escapes_doxygen_backslashes() {
1611+
let mut api = doxygen_sample_api();
1612+
api.types[0].doc = r##"Has an example.
1613+
1614+
# Example
1615+
1616+
```rust
1617+
let value = "triple """ quote";
1618+
```"##
1619+
.to_string();
1620+
let config = sample_config();
1621+
let backend = FfiBackend;
1622+
1623+
let files = backend.generate_bindings(&api, &config).unwrap();
1624+
let cbindgen = files.iter().find(|f| f.path.ends_with("cbindgen.toml")).unwrap();
1625+
let parsed = toml::from_str::<toml::Value>(&cbindgen.content).expect("cbindgen.toml must parse");
1626+
let after_includes = parsed
1627+
.get("after_includes")
1628+
.and_then(toml::Value::as_str)
1629+
.expect("after_includes must be a string");
1630+
1631+
assert!(
1632+
after_includes.contains("\\code") && after_includes.contains("\\endcode"),
1633+
"Doxygen markers must survive TOML parsing: {after_includes}"
1634+
);
1635+
assert!(
1636+
after_includes.contains("triple \"\"\" quote"),
1637+
"triple quotes must round-trip through TOML parsing: {after_includes}"
1638+
);
1639+
}
1640+
16081641
#[test]
16091642
fn test_enum_opaque_typedef_carries_doxygen_block() {
16101643
let api = doxygen_sample_api();

crates/alef-backend-ffi/templates/cbindgen_toml.jinja

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ language = "C"
33
include_guard = "{{ prefix_upper }}_H"
44
pragma_once = true
55
autogen_warning = "/* This file is auto-generated by alef. DO NOT EDIT. */"
6-
{% if forward_decls %}
7-
after_includes = """
8-
/* Opaque type forward declarations */
9-
{{ forward_decls }}
10-
"""
6+
{% if after_includes %}
7+
after_includes = {{ after_includes }}
118
{% endif %}
129
[defines]
1310
"target_os = windows" = "SKIF_WINDOWS"

crates/alef-cli/src/pipeline/format.rs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,8 @@ fn get_default_formatter(config: &ResolvedCrateConfig, lang: Language) -> Option
139139
// `cargo sort` normalises the generated Cargo.toml so prek's cargo-sort hook
140140
// is a no-op; without it, cargo-sort reformats feature indentation after the
141141
// hash is finalised, making alef verify report the file as stale.
142-
// `taplo fmt` then re-flows array/inline-table whitespace per the consumer's
143-
// `.taplo.toml` so prek's taplo hook is likewise a no-op (best-effort: missing
144-
// taplo is skipped silently via `is_tool_available`).
142+
// `oxfmt` then re-flows array/inline-table whitespace consistently with
143+
// the shared Kreuzberg pre-commit hook.
145144
Language::Wasm => {
146145
let crate_dir = config
147146
.output_for("wasm")
@@ -160,8 +159,8 @@ fn get_default_formatter(config: &ResolvedCrateConfig, lang: Language) -> Option
160159
args: vec!["sort".to_owned(), crate_dir_str],
161160
},
162161
FormatterCommand {
163-
command: "taplo".to_owned(),
164-
args: vec!["fmt".to_owned(), manifest_path],
162+
command: "pnpm".to_owned(),
163+
args: vec!["dlx".to_owned(), "oxfmt".to_owned(), manifest_path],
165164
},
166165
],
167166
work_dir: String::new(),
@@ -175,9 +174,8 @@ fn get_default_formatter(config: &ResolvedCrateConfig, lang: Language) -> Option
175174
// `cargo sort -w` normalises all workspace Cargo.toml files so prek's
176175
// cargo-sort hook is a no-op; without it the hook reformats feature
177176
// indentation after finalize_hashes, making alef verify report stale files.
178-
// `taplo fmt` then re-flows whitespace across all workspace TOML files so
179-
// prek's taplo hook is similarly a no-op (best-effort: missing taplo is
180-
// skipped silently via `is_tool_available`).
177+
// `oxfmt` then re-flows whitespace across workspace TOML/JSON/JS/TS
178+
// files consistently with the shared Kreuzberg pre-commit hook.
181179
Language::Ffi => Some(FormatterSpec {
182180
commands: vec![
183181
FormatterCommand {
@@ -189,8 +187,8 @@ fn get_default_formatter(config: &ResolvedCrateConfig, lang: Language) -> Option
189187
args: vec!["sort".to_owned(), "-w".to_owned()],
190188
},
191189
FormatterCommand {
192-
command: "taplo".to_owned(),
193-
args: vec!["fmt".to_owned()],
190+
command: "pnpm".to_owned(),
191+
args: vec!["dlx".to_owned(), "oxfmt".to_owned(), ".".to_owned()],
194192
},
195193
],
196194
work_dir: String::new(),
@@ -541,11 +539,11 @@ project_file = "{project_file}"
541539
let config = make_config("liter-llm");
542540
let spec = get_default_formatter(&config, Language::Wasm).expect("should have formatter");
543541
// Three commands: cargo fmt (rs files), cargo sort (Cargo.toml table order),
544-
// then taplo fmt (Cargo.toml whitespace/array wrapping).
542+
// then oxfmt (Cargo.toml whitespace/array wrapping).
545543
assert_eq!(
546544
spec.commands.len(),
547545
3,
548-
"WASM must have cargo fmt + cargo sort + taplo fmt steps"
546+
"WASM must have cargo fmt + cargo sort + oxfmt steps"
549547
);
550548
let fmt_cmd = &spec.commands[0];
551549
assert_eq!(fmt_cmd.command, "cargo");
@@ -560,12 +558,12 @@ project_file = "{project_file}"
560558
vec!["sort", "crates/liter-llm-wasm"],
561559
"cargo sort arg must be the crate directory, not the manifest path"
562560
);
563-
let taplo_cmd = &spec.commands[2];
564-
assert_eq!(taplo_cmd.command, "taplo");
561+
let oxfmt_cmd = &spec.commands[2];
562+
assert_eq!(oxfmt_cmd.command, "pnpm");
565563
assert_eq!(
566-
taplo_cmd.args,
567-
vec!["fmt", "crates/liter-llm-wasm/Cargo.toml"],
568-
"taplo fmt must target the Cargo.toml manifest path so prek's taplo hook is a no-op"
564+
oxfmt_cmd.args,
565+
vec!["dlx", "oxfmt", "crates/liter-llm-wasm/Cargo.toml"],
566+
"oxfmt must target the Cargo.toml manifest path"
569567
);
570568
assert!(spec.work_dir.is_empty(), "WASM formatter must run at workspace root");
571569
}
@@ -604,11 +602,11 @@ wasm = "crates/ts-pack-core-wasm/src/"
604602
let config = make_config("liter-llm");
605603
let spec = get_default_formatter(&config, Language::Ffi).expect("should have formatter");
606604
// Three commands: cargo fmt --all (rs files), cargo sort -w (Cargo.toml table
607-
// order across the workspace), then taplo fmt (Cargo.toml whitespace).
605+
// order across the workspace), then oxfmt (workspace TOML/JSON/JS/TS whitespace).
608606
assert_eq!(
609607
spec.commands.len(),
610608
3,
611-
"FFI must have cargo fmt + cargo sort + taplo fmt steps"
609+
"FFI must have cargo fmt + cargo sort + oxfmt steps"
612610
);
613611
let fmt_cmd = &spec.commands[0];
614612
assert_eq!(fmt_cmd.command, "cargo");
@@ -620,12 +618,12 @@ wasm = "crates/ts-pack-core-wasm/src/"
620618
vec!["sort", "-w"],
621619
"cargo sort must run workspace-wide so all binding crate Cargo.toml files are normalised"
622620
);
623-
let taplo_cmd = &spec.commands[2];
624-
assert_eq!(taplo_cmd.command, "taplo");
621+
let oxfmt_cmd = &spec.commands[2];
622+
assert_eq!(oxfmt_cmd.command, "pnpm");
625623
assert_eq!(
626-
taplo_cmd.args,
627-
vec!["fmt"],
628-
"taplo fmt (no path) walks the workspace per .taplo.toml so prek's taplo hook is a no-op"
624+
oxfmt_cmd.args,
625+
vec!["dlx", "oxfmt", "."],
626+
"oxfmt must walk the workspace like the shared pre-commit hook"
629627
);
630628
assert!(spec.work_dir.is_empty(), "FFI formatter must run at workspace root");
631629
}

crates/alef-e2e/src/format.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use tracing::warn;
2121
/// flag (it's an unstable cargo-fmt-only flag in nightly), so running cargo
2222
/// fmt from the e2e crate's own directory is the portable way to format a
2323
/// non-workspace-member crate. `Cargo.toml` normalisation is handled by the
24-
/// `normalize_rust_toml` post-pass (taplo fmt), not by cargo-sort: cargo-sort
24+
/// `normalize_rust_toml` post-pass (oxfmt), not by cargo-sort: cargo-sort
2525
/// relocates leading top-level comments to the bottom of the file, which
2626
/// pushes the alef header (and `# alef:hash:` line) past the 10-line
2727
/// detection window and breaks `alef verify` silently.
@@ -46,8 +46,8 @@ fn default_formatter(lang: &str) -> Option<&'static str> {
4646
/// Run a best-effort TOML normalization pass on the rust e2e crate's
4747
/// `Cargo.toml` after the language formatter has finished.
4848
///
49-
/// Runs `taplo fmt Cargo.toml` so that downstream `prek` setups that include
50-
/// a taplo hook produce no further changes after `alef e2e generate`. Without
49+
/// Runs `pnpm dlx oxfmt Cargo.toml` so that downstream `prek` setups that
50+
/// include the shared oxfmt hook produce no further changes after `alef e2e generate`. Without
5151
/// this pass, prek would rewrite the manifest (array wrapping, indentation)
5252
/// after `finalize_hashes` has captured the pre-prek content, causing
5353
/// `alef verify` to report the file as stale.
@@ -63,14 +63,14 @@ fn default_formatter(lang: &str) -> Option<&'static str> {
6363
/// header inside a `[package.metadata.alef]` section that cargo-sort
6464
/// preserves.
6565
///
66-
/// Taplo is invoked via `sh -c` and is best-effort: a missing binary or
66+
/// oxfmt is invoked via `pnpm dlx` and is best-effort: a missing binary or
6767
/// non-zero exit is ignored. This is intentional — alef cannot assume a
6868
/// particular host toolchain, and the calling project's own CI is
69-
/// responsible for enforcing that taplo is present when they ship the
69+
/// responsible for enforcing that oxfmt is present when they ship the
7070
/// corresponding prek hook.
7171
fn normalize_rust_toml(dir: &str) {
72-
let taplo_cmd = format!("(cd {dir} && taplo fmt Cargo.toml >/dev/null 2>&1) || true");
73-
let _ = std::process::Command::new("sh").args(["-c", &taplo_cmd]).status();
72+
let oxfmt_cmd = format!("(cd {dir} && pnpm dlx oxfmt Cargo.toml >/dev/null 2>&1) || true");
73+
let _ = std::process::Command::new("sh").args(["-c", &oxfmt_cmd]).status();
7474
}
7575

7676
/// Run per-language formatters for all languages that had files generated.
@@ -120,8 +120,8 @@ pub fn run_formatters(files: &[GeneratedFile], e2e_config: &E2eConfig) {
120120
}
121121
}
122122

123-
// Rust-only TOML normalization pass: run taplo on the e2e crate's
124-
// Cargo.toml so that downstream prek hooks that include taplo produce
123+
// Rust-only TOML normalization pass: run oxfmt on the e2e crate's
124+
// Cargo.toml so that downstream prek hooks that include oxfmt produce
125125
// no further changes after generation. The user's
126126
// `e2e_config.format[rust]` override (if any) typically only covers
127127
// cargo fmt — which leaves `Cargo.toml` array wrapping at its raw
@@ -158,12 +158,12 @@ mod tests {
158158
// cargo-sort must NOT be in the default rust formatter: it relocates
159159
// the alef header comments to the bottom of the file, pushing the
160160
// `# alef:hash:` line past the 10-line detection window and silently
161-
// breaking `alef verify`. TOML normalisation is delegated to taplo via
161+
// breaking `alef verify`. TOML normalisation is delegated to oxfmt via
162162
// `normalize_rust_toml`.
163163
assert!(
164164
!cmd.contains("cargo sort"),
165165
"rust formatter must NOT run cargo sort — it scrambles the alef header \
166-
location in Cargo.toml. Use taplo via normalize_rust_toml instead: {cmd}"
166+
location in Cargo.toml. Use oxfmt via normalize_rust_toml instead: {cmd}"
167167
);
168168
}
169169

@@ -217,7 +217,7 @@ mod tests {
217217
#[test]
218218
fn test_normalize_rust_toml_is_best_effort_on_missing_dir() {
219219
// Should return cleanly even though the dir does not exist; both
220-
// cargo-sort and taplo invocations are wrapped in `|| true`.
220+
// oxfmt invocation is wrapped in `|| true`.
221221
normalize_rust_toml("/nonexistent/alef-e2e-test/dir");
222222
}
223223
}

0 commit comments

Comments
 (0)