Skip to content

Commit

Permalink
Allow target file modification
Browse files Browse the repository at this point in the history
  • Loading branch information
adamreisnz committed Jun 24, 2024
1 parent 44ed184 commit c01bfa4
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 53 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
92 changes: 52 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions src/helpers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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
Expand Down Expand Up @@ -81,6 +83,7 @@ export function parseConfig(config) {
dry: false,
glob: {},
cwd: null,
getTargetFile: source => source,
fs,
fsSync,
}, config)
Expand Down
9 changes: 9 additions & 0 deletions src/helpers/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
26 changes: 16 additions & 10 deletions src/helpers/replace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/replace-in-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions src/replace-in-file.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit c01bfa4

Please sign in to comment.