diff --git a/plugin/scripts/bun-runner.js b/plugin/scripts/bun-runner.js index 6804f8fe..d34efef2 100644 --- a/plugin/scripts/bun-runner.js +++ b/plugin/scripts/bun-runner.js @@ -47,12 +47,20 @@ function fixBrokenScriptPath(argPath) { * Find Bun executable - checks PATH first, then common install locations */ function findBun() { - // Try PATH first - const pathCheck = spawnSync(IS_WINDOWS ? 'where' : 'which', ['bun'], { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - shell: IS_WINDOWS - }); + // Try PATH first. + // On Windows, pass a single command string to avoid Node 22+ DEP0190 deprecation warning + // (triggered when an args array is combined with shell:true, as the args are only + // concatenated, not escaped). Fixes #1503. + const pathCheck = IS_WINDOWS + ? spawnSync('where bun', { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + shell: true + }) + : spawnSync('which', ['bun'], { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); if (pathCheck.status === 0 && pathCheck.stdout.trim()) { // On Windows, prefer bun.cmd over bun (bun is a shell script, bun.cmd is the Windows batch file) diff --git a/tests/bun-runner.test.ts b/tests/bun-runner.test.ts new file mode 100644 index 00000000..abc45db5 --- /dev/null +++ b/tests/bun-runner.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'bun:test'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Regression tests for bun-runner.js to prevent the re-introduction of + * platform-specific issues that are difficult to catch in CI. + * + * These tests inspect the source code for known-bad patterns rather than + * executing the script, because bun-runner.js is a top-level side-effecting + * Node.js script (not an importable module) and the Windows-specific code + * paths cannot be exercised on non-Windows CI runners. + */ + +const BUN_RUNNER_PATH = join(import.meta.dir, '..', 'plugin', 'scripts', 'bun-runner.js'); +const source = readFileSync(BUN_RUNNER_PATH, 'utf-8'); + +describe('bun-runner.js findBun: DEP0190 regression guard (#1503)', () => { + it('does not use separate args array with shell:true (DEP0190 trigger pattern)', () => { + // Node 22+ emits DEP0190 when spawnSync is called with a separate args array + // AND shell:true, because the args are only concatenated (not escaped). + // The vulnerable pattern looks like: spawnSync(cmd, ['bun'], { shell: true/IS_WINDOWS }) + // This test verifies the fix in findBun() has not been reverted. + const vulnerablePattern = /spawnSync\s*\(\s*(?:IS_WINDOWS\s*\?\s*['"]where['"]\s*:[^)]+|['"]where['"]),\s*\[[^\]]+\],\s*\{[^}]*shell\s*:\s*(?:true|IS_WINDOWS)/; + expect(vulnerablePattern.test(source)).toBe(false); + }); + + it('uses a single string command for Windows where-bun lookup', () => { + // The safe pattern: pass a single combined string 'where bun' with shell:true + // so no separate args array is involved. This is the fix for DEP0190. + expect(source).toContain("spawnSync('where bun'"); + }); + + it('uses no shell option for Unix which-bun lookup', () => { + // On Unix, spawnSync('which', ['bun']) without shell:true is safe and avoids + // the deprecation warning entirely. + // Check that the unix path does NOT pass shell:true alongside the args array. + // We look for the pattern: spawnSync('which', ['bun'], { ... }) — shell should be absent. + const unixCallMatch = source.match(/spawnSync\('which',\s*\['bun'\],\s*\{([^}]+)\}/) + if (unixCallMatch) { + expect(unixCallMatch[1]).not.toContain('shell'); + } + // If the pattern is not found as expected, that means the code changed shape — + // either way we shouldn't have shell:true on the unix path + expect(source).toContain("spawnSync('which', ['bun']"); + }); +});