How we like the code in RexReplace to be.
- Slim and simple over features. If a feature makes the code complicated, we drop it instead of building it.
- Beautiful code: strict, lean, focused. Each function does one thing. No extra options or "just in case" code.
- Read like the code around it. Use the same names, idioms and amount of comments as the code next to it.
- Comments say why, not what. Write them in plain, simple English. Leave the comment out if the code already says it.
- No sloppy shortcuts. If two cases need different calls, write a plain
if/elseinstead of one call that only half fits both. - Everything is printed with
write()insrc/output.ts, so--nlknows how each stream ends. - The engine never touches the process. It resolves with the status report of the run, or throws a panic when the run can not go on. Only
src/cli.tsreadsargvand stdin, sets the exit code and ends the process. - stdout is the result. That is the names of the affected files, or the content from
--output,--output-matchor piped data. Everything else goes to stderr, so the result can always be piped.
All output goes through src/output.ts. Pick the function by who the message is for:
| Function | Shown when | For | Example |
|---|---|---|---|
print(item, delimiter, tail, stream) |
Always | The result. Puts the delimiter after each item, or only between them without a tail. Without a delimiter it is a line break, unless the item already ends with one. | A name, the content of a file, a match |
printDone() |
At the end of a run, also when it is stopped | Ends stdout and stderr with a line break when --nl is set |
|
info(name) |
Unless --quiet |
The name of an affected file. stdout, or stderr when stdout has content. | a.md |
warn(msg) |
Unless --quiet |
Something is likely a mistake, but the run goes on. Also added to status.warnings. |
Data is piped in, but no flag tells what to do with it |
error(msg, data) |
Unless --quiet-total |
Something went wrong. The run goes on, unless --halt is set: then a panic is thrown. Also added to status.errors, so the CLI gives the exit code 1. |
File not found: a.md |
die(msg, data, help) |
Unless --quiet-total |
Something went wrong and the run can not go on. Throws a panic, and the CLI exits with 1. Also added to status.errors. |
No data piped into replacement |
chat(msg) |
--verbose, also with --quiet, but not with --quiet-total |
The user: what happens to each file. Only ever to stderr. Also kept in engine.steps of the status report. |
Open sync: a.md, Nothing changed in: a.md |
- What the run did is in the status report: the engine resolves with it, and
--statusprints it. So there is no extra debug output to print along the way. --debugprints only the status report, with what went into the engine and what it did.warn(),error()anddie()add to the status report as they go, also when the quiet flags hide what they print.
- Tests are strict, lean and focused too. One thing per test, and the name of the test says what it is.
- Expect the full result where it matters (
{out, err, exit}fromrr()), so nothing extra slips through. UserrStatus()for the status of the same run. - A known bug is a
test.todo('BUG #<issue>: ...')until it is fixed. Then it becomes a real test. - The folder tells what is tested, so a file is named after the feature:
test/engine/<feature>.test.jstests the engine withrr(), andtest/cli/<feature>.test.jstests the CLI by running it with Bun's$shell, likesh($`bun ${cli} o x my.file`). - A test for an issue goes in
test/engine/fix/<issue>.test.jsortest/cli/fix/<issue>.test.js. test/cli/run.shis a demo of how RexReplace is used: a few clear examples for each feature. Checks of many combinations go intest/cli/<feature>.test.js.- Use Bun native file IO (
Bun.write,Bun.file) in new tests (#734). - Names in tests say what they hold, like
expectedOutput, notnamed. - Async work can finish in any order. Do not expect an order the code does not promise.
Write in plain, simple English, with sections like ## Today, ## Change, ## To decide, ## Good to know and ## Reference.