Files
pnpm/pnpm11/exec/commands/test/exec.ts
AltayandZoltan Kochan 30e3a83ed4 fix(exec): set package-manager environment variables (#15282)
Share lifecycle package-manager environment construction with exec and dlx
in both implementations. Child tools receive the invoking pnpm path, Node
path and initial working directory, matching npm exec.

Closes pnpm/pnpm#7037.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-09-22 23:05:23 +02:00

109 lines
3.0 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import { beforeEach, expect, jest, test } from '@jest/globals'
import { makeNodeRequireOption } from '@pnpm/exec.lifecycle'
import { prepareEmpty } from '@pnpm/prepare'
import { DEFAULT_OPTS } from './utils/index.js'
jest.unstable_mockModule('execa', () => ({
// The handler registers the returned subprocess with the child process
// tracker, so the mock returns a minimal subprocess-like object.
safeExeca: jest.fn(() => ({ pid: undefined, once: jest.fn() })),
sync: jest.fn(),
}))
const { safeExeca: execa } = await import('execa')
const { exec } = await import('@pnpm/exec.commands')
beforeEach(() => {
jest.mocked(execa).mockClear()
})
test('exec should set npm_config_user_agent', async () => {
prepareEmpty()
const userAgent = 'pnpm/0.0.0'
await exec.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
selectedProjectsGraph: {},
userAgent,
}, ['eslint'])
expect(execa).toHaveBeenCalledWith('eslint', [], expect.objectContaining({
env: expect.objectContaining({
npm_config_user_agent: userAgent,
}),
}))
})
test('exec should set the NODE_OPTIONS env var', async () => {
prepareEmpty()
await exec.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
selectedProjectsGraph: {},
nodeOptions: '--max-old-space-size=4096',
}, ['eslint'])
expect(execa).toHaveBeenCalledWith('eslint', [], expect.objectContaining({
env: expect.objectContaining({
NODE_OPTIONS: '--max-old-space-size=4096',
}),
}))
})
test('exec should use the explicit NODE environment override', async () => {
prepareEmpty()
const nodePath = path.resolve('custom-node')
await exec.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
selectedProjectsGraph: {},
extraEnv: { NODE: nodePath },
}, ['eslint'])
expect(execa).toHaveBeenCalledWith('eslint', [], expect.objectContaining({
env: expect.objectContaining({
NODE: nodePath,
npm_node_execpath: nodePath,
}),
}))
})
test('exec should merge node options with PnP require option', async () => {
prepareEmpty()
const pnpPath = path.join(process.cwd(), '.pnp.cjs')
fs.writeFileSync(pnpPath, '')
await exec.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
selectedProjectsGraph: {},
workspaceDir: undefined,
nodeOptions: '--max-old-space-size=4096',
}, ['eslint'])
// The require path is quoted and backslash-escaped on Windows, so derive the
// expected value from the same helper exec uses instead of hardcoding it.
const { NODE_OPTIONS } = makeNodeRequireOption(pnpPath, { NODE_OPTIONS: '--max-old-space-size=4096' })
expect(execa).toHaveBeenCalledWith('eslint', [], expect.objectContaining({
env: expect.objectContaining({ NODE_OPTIONS }),
}))
})
test('exec should specify the command', async () => {
prepareEmpty()
await expect(exec.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
selectedProjectsGraph: {},
}, [])
).rejects.toThrow("'pnpm exec' requires a command to run")
})