Skip to content

Commit

Permalink
feat: initial implementation of processorAsync (#205)
Browse files Browse the repository at this point in the history
* feat: initial implementation of processorAsync

* feat: types

* docs: how to use

* feat: test
  • Loading branch information
johnnyreilly authored Dec 17, 2024
1 parent 5e33265 commit 4ae6ac9
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 8 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,10 @@ const options = {
glob: {

//To include hidden files (starting with a dot)
dot: true,
dot: true,

//To fix paths on Windows OS when path.join() is used to create paths
windowsPathsNoEscape: true,
windowsPathsNoEscape: true,
},
}
```
Expand Down Expand Up @@ -437,6 +437,18 @@ const results = replaceInFileSync({
})
```

Alongside the `processor`, there is also `processorAsync` which is the equivalent for asynchronous processing. It should return a promise that resolves with the processed content:

```js
const results = await replaceInFile({
files: 'path/to/files/*.html',
processorAsync: async (input, file) => {
const asyncResult = await doAsyncOperation(input, file);
return input.replace(/foo/g, asyncResult)
},
})
```

### Using a custom file system API
`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: 6 additions & 1 deletion src/helpers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ export function parseConfig(config) {
config.glob = config.glob || {}

//Extract data
const {files, getTargetFile, from, to, processor, ignore, encoding} = config
const {files, getTargetFile, from, to, processor, processorAsync, 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 if (typeof processorAsync !== 'undefined') {
if (typeof processorAsync !== 'function' && !Array.isArray(processorAsync)) {
throw new Error(`ProcessorAsync should be either a function or an array of functions`)
}
}
else {
if (typeof files === 'undefined') {
throw new Error('Must specify file or files')
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 @@ -96,6 +96,15 @@ describe('helpers/config.js', () => {
})).to.throw(Error)
})

it('should error when an invalid `processorAsync` is specified', () => {
expect(() => parseConfig({
processorAsync: 'foo',
files: ['test1', 'test2', 'test3'],
from: [/re/g, /place/g],
to: ['b'],
})).to.throw(Error)
})

it('should error when `files` are not specified', () => {
expect(() => parseConfig({
from: [/re/g, /place/g],
Expand Down
24 changes: 23 additions & 1 deletion src/helpers/process.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ export function processSync(file, processor, config) {
return result
}

/**
* Run processors (async)
*/
export async function runProcessorsAsync(contents, processorAsync, file) {

//Ensure array and prepare result
const processorAsyncs = Array.isArray(processorAsync) ? processorAsync : [processorAsync]

//Run processors
let newContents = contents
for (const processor of processorAsyncs) {
newContents = await processor(newContents, file)
}

//Check if contents changed and prepare result
const hasChanged = (newContents !== contents)
const result = {file, hasChanged}

//Return along with new contents
return [result, newContents]
}

/**
* Helper to process in a single file (async)
*/
Expand All @@ -50,7 +72,7 @@ export async function processAsync(file, processor, config) {
const contents = await fs.readFile(file, encoding)

//Make replacements
const [result, newContents] = runProcessors(contents, processor, file)
const [result, newContents] = await runProcessorsAsync(contents, processor, file)

//Contents changed and not a dry run? Write to file
if (result.hasChanged && !dry) {
Expand Down
4 changes: 2 additions & 2 deletions src/process-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ export async function processFile(config) {

//Parse config
config = parseConfig(config)
const {files, processor, dry, verbose} = config
const {files, processor, processorAsync, dry, verbose} = config

//Dry run?
logDryRun(dry && verbose)

//Find paths and process them
const paths = await pathsAsync(files, config)
const promises = paths.map(path => processAsync(path, processor, config))
const promises = paths.map(path => processAsync(path, processor ?? processorAsync, config))
const results = await Promise.all(promises)

//Return results
Expand Down
18 changes: 17 additions & 1 deletion src/process-file.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ describe('Process a file', () => {
})
})

it('should run processorAsync', done => {
processFile({
files: 'test1',
processorAsync: async (input) => {
const replaceValue = await Promise.resolve('b')
return input.replace(/re\splace/g, replaceValue)
},
}).then(() => {
const test1 = fs.readFileSync('test1', 'utf8')
const test2 = fs.readFileSync('test2', 'utf8')
expect(test1).to.equal('a b c')
expect(test2).to.equal(testData)
done()
})
})

it('should replace contents in a single file with regex', done => {
processFile(fromToToProcessor({
files: 'test1',
Expand Down Expand Up @@ -400,7 +416,7 @@ describe('Process a file', () => {
expect(results[0].hasChanged).to.equal(false)
})

it('should return corret results for multiple files', function() {
it('should return correct results for multiple files', function() {
const results = processFileSync(fromToToProcessor({
files: ['test1', 'test2', 'test3'],
from: /re\splace/g,
Expand Down
6 changes: 5 additions & 1 deletion src/replace-in-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {processFile, processFileSync} from './process-file.js'
export async function replaceInFile(config) {

//If custom processor is provided use it instead
if (config && config.processor) {
if (config && (config.processor || config.processorAsync)) {
return await processFile(config)
}

Expand All @@ -35,6 +35,10 @@ export async function replaceInFile(config) {
*/
export function replaceInFileSync(config) {

if (config && config.processorAsync) {
throw new Error('ProcessorAsync cannot be used in synchronous mode')
}

//If custom processor is provided use it instead
if (config && config.processor) {
return processFileSync(config)
Expand Down
10 changes: 10 additions & 0 deletions src/replace-in-file.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,16 @@ describe('Replace in file', () => {
*/
describe('Sync', () => {

it('should error with processorAsync', function() {
return expect(() => replaceInFileSync({
files: 'test1',
processorAsync: async (input) => {
const replaceValue = await Promise.resolve('b')
return input.replace(/re\splace/g, replaceValue)
},
})).to.throw(Error)
})

it('should replace contents in a single file with regex', function() {
replaceInFileSync({
files: 'test1',
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ declare module 'replace-in-file' {
dry?: boolean;
glob?: object;
processor?: ProcessorCallback | Array<ProcessorCallback>;
processorAsync?: ProcessorAsyncCallback | Array<ProcessorAsyncCallback>;
}

export interface ReplaceResult {
Expand All @@ -42,3 +43,4 @@ declare module 'replace-in-file' {
type FromCallback = (file: string) => string | RegExp | (RegExp | string)[];
type ToCallback = (match: string, file: string) => string | string[];
type ProcessorCallback = (input: string, file: string) => string;
type ProcessorAsyncCallback = (input: string, file: string) => Promise<string>;

0 comments on commit 4ae6ac9

Please sign in to comment.