Skip to content

Commit 33d904b

Browse files
authored
feat: add packageJsonParser hook for custom package.json parsing (#64)
1 parent ae60382 commit 33d904b

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

src/detect.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ function* lookup(cwd: string = process.cwd()): Generator<string> {
4242

4343
async function parsePackageJson(
4444
filepath: string,
45-
onUnknown: DetectOptions['onUnknown'],
45+
options: DetectOptions,
4646
): Promise<DetectResult | null> {
47-
return (!filepath || !pathExists(filepath, 'file'))
48-
? null
49-
: await handlePackageManager(filepath, onUnknown)
47+
if (!filepath || !(await pathExists(filepath, 'file')))
48+
return null
49+
50+
return await handlePackageManager(filepath, options)
5051
}
5152

5253
/**
@@ -58,7 +59,6 @@ export async function detect(options: DetectOptions = {}): Promise<DetectResult
5859
const {
5960
cwd,
6061
strategies = ['lockfile', 'packageManager-field', 'devEngines-field'],
61-
onUnknown,
6262
} = options
6363

6464
let stopDir: ((dir: string) => boolean) | undefined
@@ -78,7 +78,7 @@ export async function detect(options: DetectOptions = {}): Promise<DetectResult
7878
for (const lock of Object.keys(LOCKS)) {
7979
if (await pathExists(path.join(directory, lock), 'file')) {
8080
const name = LOCKS[lock]
81-
const result = await parsePackageJson(path.join(directory, 'package.json'), onUnknown)
81+
const result = await parsePackageJson(path.join(directory, 'package.json'), options)
8282
if (result)
8383
return result
8484
else
@@ -90,7 +90,7 @@ export async function detect(options: DetectOptions = {}): Promise<DetectResult
9090
case 'packageManager-field':
9191
case 'devEngines-field': {
9292
// Look up for package.json
93-
const result = await parsePackageJson(path.join(directory, 'package.json'), onUnknown)
93+
const result = await parsePackageJson(path.join(directory, 'package.json'), options)
9494
if (result)
9595
return result
9696
break
@@ -137,11 +137,15 @@ function getNameAndVer(pkg: { packageManager?: string, devEngines?: { packageMan
137137

138138
async function handlePackageManager(
139139
filepath: string,
140-
onUnknown: DetectOptions['onUnknown'],
140+
options: DetectOptions,
141141
) {
142-
// read `packageManager` field in package.json
142+
// read `packageManager` field in package.json using an optional custom parser
143143
try {
144-
const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'))
144+
const content = await fs.readFile(filepath, 'utf8')
145+
const pkg = options.packageJsonParser
146+
? await options.packageJsonParser(content, filepath)
147+
: JSON.parse(content)
148+
145149
let agent: Agent | undefined
146150
const nameAndVer = getNameAndVer(pkg)
147151
if (nameAndVer) {
@@ -163,7 +167,7 @@ async function handlePackageManager(
163167
return { name, agent, version }
164168
}
165169
else {
166-
return onUnknown?.(pkg.packageManager) ?? null
170+
return options.onUnknown?.(pkg.packageManager) ?? null
167171
}
168172
}
169173
}

src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ export interface DetectOptions {
6262
* The path to stop traversing up the directory.
6363
*/
6464
stopDir?: string | ((currentDir: string) => boolean)
65+
66+
/**
67+
* Optional custom parser for `package.json` content.
68+
*
69+
* If provided, it will be used instead of `JSON.parse` when reading `package.json` files.
70+
* This can be used to support JSONC, YAML, or other formats.
71+
*
72+
* @param content - The content of the file.
73+
* @param filepath - The absolute path to the file.
74+
* @returns The parsed object or a Promise resolving to it.
75+
* @default JSON.parse
76+
*/
77+
packageJsonParser?: (content: string, filepath: string) => any | Promise<any>
6578
}
6679

6780
export interface DetectResult {

test/detect.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,45 @@ it('stops at with custom stop function', async () => {
123123
agent: 'npm',
124124
})
125125
})
126+
127+
describe('packageJsonParser', () => {
128+
it('uses custom package.json parser', async () => {
129+
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))
130+
const fixturePath = path.join(__dirname, 'fixtures', 'packager', 'npm')
131+
await fs.copy(fixturePath, cwd)
132+
133+
const parser = vi.fn((content: string) => JSON.parse(content))
134+
135+
const result = await detect({
136+
cwd,
137+
packageJsonParser: parser,
138+
})
139+
140+
expect(result).toMatchObject({
141+
name: 'npm',
142+
agent: 'npm',
143+
})
144+
145+
expect(parser).toHaveBeenCalledTimes(1)
146+
expect(parser).toHaveBeenCalledWith(expect.stringContaining('"packageManager": "npm@7"'), path.join(cwd, 'package.json'))
147+
})
148+
149+
it('uses custom package.json parser to modify result', async () => {
150+
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))
151+
const fixturePath = path.join(__dirname, 'fixtures', 'packager', 'npm')
152+
await fs.copy(fixturePath, cwd)
153+
154+
// Parser that lies about the package manager
155+
const parser = vi.fn(() => ({ packageManager: 'pnpm@8' }))
156+
157+
const result = await detect({
158+
cwd,
159+
packageJsonParser: parser,
160+
})
161+
162+
expect(result).toMatchObject({
163+
name: 'pnpm',
164+
agent: 'pnpm',
165+
})
166+
})
167+
})

0 commit comments

Comments
 (0)