Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/csharp-dotnet-10-0-12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scalar/csharp-fmt": patch
---

Rebuilt on the .NET 10.0.12 runtime (from 10.0.10). CSharpier stays at 1.3.0, and `test/native-conformance.test.ts` still asserts byte-identical output against a native `csharpier` 1.3.0.
5 changes: 5 additions & 0 deletions .changeset/php-cs-fixer-3-95-27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scalar/php-fmt": patch
---

The bundled PHP CS Fixer moves from 3.95.18 to 3.95.27, and the PHP it runs on (`@php-wasm/node-8-4` and `@php-wasm/universal`) from 3.1.47 to 3.1.55. The phar is still the official release, unmodified. `test/native-conformance.test.ts` runs the shipped artifact on a native PHP and asserts byte-identical output on every sample.
9 changes: 9 additions & 0 deletions .changeset/rubocop-1-91.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@scalar/ruby-fmt": minor
---

The bundled RuboCop moves from 1.81.6 to 1.91.0. rubocop-ast stays at 1.50.0 and parser at 3.3.12.0, so both boot-time performance patches still apply to the versions they were written against. Among RuboCop's own dependencies, parallel moves to 2.2.0, regexp_parser to 2.13.0, unicode-display_width to 3.3.0 and unicode-emoji to 4.3.0. `@ruby/wasm-wasi` moves from 2.8.1 to 2.10.1, which matches the `js` gem 2.10.1 already built into the artifact.

**This changes output**, in the Layout corrections RuboCop makes after syntax_tree. Formatting 304 files taken from the parser, rubocop-ast, syntax_tree, regexp_parser, rainbow and unicode-display_width gems, 6 come back different. Each difference is RuboCop 1.91's indentation for one of three things: a method chain wrapped onto the lines after its receiver, a block hanging off such a chain, or a parenthesised multi-line `if` condition. `test/rubocop-conformance.test.ts` asserts byte-identical output against a native `rubocop` 1.91.0, and passes. A consumer comparing against their own `rubocop --only Layout` in CI should move to 1.91.0 too.

The artifact grows from 12.7 MB to 13.2 MB.
7 changes: 7 additions & 0 deletions .changeset/rustfmt-1-10-nightly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@scalar/rust-fmt": minor
---

The bundled rustfmt moves from 1.9.0-nightly (`nightly-2026-07-19`, rust-lang/rust `eff8269f7`) to 1.10.0-nightly (`nightly-2026-09-22`, `1303417c4`). **This can change output.** rustfmt's formatting depends on the compiler it parses with, so a consumer comparing against a native `rustfmt` in CI should move to the same nightly. `test/native-conformance.test.ts` asserts byte-identical output against `rustfmt 1.10.0-nightly (1303417c41 2026-09-21)`, and passes.

The build script also needed three changes for the newer nightly. Cargo now gives each unit its own `build/<package>/<hash>/out/` directory instead of one shared `deps/`, and builds rlibs with their metadata split out into a sibling `.rmeta`. rustfmt now also links `rustc_feature`.
7 changes: 7 additions & 0 deletions .changeset/swift-format-604.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@scalar/swift-fmt": minor
---

The bundled swift-format moves from 603.0.0 to 604.0.0, compiled by the Swift 6.4.0 toolchain and Swift SDK for WebAssembly (from 6.3.3). **This can change output.** A consumer comparing against a native `swift-format` in CI should move to 604.0.0 too. `test/native-conformance.test.ts` asserts byte-identical output against a native swift-format 604.0.0 built from the same tag, and passes.

The one new configuration key in 604, `OrderedImports.shouldGroupImports`, is nested under a rule's configuration, which `FormatOptions` does not expose, so the options type is unchanged.
6 changes: 3 additions & 3 deletions .claude/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ packed by hand in `build/csharp_fmt/build.sh`.

Reference: **swift-format**. Status: exact.

Runs actual swift-format 603.0.0 — the release that pairs with Swift 6.3 — with
Runs actual swift-format 604.0.0 — the release that pairs with Swift 6.4 — with
swift-syntax's real parser, compiled by the official Swift SDK for WebAssembly
from swift.org. Not the SwiftWasm fork, and not a reimplementation.

