Skip to content

Commit

Permalink
Add caching support for longer-running processes, so that the same .e…
Browse files Browse the repository at this point in the history
…ditorconfig file isn't read, parsed, and processed many times.
  • Loading branch information
hildjj committed Oct 14, 2022
1 parent 086b7f1 commit 7238e3d
Show file tree
Hide file tree
Showing 4 changed files with 450 additions and 158 deletions.
78 changes: 52 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,56 @@ $ npm install -g editorconfig

## Usage

### in Node.js:

#### parse(filePath[, options])
### Options

options is an object with the following defaults:
Most of the API takes an `options` object, which has the following defaults:

```js
{
config: '.editorconfig',
version: pkg.version,
root: '/',
files: undefined
files: undefined,
cache: undefined,
};
```

Search for `.editorconfig` starting from the current directory to the root directory.
<dl>
<dt>config</dt>
<dd>The name of the config file to look for in the current and every parent
directory.</dd>

<dt>version</dt>
<dd>Which editorconfig spec version to use. Earlier versions had different
defaults.</dd>

<dt>root</dt>
<dd>What directory to stop processing in, even if we haven't found a file
containing root=true. Defaults to the root of the filesystem containing
`process.cwd()`.</dd>

<dt>files</dt>
<dd>Pass in an empty array, which will be filled with one object for each
config file processed. The objects will have the shape
`{filename: "[DIRECTORY]/.editorconfig", glob: "*"}`</dd>

<dt>cache</dt>
<dd>If you are going to process more than one file in the same project, pass
in a cache object. It must have `get(string): object|undefined` and
`set(string, object)` methods, like a JavaScript Map. A long-running
process might want to consider that this cache might grow over time,
and that the config files might change over time. However, we leave any
complexity of that nature to the caller, since there are so many different
approaches that might be taken based on latency, memory, and CPU trade-offs.</dd>
</dl>

### in Node.js:

#### parse(filePath[, options])

Search for `.editorconfig` files starting from the current directory to the
root directory. Combine all of the sections whose section names match
filePath into a single object.

Example:

