fix(tooling): resolve Bun executable on Windows
This commit is contained in:
+22
-7
@@ -5,18 +5,23 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveBunExecutable } from './lib/bun-executable.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const useDetachedChildren = process.platform === 'darwin';
|
||||
const webRoot = path.join(repoRoot, 'packages/web');
|
||||
|
||||
const bunExecutable = resolveBunExecutable();
|
||||
|
||||
function run(label, command, args, env = {}, options = {}) {
|
||||
return spawn(command, args, {
|
||||
cwd: options.cwd || repoRoot,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ...env },
|
||||
detached: useDetachedChildren,
|
||||
windowsHide: true,
|
||||
}).on('error', (error) => {
|
||||
console.error(`[dev:web:hmr] Failed to start ${label}:`, error);
|
||||
});
|
||||
@@ -112,15 +117,25 @@ function clearViteCache() {
|
||||
|
||||
clearViteCache();
|
||||
|
||||
const api = run('api', 'bun', ['run', '--cwd', 'packages/web', 'dev:server:watch'], {
|
||||
OPENCHAMBER_PORT: backendPort,
|
||||
// Dev backends share the relay identity with the production instance; never
|
||||
// let them capture the machine's relay host on their own.
|
||||
OPENCHAMBER_RELAY_HOST: process.env.OPENCHAMBER_RELAY_HOST || 'off',
|
||||
});
|
||||
const api = run(
|
||||
'api',
|
||||
bunExecutable,
|
||||
[
|
||||
'run',
|
||||
'--cwd',
|
||||
'packages/web',
|
||||
'dev:server:watch',
|
||||
],
|
||||
{
|
||||
OPENCHAMBER_PORT: backendPort,
|
||||
// Dev backends share the relay identity with the production instance; never
|
||||
// let them capture the machine's relay host on their own.
|
||||
OPENCHAMBER_RELAY_HOST: process.env.OPENCHAMBER_RELAY_HOST || 'off',
|
||||
},
|
||||
);
|
||||
const vite = run(
|
||||
'vite',
|
||||
'bun',
|
||||
bunExecutable,
|
||||
['x', 'vite', '--force', '--host', hmrHost, '--port', uiPort, '--strictPort'],
|
||||
{
|
||||
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 path from 'node:path';
|
||||
|
||||
import { resolveBunExecutable } from './lib/bun-executable.mjs';
|
||||
|
||||
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 MAX_PARALLEL = 4;
|
||||
const bunExecutable = resolveBunExecutable();
|
||||
|
||||
const collect = (root, found = []) => {
|
||||
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
|
||||
// specifiers these files use (`./sseProxy`), so it never ran them at all.
|
||||
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)) {
|
||||
return { label: 'node', command: 'node', args: ['--test', file] };
|
||||
@@ -53,7 +56,10 @@ const resolveCommand = (file) => {
|
||||
};
|
||||
|
||||
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 = '';
|
||||
child.stdout.on('data', (chunk) => { output += chunk; });
|
||||
child.stderr.on('data', (chunk) => { output += chunk; });
|
||||
|
||||
Reference in New Issue
Block a user