Skip to content

Commit

Permalink
feat: findFarthestFile (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Sep 6, 2022
1 parent 9189bca commit 0d8319e
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 12 deletions.
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -16,9 +16,10 @@
],
"scripts": {
"prepack": "unbuild",
"release": "standard-version && pnpm publish && git push --follow-tags",
"dev": "vitest",
"release": "pnpm test && standard-version && pnpm publish && git push --follow-tags",
"lint": "eslint --ext .ts,.js,.mjs,.cjs .",
"test": "vitest run",
"test": "vitest run --coverage",
"test:types": "tsc --noEmit --module esnext --skipLibCheck --moduleResolution node ./test/*.test.ts"
},
"dependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
@@ -1,13 +1,13 @@
import { promises as fsp } from 'fs'
import { ResolveOptions as _ResolveOptions, resolvePath } from 'mlly'
import { isAbsolute } from 'pathe'
import { findNearestFile, FindNearestFileOptions } from './utils'
import { FindFileOptions, findNearestFile } from './utils'
import type { PackageJson, TSConfig } from './types'

export * from './types'
export * from './utils'

export type ResolveOptions = _ResolveOptions & FindNearestFileOptions
export type ResolveOptions = _ResolveOptions & FindFileOptions

export function definePackageJSON (pkg: PackageJson): PackageJson {
return pkg
Expand Down
35 changes: 29 additions & 6 deletions src/utils.ts
@@ -1,7 +1,7 @@
import { statSync } from 'fs'
import { join, resolve } from 'pathe'

export interface FindNearestFileOptions {
export interface FindFileOptions {
/**
* The starting directory for the search.
* @default . (same as `process.cwd()`)
Expand All @@ -12,6 +12,10 @@ export interface FindNearestFileOptions {
* @default /^node_modules$/
*/
rootPattern?: RegExp
/**
* If true, search starts from root level descending into subdirectories
*/
reverse?: boolean
/**
* A matcher that can evaluate whether the given path is a valid file (for example,
* by testing whether the file path exists.
Expand All @@ -21,9 +25,13 @@ export interface FindNearestFileOptions {
test?: (filePath: string) => boolean | null | Promise<boolean | null>
}

const defaultFindOptions: Required<FindNearestFileOptions> = {
/** @deprecated */
export type FindNearestFileOptions = FindFileOptions

const defaultFindOptions: Required<FindFileOptions> = {
startingFrom: '.',
rootPattern: /^node_modules$/,
reverse: false,
test: (filePath: string) => {
try {
if (statSync(filePath).isFile()) { return true }
Expand All @@ -32,7 +40,7 @@ const defaultFindOptions: Required<FindNearestFileOptions> = {
}
}

export async function findNearestFile (filename: string, _options: FindNearestFileOptions = {}): Promise<string> {
export async function findFile (filename: string, _options: FindFileOptions = {}): Promise<string> {
const options = { ...defaultFindOptions, ..._options }
const basePath = resolve(options.startingFrom)
const leadingSlash = basePath[0] === '/'
Expand All @@ -47,10 +55,25 @@ export async function findNearestFile (filename: string, _options: FindNearestFi
let root = segments.findIndex(r => r.match(options.rootPattern))
if (root === -1) { root = 0 }

for (let i = segments.length; i > root; i--) {
const filePath = join(...segments.slice(0, i), filename)
if (await options.test(filePath)) { return filePath }
if (!options.reverse) {
for (let i = segments.length; i > root; i--) {
const filePath = join(...segments.slice(0, i), filename)
if (await options.test(filePath)) { return filePath }
}
} else {
for (let i = root + 1; i < segments.length; i++) {
const filePath = join(...segments.slice(0, i), filename)
if (await options.test(filePath)) { return filePath }
}
}

throw new Error(`Cannot find matching ${filename} in ${options.startingFrom} or parent directories`)
}

export function findNearestFile (filename: string, _options: FindFileOptions = {}): Promise<string> {
return findFile(filename, _options)
}

export function findFarthestFile (filename: string, _options: FindFileOptions = {}): Promise<string> {
return findFile(filename, { ..._options, reverse: true })
}
4 changes: 4 additions & 0 deletions test/fixture/sub/package.json
@@ -0,0 +1,4 @@
{
"name": "foo-sub",
"version": "1.0.0"
}
9 changes: 7 additions & 2 deletions test/index.test.ts
Expand Up @@ -9,7 +9,8 @@ import {
resolvePackageJSON,
writePackageJSON,
writeTSConfig,
TSConfig
TSConfig,
ResolveOptions
} from '../src'

const fixtureDir = resolve(dirname(fileURLToPath(import.meta.url)), 'fixture')
Expand All @@ -20,11 +21,15 @@ async function expectToReject (p: Promise<any>) {
return expect(await p.then(() => null).catch((err: Error) => err.toString()))
}

function testResolve (filename: string, resolveFn: (id?: string) => Promise<string | null>) {
function testResolve (filename: string, resolveFn: (id?: string, opts?: ResolveOptions) => Promise<string | null>) {
it('finds a package.json in root directory', async () => {
const pkgPath = await resolveFn(rFixture('.'))
expect(pkgPath).to.equal(rFixture(filename))
})
it('finds package.json from root', async () => {
const pkgPath = await resolveFn(rFixture('.'), { reverse: true })
expect(pkgPath).to.equal(rFixture('../..', filename)) // Top level in pkg-types repo
})
it('handles non-existent paths', async () => {
const pkgPath = await resolveFn(rFixture('further', 'dir', 'file.json'))
expect(pkgPath).to.equal(rFixture(filename))
Expand Down

0 comments on commit 0d8319e

Please sign in to comment.