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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .prettierignore

This file was deleted.

6 changes: 0 additions & 6 deletions .prettierrc

This file was deleted.

68 changes: 68 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -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": ["**", "!bench/**", "!test/manual/**"],
"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"]
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/alias.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
12 changes: 6 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -461,7 +461,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 '\\\\';
}
Expand All @@ -482,7 +482,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];
}
Expand Down Expand Up @@ -547,13 +547,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.
Expand All @@ -567,7 +567,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;
});

Expand Down
29 changes: 13 additions & 16 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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)
);

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 = '❌',
Expand Down Expand Up @@ -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)}`;
}
Expand All @@ -729,19 +726,19 @@ function getFilePaths(conf) {

// With --absolute-path each file is known by its full path from here on. That is what --exclude-re sees, and the name that is printed.
if (conf.absolutePath) {
files = files.map((file) => path.resolve(file));
files = files.map(file => path.resolve(file));
}

// --exclude-glob is always globs, also for the name of a file. They are used together, and the files they find are taken away.
if (excludeGlob.length) {
const excluded = new Set(globs.sync(excludeGlob).map((file) => path.resolve(file)));
files = files.filter((file) => !excluded.has(path.resolve(file)));
const excluded = new Set(globs.sync(excludeGlob).map(file => path.resolve(file)));
files = files.filter(file => !excluded.has(path.resolve(file)));
}

excludeRe
.map((el) => getFinalRegex(getFinalPattern(el, conf), conf))
.forEach((re) => {
files = files.filter((file) => !file.match(re));
.map(el => getFinalRegex(getFinalPattern(el, conf), conf))
.forEach(re => {
files = files.filter(file => !file.match(re));
});

return files.sort();
Expand All @@ -752,7 +749,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 {
Expand All @@ -762,7 +759,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;
Expand Down
2 changes: 1 addition & 1 deletion src/output.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down
13 changes: 6 additions & 7 deletions test/fuzzy/basics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
});
});
26 changes: 7 additions & 19 deletions test/fuzzy/crossover.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions test/fuzzy/dot-all.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'), '_')
);
Expand Down
8 changes: 3 additions & 5 deletions test/fuzzy/encoding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
Loading
Loading