Expand Down Expand Up @@ -69,33 +103,25 @@ const filePath = path.join(__dirname, 'sample.js');
*/
```

When the `files` option is an array, it will be filled with objects that
describe which .editorcofig files and glob section names contributed to the
returned configuration.

#### parseSync(filePath[, options])

Synchronous version of `editorconfig.parse()`.

#### parseString(fileContent)
#### parseBuffer(fileContent)

The `parse()` function above uses `parseString()` under the hood. If you have your file contents
just pass it to `parseString()` and it'll return the same results as `parse()`.
The `parse()` function above uses `parseBuffer()` under the hood. If you have
the contents of a config file, and want to see what is being processed for
just that file rather than the full directory hierarchy, this might be useful.

#### parseFromFiles(filePath, configs[, options])
#### parseString(fileContent)

options is an object with the following defaults:
This is a thin wrapper around `parseBuffer()` for backward-compatibility.
Prefer `parseBuffer()` to avoid an unnecessary UTF8-to-UTF16-to-UTF8
conversion. Deprecated.

```js
{
config: '.editorconfig',
version: pkg.version,
root: '/',
files: undefined
};
```
#### parseFromFiles(filePath, configs[, options])

Specify the `.editorconfig`.
Low-level interface, which exists only for backward-compatibility. Deprecated.

Example:

Expand All @@ -115,7 +141,7 @@ const configs = [
const filePath = path.join(__dirname, '/sample.js');

(async () => {
console.log(await editorconfig.parseFromFiles(filePath, configs))
console.log(await editorconfig.parseFromFiles(filePath, Promise.resolve(configs)))
})();
/*
{
Expand All @@ -132,7 +158,7 @@ const filePath = path.join(__dirname, '/sample.js');

#### parseFromFilesSync(filePath, configs[, options])

Synchronous version of `editorconfig.parseFromFiles()`.
Synchronous version of `editorconfig.parseFromFiles()`. Deprecated.

### in Command Line

Expand Down
42 changes: 34 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,27 @@ import * as editorconfig from './'

import pkg from '../package.json'

/**
* Default output routine, goes to stdout.
*
* @param s String to output
*/
function writeStdOut(s: string): void {
process.stdout.write(s)
}

export default function cli(
/**
* Command line interface for editorconfig. Pulled out into a separate module
* to make it easier to test.
*
* @param args Usually process.argv. Note that the first two parameters are
* usually 'node' and 'editorconfig'
* @param testing If testing, you may pass in a Commander OutputConfiguration
* so that you can capture stdout and stderror. If `testing` is provided,
* this routine will throw an error instead of calling `process.exit`.
* @returns An array of combined properties, one for each file argument.
*/
export default async function cli(
args: string[],
testing?: OutputConfiguration
): Promise<editorconfig.Props[]> {
Expand Down Expand Up @@ -42,17 +58,27 @@ export default function cli(

const files = program.args
const opts = program.opts()
const cache = new Map<string, editorconfig.ProcessedFileConfig>()
const visited = opts.files ?
files.map<editorconfig.Visited[]>(() => []) :
undefined

return Promise.all(
files.map((filePath, i) => editorconfig.parse(filePath, {
config: opts.f as string,
version: opts.b as string,
files: visited ? visited[i] : undefined,
}))
).then((parsed) => {
// Process sequentially so caching works
async function processAll(): Promise<editorconfig.Props[]> {
const p = []
let i = 0
for (const filePath of files) {
p.push(await editorconfig.parse(filePath, {
config: opts.f as string,
version: opts.b as string,
files: visited ? visited[i++] : undefined,
cache,
}))
}
return p
}

return await processAll().then((parsed) => {
const header = parsed.length > 1
parsed.forEach((props, i) => {
if (header) {
Expand Down
50 changes: 50 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ describe('parse', () => {
visited[0].glob.should.eql('*')
visited[0].fileName.should.endWith('.editorconfig')
})

it('caches', async () => {
const cache = new Map()
const cfg = await editorconfig.parse(target, {cache})
cfg.should.eql(expected)
cache.size.should.be.eql(1)
await editorconfig.parse(target, {cache})
cache.size.should.be.eql(1)
})

it('caches sync', () => {
const cache = new Map()
const cfg = editorconfig.parseSync(target, {cache})
cfg.should.eql(expected)
cache.size.should.be.eql(1)
editorconfig.parseSync(target, {cache})
cache.size.should.be.eql(1)
})
})

describe('parseFromFiles', () => {
Expand All @@ -55,6 +73,10 @@ describe('parseFromFiles', () => {
contents: fs.readFileSync(configPath),
})
const target = path.join(__dirname, '/app.js')
const configs2 = [
{ name: 'early', contents: Buffer.alloc(0) },
configs[0],
]

it('async', async () => {
const cfg: editorconfig.Props =
Expand All @@ -75,6 +97,34 @@ describe('parseFromFiles', () => {
cfg.should.eql({ foo: 'null' })
})

it('caches async', async () => {
const cache = new Map()
const cfg = await editorconfig.parseFromFiles(
target, Promise.resolve(configs2), {cache}
)
cfg.should.eql(expected)
cache.size.should.be.eql(2)
const cfg2 = await editorconfig.parseFromFiles(
target, Promise.resolve(configs2), {cache}
)
cfg2.should.eql(expected)
cache.size.should.be.eql(2)
})

it('caches sync', () => {
const cache = new Map()
const cfg = editorconfig.parseFromFilesSync(
target, configs2, {cache}
)
cfg.should.eql(expected)
cache.size.should.be.eql(2)
const cfg2 = editorconfig.parseFromFilesSync(
target, configs2, {cache}
)
cfg2.should.eql(expected)
cache.size.should.be.eql(2)
})

it('handles minimatch escapables', () => {
// Note that this `#` does not actually test the /^#/ escaping logic,
// because this path will go through a `path.dirname` before that happens.
Expand Down

0 comments on commit 7238e3d

Please sign in to comment.