Expand Down Expand Up @@ -502,7 +502,7 @@ Swift global.
default wasm stack overflows on ordinary Swift — swift-syntax's own
`UnicodeScalarExtensions.swift` is 10KB and chains ~70 `||` operators into one
expression, and walking a tree that deep traps with `memory access out of
bounds`. And the SDK must be named by **id** (`swift-6.3.3-RELEASE_wasm`), not
bounds`. And the SDK must be named by **id** (`swift-6.4.0-RELEASE_wasm`), not
by triple: the normal and embedded SDKs both declare `wasm32-unknown-wasip1`,
and the ambiguity resolves to the embedded one, which fails deep inside
swift-syntax with "unavailable in embedded Swift".
Expand All @@ -521,7 +521,7 @@ Exception, so a paid product may ship a copy.

Reference: **PHP CS Fixer**. Status: exact.

Runs the official php-cs-fixer 3.95.18 phar, unmodified, on actual PHP 8.4
Runs the official php-cs-fixer 3.95.27 phar, unmodified, on actual PHP 8.4
compiled to WebAssembly. It works because PHP CS Fixer is pure PHP and a
maintained wasm PHP already exists, so there is nothing to compile.

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Composer, no native binaries, no postinstall downloads.

| Package | Reference | Artifact | Status | Browser |
|:---|:---|---:|:---|:---|
| [`@scalar/ruby-fmt`](packages/ruby) | syntax_tree + RuboCop | 12.7 MB | ✅ exact +3 fixes | ✅ |
| [`@scalar/ruby-fmt`](packages/ruby) | syntax_tree + RuboCop | 13.2 MB | ✅ exact +3 fixes | ✅ |
| [`@scalar/java-fmt`](packages/java) | google-java-format | 0.77 MB | ✅ exact | ✅ |
| [`@scalar/kotlin-fmt`](packages/kotlin) | ktfmt | 0.82 MB | ✅ exact | ✅ |
| [`@scalar/csharp-fmt`](packages/csharp) | CSharpier | 4.2 MB | ✅ exact | ✅ |
Expand Down Expand Up @@ -131,7 +131,7 @@ arrives: fetched rather than read from disk.

**Run it in a worker.** Every one of these compiles multi-megabyte wasm and holds
tens to hundreds of megabytes of linear memory. On the main thread that is a
frozen tab, and Swift — 12.5 MB over the wire, 49.4 MB of wasm — is one you want
frozen tab, and Swift — 12.5 MB over the wire, 49.5 MB of wasm — is one you want
behind an explicit user action rather than on page load.

### Where the bytes come from
Expand Down Expand Up @@ -225,7 +225,7 @@ RuboCop either way. See
[`packages/ruby`](packages/ruby#two-tools-and-why-both) for the rest of what it
costs.

It ships as one 12.7 MB `ruby_fmt.wasm.br` with CRuby and the gems baked in,
It ships as one 13.2 MB `ruby_fmt.wasm.br` with CRuby and the gems baked in,
built by [`build/ruby_fmt/build.sh`](build/ruby_fmt/build.sh) - stdlib the
formatter never loads is stripped, then `wasm-opt -Os`, then
[wizer](https://github.com/bytecodealliance/wizer), then brotli. The wizer step
Expand Down Expand Up @@ -338,8 +338,8 @@ traps under AOT, so the error text is assembled from its parts instead.

## Swift

See [`packages/swift`](packages/swift). It runs the real swift-format 603.0.0 -
the release that pairs with Swift 6.3, swift-syntax's parser included - compiled
See [`packages/swift`](packages/swift). It runs the real swift-format 604.0.0 -
the release that pairs with Swift 6.4, swift-syntax's parser included - compiled
by the **official** [Swift SDK for WebAssembly](https://www.swift.org/documentation/articles/wasm-getting-started.html)
from swift.org rather than the SwiftWasm fork. A conformance test asserts
byte-identical output against a native `swift-format`, and the build was checked
Expand Down Expand Up @@ -371,7 +371,7 @@ ship a copy.

## PHP

See [`packages/php`](packages/php). It runs the real PHP CS Fixer 3.95.18 - the
See [`packages/php`](packages/php). It runs the real PHP CS Fixer 3.95.27 - the
official phar, unmodified - on real PHP 8.4 compiled to WebAssembly, so output
is byte-identical to the same phar on a native PHP. A conformance test asserts
that, and the build was checked against 1117 real PHP files (PHP CS Fixer's own
Expand Down
2 changes: 1 addition & 1 deletion build/csharp_fmt/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ DOTNET_VERSION="${DOTNET_VERSION:-10.0}"
CSHARPIER_VERSION="${CSHARPIER_VERSION:-1.3.0}"
# Ships the wasmconsole template. The templates are not part of the SDK or the
# workload; they are a separate NuGet package, versioned per TFM.
TEMPLATES_VERSION="${TEMPLATES_VERSION:-10.0.10}"
TEMPLATES_VERSION="${TEMPLATES_VERSION:-10.0.12}"

TOOLCHAIN="$PWD/toolchain"
OUT_DIR="../../packages/csharp"
Expand Down
2 changes: 1 addition & 1 deletion build/php_fmt/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ cd "$(dirname "$0")"
# Pinned rather than "latest" so a rebuild is reproducible and a version bump is
# a reviewable diff. The conformance test resolves this same version from the
# artifact, so the native side and the wasm side can never silently disagree.
PCF_VERSION="${PCF_VERSION:-3.95.18}"
PCF_VERSION="${PCF_VERSION:-3.95.27}"
OUT="../../packages/php/php_fmt.phar.br"
URL="https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v${PCF_VERSION}/php-cs-fixer.phar"

Expand Down
2 changes: 1 addition & 1 deletion build/ruby_fmt/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ gem "syntax_tree", "6.3.0"
# carries prism 1.2.0, so on that build the whole 1.75+ line simply fails to
# load; Ruby 4.0.0 carries 1.7.0, which is why the bump was the price of
# admission for a rubocop newer than 1.74.0.
gem "rubocop", "1.81.6"
gem "rubocop", "1.91.0"
gem "rubocop-ast", "1.50.0"

# Two gems CRuby already carries, kept out of the bundle by placeholder
Expand Down
30 changes: 15 additions & 15 deletions build/ruby_fmt/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,23 @@ GEM
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
logger (1.7.0)
parallel (1.28.0)
parallel (2.2.0)
parser (3.3.12.0)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.8.1)
rainbow (3.1.1)
regexp_parser (2.12.0)
rubocop (1.81.6)
json (~> 2.3)
regexp_parser (2.13.0)
rubocop (1.91.0)
json (>= 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.50.0)
Expand All @@ -55,9 +55,9 @@ GEM
logger
syntax_tree (6.3.0)
prettier_print (>= 1.2.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
unicode-display_width (3.3.0)
unicode-emoji (~> 4.3)
unicode-emoji (4.3.0)

PLATFORMS
aarch64-linux
Expand All @@ -72,7 +72,7 @@ DEPENDENCIES
js (= 2.10.1)
json!
prism!
rubocop (= 1.81.6)
rubocop (= 1.91.0)
rubocop-ast (= 1.50.0)
ruby_wasm (= 2.10.1)
syntax_tree (= 6.3.0)
Expand All @@ -84,14 +84,14 @@ CHECKSUMS
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
parallel (1.28.0) sha256=33e6de1484baf2524792d178b0913fc8eb94c628d6cfe45599ad4458c638c970
parallel (2.2.0) sha256=e1059c5fd7b649558a0aec38a769f06a42942bdb40503d005a59c352fe011cd8
parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
prettier_print (1.2.1) sha256=a72838b5f23facff21f90a5423cdcdda19e4271092b41f4ea7f50b83929e6ff9
prism (1.7.0)
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
rubocop (1.81.6) sha256=a7095eca1b79bbbe7d81c74c11ed2c532faa868bd9d31c2f12b7be7fbfbfef42
regexp_parser (2.13.0) sha256=d105d95d2138954e73914d2fbfb3d0a8f8605a49a3e89864c32e78b9107cb770
rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0
rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
ruby_wasm (2.10.1) sha256=874e0acb783a8326bfb0db892004816c26963a4276a9f719791a8828e53a05f1
Expand All @@ -102,8 +102,8 @@ CHECKSUMS
ruby_wasm (2.10.1-x86_64-linux) sha256=728e29dc688246572b56dcef1c7b131431d07865320c9b8434dd1ccdfd63db2b
ruby_wasm (2.10.1-x86_64-linux-musl) sha256=c4d16572f9a465e460490d0608e817239c5e0ddfc9553f18db2bcaebecbcdd8d
syntax_tree (6.3.0) sha256=56e25a9692c798ec94c5442fe94c5e94af76bef91edc8bb02052cbdecf35f13d
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
unicode-display_width (3.3.0) sha256=4b7aa66a4b11db50f6f7e98411215cd0dd10eaecb665d36eec5bcacb3fa0b613
unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce

BUNDLED WITH
4.0.9
10 changes: 5 additions & 5 deletions build/rust_fmt/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ cd "$(dirname "$0")"
# move together and neither moves alone. RUST_COMMIT must be the commit the
# nightly was built from - `rustc +nightly-<date> -vV` prints it - because the
# rustc_private crates are built from that source and loaded by that compiler.
RUST_NIGHTLY="${RUST_NIGHTLY:-nightly-2026-07-19}"
RUST_COMMIT="${RUST_COMMIT:-eff8269f797067c30555e77f160ec84c0ed15cd9}"
RUST_VERSION="${RUST_VERSION:-1.99.0}"
RUST_NIGHTLY="${RUST_NIGHTLY:-nightly-2026-09-22}"
RUST_COMMIT="${RUST_COMMIT:-1303417c416e1595173d9689e7394c31e136ae95}"
RUST_VERSION="${RUST_VERSION:-1.100.0}"

TARGET="wasm32-wasip1"
CHECKOUT="$PWD/rust"
Expand Down Expand Up @@ -106,8 +106,8 @@ cargo build -p rustc_parse -p rustc_expand -p rustc_ast_pretty \
--target "$TARGET" --release

echo "==> collecting externs"
WASM_DEPS="$CHECKOUT/target/$TARGET/release/deps"
HOST_DEPS="$CHECKOUT/target/release/deps"
WASM_DEPS="$(python3 "$OLDPWD/collect-artifacts.py" deps "$CHECKOUT/target/$TARGET/release" "$CHECKOUT/target/wasm-deps")"
HOST_DEPS="$(python3 "$OLDPWD/collect-artifacts.py" deps "$CHECKOUT/target/release" "$CHECKOUT/target/host-deps")"
PROC_MACROS="$CHECKOUT/target/proc-macros"

EXTERNS="$(python3 "$OLDPWD/collect-artifacts.py" externs "$WASM_DEPS")"
Expand Down
67 changes: 56 additions & 11 deletions build/rust_fmt/collect-artifacts.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
#!/usr/bin/env python3
"""Collect the build outputs rustfmt needs injected, from a cargo target dir.

Two modes, both consumed by the RUSTC_WRAPPER in inject-externs.sh:
deps <profile-dir> <flat-dir>
Print the directory holding a profile's compiled crates, flat, the way
cargo's `deps/` used to. Cargo's newer build-dir layout gives each unit
its own `build/<package>/<hash>/out/` instead, which no single
`-L dependency=` can point at, so when there is no `deps/` the crates
are hard-linked into <flat-dir> and that is printed instead. Either way
the modes below, and the -L paths the build passes, see one directory.

The other two modes are consumed by the RUSTC_WRAPPER in inject-externs.sh:

externs <wasm-deps-dir>
Print `--extern name=path` for the eight rustc_private crates rustfmt
loads plus thin_vec. Exits non-zero naming anything missing, so a broken
compiler-crate build fails here rather than as a confusing E0463 later.
Print `--extern name=path` for the nine rustc_private crates rustfmt
loads plus thin_vec. Each crate gets its .rmeta as well as its .rlib
when there is one: cargo now builds rlibs without their metadata
embedded, leaving a stub that rustc refuses on its own. Exits non-zero
naming anything missing, so a broken compiler-crate build fails here
rather than as a confusing E0463 later.

procmacros <host-deps-dir> <out-dir>
Copy the host proc-macro .so files into a directory of their own.
Copy every host proc-macro .so into a directory of their own.
They must be on a -L path for the wasm build to resolve rustc_macros and
derive_where, but pointing -L at the whole host deps dir shadows the wasm
builds of tracing/ignore/annotate_snippets and breaks the build.

Cargo leaves several hashed copies of a crate behind across check and build
runs, so both modes take the newest per crate name.
runs, so `externs` takes the newest per crate name. `procmacros` keeps them
all, because there a second copy can be a second version rather than a stale
one.
"""

import os
Expand All @@ -32,6 +45,7 @@
"rustc_data_structures",
"rustc_errors",
"rustc_expand",
"rustc_feature",
"rustc_parse",
"rustc_session",
"rustc_span",
Expand All @@ -54,26 +68,57 @@ def newest_per_crate(directory, pattern):
return best


def deps(profile_dir, flat_dir):
legacy = os.path.join(profile_dir, "deps")
if os.path.isdir(legacy):
print(legacy)
return
shutil.rmtree(flat_dir, ignore_errors=True)
os.makedirs(flat_dir)
units = os.path.join(profile_dir, "build")
for package in sorted(os.listdir(units)):
for unit in sorted(os.listdir(os.path.join(units, package))):
out = os.path.join(units, package, unit, "out")
if not os.path.isdir(out):
continue
for entry in os.listdir(out):
if re.match(r"^lib.+-[0-9a-f]{16}\.(rlib|rmeta|so)$", entry):
os.link(os.path.join(out, entry), os.path.join(flat_dir, entry))
print(flat_dir)


def externs(deps_dir):
found = newest_per_crate(deps_dir, r"^lib(.+)-[0-9a-f]{16}\.rlib$")
found = {k: v for k, v in found.items() if k in SYSROOT_CRATES}
missing = [c for c in SYSROOT_CRATES if c not in found]
if missing:
sys.exit(f"missing wasm rlibs in {deps_dir}: {', '.join(missing)}")
print(" ".join(f"--extern {n}={p}" for n, (p, _) in sorted(found.items())))
flags = []
for name, (path, _) in sorted(found.items()):
flags.append(f"--extern {name}={path}")
rmeta = path[: -len(".rlib")] + ".rmeta"
if os.path.exists(rmeta):
flags.append(f"--extern {name}={rmeta}")
print(" ".join(flags))


def procmacros(deps_dir, out_dir):
found = newest_per_crate(deps_dir, r"^lib(.+)-[0-9a-f]{16}\.so$")
# Every copy, not the newest per name: the graph can hold two versions of
# one proc macro (thiserror-impl 1 and 2, say) under the same crate name,
# and rustc picks between them by the hash each dependent recorded. Keeping
# only one fails with E0463 naming whichever was dropped.
found = [e for e in os.listdir(deps_dir) if re.match(r"^lib.+-[0-9a-f]{16}\.so$", e)]
shutil.rmtree(out_dir, ignore_errors=True)
os.makedirs(out_dir)
for path, _ in found.values():
shutil.copy2(path, out_dir)
for entry in found:
shutil.copy2(os.path.join(deps_dir, entry), out_dir)
print(f"collected {len(found)} proc macros", file=sys.stderr)


if __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "externs":
if len(sys.argv) >= 4 and sys.argv[1] == "deps":
deps(sys.argv[2], sys.argv[3])
elif len(sys.argv) >= 3 and sys.argv[1] == "externs":
externs(sys.argv[2])
elif len(sys.argv) >= 4 and sys.argv[1] == "procmacros":
procmacros(sys.argv[2], sys.argv[3])
Expand Down
Loading