fix(tooling): resolve Bun executable on Windows
This commit is contained in:
@@ -40,14 +40,14 @@ bun install
|
|||||||
bun run electron:dev
|
bun run electron:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
`bun run electron:dev` starts the web dev server with HMR, then launches Electron against `packages/electron/main.mjs`.
|
`bun run electron:dev` starts the web dev server with HMR, then launches Electron against `packages/electron/main.mjs`. On Windows, the HMR launcher resolves npm's `bun.cmd` shim to the underlying `bun.exe` before spawning Bun child processes.
|
||||||
|
|
||||||
The Electron workspace package trusts Electron's install script so `bun install` downloads the platform runtime in fresh checkouts and worktrees.
|
The Electron workspace package trusts Electron's install script so `bun install` downloads the platform runtime in fresh checkouts and worktrees.
|
||||||
|
|
||||||
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Older Electron releases bundled `extract-zip@2.0.1`, which under Node 24 silently unpacked only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. Electron 43+ ships its own fixed extractor (`@electron-internal/extract-zip`), but to keep interrupted or wrong-architecture installs from blocking desktop work:
|
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Older Electron releases bundled `extract-zip@2.0.1`, which under Node 24 silently unpacked only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. Electron 43+ ships its own fixed extractor (`@electron-internal/extract-zip`), but to keep interrupted or wrong-architecture installs from blocking desktop work:
|
||||||
|
|
||||||
- The root `postinstall` runs `ensure-electron.mjs --best-effort`, which detects an incomplete Electron install (missing binary, stale `dist/version`/`path.txt`, or a binary of the wrong architecture) and repairs it by re-running the postinstall under Bun (which extracts correctly), falling back to Node.
|
- The root `postinstall` runs `ensure-electron.mjs --best-effort`, which detects an incomplete Electron install (missing binary, stale `dist/version`/`path.txt`, or a binary of the wrong architecture) and repairs it by re-running the postinstall under Bun (which extracts correctly), falling back to Node.
|
||||||
- `electron-dev.mjs` runs the same check (fail-fast, not best-effort) before launching, so `bun run electron:dev` self-heals even when an install was interrupted.
|
- `electron-dev.mjs` runs the same check (fail-fast, not best-effort) before launching, so `bun run electron:dev` self-heals even when an install was interrupted. On Windows, the repair resolves npm's `bun.cmd` shim to the underlying `bun.exe` before spawning it from Node.
|
||||||
- The check can be run on demand with `bun run --cwd packages/electron ensure:electron`; set `ELECTRON_SKIP_BINARY_DOWNLOAD=1` to skip repair (e.g. CI without a network).
|
- The check can be run on demand with `bun run --cwd packages/electron ensure:electron`; set `ELECTRON_SKIP_BINARY_DOWNLOAD=1` to skip repair (e.g. CI without a network).
|
||||||
- Unit tests in `scripts/ensure-electron.test.mjs` (run via `bun run --cwd packages/electron test:architecture`) cover healthy/missing/stale installs, wrong-architecture binaries, repair fallback, and `--best-effort`.
|
- Unit tests in `scripts/ensure-electron.test.mjs` (run via `bun run --cwd packages/electron test:architecture`) cover healthy/missing/stale installs, wrong-architecture binaries, repair fallback, and `--best-effort`.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import path from 'node:path';
|
|||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
import { resolveBunExecutable } from '../../../scripts/lib/bun-executable.mjs';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const repoElectronDir = path.resolve(__dirname, '..');
|
const repoElectronDir = path.resolve(__dirname, '..');
|
||||||
@@ -225,7 +227,11 @@ export function isComplete(electronDir, expected = expectedArch()) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInstallCommands(env) {
|
function resolveInstallCommands(env, bunResolver = resolveBunExecutable) {
|
||||||
|
const normalize = (commands) => commands.map(([bin, args]) => [
|
||||||
|
bin === 'bun' ? bunResolver({ env }) : bin,
|
||||||
|
args,
|
||||||
|
]);
|
||||||
if (env.OPENCHAMBER_ELECTRON_INSTALL_COMMANDS) {
|
if (env.OPENCHAMBER_ELECTRON_INSTALL_COMMANDS) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(env.OPENCHAMBER_ELECTRON_INSTALL_COMMANDS);
|
const parsed = JSON.parse(env.OPENCHAMBER_ELECTRON_INSTALL_COMMANDS);
|
||||||
@@ -233,22 +239,22 @@ function resolveInstallCommands(env) {
|
|||||||
Array.isArray(parsed) &&
|
Array.isArray(parsed) &&
|
||||||
parsed.every((command) => Array.isArray(command) && typeof command[0] === 'string')
|
parsed.every((command) => Array.isArray(command) && typeof command[0] === 'string')
|
||||||
) {
|
) {
|
||||||
return parsed;
|
return normalize(parsed);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to the default commands.
|
// Fall through to the default commands.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [
|
return normalize([
|
||||||
['bun', ['install.js']],
|
['bun', ['install.js']],
|
||||||
['node', ['install.js']],
|
['node', ['install.js']],
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function repair(electronDir, options = {}) {
|
export function repair(electronDir, options = {}) {
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const runner = options.runner ?? spawnSync;
|
const runner = options.runner ?? spawnSync;
|
||||||
const commands = options.commands ?? resolveInstallCommands(env);
|
const commands = options.commands ?? resolveInstallCommands(env, options.bunResolver);
|
||||||
|
|
||||||
// A partial extraction can leave stale entries (and a stale path.txt) that
|
// A partial extraction can leave stale entries (and a stale path.txt) that
|
||||||
// a re-run would merge with. Start clean so the repair is deterministic.
|
// a re-run would merge with. Start clean so the repair is deterministic.
|
||||||
@@ -265,6 +271,7 @@ export function repair(electronDir, options = {}) {
|
|||||||
cwd: electronDir,
|
cwd: electronDir,
|
||||||
stdio: options.stdio ?? 'inherit',
|
stdio: options.stdio ?? 'inherit',
|
||||||
env: { ...env, ELECTRON_SKIP_BINARY_DOWNLOAD: undefined },
|
env: { ...env, ELECTRON_SKIP_BINARY_DOWNLOAD: undefined },
|
||||||
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.warn(`[electron:ensure] could not run \`${label}\`: ${result.error.message}`);
|
console.warn(`[electron:ensure] could not run \`${label}\`: ${result.error.message}`);
|
||||||
|
|||||||
@@ -259,6 +259,32 @@ test('repair skips a command whose runner errors and keeps trying the rest', (t)
|
|||||||
assert.deepEqual(calls, commands);
|
assert.deepEqual(calls, commands);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('repair resolves Bun before invoking the install command', (t) => {
|
||||||
|
const dir = withFixture(t);
|
||||||
|
const calls = [];
|
||||||
|
const resolvedBun = 'C:\\npm\\node_modules\\bun\\bin\\bun.exe';
|
||||||
|
const result = repair(dir, {
|
||||||
|
env: { TEST_ENV: 'present' },
|
||||||
|
bunResolver: ({ env }) => {
|
||||||
|
assert.equal(env.TEST_ENV, 'present');
|
||||||
|
return resolvedBun;
|
||||||
|
},
|
||||||
|
runner: (bin, args, options) => {
|
||||||
|
calls.push([bin, args, options.windowsHide]);
|
||||||
|
if (bin !== resolvedBun) return { status: 1 };
|
||||||
|
const platform = platformPath();
|
||||||
|
fs.mkdirSync(path.join(dir, 'dist', path.dirname(platform)), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'dist', 'version'), '41.2.1');
|
||||||
|
fs.writeFileSync(path.join(dir, 'path.txt'), platform);
|
||||||
|
fs.writeFileSync(path.join(dir, 'dist', platform), headerBytesForArch(expectedArch()));
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result, true);
|
||||||
|
assert.deepEqual(calls, [[resolvedBun, ['install.js'], true]]);
|
||||||
|
});
|
||||||
|
|
||||||
test('repair returns false when the runner reports a failure status for every command', (t) => {
|
test('repair returns false when the runner reports a failure status for every command', (t) => {
|
||||||
const dir = withFixture(t);
|
const dir = withFixture(t);
|
||||||
const calls = [];
|
const calls = [];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run build:watch",
|
"dev": "bun run build:watch",
|
||||||
"dev:server": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} bun server/index.js --port ${OPENCHAMBER_PORT:-3001}",
|
"dev:server": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} bun server/index.js --port ${OPENCHAMBER_PORT:-3001}",
|
||||||
"dev:server:watch": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} nodemon --watch server --ext js --exec \"bun server/index.js --port ${OPENCHAMBER_PORT:-3001}\"",
|
"dev:server:watch": "node ./scripts/dev-server-watch.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:watch": "vite build --watch",
|
"build:watch": "vite build --watch",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
import { resolveBunExecutable } from '../../../scripts/lib/bun-executable.mjs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const webRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
export function createDevServerWatchCommand(options = {}) {
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
const bunExecutable = options.bunExecutable ?? resolveBunExecutable({ env, platform });
|
||||||
|
const configuredPort = env.OPENCHAMBER_PORT?.trim();
|
||||||
|
const port = configuredPort || '3001';
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
|
||||||
|
throw new Error(`Invalid OPENCHAMBER_PORT: ${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: bunExecutable,
|
||||||
|
args: platform === 'win32'
|
||||||
|
? ['--watch', 'server/index.js', '--port', port]
|
||||||
|
: [
|
||||||
|
'x',
|
||||||
|
'nodemon',
|
||||||
|
'--watch',
|
||||||
|
'server',
|
||||||
|
'--ext',
|
||||||
|
'js',
|
||||||
|
'--exec',
|
||||||
|
`bun server/index.js --port ${port}`,
|
||||||
|
],
|
||||||
|
spawnOptions: {
|
||||||
|
cwd: webRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...env,
|
||||||
|
OPENCHAMBER_RELAY_HOST: env.OPENCHAMBER_RELAY_HOST || 'off',
|
||||||
|
},
|
||||||
|
windowsHide: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runDevServerWatch(options = {}) {
|
||||||
|
const runner = options.runner ?? spawn;
|
||||||
|
const { command, args, spawnOptions } = createDevServerWatchCommand(options);
|
||||||
|
const child = runner(command, args, spawnOptions);
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
console.error('[dev:web:server] Failed to start server watcher:', error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
process.kill(process.pid, signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exitCode = code ?? 1;
|
||||||
|
});
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||||
|
if (isMain) {
|
||||||
|
try {
|
||||||
|
runDevServerWatch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[dev:web:server] Unable to configure server watcher:', error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { createDevServerWatchCommand } from './dev-server-watch.mjs';
|
||||||
|
|
||||||
|
describe('createDevServerWatchCommand', () => {
|
||||||
|
it('uses the package default port and disables relay hosting by default', () => {
|
||||||
|
const command = createDevServerWatchCommand({
|
||||||
|
platform: 'win32',
|
||||||
|
env: {},
|
||||||
|
bunExecutable: '/opt/bun/bin/bun',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(command.command).toBe('/opt/bun/bin/bun');
|
||||||
|
expect(command.args).toEqual(['--watch', 'server/index.js', '--port', '3001']);
|
||||||
|
expect(command.spawnOptions.env.OPENCHAMBER_RELAY_HOST).toBe('off');
|
||||||
|
expect(command.spawnOptions.windowsHide).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the configured port and relay host to the Bun watcher', () => {
|
||||||
|
const command = createDevServerWatchCommand({
|
||||||
|
platform: 'win32',
|
||||||
|
env: {
|
||||||
|
OPENCHAMBER_PORT: '58992',
|
||||||
|
OPENCHAMBER_RELAY_HOST: 'relay.example.test',
|
||||||
|
},
|
||||||
|
bunExecutable: 'C:\\Tools\\Bun\\bun.exe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(command.args).toEqual(['--watch', 'server/index.js', '--port', '58992']);
|
||||||
|
expect(command.spawnOptions.env.OPENCHAMBER_RELAY_HOST).toBe('relay.example.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an invalid configured port before spawning', () => {
|
||||||
|
expect(() => createDevServerWatchCommand({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { OPENCHAMBER_PORT: 'not-a-port' },
|
||||||
|
bunExecutable: 'bun',
|
||||||
|
})).toThrow(/Invalid OPENCHAMBER_PORT/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves the nodemon watcher command outside Windows', () => {
|
||||||
|
const command = createDevServerWatchCommand({
|
||||||
|
platform: 'linux',
|
||||||
|
env: { OPENCHAMBER_PORT: '4200' },
|
||||||
|
bunExecutable: '/opt/bun/bin/bun',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(command.command).toBe('/opt/bun/bin/bun');
|
||||||
|
expect(command.args).toEqual([
|
||||||
|
'x',
|
||||||
|
'nodemon',
|
||||||
|
'--watch',
|
||||||
|
'server',
|
||||||
|
'--ext',
|
||||||
|
'js',
|
||||||
|
'--exec',
|
||||||
|
'bun server/index.js --port 4200',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -138,7 +138,7 @@ export const createServerStartupRuntime = (dependencies) => {
|
|||||||
// Cover every signal a shell or dev harness may use to stop/restart us, so
|
// Cover every signal a shell or dev harness may use to stop/restart us, so
|
||||||
// the managed OpenCode child is always torn down gracefully instead of
|
// the managed OpenCode child is always torn down gracefully instead of
|
||||||
// orphaned: SIGINT/SIGQUIT (Ctrl+C/Ctrl+\), SIGTERM (kill/default), SIGHUP
|
// orphaned: SIGINT/SIGQUIT (Ctrl+C/Ctrl+\), SIGTERM (kill/default), SIGHUP
|
||||||
// (terminal close), SIGUSR2 (nodemon restart for `dev:server:watch`).
|
// (terminal close), SIGUSR2 (nodemon restart outside Windows).
|
||||||
process.on('SIGTERM', handleSignal);
|
process.on('SIGTERM', handleSignal);
|
||||||
process.on('SIGINT', handleSignal);
|
process.on('SIGINT', handleSignal);
|
||||||
process.on('SIGQUIT', handleSignal);
|
process.on('SIGQUIT', handleSignal);
|
||||||
|
|||||||
+18
-3
@@ -5,18 +5,23 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { resolveBunExecutable } from './lib/bun-executable.mjs';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const repoRoot = path.resolve(__dirname, '..');
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
const useDetachedChildren = process.platform === 'darwin';
|
const useDetachedChildren = process.platform === 'darwin';
|
||||||
const webRoot = path.join(repoRoot, 'packages/web');
|
const webRoot = path.join(repoRoot, 'packages/web');
|
||||||
|
|
||||||
|
const bunExecutable = resolveBunExecutable();
|
||||||
|
|
||||||
function run(label, command, args, env = {}, options = {}) {
|
function run(label, command, args, env = {}, options = {}) {
|
||||||
return spawn(command, args, {
|
return spawn(command, args, {
|
||||||
cwd: options.cwd || repoRoot,
|
cwd: options.cwd || repoRoot,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: { ...process.env, ...env },
|
env: { ...process.env, ...env },
|
||||||
detached: useDetachedChildren,
|
detached: useDetachedChildren,
|
||||||
|
windowsHide: true,
|
||||||
}).on('error', (error) => {
|
}).on('error', (error) => {
|
||||||
console.error(`[dev:web:hmr] Failed to start ${label}:`, error);
|
console.error(`[dev:web:hmr] Failed to start ${label}:`, error);
|
||||||
});
|
});
|
||||||
@@ -112,15 +117,25 @@ function clearViteCache() {
|
|||||||
|
|
||||||
clearViteCache();
|
clearViteCache();
|
||||||
|
|
||||||
const api = run('api', 'bun', ['run', '--cwd', 'packages/web', 'dev:server:watch'], {
|
const api = run(
|
||||||
|
'api',
|
||||||
|
bunExecutable,
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
'--cwd',
|
||||||
|
'packages/web',
|
||||||
|
'dev:server:watch',
|
||||||
|
],
|
||||||
|
{
|
||||||
OPENCHAMBER_PORT: backendPort,
|
OPENCHAMBER_PORT: backendPort,
|
||||||
// Dev backends share the relay identity with the production instance; never
|
// Dev backends share the relay identity with the production instance; never
|
||||||
// let them capture the machine's relay host on their own.
|
// let them capture the machine's relay host on their own.
|
||||||
OPENCHAMBER_RELAY_HOST: process.env.OPENCHAMBER_RELAY_HOST || 'off',
|
OPENCHAMBER_RELAY_HOST: process.env.OPENCHAMBER_RELAY_HOST || 'off',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
const vite = run(
|
const vite = run(
|
||||||
'vite',
|
'vite',
|
||||||
'bun',
|
bunExecutable,
|
||||||
['x', 'vite', '--force', '--host', hmrHost, '--port', uiPort, '--strictPort'],
|
['x', 'vite', '--force', '--host', hmrHost, '--port', uiPort, '--strictPort'],
|
||||||
{
|
{
|
||||||
OPENCHAMBER_PORT: backendPort,
|
OPENCHAMBER_PORT: backendPort,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const findWindowsExecutable = (name, env) => {
|
||||||
|
const result = spawnSync('where.exe', [name], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
env,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String(result.stdout || '')
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.find(Boolean) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveBunExecutable(options = {}) {
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
const fileExists = options.fileExists ?? existsSync;
|
||||||
|
const findOnPath = options.findOnPath ?? findWindowsExecutable;
|
||||||
|
const pathApi = platform === 'win32' ? path.win32 : path;
|
||||||
|
|
||||||
|
const npmExecutable = env.npm_execpath?.trim();
|
||||||
|
const npmExecutableName = platform === 'win32' ? 'bun.exe' : 'bun';
|
||||||
|
if (
|
||||||
|
npmExecutable &&
|
||||||
|
pathApi.basename(npmExecutable).toLowerCase() === npmExecutableName &&
|
||||||
|
fileExists(npmExecutable)
|
||||||
|
) {
|
||||||
|
return npmExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
const directExecutable = findOnPath('bun.exe', env);
|
||||||
|
if (directExecutable) {
|
||||||
|
return directExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shim = findOnPath('bun.cmd', env);
|
||||||
|
if (shim) {
|
||||||
|
const executable = pathApi.resolve(
|
||||||
|
pathApi.dirname(shim),
|
||||||
|
'node_modules',
|
||||||
|
'bun',
|
||||||
|
'bin',
|
||||||
|
'bun.exe',
|
||||||
|
);
|
||||||
|
if (fileExists(executable)) {
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'bun';
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { resolveBunExecutable } from './bun-executable.mjs';
|
||||||
|
|
||||||
|
test('uses the Bun executable that launched the package script', () => {
|
||||||
|
const executable = '/opt/bun/bin/bun';
|
||||||
|
assert.equal(resolveBunExecutable({
|
||||||
|
platform: 'linux',
|
||||||
|
env: { npm_execpath: executable },
|
||||||
|
fileExists: (candidate) => candidate === executable,
|
||||||
|
}), executable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prefers bun.exe from PATH on Windows', () => {
|
||||||
|
const executable = 'C:\\Tools\\Bun\\bun.exe';
|
||||||
|
assert.equal(resolveBunExecutable({
|
||||||
|
platform: 'win32',
|
||||||
|
env: {},
|
||||||
|
fileExists: () => false,
|
||||||
|
findOnPath: (name) => name === 'bun.exe' ? executable : null,
|
||||||
|
}), executable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not return an extensionless Windows npm shim', () => {
|
||||||
|
const shim = 'C:\\Users\\dev\\AppData\\Roaming\\npm\\bun';
|
||||||
|
const executable = 'C:\\Tools\\Bun\\bun.exe';
|
||||||
|
|
||||||
|
assert.equal(resolveBunExecutable({
|
||||||
|
platform: 'win32',
|
||||||
|
env: { npm_execpath: shim },
|
||||||
|
fileExists: (candidate) => candidate === shim,
|
||||||
|
findOnPath: (name) => name === 'bun.exe' ? executable : null,
|
||||||
|
}), executable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('derives bun.exe when Windows PATH exposes only the npm bun.cmd shim', () => {
|
||||||
|
const shim = 'C:\\Users\\dev\\AppData\\Roaming\\npm\\bun.cmd';
|
||||||
|
const executable = path.win32.resolve(
|
||||||
|
path.win32.dirname(shim),
|
||||||
|
'node_modules',
|
||||||
|
'bun',
|
||||||
|
'bin',
|
||||||
|
'bun.exe',
|
||||||
|
);
|
||||||
|
const env = { Path: 'C:\\Users\\dev\\AppData\\Roaming\\npm' };
|
||||||
|
const lookups = [];
|
||||||
|
|
||||||
|
assert.equal(resolveBunExecutable({
|
||||||
|
platform: 'win32',
|
||||||
|
env,
|
||||||
|
fileExists: (candidate) => candidate === executable,
|
||||||
|
findOnPath: (name, lookupEnv) => {
|
||||||
|
assert.equal(lookupEnv, env);
|
||||||
|
lookups.push(name);
|
||||||
|
return name === 'bun.cmd' ? shim : null;
|
||||||
|
},
|
||||||
|
}), executable);
|
||||||
|
assert.deepEqual(lookups, ['bun.exe', 'bun.cmd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to bun when no directly spawnable executable is available', () => {
|
||||||
|
assert.equal(resolveBunExecutable({
|
||||||
|
platform: 'win32',
|
||||||
|
env: {},
|
||||||
|
fileExists: () => false,
|
||||||
|
findOnPath: () => null,
|
||||||
|
}), 'bun');
|
||||||
|
});
|
||||||
@@ -19,9 +19,12 @@ import { spawn } from 'node:child_process';
|
|||||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { resolveBunExecutable } from './lib/bun-executable.mjs';
|
||||||
|
|
||||||
const TEST_FILE = /\.(test|spec)\.(js|cjs|mjs|jsx|ts|tsx)$/;
|
const TEST_FILE = /\.(test|spec)\.(js|cjs|mjs|jsx|ts|tsx)$/;
|
||||||
const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-bundle', 'build', 'out', '.git', 'ios', 'android']);
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-bundle', 'build', 'out', '.git', 'ios', 'android']);
|
||||||
const MAX_PARALLEL = 4;
|
const MAX_PARALLEL = 4;
|
||||||
|
const bunExecutable = resolveBunExecutable();
|
||||||
|
|
||||||
const collect = (root, found = []) => {
|
const collect = (root, found = []) => {
|
||||||
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
||||||
@@ -44,7 +47,7 @@ const resolveCommand = (file) => {
|
|||||||
// implements. Node's ESM loader cannot resolve the extensionless local
|
// implements. Node's ESM loader cannot resolve the extensionless local
|
||||||
// specifiers these files use (`./sseProxy`), so it never ran them at all.
|
// specifiers these files use (`./sseProxy`), so it never ran them at all.
|
||||||
if (isTypeScript || /from\s+['"]bun:test['"]/.test(source)) {
|
if (isTypeScript || /from\s+['"]bun:test['"]/.test(source)) {
|
||||||
return { label: 'bun', command: 'bun', args: ['test', file] };
|
return { label: 'bun', command: bunExecutable, args: ['test', file] };
|
||||||
}
|
}
|
||||||
if (/from\s+['"]node:test['"]/.test(source)) {
|
if (/from\s+['"]node:test['"]/.test(source)) {
|
||||||
return { label: 'node', command: 'node', args: ['--test', file] };
|
return { label: 'node', command: 'node', args: ['--test', file] };
|
||||||
@@ -53,7 +56,10 @@ const resolveCommand = (file) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const run = ({ command, args }) => new Promise((resolve) => {
|
const run = ({ command, args }) => new Promise((resolve) => {
|
||||||
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
const child = spawn(command, args, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
let output = '';
|
let output = '';
|
||||||
child.stdout.on('data', (chunk) => { output += chunk; });
|
child.stdout.on('data', (chunk) => { output += chunk; });
|
||||||
child.stderr.on('data', (chunk) => { output += chunk; });
|
child.stderr.on('data', (chunk) => { output += chunk; });
|
||||||
|
|||||||
Reference in New Issue
Block a user