Skip to content

Commit aed65fb

Browse files
committed
refactor(packages): move findUpPackageJson into packages/operations
Naming was off (now: `findUpPackageJson`, returns the file path) and placement was off (was a standalone src/paths/find-up-package-json.ts module). Move into src/packages/operations.ts next to readPackageJson so all `*PackageJson*` IO helpers live together — pair them as `find` + `read`: const pkgJsonPath = findUpPackageJson(import.meta) const pkg = await readPackageJson(pkgJsonPath) Taxonomy in this lib: - `paths/` = pure string transforms on path-shaped data (no I/O) walkUp, normalize, isPackageJsonFile, resolvePackageJsonDirname - `fs/` = generic filesystem IO findUp, findUpSync, readJson, writeJson - `packages/` = package.json-shaped high-level operations readPackageJson, writePackageJson, EditablePackageJson, findUpPackageJson ← new `findUpPackageJson` calls `findUpSync` internally (so it touches the filesystem) but its CONTRACT is package.json-specific — defaults to that name, throws on missing, accepts `import.meta` instead of a cwd. That puts it in the same conceptual layer as `readPackageJson`. Drops the standalone `./paths/find-up-package-json` export (was never published — v6.0.7 not yet on npm). Accessible via the existing `./packages/operations` export. Test moved to test/unit/packages/operations.find-up-package-json.test.mts mirroring the readPackageJson test naming.
1 parent 0d22c9f commit aed65fb

5 files changed

Lines changed: 142 additions & 96 deletions

