use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Result, bail};
use crate::resolver::ResolutionOverrides;
use crate::tool;
use crate::types::{Ecosystem, PackageManager, ProjectContext};
#[derive(Debug)]
pub(super) struct LocalDispatch {
pub(super) label: String,
pub(super) command: Command,
}
pub(super) fn try_path_token(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
token: &str,
args: &[String],
) -> Result<Option<LocalDispatch>> {
if !has_local_prefix(token) {
return Ok(None);
}
let path = resolve_path(&ctx.root, token);
dispatch_for_path(ctx, overrides, token, &path, args)
}
pub(super) fn try_bare_file(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
token: &str,
args: &[String],
) -> Result<Option<LocalDispatch>> {
if has_local_prefix(token) {
return Ok(None);
}
bare_file_in(ctx, overrides, &ctx.root, token, args)
}
fn bare_file_in(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
base: &Path,
token: &str,
args: &[String],
) -> Result<Option<LocalDispatch>> {
let path = base.join(token);
let Ok(meta) = fs::metadata(&path) else {
return Ok(None);
};
if !meta.is_file() {
return Ok(None);
}
let (label, command) = build_command(ctx, overrides, &path, &meta, args)?;
Ok(Some(LocalDispatch { label, command }))
}
fn dispatch_for_path(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
token: &str,
path: &Path,
args: &[String],
) -> Result<Option<LocalDispatch>> {
let Ok(meta) = fs::metadata(path) else {
if has_local_prefix(token) {
bail!("no such file: {token}");
}
return Ok(None);
};
if !meta.is_file() {
return Ok(None);
}
let (label, command) = build_command(ctx, overrides, path, &meta, args)?;
Ok(Some(LocalDispatch { label, command }))
}
fn build_command(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
path: &Path,
meta: &fs::Metadata,
args: &[String],
) -> Result<(String, Command)> {
let shebang = read_shebang(path);
let routing = routing_for_extension(ctx, overrides, path);
if is_directly_executable(path, meta)
&& (shebang.is_some() || matches!(routing, SourceRouting::Unrecognized))
{
let mut command = Command::new(path);
command.args(args);
return Ok((String::from("exec"), command));
}
if let Some(shebang) = shebang {
return Ok(shebang_command(&shebang, path, args));
}
match routing {
SourceRouting::Runtime(runtime) => {
let (label, command) = command_for_runtime(runtime, path, args);
return Ok((label.to_string(), command));
}
SourceRouting::NodeCannotRunJsx => bail!(
"node cannot run {}: Node has no JSX/TSX transform (it type-strips only \
.ts/.mts/.cts).\nhint: run it with bun or deno (a Bun/Deno project, or pass `--pm \
bun`/`--pm deno`).",
path.display(),
),
SourceRouting::Unrecognized => {}
}
bail!(
"don't know how to run {}: it is not executable, has no `#!` shebang, and has no \
recognized source extension.\nhint: add a shebang, mark it executable (chmod +x), or \
give it a known extension (.ts/.tsx/.js/.mjs/.cjs/.py/.go).",
path.display(),
);
}
pub(super) fn has_local_prefix(token: &str) -> bool {
token.starts_with("./")
|| token.starts_with("../")
|| token.starts_with(".\\")
|| token.starts_with("..\\")
|| token.starts_with('/')
|| token.starts_with('\\')
|| token.starts_with('~')
|| is_windows_drive_abs(token)
}
fn is_windows_drive_abs(token: &str) -> bool {
let bytes = token.as_bytes();
bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'/' || bytes[2] == b'\\')
}
fn resolve_path(base: &Path, token: &str) -> PathBuf {
let expanded = crate::expand_tilde(Path::new(token));
if expanded.is_absolute() {
return expanded;
}
base.join(expanded)
}
fn is_directly_executable(path: &Path, meta: &fs::Metadata) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let _ = path;
meta.permissions().mode() & 0o111 != 0
}
#[cfg(windows)]
{
let _ = meta;
matches!(ext_lower(path).as_deref(), Some("exe" | "com"))
}
#[cfg(not(any(unix, windows)))]
{
let _ = (path, meta);
false
}
}
fn ext_lower(path: &Path) -> Option<String> {
path.extension()
.and_then(|ext| ext.to_str())
.map(str::to_ascii_lowercase)
}
#[derive(Debug, PartialEq, Eq)]
struct Shebang {
program: String,
args: Vec<String>,
}
fn read_shebang(path: &Path) -> Option<Shebang> {
use std::io::Read as _;
let Ok(mut file) = fs::File::open(path) else {
return None;
};
let mut buf = [0u8; 256];
let Ok(read) = file.read(&mut buf) else {
return None;
};
let head = &buf[..read];
if !head.starts_with(b"#!") {
return None;
}
let line_end = head
.iter()
.position(|&byte| byte == b'\n')
.unwrap_or(head.len());
let Ok(line) = std::str::from_utf8(&head[..line_end]) else {
return None;
};
parse_shebang(line)
}
fn parse_shebang(line: &str) -> Option<Shebang> {
let body = line.strip_prefix("#!")?.trim();
if body.is_empty() {
return None;
}
let (interpreter, rest) = match body.split_once(char::is_whitespace) {
Some((interp, rest)) => (interp, rest.trim()),
None => (body, ""),
};
if is_env(interpreter) {
let split_form = rest
.strip_prefix("--split-string=")
.or_else(|| rest.strip_prefix("--split-string"))
.or_else(|| rest.strip_prefix("-S"))
.map(str::trim);
let words = split_form.map_or_else(|| single_arg(rest), split_env_string);
let mut parts = words.into_iter();
let program = parts.next()?;
let args = parts.collect();
return Some(Shebang { program, args });
}
Some(Shebang {
program: interpreter.to_string(),
args: single_arg(rest),
})
}
fn single_arg(rest: &str) -> Vec<String> {
if rest.is_empty() {
Vec::new()
} else {
vec![rest.to_owned()]
}
}
fn split_env_string(s: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut started = false;
let mut chars = s.chars();
while let Some(c) = chars.next() {
match c {
c if c.is_ascii_whitespace() => {
if started {
words.push(std::mem::take(&mut current));
started = false;
}
}
'\'' => {
started = true;
for quoted in chars.by_ref() {
if quoted == '\'' {
break;
}
current.push(quoted);
}
}
'"' => {
started = true;
while let Some(quoted) = chars.next() {
match quoted {
'"' => break,
'\\' => match chars.next() {
Some(esc @ ('"' | '\\' | '$' | '`')) => current.push(esc),
Some(other) => {
current.push('\\');
current.push(other);
}
None => current.push('\\'),
},
_ => current.push(quoted),
}
}
}
'\\' => {
started = true;
if let Some(escaped) = chars.next() {
current.push(escaped);
}
}
_ => {
started = true;
current.push(c);
}
}
}
if started {
words.push(current);
}
words
}
fn is_env(interpreter: &str) -> bool {
Path::new(interpreter)
.file_name()
.is_some_and(|name| name == "env")
}
fn shebang_command(shebang: &Shebang, file: &Path, args: &[String]) -> (String, Command) {
let mut command = tool::program::command(&shebang.program);
command.args(&shebang.args).arg(file).args(args);
(shebang_label(shebang), command)
}
fn shebang_label(shebang: &Shebang) -> String {
let program = Path::new(&shebang.program)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(shebang.program.as_str());
if shebang.args.is_empty() {
program.to_string()
} else {
format!("{program} {}", shebang.args.join(" "))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Runtime {
Bun,
Deno,
Node,
Uv,
Python,
Go,
#[cfg(windows)]
WindowsScript,
}
#[derive(Debug, PartialEq, Eq)]
enum SourceRouting {
Runtime(Runtime),
NodeCannotRunJsx,
Unrecognized,
}
fn routing_for_extension(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
path: &Path,
) -> SourceRouting {
let Some(ext) = ext_lower(path) else {
return SourceRouting::Unrecognized;
};
match ext.as_str() {
"ts" | "mts" | "cts" | "js" | "mjs" | "cjs" => {
SourceRouting::Runtime(js_runtime(ctx, overrides))
}
"jsx" | "tsx" => match js_runtime(ctx, overrides) {
runtime @ (Runtime::Bun | Runtime::Deno) => SourceRouting::Runtime(runtime),
_ => SourceRouting::NodeCannotRunJsx,
},
"py" => SourceRouting::Runtime(py_runtime(ctx, overrides)),
"go" => SourceRouting::Runtime(Runtime::Go),
#[cfg(windows)]
"ps1" | "bat" | "cmd" => SourceRouting::Runtime(Runtime::WindowsScript),
_ => SourceRouting::Unrecognized,
}
}
fn js_runtime(ctx: &ProjectContext, overrides: &ResolutionOverrides) -> Runtime {
if let Some(runtime) = js_runtime_from_override(overrides) {
return runtime;
}
if ctx.package_managers.contains(&PackageManager::Deno) {
return Runtime::Deno;
}
if ctx.package_managers.contains(&PackageManager::Bun) {
return Runtime::Bun;
}
Runtime::Node
}
fn js_runtime_from_override(overrides: &ResolutionOverrides) -> Option<Runtime> {
let pm = overrides
.pm
.as_ref()
.map(|over| over.pm)
.or_else(|| {
overrides
.pm_by_ecosystem
.get(&Ecosystem::Node)
.map(|over| over.pm)
})
.or_else(|| {
overrides
.pm_by_ecosystem
.get(&Ecosystem::Deno)
.map(|over| over.pm)
})?;
match pm {
PackageManager::Deno => Some(Runtime::Deno),
PackageManager::Bun => Some(Runtime::Bun),
node if node.is_node() => Some(Runtime::Node),
_ => None,
}
}
fn py_runtime(ctx: &ProjectContext, overrides: &ResolutionOverrides) -> Runtime {
let overridden = overrides.pm.as_ref().map(|over| over.pm).or_else(|| {
overrides
.pm_by_ecosystem
.get(&Ecosystem::Python)
.map(|over| over.pm)
});
if let Some(pm) = overridden {
if pm == PackageManager::Uv {
return Runtime::Uv;
}
if pm.ecosystem() == Ecosystem::Python {
return Runtime::Python;
}
}
if ctx.package_managers.contains(&PackageManager::Uv) {
Runtime::Uv
} else {
Runtime::Python
}
}
fn command_for_runtime(runtime: Runtime, file: &Path, args: &[String]) -> (&'static str, Command) {
match runtime {
Runtime::Bun => ("bun", tool::bun::run_file_cmd(file, args)),
Runtime::Deno => ("deno run", tool::deno::run_file_cmd(file, args)),
Runtime::Node => ("node", tool::node::run_file_cmd(file, args)),
Runtime::Uv => ("uv run", tool::uv::run_file_cmd(file, args)),
Runtime::Python => (
tool::python::PYTHON_BIN,
tool::python::run_file_cmd(file, args),
),
Runtime::Go => ("go run", tool::go_pm::run_file_cmd(file, args)),
#[cfg(windows)]
Runtime::WindowsScript => windows_script_command(file, args),
}
}
#[cfg(windows)]
fn windows_script_command(file: &Path, args: &[String]) -> (&'static str, Command) {
if ext_lower(file).as_deref() == Some("ps1") {
let mut command = tool::program::command("powershell");
command
.arg("-NoProfile")
.arg("-ExecutionPolicy")
.arg("Bypass")
.arg("-File")
.arg(file)
.args(args);
("powershell", command)
} else {
let mut command = tool::program::command("cmd");
command.arg("/c").arg(file).args(args);
("cmd", command)
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::{
Runtime, SourceRouting, bare_file_in, build_command, dispatch_for_path, has_local_prefix,
js_runtime, parse_shebang, py_runtime, routing_for_extension, try_bare_file,
try_path_token,
};
use crate::resolver::ResolutionOverrides;
use crate::tool::test_support::TempDir;
use crate::types::{PackageManager, ProjectContext};
fn context(pms: Vec<PackageManager>) -> ProjectContext {
ProjectContext {
root: PathBuf::from("."),
package_managers: pms,
task_runners: Vec::new(),
tasks: Vec::new(),
node_version: None,
current_node: None,
is_monorepo: false,
warnings: Vec::new(),
}
}
fn context_rooted(pms: Vec<PackageManager>, root: PathBuf) -> ProjectContext {
ProjectContext {
root,
..context(pms)
}
}
fn args_of(command: &std::process::Command) -> Vec<String> {
command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn has_local_prefix_distinguishes_paths_from_remote_specs() {
assert!(has_local_prefix("./x"));
assert!(has_local_prefix("../x"));
assert!(has_local_prefix("/x"));
assert!(has_local_prefix("~/x"));
assert!(has_local_prefix(r".\x"));
assert!(has_local_prefix(r"C:\x"));
assert!(!has_local_prefix("@scope/pkg"));
assert!(!has_local_prefix("github.com/owner/tool"));
assert!(!has_local_prefix("bin/x.ts"));
}
#[test]
fn parse_shebang_plain_interpreter() {
let parsed = parse_shebang("#!/bin/sh").expect("shebang should parse");
assert_eq!(parsed.program, "/bin/sh");
assert!(parsed.args.is_empty());
}
#[test]
fn parse_shebang_interpreter_with_args() {
let parsed = parse_shebang("#!/usr/bin/python3 -O").expect("shebang should parse");
assert_eq!(parsed.program, "/usr/bin/python3");
assert_eq!(parsed.args, ["-O"]);
}
#[test]
fn parse_shebang_env_resolves_real_interpreter() {
let parsed = parse_shebang("#!/usr/bin/env python3").expect("shebang should parse");
assert_eq!(parsed.program, "python3");
assert!(parsed.args.is_empty());
}
#[test]
fn parse_shebang_env_split_string_forms() {
for line in [
"#!/usr/bin/env -S deno run -A",
"#!/usr/bin/env --split-string=deno run -A",
"#!/usr/bin/env --split-string deno run -A",
"#!/usr/bin/env -Sdeno run -A",
] {
let parsed = parse_shebang(line).unwrap_or_else(|| panic!("should parse {line:?}"));
assert_eq!(parsed.program, "deno", "program from {line:?}");
assert_eq!(parsed.args, ["run", "-A"], "args from {line:?}");
}
}
#[test]
fn parse_shebang_env_split_string_preserves_quoted_args() {
let double = parse_shebang(r#"#!/usr/bin/env -S deno run --allow-read="a b""#)
.expect("double-quoted -S shebang should parse");
assert_eq!(double.program, "deno");
assert_eq!(double.args, ["run", "--allow-read=a b"]);
let single = parse_shebang("#!/usr/bin/env -S deno run --allow-read='a b'")
.expect("single-quoted -S shebang should parse");
assert_eq!(single.args, ["run", "--allow-read=a b"]);
}
#[test]
fn parse_shebang_plain_env_keeps_tail_as_single_argument() {
let parsed = parse_shebang("#!/usr/bin/env python3 -O").expect("should parse");
assert_eq!(parsed.program, "python3 -O");
assert!(parsed.args.is_empty());
let one = parse_shebang("#!/usr/bin/env python3").expect("should parse");
assert_eq!(one.program, "python3");
assert!(one.args.is_empty());
}
#[test]
fn parse_shebang_direct_interpreter_keeps_tail_as_single_argument() {
let parsed = parse_shebang("#!/bin/awk -f -v").expect("should parse");
assert_eq!(parsed.program, "/bin/awk");
assert_eq!(parsed.args, ["-f -v"]);
}
#[test]
fn parse_shebang_rejects_non_shebang() {
assert!(parse_shebang("not a shebang").is_none());
assert!(parse_shebang("#!").is_none());
assert!(parse_shebang("#! ").is_none());
}
#[test]
fn js_runtime_follows_detected_project() {
let defaults = ResolutionOverrides::default();
assert_eq!(
js_runtime(&context(vec![PackageManager::Deno]), &defaults),
Runtime::Deno,
);
assert_eq!(
js_runtime(&context(vec![PackageManager::Bun]), &defaults),
Runtime::Bun,
);
assert_eq!(
js_runtime(&context(vec![PackageManager::Pnpm]), &defaults),
Runtime::Node,
);
assert_eq!(js_runtime(&context(vec![]), &defaults), Runtime::Node);
}
#[test]
fn py_runtime_prefers_uv_when_detected() {
let defaults = ResolutionOverrides::default();
assert_eq!(
py_runtime(&context(vec![PackageManager::Uv]), &defaults),
Runtime::Uv,
);
assert_eq!(
py_runtime(&context(vec![PackageManager::Poetry]), &defaults),
Runtime::Python,
);
assert_eq!(py_runtime(&context(vec![]), &defaults), Runtime::Python);
}
#[test]
fn routing_for_extension_maps_known_sources() {
let ctx = context(vec![PackageManager::Bun]);
let defaults = ResolutionOverrides::default();
assert_eq!(
routing_for_extension(&ctx, &defaults, Path::new("a.ts")),
SourceRouting::Runtime(Runtime::Bun),
);
assert_eq!(
routing_for_extension(&ctx, &defaults, Path::new("a.go")),
SourceRouting::Runtime(Runtime::Go),
);
assert_eq!(
routing_for_extension(&ctx, &defaults, Path::new("a.py")),
SourceRouting::Runtime(Runtime::Python),
);
assert_eq!(
routing_for_extension(&ctx, &defaults, Path::new("a.txt")),
SourceRouting::Unrecognized,
);
}
#[test]
fn routing_for_extension_routes_jsx_tsx_to_bun_or_deno() {
let defaults = ResolutionOverrides::default();
for ext in ["a.jsx", "a.tsx"] {
assert_eq!(
routing_for_extension(
&context(vec![PackageManager::Bun]),
&defaults,
Path::new(ext),
),
SourceRouting::Runtime(Runtime::Bun),
"{ext} should run via bun in a bun project",
);
assert_eq!(
routing_for_extension(
&context(vec![PackageManager::Deno]),
&defaults,
Path::new(ext),
),
SourceRouting::Runtime(Runtime::Deno),
"{ext} should run via deno in a deno project",
);
}
}
#[test]
fn routing_for_extension_rejects_jsx_tsx_on_node() {
let defaults = ResolutionOverrides::default();
for ext in ["a.jsx", "a.tsx"] {
assert_eq!(
routing_for_extension(
&context(vec![PackageManager::Pnpm]),
&defaults,
Path::new(ext),
),
SourceRouting::NodeCannotRunJsx,
"{ext} must not route to bare node",
);
assert_eq!(
routing_for_extension(&context(vec![]), &defaults, Path::new(ext)),
SourceRouting::NodeCannotRunJsx,
"{ext} must not route to bare node in a no-PM project",
);
}
}
#[test]
fn build_command_runs_ts_via_detected_runtime() {
let dir = TempDir::new("local-ts");
let file = dir.path().join("script.ts");
std::fs::write(&file, "console.log('hi')\n").expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[String::from("--flag")],
)
.expect("ts file should build a command");
assert_eq!(label, "bun");
assert_eq!(command.get_program().to_string_lossy(), "bun");
assert_eq!(
args_of(&command),
[file.to_string_lossy().into_owned(), String::from("--flag")],
);
}
#[test]
fn build_command_runs_py_via_uv_when_detected() {
let dir = TempDir::new("local-py");
let file = dir.path().join("task.py");
std::fs::write(&file, "print('hi')\n").expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Uv]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect("py file should build a command");
assert_eq!(label, "uv run");
assert_eq!(command.get_program().to_string_lossy(), "uv");
assert_eq!(
args_of(&command),
[String::from("run"), file.to_string_lossy().into_owned()],
);
}
#[test]
fn build_command_honors_shebang_without_exec_bit() {
let dir = TempDir::new("local-shebang");
let file = dir.path().join("noexec.sh");
std::fs::write(&file, "#!/usr/bin/env -S deno run -A\nconsole.log(1)\n")
.expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[String::from("x")],
)
.expect("shebang file should build a command");
assert_eq!(label, "deno run -A");
assert_eq!(command.get_program().to_string_lossy(), "deno");
assert_eq!(
args_of(&command),
[
String::from("run"),
String::from("-A"),
file.to_string_lossy().into_owned(),
String::from("x"),
],
);
}
#[test]
fn build_command_errors_on_unrunnable_file() {
let dir = TempDir::new("local-unknown");
let file = dir.path().join("data.bin");
std::fs::write(&file, [0u8, 1, 2, 3]).expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let err = build_command(
&context(vec![]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect_err("a non-runnable file should error");
assert!(format!("{err:#}").contains("don't know how to run"));
}
#[test]
fn build_command_errors_on_jsx_in_node_project() {
let dir = TempDir::new("local-tsx-node");
let file = dir.path().join("app.tsx");
std::fs::write(&file, "export const App = () => <div/>;\n")
.expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let err = build_command(
&context(vec![PackageManager::Pnpm]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect_err("a .tsx in a node project should error, not build `node app.tsx`");
assert!(format!("{err:#}").contains("node cannot run"));
}
#[test]
fn build_command_runs_tsx_via_bun_when_detected() {
let dir = TempDir::new("local-tsx-bun");
let file = dir.path().join("app.tsx");
std::fs::write(&file, "export const App = () => <div/>;\n")
.expect("file should be written");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect("a .tsx in a bun project should build a bun command");
assert_eq!(label, "bun");
assert_eq!(command.get_program().to_string_lossy(), "bun");
}
#[cfg(unix)]
#[test]
fn build_command_spawns_execute_only_binary_directly() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("local-exec-only");
let file = dir.path().join("tool");
std::fs::write(&file, [0u8, 1, 2, 3]).expect("file should be written");
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o111))
.expect("chmod should succeed");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![]),
&ResolutionOverrides::default(),
&file,
&meta,
&[String::from("arg")],
)
.expect("an execute-only binary should spawn directly, not fail the shebang read");
assert_eq!(label, "exec");
assert_eq!(PathBuf::from(command.get_program()), file);
assert_eq!(args_of(&command), [String::from("arg")]);
}
#[cfg(unix)]
#[test]
fn build_command_routes_execute_only_source_to_runtime_not_pm_exec() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("local-exec-only-ts");
let file = dir.path().join("main.ts");
std::fs::write(&file, "console.log(1)\n").expect("file should be written");
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o111))
.expect("chmod should succeed");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect("an execute-only source file should route to its runtime");
assert_eq!(label, "bun", "a 0111 .ts routes to bun, never bunx");
assert_eq!(command.get_program().to_string_lossy(), "bun");
}
#[cfg(unix)]
#[test]
fn build_command_spawns_executable_directly() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("local-exec");
let file = dir.path().join("build.sh");
std::fs::write(&file, "#!/bin/sh\necho hi\n").expect("file should be written");
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o755))
.expect("chmod should succeed");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![]),
&ResolutionOverrides::default(),
&file,
&meta,
&[String::from("arg")],
)
.expect("executable should build a command");
assert_eq!(label, "exec");
assert_eq!(
PathBuf::from(command.get_program()),
file,
"an executable file is spawned by its own path",
);
assert_eq!(args_of(&command), [String::from("arg")]);
}
#[cfg(unix)]
#[test]
fn build_command_runs_exec_bit_source_without_shebang_via_runtime() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("local-exec-ts");
let file = dir.path().join("deploy.ts");
std::fs::write(&file, "console.log('hi')\n").expect("file should be written");
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o755))
.expect("chmod should succeed");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[String::from("--flag")],
)
.expect("an exec-bit source file should build a runtime command");
assert_eq!(label, "bun", "a shebang-less +x .ts runs via bun, not exec");
assert_eq!(command.get_program().to_string_lossy(), "bun");
assert_eq!(
args_of(&command),
[file.to_string_lossy().into_owned(), String::from("--flag")],
);
}
#[cfg(unix)]
#[test]
fn build_command_execs_exec_bit_source_with_shebang_directly() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("local-exec-ts-shebang");
let file = dir.path().join("deploy.ts");
std::fs::write(&file, "#!/usr/bin/env -S deno run -A\nconsole.log('hi')\n")
.expect("file should be written");
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o755))
.expect("chmod should succeed");
let meta = std::fs::metadata(&file).expect("metadata should read");
let (label, command) = build_command(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
&file,
&meta,
&[],
)
.expect("an exec-bit shebang file should build a command");
assert_eq!(label, "exec", "a +x file with a shebang spawns directly");
assert_eq!(PathBuf::from(command.get_program()), file);
}
#[test]
fn dispatch_for_path_passes_through_directories() {
let dir = TempDir::new("local-dir");
let result = dispatch_for_path(
&context(vec![]),
&ResolutionOverrides::default(),
"./some-dir",
dir.path(),
&[],
)
.expect("a directory should not error");
assert!(result.is_none(), "directories fall through, not run");
}
#[test]
fn dispatch_for_path_errors_on_missing_explicit_path() {
let dir = TempDir::new("local-missing");
let missing = dir.path().join("ghost.ts");
let err = dispatch_for_path(
&context(vec![]),
&ResolutionOverrides::default(),
"./ghost.ts",
&missing,
&[],
)
.expect_err("an explicit missing path should error");
assert!(format!("{err:#}").contains("no such file"));
}
#[test]
fn dispatch_for_path_passes_through_missing_remote_spec() {
let dir = TempDir::new("local-remote");
let missing = dir.path().join("biome");
let result = dispatch_for_path(
&context(vec![]),
&ResolutionOverrides::default(),
"@biomejs/biome",
&missing,
&[],
)
.expect("a remote spec should not error");
assert!(
result.is_none(),
"a separator-bearing remote spec falls through to PM-exec",
);
}
#[test]
fn try_path_token_ignores_bare_names() {
let result = try_path_token(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
"test",
&[],
)
.expect("bare names should not error");
assert!(
result.is_none(),
"bare names are handled by the bare-file fallback, not the path branch"
);
}
#[test]
fn bare_file_runs_a_runnable_file_in_base_dir() {
let dir = TempDir::new("bare-file");
std::fs::write(dir.path().join("main.ts"), "console.log(1)\n")
.expect("file should be written");
let dispatch = bare_file_in(
&context(vec![PackageManager::Deno]),
&ResolutionOverrides::default(),
dir.path(),
"main.ts",
&[],
)
.expect("a runnable bare file should not error")
.expect("a runnable bare file should dispatch");
assert_eq!(dispatch.label, "deno run");
assert_eq!(dispatch.command.get_program().to_string_lossy(), "deno");
}
#[test]
fn bare_file_missing_token_falls_through_to_pm_exec() {
let dir = TempDir::new("bare-file-miss");
let ctx = context(vec![PackageManager::Bun]);
let defaults = ResolutionOverrides::default();
assert!(
bare_file_in(&ctx, &defaults, dir.path(), "biome", &[])
.expect("a missing token should not error")
.is_none(),
"a non-existent token must fall through to PM-exec",
);
}
#[test]
fn bare_file_existing_unsupported_file_errors() {
let dir = TempDir::new("bare-file-unsupported");
std::fs::write(dir.path().join("data.bin"), [0u8, 1, 2]).expect("file should be written");
let ctx = context(vec![PackageManager::Bun]);
let defaults = ResolutionOverrides::default();
let err = bare_file_in(&ctx, &defaults, dir.path(), "data.bin", &[])
.expect_err("an existing unsupported file must error, not fall through to PM-exec");
assert!(format!("{err:#}").contains("don't know how to run"));
}
#[test]
fn bare_file_errors_on_jsx_in_node_project() {
let dir = TempDir::new("bare-tsx-node");
std::fs::write(
dir.path().join("app.tsx"),
"export const App = () => <div/>;\n",
)
.expect("file should be written");
let err = bare_file_in(
&context(vec![PackageManager::Pnpm]),
&ResolutionOverrides::default(),
dir.path(),
"app.tsx",
&[],
)
.expect_err("a bare .tsx in a node project must error, not build a pnpm/npx command");
assert!(format!("{err:#}").contains("node cannot run"));
}
#[test]
fn try_bare_file_declines_local_prefix_tokens() {
let ctx = context(vec![PackageManager::Bun]);
let defaults = ResolutionOverrides::default();
for token in ["./main.ts", "../main.ts", "/abs/main.ts", "~/main.ts"] {
assert!(
try_bare_file(&ctx, &defaults, token, &[])
.expect("a local-prefix token should not error")
.is_none(),
"{token} carries a local prefix and must be declined here",
);
}
}
#[test]
fn try_path_token_declines_relative_separator_tokens() {
let result = try_path_token(
&context(vec![PackageManager::Bun]),
&ResolutionOverrides::default(),
"bin/tool",
&[],
)
.expect("a relative separator token should not error");
assert!(
result.is_none(),
"a prefix-less relative path is left to the after-miss fallback",
);
}
#[test]
fn bare_file_resolves_relative_separator_token() {
let dir = TempDir::new("bare-file-nested");
std::fs::create_dir(dir.path().join("bin")).expect("subdir should be created");
std::fs::write(dir.path().join("bin").join("main.ts"), "console.log(1)\n")
.expect("file should be written");
let dispatch = bare_file_in(
&context(vec![PackageManager::Deno]),
&ResolutionOverrides::default(),
dir.path(),
"bin/main.ts",
&[],
)
.expect("a runnable relative-separator file should not error")
.expect("a runnable relative-separator file should dispatch");
assert_eq!(dispatch.label, "deno run");
assert_eq!(dispatch.command.get_program().to_string_lossy(), "deno");
}
#[test]
fn try_bare_file_resolves_against_ctx_root() {
let dir = TempDir::new("bare-root");
std::fs::write(dir.path().join("main.ts"), "console.log(1)\n")
.expect("file should be written");
let ctx = context_rooted(vec![PackageManager::Deno], dir.path().to_path_buf());
let dispatch = try_bare_file(&ctx, &ResolutionOverrides::default(), "main.ts", &[])
.expect("a runnable bare file under ctx.root should not error")
.expect("a runnable bare file under ctx.root should dispatch");
assert_eq!(dispatch.label, "deno run");
assert_eq!(dispatch.command.get_program().to_string_lossy(), "deno");
}
#[test]
fn try_path_token_resolves_relative_prefix_against_ctx_root() {
let dir = TempDir::new("path-root");
std::fs::write(dir.path().join("main.ts"), "console.log(1)\n")
.expect("file should be written");
let ctx = context_rooted(vec![PackageManager::Bun], dir.path().to_path_buf());
let dispatch = try_path_token(&ctx, &ResolutionOverrides::default(), "./main.ts", &[])
.expect("an explicit relative path should not error")
.expect("a runnable ./main.ts under ctx.root should dispatch");
assert_eq!(dispatch.label, "bun");
assert_eq!(dispatch.command.get_program().to_string_lossy(), "bun");
}
}