runtime-detector.ts•5.37 kB
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { existsSync } from 'fs';
import { basename, extname } from 'path';
const RUNTIME_NODE = 'node';
const RUNTIME_BUN = 'bun';
const RUNTIME_COMPILED = 'compiled-binary';
const NODE_PATTERNS = ['node', 'nodejs'];
const BUN_PATTERNS = ['bun'];
const SCRIPT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'];
/**
* CLI runtime types supported by the detector
*/
export type CLIRuntimeType = 'node' | 'bun' | 'compiled-binary';
/**
* Information about the CLI runtime environment
*/
export interface CLIRuntimeInfo {
/** Type of CLI runtime or execution mode */
type: CLIRuntimeType;
/** Path to the executable (node, bun, or the compiled binary itself) */
execPath: string;
/** Path to the script being executed (undefined for compiled binaries) */
scriptPath?: string;
/** Whether this is a compiled binary (e.g., Bun-compiled) */
isCompiledBinary: boolean;
/** Platform information */
platform: NodeJS.Platform;
}
/**
* Safely checks if a file exists without throwing errors
* @param path - File path to check
* @returns true if the file exists, false otherwise
*/
function safeExistsSync(path: string | undefined): boolean {
if (!path) return false;
try {
return existsSync(path);
} catch {
return false;
}
}
/**
* Checks if the given path has a recognized script file extension
* @param path - File path to check
* @returns true if the path ends with a known script extension
* @internal Kept for potential future use, though not currently used in detection logic
*/
function isLikelyScriptFile(path: string | undefined): boolean {
if (!path) return false;
const ext = extname(path).toLowerCase();
return SCRIPT_EXTENSIONS.includes(ext);
}
/**
* Checks if executable name contains any of the given patterns
* @param execName - Name of the executable
* @param patterns - Array of patterns to match against
* @returns true if any pattern is found in the executable name
*/
function matchesPatterns(execName: string, patterns: string[]): boolean {
const lowerExecName = execName.toLowerCase();
return patterns.some((pattern) => lowerExecName.includes(pattern));
}
/**
* Detects the current CLI runtime environment and execution context.
* This helps determine how to properly spawn child processes.
*
* @returns CLI runtime information including type, paths, and platform
* @throws Error if unable to determine CLI runtime executable path
*/
export function detectCLIRuntime(): CLIRuntimeInfo {
const platform = process.platform;
const execPath = process.execPath;
if (!execPath || execPath.trim() === '') {
throw new Error('Unable to determine CLI runtime executable path');
}
const argv0 = process.argv[0];
const argv1 = process.argv[1];
const execBasename = basename(execPath);
const argv0Basename = argv0 ? basename(argv0) : '';
const hasBunVersion = 'bun' in (process.versions || {});
const hasNodeVersion = 'node' in (process.versions || {});
const execMatchesBun = matchesPatterns(execBasename, BUN_PATTERNS);
const execMatchesNode = matchesPatterns(execBasename, NODE_PATTERNS);
const argv0MatchesBun = matchesPatterns(argv0Basename, BUN_PATTERNS);
const argv0MatchesNode = matchesPatterns(argv0Basename, NODE_PATTERNS);
const hasScriptArg = !!argv1;
const scriptExists = hasScriptArg && safeExistsSync(argv1);
let type: CLIRuntimeType;
let scriptPath: string | undefined;
let isCompiledBinary: boolean;
// Determine runtime type based on most reliable indicators
if (hasBunVersion || execMatchesBun || argv0MatchesBun) {
// Check if this is a Bun-compiled binary
// Bun compiled binaries have virtual paths like /$bunfs/root/...
if (
argv1 &&
(argv1.startsWith('/$bunfs/') || /^[A-Za-z]:[\\/]+~BUN[\\/]+/.test(argv1))
) {
// This is a Bun-compiled binary
type = RUNTIME_COMPILED;
scriptPath = undefined;
isCompiledBinary = true;
} else {
// Regular Bun runtime
type = RUNTIME_BUN;
scriptPath = argv1;
isCompiledBinary = false;
}
} else if (hasNodeVersion || execMatchesNode || argv0MatchesNode) {
// Definitely Node.js
type = RUNTIME_NODE;
scriptPath = argv1;
isCompiledBinary = false;
} else if (!hasScriptArg || !scriptExists) {
// No script argument or script doesn't exist - likely compiled binary
type = RUNTIME_COMPILED;
scriptPath = undefined;
isCompiledBinary = true;
} else {
// Have a script argument that exists but unknown runtime
// This handles cases like custom Node.js builds with unusual names
type = RUNTIME_NODE;
scriptPath = argv1;
isCompiledBinary = false;
}
return {
type,
execPath,
scriptPath,
isCompiledBinary,
platform,
};
}