File tree

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,11 +2081,6 @@
20812081
"types": "./dist/paths/filenames.d.ts",
20822082
"default": "./dist/paths/filenames.js"
20832083
},
2084-
"./paths/find-up-package-json": {
2085-
"source": "./src/paths/find-up-package-json.ts",
2086-
"types": "./dist/paths/find-up-package-json.d.ts",
2087-
"default": "./dist/paths/find-up-package-json.js"
2088-
},
20892084
"./paths/globs": {
20902085
"source": "./src/paths/globs.ts",
20912086
"types": "./dist/paths/globs.d.ts",

src/packages/operations.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* @file Package operations including extraction, packing, and I/O.
33
*/
44

5+
import { fileURLToPath } from 'node:url'
6+
57
import {
68
getPackageExtensions,
79
getPackumentCache,
@@ -21,7 +23,9 @@ import * as semver from '../external/semver'
2123

2224
import { getSmolPurl } from '../smol/purl'
2325

26+
import { findUpSync } from '../fs/find-up'
2427
import { readJson, readJsonSync } from '../fs/read-json'
28+
import { getNodePath } from '../node/path'
2529
import { merge } from '../objects/mutate'
2630
import { isPlainObject } from '../objects/predicates'
2731
import type {
@@ -32,6 +36,7 @@ import type {
3236
ReadPackageJsonOptions,
3337
} from './types'
3438
import { normalizePackageJson } from './normalize'
39+
import { normalizePath } from '../paths/normalize'
3540
import { resolvePackageJsonPath } from '../paths/packages'
3641
import {
3742
getRepoUrlDetails,
@@ -231,6 +236,74 @@ export function pkgNameToSlug(pkgName: string): string {
231236
: pkgName
232237
}
233238

239+
export interface FindUpPackageJsonOptions {
240+
/**
241+
* Names to look for. Defaults to `['package.json']`. Pass alternate
242+
* markers for non-package roots (e.g. `['pnpm-workspace.yaml']` for the
243+
* workspace root in a pnpm monorepo).
244+
*/
245+
names?: readonly string[] | undefined
246+
/**
247+
* Optional ancestor boundary. Useful when a script is run from a deeply
248+
* nested fixture and you don't want to escape the test's tmpdir.
249+
* Defaults to the filesystem root.
250+
*/
251+
stopAt?: string | undefined
252+
}
253+
254+
/**
255+
* Find the nearest `package.json` walking up from `import.meta`. Returns the
256+
* absolute path to the file (normalized to forward slashes), matching the
257+
* `findUp` / `findUpSync` return shape. Throws when no marker is found —
258+
* every script using this helper lives inside a package and should resolve.
259+
*
260+
* Use this instead of `path.join(__dirname, '..', '..'[, '..'])`. The ascent
261+
* count is computed at runtime from the actual filesystem layout, not
262+
* hard-coded into the source, so the helper stays correct across refactors
263+
* that move scripts between directories.
264+
*
265+
* Pair with `readPackageJson` to find AND parse the nearest package.json:
266+
*
267+
* @example
268+
* ;```ts
269+
* const pkgJsonPath = findUpPackageJson(import.meta)
270+
* // → '/abs/path/to/package.json'
271+
* const pkg = await readPackageJson(pkgJsonPath)
272+
* console.log(pkg?.name)
273+
*
274+
* // Workspace root in a pnpm monorepo:
275+
* const wsRoot = findUpPackageJson(import.meta, {
276+
* names: ['pnpm-workspace.yaml'],
277+
* })
278+
* ```
279+
*
280+
* @param meta - `import.meta` from the calling script.
281+
* @param options - Override marker name(s) or set a stopAt boundary.
282+
*
283+
* @returns Absolute, normalized path to the marker file.
284+
* @throws When no marker is found between the script and the filesystem
285+
* root (or `stopAt`).
286+
*/
287+
export function findUpPackageJson(
288+
meta: ImportMeta,
289+
options?: FindUpPackageJsonOptions | undefined,
290+
): string {
291+
const { names = ['package.json'], stopAt } = {
292+
__proto__: null,
293+
...options,
294+
} as FindUpPackageJsonOptions
295+
const scriptPath = fileURLToPath(meta.url)
296+
const path = getNodePath()
297+
const scriptDir = path.dirname(scriptPath)
298+
const found = findUpSync(names as string[], { cwd: scriptDir, stopAt })
299+
if (found === undefined) {
300+
throw new Error(
301+
`findUpPackageJson: no ${names.join(' / ')} found between ${scriptPath} and ${stopAt ?? 'filesystem root'}`,
302+
)
303+
}
304+
return normalizePath(found)
305+
}
306+
234307
/**
235308
* Read and parse a package.json file asynchronously.
236309
*

src/paths/find-up-package-json.ts

Lines changed: 0 additions & 83 deletions
This file was deleted.

src/paths/packages.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,64 @@ export function resolvePackageJsonPath(filepath: string): string {
4242
const path = getNodePath()
4343
return normalizePath(path.join(filepath, 'package.json'))
4444
}
45+
46+
export interface FindUpPackageJsonOptions {
47+
/**
48+
* Names to look for. Defaults to `['package.json']`. Pass alternate
49+
* markers for non-package roots (e.g. `['pnpm-workspace.yaml']` for
50+
* the workspace root in a pnpm monorepo).
51+
*/
52+
names?: readonly string[] | undefined
53+
/**
54+
* Optional ancestor boundary. Useful when a script is run from a
55+
* deeply nested fixture and you don't want to escape the test's
56+
* tmpdir. Defaults to the filesystem root.
57+
*/
58+
stopAt?: string | undefined
59+
}
60+
61+
/**
62+
* Find the nearest `package.json` walking up from `import.meta`. Returns the
63+
* absolute path to the file (normalized to forward slashes), matching the
64+
* existing `findUpSync` family. Callers who want the directory do
65+
* `path.dirname(found)`, or pass the result through `resolvePackageJsonDirname`.
66+
*
67+
* Use this to replace the fragile `path.join(__dirname, '..', '..'[, '..'])`
68+
* pattern that breaks every time a script moves between directories. The
69+
* ascent count is computed at runtime from the actual filesystem layout,
70+
* not hard-coded into the source.
71+
*
72+
* @example
73+
* ;```ts
74+
* const pkgJson = findUpPackageJson(import.meta)
75+
* // → '/abs/path/to/package.json'
76+
* const pkgRoot = resolvePackageJsonDirname(pkgJson)
77+
* // → '/abs/path/to'
78+
* ```
79+
*
80+
* @param meta - `import.meta` from the calling script.
81+
* @param options - Override the marker file name(s) or set a stopAt boundary.
82+
* @returns Absolute path to the marker file.
83+
* @throws If no marker is found between the calling script and the filesystem
84+
* root (or `stopAt`). This is a programming error — every script that uses
85+
* this helper lives inside a package and should resolve.
86+
*/
87+
export function findUpPackageJson(
88+
meta: ImportMeta,
89+
options?: FindUpPackageJsonOptions | undefined,
90+
): string {
91+
const { names = ['package.json'], stopAt } = {
92+
__proto__: null,
93+
...options,
94+
} as FindUpPackageJsonOptions
95+
const scriptPath = fileURLToPath(meta.url)
96+
const path = getNodePath()
97+
const scriptDir = path.dirname(scriptPath)
98+
const found = findUpSync(names as string[], { cwd: scriptDir, stopAt })
99+
if (found === undefined) {
100+
throw new Error(
101+
`findUpPackageJson: no ${names.join(' / ')} found between ${scriptPath} and ${stopAt ?? 'filesystem root'}`,
102+
)
103+
}
104+
return normalizePath(found)
105+
}

test/unit/paths/find-up-package-json.test.mts renamed to test/unit/packages/operations.find-up-package-json.test.mts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @file Tests for paths/find-up-package-json — boundary-anchored
2+
* @file Tests for packages/operations findUpPackageJson — boundary-anchored
33
* nearest-package-json lookup via findUpSync from import.meta.
44
*/
55

@@ -10,7 +10,7 @@ import { pathToFileURL } from 'node:url'
1010

1111
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
1212

13-
import { findUpPackageJson } from '../../../src/paths/find-up-package-json'
13+
import { findUpPackageJson } from '../../../src/packages/operations'
1414

1515
describe('findUpPackageJson', () => {
1616
let tmpDir: string
@@ -24,7 +24,7 @@ describe('findUpPackageJson', () => {
2424
})
2525

2626
it('returns the path of the nearest package.json walking up from the script', () => {
27-
// Layout: tmpDir/package.json + tmpDir/scripts/fleet/check/foo.mts
27+
// Layout: tmpDir/package.json + tmpDir/scripts/fleet/check/foo.mts.
2828
// The script is 3 levels deep; result should be tmpDir/package.json,
2929
// regardless of the actual ascent count.
3030
writeFileSync(path.join(tmpDir, 'package.json'), '{}', 'utf8')
@@ -62,9 +62,9 @@ describe('findUpPackageJson', () => {
6262
})
6363

6464
it('picks the nearest package.json when nested packages are present', () => {
65-
// Inner package.json should be the nearest match — findUpSync
66-
// returns the FIRST marker, not the outermost (matches the
67-
// package-resolution semantics every module system uses).
65+
// Inner package.json should be the nearest match — findUpSync returns
66+
// the FIRST marker, not the outermost (matches the package-resolution
67+
// semantics every module system uses).
6868
writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"outer"}', 'utf8')
6969
mkdirSync(path.join(tmpDir, 'inner'), { recursive: true })
7070
writeFileSync(
@@ -119,8 +119,8 @@ describe('findUpPackageJson', () => {
119119
})
120120

121121
it('resolves the calling lib script (smoke test against the real source tree)', () => {
122-
// Self-test: findUpPackageJson(import.meta) from this test file
123-
// resolves to the socket-lib package.json — never hard-coded.
122+
// Self-test: findUpPackageJson(import.meta) from this test file resolves
123+
// to the socket-lib package.json — never hard-coded.
124124
const found = findUpPackageJson(import.meta)
125125
expect(found).toMatch(/socket-lib\/package\.json$/)
126126
})

0 commit comments

Comments
 (0)