From 7e04e4cf0b3d0df01a1c4940daf21703cb7e9df4 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:00:19 +1100 Subject: [PATCH 1/9] Let the engine return a status report and the CLI own the process to close #742 --- README.md | 5 +- devguide.md | 22 ++-- src/cli.ts | 221 ++++++++++++++++++-------------- src/engine.ts | 150 +++++++++++++--------- src/output.ts | 57 ++++----- test/cli/run.sh | 2 +- test/fuzzy/crossover.test.js | 1 - test/js/crossover.test.js | 1 - test/js/debug.test.js | 125 ++++++++---------- test/js/fix/703.test.js | 4 +- test/js/fix/705.test.js | 6 - test/js/fix/742.test.js | 238 +++++++++++++++++++++++++++++++++++ test/js/helper.js | 29 +++-- test/js/output.test.js | 4 +- test/js/quiet-total.test.js | 11 +- test/js/quiet.test.js | 17 ++- test/js/verbose.test.js | 32 ++--- test/js/void-async.test.js | 26 ++-- 18 files changed, 602 insertions(+), 349 deletions(-) create mode 100644 test/js/fix/742.test.js diff --git a/README.md b/README.md index b18f9400..5233aa0c 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,10 @@ Use --js-pipe to access the pipe data in your javascript code. | | **`--tail`** Shorthand for setting both --name-delimiter-tail and --content-delimiter-tail. Use --no-tail to not print the delimiter after the last name and the last content. The specific tail flags win over --tail. [boolean] | | `-l` | **`--nl`** Let what is printed to stdout and stderr (the names, or the content from -o, -m or piped data) always end with a new line. No extra new line if it already ends with a new line, or if nothing was printed. [boolean] | | `-H` | **`--halt`**, **`--bail`** Halt on first error [boolean] [default: false] | -| `-d` | **`--debug`** Print debug info [boolean] | +| `-S` | **`--status-compact`** As the last thing before ending any execution a status report will be printed as one line of JSON. Will print even in combination with `--quiet-total`. Will incude extra info about the inner workings of the engine in combination with the `--verbose` flag. [boolean] | +| | **`--status`** Same as `--status-compact`, but always starting with a `\n` followed by pretty printed JSON. [boolean] | +| `-d` | **`--debug`** Only print a status report, with what went into the engine and its steps. The result of --output and --output-match will still printed. [boolean] | + | `-€` | **`--void-euro`** Void having `€` as alias for `$`. The alias is used in all inputs, but never in globs, file names or piped data. [boolean] | | `-§` | **`--void-section`** Void having `§` as alias for `\`. The alias is used in all inputs, but never in globs, file names or piped data. [boolean] | | `-o` | **`--output`** Output the final result instead of saving to file. Will also output content even if no replacement has taken place. [boolean] | diff --git a/devguide.md b/devguide.md index f2ee143b..33fae48c 100644 --- a/devguide.md +++ b/devguide.md @@ -10,6 +10,7 @@ How we like the code in RexReplace to be. - **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`/`else` instead of one call that only half fits both. - **Everything is printed with `write()` in `src/output.ts`,** so `--nl` knows 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.ts` reads `argv` and 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-match` or piped data. Everything else goes to stderr, so the result can always be piped. ## Telling what is going on @@ -21,22 +22,19 @@ All output goes through `src/output.ts`. Pick the function by who the message is | `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 | `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. | `File not found: a.md` | -| `die(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Exits with 1. | `No data piped into replacement` | -| `chat(msg)` | `--verbose` (or `--debug`) | The user: what happens to each file | `Open sync: a.md`, `Nothing changed in: a.md` | -| `debug(data)` | `--debug` | The developer: what happens inside, at points that matter | `Work on content from: a.md` | -| `step(data)` | `--verbose` and `--debug` | The developer: a trace of every step, with the data at that step | `Get final regex`, the full config | - -- The three levels of telling go from little to a lot: `chat`, then `debug`, then `step`. -- `chat` falls back to `debug` without `--verbose`, so `--debug` alone also shows what happens to each file. -- `debug` and `step` print their data as JSON. That keeps line breaks and spaces visible. -- `chat`, `debug` and `step` only ever write to stderr. +| `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 `--status` prints it. So there is no extra debug output to print along the way. +- `--debug` prints only the status report, with what went into the engine and what it did. +- `warn()`, `error()` and `die()` add to the status report as they go, also when the quiet flags hide what they print. ## Tests - **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}`), so nothing extra slips through. +- Expect the full result where it matters (`{out, err, exit}` from `rr()`), so nothing extra slips through. Use `rrStatus()` for the status of the same run. - A known bug is a `test.todo('BUG #: ...')` until it is fixed. Then it becomes a real test. - A test for an issue goes in `test/js/fix/.test.js`. - Use Bun native file IO (`Bun.write`, `Bun.file`) and `Bun.spawnSync` in new tests (#734). diff --git a/src/cli.ts b/src/cli.ts index 53a82936..bd68b44b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,8 +15,7 @@ let needHelp = 0; if (process.argv.length < 4) { if (/^(-v|--?version)$/.test(process.argv[process.argv.length - 1])) { console.log(version); - process.exitCode = 0; - process.exit(); + process.exit(0); } else if (/^(-h|--?help)$/.test(process.argv[process.argv.length - 1])) { needHelp = 1; } else { @@ -97,6 +96,17 @@ const yargs = require('yargs') // Not .choices() as node takes the name of an encoding in any case, like `UTF8` .check((argv) => Buffer.isEncoding(argv.encoding) || `Unknown encoding: ${argv.encoding}`) + .string('x') + .describe( + 'x', + 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.' + ) + .alias('x', 'exclude-re') + + .string('X') + .describe('X', 'Exclude files found with this glob. Can be used multiple times.') + .alias('X', 'exclude-glob') + .alias('E', 'engine') .describe( 'E', @@ -177,10 +187,6 @@ const yargs = require('yargs') .alias('H', 'bail') .default('H', false) - .boolean('d') - .describe('d', 'Print debug info') - .alias('d', 'debug') - .boolean('€') .describe( '€', @@ -302,16 +308,6 @@ The following values defaults to \`❌\` if haystack does not originate from a f All variables, except from module, date objects, \`nl\` and \`_\`, has a corresponding variable name followed by \`_\` where the content has an extra space at the end (for easy concatenation). ` ) - .string('x') - .describe( - 'x', - 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.' - ) - .alias('x', 'exclude-re') - - .string('X') - .describe('X', 'Exclude files found with this glob. Can be used multiple times.') - .alias('X', 'exclude-glob') /* .boolean('N') @@ -395,7 +391,6 @@ N n O P r -S t U W w @@ -404,6 +399,25 @@ Z z */ + .boolean('S') + .alias('S', 'status-compact') + .describe( + 'S', + 'As the last thing before ending any execution a status report will be printed as one line of JSON. Will print even in combination with `--quiet-total`. Will incude extra info about the inner workings of the engine in combination with the `--verbose` flag.' + ) + + .boolean('status') + .describe( + 'status', + 'Same as `--status-compact`, but always starting with a `\n` followed by pretty printed JSON' + ) + + .boolean('d') + .describe( + 'd', + 'Print only the status report, with what went into the engine and what it did. Does not block the output of --output and --output-match.' + ) + .alias('d', 'debug') .help('h') .describe('h', 'Display help.') @@ -411,12 +425,30 @@ Z z .epilog(`Inspiration: .oO(What should 'sed' have been by now?)`); +// The engine resolves with the status of the run, or throws a panic when the run can not go on. +// The CLI is the only part that acts on the process. +function printStatus(config, status) { + if (config.status) { + process.stdout.write('\n' + JSON.stringify(status, null, 2) + '\n'); + } else if (config.statusCompact) { + process.stdout.write(JSON.stringify(status) + '\n'); + } +} + +function exitOnPanic(config, e) { + if (!e?.panic) { + throw e; + } + // A panic from the CLI itself comes before the engine has a status + if (e.status) { + printStatus(config, e.status); + } + process.exit(1); +} + function backOut(exitcode = 1) { - const help = yargs.showHelp(); - const io = exitcode ? console.error : console.log; - //io(help); - process.exitCode = exitcode; - process.exit(); + yargs.showHelp(); + process.exit(exitcode); } const lineBreakEscapes = {'\n': '\\n', '\r': '\\r', '\u2028': '\\u2028', '\u2029': '\\u2029'}; @@ -437,111 +469,108 @@ function unescapeString(str = '') { return new Function(`return '${source}'`)(); } -(function () { - if (0 < needHelp) { - return backOut(needHelp - 1); - } - - // All options into one big config object for the rexreplace core - let config: any = {}; +if (0 < needHelp) { + backOut(needHelp - 1); +} - // Use only camelCase full length version of settings so we make sure the core can be documented properly - Object.keys(yargs.argv).forEach((key) => { - if (1 < key.length && key.indexOf('-') < 0) { - config[key] = yargs.argv[key]; - } - }); +// All options into one big config object for the rexreplace core +const config: any = {}; - if (config.jsPipe) { - config.replacementJs = config.pipe = true; +// Use only camelCase full length version of settings so we make sure the core can be documented properly +Object.keys(yargs.argv).forEach((key) => { + if (1 < key.length && key.indexOf('-') < 0) { + config[key] = yargs.argv[key]; } +}); + +if (config.jsPipe) { + config.replacementJs = config.pipe = true; +} - outputConfig(config); +if (config.debug) { + config.status = config.verbose = config.quietTotal = true; +} - let pipeInUse = false; - let pipeData = ''; +outputConfig(config); - config.pipedData = null; +config.pipedData = null; - config.showHelp = yargs.showHelp; +config.showHelp = yargs.showHelp; - config.pattern = pattern; +config.pattern = pattern; - config.includeGlob = yargs.argv._; +config.includeGlob = yargs.argv._; - config.excludeGlob = [].concat(yargs.argv.excludeGlob).filter(Boolean); +config.excludeGlob = [].concat(yargs.argv.excludeGlob).filter(Boolean); - config.excludeRe = [].concat(yargs.argv.excludeRe).filter(Boolean); +config.excludeRe = [].concat(yargs.argv.excludeRe).filter(Boolean); - config.replacement = replacement; +config.replacement = replacement; - // The aliases come before the escape sequences are interpreted, so `§n` gives a line break just like `\n` - const typed = {...config}; - alias(config); +// The aliases come before the escape sequences are interpreted, so `§n` gives a line break just like `\n` +const typed = {...config}; +alias(config); - // The error shows the value as it was typed - const unescape = (key, name) => { +// The error shows the value as it was typed +const unescape = (key, name) => { + try { + config[key] = unescapeString(config[key]); + } catch (e) { + // The engine is not running yet, so the panic is caught here try { - config[key] = unescapeString(config[key]); - } catch (e) { die('Invalid escape sequence in ' + name + ':', typed[key]); + } catch (panic) { + exitOnPanic(config, panic); } - }; - - if (!config.replacementJs) { - unescape('replacement', 'replacement'); } +}; - if (config.delimiter) { - unescape('delimiter', '--delimiter'); - } - if (config.nameDelimiter) { - unescape('nameDelimiter', '--name-delimiter'); - } - if (config.contentDelimiter) { - unescape('contentDelimiter', '--content-delimiter'); - } +if (!config.replacementJs) { + unescape('replacement', 'replacement'); +} - /*if(Boolean(process.stdout.isTTY)){ - config.output = true; - }*/ +if (config.delimiter) { + unescape('delimiter', '--delimiter'); +} +if (config.nameDelimiter) { + unescape('nameDelimiter', '--name-delimiter'); +} +if (config.contentDelimiter) { + unescape('contentDelimiter', '--content-delimiter'); +} - // The engine tells how many errors there were, and the CLI turns that into the exit code - const run = () => - rexreplace.engine(config).then((errors) => { - if (errors) { +// The exit code is only set, so the process ends by itself when all output is written +const run = () => + rexreplace.engine(config).then( + (status) => { + printStatus(config, status); + if (status.errors.length) { process.exitCode = 1; } - }); + }, + (e) => exitOnPanic(config, e) + ); - // Like grep, stdin is only read when piped data is expected. An open pipe that nobody writes to would make the run wait forever. - const pipeDataExpected = !config.includeGlob.length || config.replacementPipe || config.pipe; +// Like grep, stdin is only read when piped data is expected. An open pipe that nobody writes to would make the run wait forever. +const pipeDataExpected = !config.includeGlob.length || config.replacementPipe || config.pipe; - if (!pipeDataExpected || Boolean(process.stdin.isTTY)) { - return run(); - } +if (!pipeDataExpected || Boolean(process.stdin.isTTY)) { + run(); +} else { + // Stays null when nothing comes through the pipe + let pipeData = null; process.stdin.setEncoding(config.encoding); - process.stdin.on('readable', () => { - let chunk = process.stdin.read(); - - if (null !== chunk) { - pipeInUse = true; - pipeData += chunk; - while ((chunk = process.stdin.read())) { - pipeData += chunk; - } - } + process.stdin.on('data', (chunk) => { + pipeData = (pipeData ?? '') + chunk; }); process.stdin.on('end', () => { - if (pipeInUse) { - if (yargs.argv.trimPipe) { - pipeData = pipeData.trim(); - } - config.pipedData = pipeData; + if (null !== pipeData && config.trimPipe) { + pipeData = pipeData.trim(); } + config.pipedData = pipeData; run(); }); -})(); +} diff --git a/src/engine.ts b/src/engine.ts index 04233151..7708c470 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -8,19 +8,7 @@ const globs = require('fast-glob'); const now = new Date(); -import { - outputConfig, - errorCount, - step, - debug, - chat, - info, - print, - printDone, - warn, - error, - die, -} from './output.ts'; +import {outputConfig, chat, info, print, printDone, warn, error, die} from './output.ts'; const re = { mctime: /[mc]time/, @@ -32,7 +20,8 @@ const re = { }; // Only the version ends up in the bundle -export {version} from '../package.json'; +import {version} from '../package.json'; +export {version}; // The regex engine comes with the runtime: Node has V8 and Bun has JSC (JavaScriptCore) const runtime = process.versions.bun ? 'Bun' : 'Node'; @@ -44,19 +33,49 @@ function validateEncoding(encoding = 'utf8') { } } -// Returns a promise that resolves with the number of errors when all files are done +// Resolves with the status of the run when all files are done. +// When the run can not go on, a panic is thrown with the status so far. export async function engine(config: any = {engine: 'default'}) { - await run(config); - return errorCount(); + config.statusInfo = newStatus(config); + outputConfig(config); + try { + await run(config); + } catch (e) { + if (e?.panic) { + e.status = config.statusInfo; + } + throw e; + } finally { + printDone(); + } + return config.statusInfo; +} + +function newStatus(config) { + const status: any = {files: {searched: [], updated: []}, pipe: null, warnings: [], errors: []}; + + // The status report with --verbose also tells what went into the engine and what it did. + // The config is copied before it gets the status, and the piped data is told about in `pipe`. + if (config.verbose && (config.status || config.statusCompact)) { + status.engine = { + version, + runtime: runtime + ' ' + runtimeEngine, + config: JSON.parse( + JSON.stringify(config, (key, value) => + 'statusInfo' === key || 'pipedData' === key ? undefined : value + ) + ), + execution: null, + steps: [], + }; + } + + return status; } async function run(config) { - outputConfig(config); validateEncoding(config.encoding); - step('Displaying steps for:'); - step(config); - config.pattern = getFinalPattern(config.pattern, config) || ''; config.replacement = getFinalReplacement(config.replacement, config) || ''; @@ -73,18 +92,34 @@ async function run(config) { config.regex = getFinalRegex(config.pattern, config) || ''; - step(config); + // What the regex engine is asked to do. With --replacement-js and --output-match it gets a function. + if (config.statusInfo.engine) { + config.statusInfo.engine.execution = { + search: config.regex.source, + flags: config.regex.flags, + replacement: config.replacementJs || config.outputMatch ? null : config.replacement, + replacementSrc: + config.replacementJs && !config.outputMatch ? config.replacementOri : null, + }; + } const piped = handlePipedData(config); + if (null !== config.pipedData) { + config.statusInfo.pipe = { + bytes: Buffer.byteLength(config.pipedData, config.encoding), + type: pipeType(config, piped), + updated: false, + }; + } + // The more specific tail wins. Piped data that is printed is left as it came in, but matches are matches, from a file or a pipe. config.nameDelimiterTail = config.nameDelimiterTail ?? config.tail ?? true; config.contentDelimiterTail = config.contentDelimiterTail ?? config.tail ?? !(piped && config.output); if (piped) { - doReplacement('Piped data', config, config.pipedData); - return printDone(); + return doReplacement('Piped data', config, config.pipedData); } config.files = getFilePaths(config); @@ -99,8 +134,6 @@ async function run(config) { config.voidAsync = config.voidAsync || 1 === config.files.length || config.output || config.outputMatch; - step(config); - const files = config.files.filter((filepath) => fs.existsSync(filepath) ? true : error('File not found:', filepath) ); @@ -120,10 +153,10 @@ async function run(config) { }; await Promise.all(Array.from({length: workers}, worker)); - return printDone(); } function openFile(file, config) { + config.statusInfo.files.searched.push(file); if (config.voidAsync) { chat('Open sync: ' + file); let data; @@ -143,7 +176,7 @@ function openFile(file, config) { // postfix argument names to limit the probabillity of user inputted javascript accidently using same values function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) { - debug('Work on content from: ' + _file_rr); + chat('Work on content from: ' + _file_rr); // Main regexp of the whole thing const result = _data_rr.replace( @@ -156,11 +189,15 @@ function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) { if (_config_rr.output || _config_rr.outputMatch) { // With --output-match the matches are printed by the replacement if (_config_rr.output) { - debug('Output result from: ' + _file_rr); + chat('Output result from: ' + _file_rr); print(result, _config_rr.contentDelimiter, _config_rr.contentDelimiterTail); } // The file is not changed, but it is still named if its content would change - if ((_config_rr.nameAll || result !== _data_rr) && !_config_rr.dataIsPiped) { + if (_config_rr.dataIsPiped) { + _config_rr.statusInfo.pipe.updated = result !== _data_rr; + } else if (result !== _data_rr) { + updated(_file_rr, _config_rr); + } else if (_config_rr.nameAll) { info(_file_rr); } return; @@ -178,13 +215,13 @@ function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) { // Release the memory while storing files _data_rr = ''; - debug('Write new content to: ' + _file_rr); + chat('Write new content to: ' + _file_rr); // Write directly to the same file (if the process is killed all new and old data is lost) if (_config_rr.voidBackup) { return fs.promises .writeFile(_file_rr, result, _config_rr.encoding) - .then(() => info(_file_rr), error); + .then(() => updated(_file_rr, _config_rr), error); } //Make sure data is always on disk @@ -202,19 +239,37 @@ function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) { } catch (e) { return error(e); } - return info(_file_rr); + return updated(_file_rr, _config_rr); } return fs.promises .rename(oriFile, backupFile) .then(() => fs.promises.writeFile(oriFile, result, _config_rr.encoding)) .then(() => _config_rr.keepBackup || fs.promises.unlink(backupFile)) - .then(() => info(_file_rr), error); + .then(() => updated(_file_rr, _config_rr), error); } -function handlePipedData(config) { - step('Check Piped Data'); +// The content of the file was changed, or would be with --output and --output-match, so it is named +function updated(file, config) { + config.statusInfo.files.updated.push(file); + info(file); +} +// How the piped data was used +function pipeType(config, piped) { + if (piped) { + return 'CONTENT'; + } + if (config.replacementJs) { + return 'JS'; + } + if (config.replacementPipe) { + return 'REPLACEMENT'; + } + return 'UNUSED'; +} + +function handlePipedData(config) { if (config.includeGlob.length) { // Empty piped data (or only a line break) is not worth a warning if ( @@ -241,8 +296,6 @@ function handlePipedData(config) { } function getFinalPattern(pattern, conf: any) { - step('Get final pattern'); - if (conf.literal) { pattern = pattern.replace(re.regexSpecialChars, '\\$&'); } @@ -252,18 +305,15 @@ function getFinalPattern(pattern, conf: any) { pattern = new Function('return '+pattern)(); }*/ - step(pattern); return pattern; } function getFinalReplacement(replacement, conf: any) { - step('Get final replacement'); /*if(config.replacementFile){ return oneLinerFromFile(fs.readFileSync(replacement,'utf8')); }*/ if (conf.replacementPipe) { - step('Piping replacement'); conf.pipedDataUsed = true; if (null === conf.pipedData) { return die('No data piped into replacement'); @@ -272,15 +322,11 @@ function getFinalReplacement(replacement, conf: any) { } if (conf.outputMatch) { - step('Output match'); - if (parseInt(process.versions.node) < 6) { return die('outputMatch is only supported in node 6+'); } return function (match, ...rest) { - step(arguments); - // The groups are followed by the offset and the full text, and by the named groups if the pattern has any. // Without groups the full match is printed, otherwise the groups are printed together. const groups = rest.slice(0, 'object' === typeof rest[rest.length - 1] ? -3 : -2); @@ -303,8 +349,6 @@ function getFinalReplacement(replacement, conf: any) { return die('Captured groups for javascript replacement is only supported in node 6+'); } - step(replacement); - return replacement; } @@ -321,8 +365,6 @@ function getFinalReplacement(replacement, conf: any) { function getFinalRegex(pattern, config) { const engine = 'default' === config.engine ? runtimeEngine : config.engine; - step('Get final regex with engine: ' + engine); - let regex; let flags = getFlags(config); @@ -332,7 +374,6 @@ function getFinalRegex(pattern, config) { try { regex = new RegExp(pattern, flags); } catch (e) { - if (config.debug) throw new Error(e); die(e.message); } break; @@ -348,7 +389,6 @@ function getFinalRegex(pattern, config) { //regex = new RE2(pattern, flags); regex = new RegExp(pattern, flags); } catch (e) { - if (config.debug) throw new Error(e); die(e.message); } break; @@ -356,14 +396,10 @@ function getFinalRegex(pattern, config) { die(`Engine ${config.engine} not supported`); } - step(regex); - return regex; } function getFlags(config) { - step('Get flags'); - let flags = ''; if (!config.voidGlobal) { @@ -386,8 +422,6 @@ function getFlags(config) { flags += 'u'; } - step(flags); - return flags; } @@ -585,8 +619,6 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { } // Capture groups used, so need to run once per match return function () { - step(arguments); - const __pipe = _pipe, __text = _text, __find = _find, diff --git a/src/output.ts b/src/output.ts index 34ec16aa..4e3bb648 100644 --- a/src/output.ts +++ b/src/output.ts @@ -11,17 +11,17 @@ const lastItemChar = new Map(); // The char code of the last character printed to each stream const lastPrintedChar = new Map(); -// The number of errors in this run, so the CLI can give an exit code -let errors = 0; - export const outputConfig = function (_config) { config = _config; lastItemChar.clear(); lastPrintedChar.clear(); - errors = 0; }; -export const errorCount = () => errors; +// Thrown when the run can not go on. The engine adds the status so far, and the CLI ends the process. +export class Panic extends Error { + panic = true; + status = null; +} // Everything is printed from here, so the last character of each stream is known const write = function (text, stream) { @@ -79,15 +79,17 @@ const note = function (msg) { }; export const warn = function (msg) { + config.statusInfo?.warnings.push(msg); // The line break gives an empty line under the warning note(' 🟡 ' + msg + '\n'); }; +// --verbose was asked for, so --quiet does not hide it. --quiet-total wins and hides it too. +// The status report keeps the same lines in `engine.steps`, so they are there even when they are not printed. export const chat = function (msg) { - if (config.verbose) { - note(msg); - } else { - debug(msg); + config.statusInfo?.engine?.steps.push(msg); + if (config.verbose && !config.quietTotal) { + write(font.gray(msg) + '\n', process.stderr); } }; @@ -95,40 +97,23 @@ export const die = function (msg = '', data = '', displayHelp = false) { if (displayHelp && !config.quietTotal) { config.showHelp(); } - // The line break gives an empty line under the error - if (msg && data) { - error(' ❌ ' + msg, data + '\n'); - } else if (msg) { - error(' ❌ ' + msg + '\n'); + const text = msg + (data ? ' ' + data : ''); + config.statusInfo?.errors.push(text); + if (!config.quietTotal) { + // The line break gives an empty line under the error + write(font.red(' ❌ ' + text) + '\n\n', process.stderr); } - kill(); + throw new Panic(text); }; export const error = function (msg, data = '') { + const text = msg + (data ? ' ' + data : ''); + config.statusInfo?.errors.push(text); if (!config.quietTotal) { - write(font.red(msg) + (data ? ' ' + data : '') + '\n', process.stderr); + write(font.red(text) + '\n', process.stderr); } if (config.halt) { - kill(); + throw new Panic(text); } - errors++; return false; }; - -export function debug(data) { - if (config.debug) { - write(font.gray(JSON.stringify(data, null, 4)) + '\n', process.stderr); - } -} - -export function step(data) { - if (config.verbose) { - debug(data); - } -} - -function kill(exitCode = 1) { - // What was printed before the stop still ends with a line break - printDone(); - process.exit(exitCode); -} diff --git a/test/cli/run.sh b/test/cli/run.sh index 3a4c28a4..60825c8e 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -329,7 +329,7 @@ reset assert "rexreplace 'b' 'x' my.file -V 2>/dev/null" 'my.file' reset -assert "rexreplace 'b' 'x' my.file -V 2>&1 >/dev/null | grep -c 'my.file'" '1' +assert "rexreplace 'b' 'x' my.file -V 2>&1 >/dev/null | grep -cx 'my.file'" '0' reset assert "rexreplace 'b' 'x' my.file -q" '' diff --git a/test/fuzzy/crossover.test.js b/test/fuzzy/crossover.test.js index f3d4b245..6ce16f1e 100644 --- a/test/fuzzy/crossover.test.js +++ b/test/fuzzy/crossover.test.js @@ -14,7 +14,6 @@ const flags = [ 'quiet', 'quietTotal', 'halt', - 'debug', 'voidEuro', 'voidSection', 'output', diff --git a/test/js/crossover.test.js b/test/js/crossover.test.js index f6324156..46fda5af 100644 --- a/test/js/crossover.test.js +++ b/test/js/crossover.test.js @@ -45,7 +45,6 @@ describe('every pair of flags', () => { 'quiet', 'quietTotal', 'halt', - 'debug', 'voidEuro', 'voidSection', 'output', diff --git a/test/js/debug.test.js b/test/js/debug.test.js index 2e10b9e2..396d2296 100644 --- a/test/js/debug.test.js +++ b/test/js/debug.test.js @@ -1,87 +1,64 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import path from 'path'; -describe('--debug', () => { - test('prints what happens to a file', async () => { - fs.writeFileSync('my.file', 'foobar'); - const result = await rr('o', 'x', {includeGlob: ['my.file'], debug: true}); - expect(result.err).toBe( - [ - '"1 files found"', - '"Open sync: my.file"', - '"Work on content from: my.file"', - '"Write new content to: my.file"', - '', - ].join('\n') - ); - expect(result.out).toBe('my.file\n'); - }); +// --debug is set by the CLI, so these cases run the CLI itself +const cli = path.join(import.meta.dir, '../../src/cli.ts'); - test('prints what happens to piped data', async () => { - const result = await rr('o', 'x', {pipedData: 'foobar', debug: true}); - expect(result.err).toBe( - '"Work on content from: Piped data"\n"Output result from: Piped data"\n' - ); +function runCli(...args) { + const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, ...args], { + stdin: 'ignore', }); + return {out: stdout.toString(), err: stderr.toString(), exit: exitCode}; +} - test('is not the default', async () => { - expect((await rr('o', 'x', {pipedData: 'foobar'})).err).toBe(''); - }); +describe('--debug', () => { + test.each(['--debug', '-d'])( + '%s prints only the status report, with what went into the engine and what it did', + (flag) => { + fs.writeFileSync('my.file', 'foobar'); + const result = runCli('o', 'x', 'my.file', flag); + expect(result.err).toBe(''); + expect(result.exit).toBe(0); + expect(result.out.startsWith('\n{')).toBe(true); + const status = JSON.parse(result.out); + expect(status.files).toEqual({searched: ['my.file'], updated: ['my.file']}); + expect(status.engine.config).toMatchObject({pattern: 'o', replacement: 'x'}); + expect(status.engine.execution).toEqual({ + search: 'o', + flags: 'gim', + replacement: 'x', + replacementSrc: null, + }); + expect(status.engine.steps).toEqual([ + '1 files found', + 'Open sync: my.file', + 'Work on content from: my.file', + 'Write new content to: my.file', + ]); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); + } + ); - test('never writes to stdout', async () => { - expect((await rr('o', 'x', {pipedData: 'foobar', debug: true})).out).toBe('fxxbar'); + test('still prints the result of --output before the status report', () => { + fs.writeFileSync('my.file', 'foobar'); + const result = runCli('o', 'x', 'my.file', '--debug', '--output'); + expect(result.out.startsWith('fxxbar\n\n{')).toBe(true); + expect(JSON.parse(result.out.slice('fxxbar\n'.length)).files.updated).toEqual(['my.file']); }); - test('throws the full error for an invalid pattern', async () => { - await expect(rr('(', 'x', {pipedData: 'foobar', debug: true})).rejects.toThrow( - 'Invalid regular expression' - ); + test('gives the errors in the status report, and the exit code 1', () => { + const result = runCli('o', 'x', 'nope.file', '--debug'); + expect(result.err).toBe(''); + expect(result.exit).toBe(1); + expect(JSON.parse(result.out).errors).toEqual(['0 files found']); }); - describe('combined', () => { - test('with --verbose every step and the config is printed', async () => { - const config = {pipedData: 'foobar', debug: true, verbose: true, voidGlobal: true}; - const result = await rr('o', 'x', config); - expect(result.err).toContain('"Get flags"\n"im"'); - expect(result.err).toContain('"voidGlobal": true'); - expect(result.out).toBe('fxobar'); - }); - - test('with --quiet and --quiet-total the debug info is still printed', async () => { - const config = {pipedData: 'foobar', debug: true}; - expect((await rr('o', 'x', {...config, quiet: true})).err).toContain( - 'Work on content from' - ); - expect((await rr('o', 'x', {...config, quietTotal: true})).err).toContain( - 'Work on content from' - ); - }); - - test('with --output', async () => { - fs.writeFileSync('my.file', 'foobar'); - const result = await rr('o', 'x', { - includeGlob: ['my.file'], - debug: true, - output: true, - }); - expect(result.out).toBe('fxxbar\n'); - expect(result.err).toContain('"Output result from: my.file"'); - expect(result.err).not.toContain('Write new content'); - }); - - test('with a file where nothing is replaced', async () => { - fs.writeFileSync('my.file', 'foobar'); - const result = await rr('nope', 'x', {includeGlob: ['my.file'], debug: true}); - expect(result.err).toContain('"Nothing changed in: my.file"'); - }); - - test('with --replacement-js and --output-match', async () => { - const js = await rr('o', '1+1', {pipedData: 'fo', debug: true, replacementJs: true}); - expect(js.out).toBe('f2'); - const match = await rr('o', '_', {pipedData: 'fo', debug: true, outputMatch: true}); - expect(match.out).toBe('o\n'); - expect(match.err).toBe('"Work on content from: Piped data"\n'); - }); + test('gives the error of an invalid pattern in the status report', () => { + fs.writeFileSync('my.file', 'foobar'); + const result = runCli('(', 'x', 'my.file', '--debug'); + expect(result.err).toBe(''); + expect(result.exit).toBe(1); + expect(JSON.parse(result.out).errors[0]).toContain('Invalid regular expression'); }); }); diff --git a/test/js/fix/703.test.js b/test/js/fix/703.test.js index ea457072..169ea0b1 100644 --- a/test/js/fix/703.test.js +++ b/test/js/fix/703.test.js @@ -32,8 +32,8 @@ describe('#703 the names of affected files', () => { expect(result.err).toBe(''); }); - test('are alone on stdout with --verbose and --debug', async () => { - const result = await run({verbose: true, debug: true}); + test('are alone on stdout with --verbose', async () => { + const result = await run({verbose: true}); expect(result.out).toBe('a.file\nc.file\n'); expect(result.err).toContain('Open sync: a.file'); expect(result.err).toContain('Nothing changed in: b.file'); diff --git a/test/js/fix/705.test.js b/test/js/fix/705.test.js index 5dd24e52..210057b7 100644 --- a/test/js/fix/705.test.js +++ b/test/js/fix/705.test.js @@ -95,12 +95,6 @@ describe('#705 the engine returns a promise that resolves when all files are don }); describe('the promise is rejected when the work can not be done', () => { - test('like with an invalid pattern in debug mode', async () => { - fs.writeFileSync('my.file', 'foo'); - const flags = {includeGlob: ['my.file'], debug: true}; - await expect(rr('(', 'x', flags)).rejects.toThrow('Invalid regular expression'); - }); - test.each(flows)('like with code that fails with %s', async (name, flags) => { fs.writeFileSync('my.file', 'foo'); const config = {includeGlob: ['my.file'], replacementJs: true, ...flags}; diff --git a/test/js/fix/742.test.js b/test/js/fix/742.test.js new file mode 100644 index 00000000..855433ff --- /dev/null +++ b/test/js/fix/742.test.js @@ -0,0 +1,238 @@ +// https://github.com/mathiasrw/rexreplace/issues/742 +// The engine ended the process itself and resolved with nothing useful. Now it resolves with a status, a panic is thrown +// when the run can not go on, and only the CLI acts on the process. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import path from 'path'; +import {rr, rrStatus} from '../helper.js'; + +const cli = path.join(import.meta.dir, '../../../src/cli.ts'); + +// Make a.file and c.file with `foo`, and b.file with `bar` (nothing to replace) +function makeFiles() { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + return {includeGlob: ['*.file']}; +} + +function runCli(...args) { + const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, ...args], { + stdin: 'ignore', + }); + return {out: stdout.toString(), err: stderr.toString(), exit: exitCode}; +} + +describe('#742 the engine resolves with a status', () => { + test('with the files searched and updated', async () => { + expect(await rrStatus('o', 'x', makeFiles())).toEqual({ + files: {searched: ['a.file', 'b.file', 'c.file'], updated: ['a.file', 'c.file']}, + pipe: null, + warnings: [], + errors: [], + }); + }); + + test('with the files that would be updated with --output', async () => { + const status = await rrStatus('o', 'x', {...makeFiles(), output: true}); + expect(status.files.updated).toEqual(['a.file', 'c.file']); + expect(fs.readFileSync('a.file', 'utf8')).toBe('foo'); + }); + + test('with --name-all only updates the files that are changed', async () => { + const status = await rrStatus('o', 'x', {...makeFiles(), nameAll: true}); + expect(status.files.updated).toEqual(['a.file', 'c.file']); + }); + + test('with the errors, also when --quiet-total hides them', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('b.file', 0); + const status = await rrStatus('o', 'x', {includeGlob: ['*.file'], quietTotal: true}); + expect(status.files).toEqual({searched: ['a.file', 'b.file'], updated: ['a.file']}); + expect(status.errors).toHaveLength(1); + expect(status.errors[0]).toContain('EACCES'); + }); + + test('with the warnings, also when --quiet hides them', async () => { + fs.writeFileSync('a.file', 'foo'); + const status = await rrStatus('o', 'x', { + includeGlob: ['a.file'], + pipedData: 'x', + quiet: true, + }); + expect(status.warnings).toEqual([ + 'Data is piped in, but no flag tells what to do with it. It is not used.', + ]); + }); +}); + +describe('#742 the status tells about the piped data', () => { + test('as the content that is updated', async () => { + expect((await rrStatus('o', 'x', {pipedData: 'foo'})).pipe).toEqual({ + bytes: 3, + type: 'CONTENT', + updated: true, + }); + expect((await rrStatus('z', 'x', {pipedData: 'foo'})).pipe.updated).toBe(false); + }); + + test('with the size in bytes', async () => { + expect((await rrStatus('o', 'x', {pipedData: 'æø🦖'})).pipe.bytes).toBe(8); + }); + + test.each([ + ['REPLACEMENT', {replacementPipe: true}], + ['JS', {replacementJs: true}], + ['JS', {replacementPipe: true, replacementJs: true}], + ['UNUSED', {}], + ])('as %s', async (type, flags) => { + fs.writeFileSync('a.file', 'foo'); + const status = await rrStatus('o', 'pipe', { + includeGlob: ['a.file'], + pipedData: '"y"', + ...flags, + }); + expect(status.pipe).toEqual({bytes: 3, type, updated: false}); + }); +}); + +describe('#742 a panic', () => { + test('stops the run with --halt, with the status so far', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + fs.chmodSync('b.file', 0); + const config = {includeGlob: ['*.file'], halt: true}; + expect((await rr('o', 'x', config)).exit).toBe(1); + fs.writeFileSync('a.file', 'foo'); + const status = await rrStatus('o', 'x', config); + expect(status.files).toEqual({searched: ['a.file', 'b.file'], updated: ['a.file']}); + expect(status.errors[0]).toContain('EACCES'); + }); + + test('is thrown when the run can not go on, with the error in the status', async () => { + expect(await rr('(', 'x', {pipedData: 'foo'})).toMatchObject({out: '', exit: 1}); + expect((await rrStatus('(', 'x', {pipedData: 'foo'})).errors).toHaveLength(1); + }); +}); + +describe('#742 the engine part of the status', () => { + const debug = {status: true, verbose: true}; + + test('is only there with --verbose on --status or --status-compact', async () => { + expect(await rrStatus('o', 'x', {pipedData: 'foo', verbose: true})).not.toHaveProperty( + 'engine' + ); + expect(await rrStatus('o', 'x', {pipedData: 'foo', status: true})).not.toHaveProperty( + 'engine' + ); + const status = await rrStatus('o', 'x', { + pipedData: 'foo', + statusCompact: true, + verbose: true, + }); + expect(status.engine.version).toMatch(/^\d+\.\d+\.\d+/); + expect(status.engine.runtime).toBe('Bun JSC'); + }); + + test('has the config as it came in, but not the piped data', async () => { + const {config} = (await rrStatus('o', 'x', {pipedData: 'foo', ...debug})).engine; + expect(config).toMatchObject({pattern: 'o', replacement: 'x', status: true}); + expect(config).not.toHaveProperty('pipedData'); + expect(config).not.toHaveProperty('statusInfo'); + }); + + test.each([ + ['a replacement', {}, 'x', null], + ['--replacement-js', {replacementJs: true}, null, '"x"'], + ['--output-match', {outputMatch: true}, null, null], + ])( + 'tells what the regex engine was asked to do with %s', + async (name, flags, replacement, src) => { + const code = flags.replacementJs ? '"x"' : 'x'; + const status = await rrStatus('o+', code, { + pipedData: 'foo', + voidMultiline: true, + ...debug, + ...flags, + }); + expect(status.engine.execution).toEqual({ + search: 'o+', + flags: 'gi', + replacement, + replacementSrc: src, + }); + } + ); + + test('has the steps of the run in order, also when they are not printed', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + const config = {includeGlob: ['*.file'], ...debug, quietTotal: true}; + expect((await rr('o', 'x', config)).err).toBe(''); + fs.writeFileSync('a.file', 'foo'); + expect((await rrStatus('o', 'x', config)).engine.steps).toEqual([ + '2 files found', + 'Open sync: a.file', + 'Work on content from: a.file', + 'Write new content to: a.file', + 'Open sync: b.file', + 'Work on content from: b.file', + 'Nothing changed in: b.file', + ]); + }); + + test('tells the replacement that was piped in', async () => { + fs.writeFileSync('a.file', 'foo'); + const status = await rrStatus('o', '_', { + includeGlob: ['a.file'], + pipedData: 'y', + replacementPipe: true, + ...debug, + }); + expect(status.engine.execution.replacement).toBe('y'); + }); +}); + +describe('#742 the CLI', () => { + test('--status prints a line break and the status report with an indent of two spaces after the result', () => { + fs.writeFileSync('a.file', 'foo'); + const result = runCli('o', 'x', 'a.file', '--status'); + const status = { + files: {searched: ['a.file'], updated: ['a.file']}, + pipe: null, + warnings: [], + errors: [], + }; + expect(result).toEqual({ + out: 'a.file\n\n' + JSON.stringify(status, null, 2) + '\n', + err: '', + exit: 0, + }); + }); + + test.each(['--status-compact', '-S'])( + '%s prints the status report on one line, with no line break in front', + (flag) => { + fs.writeFileSync('a.file', 'foo'); + const result = runCli('o', 'x', 'a.file', flag, '--quiet-total'); + expect(result.out).toBe( + '{"files":{"searched":["a.file"],"updated":["a.file"]},"pipe":null,"warnings":[],"errors":[]}\n' + ); + } + ); + + test('sets the exit code to 1 when a file gives an error, and prints the status report', () => { + const result = runCli('o', 'x', 'nope.file', '--status-compact', '--quiet-total'); + expect(JSON.parse(result.out).errors).toEqual(['0 files found']); + expect(result.exit).toBe(1); + }); + + test('ends the process on a panic, and still prints the status report', () => { + fs.writeFileSync('a.file', 'foo'); + const result = runCli('(', 'x', 'a.file', '--status-compact', '--quiet-total'); + expect(JSON.parse(result.out).errors).toHaveLength(1); + expect(result.exit).toBe(1); + }); +}); diff --git a/test/js/helper.js b/test/js/helper.js index 0528a5c7..cec856e2 100644 --- a/test/js/helper.js +++ b/test/js/helper.js @@ -21,11 +21,20 @@ afterEach(() => { }); // Run the engine in this process (no CLI involved) with the defaults and the `€` and `§` aliases the CLI would give it. -// Gives what was sent to stdout (`out`) and stderr (`err`) plus the exit code if the engine wanted to stop the process (`exit`). +// Gives what was sent to stdout (`out`) and stderr (`err`) plus the exit code the CLI would give (`exit`). // Files are handled one at a time unless `voidAsync: false` is given, so the output comes in the same order on every run. export async function rr(pattern, replacement, config = {}) { + return (await run(pattern, replacement, config)).result; +} + +// The same run as rr(), but gives the status the engine resolves with (or the status of a panic) +export async function rrStatus(pattern, replacement, config = {}) { + return (await run(pattern, replacement, config)).status; +} + +async function run(pattern, replacement, config) { const result = {out: '', err: '', exit: null}; - const halt = {}; + let status; const spies = [ spyOn(process.stdout, 'write').mockImplementation((data) => { @@ -39,9 +48,9 @@ export async function rr(pattern, replacement, config = {}) { result.err += data; return true; }), - spyOn(process, 'exit').mockImplementation((code) => { - result.exit = code; - throw halt; + // Only the CLI acts on the process + spyOn(process, 'exit').mockImplementation(() => { + throw new Error('The engine must not end the process'); }), ]; @@ -59,19 +68,23 @@ export async function rr(pattern, replacement, config = {}) { ...config, }; alias(options); + status = await engine(options); // Like the CLI, an error gives the exit code 1 when the run is done - if (await engine(options)) { + if (status.errors.length) { result.exit = 1; } } catch (e) { - if (e !== halt) { + // Like the CLI, a panic stops the run with the exit code 1 + if (!e?.panic) { throw e; } + status = e.status; + result.exit = 1; } finally { spies.forEach((spy) => spy.mockRestore()); } - return result; + return {result, status}; } // What the content of the files gives by default: a line break after each item, unless it already ends with one diff --git a/test/js/output.test.js b/test/js/output.test.js index f948c923..4aef5514 100644 --- a/test/js/output.test.js +++ b/test/js/output.test.js @@ -61,9 +61,9 @@ describe('--output', () => { expect((await rr('o', 'x', {...config, quietTotal: true})).out).toBe('fxxbar\n'); }); - test('with --verbose and --debug the chatter goes to stderr only', async () => { + test('with --verbose the chatter goes to stderr only', async () => { fs.writeFileSync('my.file', 'foobar'); - const config = {includeGlob: ['my.file'], output: true, verbose: true, debug: true}; + const config = {includeGlob: ['my.file'], output: true, verbose: true}; const result = await rr('o', 'x', config); expect(result.out).toBe('fxxbar\n'); expect(result.err).toContain('Output result from: my.file'); diff --git a/test/js/quiet-total.test.js b/test/js/quiet-total.test.js index fdd01d50..379be139 100644 --- a/test/js/quiet-total.test.js +++ b/test/js/quiet-total.test.js @@ -30,15 +30,10 @@ describe('--quiet-total', () => { }); describe('combined', () => { - test('with --quiet and --verbose it stays silent', async () => { + test('with --verbose it stays silent, as --quiet-total wins', async () => { fs.writeFileSync('my.file', 'foobar'); - const config = {includeGlob: ['my.file'], quietTotal: true, quiet: true, verbose: true}; - expect((await rr('o', 'x', config)).err).toBe(''); - }); - - test('with --debug the debug info is still shown', async () => { - const result = await rr('o', 'x', {pipedData: 'foobar', quietTotal: true, debug: true}); - expect(result.err).toContain('Work on content from: Piped data'); + const config = {includeGlob: ['my.file'], quietTotal: true, verbose: true}; + expect(await rr('o', 'x', config)).toEqual({out: '', err: '', exit: null}); }); test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async (flag) => { diff --git a/test/js/quiet.test.js b/test/js/quiet.test.js index 93878e3d..95539ae5 100644 --- a/test/js/quiet.test.js +++ b/test/js/quiet.test.js @@ -31,18 +31,15 @@ describe('--quiet', () => { }); describe('combined', () => { - test('with --verbose it stays quiet', async () => { + test('with --verbose the chatter is still shown, as it was asked for', async () => { fs.writeFileSync('my.file', 'foobar'); - expect( - (await rr('o', 'x', {includeGlob: ['my.file'], quiet: true, verbose: true})).err - ).toBe(''); - }); - - test('with --debug the debug info is still shown', async () => { - fs.writeFileSync('my.file', 'foobar'); - const result = await rr('o', 'x', {includeGlob: ['my.file'], quiet: true, debug: true}); - expect(result.err).toContain('Work on content from: my.file'); + const result = await rr('o', 'x', { + includeGlob: ['my.file'], + quiet: true, + verbose: true, + }); expect(result.out).toBe(''); + expect(result.err).toContain('Open sync: my.file'); }); test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async (flag) => { diff --git a/test/js/verbose.test.js b/test/js/verbose.test.js index 02c438e3..8bce918d 100644 --- a/test/js/verbose.test.js +++ b/test/js/verbose.test.js @@ -6,7 +6,9 @@ describe('--verbose', () => { test('tells what is going on', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', {includeGlob: ['my.file'], verbose: true}); - expect(result.err).toBe('1 files found\nOpen sync: my.file\n'); + expect(result.err).toBe( + '1 files found\nOpen sync: my.file\nWork on content from: my.file\nWrite new content to: my.file\n' + ); expect(result.out).toBe('my.file\n'); }); @@ -45,26 +47,16 @@ describe('--verbose', () => { }); describe('combined', () => { - test('with --quiet or --quiet-total nothing is told', async () => { + test('is not hidden by --quiet, as it was asked for', async () => { fs.writeFileSync('my.file', 'foobar'); - const config = {includeGlob: ['my.file'], verbose: true}; - expect((await rr('o', 'x', {...config, quiet: true})).err).toBe(''); - expect((await rr('x', 'o', {...config, quietTotal: true})).err).toBe(''); - }); - - test('with --debug every step and the full config is shown', async () => { - const result = await rr('o', 'x', {pipedData: 'foobar', verbose: true, debug: true}); - expect(result.err).toContain('"Displaying steps for:"'); - expect(result.err).toContain('"pattern": "o"'); - // The tests are run by Bun, and Bun has JSC - expect(result.err).toContain('"Get final regex with engine: JSC"'); - expect(result.err).toContain('"gim"'); - expect(result.out).toBe('fxxbar'); + const config = {includeGlob: ['my.file'], verbose: true, quiet: true}; + expect((await rr('o', 'x', config)).err).toContain('Open sync: my.file'); }); - test('with --debug the steps are not shown without --verbose', async () => { - const result = await rr('o', 'x', {pipedData: 'foobar', debug: true}); - expect(result.err).not.toContain('Displaying steps for'); + test('is hidden by --quiet-total, as it wins', async () => { + fs.writeFileSync('my.file', 'foobar'); + const config = {includeGlob: ['my.file'], verbose: true, quietTotal: true}; + expect((await rr('o', 'x', config)).err).toBe(''); }); test('with --output the result is still clean', async () => { @@ -75,7 +67,9 @@ describe('--verbose', () => { output: true, }); expect(result.out).toBe('fxxbar\n'); - expect(result.err).toBe('1 files found\nOpen sync: my.file\nmy.file\n'); + expect(result.err).toBe( + '1 files found\nOpen sync: my.file\nWork on content from: my.file\nOutput result from: my.file\nmy.file\n' + ); }); test('with --replacement-js piped data counts as used', async () => { diff --git a/test/js/void-async.test.js b/test/js/void-async.test.js index 26dd7690..df3163f3 100644 --- a/test/js/void-async.test.js +++ b/test/js/void-async.test.js @@ -7,32 +7,32 @@ import {rr} from './helper.js'; describe('--void-async', () => { test('is done with a file before the next one is opened', async () => { ['a', 'b'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); - const config = {includeGlob: ['*.file'], voidAsync: true, debug: true}; + const config = {includeGlob: ['*.file'], voidAsync: true, verbose: true}; const result = await rr('o', 'x', config); expect(result.err.trim().split('\n')).toEqual([ - '"2 files found"', - '"Open sync: a.file"', - '"Work on content from: a.file"', - '"Write new content to: a.file"', - '"Open sync: b.file"', - '"Work on content from: b.file"', - '"Write new content to: b.file"', + '2 files found', + 'Open sync: a.file', + 'Work on content from: a.file', + 'Write new content to: a.file', + 'Open sync: b.file', + 'Work on content from: b.file', + 'Write new content to: b.file', ]); expect(result.out).toBe('a.file\nb.file\n'); }); test('is not the default: all files are opened before the first one is done', async () => { ['a', 'b'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); - const config = {includeGlob: ['*.file'], voidAsync: false, debug: true}; + const config = {includeGlob: ['*.file'], voidAsync: false, verbose: true}; const result = await rr('o', 'x', config); const lines = result.err.split('\n'); expect(lines.slice(0, 3)).toEqual([ - '"2 files found"', - '"Open async: a.file"', - '"Open async: b.file"', + '2 files found', + 'Open async: a.file', + 'Open async: b.file', ]); // Either file can be read first - expect(lines[3]).toMatch(/^"Work on content from: [ab]\.file"$/); + expect(lines[3]).toMatch(/^Work on content from: [ab]\.file$/); expect(result.out.split('\n').sort()).toEqual(['', 'a.file', 'b.file']); expect(['a', 'b'].map((name) => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ 'fxx', From 9329424d80e910f9994eea73368e684b5e53905e Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:10:31 +1100 Subject: [PATCH 2/9] Keep file globs that read as numbers as strings to close #733 --- src/cli.ts | 2 ++ test/js/basics.test.js | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index bd68b44b..a9573c9f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,6 +26,8 @@ if (process.argv.length < 4) { } const yargs = require('yargs') + // A file can be named with only digits, like `2024`, so each glob is kept as the string that was given + .parserConfiguration({'parse-positional-numbers': false}) .strict() .usage( diff --git a/test/js/basics.test.js b/test/js/basics.test.js index 496895fe..bcde49e4 100644 --- a/test/js/basics.test.js +++ b/test/js/basics.test.js @@ -182,15 +182,25 @@ describe('files', () => { }); // The file names are read by src/cli.ts, so this runs the CLI itself - test.todo('BUG #733: can have a name that is only a number', async () => { - await Bun.write('2024', 'foobar'); - const cli = path.join(import.meta.dir, '../../src/cli.ts'); - const {stdout, exitCode} = Bun.spawnSync([process.execPath, cli, 'o', 'x', '2024'], { - stdin: 'ignore', - }); - expect({out: stdout.toString(), exit: exitCode}).toEqual({out: '2024\n', exit: 0}); - expect(await Bun.file('2024').text()).toBe('fxxbar'); - }); + test.each(['2024', '1e3', '0x10'])( + 'can have a name that reads as a number, like %s (#733)', + async (name) => { + await Bun.write(name, 'foobar'); + const cli = path.join(import.meta.dir, '../../src/cli.ts'); + const {stdout, stderr, exitCode} = Bun.spawnSync( + [process.execPath, cli, 'o', 'x', name], + { + stdin: 'ignore', + } + ); + expect({out: stdout.toString(), err: stderr.toString(), exit: exitCode}).toEqual({ + out: name + '\n', + err: '', + exit: 0, + }); + expect(await Bun.file(name).text()).toBe('fxxbar'); + } + ); test('are reported if none are found', async () => { const result = await rr('o', 'x', {includeGlob: ['nope.file']}); From eaed1d0c51c91bfb29d94e6ef300ca6684fbe445 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:12:17 +1100 Subject: [PATCH 3/9] Only escape the regex chars that have a meaning with --literal to close #686 --- src/engine.ts | 3 ++- test/js/literal.test.js | 18 ++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 7708c470..f802208f 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -14,7 +14,8 @@ const re = { mctime: /[mc]time/, colon: /:/g, capturedGroupRef: /\$\d/, - regexSpecialChars: /[-\[\]{}()*+?.,\/\\^$|#\s]/g, + // Only the chars with a meaning outside a character class. Unicode mode does not allow to escape the others (#686). + regexSpecialChars: /[\[\]{}()*+?.\/\\^$|]/g, byteOrSize: /bytes|size/, folderName: /[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/, }; diff --git a/test/js/literal.test.js b/test/js/literal.test.js index 8c5152a4..b90fade9 100644 --- a/test/js/literal.test.js +++ b/test/js/literal.test.js @@ -83,18 +83,12 @@ describe('--literal', () => { expect(fs.readFileSync('axfile', 'utf8')).toBe('fxxbar'); }); - test.todo( - 'BUG #686: with --unicode a pattern containing - , # or whitespace is rejected as invalid', - async () => { - for (const char of '-,# ') { - const result = await rr(`a${char}b`, 'x', { - pipedData: `a${char}b`, - literal: true, - unicode: true, - }); - expect(result.exit, char).toBe(null); - expect(result.out, char).toBe('x'); - } + test.each([...'-,# []{}()*+?./\\^$|'])( + 'with --unicode finds a pattern with %j in it (#686)', + async (char) => { + const pattern = `a${char}b`; + const config = {pipedData: pattern, literal: true, unicode: true}; + expect(await rr(pattern, 'x', config)).toEqual({out: 'x', err: '', exit: null}); } ); }); From ab037854e7c12709315165942120a43c874a55f6 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:13:59 +1100 Subject: [PATCH 4/9] Give size as 0 Bytes for empty content in --replacement-js to close #687 --- src/engine.ts | 3 ++- test/js/replacement-js.test.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index f802208f..c9b4d2e4 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -430,7 +430,8 @@ function readableSize(size) { if (1 === size) { return '1 Byte'; } - const i = Math.floor(Math.log(size) / Math.log(1024)); + // 0 has no logarithm, so it is counted in bytes + const i = size ? Math.floor(Math.log(size) / Math.log(1024)) : 0; return ( (size / Math.pow(1024, i)).toFixed(!!i ? 1 : 0) + ' ' + ['Bytes', 'KB', 'MB', 'GB', 'TB'][i] ); diff --git a/test/js/replacement-js.test.js b/test/js/replacement-js.test.js index c9fcf852..a3f045da 100644 --- a/test/js/replacement-js.test.js +++ b/test/js/replacement-js.test.js @@ -194,7 +194,7 @@ describe('--replacement-js', () => { expect(await js('foobar', 'bytes + "|" + size', 'foobar')).toBe('6|6 Bytes'); }); - test.todo('BUG #687: size of empty content is "0 Bytes"', async () => { + test('size of empty content is "0 Bytes" (#687)', async () => { expect(await inFile('bytes + "|" + size', '')).toBe('0|0 Bytes'); }); From 7981cec517c4339b5db31d8c26a94e66f742a639 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:15:42 +1100 Subject: [PATCH 5/9] Count bytes and size in the encoding of the file in --replacement-js to close #688 --- src/engine.ts | 3 ++- test/js/encoding.test.js | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index c9b4d2e4..9446cc37 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -563,7 +563,8 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { } if (needsByteOrSize && -1 === _bytes) { - _bytes = Buffer.from(_text).length; + // The size in the encoding of the file, not of the text as utf8 + _bytes = Buffer.byteLength(_text, _config_rr.encoding); _size = readableSize(_bytes); } diff --git a/test/js/encoding.test.js b/test/js/encoding.test.js index 8fc75d70..d6d7d53d 100644 --- a/test/js/encoding.test.js +++ b/test/js/encoding.test.js @@ -125,18 +125,15 @@ describe('--encoding', () => { expect((await rr('æ', 'text.length + text', config)).out).toBe('f3fæoo\n'); }); - test.todo( - 'BUG #688: with --replacement-js `bytes` is the size of the file in its encoding', - async () => { - fs.writeFileSync('my.file', latin1()); - const config = { - includeGlob: ['my.file'], - encoding: 'latin1', - replacementJs: true, - output: true, - }; - expect((await rr('^', 'bytes + ":"', config)).out).toBe('3:fæo'); - } - ); + test('with --replacement-js `bytes` and `size` are the size of the file in its encoding (#688)', async () => { + fs.writeFileSync('my.file', latin1()); + const config = { + includeGlob: ['my.file'], + encoding: 'latin1', + replacementJs: true, + output: true, + }; + expect((await rr('^', 'bytes + ":" + size + ":"', config)).out).toBe('3:3 Bytes:fæo\n'); + }); }); }); From 0d4530f27731be75e412ff0c60ab845d86a1c87c Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 05:25:26 +1100 Subject: [PATCH 6/9] Insert the result of --replacement-js as it is to close #689 --- src/engine.ts | 10 ++++++---- test/js/replacement-js.test.js | 11 ++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 9446cc37..16feff68 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -179,10 +179,10 @@ function openFile(file, config) { function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) { chat('Work on content from: ' + _file_rr); - // Main regexp of the whole thing + // Main regexp of the whole thing. With --output-match the replacement prints the matches, so the code of --replacement-js is not run. const result = _data_rr.replace( _config_rr.regex, - _config_rr.replacementJs + _config_rr.replacementJs && !_config_rr.outputMatch ? dynamicReplacement(_file_rr, _config_rr, _data_rr) : _config_rr.replacement ); @@ -568,9 +568,10 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { _size = readableSize(_bytes); } - // Run only once if no captured groups (replacement cant change) + // Run only once if no captured groups (replacement cant change). + // The result is given back from a function, so `$&` and `$$` in it are inserted as they are and not expanded. if (!/\$\d/.test(_config_rr.replacement)) { - return dynamicContent( + const result = dynamicContent( require, fs, globs, @@ -619,6 +620,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { _, code_rr ); + return () => result; } // Capture groups used, so need to run once per match return function () { diff --git a/test/js/replacement-js.test.js b/test/js/replacement-js.test.js index a3f045da..2bead76f 100644 --- a/test/js/replacement-js.test.js +++ b/test/js/replacement-js.test.js @@ -269,13 +269,10 @@ describe('--replacement-js', () => { }); }); - test.todo( - 'BUG #689: the result of the code is inserted as it is (now $& and $$ in it are expanded)', - async () => { - expect(await js('o', '"$" + "&"', 'fo')).toBe('f$&'); - expect(await js('o', '"$" + "$"', 'fo')).toBe('f$$'); - } - ); + test('the result of the code is inserted as it is, also $& and $$ (#689)', async () => { + expect(await js('o', '"$" + "&"', 'fo')).toBe('f$&'); + expect(await js('o', '"$" + "$"', 'fo')).toBe('f$$'); + }); describe('combined', () => { test('with --void-euro there is only the $ to get to the groups', async () => { From 7cf610eb99ddd151d1f4cf7e677ea5021ff09359 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 06:03:33 +1100 Subject: [PATCH 7/9] Use a path that is a file as it is, and only glob the rest, to close #718 --- src/engine.ts | 31 ++++++++++++++++-- test/js/fix/718.test.js | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 test/js/fix/718.test.js diff --git a/src/engine.ts b/src/engine.ts index 16feff68..c395852c 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -725,7 +725,7 @@ function localTimeString(dateObj = new Date()) { function getFilePaths(conf) { let {includeGlob, excludeGlob, excludeRe} = conf; - let filesToInclude = globs.sync(includeGlob); + let filesToInclude = findFiles(includeGlob); if (excludeRe.length) { excludeRe @@ -736,8 +736,8 @@ function getFilePaths(conf) { } if (excludeGlob.length) { - const filesToExclude = globs.sync(excludeGlob); - filesToInclude = filesToInclude.filter((el) => !filesToExclude.includes(el)); + const filesToExclude = findFiles(excludeGlob).map((file) => path.resolve(file)); + filesToInclude = filesToInclude.filter((el) => !filesToExclude.includes(path.resolve(el))); } filesToInclude.sort(); @@ -745,3 +745,28 @@ function getFilePaths(conf) { // With --absolute-path each file is known by its full path from here on, so that is the name that is printed return conf.absolutePath ? filesToInclude.map((file) => path.resolve(file)) : filesToInclude; } + +// A path that is a file is used as it is, so a name with glob characters, like `a[1].md` or `my (1).md`, gives only that file. +// The other paths are globbed together, so a pattern that starts with `!` can take files away from the other patterns. +function findFiles(paths) { + const files = []; + const patterns = []; + paths.forEach((file) => { + if (fs.statSync(file, {throwIfNoEntry: false})?.isFile()) { + files.push(file); + } else { + patterns.push(file); + } + }); + + // A file given by its name and also found by a glob is only handled once + const seen = new Set(); + return files.concat(patterns.length ? globs.sync(patterns) : []).filter((file) => { + const fullPath = path.resolve(file); + if (seen.has(fullPath)) { + return false; + } + seen.add(fullPath); + return true; + }); +} diff --git a/test/js/fix/718.test.js b/test/js/fix/718.test.js new file mode 100644 index 00000000..931cd4cf --- /dev/null +++ b/test/js/fix/718.test.js @@ -0,0 +1,71 @@ +// https://github.com/mathiasrw/rexreplace/issues/718 +// Every path was treated as a glob, so a file with glob characters in its name was not found, or other files were changed with it. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {rr} from '../helper.js'; + +const content = (name) => fs.readFileSync(name, 'utf8'); + +describe('#718 a path that is a file is used as it is', () => { + test.each(['my (1).file', '(draft).file', '{a,b}.file', '!important.file', 'a+(b).file'])( + '%s is found', + async (name) => { + fs.writeFileSync(name, 'foo'); + expect(await rr('o', 'x', {includeGlob: [name]})).toEqual({ + out: name + '\n', + err: '', + exit: null, + }); + expect(content(name)).toBe('fxx'); + } + ); + + test.each([ + ['a[1].file', 'a1.file'], + ['[id].file', 'i.file'], + ['what?.file', 'whatX.file'], + ['star*.file', 'star123.file'], + ])('%s does not also change %s', async (name, other) => { + fs.writeFileSync(name, 'foo'); + fs.writeFileSync(other, 'foo'); + expect(await rr('o', 'x', {includeGlob: [name]})).toEqual({ + out: name + '\n', + err: '', + exit: null, + }); + expect(content(name)).toBe('fxx'); + expect(content(other)).toBe('foo'); + }); + + test('a path that is not a file is still a glob', async () => { + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + const result = await rr('o', 'x', {includeGlob: ['*.file']}); + expect(result.out).toBe('a1.file\na[1].file\n'); + }); + + test('a file given by its name and also found by a glob is only handled once', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + const result = await rr('o', 'x', {includeGlob: ['./a.file', '*.file']}); + expect(result).toEqual({out: './a.file\nb.file\n', err: '', exit: null}); + expect(fs.readdirSync('.').sort()).toEqual(['a.file', 'b.file']); + }); + + test('--exclude-glob also takes away a file given by its name', async () => { + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + const config = {includeGlob: ['a[1].file', 'a1.file'], excludeGlob: ['a[1].file']}; + expect((await rr('o', 'x', config)).out).toBe('a1.file\n'); + expect(content('a[1].file')).toBe('foo'); + expect(content('a1.file')).toBe('fxx'); + }); + + test('--exclude-re also takes away a file given by its name', async () => { + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + const config = {includeGlob: ['a[1].file', 'a1.file'], excludeRe: ['\\[']}; + expect((await rr('o', 'x', config)).out).toBe('a1.file\n'); + expect(content('a[1].file')).toBe('foo'); + }); +}); From eea22f9354de6aa77c5ab9869feffd667a4ae9bd Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 10:04:24 +1100 Subject: [PATCH 8/9] Use Biome for code formatting to close #743 --- .prettierignore | 4 -- .prettierrc | 6 --- biome.json | 68 +++++++++++++++++++++++++++++ package.json | 6 +-- src/alias.ts | 2 +- src/cli.ts | 12 ++--- src/engine.ts | 29 ++++++------ src/output.ts | 2 +- test/fuzzy/basics.test.js | 13 +++--- test/fuzzy/crossover.test.js | 26 +++-------- test/fuzzy/dot-all.test.js | 4 +- test/fuzzy/encoding.test.js | 8 ++-- test/fuzzy/exclude-glob.test.js | 13 ++---- test/fuzzy/exclude-re.test.js | 6 +-- test/fuzzy/keep-backup.test.js | 4 +- test/fuzzy/literal.test.js | 8 +--- test/fuzzy/output-match.test.js | 4 +- test/fuzzy/output.test.js | 6 +-- test/fuzzy/replacement-js.test.js | 6 +-- test/fuzzy/unicode.test.js | 12 +---- test/fuzzy/void-async.test.js | 6 +-- test/fuzzy/void-euro.test.js | 9 ++-- test/fuzzy/void-ignore-case.test.js | 7 ++- test/fuzzy/void-section.test.js | 8 +--- test/js/basics.test.js | 21 ++++----- test/js/crossover.test.js | 24 +++++----- test/js/debug.test.js | 2 +- test/js/encoding.test.js | 6 +-- test/js/exclude-glob.test.js | 47 ++++++++++---------- test/js/exclude-re.test.js | 17 ++++---- test/js/fix/683.test.js | 16 +++---- test/js/fix/692.test.js | 6 +-- test/js/fix/699.test.js | 47 ++++++++++---------- test/js/fix/703.test.js | 32 ++++---------- test/js/fix/705.test.js | 6 +-- test/js/fix/708.test.js | 2 +- test/js/fix/718.test.js | 4 +- test/js/fix/719.test.js | 4 +- test/js/fix/726.test.js | 2 +- test/js/fix/728.test.js | 2 +- test/js/fix/742.test.js | 6 +-- test/js/halt.test.js | 4 +- test/js/helper.js | 16 +++---- test/js/keep-backup.test.js | 23 +++++----- test/js/literal.test.js | 20 ++++----- test/js/output-match.test.js | 22 +++------- test/js/output.test.js | 2 +- test/js/quiet-total.test.js | 8 ++-- test/js/quiet.test.js | 2 +- test/js/replacement-js.test.js | 24 +++++----- test/js/replacement-pipe.test.js | 4 +- test/js/unicode.test.js | 9 ++-- test/js/verbose.test.js | 2 +- test/js/void-async.test.js | 19 ++++---- test/js/void-backup.test.js | 6 +-- test/js/void-euro.test.js | 14 +++--- test/js/void-global.test.js | 8 +--- test/js/void-ignore-case.test.js | 4 +- test/js/void-multiline.test.js | 4 +- test/js/void-section.test.js | 7 +-- yarn.lock | 59 ++++++++++++++++++++++--- 61 files changed, 383 insertions(+), 387 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 biome.json diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d70c9e6c..00000000 --- a/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -dist/ -bin/ -*.md -*.html \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ec3b45c5..00000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -printWidth: 100 -tabWidth: 4 -useTabs: true -bracketSpacing: false -trailingComma: es5 -singleQuote: true diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..143671f1 --- /dev/null +++ b/biome.json @@ -0,0 +1,68 @@ +{ + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + }, + "formatter": { + "indentStyle": "tab", + "lineWidth": 100, + "bracketSpacing": false + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "arrowParentheses": "asNeeded" + } + }, + "json": { + "formatter": {"indentStyle": "tab"} + }, + "linter": { + "enabled": true, + "includes": ["**"], + "rules": { + "correctness": { + "preset": "recommended" + }, + "suspicious": { + "preset": "recommended", + "noApproximativeNumericConstant": "off", + "noExplicitAny": "off" + }, + "style": { + "preset": "recommended", + "useTemplate": "off" + }, + "complexity": { + "preset": "recommended" + } + } + }, + "overrides": [ + { + "includes": ["test/**"], + "linter": { + "rules": { + "correctness": { + "noUnusedVariables": "off", + "noUnusedImports": "off", + "noUnusedFunctionParameters": "off" + }, + "suspicious": { + "noImplicitAnyLet": "off" + }, + "complexity": { + "useLiteralKeys": "off" + } + } + } + } + ], + "files": { + "includes": ["**", "!**/dist", "!**/bin", "!**/node_modules", "!**/*.html"] + } +} diff --git a/package.json b/package.json index efd55047..d9af7bc0 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "test-speed": "bash test/speed/run.sh", "rebase": "yarn is-git-clean && git fetch && git rebase origin/main", "load-options": "# rr -h | rr 'Options:(.+)Examples:' _ -ms | rr '\\n {26,}|\\n\\n *' ' ' | rr \"'\" '`' | rr '^ (-.+?), (--[^ ]+) *' '`€1` | **`€2`** ' | rr '(^---- . ----).+?(## Good to know)' '€1 + nl + pipe + nl + nl + €2' readme.md -jspT", - "test-format": "prettier . --list-different || (echo 'Please correct file formatting using `yarn format` and try again.' && exit 1)", - "format": "prettier . --write", + "test-format": "biome format || (echo 'Please correct file formatting using `yarn format` and try again.' && exit 1)", + "format": "biome format --write", "is-git-clean": "(git diff --quiet --exit-code --cached && git diff --quiet --exit-code) || (echo Please commit or stash changes && exit 1)", "release": "yarn version", "prerelease": "yarn is-git-clean && yarn test && yarn test-minify && yarn load-options", @@ -60,9 +60,9 @@ "sed" ], "devDependencies": { + "@biomejs/biome": "2.5.15", "@types/node": "24.19.1", "assert": "^2.0.0", - "prettier": "3.9.9", "yarn": "1.22.22" }, "resolutions": { diff --git a/src/alias.ts b/src/alias.ts index a362b791..e872b519 100644 --- a/src/alias.ts +++ b/src/alias.ts @@ -1,7 +1,7 @@ // The `€` and `§` aliases are for the shell, where `$` and `\` are hard to type, so the CLI uses them and not the engine. // Globs, file names and piped data never use them, as `€` and `§` can be part of the text. export function alias(config) { - const swap = (str) => { + const swap = str => { if (!str) { return str; } diff --git a/src/cli.ts b/src/cli.ts index a9573c9f..4cebd6c5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -96,7 +96,7 @@ const yargs = require('yargs') .alias('e', 'encoding') .describe('e', 'Encoding of files/piped data.') // Not .choices() as node takes the name of an encoding in any case, like `UTF8` - .check((argv) => Buffer.isEncoding(argv.encoding) || `Unknown encoding: ${argv.encoding}`) + .check(argv => Buffer.isEncoding(argv.encoding) || `Unknown encoding: ${argv.encoding}`) .string('x') .describe( @@ -458,7 +458,7 @@ const lineBreakEscapes = {'\n': '\\n', '\r': '\\r', '\u2028': '\\u2028', '\u2029 // Let javascript interpret escape sequences (like `\n` or `\x41`) by evaluating the replacement as the content of a single quoted string. // Anything that would end the string or break the source must be neutralised first: unescaped single quotes, real line breaks and a lone backslash at the end. function unescapeString(str = '') { - const source = str.replace(/\\(?:\r\n|[\s\S])?|['\n\r\u2028\u2029]/g, (match) => { + const source = str.replace(/\\(?:\r\n|[\s\S])?|['\n\r\u2028\u2029]/g, match => { if ('\\' === match) { return '\\\\'; } @@ -479,7 +479,7 @@ if (0 < needHelp) { const config: any = {}; // Use only camelCase full length version of settings so we make sure the core can be documented properly -Object.keys(yargs.argv).forEach((key) => { +Object.keys(yargs.argv).forEach(key => { if (1 < key.length && key.indexOf('-') < 0) { config[key] = yargs.argv[key]; } @@ -544,13 +544,13 @@ if (config.contentDelimiter) { // The exit code is only set, so the process ends by itself when all output is written const run = () => rexreplace.engine(config).then( - (status) => { + status => { printStatus(config, status); if (status.errors.length) { process.exitCode = 1; } }, - (e) => exitOnPanic(config, e) + e => exitOnPanic(config, e) ); // Like grep, stdin is only read when piped data is expected. An open pipe that nobody writes to would make the run wait forever. @@ -564,7 +564,7 @@ if (!pipeDataExpected || Boolean(process.stdin.isTTY)) { process.stdin.setEncoding(config.encoding); - process.stdin.on('data', (chunk) => { + process.stdin.on('data', chunk => { pipeData = (pipeData ?? '') + chunk; }); diff --git a/src/engine.ts b/src/engine.ts index c395852c..3e4895f7 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -99,8 +99,7 @@ async function run(config) { search: config.regex.source, flags: config.regex.flags, replacement: config.replacementJs || config.outputMatch ? null : config.replacement, - replacementSrc: - config.replacementJs && !config.outputMatch ? config.replacementOri : null, + replacementSrc: config.replacementJs && !config.outputMatch ? config.replacementOri : null, }; } @@ -135,7 +134,7 @@ async function run(config) { config.voidAsync = config.voidAsync || 1 === config.files.length || config.output || config.outputMatch; - const files = config.files.filter((filepath) => + const files = config.files.filter(filepath => fs.existsSync(filepath) ? true : error('File not found:', filepath) ); @@ -171,7 +170,7 @@ function openFile(file, config) { chat('Open async: ' + file); return fs.promises .readFile(file, config.encoding) - .then((data) => doReplacement(file, config, data), error); + .then(data => doReplacement(file, config, data), error); } } @@ -449,7 +448,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { _ = ' ', _nl = '\n'; - // prettier-ignore + // biome-ignore format: one variable on each line let _file = '❌', _file_rel = '❌', _dirpath = '❌', @@ -715,9 +714,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { function localTimeString(dateObj = new Date()) { return `${dateObj.getFullYear()}-${('0' + (dateObj.getMonth() + 1)).slice(-2)}-${( '0' + dateObj.getDate() - ).slice( - -2 - )} ${('0' + dateObj.getHours()).slice(-2)}:${('0' + dateObj.getMinutes()).slice(-2)}:${( + ).slice(-2)} ${('0' + dateObj.getHours()).slice(-2)}:${('0' + dateObj.getMinutes()).slice(-2)}:${( '0' + dateObj.getSeconds() ).slice(-2)}.${('00' + dateObj.getMilliseconds()).slice(-3)}`; } @@ -729,21 +726,21 @@ function getFilePaths(conf) { if (excludeRe.length) { excludeRe - .map((el) => getFinalRegex(getFinalPattern(el, conf), conf)) - .forEach((re) => { - filesToInclude = filesToInclude.filter((el) => !el.match(re)); + .map(el => getFinalRegex(getFinalPattern(el, conf), conf)) + .forEach(re => { + filesToInclude = filesToInclude.filter(el => !el.match(re)); }); } if (excludeGlob.length) { - const filesToExclude = findFiles(excludeGlob).map((file) => path.resolve(file)); - filesToInclude = filesToInclude.filter((el) => !filesToExclude.includes(path.resolve(el))); + const filesToExclude = findFiles(excludeGlob).map(file => path.resolve(file)); + filesToInclude = filesToInclude.filter(el => !filesToExclude.includes(path.resolve(el))); } filesToInclude.sort(); // With --absolute-path each file is known by its full path from here on, so that is the name that is printed - return conf.absolutePath ? filesToInclude.map((file) => path.resolve(file)) : filesToInclude; + return conf.absolutePath ? filesToInclude.map(file => path.resolve(file)) : filesToInclude; } // A path that is a file is used as it is, so a name with glob characters, like `a[1].md` or `my (1).md`, gives only that file. @@ -751,7 +748,7 @@ function getFilePaths(conf) { function findFiles(paths) { const files = []; const patterns = []; - paths.forEach((file) => { + paths.forEach(file => { if (fs.statSync(file, {throwIfNoEntry: false})?.isFile()) { files.push(file); } else { @@ -761,7 +758,7 @@ function findFiles(paths) { // A file given by its name and also found by a glob is only handled once const seen = new Set(); - return files.concat(patterns.length ? globs.sync(patterns) : []).filter((file) => { + return files.concat(patterns.length ? globs.sync(patterns) : []).filter(file => { const fullPath = path.resolve(file); if (seen.has(fullPath)) { return false; diff --git a/src/output.ts b/src/output.ts index 4e3bb648..985cd895 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,5 +1,5 @@ let font: any = {}; -font.red = font.green = font.gray = (str) => str; +font.red = font.green = font.gray = str => str; // check for node version supporting chalk - if so overwrite `font` //const font = import('chalk'); diff --git a/test/fuzzy/basics.test.js b/test/fuzzy/basics.test.js index dca96c54..1187c927 100644 --- a/test/fuzzy/basics.test.js +++ b/test/fuzzy/basics.test.js @@ -34,15 +34,14 @@ describe('basics', () => { const expected = (await rr(pattern, replacement, {pipedData: text})).out; fs.writeFileSync('my.file', text); - expect( - (await rr(pattern, replacement, {includeGlob: ['my.file'], output: true})).out - ).toBe(ended(expected)); + expect((await rr(pattern, replacement, {includeGlob: ['my.file'], output: true})).out).toBe( + ended(expected) + ); await rr(pattern, replacement, {includeGlob: ['my.file']}); - expect( - fs.readFileSync('my.file', 'utf8'), - JSON.stringify({pattern, replacement, text}) - ).toBe(expected); + expect(fs.readFileSync('my.file', 'utf8'), JSON.stringify({pattern, replacement, text})).toBe( + expected + ); } }); }); diff --git a/test/fuzzy/crossover.test.js b/test/fuzzy/crossover.test.js index 6ce16f1e..e87eca20 100644 --- a/test/fuzzy/crossover.test.js +++ b/test/fuzzy/crossover.test.js @@ -31,7 +31,7 @@ describe('crossover', () => { test('any combination of flags', async () => { const rnd = random(1); for (let i = 0; i < 300; i++) { - const config = Object.fromEntries(flags.map((flag) => [flag, 0 === rnd.int(3)])); + const config = Object.fromEntries(flags.map(flag => [flag, 0 === rnd.int(3)])); const pattern = rnd.pick(patterns); const text = rnd.text('abAB \næ.*+', 0, 30); const replacement = rnd.text('xy \n<>&', 0, 3); @@ -51,7 +51,7 @@ describe('crossover', () => { const matches = ended( ...(config.voidGlobal ? [text.match(regex)] : Array.from(text.matchAll(regex))) .filter(Boolean) - .map((match) => (1 < match.length ? match.slice(1).join('') : match[0])) + .map(match => (1 < match.length ? match.slice(1).join('') : match[0])) ); const given = config.replacementJs ? JSON.stringify(replacement) : replacement; @@ -70,18 +70,11 @@ describe('crossover', () => { // The name of the file is only printed if the file was changed const named = replaced !== text && !config.quiet && !config.quietTotal; expect(result.out, note).toBe( - config.outputMatch - ? matches - : config.output - ? ended(replaced) - : named - ? file + '\n' - : '' + config.outputMatch ? matches : config.output ? ended(replaced) : named ? file + '\n' : '' ); const expected = written ? replaced : text; - const files = - written && replaced !== text && config.keepBackup && !config.voidBackup ? 2 : 1; + const files = written && replaced !== text && config.keepBackup && !config.voidBackup ? 2 : 1; expect(fs.readFileSync(file, 'utf8'), note).toBe(expected); expect(fs.readdirSync(`run${i}`), note).toHaveLength(files); @@ -92,14 +85,9 @@ describe('crossover', () => { const rnd = random(2); for (let i = 0; i < 200; i++) { const config = Object.fromEntries( - [ - 'literal', - 'voidIgnoreCase', - 'voidGlobal', - 'dotAll', - 'voidMultiline', - 'unicode', - ].map((flag) => [flag, 0 === rnd.int(2)]) + ['literal', 'voidIgnoreCase', 'voidGlobal', 'dotAll', 'voidMultiline', 'unicode'].map( + flag => [flag, 0 === rnd.int(2)] + ) ); const pattern = rnd.pick(patterns); const text = rnd.text('abAB \r\næ中.*+', 0, 40); diff --git a/test/fuzzy/dot-all.test.js b/test/fuzzy/dot-all.test.js index 37352def..d8c15bb0 100644 --- a/test/fuzzy/dot-all.test.js +++ b/test/fuzzy/dot-all.test.js @@ -16,9 +16,7 @@ describe('--dot-all', () => { continue; } - expect((await rr(pattern, '_', {pipedData: text})).out, note).toBe( - text.replace(regex, '_') - ); + expect((await rr(pattern, '_', {pipedData: text})).out, note).toBe(text.replace(regex, '_')); expect((await rr(pattern, '_', {pipedData: text, dotAll: true})).out, note).toBe( text.replace(new RegExp(pattern, 'gims'), '_') ); diff --git a/test/fuzzy/encoding.test.js b/test/fuzzy/encoding.test.js index 488020e5..ad67345e 100644 --- a/test/fuzzy/encoding.test.js +++ b/test/fuzzy/encoding.test.js @@ -10,20 +10,18 @@ describe('--encoding', () => { const junk = () => Buffer.from( Array.from({length: rnd.int(40)}, () => rnd.int(255)).filter( - (byte) => !'qQzZ'.includes(String.fromCharCode(byte)) + byte => !'qQzZ'.includes(String.fromCharCode(byte)) ) ); const [before, after] = [junk(), junk()]; fs.writeFileSync('my.file', Buffer.concat([before, Buffer.from('qzq'), after])); await rr('qzq', 'ok', {includeGlob: ['my.file'], encoding: 'latin1'}); - expect(fs.readFileSync('my.file')).toEqual( - Buffer.concat([before, Buffer.from('ok'), after]) - ); + expect(fs.readFileSync('my.file')).toEqual(Buffer.concat([before, Buffer.from('ok'), after])); } }); - test.each(['utf8', 'utf16le'])('%s keeps any unicode text intact', async (encoding) => { + test.each(['utf8', 'utf16le'])('%s keeps any unicode text intact', async encoding => { const rnd = random(2); const alphabet = ['a', 'b', ' ', '\n', 'æ', 'ß', '中', '🦖', '👍🏽', 'é', '​', '€']; for (let i = 0; i < 200; i++) { diff --git a/test/fuzzy/exclude-glob.test.js b/test/fuzzy/exclude-glob.test.js index e8458fe9..1d637729 100644 --- a/test/fuzzy/exclude-glob.test.js +++ b/test/fuzzy/exclude-glob.test.js @@ -4,12 +4,12 @@ import {rr, random} from '../js/helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => { + names.forEach(name => { fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); fs.writeFileSync(name, 'foo'); }); await rr('foo', 'x', config); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; describe('--exclude-glob', () => { @@ -17,12 +17,7 @@ describe('--exclude-glob', () => { const rnd = random(1); for (let i = 0; i < 100; i++) { const names = Array.from( - new Set( - Array.from( - {length: 1 + rnd.int(7)}, - () => `run${i}/${rnd.text('abc', 1, 4)}.file` - ) - ) + new Set(Array.from({length: 1 + rnd.int(7)}, () => `run${i}/${rnd.text('abc', 1, 4)}.file`)) ); const [glob, regex] = rnd.pick([ ['a*', /\/a[^/]*$/], @@ -33,7 +28,7 @@ describe('--exclude-glob', () => { ['*c*', /\/[^/]*c[^/]*$/], ['{a,bb}.file', /\/(a|bb)\.file$/], ]); - const expected = names.filter((name) => !regex.test(name)).sort(); + const expected = names.filter(name => !regex.test(name)).sort(); const config = { includeGlob: [`run${i}/*`], excludeGlob: [`run${i}/${glob}`], diff --git a/test/fuzzy/exclude-re.test.js b/test/fuzzy/exclude-re.test.js index 05f74239..9b672320 100644 --- a/test/fuzzy/exclude-re.test.js +++ b/test/fuzzy/exclude-re.test.js @@ -4,12 +4,12 @@ import {rr, random} from '../js/helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => { + names.forEach(name => { fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); fs.writeFileSync(name, 'foo'); }); await rr('foo', 'x', config); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; describe('--exclude-re', () => { @@ -28,7 +28,7 @@ describe('--exclude-re', () => { const exclude = rnd.pick(['a', 'b$', '/c', 'a.*b', '^run\\d+/a', 'bb|cc', '\\.file$']); const voidIgnoreCase = !rnd.int(1); const regex = new RegExp(exclude, voidIgnoreCase ? '' : 'i'); - const expected = names.filter((name) => !regex.test(name)).sort(); + const expected = names.filter(name => !regex.test(name)).sort(); const config = { includeGlob: [`run${i}/*`], excludeRe: [exclude], diff --git a/test/fuzzy/keep-backup.test.js b/test/fuzzy/keep-backup.test.js index 520163c7..193190b2 100644 --- a/test/fuzzy/keep-backup.test.js +++ b/test/fuzzy/keep-backup.test.js @@ -2,7 +2,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr, random} from '../js/helper.js'; -const backups = (dir = '.') => fs.readdirSync(dir).filter((name) => name.endsWith('.backup')); +const backups = (dir = '.') => fs.readdirSync(dir).filter(name => name.endsWith('.backup')); describe('--keep-backup', () => { test('the backup is always the original and the file is always the result', async () => { @@ -19,7 +19,7 @@ describe('--keep-backup', () => { expect(fs.readFileSync(`run${i}/my.file`, 'utf8'), note).toBe(expected); expect(backups('run' + i), note).toHaveLength(expected === text ? 0 : 1); - backups('run' + i).forEach((name) => + backups('run' + i).forEach(name => expect(fs.readFileSync(`run${i}/${name}`, 'utf8'), note).toBe(text) ); } diff --git a/test/fuzzy/literal.test.js b/test/fuzzy/literal.test.js index 8bb7ba18..7741d527 100644 --- a/test/fuzzy/literal.test.js +++ b/test/fuzzy/literal.test.js @@ -9,9 +9,7 @@ describe('--literal', () => { for (let i = 0; i < 500; i++) { const pattern = rnd.text(alphabet, 1, 6); const replacement = rnd.text('xy\'"\\\n ', 0, 3); - const text = Array.from({length: rnd.int(5)}, () => rnd.text(alphabet, 0, 8)).join( - pattern - ); + const text = Array.from({length: rnd.int(5)}, () => rnd.text(alphabet, 0, 8)).join(pattern); const config = { pipedData: text, literal: true, @@ -32,9 +30,7 @@ describe('--literal', () => { for (let i = 0; i < 300; i++) { const pattern = rnd.text('.*+?()[]{}|^$\\', 1, 5); const text = rnd.text('abc \n', 0, 30); - expect((await rr(pattern, 'x', {pipedData: text, literal: true})).out, pattern).toBe( - text - ); + expect((await rr(pattern, 'x', {pipedData: text, literal: true})).out, pattern).toBe(text); } }); }); diff --git a/test/fuzzy/output-match.test.js b/test/fuzzy/output-match.test.js index 840f7812..100cc12a 100644 --- a/test/fuzzy/output-match.test.js +++ b/test/fuzzy/output-match.test.js @@ -11,7 +11,7 @@ describe('--output-match', () => { expect( (await rr(pattern, '_', {pipedData: text, outputMatch: true})).out, JSON.stringify({pattern, text}) - ).toBe(matches.map((match) => match + '\n').join('')); + ).toBe(matches.map(match => match + '\n').join('')); } }); @@ -24,7 +24,7 @@ describe('--output-match', () => { expect( (await rr(pattern, '_', {pipedData: text, outputMatch: true})).out, JSON.stringify({pattern, text}) - ).toBe(matches.map((match) => match.slice(1).join('') + '\n').join('')); + ).toBe(matches.map(match => match.slice(1).join('') + '\n').join('')); } }); }); diff --git a/test/fuzzy/output.test.js b/test/fuzzy/output.test.js index 5b239c05..0bc81e8d 100644 --- a/test/fuzzy/output.test.js +++ b/test/fuzzy/output.test.js @@ -1,7 +1,7 @@ import {describe, test, expect} from 'bun:test'; import {rr, random, ended} from '../js/helper.js'; -const read = (names) => Promise.all(names.map((name) => Bun.file(name).text())); +const read = names => Promise.all(names.map(name => Bun.file(name).text())); describe('--output', () => { test('sends exactly what would have been written to the files with the content delimiter after each', async () => { @@ -9,7 +9,7 @@ describe('--output', () => { for (let i = 0; i < 100; i++) { const names = ['a', 'b', 'c', 'd', 'e', 'f'] .slice(0, 1 + rnd.int(5)) - .map((name) => name + '.file'); + .map(name => name + '.file'); const texts = names.map(() => rnd.text('abAB01 \t\r\n\0\\\'"`$€§æ中🦖', 0, 200)); const pattern = rnd.pick(['a', 'b+', '^.', '\\s', 'ab', '$', '\\r?\\n']); // null is the default: a line break, unless the content already ends with one @@ -27,7 +27,7 @@ describe('--output', () => { expect(out, JSON.stringify({pattern, contentDelimiter, texts})).toBe( null === contentDelimiter ? ended(...(await read(names))) - : (await read(names)).map((text) => text + contentDelimiter).join('') + : (await read(names)).map(text => text + contentDelimiter).join('') ); } }); diff --git a/test/fuzzy/replacement-js.test.js b/test/fuzzy/replacement-js.test.js index 6bfdc1d2..3336891b 100644 --- a/test/fuzzy/replacement-js.test.js +++ b/test/fuzzy/replacement-js.test.js @@ -12,9 +12,7 @@ describe('--replacement-js', () => { for (let i = 0; i < 500; i++) { const text = rnd.text(alphabet, 1, 20); expect(await js('[^]+', '$0', text), JSON.stringify(text)).toBe(text); - expect(await js('([^]+)', '$1.length', text), JSON.stringify(text)).toBe( - String(text.length) - ); + expect(await js('([^]+)', '$1.length', text), JSON.stringify(text)).toBe(String(text.length)); } }); @@ -35,7 +33,7 @@ describe('--replacement-js', () => { const operator = rnd.pick(['+', '-', '*', '%', '/']); const operand = 1 + rnd.int(9); expect(await js('(\\d+)', `$1 ${operator} ${operand}`, numbers.join(' '))).toBe( - numbers.map((n) => eval(`"${n}" ${operator} ${operand}`)).join(' ') + numbers.map(n => eval(`"${n}" ${operator} ${operand}`)).join(' ') ); } }); diff --git a/test/fuzzy/unicode.test.js b/test/fuzzy/unicode.test.js index 0c72f639..321e757e 100644 --- a/test/fuzzy/unicode.test.js +++ b/test/fuzzy/unicode.test.js @@ -7,17 +7,7 @@ describe('--unicode', () => { test('gives the same as String.replace with and without the u flag', async () => { const rnd = random(1); for (let i = 0; i < 500; i++) { - const pattern = rnd.pick([ - '.', - '..', - '[^a]', - '\\S+', - 'a.', - '.b', - '[🦖🦕]', - '🦖+', - '\\W', - ]); + const pattern = rnd.pick(['.', '..', '[^a]', '\\S+', 'a.', '.b', '[🦖🦕]', '🦖+', '\\W']); const text = rnd.text(alphabet, 0, 15); const note = JSON.stringify({pattern, text}); diff --git a/test/fuzzy/void-async.test.js b/test/fuzzy/void-async.test.js index be6840b9..dc3061dc 100644 --- a/test/fuzzy/void-async.test.js +++ b/test/fuzzy/void-async.test.js @@ -9,11 +9,11 @@ describe('--void-async', () => { const names = ['a', 'b', 'c', 'd', 'e'].slice(0, 1 + rnd.int(4)); const texts = names.map(() => rnd.text('ab \næ🦖', 0, 20)); const pattern = rnd.pick(['a', 'b+', '\\s', 'ab']); - const expected = texts.map((text) => text.replace(new RegExp(pattern, 'gim'), '_')); - const result = (dir) => names.map((name) => fs.readFileSync(dir + name, 'utf8')); + const expected = texts.map(text => text.replace(new RegExp(pattern, 'gim'), '_')); + const result = dir => names.map(name => fs.readFileSync(dir + name, 'utf8')); const [sync, async] = [`sync${i}/`, `async${i}/`]; - [sync, async].forEach((dir) => { + [sync, async].forEach(dir => { fs.mkdirSync(dir, {recursive: true}); names.forEach((name, n) => fs.writeFileSync(dir + name, texts[n])); }); diff --git a/test/fuzzy/void-euro.test.js b/test/fuzzy/void-euro.test.js index 9022c530..4b468f20 100644 --- a/test/fuzzy/void-euro.test.js +++ b/test/fuzzy/void-euro.test.js @@ -9,17 +9,16 @@ describe('--void-euro', () => { const replacement = rnd.text(['$1', '$2', '$&', '$$', '-', 'x'], 0, 4); const text = rnd.text('ab \n', 0, 20); const expected = text.replace(new RegExp(pattern, 'gim'), replacement); - const euro = (str) => str.replace(/\$/g, '€'); + const euro = str => str.replace(/\$/g, '€'); const note = JSON.stringify({pattern, replacement, text}); expect((await rr(pattern, replacement, {pipedData: text})).out, note).toBe(expected); expect((await rr(euro(pattern), euro(replacement), {pipedData: text})).out, note).toBe( expected ); - expect( - (await rr(pattern, replacement, {pipedData: text, voidEuro: true})).out, - note - ).toBe(expected); + expect((await rr(pattern, replacement, {pipedData: text, voidEuro: true})).out, note).toBe( + expected + ); } }); diff --git a/test/fuzzy/void-ignore-case.test.js b/test/fuzzy/void-ignore-case.test.js index b995df9e..02429715 100644 --- a/test/fuzzy/void-ignore-case.test.js +++ b/test/fuzzy/void-ignore-case.test.js @@ -12,10 +12,9 @@ describe('--void-ignore-case', () => { expect((await rr(pattern, '_', {pipedData: text})).out, note).toBe( text.replace(new RegExp(pattern || '(?:)', 'gim'), '_') ); - expect( - (await rr(pattern, '_', {pipedData: text, voidIgnoreCase: true})).out, - note - ).toBe(text.replace(new RegExp(pattern || '(?:)', 'gm'), '_')); + expect((await rr(pattern, '_', {pipedData: text, voidIgnoreCase: true})).out, note).toBe( + text.replace(new RegExp(pattern || '(?:)', 'gm'), '_') + ); } }); }); diff --git a/test/fuzzy/void-section.test.js b/test/fuzzy/void-section.test.js index 9364c013..89c8d853 100644 --- a/test/fuzzy/void-section.test.js +++ b/test/fuzzy/void-section.test.js @@ -17,12 +17,8 @@ describe('--void-section', () => { } const section = pattern.replace(/\\/g, '§'); - expect((await rr(pattern, '_', {pipedData: text})).out, note).toBe( - text.replace(regex, '_') - ); - expect((await rr(section, '_', {pipedData: text})).out, note).toBe( - text.replace(regex, '_') - ); + expect((await rr(pattern, '_', {pipedData: text})).out, note).toBe(text.replace(regex, '_')); + expect((await rr(section, '_', {pipedData: text})).out, note).toBe(text.replace(regex, '_')); expect((await rr(pattern, '_', {pipedData: text, voidSection: true})).out, note).toBe( text.replace(regex, '_') ); diff --git a/test/js/basics.test.js b/test/js/basics.test.js index bcde49e4..e650ea6d 100644 --- a/test/js/basics.test.js +++ b/test/js/basics.test.js @@ -56,9 +56,9 @@ describe('replacement', () => { }); test('can refer to named groups', async () => { - expect( - (await rr('(?f)(?.*)', '$$', {pipedData: 'foobar'})).out - ).toBe('oobarf'); + expect((await rr('(?f)(?.*)', '$$', {pipedData: 'foobar'})).out).toBe( + 'oobarf' + ); }); test('can refer to the full match and what is around it', async () => { @@ -134,7 +134,7 @@ describe('files', () => { }); test('are handled in alphabetical order', async () => { - ['c', 'a', 'b'].forEach((name) => fs.writeFileSync(name + '.file', name)); + ['c', 'a', 'b'].forEach(name => fs.writeFileSync(name + '.file', name)); expect((await rr('$', '!', {includeGlob: ['c.file', '*.file'], output: true})).out).toBe( 'a!\nb!\nc!\n' ); @@ -157,7 +157,7 @@ describe('files', () => { }); test('can be pinpointed with braces and negation', async () => { - ['a', 'b', 'c'].forEach((name) => fs.writeFileSync(name + '.file', 'foobar')); + ['a', 'b', 'c'].forEach(name => fs.writeFileSync(name + '.file', 'foobar')); await rr('foo', '1', {includeGlob: ['{a,b}.file']}); await rr('bar', '2', {includeGlob: ['*.file', '!b.file']}); expect(fs.readFileSync('a.file', 'utf8')).toBe('12'); @@ -184,15 +184,12 @@ describe('files', () => { // The file names are read by src/cli.ts, so this runs the CLI itself test.each(['2024', '1e3', '0x10'])( 'can have a name that reads as a number, like %s (#733)', - async (name) => { + async name => { await Bun.write(name, 'foobar'); const cli = path.join(import.meta.dir, '../../src/cli.ts'); - const {stdout, stderr, exitCode} = Bun.spawnSync( - [process.execPath, cli, 'o', 'x', name], - { - stdin: 'ignore', - } - ); + const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, 'o', 'x', name], { + stdin: 'ignore', + }); expect({out: stdout.toString(), err: stderr.toString(), exit: exitCode}).toEqual({ out: name + '\n', err: '', diff --git a/test/js/crossover.test.js b/test/js/crossover.test.js index 46fda5af..86ee983d 100644 --- a/test/js/crossover.test.js +++ b/test/js/crossover.test.js @@ -13,10 +13,10 @@ describe('regex flags', () => { flags.filter((flag, n) => bits & (1 << n)) ); - test.each(combinations.map((on) => [on.join(' + ') || 'none', on]))( + test.each(combinations.map(on => [on.join(' + ') || 'none', on]))( '%s gives the same as String.replace', async (name, on) => { - const config = Object.fromEntries(on.map((flag) => [flag, true])); + const config = Object.fromEntries(on.map(flag => [flag, true])); const jsFlags = (config.voidGlobal ? '' : 'g') + (config.voidIgnoreCase ? '' : 'i') + @@ -56,7 +56,7 @@ describe('every pair of flags', () => { 'replacementJs', ]; - const pairs = flags.flatMap((first, n) => flags.slice(n).map((second) => [first, second])); + const pairs = flags.flatMap((first, n) => flags.slice(n).map(second => [first, second])); // Replace "o" with "1" in a file containing "foo". The 1 works as plain text, as javascript and when piped in. test.each(pairs)('%s + %s', async (first, second) => { @@ -99,9 +99,9 @@ describe('same result no matter how it is delivered', () => { test('--output', async () => { fs.writeFileSync('my.file', text); - expect( - (await rr('a.b', '[$&]', {includeGlob: ['my.file'], output: true, ...search})).out - ).toBe(expected); + expect((await rr('a.b', '[$&]', {includeGlob: ['my.file'], output: true, ...search})).out).toBe( + expected + ); }); test.each([ @@ -121,9 +121,9 @@ describe('same result no matter how it is delivered', () => { test('--replacement-pipe and --replacement-js', async () => { fs.writeFileSync('my.file', text); const config = {includeGlob: ['my.file'], output: true, ...search}; - expect( - (await rr('a.b', '_', {...config, replacementPipe: true, pipedData: '[$&]'})).out - ).toBe(expected); + expect((await rr('a.b', '_', {...config, replacementPipe: true, pipedData: '[$&]'})).out).toBe( + expected + ); expect((await rr('a.b', '"[" + $0 + "]"', {...config, replacementJs: true})).out).toBe( expected ); @@ -167,10 +167,8 @@ describe('many flags at once', () => { expect(fs.readFileSync('src/a.test.js', 'utf8')).toBe('VERSION'); expect(fs.readFileSync('src/deep/c.md', 'utf8')).toBe('VERSION'); expect(fs.readFileSync('dist/a.js', 'utf8')).toBe('VERSION'); - expect(fs.readdirSync('src').filter((name) => name.endsWith('.backup'))).toHaveLength(1); - expect(fs.readdirSync('src/deep').filter((name) => name.endsWith('.backup'))).toHaveLength( - 1 - ); + expect(fs.readdirSync('src').filter(name => name.endsWith('.backup'))).toHaveLength(1); + expect(fs.readdirSync('src/deep').filter(name => name.endsWith('.backup'))).toHaveLength(1); }); test('list the first match of each line across files', async () => { diff --git a/test/js/debug.test.js b/test/js/debug.test.js index 396d2296..cb3f2b0c 100644 --- a/test/js/debug.test.js +++ b/test/js/debug.test.js @@ -15,7 +15,7 @@ function runCli(...args) { describe('--debug', () => { test.each(['--debug', '-d'])( '%s prints only the status report, with what went into the engine and what it did', - (flag) => { + flag => { fs.writeFileSync('my.file', 'foobar'); const result = runCli('o', 'x', 'my.file', flag); expect(result.err).toBe(''); diff --git a/test/js/encoding.test.js b/test/js/encoding.test.js index d6d7d53d..936b65e4 100644 --- a/test/js/encoding.test.js +++ b/test/js/encoding.test.js @@ -44,9 +44,7 @@ describe('--encoding', () => { encoding: 'latin1', voidIgnoreCase: true, }); - expect(fs.readFileSync('my.file')).toEqual( - Buffer.concat([bytes, Buffer.from('bar'), bytes]) - ); + expect(fs.readFileSync('my.file')).toEqual(Buffer.concat([bytes, Buffer.from('bar'), bytes])); }); // The error message, and not the stack trace from deep inside node, when an encoding is unknown (#690) @@ -85,7 +83,7 @@ describe('--encoding', () => { test('with --keep-backup the backup has the original bytes', async () => { fs.writeFileSync('my.file', latin1()); await rr('æ', 'ø', {includeGlob: ['my.file'], encoding: 'latin1', keepBackup: true}); - const backup = fs.readdirSync('.').find((name) => name.endsWith('.backup')); + const backup = fs.readdirSync('.').find(name => name.endsWith('.backup')); expect(fs.readFileSync(backup)).toEqual(latin1()); }); diff --git a/test/js/exclude-glob.test.js b/test/js/exclude-glob.test.js index c27eab29..699ba215 100644 --- a/test/js/exclude-glob.test.js +++ b/test/js/exclude-glob.test.js @@ -4,12 +4,12 @@ import {rr} from './helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => { + names.forEach(name => { fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); fs.writeFileSync(name, 'foo'); }); await rr('foo', 'x', config); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; describe('--exclude-glob', () => { @@ -25,22 +25,26 @@ describe('--exclude-glob', () => { test('can be given several times', async () => { const names = ['a.file', 'b.file', 'c.file']; - expect( - await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a.*', 'b.*']}) - ).toEqual(['c.file']); + expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a.*', 'b.*']})).toEqual([ + 'c.file', + ]); }); test('understands braces and **', async () => { const names = ['a.file', 'b.file', 'c.file', 'sub/a.file', 'sub/deep/d.file']; - expect( - await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['{a,b}.file']}) - ).toEqual(['c.file', 'sub/a.file', 'sub/deep/d.file']); - expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['sub/**']})).toEqual( - ['a.file', 'b.file', 'c.file'] + expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['{a,b}.file']})).toEqual( + ['c.file', 'sub/a.file', 'sub/deep/d.file'] ); - expect( - await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['**/a.file']}) - ).toEqual(['b.file', 'c.file', 'sub/deep/d.file']); + expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['sub/**']})).toEqual([ + 'a.file', + 'b.file', + 'c.file', + ]); + expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['**/a.file']})).toEqual([ + 'b.file', + 'c.file', + 'sub/deep/d.file', + ]); }); test('reports when all files are left out', async () => { @@ -68,18 +72,15 @@ describe('--exclude-glob', () => { expect(await changed(names, config)).toEqual(['c.file']); }); - test.each(['output', 'outputMatch'])( - 'with --%s only the files left are sent', - async (flag) => { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('b.file', 'boo'); - const config = {includeGlob: ['*.file'], excludeGlob: ['a.*'], [flag]: true}; - expect((await rr('.oo', '$&', config)).out.trim()).toBe('boo'); - } - ); + test.each(['output', 'outputMatch'])('with --%s only the files left are sent', async flag => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'boo'); + const config = {includeGlob: ['*.file'], excludeGlob: ['a.*'], [flag]: true}; + expect((await rr('.oo', '$&', config)).out.trim()).toBe('boo'); + }); test('with --verbose the count is the files left', async () => { - ['a', 'b', 'c'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['a', 'b', 'c'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); const config = {includeGlob: ['*.file'], excludeGlob: ['a.*'], verbose: true}; expect((await rr('o', 'x', config)).err).toContain('2 files found'); }); diff --git a/test/js/exclude-re.test.js b/test/js/exclude-re.test.js index b9f7e614..f6f4f148 100644 --- a/test/js/exclude-re.test.js +++ b/test/js/exclude-re.test.js @@ -4,12 +4,12 @@ import {rr} from './helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => { + names.forEach(name => { fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); fs.writeFileSync(name, 'foo'); }); await rr('foo', 'x', config); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; describe('--exclude-re', () => { @@ -25,9 +25,9 @@ describe('--exclude-re', () => { test('is a regex', async () => { const names = ['a1.file', 'a2.file', 'ab.file']; - expect( - await changed(names, {includeGlob: ['*.file'], excludeRe: ['a\\d\\.file$']}) - ).toEqual(['ab.file']); + expect(await changed(names, {includeGlob: ['*.file'], excludeRe: ['a\\d\\.file$']})).toEqual([ + 'ab.file', + ]); }); test('can be given several times', async () => { @@ -39,9 +39,10 @@ describe('--exclude-re', () => { test('sees the path of files in sub directories', async () => { const names = ['a.file', 'sub/a.file', 'sub/deep/a.file']; - expect( - await changed(names, {includeGlob: ['**/*.file'], excludeRe: ['^sub/[^/]+$']}) - ).toEqual(['a.file', 'sub/deep/a.file']); + expect(await changed(names, {includeGlob: ['**/*.file'], excludeRe: ['^sub/[^/]+$']})).toEqual([ + 'a.file', + 'sub/deep/a.file', + ]); }); test('reports when all files are left out', async () => { diff --git a/test/js/fix/683.test.js b/test/js/fix/683.test.js index 3888b738..66cc0cd1 100644 --- a/test/js/fix/683.test.js +++ b/test/js/fix/683.test.js @@ -6,9 +6,9 @@ import {rr} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => fs.writeFileSync(name, 'foo')); + names.forEach(name => fs.writeFileSync(name, 'foo')); await rr('foo', 'x', {includeGlob: ['*.file'], ...config}); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; describe('#683 --exclude-re follows the regex flags of the main search', () => { @@ -20,20 +20,14 @@ describe('#683 --exclude-re follows the regex flags of the main search', () => { }); test('ignores case as default', async () => { - expect(await changed(['MY.file', 'your.file'], {excludeRe: ['^my']})).toEqual([ - 'your.file', - ]); - expect(await changed(['my.file', 'your.file'], {excludeRe: ['^MY']})).toEqual([ - 'your.file', - ]); + expect(await changed(['MY.file', 'your.file'], {excludeRe: ['^my']})).toEqual(['your.file']); + expect(await changed(['my.file', 'your.file'], {excludeRe: ['^MY']})).toEqual(['your.file']); }); test('--void-ignore-case makes the case matter', async () => { const names = ['MY.file', 'your.file']; expect(await changed(names, {excludeRe: ['^my'], voidIgnoreCase: true})).toEqual(names); - expect(await changed(names, {excludeRe: ['^MY'], voidIgnoreCase: true})).toEqual([ - 'your.file', - ]); + expect(await changed(names, {excludeRe: ['^MY'], voidIgnoreCase: true})).toEqual(['your.file']); }); test('--dot-all lets a dot match a line break in the path', async () => { diff --git a/test/js/fix/692.test.js b/test/js/fix/692.test.js index a14fad4c..5471e10c 100644 --- a/test/js/fix/692.test.js +++ b/test/js/fix/692.test.js @@ -16,14 +16,14 @@ function run(...args) { describe('#692 single argument', () => { const version = run('--version').out; - test.each(['-v', '-version', '--version'])('%s prints only the version', (arg) => { + test.each(['-v', '-version', '--version'])('%s prints only the version', arg => { const result = run(arg); expect(result.out).toBe(version); expect(result.out.trim()).toMatch(/^\S+$/); expect(result.exit).toBe(0); }); - test.each(['-h', '-help', '--help'])('%s prints the help with exit code 0', (arg) => { + test.each(['-h', '-help', '--help'])('%s prints the help with exit code 0', arg => { const result = run(arg); expect(result.out).toContain('Options:'); expect(result.err).toBe(''); @@ -33,7 +33,7 @@ describe('#692 single argument', () => { // -V is verbose and -H is halt: other flags, not a request for version or help test.each(['my-vile', '-V', 'o-ho', '-H', 'a-v', 'a-h', 'xversion', 'xhelp'])( '%s is a usage error: help on stderr, nothing on stdout, exit code 1', - (arg) => { + arg => { const result = run(arg); expect(result.out).toBe(''); expect(result.err).toContain('Options:'); diff --git a/test/js/fix/699.test.js b/test/js/fix/699.test.js index ffb318f6..e22d8946 100644 --- a/test/js/fix/699.test.js +++ b/test/js/fix/699.test.js @@ -7,9 +7,9 @@ import {rr} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { - names.forEach((name) => fs.writeFileSync(name, 'foo')); + names.forEach(name => fs.writeFileSync(name, 'foo')); await rr('foo', 'x', config); - return names.filter((name) => 'x' === fs.readFileSync(name, 'utf8')); + return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); }; const voided = {voidEuro: true, voidSection: true}; @@ -37,9 +37,10 @@ describe('#699 € and § are only aliases in the pattern, the replacement and - test('--void-euro and --void-section make no difference', async () => { const names = ['cost€.file', 'cost$.file', 'par§.file']; - expect( - await changed(names, {includeGlob: ['cost€.file', 'par§.file'], ...voided}) - ).toEqual(['cost€.file', 'par§.file']); + expect(await changed(names, {includeGlob: ['cost€.file', 'par§.file'], ...voided})).toEqual([ + 'cost€.file', + 'par§.file', + ]); }); }); @@ -47,28 +48,30 @@ describe('#699 € and § are only aliases in the pattern, the replacement and - const names = ['a[1].file', 'cost$.file', 'cost€.file', 'par§.file']; test('€ and § are plain chars', async () => { - expect( - await changed(names, {includeGlob: ['*.file'], excludeGlob: ['cost€.file']}) - ).toEqual(['a[1].file', 'cost$.file', 'par§.file']); - expect( - await changed(names, {includeGlob: ['*.file'], excludeGlob: ['par§.file']}) - ).toEqual(['a[1].file', 'cost$.file', 'cost€.file']); + expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['cost€.file']})).toEqual([ + 'a[1].file', + 'cost$.file', + 'par§.file', + ]); + expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['par§.file']})).toEqual([ + 'a[1].file', + 'cost$.file', + 'cost€.file', + ]); }); test('§ does not escape like a backslash does', async () => { expect( await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a\\[1\\].file']}) ).toEqual(['cost$.file', 'cost€.file', 'par§.file']); - expect( - await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a§[1§].file']}) - ).toEqual(names); + expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a§[1§].file']})).toEqual( + names + ); }); test('--void-euro and --void-section make no difference', async () => { const config = {includeGlob: ['*.file'], excludeGlob: ['cost€.file', 'par§.file']}; - expect(await changed(names, {...config, ...voided})).toEqual( - await changed(names, config) - ); + expect(await changed(names, {...config, ...voided})).toEqual(await changed(names, config)); }); }); @@ -99,9 +102,7 @@ describe('#699 € and § are only aliases in the pattern, the replacement and - test('the replacement', async () => { expect((await rr('(b)', '[€1§]', {pipedData: 'foobar'})).out).toBe('foo[b\\]ar'); - expect((await rr('(b)', '[€1§]', {pipedData: 'foobar', ...voided})).out).toBe( - 'foo[€1§]ar' - ); + expect((await rr('(b)', '[€1§]', {pipedData: 'foobar', ...voided})).out).toBe('foo[€1§]ar'); }); test('the replacement with --replacement-js', async () => { @@ -111,9 +112,9 @@ describe('#699 € and § are only aliases in the pattern, the replacement and - test('--exclude-re', async () => { const names = ['a1.file', 'ab.file', 'ab.files']; - expect(await changed(names, {includeGlob: ['a*'], excludeRe: ['§d', 'file€']})).toEqual( - ['ab.files'] - ); + expect(await changed(names, {includeGlob: ['a*'], excludeRe: ['§d', 'file€']})).toEqual([ + 'ab.files', + ]); expect( await changed(names, {includeGlob: ['a*'], excludeRe: ['§d', 'file€'], ...voided}) ).toEqual(names); diff --git a/test/js/fix/703.test.js b/test/js/fix/703.test.js index 169ea0b1..7e578071 100644 --- a/test/js/fix/703.test.js +++ b/test/js/fix/703.test.js @@ -39,7 +39,7 @@ describe('#703 the names of affected files', () => { expect(result.err).toContain('Nothing changed in: b.file'); }); - test.each(['quiet', 'quietTotal'])('are not printed with --%s', async (flag) => { + test.each(['quiet', 'quietTotal'])('are not printed with --%s', async flag => { expect(await run({[flag]: true})).toEqual({out: '', err: '', exit: null}); expect(await Bun.file('c.file').text()).toBe('fxx'); }); @@ -68,9 +68,7 @@ describe('#703 the names of affected files', () => { test('keeps a name with a line break in one piece', async () => { await Bun.write('my\n.file', 'foo'); - expect((await rr('o', 'x', {includeGlob: ['*.file'], null: true})).out).toBe( - 'my\n.file\0' - ); + expect((await rr('o', 'x', {includeGlob: ['*.file'], null: true})).out).toBe('my\n.file\0'); }); }); @@ -89,12 +87,8 @@ describe('#703 the names of affected files', () => { test('has the € and § aliases like the replacement', async () => { expect((await run({nameDelimiter: '€§'})).out).toBe('a.file$\\c.file$\\'); - expect((await run({nameDelimiter: '€§', voidEuro: true})).out).toBe( - 'a.file€\\c.file€\\' - ); - expect((await run({nameDelimiter: '€§', voidSection: true})).out).toBe( - 'a.file$§c.file$§' - ); + expect((await run({nameDelimiter: '€§', voidEuro: true})).out).toBe('a.file€\\c.file€\\'); + expect((await run({nameDelimiter: '€§', voidSection: true})).out).toBe('a.file$§c.file$§'); }); }); @@ -219,9 +213,7 @@ describe('#703 the names of affected files', () => { test('leaves piped data as it is by default', async () => { expect((await rr('o', 'x', {pipedData: 'foo'})).out).toBe('fxx'); - expect((await rr('o', 'x', {pipedData: 'foo', contentDelimiter: ', '})).out).toBe( - 'fxx' - ); + expect((await rr('o', 'x', {pipedData: 'foo', contentDelimiter: ', '})).out).toBe('fxx'); }); test('is put after the last match, from a file or a pipe', async () => { @@ -292,9 +284,7 @@ describe('#703 the names of affected files', () => { }); test('adds nothing when nothing is printed', async () => { - expect( - (await rr('nope', 'x', {pipedData: 'foo', outputMatch: true, nl: true})).out - ).toBe(''); + expect((await rr('nope', 'x', {pipedData: 'foo', outputMatch: true, nl: true})).out).toBe(''); await Bun.write('a.file', 'foo'); expect((await rr('nope', 'x', {includeGlob: ['a.file'], nl: true})).out).toBe(''); }); @@ -329,9 +319,7 @@ describe('#703 the names of affected files', () => { }); test('the most specific delimiter wins', async () => { - expect((await run({output: true, delimiter: ', ', null: true})).out).toBe( - 'fxx, bar, fxx, ' - ); + expect((await run({output: true, delimiter: ', ', null: true})).out).toBe('fxx, bar, fxx, '); expect( await run({ output: true, @@ -362,7 +350,7 @@ describe('#703 the names of affected files', () => { expect((await run({[flag]: true})).err).toBe((await run()).out); }); - test.each(['quiet', 'quietTotal'])('are not printed with --%s', async (quiet) => { + test.each(['quiet', 'quietTotal'])('are not printed with --%s', async quiet => { expect(await run({[flag]: true, [quiet]: true})).toEqual({out, err: '', exit: null}); }); @@ -389,9 +377,7 @@ describe('#703 the names of affected files', () => { const file = path.resolve('other', 'my.file'); await Bun.write(file, 'foo'); process.chdir('work'); - expect((await rr('o', 'x', {includeGlob: [file], absolutePath: true})).out).toBe( - file + '\n' - ); + expect((await rr('o', 'x', {includeGlob: [file], absolutePath: true})).out).toBe(file + '\n'); expect(await Bun.file(file).text()).toBe('fxx'); }); diff --git a/test/js/fix/705.test.js b/test/js/fix/705.test.js index 210057b7..5526b0ab 100644 --- a/test/js/fix/705.test.js +++ b/test/js/fix/705.test.js @@ -5,7 +5,7 @@ import fs from 'fs'; import {engine} from '../../../src/engine.ts'; import {rr} from '../helper.js'; -const content = (names) => names.map((name) => fs.readFileSync(name, 'utf8')); +const content = names => names.map(name => fs.readFileSync(name, 'utf8')); // The name of the backup is longer than a file system allows, so the file can not be renamed const tooLong = 'b'.repeat(240) + '.file'; @@ -32,7 +32,7 @@ describe('#705 the engine returns a promise that resolves when all files are don test('waits for every file', async () => { const names = Array.from({length: 20}, (_, n) => `my${n}.file`); - names.forEach((name) => fs.writeFileSync(name, 'foo')); + names.forEach(name => fs.writeFileSync(name, 'foo')); const result = await rr('o', 'x', {includeGlob: ['*.file'], voidAsync: false}); @@ -76,7 +76,7 @@ describe('#705 the engine returns a promise that resolves when all files are don describe('a file with an error', () => { test.each(flows)('does not stop the other files with %s', async (name, flags) => { - ['a.file', tooLong, 'c.file'].forEach((name) => fs.writeFileSync(name, 'foo')); + ['a.file', tooLong, 'c.file'].forEach(name => fs.writeFileSync(name, 'foo')); const result = await rr('o', 'x', {includeGlob: ['*.file'], ...flags}); diff --git a/test/js/fix/708.test.js b/test/js/fix/708.test.js index f2b4dd59..3fc6e88f 100644 --- a/test/js/fix/708.test.js +++ b/test/js/fix/708.test.js @@ -10,7 +10,7 @@ const workers = Math.max(16, os.availableParallelism() * 2); // Make `count` files with the content `foo` and give their names async function makeFiles(count) { const names = Array.from({length: count}, (_, i) => `file-${i}.txt`); - await Promise.all(names.map((name) => Bun.write(name, 'foo'))); + await Promise.all(names.map(name => Bun.write(name, 'foo'))); return names; } diff --git a/test/js/fix/718.test.js b/test/js/fix/718.test.js index 931cd4cf..54fe0c2c 100644 --- a/test/js/fix/718.test.js +++ b/test/js/fix/718.test.js @@ -4,12 +4,12 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from '../helper.js'; -const content = (name) => fs.readFileSync(name, 'utf8'); +const content = name => fs.readFileSync(name, 'utf8'); describe('#718 a path that is a file is used as it is', () => { test.each(['my (1).file', '(draft).file', '{a,b}.file', '!important.file', 'a+(b).file'])( '%s is found', - async (name) => { + async name => { fs.writeFileSync(name, 'foo'); expect(await rr('o', 'x', {includeGlob: [name]})).toEqual({ out: name + '\n', diff --git a/test/js/fix/719.test.js b/test/js/fix/719.test.js index 96ad634d..17fc6f7e 100644 --- a/test/js/fix/719.test.js +++ b/test/js/fix/719.test.js @@ -18,7 +18,7 @@ describe('#719 a warning when piped data is not used', () => { expect(await Bun.file('my.file').text()).toBe('foo_ar'); }); - test.each(['quiet', 'quietTotal'])('is not shown with --%s', async (flag) => { + test.each(['quiet', 'quietTotal'])('is not shown with --%s', async flag => { expect(await piped({[flag]: true})).toEqual({out: '', err: '', exit: null}); }); @@ -45,7 +45,7 @@ describe('#719 a warning when piped data is not used', () => { expect(await Bun.file('my.file').text()).toBe('fooxar'); }); - test.each([null, '', '\n', '\r\n'])('is not given when %j is piped in', async (pipedData) => { + test.each([null, '', '\n', '\r\n'])('is not given when %j is piped in', async pipedData => { expect(await piped({pipedData})).toEqual({out: expectedOutput, err: '', exit: null}); }); diff --git a/test/js/fix/726.test.js b/test/js/fix/726.test.js index 57d0b47a..843e7147 100644 --- a/test/js/fix/726.test.js +++ b/test/js/fix/726.test.js @@ -55,7 +55,7 @@ describe('#726 a file given with an absolute path is changed', () => { await rr('o', 'x', {includeGlob: [file], keepBackup: true, ...flags}); const [backup, ...rest] = fs .readdirSync(path.dirname(file)) - .filter((name) => name !== 'my.file'); + .filter(name => name !== 'my.file'); expect(rest).toEqual([]); expect(backup).toMatch(/^my\.file\..+\.backup$/); expect(await Bun.file(path.join(path.dirname(file), backup)).text()).toBe('foobar'); diff --git a/test/js/fix/728.test.js b/test/js/fix/728.test.js index ef4c1e8c..ac4359e2 100644 --- a/test/js/fix/728.test.js +++ b/test/js/fix/728.test.js @@ -44,7 +44,7 @@ describe('#728 the exit code is 1 when a file gives an error', () => { } }); - test.each(['quiet', 'quietTotal'])('also with --%s', async (flag) => { + test.each(['quiet', 'quietTotal'])('also with --%s', async flag => { expect((await rr('o', 'x', {...oneUnreadable(), [flag]: true})).exit).toBe(1); }); }); diff --git a/test/js/fix/742.test.js b/test/js/fix/742.test.js index 855433ff..9e37236c 100644 --- a/test/js/fix/742.test.js +++ b/test/js/fix/742.test.js @@ -124,9 +124,7 @@ describe('#742 the engine part of the status', () => { expect(await rrStatus('o', 'x', {pipedData: 'foo', verbose: true})).not.toHaveProperty( 'engine' ); - expect(await rrStatus('o', 'x', {pipedData: 'foo', status: true})).not.toHaveProperty( - 'engine' - ); + expect(await rrStatus('o', 'x', {pipedData: 'foo', status: true})).not.toHaveProperty('engine'); const status = await rrStatus('o', 'x', { pipedData: 'foo', statusCompact: true, @@ -214,7 +212,7 @@ describe('#742 the CLI', () => { test.each(['--status-compact', '-S'])( '%s prints the status report on one line, with no line break in front', - (flag) => { + flag => { fs.writeFileSync('a.file', 'foo'); const result = runCli('o', 'x', 'a.file', flag, '--quiet-total'); expect(result.out).toBe( diff --git a/test/js/halt.test.js b/test/js/halt.test.js index 455faf34..42bd66c8 100644 --- a/test/js/halt.test.js +++ b/test/js/halt.test.js @@ -58,7 +58,7 @@ describe('--halt', () => { expect(result.out).toMatch(/-H,\s*--halt,\s*--bail/); }); - test.each(['--bail', '--halt', '-H'])('%s stops on the first error like --halt', (flag) => { + test.each(['--bail', '--halt', '-H'])('%s stops on the first error like --halt', flag => { const result = runCli('o', 'x', 'nope.file', flag); expect(result.err).toContain('0 files found'); expect(result.exit).toBe(1); @@ -73,7 +73,7 @@ describe('--halt', () => { }); describe('combined', () => { - test.each(['quiet', 'quietTotal'])('with --%s it still stops', async (flag) => { + test.each(['quiet', 'quietTotal'])('with --%s it still stops', async flag => { const result = await rr('o', 'x', { includeGlob: ['nope.file'], halt: true, diff --git a/test/js/helper.js b/test/js/helper.js index cec856e2..4dc9779a 100644 --- a/test/js/helper.js +++ b/test/js/helper.js @@ -37,14 +37,14 @@ async function run(pattern, replacement, config) { let status; const spies = [ - spyOn(process.stdout, 'write').mockImplementation((data) => { + spyOn(process.stdout, 'write').mockImplementation(data => { result.out += data; return true; }), spyOn(console, 'error').mockImplementation((...data) => { result.err += data.join(' ') + '\n'; }), - spyOn(process.stderr, 'write').mockImplementation((data) => { + spyOn(process.stderr, 'write').mockImplementation(data => { result.err += data; return true; }), @@ -81,7 +81,7 @@ async function run(pattern, replacement, config) { status = e.status; result.exit = 1; } finally { - spies.forEach((spy) => spy.mockRestore()); + spies.forEach(spy => spy.mockRestore()); } return {result, status}; @@ -89,7 +89,7 @@ async function run(pattern, replacement, config) { // What the content of the files gives by default: a line break after each item, unless it already ends with one export function ended(...items) { - return items.map((item) => (item.endsWith('\n') ? item : item + '\n')).join(''); + return items.map(item => (item.endsWith('\n') ? item : item + '\n')).join(''); } // Fuzz tests get new input on every run. Set RR_FUZZ_SEED to repeat a run. @@ -113,13 +113,11 @@ export function random(id) { }; const rnd = { - int: (max) => Math.floor(next() * (max + 1)), - pick: (list) => list[rnd.int(list.length - 1)], + int: max => Math.floor(next() * (max + 1)), + pick: list => list[rnd.int(list.length - 1)], // String of min to max elements from alphabet (a string or an array of strings) text: (alphabet, min, max) => - Array.from({length: min + rnd.int(max - min)}, () => - rnd.pick(Array.from(alphabet)) - ).join(''), + Array.from({length: min + rnd.int(max - min)}, () => rnd.pick(Array.from(alphabet))).join(''), }; return rnd; diff --git a/test/js/keep-backup.test.js b/test/js/keep-backup.test.js index 6d81eac1..ca071e0f 100644 --- a/test/js/keep-backup.test.js +++ b/test/js/keep-backup.test.js @@ -2,7 +2,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from './helper.js'; -const backups = (dir = '.') => fs.readdirSync(dir).filter((name) => name.endsWith('.backup')); +const backups = (dir = '.') => fs.readdirSync(dir).filter(name => name.endsWith('.backup')); describe('--keep-backup', () => { test('keeps the original content in a backup file next to the file', async () => { @@ -37,8 +37,8 @@ describe('--keep-backup', () => { fs.writeFileSync('b.file', 'bar'); fs.writeFileSync('sub/c.file', 'foo'); await rr('foo', 'x', {includeGlob: ['**/*.file'], keepBackup: true}); - expect(backups().map((name) => name.slice(0, 7))).toEqual(['a.file.']); - expect(backups('sub').map((name) => name.slice(0, 7))).toEqual(['c.file.']); + expect(backups().map(name => name.slice(0, 7))).toEqual(['a.file.']); + expect(backups('sub').map(name => name.slice(0, 7))).toEqual(['c.file.']); expect(fs.readFileSync('sub/' + backups('sub')[0], 'utf8')).toBe('foo'); }); @@ -57,14 +57,11 @@ describe('--keep-backup', () => { expect(fs.readFileSync(backups()[0], 'utf8')).toBe('foobar'); }); - test.each(['output', 'outputMatch'])( - 'with --%s there is nothing to back up', - async (flag) => { - fs.writeFileSync('my.file', 'foobar'); - await rr('o', 'x', {includeGlob: ['my.file'], keepBackup: true, [flag]: true}); - expect(fs.readdirSync('.')).toEqual(['my.file']); - } - ); + test.each(['output', 'outputMatch'])('with --%s there is nothing to back up', async flag => { + fs.writeFileSync('my.file', 'foobar'); + await rr('o', 'x', {includeGlob: ['my.file'], keepBackup: true, [flag]: true}); + expect(fs.readdirSync('.')).toEqual(['my.file']); + }); test('with --replacement-js', async () => { fs.writeFileSync('my.file', 'foobar'); @@ -81,7 +78,7 @@ describe('--keep-backup', () => { fs.writeFileSync('my.file', 'foobar'); await rr('o', 'x', {includeGlob: ['my.*'], keepBackup: true}); // The time in the name of the backup is in milliseconds - await new Promise((resolve) => setTimeout(resolve, 5)); + await new Promise(resolve => setTimeout(resolve, 5)); await rr('x', 'y', { includeGlob: ['my.*'], excludeGlob: ['*.backup'], @@ -90,7 +87,7 @@ describe('--keep-backup', () => { expect(fs.readFileSync('my.file', 'utf8')).toBe('fyybar'); expect( backups() - .map((name) => fs.readFileSync(name, 'utf8')) + .map(name => fs.readFileSync(name, 'utf8')) .sort() ).toEqual(['foobar', 'fxxbar']); }); diff --git a/test/js/literal.test.js b/test/js/literal.test.js index b90fade9..4f2c5dd2 100644 --- a/test/js/literal.test.js +++ b/test/js/literal.test.js @@ -12,7 +12,7 @@ describe('--literal', () => { expect((await rr('.', 'x', {pipedData: 'a.b'})).out).toBe('xxx'); }); - test.each(Array.from('-[]{}()*+?.,/\\^$|#'))('takes %s literally', async (char) => { + test.each(Array.from('-[]{}()*+?.,/\\^$|#'))('takes %s literally', async char => { const text = `a${char}b${char}${char}c`; expect((await rr(char, '_', {pipedData: text, literal: true})).out).toBe('a_b__c'); }); @@ -38,9 +38,7 @@ describe('--literal', () => { describe('combined', () => { test('is still case insensitive and global', async () => { - expect((await rr('a.', 'x', {pipedData: 'a. A. ab', literal: true})).out).toBe( - 'x x ab' - ); + expect((await rr('a.', 'x', {pipedData: 'a. A. ab', literal: true})).out).toBe('x x ab'); }); test('with --void-ignore-case and --void-global', async () => { @@ -56,12 +54,12 @@ describe('--literal', () => { test('€ and § are still aliases for $ and \\ unless voided', async () => { expect((await rr('€', 'x', {pipedData: '€ $', literal: true})).out).toBe('€ x'); expect((await rr('§', 'x', {pipedData: '§ \\', literal: true})).out).toBe('§ x'); - expect( - (await rr('€', 'x', {pipedData: '€ $', literal: true, voidEuro: true})).out - ).toBe('x $'); - expect( - (await rr('§', 'x', {pipedData: '§ \\', literal: true, voidSection: true})).out - ).toBe('x \\'); + expect((await rr('€', 'x', {pipedData: '€ $', literal: true, voidEuro: true})).out).toBe( + 'x $' + ); + expect((await rr('§', 'x', {pipedData: '§ \\', literal: true, voidSection: true})).out).toBe( + 'x \\' + ); }); test('with --output-match', async () => { @@ -85,7 +83,7 @@ describe('--literal', () => { test.each([...'-,# []{}()*+?./\\^$|'])( 'with --unicode finds a pattern with %j in it (#686)', - async (char) => { + async char => { const pattern = `a${char}b`; const config = {pipedData: pattern, literal: true, unicode: true}; expect(await rr(pattern, 'x', config)).toEqual({out: 'x', err: '', exit: null}); diff --git a/test/js/output-match.test.js b/test/js/output-match.test.js index a598017d..f605a8cf 100644 --- a/test/js/output-match.test.js +++ b/test/js/output-match.test.js @@ -4,18 +4,14 @@ import {rr} from './helper.js'; describe('--output-match', () => { test('prints each match on its own line', async () => { - expect((await rr('[fb].', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe( - 'fo\nba\n' - ); + expect((await rr('[fb].', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe('fo\nba\n'); }); test('prints only the groups (with nothing in between) if the pattern has groups', async () => { expect((await rr('([fb](.))', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe( 'foo\nbaa\n' ); - expect((await rr('(f)o(o)', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe( - 'fo\n' - ); + expect((await rr('(f)o(o)', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe('fo\n'); }); test('prints nothing if there is no match', async () => { @@ -43,9 +39,9 @@ describe('--output-match', () => { test('prints matches from several files in alphabetical order', async () => { fs.writeFileSync('b.file', 'boo'); fs.writeFileSync('a.file', 'foo'); - expect( - (await rr('.o', '_', {includeGlob: ['b.file', 'a.file'], outputMatch: true})).out - ).toBe('fo\nbo\n'); + expect((await rr('.o', '_', {includeGlob: ['b.file', 'a.file'], outputMatch: true})).out).toBe( + 'fo\nbo\n' + ); }); test('prints a match that spans lines as it is', async () => { @@ -57,9 +53,7 @@ describe('--output-match', () => { }); test('prints nothing for a group that did not take part in the match (#684)', async () => { - expect((await rr('(f)|(b)', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe( - 'f\nb\n' - ); + expect((await rr('(f)|(b)', '_', {pipedData: 'foobar', outputMatch: true})).out).toBe('f\nb\n'); }); test('prints named groups like any other group (#685)', async () => { @@ -97,9 +91,7 @@ describe('--output-match', () => { test('with --replacement-js the code is not run', async () => { const config = {pipedData: 'foobar', outputMatch: true, replacementJs: true}; - expect((await rr('[fb].', 'throw new Error("should not run")', config)).out).toBe( - 'fo\nba\n' - ); + expect((await rr('[fb].', 'throw new Error("should not run")', config)).out).toBe('fo\nba\n'); }); test('with --replacement-pipe the piped replacement is not used', async () => { diff --git a/test/js/output.test.js b/test/js/output.test.js index 4aef5514..d0cda2f1 100644 --- a/test/js/output.test.js +++ b/test/js/output.test.js @@ -85,7 +85,7 @@ describe('--output', () => { test.each([{voidAsync: true}, {voidBackup: true}, {voidAsync: true, voidBackup: true}])( 'with %o the file is still left alone', - async (config) => { + async config => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', { includeGlob: ['my.file'], diff --git a/test/js/quiet-total.test.js b/test/js/quiet-total.test.js index 379be139..6a507356 100644 --- a/test/js/quiet-total.test.js +++ b/test/js/quiet-total.test.js @@ -13,9 +13,9 @@ describe('--quiet-total', () => { test('does not display errors', async () => { expect((await rr('(', 'x', {pipedData: 'foobar', quietTotal: true})).err).toBe(''); expect((await rr('o', 'x', {includeGlob: ['nope.file'], quietTotal: true})).err).toBe(''); - expect( - (await rr('o', 'x', {pipedData: 'foobar', engine: 'nope', quietTotal: true})).err - ).toBe(''); + expect((await rr('o', 'x', {pipedData: 'foobar', engine: 'nope', quietTotal: true})).err).toBe( + '' + ); }); test('is not the default', async () => { @@ -36,7 +36,7 @@ describe('--quiet-total', () => { expect(await rr('o', 'x', config)).toEqual({out: '', err: '', exit: null}); }); - test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async (flag) => { + test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async flag => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', { includeGlob: ['my.file'], diff --git a/test/js/quiet.test.js b/test/js/quiet.test.js index 95539ae5..8b54ab53 100644 --- a/test/js/quiet.test.js +++ b/test/js/quiet.test.js @@ -42,7 +42,7 @@ describe('--quiet', () => { expect(result.err).toContain('Open sync: my.file'); }); - test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async (flag) => { + test.each(['output', 'outputMatch'])('with --%s stdout is not affected', async flag => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', { includeGlob: ['my.file'], diff --git a/test/js/replacement-js.test.js b/test/js/replacement-js.test.js index 2bead76f..ee819f0a 100644 --- a/test/js/replacement-js.test.js +++ b/test/js/replacement-js.test.js @@ -84,9 +84,9 @@ describe('--replacement-js', () => { }); test('can be more than nine groups', async () => { - expect( - await js('(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)', '$11 + $10 + $1', 'abcdefghijk') - ).toBe('kja'); + expect(await js('(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)', '$11 + $10 + $1', 'abcdefghijk')).toBe( + 'kja' + ); }); test('make the code run once per match', async () => { @@ -105,9 +105,9 @@ describe('--replacement-js', () => { describe('variables', () => { test('fs, path and globs are available', async () => { - expect( - await inFile('typeof fs.readFileSync + typeof path.join + typeof globs.sync') - ).toBe('functionfunctionfunction'); + expect(await inFile('typeof fs.readFileSync + typeof path.join + typeof globs.sync')).toBe( + 'functionfunctionfunction' + ); expect(await inFile('fs.readFileSync(file, "utf8").toUpperCase()')).toBe('FOOBAR'); expect(await inFile('globs.sync("*").join()')).toBe('my.file'); }); @@ -202,9 +202,7 @@ describe('--replacement-js', () => { expect(await inFile('time')).toMatch(timestamp); expect(await inFile('now === time')).toBe('true'); expect(await inFile('time_obj instanceof Date')).toBe('true'); - expect(Math.abs(Date.now() - Number(await inFile('time_obj.getTime()')))).toBeLessThan( - 60000 - ); + expect(Math.abs(Date.now() - Number(await inFile('time_obj.getTime()')))).toBeLessThan(60000); }); test('mtime and ctime tell about the last change of the file', async () => { @@ -250,15 +248,15 @@ describe('--replacement-js', () => { 'bytes', 'size', ]; - const code = `[${names.map((name) => `${name}_ === ${name} + " "`).join()}].join()`; + const code = `[${names.map(name => `${name}_ === ${name} + " "`).join()}].join()`; expect(await inFile(code)).toBe(names.map(() => 'true').join()); expect(await js('foobar', code, 'foobar')).toBe(names.map(() => 'true').join()); }); test('are the same when the code runs once per match', async () => { - expect( - await inFile('$0; [filename, name, ext, bytes, text, find, _, nl].join("|")') - ).toBe('my.file|my|.file|6|foobar|^[^]*$| |\n'); + expect(await inFile('$0; [filename, name, ext, bytes, text, find, _, nl].join("|")')).toBe( + 'my.file|my|.file|6|foobar|^[^]*$| |\n' + ); }); test('follow the file when several files are handled', async () => { diff --git a/test/js/replacement-pipe.test.js b/test/js/replacement-pipe.test.js index b440232d..b0b8ad7d 100644 --- a/test/js/replacement-pipe.test.js +++ b/test/js/replacement-pipe.test.js @@ -40,9 +40,7 @@ describe('--replacement-pipe', () => { test('lets the piped data refer to the match and its groups', async () => { fs.writeFileSync('my.file', 'foobar'); const config = {includeGlob: ['my.file'], replacementPipe: true, output: true}; - expect((await rr('(o)(b)', '_', {...config, pipedData: '$2$1[$&]'})).out).toBe( - 'fobo[ob]ar\n' - ); + expect((await rr('(o)(b)', '_', {...config, pipedData: '$2$1[$&]'})).out).toBe('fobo[ob]ar\n'); }); test('can be an empty string to remove what is matched', async () => { diff --git a/test/js/unicode.test.js b/test/js/unicode.test.js index a4b0474d..df24f638 100644 --- a/test/js/unicode.test.js +++ b/test/js/unicode.test.js @@ -18,12 +18,11 @@ describe('--unicode', () => { test('understands unicode property escapes', async () => { expect( - (await rr('\\p{Lu}', '_', {pipedData: 'aÆbÅc', unicode: true, voidIgnoreCase: true})) - .out + (await rr('\\p{Lu}', '_', {pipedData: 'aÆbÅc', unicode: true, voidIgnoreCase: true})).out ).toBe('a_b_c'); - expect( - (await rr('\\p{Script=Greek}+', '_', {pipedData: 'abc αβγ', unicode: true})).out - ).toBe('abc _'); + expect((await rr('\\p{Script=Greek}+', '_', {pipedData: 'abc αβγ', unicode: true})).out).toBe( + 'abc _' + ); }); test('counts an emoji as one char in quantifiers and classes', async () => { diff --git a/test/js/verbose.test.js b/test/js/verbose.test.js index 8bce918d..dcaf7ac3 100644 --- a/test/js/verbose.test.js +++ b/test/js/verbose.test.js @@ -29,7 +29,7 @@ describe('--verbose', () => { }); test('tells how many files were found', async () => { - ['a', 'b', 'c'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['a', 'b', 'c'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); expect((await rr('o', 'x', {includeGlob: ['*.file'], verbose: true})).err).toContain( '3 files found' ); diff --git a/test/js/void-async.test.js b/test/js/void-async.test.js index df3163f3..0542b42d 100644 --- a/test/js/void-async.test.js +++ b/test/js/void-async.test.js @@ -6,7 +6,7 @@ import {rr} from './helper.js'; describe('--void-async', () => { test('is done with a file before the next one is opened', async () => { - ['a', 'b'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['a', 'b'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); const config = {includeGlob: ['*.file'], voidAsync: true, verbose: true}; const result = await rr('o', 'x', config); expect(result.err.trim().split('\n')).toEqual([ @@ -22,7 +22,7 @@ describe('--void-async', () => { }); test('is not the default: all files are opened before the first one is done', async () => { - ['a', 'b'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['a', 'b'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); const config = {includeGlob: ['*.file'], voidAsync: false, verbose: true}; const result = await rr('o', 'x', config); const lines = result.err.split('\n'); @@ -34,14 +34,11 @@ describe('--void-async', () => { // Either file can be read first expect(lines[3]).toMatch(/^Work on content from: [ab]\.file$/); expect(result.out.split('\n').sort()).toEqual(['', 'a.file', 'b.file']); - expect(['a', 'b'].map((name) => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ - 'fxx', - 'fxx', - ]); + expect(['a', 'b'].map(name => fs.readFileSync(name + '.file', 'utf8'))).toEqual(['fxx', 'fxx']); }); test('handles the files one by one in alphabetical order', async () => { - ['c', 'a', 'd', 'b'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['c', 'a', 'd', 'b'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); const result = await rr('o', 'x', {includeGlob: ['*.file'], voidAsync: true}); expect(result.out).toBe('a.file\nb.file\nc.file\nd.file\n'); }); @@ -81,21 +78,21 @@ describe('--void-async', () => { test.each(['output', 'outputMatch'])( 'is forced by --%s on several files to keep the order', - async (flag) => { - ['b', 'c', 'a'].forEach((name) => fs.writeFileSync(name + '.file', name + '\n')); + async flag => { + ['b', 'c', 'a'].forEach(name => fs.writeFileSync(name + '.file', name + '\n')); const config = {includeGlob: ['*.file'], voidAsync: false, [flag]: true}; expect((await rr('[abc]', '$&', config)).out).toBe('a\nb\nc\n'); } ); test('with --replacement-js and several files', async () => { - ['a', 'b', 'c'].forEach((name) => fs.writeFileSync(name + '.file', 'foo')); + ['a', 'b', 'c'].forEach(name => fs.writeFileSync(name + '.file', 'foo')); await rr('foo', 'name + name', { includeGlob: ['*.file'], voidAsync: true, replacementJs: true, }); - expect(['a', 'b', 'c'].map((name) => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ + expect(['a', 'b', 'c'].map(name => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ 'aa', 'bb', 'cc', diff --git a/test/js/void-backup.test.js b/test/js/void-backup.test.js index 0a0a846c..288677c2 100644 --- a/test/js/void-backup.test.js +++ b/test/js/void-backup.test.js @@ -20,9 +20,9 @@ describe('--void-backup', () => { }); test('handles several files', async () => { - ['a', 'b', 'c'].forEach((name) => fs.writeFileSync(name + '.file', 'foo' + name)); + ['a', 'b', 'c'].forEach(name => fs.writeFileSync(name + '.file', 'foo' + name)); await rr('foo', 'x', {includeGlob: ['*.file'], voidBackup: true}); - expect(['a', 'b', 'c'].map((name) => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ + expect(['a', 'b', 'c'].map(name => fs.readFileSync(name + '.file', 'utf8'))).toEqual([ 'xa', 'xb', 'xc', @@ -30,7 +30,7 @@ describe('--void-backup', () => { }); describe('combined', () => { - test.each([true, false])('with --void-async set to %p', async (voidAsync) => { + test.each([true, false])('with --void-async set to %p', async voidAsync => { fs.writeFileSync('my.file', 'foobar'); await rr('o', 'x', {includeGlob: ['my.file'], voidBackup: true, voidAsync}); expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); diff --git a/test/js/void-euro.test.js b/test/js/void-euro.test.js index 15730a64..7a74ecd3 100644 --- a/test/js/void-euro.test.js +++ b/test/js/void-euro.test.js @@ -15,9 +15,7 @@ describe('--void-euro', () => { test('makes € a plain char in the pattern', async () => { expect((await rr('.€', 'x', {pipedData: 'foobar', voidEuro: true})).out).toBe('foobar'); - expect((await rr('.€', 'x', {pipedData: 'price: 5€', voidEuro: true})).out).toBe( - 'price: x' - ); + expect((await rr('.€', 'x', {pipedData: 'price: 5€', voidEuro: true})).out).toBe('price: x'); }); test('makes € a plain char in the replacement', async () => { @@ -26,9 +24,7 @@ describe('--void-euro', () => { }); test('leaves $ working as usual', async () => { - expect((await rr('(.)$', '$1!', {pipedData: 'foobar', voidEuro: true})).out).toBe( - 'foobar!' - ); + expect((await rr('(.)$', '$1!', {pipedData: 'foobar', voidEuro: true})).out).toBe('foobar!'); }); test('never touches a € in the content', async () => { @@ -49,9 +45,9 @@ describe('--void-euro', () => { test('with --literal', async () => { expect((await rr('5€', 'x', {pipedData: '5€ 5$', literal: true})).out).toBe('5€ x'); - expect( - (await rr('5€', 'x', {pipedData: '5€ 5$', literal: true, voidEuro: true})).out - ).toBe('x 5$'); + expect((await rr('5€', 'x', {pipedData: '5€ 5$', literal: true, voidEuro: true})).out).toBe( + 'x 5$' + ); }); test('with --replacement-js the € is how to get to the groups', async () => { diff --git a/test/js/void-global.test.js b/test/js/void-global.test.js index a17c9882..96c59ca8 100644 --- a/test/js/void-global.test.js +++ b/test/js/void-global.test.js @@ -32,12 +32,8 @@ describe('--void-global', () => { }); test('with the default multiline only the first line is touched', async () => { - expect((await rr('^', '> ', {pipedData: 'a\nb\nc', voidGlobal: true})).out).toBe( - '> a\nb\nc' - ); - expect((await rr('$', '!', {pipedData: 'a\nb\nc', voidGlobal: true})).out).toBe( - 'a!\nb\nc' - ); + expect((await rr('^', '> ', {pipedData: 'a\nb\nc', voidGlobal: true})).out).toBe('> a\nb\nc'); + expect((await rr('$', '!', {pipedData: 'a\nb\nc', voidGlobal: true})).out).toBe('a!\nb\nc'); }); test('with --void-multiline $ is the end of the content', async () => { diff --git a/test/js/void-ignore-case.test.js b/test/js/void-ignore-case.test.js index 46e69cd9..d64a10ea 100644 --- a/test/js/void-ignore-case.test.js +++ b/test/js/void-ignore-case.test.js @@ -26,9 +26,7 @@ describe('--void-ignore-case', () => { test('applies to backreferences inside the pattern', async () => { expect((await rr('(a)\\1', '_', {pipedData: 'aA aa'})).out).toBe('_ _'); - expect((await rr('(a)\\1', '_', {pipedData: 'aA aa', voidIgnoreCase: true})).out).toBe( - 'aA _' - ); + expect((await rr('(a)\\1', '_', {pipedData: 'aA aa', voidIgnoreCase: true})).out).toBe('aA _'); }); describe('combined', () => { diff --git a/test/js/void-multiline.test.js b/test/js/void-multiline.test.js index bd89acbb..2778d7ba 100644 --- a/test/js/void-multiline.test.js +++ b/test/js/void-multiline.test.js @@ -18,9 +18,7 @@ describe('--void-multiline', () => { }); test('makes no difference for a single line', async () => { - expect((await rr('^f|r$', 'x', {pipedData: 'foobar', voidMultiline: true})).out).toBe( - 'xoobax' - ); + expect((await rr('^f|r$', 'x', {pipedData: 'foobar', voidMultiline: true})).out).toBe('xoobax'); }); test('does not see a final newline as the end of the content', async () => { diff --git a/test/js/void-section.test.js b/test/js/void-section.test.js index 7992dcbe..02c10999 100644 --- a/test/js/void-section.test.js +++ b/test/js/void-section.test.js @@ -58,12 +58,9 @@ describe('--void-section', () => { }); test('with --literal', async () => { - expect((await rr('a§b', 'x', {pipedData: 'a§b a\\b', literal: true})).out).toBe( - 'a§b x' - ); + expect((await rr('a§b', 'x', {pipedData: 'a§b a\\b', literal: true})).out).toBe('a§b x'); expect( - (await rr('a§b', 'x', {pipedData: 'a§b a\\b', literal: true, voidSection: true})) - .out + (await rr('a§b', 'x', {pipedData: 'a§b a\\b', literal: true, voidSection: true})).out ).toBe('x a\\b'); }); diff --git a/yarn.lock b/yarn.lock index 8f58935a..dce61abe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,60 @@ # yarn lockfile v1 +"@biomejs/biome@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.5.15.tgz#0a7d323f59b0d361e032ddcf7c4907d4f979eb22" + integrity sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "2.5.15" + "@biomejs/cli-darwin-x64" "2.5.15" + "@biomejs/cli-linux-arm64" "2.5.15" + "@biomejs/cli-linux-arm64-musl" "2.5.15" + "@biomejs/cli-linux-x64" "2.5.15" + "@biomejs/cli-linux-x64-musl" "2.5.15" + "@biomejs/cli-win32-arm64" "2.5.15" + "@biomejs/cli-win32-x64" "2.5.15" + +"@biomejs/cli-darwin-arm64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.15.tgz#0d5e5c6a672617cb61101efafc638f993b7985a8" + integrity sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg== + +"@biomejs/cli-darwin-x64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.15.tgz#bb0a1b696e7270ca187c514573baf9a82ba0fec5" + integrity sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw== + +"@biomejs/cli-linux-arm64-musl@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.15.tgz#63839d4c11081d53cffa0cecf388769734551d1b" + integrity sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw== + +"@biomejs/cli-linux-arm64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.15.tgz#ea868026056577eeb43c7e7a1517627115655e20" + integrity sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw== + +"@biomejs/cli-linux-x64-musl@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.15.tgz#e969cebdfbdc7c69eef154e20a0672e0184dc6f1" + integrity sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ== + +"@biomejs/cli-linux-x64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.15.tgz#4e5fb14b5ed66ff61e57510cbeef7dc5889d9959" + integrity sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA== + +"@biomejs/cli-win32-arm64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.15.tgz#a1c2c23500cce101ee9593623ed8af57794c3177" + integrity sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg== + +"@biomejs/cli-win32-x64@2.5.15": + version "2.5.15" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.15.tgz#f652af401e7177466d0f20b1ba7f3998ee317401" + integrity sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -402,11 +456,6 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -prettier@3.9.9: - version "3.9.9" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.9.tgz#09b826918c91cd4cbc80e0cbd1d2a922ff04f233" - integrity sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" From 4bd3b5a021912989ceca9c277f813826ff3d0e4f Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 10:06:01 +1100 Subject: [PATCH 9/9] Use the Biome config from argmate with only bin left out --- biome.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 143671f1..4043a395 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,7 @@ }, "linter": { "enabled": true, - "includes": ["**"], + "includes": ["**", "!bench/**", "!test/manual/**"], "rules": { "correctness": { "preset": "recommended" @@ -63,6 +63,6 @@ } ], "files": { - "includes": ["**", "!**/dist", "!**/bin", "!**/node_modules", "!**/*.html"] + "includes": ["**", "!**/dist", "!**/bin", "!**/node_modules"] } }