From c01bfa4eb15640bb8b0682c4727e6c7c2c147ee0 Mon Sep 17 00:00:00 2001 From: Adam Reis Date: Mon, 24 Jun 2024 14:20:39 +1200 Subject: [PATCH] Allow target file modification --- CHANGELOG.md | 12 +++++ README.md | 92 +++++++++++++++++++++---------------- src/helpers/config.js | 7 ++- src/helpers/config.spec.js | 9 ++++ src/helpers/replace.js | 26 +++++++---- src/replace-in-file.js | 2 +- src/replace-in-file.spec.js | 29 ++++++++++++ 7 files changed, 124 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70bb832..493af10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ The package has been converted to an ES module and now requires Node 18 or highe - To use a custom `fs` implementation, you must now specify `fs` config parameter for the async API, and `fsSync` for the sync API. For the asynchronous APIs, the provided `fs` must provide the `readFile` and `writeFile` methods. For the synchronous APIs, the provided `fsSync` must provide the `readFileSync` and `writeFileSync` methods. - If a `cwd` parameter is provided, it will no longer be prefixed to each path using basic string concatenation, but rather uses `path.join()` to ensure correct path concatenation. +### New features +You can now specify a `getTargetFile` config param to modify the target file for saving the new file contents to. For example: + +```js +const options = { + files: 'path/to/files/*.html', + getTargetFile: source => `new/path/${source}`, + from: 'foo', + to: 'bar', +} +``` + ## 7.0.0 Strings provided to the `from` value are now escaped for regex matching when counting of matches is enabled. This is unlikely to result in any breaking changes, but as a precaution the major version has been bumped. diff --git a/README.md b/README.md index 18a0bcb..54b3b16 100644 --- a/README.md +++ b/README.md @@ -171,46 +171,6 @@ console.log(results) // ] ``` -### Custom processor - -For advanced usage where complex processing is needed it's possible to use a callback that will receive content as an argument and should return it processed. - -```js -const results = replaceInFileSync({ - files: 'path/to/files/*.html', - processor: (input) => input.replace(/foo/g, 'bar'), -}) -``` -The custom processor will receive the path of the file being processed as a second parameter: - -```js -const results = replaceInFileSync({ - files: 'path/to/files/*.html', - processor: (input, file) => input.replace(/foo/g, file), -}) -``` - -### Array of custom processors - -Passing processor function also supports passing an array of functions that will be executed sequentially - -```js -function someProcessingA(input) { - const chapters = input.split('###') - chapters[1] = chapters[1].replace(/foo/g, 'bar') - return chapters.join('###') -} - -function someProcessingB(input) { - return input.replace(/foo/g, 'bar') -} - -const results = replaceInFileSync({ - files: 'path/to/files/*.html', - processor: [someProcessingA, someProcessingB], -}) -``` - ## Advanced usage ### Replace a single file or glob @@ -344,6 +304,18 @@ const options = { } ``` +### Saving to a different file +You can specify a `getTargetFile` config param to modify the target file for saving the new file contents to. For example: + +```js +const options = { + files: 'path/to/files/*.html', + getTargetFile: source => `new/path/${source}`, + from: 'foo', + to: 'bar', +} +``` + ### Ignore a single file or glob ```js @@ -422,6 +394,46 @@ const options = { } ``` +### Custom processor + +For advanced usage where complex processing is needed it's possible to use a callback that will receive content as an argument and should return it processed. + +```js +const results = await replaceInFile({ + files: 'path/to/files/*.html', + processor: (input) => input.replace(/foo/g, 'bar'), +}) +``` +The custom processor will receive the path of the file being processed as a second parameter: + +```js +const results = await replaceInFile({ + files: 'path/to/files/*.html', + processor: (input, file) => input.replace(/foo/g, file), +}) +``` + +### Array of custom processors + +Passing processor function also supports passing an array of functions that will be executed sequentially + +```js +function someProcessingA(input) { + const chapters = input.split('###') + chapters[1] = chapters[1].replace(/foo/g, 'bar') + return chapters.join('###') +} + +function someProcessingB(input) { + return input.replace(/foo/g, 'bar') +} + +const results = replaceInFileSync({ + files: 'path/to/files/*.html', + processor: [someProcessingA, someProcessingB], +}) +``` + ### File system `replace-in-file` defaults to using `'node:fs/promises'` and `'node:fs'` to provide file reading and write APIs. You can provide an `fs` or `fsSync` object of your own to switch to a different file system, such as a mock file system for unit tests. diff --git a/src/helpers/config.js b/src/helpers/config.js index b324caf..044a181 100644 --- a/src/helpers/config.js +++ b/src/helpers/config.js @@ -31,14 +31,13 @@ export function parseConfig(config) { config.glob = config.glob || {} //Extract data - const {files, from, to, processor, ignore, encoding} = config + const {files, getTargetFile, from, to, processor, ignore, encoding} = config if (typeof processor !== 'undefined') { if (typeof processor !== 'function' && !Array.isArray(processor)) { throw new Error(`Processor should be either a function or an array of functions`) } } else { - //Validate values if (typeof files === 'undefined') { throw new Error('Must specify file or files') } @@ -48,6 +47,9 @@ export function parseConfig(config) { if (typeof to === 'undefined') { throw new Error('Must specify a replacement (can be blank string)') } + if (typeof getTargetFile !== 'undefined' && typeof getTargetFile !== 'function') { + throw new Error(`Target file transformation parameter should be a function that takes the source file path as argument and returns the target file path`) + } } //Ensure arrays @@ -81,6 +83,7 @@ export function parseConfig(config) { dry: false, glob: {}, cwd: null, + getTargetFile: source => source, fs, fsSync, }, config) diff --git a/src/helpers/config.spec.js b/src/helpers/config.spec.js index 5c19a8f..fadb3ab 100644 --- a/src/helpers/config.spec.js +++ b/src/helpers/config.spec.js @@ -127,6 +127,15 @@ describe('helpers/config.js', () => { })).to.throw(Error) }) + it('should error when an invalid `getTargetFile` handler is specified', () => { + expect(() => parseConfig({ + getTargetFile: 'foo', + files: ['test1', 'test2', 'test3'], + from: [/re/g, /place/g], + to: ['b'], + })).to.throw(Error) + }) + it('should convert `files` to an array', () => { const parsed = parseConfig({ files: 'test1', diff --git a/src/helpers/replace.js b/src/helpers/replace.js index 318f0ba..5a816af 100644 --- a/src/helpers/replace.js +++ b/src/helpers/replace.js @@ -86,20 +86,23 @@ export function makeReplacements(contents, from, to, file, count) { /** * Helper to replace in a single file (sync) */ -export function replaceSync(file, from, to, config) { +export function replaceSync(source, from, to, config) { //Extract relevant config and read file contents - const {encoding, dry, countMatches, fsSync} = config - const contents = fsSync.readFileSync(file, encoding) + const {getTargetFile, encoding, dry, countMatches, fsSync} = config + const contents = fsSync.readFileSync(source, encoding) //Replace contents and check if anything changed const [result, newContents] = makeReplacements( - contents, from, to, file, countMatches + contents, from, to, source, countMatches ) + //Get target file + const target = getTargetFile(source) + //Contents changed and not a dry run? Write to file if (result.hasChanged && !dry) { - fsSync.writeFileSync(file, newContents, encoding) + fsSync.writeFileSync(target, newContents, encoding) } //Return result @@ -109,20 +112,23 @@ export function replaceSync(file, from, to, config) { /** * Helper to replace in a single file (async) */ -export async function replaceAsync(file, from, to, config) { +export async function replaceAsync(source, from, to, config) { //Extract relevant config and read file contents - const {encoding, dry, countMatches, fs} = config - const contents = await fs.readFile(file, encoding) + const {getTargetFile, encoding, dry, countMatches, fs} = config + const contents = await fs.readFile(source, encoding) //Make replacements const [result, newContents] = makeReplacements( - contents, from, to, file, countMatches + contents, from, to, source, countMatches ) + //Get target file + const target = getTargetFile(source) + //Contents changed and not a dry run? Write to file if (result.hasChanged && !dry) { - await fs.writeFile(file, newContents, encoding) + await fs.writeFile(target, newContents, encoding) } //Return result diff --git a/src/replace-in-file.js b/src/replace-in-file.js index 58de1d1..8df67f3 100644 --- a/src/replace-in-file.js +++ b/src/replace-in-file.js @@ -23,7 +23,7 @@ export async function replaceInFile(config) { //Find paths and process them const paths = await pathsAsync(files, config) - const promises = paths.map(file => replaceAsync(file, from, to, config)) + const promises = paths.map(path => replaceAsync(path, from, to, config)) const results = await Promise.all(promises) //Return results diff --git a/src/replace-in-file.spec.js b/src/replace-in-file.spec.js index e300870..f47bc88 100644 --- a/src/replace-in-file.spec.js +++ b/src/replace-in-file.spec.js @@ -104,6 +104,22 @@ describe('Replace in file', () => { }) }) + it(`should store in the correct target file if getTargetFile is used`, done => { + replaceInFile({ + files: 'test1', + getTargetFile: () => 'test2', + from: 're place', + to: 'b', + }) + .then(() => { + const test1 = fs.readFileSync('test1', 'utf8') + const test2 = fs.readFileSync('test2', 'utf8') + expect(test1).to.equal('a re place c') + expect(test2).to.equal('a b c') + done() + }) + }) + it(`should pass the match as first arg and file as last arg to a replacer function and replace contents with a string replacement`, done => { replaceInFile({ files: 'test1', @@ -507,6 +523,19 @@ describe('Replace in file', () => { expect(test1).to.equal('a b c') }) + it(`should store in the correct target file if getTargetFile is used`, () => { + replaceInFileSync({ + files: 'test1', + getTargetFile: () => 'test2', + from: 're place', + to: 'b', + }) + const test1 = fs.readFileSync('test1', 'utf8') + const test2 = fs.readFileSync('test2', 'utf8') + expect(test1).to.equal('a re place c') + expect(test2).to.equal('a b c') + }) + it(`should pass the match as first arg and file as last arg to a replacer function and replace contents with a string replacement`, function() { replaceInFileSync({ files: 'test1',