Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial implementation of processorAsync #205

Merged
merged 4 commits into from
Dec 17, 2024
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
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 @@ -28,6 +28,7 @@ declare module 'replace-in-file' {
dry?: boolean;
glob?: object;
processor?: ProcessorCallback | Array<ProcessorCallback>;
processorAsync?: ProcessorAsyncCallback | Array<ProcessorAsyncCallback>;
}

export interface ReplaceResult {
Expand All @@ -41,3 +42,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>;