PackFMT #
PackFMT formats Reverie's authored scripts: KubeJS (kubejs/*_scripts/**/*.js) and ZenScript (scripts/**/*.zs). It is part of PackLSP and uses the same parsers, so it runs as packlsp fmt, and Packwand runs it for packwand fmt. Mod configs and JSON are out of scope, since mods rewrite them in their own style.
PackFMT keeps your line breaks. It fixes whitespace, indentation and a few token-level details, and never wraps or joins lines.
House style #
The style follows Reverie's newer scripts (silver-lining), then Prettier, then the Google JavaScript style guide.
| Rule | KubeJS .js |
ZenScript .zs |
Status |
|---|---|---|---|
| Line endings | LF | LF | done |
| Trailing whitespace | removed | removed | done |
| Blank lines | at most 1 in a row; none at the start or end of the file | same | done |
| Final newline | exactly one | exactly one | done |
| Indentation | 4 spaces, by nesting; continuation lines +1 (below) | 4 spaces, the same way | done |
| Quotes | single, unless double needs fewer escapes | unchanged (CraftTweaker style) | done |
| Semicolons | removed where that is safe (see below) | kept (CraftTweaker requires them) | done |
| Comments | //x → // x |
//x → // x |
done |
| Brackets | { a }, [a] on one line |
— | done |
| Arrow functions | event =>, not (event) => |
— | done |
| Spacing | between tokens on one line (below) | — | done |
| Trailing commas | kept as written, never added (KubeJS 6 rejects some) | — | done |
| Comment blocks | at most 5 lines (reported, not fixed) | same | done |
| Line length | 120 (reported, not fixed) | same | done |
Indentation #
Only leading whitespace changes. Line breaks stay where you put them.
- Inside brackets, a line sits one level past the line holding its innermost open bracket. So
foo({on one line opens a single level. - A line starting with a closer (
},),]) lines up with the line of its opener. - A continuation line sits one level past the line its statement, argument, property or element starts on. Examples are a chained
.call(), a condition broken before&&, the?and:of a ternary, or a value on the line after=. It never drifts further right, however long the chain. - A
{that follows)or) =>lines up with the line of that(. So the body of anifwhose condition spans lines sits one level past theif. else,catchandfinallyline up with their statement. A body without braces on its own line is a continuation. Statements undercasesit one level past it.- An own-line comment takes the indent of the next line of code, or the inner level when that line is a closer. The lines inside a block comment move with its first line, as long as every one of them is indented at least as far. Otherwise the comment is left as written.
- Lines starting inside a string, template literal or regex keep their whitespace, and so do lines in an ignored region. Tabs in indentation become spaces; tabs later in a line stay.
- ZenScript follows the same rules. Its items are judged from tokens: a token after
;,,,{,}or an open bracket starts an item. Preprocessor lines (#priority) go to column 0. - A file that does not parse is not re-indented at all.
Text inside strings, template literals and regex literals is never changed, apart from line endings in templates (JavaScript itself reads CRLF there as LF). Block comments keep their blank lines.
PackFMT never changes what a // comment says. KubeJS reads a property from any // line of the form key: value, so rewording or moving one could change how a script loads.
Spacing #
These rules apply between two tokens on the same line with no comment between them:
- One space:
- around binary and assignment operators,
=>, and a ternary's?and:; - after
,,;and an object's or case's:; - inside
{ … }on one line (objects followbracketSpacing); - in
if (,for (,while (,switch (,catch (,function (,) {and} else.
- around binary and assignment operators,
- No space:
- inside
( … )and[ … ]; - before
,,;and:; - around
.and?.; - after
...and a unary operator (-a,!b,++i); - before a call's
(and a named function's or method's(.
- inside
A space is never removed where the two tokens would merge into one (- -a stays as written), and 1 .toString() keeps its space. Template literals, including ${ … }, are left alone.
Semicolons #
A statement's ; is removed only when all of these hold:
- The next token is on a later line, or is
}, or the file ends there. - The next token is not one of these 10, which would join the two statements:
(open paren[open bracket`backtick: a template literal or tagged template+plus-minus/slash/=divide-assign++increment--decrement<less-than
- It does not end an empty statement or sit in a
for (;;)header.
Safety #
PackFMT checks every result before writing it. If any check fails, the file is left as it was and PF0003 is reported.
- For a script that parses, the syntax tree is the same as before. It is flow's ESTree output, compared without locations, comments or raw literal text, so a quote change, a dropped
;or dropped parentheses compare equal, and anything that changes what the code does fails. For ZenScript, and for scripts that do not parse, the tokens must be the same apart from whitespace. - Every comment has the same text, apart from whitespace (
//xand// xcount as the same). - The KubeJS header properties are the same.
- Formatting the result again changes nothing.
To check PackFMT itself on real scripts:
scripts/fmt-corpus.sh <dir>...runsfmt --verifyand fails on any refusal.node scripts/fmt-stress.mjs <dir>...does the same over thousands of mutated copies. Each copy either has random lines deleted, or has lines starting with each semicolon hazard inserted after a;.--mutant-checkreverses the stress run: it expects refusals, which proves the safety net catches a broken build.
A file with a syntax error only gets the whitespace rules, and PF0002 says so. That includes Rhino-only syntax that the JavaScript parser cannot read, such as for each.
Style checks #
Some rules can't be fixed without rewording or restructuring code, so PackFMT reports them instead. They show up in the editor and with packlsp check --style, never in packlsp fmt, so packwand fmt --check doesn't fail on them. diagnostics.md lists each code and its default severity:
- PF0101 comment block over 5 lines (commented-out code counts, and a quick fix deletes a block that is only code)
- PF0102 line over 120 characters
- PF1001
var - PF1002
==and!=, off unless you enable it - PF1003 a braceless body on its own line
- PF1004 no header comment
- PF1005 a top-level constant not in UPPER_SNAKE_CASE, with a quick fix that renames it across the script folder
- PF1006 KubeJS template residue, with a quick fix that deletes it
- PF1007 a header setting after the code
Change any code's severity, or turn it off, through .packlsp.json "severity" or the packlsp.diagnostics.severity setting. Naming PF1002 in either one switches it on.
Skipping code #
| Comment | Effect |
|---|---|
// packfmt-ignore-file |
Before the first line of code: the whole file is left alone. |
// packfmt-ignore |
The next line is left as written. |
// packfmt-ignore-start … // packfmt-ignore-end |
Everything between is left as written (line endings still become LF). |
// packfmt-disable-next-line PF0101 |
Do not report these codes on the next line. |
In ZenScript, # comments work too.
Settings #
Override the house style in the pack's .packlsp.json, under "format". Any key that is left out keeps its default. A bad value is reported as PF0004 and the default is used.
{
"format": {
"enabled": true, // false: packlsp fmt skips this pack
"indent": 4,
"quotes": "single", // or "double"
"semicolons": "remove", // or "preserve"
"arrowParens": "avoid", // or "preserve"
"bracketSpacing": true,
"endOfLine": "lf", // or "preserve"
"lineLength": 120,
"maxCommentLines": 5,
"maxBlankLines": 1,
// Globs relative to the pack root; * stays in one folder, ** crosses folders.
"ignore": ["kubejs/server_scripts/advjs_server.js"],
"zenscript": { "indent": 4 }
}
}
In the editor #
The same applies in IntelliJ IDEA through the LSP4IJ template (guide.md), where it's Reformat Code.
Format Document and Format Selection use PackFMT for KubeJS scripts and ZenScript files. Other formatters keep JSON and the rest. A CRLF file is switched to LF when the pack's endOfLine is lf. A result PackFMT refuses changes nothing and is logged in the PackLSP output channel.
To format on save, set PackLSP as the formatter in the modpack repository's .vscode/settings.json. editor.defaultFormatter can't be limited to a folder, so this belongs in the repository, not in your user settings:
{
"[javascript]": { "editor.defaultFormatter": "reverie-projects.packlsp", "editor.formatOnSave": true },
"[zenscript]": { "editor.defaultFormatter": "reverie-projects.packlsp", "editor.formatOnSave": true },
"files.eol": "\n"
}
Style checks show up as diagnostics. Turn them off with packlsp.format.styleDiagnostics, or change single codes with packlsp.diagnostics.severity. PF1006's quick fix deletes the template line. Agents get the same edits from the MCP format tool.
Command line #
packlsp fmt <path>... # format in place
packlsp fmt --check <path>... # report files that would change; exit 1 if any
packlsp fmt --verify <path>... # run the safety checks only; exit 1 if any result is refused
packlsp fmt --json ... # one JSON report (below)
packlsp fmt --strict ... # warnings also fail
packlsp fmt --stdin-filepath <path> < in.js # stdin to stdout; <path> picks the language and the pack
A folder is searched for the scripts KubeJS loads (kubejs/{startup,server,client}_scripts) and for every .zs file. A file named directly is formatted whenever it is .js or .zs. Files are replaced atomically, and unchanged files are not touched.
Exit status: 0 on success; 1 when a file would change (--check), a result was refused, or an issue is an error (a warning too with --strict); 2 for usage errors.
JSON report #
{
"schema": 1, "tool": "packfmt", "version": "26.10.2", "mode": "check",
"checked": 3, "changed": 1, "failed": 0,
"files": [{"path": "pack/kubejs/server_scripts/a.js", "status": "would_format", "edits": 2}],
"issues": [{"severity": "warning", "code": "PF0001", "path": "pack/kubejs/server_scripts/a.js",
"message": "File is not formatted (2 changes); run packlsp fmt.",
"remedy": "run packlsp fmt", "remedy_subject": "pack/kubejs/server_scripts/a.js",
"position": {"line": 1, "column": 1}}]
}
mode:write,checkorverify.files[].status:unchangedformatted(written)partial(written, whitespace only)would_formatrefusedskipped(with areason)
issues: Packwand'scontent-lintissue shape, pluscode. Lines and columns are 1-based; columns are UTF-8 bytes.error: appears on a fatal error, such as a missing path or an unknown option. It is{"kind": "not_found" | "invalid_request" | "operation_failed", "message": …}, andfilesandissuesare then empty.
Packwand integration (handoff spec) #
packwand fmt belongs to Packwand (Zig, packwandzig), but all formatting logic lives in PackLSP. Packwand finds the packlsp binary and reads its JSON report. This section is the contract for that work.
-
Command.
packwand fmt [PATH...] [--check] [--strict] [--json] [--packlsp <bin>]- Lives in
src/packwand_cli/fmt_command.zig, modelled onminify_command.zig. - Effects row
.mutate. - Ledger entry
D-ZIG-FMT-1. AGENTS.md requires approval before adding a Zig-only command. - PATH defaults to
[fmt] pathsinmpm-config.toml, falling back tomodpacks. PackLSP does the walking.
- Lives in
-
Finding the binary.
mpm_infra/files/PacklspSidecar.zig, afterPackeaterSidecar.zig, looks in this order:--packlspPACKWAND_PACKLSP_BINpacklsp[.exe]next to the packwand executablePATH- the binary inside the newest installed PackLSP editor extension (
~/.vscode-oss/extensionsor~/.vscode/extensions,reverie-projects.packlsp-<version>), so editor users need no setup
If none exists, it fails with
operation_failed, naming every place it tried. A binary from before PackFMT (it answersfmtwith its usage text) fails naming that binary. -
Running it. Packwand builds the argv directly, without a shell, and always adds
--json:packlsp fmt --json [--check] [--strict] <paths>, run in the-Cdirectory. Tests stand in shell scripts forpacklsp. -
Reading the report.
- stdout must be one object with
schema == 1,tool == "packfmt"and aversionat or above Packwand's minimum. error.kindmaps as for Packeater:not_foundandinvalid_requestbecomeinvalid_request; anything else becomesoperation_failed.issuesmap straight ontompm_api/diagnostics/issue.zig.- Each
would_formatfile becomes an error issue, "File is not formatted", with remedy "run packwand fmt". - Output that does not parse becomes
operation_failed, with the tail of stderr. --checkends with "N file(s) require formatting" and exit 1, likeminify --check.
- stdout must be one object with
-
mpm-config.toml [fmt](not done): keys such aspacklsp_binandpathswould replace Packwand's contract tests that count config keys, so they wait. The style itself stays in each pack's.packlsp.json. -
doctor. An optionalpacklsprow shows wherefmtwould find the binary (D-ZIG-FMT-2). Thepackeaterrow now findspackeater-sidecarwhere builds do (D-ZIG-DOCTOR-1, its own change). -
monorepo3. PackFMT is an optional editor tool, so no CI step fetches or runs it. What remains is optional: a one-time formatting commit,
.gitattributes*.js/*.zstext eol=lf, and one manual boot of a formatted 1.20.1 pack, to confirm Rhino reads scripts without semicolons the same way.