Skip to content

Commit fe5997e

Browse files
danielroepi0
andauthored
feat: add readNearest* and findNearest* utilities (#3)
Co-authored-by: Pooya Parsa <pyapar@gmail.com>
1 parent 77ad42e commit fe5997e

5 files changed

Lines changed: 190 additions & 2 deletions

File tree

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ import { writePackageJSON } from 'pkg-types'
3434
await writePackageJSON('path/to/package.json', pkg)
3535
```
3636

37+
### `findNearestPackageJSON`
38+
39+
```js
40+
import { findNearestPackageJSON } from 'pkg-types'
41+
const filename = await findNearestPackageJSON()
42+
// or
43+
const packageJson = await findNearestPackageJSON('/fully/resolved/path/to/folder')
44+
```
45+
46+
### `readNearestPackageJSON`
47+
48+
```js
49+
import { readNearestPackageJSON } from 'pkg-types'
50+
const filename = await readNearestPackageJSON()
51+
// or
52+
const packageJson = await readNearestPackageJSON('/fully/resolved/path/to/folder')
53+
```
54+
3755
### `readTSConfig`
3856

3957
```js
@@ -50,6 +68,35 @@ import { writeTSConfig } from 'pkg-types'
5068
await writeTSConfig('path/to/tsconfig.json', tsconfig)
5169
```
5270

71+
### `findNearestTSConfig`
72+
73+
```js
74+
import { findNearestTSConfig } from 'pkg-types'
75+
const filename = await findNearestTSConfig()
76+
// or
77+
const tsconfig = await findNearestTSConfig('/fully/resolved/path/to/folder')
78+
```
79+
80+
### `readNearestTSConfig`
81+
82+
```js
83+
import { readNearestTSConfig } from 'pkg-types'
84+
const filename = await readNearestTSConfig()
85+
// or
86+
const tsconfig = await readNearestTSConfig('/fully/resolved/path/to/folder')
87+
```
88+
89+
### `findNearestFile`
90+
91+
```js
92+
import { findNearestFile } from 'pkg-types'
93+
const filename = await findNearestFile('README.md', {
94+
startingFrom: id,
95+
rootPattern: /^node_modules$/,
96+
matcher: filename => filename.endsWith('.md'),
97+
})
98+
```
99+
53100
## Types
54101

55102
**Note:** In order to make types working, you need to install `typescript` as a devDependency.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
"build": "unbuild",
1818
"release": "standard-version && npm publish && git push --follow-tags",
1919
"test": "mocha -r jiti/register ./test/**/*.test.*",
20-
"test:types": "tsc --noEmit --module esnext --moduleResolution node ./test/*.test.ts"
20+
"test:types": "tsc --noEmit --module esnext --skipLibCheck --moduleResolution node ./test/*.test.ts"
2121
},
2222
"dependencies": {
23-
"jsonc-parser": "^3.0.0"
23+
"jsonc-parser": "^3.0.0",
24+
"pathe": "^0.2.0"
2425
},
2526
"devDependencies": {
2627
"@types/chai": "^4.2.22",

src/fs.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { statSync } from 'fs'
2+
import { join, resolve } from 'pathe'
3+
import { PackageJson, readPackageJSON, readTSConfig, TSConfig } from '.'
4+
5+
export interface FindNearestFileOptions {
6+
/**
7+
* The starting directory for the search.
8+
* @default . (same as `process.cwd()`)
9+
*/
10+
startingFrom?: string
11+
/**
12+
* A pattern to match a path segment above which you don't want to ascend
13+
* @default /^node_modules$/
14+
*/
15+
rootPattern?: RegExp
16+
/**
17+
* A matcher that can evaluate whether the given path is a valid file (for example,
18+
* by testing whether the file path exists.
19+
*
20+
* @default fs.statSync(path).isFile()
21+
*/
22+
test?: (filePath: string) => boolean | null | Promise<boolean | null>
23+
}
24+
25+
const defaultFindOptions: Required<FindNearestFileOptions> = {
26+
startingFrom: '.',
27+
rootPattern: /^node_modules$/,
28+
test: (filePath: string) => {
29+
try {
30+
if (statSync(filePath).isFile()) { return true }
31+
} catch { }
32+
return null
33+
}
34+
}
35+
36+
export async function findNearestFile (filename: string, _options: FindNearestFileOptions = {}) {
37+
const options = { ...defaultFindOptions, ..._options }
38+
const basePath = resolve(options.startingFrom)
39+
const leadingSlash = basePath[0] === '/'
40+
const segments = basePath.split('/').filter(Boolean)
41+
42+
// Restore leading slash
43+
if (leadingSlash) {
44+
segments[0] = '/' + segments[0]
45+
}
46+
47+
// Limit to node_modules scope if it exists
48+
let root = segments.findIndex(r => r.match(options.rootPattern))
49+
if (root === -1) root = 0
50+
51+
for (let i = segments.length; i > root; i--) {
52+
const filePath = join(...segments.slice(0, i), filename)
53+
if (await options.test(filePath)) { return filePath }
54+
}
55+
56+
return null
57+
}
58+
59+
export async function findNearestPackageJSON (id: string = process.cwd()): Promise<string | null> {
60+
return findNearestFile('package.json', { startingFrom: id })
61+
}
62+
63+
export async function findNearestTSConfig (id: string = process.cwd()): Promise<string | null> {
64+
return findNearestFile('tsconfig.json', { startingFrom: id })
65+
}
66+
67+
export async function readNearestPackageJSON (id?: string): Promise<PackageJson | null> {
68+
const filePath = await findNearestPackageJSON(id)
69+
70+
if (!filePath) { return null }
71+
72+
return readPackageJSON(filePath)
73+
}
74+
75+
export async function readNearestTSConfig (id?: string): Promise<TSConfig | null> {
76+
const filePath = await findNearestTSConfig(id)
77+
78+
if (!filePath) { return null }
79+
80+
return readTSConfig(filePath)
81+
}
82+

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { promises as fsp } from 'fs'
33
import type { PackageJson, TSConfig } from './types'
44

55
export * from './types'
6+
export * from './fs'
67

78
export function definePackageJSON(pkg: PackageJson): PackageJson {
89
return pkg

test/fs.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { fileURLToPath } from 'url'
2+
import { dirname, resolve } from 'path'
3+
import { expect } from 'chai'
4+
import {
5+
readNearestPackageJSON,
6+
readNearestTSConfig,
7+
findNearestPackageJSON,
8+
findNearestTSConfig,
9+
PackageJson,
10+
TSConfig,
11+
} from '../src'
12+
13+
14+
const fixtureDir = resolve(dirname(fileURLToPath(import.meta.url)), 'fixture')
15+
16+
const rFixture = (...p: string[]) => resolve(fixtureDir, ...p)
17+
18+
const tests = {
19+
'tsconfig.json': [readNearestTSConfig, findNearestTSConfig],
20+
'package.json': [readNearestPackageJSON, findNearestPackageJSON]
21+
} as const
22+
for (const filename in tests) {
23+
const [read, find] = tests[filename as 'tsconfig.json']
24+
describe(find.name, () => {
25+
it('finds a package.json in root directory', async () => {
26+
const pkgPath = await find(rFixture('.'))
27+
expect(pkgPath).to.equal(rFixture(filename))
28+
})
29+
it('handles non-existent paths', async () => {
30+
const pkgPath = await find(rFixture('further', 'dir', 'file.json'))
31+
expect(pkgPath).to.equal(rFixture(filename))
32+
})
33+
it('works all the way up the tree', async () => {
34+
const pkgPath = await find('/a/full/nonexistent/path')
35+
expect(pkgPath).to.equal(null)
36+
})
37+
it('stops at `node_modules`', async () => {
38+
const pkgPath = await find(rFixture('further', 'node_modules', 'file.json'))
39+
expect(pkgPath).to.equal(null)
40+
})
41+
it(`finds the working directory ${filename}`, async () => {
42+
const pkgPath = await find()
43+
expect(pkgPath).to.equal(rFixture('../..', filename))
44+
})
45+
})
46+
47+
describe(read.name, () => {
48+
it('correctly reads a package', async () => {
49+
const data = await read(rFixture(filename))
50+
if (filename === 'package.json') {
51+
expect((data as PackageJson).name).to.equal('foo')
52+
} else {
53+
expect((data as TSConfig).include).to.contain('src')
54+
}
55+
})
56+
})
57+
}

0 commit comments

Comments
 (0)