|
| 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 | + |
0 commit comments