Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/cli/src/commands/optimize/agent-installer.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { WIN32 } from '@socketsecurity/lib-stable/constants/platform'
import { getOwn } from '@socketsecurity/lib-stable/objects/inspect'
import { spawn } from '@socketsecurity/lib-stable/process/spawn/child'

import { cmdFlagsToString } from '../../util/process/cmd.mts'
import { mergeNodeOptions } from '../../util/process/cmd.mts'

import type { EnvDetails } from '../../util/ecosystem/environment.mjs'
import type { SpinnerInstance } from '@socketsecurity/lib-stable/spinner/types'
Expand Down Expand Up @@ -91,7 +91,9 @@ export function runAgentInstall(
...process.env,
// Set CI mode for pnpm to ensure consistent behavior.
...(isPnpm ? { CI: '1' } : {}),
NODE_OPTIONS: cmdFlagsToString([
// Merge our flags into any inherited NODE_OPTIONS instead of replacing
// it, so a user's globally-configured NODE_OPTIONS is preserved.
NODE_OPTIONS: mergeNodeOptions(process.env['NODE_OPTIONS'], [
...(skipNodeHardenFlags ? [] : getNodeHardenFlags()),
...getNodeNoWarningsFlags(),
...getNodeDisableSigusr1Flags(),
Expand Down
22 changes: 22 additions & 0 deletions packages/cli/src/util/process/cmd.mts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,25 @@ export function filterFlags(
export function isHelpFlag(cmdArg: string): boolean {
return helpFlags.has(cmdArg)
}

/**
* Merge Node flags into a NODE_OPTIONS value without clobbering an inherited
* one.
*
* A child process' NODE_OPTIONS env var REPLACES (does not extend) the
* parent's, so setting it to only our own flags silently drops any NODE_OPTIONS
* the user configured globally. This joins, in order, the caller's existing
* NODE_OPTIONS ahead of the flags we add so both are honoured.
*
* The value is intentionally not quoted: it is assigned directly to an env var
* (not passed through a shell), and consumers that re-tokenize NODE_OPTIONS on
* whitespace (e.g. Next.js) mishandle embedded quotes.
*/
export function mergeNodeOptions(
envNodeOptions: string | undefined,
addedFlags: string[] | readonly string[],
): string {
return [envNodeOptions, cmdFlagsToString(addedFlags)]
.filter(Boolean)
.join(' ')
}
34 changes: 34 additions & 0 deletions packages/cli/test/unit/commands/optimize/agent-installer.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ vi.mock(import('../../../../src/util/process/cmd.mts'), () => ({
.map(([k, v]) => `--${k}=${String(v)}`)
.join(' '),
),
mergeNodeOptions: vi.fn((envNodeOptions, addedFlags) =>
[envNodeOptions, ...(addedFlags || [])].filter(Boolean).join(' '),
),
}))

vi.mock(
Expand Down Expand Up @@ -109,6 +112,37 @@ describe('agent installer utilities', () => {
)
})

it('preserves an inherited NODE_OPTIONS instead of clobbering it', async () => {
const { spawn } = vi.mocked(
await import('@socketsecurity/lib-stable/process/spawn/child'),
)
spawn.mockReturnValue(Promise.resolve({ status: 0 }) as unknown)

const pkgEnvDetails = {
agent: 'npm',
agentExecPath: '/usr/bin/npm',
pkgPath: '/test/project',
agentVersion: { major: 10, minor: 0, patch: 0 },
} as unknown

const originalNodeOptions = process.env['NODE_OPTIONS']
process.env['NODE_OPTIONS'] = '--max-old-space-size=4096'
try {
await runAgentInstall(pkgEnvDetails)
} finally {
if (originalNodeOptions === undefined) {
delete process.env['NODE_OPTIONS']
} else {
process.env['NODE_OPTIONS'] = originalNodeOptions
}
}

const spawnEnv = (
spawn.mock.calls[0]![2] as { env: Record<string, string> }
).env
expect(spawnEnv['NODE_OPTIONS']).toContain('--max-old-space-size=4096')
})

it('uses spawn for pnpm agent', async () => {
const { spawn } = vi.mocked(
await import('@socketsecurity/lib-stable/process/spawn/child'),
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/test/unit/util/process/cmd.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
cmdPrefixMessage,
filterFlags,
isHelpFlag,
mergeNodeOptions,
} from '../../../../src/util/process/cmd.mts'

describe('cmd utilities', () => {
Expand Down Expand Up @@ -99,6 +100,46 @@ describe('cmd utilities', () => {
})
})

describe('mergeNodeOptions', () => {
const addedFlags = [
'--disable-warning=ExperimentalWarning',
'--no-warnings',
]

it('prepends the inherited NODE_OPTIONS ahead of added flags', () => {
const result = mergeNodeOptions('--max-old-space-size=4096', addedFlags)
expect(result).toBe(
'--max-old-space-size=4096 --disable-warning=ExperimentalWarning --no-warnings',
)
})

it('returns only the added flags when NODE_OPTIONS is undefined', () => {
const result = mergeNodeOptions(undefined, addedFlags)
expect(result).toBe('--disable-warning=ExperimentalWarning --no-warnings')
})

it('drops an empty NODE_OPTIONS without adding a stray separator', () => {
const result = mergeNodeOptions('', addedFlags)
expect(result).toBe('--disable-warning=ExperimentalWarning --no-warnings')
})

it('preserves the inherited NODE_OPTIONS when there are no added flags', () => {
expect(mergeNodeOptions('--enable-source-maps', [])).toBe(
'--enable-source-maps',
)
})

it('does not wrap or quote the merged value', () => {
const result = mergeNodeOptions('--enable-source-maps', addedFlags)
expect(result).not.toContain("'")
expect(result).not.toContain('"')
})

it('returns an empty string when nothing is provided', () => {
expect(mergeNodeOptions(undefined, [])).toBe('')
})
})

describe('isHelpFlag', () => {
it('identifies --help flag', () => {
expect(isHelpFlag('--help')).toBe(true)
Expand Down
Loading