use std::collections::HashSet;
use std::collections::VecDeque;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use deno_ast::MediaType;
use deno_ast::ModuleSpecifier;
use deno_config::deno_json::NodeModulesDirMode;
use deno_core::anyhow::Context;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::futures::FutureExt;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use deno_npm_installer::graph::NpmCachingStrategy;
use deno_path_util::resolve_url_or_path;
use deno_path_util::url_from_file_path;
use deno_path_util::url_to_file_path;
use deno_terminal::colors;
use rand::Rng;
use super::installer::BinNameResolver;
use crate::args::CliOptions;
use crate::args::CompileFlags;
use crate::args::ConfigFlag;
use crate::args::DenoSubcommand;
use crate::args::Flags;
use crate::args::TypeCheckMode;
use crate::factory::CliFactory;
use crate::graph_util::ModuleGraphCreator;
use crate::standalone::binary::WriteBinOptions;
use crate::standalone::binary::is_standalone_binary;
use crate::util::temp::create_temp_node_modules_dir;
pub async fn compile(
mut flags: Flags,
mut compile_flags: CompileFlags,
) -> Result<(), AnyError> {
let source_dir = if compile_flags.source_file == "." {
Some(flags.initial_cwd.clone().unwrap_or_else(|| {
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
}))
} else {
let path = PathBuf::from(&compile_flags.source_file);
let path = if path.is_absolute() {
path
} else {
flags
.initial_cwd
.clone()
.unwrap_or_else(|| {
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
})
.join(path)
};
path.is_dir().then_some(path)
};
let _framework_entrypoint_file = if let Some(dir) = source_dir {
if let Some(detection) = super::framework::detect_framework(&dir)? {
log::info!("Detected {} framework", detection.name);
if let Some(build_cmd) = &detection.build_command {
log::info!(
"{} {} project...",
colors::green("Building"),
detection.name,
);
let status = std::process::Command::new(&build_cmd[0])
.args(&build_cmd[1..])
.current_dir(&dir)
.status()
.with_context(|| {
format!("Failed to run build command: {}", build_cmd.join(" "))
})?;
if !status.success() {
bail!(
"{} build failed (exit code: {})",
detection.name,
status.code().unwrap_or(-1)
);
}
}
flags.unstable_config.detect_cjs = true;
if matches!(detection.name, "Next.js" | "SvelteKit")
&& !matches!(flags.type_check_mode, TypeCheckMode::None)
{
log::info!(
"Disabling Deno type checking for {} compile; the framework handles app compilation itself",
detection.name
);
flags.type_check_mode = TypeCheckMode::None;
}
let entrypoint_path = dir.join(format!(
".deno_compile_entry_{:08x}.ts",
rand::thread_rng().r#gen::<u32>()
));
std::fs::write(&entrypoint_path, detection.entrypoint_code)?;
compile_flags.source_file = entrypoint_path.display().to_string();
if compile_flags.output.is_none()
&& let Some(dir_name) = dir.file_name()
{
compile_flags.output = Some(dir_name.to_string_lossy().into_owned());
}
for inc in detection.include_paths {
let resolved = dir.join(&inc).display().to_string();
if !compile_flags.include.contains(&resolved) {
compile_flags.include.push(resolved);
}
}
Some(entrypoint_path)
} else {
bail!(
"Could not detect a supported framework in '{}'.\n\
Supported frameworks: Next.js, Astro, Fresh, Remix, SvelteKit, Nuxt, SolidStart, TanStack Start, Vite SSR\n\
Provide an explicit entrypoint instead of a directory.",
dir.display()
);
}
} else {
None
};
flags.subcommand = DenoSubcommand::Compile(compile_flags.clone());
struct CleanupGuard(Vec<PathBuf>);
impl Drop for CleanupGuard {
fn drop(&mut self) {
for path in &self.0 {
let _ = std::fs::remove_file(path);
}
}
}
let _framework_cleanup =
_framework_entrypoint_file.map(|p| CleanupGuard(vec![p]));
let _temp_dir =
if compile_flags.source_file.to_lowercase().starts_with("npm:")
&& flags.node_modules_dir.is_none()
&& !matches!(flags.config_flag, ConfigFlag::Path(_))
{
let temp_node_modules_dir = create_temp_node_modules_dir()
.context("Failed creating temp directory for node_modules folder.")?;
flags.initial_cwd = Some(temp_node_modules_dir.parent().to_path_buf());
flags.internal.root_node_modules_dir_override =
Some(temp_node_modules_dir.node_modules_dir_path().to_path_buf());
flags.node_modules_dir = Some(NodeModulesDirMode::Auto);
Some(temp_node_modules_dir)
} else {
None
};
let _bundle_cleanup = if compile_flags.bundle {
log::warn!(
"{} deno compile --bundle is experimental and may change.",
colors::yellow("Warning")
);
let initial_cwd_for_pkg = flags.initial_cwd.clone().unwrap_or_else(|| {
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
});
if let Some(pkg_json_path) =
closest_package_json(&initial_cwd_for_pkg, &compile_flags.source_file)
{
let display = pkg_json_path.display().to_string();
if !compile_flags.include.contains(&display) {
compile_flags.include.push(display);
}
}
let BundleForCompileResult {
path: bundle_path,
needs_npm_embed,
referenced_abs_paths,
extra_cleanup,
} = run_bundle_for_compile(&flags, &compile_flags)
.boxed_local()
.await?;
flags.internal.compile_bundle_embed_node_modules = needs_npm_embed;
for path in &referenced_abs_paths {
let in_node_modules =
path.components().any(|c| c.as_os_str() == "node_modules");
if !in_node_modules {
let included = path.display().to_string();
if !compile_flags.include.contains(&included) {
compile_flags.include.push(included);
}
}
}
flags.internal.compile_bundle_referenced_paths = referenced_abs_paths;
compile_flags.source_file = bundle_path.to_string_lossy().into_owned();
for worker_path in &extra_cleanup {
compile_flags
.include
.push(worker_path.display().to_string());
}
flags.subcommand = DenoSubcommand::Compile(compile_flags.clone());
let mut cleanup = vec![bundle_path];
cleanup.extend(extra_cleanup);
Some(CleanupGuard(cleanup))
} else {
None
};
let flags = Arc::new(flags);
if compile_flags.eszip {
compile_eszip(flags, compile_flags).boxed_local().await
} else {
compile_binary(flags, compile_flags).boxed_local().await
}
}
struct BundleForCompileResult {
path: PathBuf,
needs_npm_embed: bool,
referenced_abs_paths: Vec<PathBuf>,
extra_cleanup: Vec<PathBuf>,
}
async fn run_bundle_for_compile(
flags: &Flags,
compile_flags: &CompileFlags,
) -> Result<BundleForCompileResult, AnyError> {
let bundle_flags = Arc::new(flags.clone());
let initial_cwd = flags.initial_cwd.clone().unwrap_or_else(|| {
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
});
let main_bytes = bundle_one_for_compile(
bundle_flags.clone(),
compile_flags.source_file.clone(),
compile_flags.minify,
)
.await?;
let main_rewrite = rewrite_absolute_bundle_paths(&main_bytes, &initial_cwd)?;
let mut needs_npm_embed = main_rewrite.rewrote_paths;
let mut all_referenced_paths: Vec<PathBuf> =
main_rewrite.referenced_abs_paths.clone();
let main_src = std::str::from_utf8(&main_rewrite.bytes)
.context("Bundle output is not valid UTF-8")?;
let worker_urls = discover_worker_urls(main_src, &initial_cwd);
let mut url_replacements: Vec<(String, String)> = Vec::new();
let mut extra_cleanup: Vec<PathBuf> = Vec::new();
for (worker_url, worker_abs) in &worker_urls {
let worker_bytes = bundle_one_for_compile(
bundle_flags.clone(),
worker_abs.display().to_string(),
compile_flags.minify,
)
.await?;
let worker_rewrite =
rewrite_absolute_bundle_paths(&worker_bytes, &initial_cwd)?;
needs_npm_embed |= worker_rewrite.rewrote_paths;
all_referenced_paths.extend(worker_rewrite.referenced_abs_paths.clone());
let worker_path = initial_cwd.join(format!(
".deno_compile_worker_{:08x}.mjs",
rand::thread_rng().r#gen::<u32>()
));
std::fs::write(&worker_path, &worker_rewrite.bytes).with_context(|| {
format!(
"Writing bundled worker entrypoint to '{}'",
worker_path.display()
)
})?;
let worker_file_name = worker_path
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
url_replacements
.push((worker_url.clone(), format!("./{worker_file_name}")));
extra_cleanup.push(worker_path);
}
let final_main_src = if url_replacements.is_empty() {
main_src.to_string()
} else {
rewrite_worker_urls(main_src, &url_replacements)
};
let bundle_path = initial_cwd.join(format!(
".deno_compile_bundle_{:08x}.mjs",
rand::thread_rng().r#gen::<u32>()
));
std::fs::write(&bundle_path, final_main_src.as_bytes()).with_context(
|| format!("Writing bundled entrypoint to '{}'", bundle_path.display()),
)?;
Ok(BundleForCompileResult {
path: bundle_path,
needs_npm_embed,
referenced_abs_paths: all_referenced_paths,
extra_cleanup,
})
}
async fn bundle_one_for_compile(
flags: Arc<Flags>,
entrypoint: String,
minify: bool,
) -> Result<Vec<u8>, AnyError> {
let external = vec!["*.node".to_string()];
super::bundle::bundle_for_compile(flags, entrypoint, external, minify)
.boxed_local()
.await
}
fn discover_worker_urls(
bundle_src: &str,
initial_cwd: &Path,
) -> Vec<(String, PathBuf)> {
let call_pattern = lazy_regex::regex!(
r#"new\s+URL\s*\(\s*(.+?)\s*,\s*import\.meta\.url\s*\)"#
);
let url_string_pattern =
lazy_regex::regex!(r#""([^"\n]+\.(?:ts|tsx|js|jsx|mjs|cjs))""#);
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for call_caps in call_pattern.captures_iter(bundle_src) {
let first_arg = call_caps.get(1).unwrap().as_str();
for url_caps in url_string_pattern.captures_iter(first_arg) {
let url = url_caps.get(1).unwrap().as_str().to_string();
if !seen.insert(url.clone()) {
continue;
}
if let Some(path) = resolve_worker_url_target(&url, initial_cwd) {
out.push((url, path));
}
}
}
out
}
fn resolve_worker_url_target(url: &str, initial_cwd: &Path) -> Option<PathBuf> {
let candidate = if Path::new(url).is_absolute() {
PathBuf::from(url)
} else {
initial_cwd.join(url)
};
if candidate.is_file() {
return Some(candidate);
}
let basename = Path::new(url).file_name()?;
find_file_by_name(initial_cwd, basename)
}
fn find_file_by_name(root: &Path, name: &std::ffi::OsStr) -> Option<PathBuf> {
let mut pending = std::collections::VecDeque::from([root.to_path_buf()]);
while let Some(dir) = pending.pop_front() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
let file_name = path.file_name();
if path.is_dir() {
if matches!(
file_name,
Some(n) if n == std::ffi::OsStr::new("node_modules")
|| n == std::ffi::OsStr::new(".git")
|| n == std::ffi::OsStr::new("target")
) {
continue;
}
pending.push_back(path);
} else if file_name == Some(name) {
return Some(path);
}
}
}
None
}
fn closest_package_json(
initial_cwd: &Path,
source_file: &str,
) -> Option<PathBuf> {
let source_path = if Path::new(source_file).is_absolute() {
PathBuf::from(source_file)
} else {
initial_cwd.join(source_file)
};
let mut dir = source_path.parent()?.to_path_buf();
loop {
let candidate = dir.join("package.json");
if candidate.is_file() {
return Some(candidate);
}
if !dir.pop() {
return None;
}
}
}
fn rewrite_worker_urls(
bundle_src: &str,
replacements: &[(String, String)],
) -> String {
let pattern = lazy_regex::regex!(
r#"(new\s+URL\s*\(\s*)(.+?)(\s*,\s*import\.meta\.url\s*\))"#
);
pattern
.replace_all(bundle_src, |caps: ®ex::Captures<'_>| {
let prefix = &caps[1];
let mut arg = caps[2].to_string();
let suffix = &caps[3];
for (orig, replacement) in replacements {
let needle = format!("\"{orig}\"");
let with = format!("\"{replacement}\"");
arg = arg.replace(&needle, &with);
}
format!("{prefix}{arg}{suffix}")
})
.into_owned()
}
struct RewriteResult {
bytes: Vec<u8>,
rewrote_paths: bool,
referenced_abs_paths: Vec<PathBuf>,
}
fn rewrite_absolute_bundle_paths(
bundle_bytes: &[u8],
bundle_dir: &Path,
) -> Result<RewriteResult, AnyError> {
let src = std::str::from_utf8(bundle_bytes)
.context("Bundle output is not valid UTF-8")?;
Ok(rewrite_absolute_bundle_paths_inner(src, bundle_dir, |p| {
p.exists()
}))
}
fn rewrite_absolute_bundle_paths_inner(
src: &str,
bundle_dir: &Path,
path_exists: impl Fn(&Path) -> bool,
) -> RewriteResult {
let pattern = lazy_regex::regex!(
r#""((?:[A-Za-z]:)?[\\/][^"\n]+\.(?:js|cjs|mjs|json))""#
);
let mut any_rewrite = false;
let mut referenced_abs_paths: Vec<PathBuf> = Vec::new();
let rewritten = pattern.replace_all(src, |caps: ®ex::Captures<'_>| {
let abs = caps.get(1).unwrap().as_str().replace("\\\\", "\\");
let path = Path::new(&abs);
if !path_exists(path) {
return caps[0].to_string();
}
let Some(rel) = pathdiff::diff_paths(path, bundle_dir) else {
return caps[0].to_string();
};
any_rewrite = true;
referenced_abs_paths.push(path.to_path_buf());
let rel_str: String = rel.to_string_lossy().replace('\\', "/");
format!("__internalResolveBundlePath({:?})", rel_str.as_str())
});
if !any_rewrite {
return RewriteResult {
bytes: src.as_bytes().to_vec(),
rewrote_paths: false,
referenced_abs_paths,
};
}
let prefix = r#"// Injected by deno compile --bundle: resolve absolute paths emitted by
// esbuild's CJS-from-ESM wrapper against the bundle file's runtime
// location instead of the build-time absolute path.
import { fileURLToPath as __internalFileURLToPath } from "node:url";
import * as __internalPath from "node:path";
const __internalBundleDir = __internalPath.dirname(__internalFileURLToPath(import.meta.url));
function __internalResolveBundlePath(rel) {
return __internalPath.join(__internalBundleDir, rel);
}
"#;
RewriteResult {
bytes: format!("{prefix}{rewritten}").into_bytes(),
rewrote_paths: true,
referenced_abs_paths,
}
}
async fn compile_binary(
flags: Arc<Flags>,
compile_flags: CompileFlags,
) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags);
let cli_options = factory.cli_options()?;
let module_graph_creator = factory.module_graph_creator().await?;
let binary_writer = factory.create_compile_binary_writer().await?;
let entrypoint = cli_options.resolve_main_module()?;
let bin_name_resolver = factory.bin_name_resolver()?;
let output_path = resolve_compile_executable_output_path(
&bin_name_resolver,
&compile_flags,
cli_options.initial_cwd(),
)
.await?;
let compile_config = cli_options.start_dir.to_compile_config()?;
let mut effective_include = compile_config.include.clone();
for inc in &compile_flags.include {
if !effective_include.contains(inc) {
effective_include.push(inc.clone());
}
}
let mut effective_exclude = compile_config.exclude.clone();
for exc in &compile_flags.exclude {
if !effective_exclude.contains(exc) {
effective_exclude.push(exc.clone());
}
}
let roots = get_module_roots_and_include_paths(
entrypoint,
&effective_include,
&effective_exclude,
cli_options,
)?;
let graph =
build_compile_graph(module_graph_creator, cli_options, &roots).await?;
let initial_cwd =
deno_path_util::url_from_directory_path(cli_options.initial_cwd())?;
log::info!(
"{} {} to {}",
colors::green("Compile"),
crate::util::path::relative_specifier_path_for_display(
&initial_cwd,
entrypoint
),
{
if let Ok(output_path) = deno_path_util::url_from_file_path(&output_path)
{
crate::util::path::relative_specifier_path_for_display(
&initial_cwd,
&output_path,
)
} else {
output_path.display().to_string()
}
}
);
validate_output_path(&output_path)?;
let mut temp_filename = output_path.file_name().unwrap().to_owned();
temp_filename.push(format!(
".tmp-{}",
faster_hex::hex_encode(
&rand::thread_rng().r#gen::<[u8; 8]>(),
&mut [0u8; 16]
)
.unwrap()
));
let temp_path = output_path.with_file_name(temp_filename);
let file = std::fs::File::create(&temp_path).with_context(|| {
format!("Opening temporary file '{}'", temp_path.display())
})?;
let write_result = binary_writer
.write_bin(WriteBinOptions {
writer: file,
display_output_filename: &output_path
.file_name()
.unwrap()
.to_string_lossy(),
graph: &graph,
entrypoint,
include_paths: &roots.include_paths,
exclude_paths: effective_exclude
.iter()
.map(|p| cli_options.initial_cwd().join(p))
.chain(std::iter::once(
cli_options.initial_cwd().join(&output_path),
))
.chain(std::iter::once(cli_options.initial_cwd().join(&temp_path)))
.collect(),
compile_flags: &compile_flags,
})
.await
.with_context(|| {
format!(
"Writing deno compile executable to temporary file '{}'",
temp_path.display()
)
});
#[cfg(unix)]
let write_result = write_result.and_then(|_| {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(&temp_path, perms).with_context(|| {
format!(
"Setting permissions on temporary file '{}'",
temp_path.display()
)
})
});
let write_result = write_result.and_then(|_| {
std::fs::rename(&temp_path, &output_path).with_context(|| {
format!(
"Renaming temporary file '{}' to '{}'",
temp_path.display(),
output_path.display()
)
})
});
if let Err(err) = write_result {
let _ = std::fs::remove_file(temp_path);
return Err(err);
}
Ok(())
}
async fn compile_eszip(
flags: Arc<Flags>,
compile_flags: CompileFlags,
) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags);
let cli_options = factory.cli_options()?;
let module_graph_creator = factory.module_graph_creator().await?;
let parsed_source_cache = factory.parsed_source_cache()?;
let compiler_options_resolver = factory.compiler_options_resolver()?;
let bin_name_resolver = factory.bin_name_resolver()?;
let entrypoint = cli_options.resolve_main_module()?;
let mut output_path = resolve_compile_executable_output_path(
&bin_name_resolver,
&compile_flags,
cli_options.initial_cwd(),
)
.await?;
output_path.set_extension("eszip");
let maybe_import_map_specifier =
cli_options.resolve_specified_import_map_specifier()?;
let compile_config = cli_options.start_dir.to_compile_config()?;
let mut effective_include = compile_config.include.clone();
for inc in &compile_flags.include {
if !effective_include.contains(inc) {
effective_include.push(inc.clone());
}
}
let mut effective_exclude = compile_config.exclude.clone();
for exc in &compile_flags.exclude {
if !effective_exclude.contains(exc) {
effective_exclude.push(exc.clone());
}
}
let roots = get_module_roots_and_include_paths(
entrypoint,
&effective_include,
&effective_exclude,
cli_options,
)?;
let graph =
build_compile_graph(module_graph_creator, cli_options, &roots).await?;
let transpile_and_emit_options = compiler_options_resolver
.for_specifier(cli_options.workspace().root_dir_url())
.transpile_options()?;
let transpile_options = transpile_and_emit_options.transpile.clone();
let emit_options = transpile_and_emit_options.emit.clone();
let parser = parsed_source_cache.as_capturing_parser();
let root_dir_url = cli_options.workspace().root_dir_url();
log::debug!("Binary root dir: {}", root_dir_url);
let relative_file_base = eszip::EszipRelativeFileBaseUrl::new(root_dir_url);
let mut eszip = eszip::EszipV2::from_graph(eszip::FromGraphOptions {
graph,
parser,
transpile_options,
emit_options,
relative_file_base: Some(relative_file_base),
npm_packages: None,
module_kind_resolver: Default::default(),
npm_snapshot: Default::default(),
})?;
if let Some(import_map_specifier) = maybe_import_map_specifier {
let import_map_path = import_map_specifier.to_file_path().unwrap();
let import_map_content = std::fs::read_to_string(&import_map_path)
.with_context(|| {
format!("Failed to read import map: {:?}", import_map_path)
})?;
let import_map_specifier_str = if let Some(relative_import_map_specifier) =
root_dir_url.make_relative(&import_map_specifier)
{
relative_import_map_specifier
} else {
import_map_specifier.to_string()
};
eszip.add_import_map(
eszip::ModuleKind::Json,
import_map_specifier_str,
import_map_content.as_bytes().to_vec().into(),
);
}
log::info!(
"{} {} to {}",
colors::green("Compile"),
entrypoint,
output_path.display(),
);
validate_output_path(&output_path)?;
let mut file = std::fs::File::create(&output_path).with_context(|| {
format!("Opening ESZip file '{}'", output_path.display())
})?;
let write_result = {
let r = file.write_all(&eszip.into_bytes());
drop(file);
r
};
if let Err(err) = write_result {
let _ = std::fs::remove_file(output_path);
return Err(err.into());
}
Ok(())
}
fn validate_output_path(output_path: &Path) -> Result<(), AnyError> {
if output_path.exists() {
if output_path.is_dir() {
bail!(
concat!(
"Could not compile to file '{}' because a directory exists with ",
"the same name. You can use the `--output <file-path>` flag to ",
"provide an alternative name."
),
output_path.display()
);
}
if !is_standalone_binary(output_path) {
bail!(
concat!(
"Could not compile to file '{}' because the file already exists ",
"and cannot be overwritten. Please delete the existing file or ",
"use the `--output <file-path>` flag to provide an alternative name."
),
output_path.display()
);
}
std::fs::remove_file(output_path)?;
} else {
let output_base = &output_path.parent().unwrap();
if output_base.exists() && output_base.is_file() {
bail!(
concat!(
"Could not compile to file '{}' because its parent directory ",
"is an existing file. You can use the `--output <file-path>` flag to ",
"provide an alternative name.",
),
output_base.display(),
);
}
std::fs::create_dir_all(output_base)?;
}
Ok(())
}
struct CompileModuleRoots {
strict: Vec<ModuleSpecifier>,
include: Vec<ModuleSpecifier>,
include_paths: Vec<ModuleSpecifier>,
}
async fn build_compile_graph(
module_graph_creator: &ModuleGraphCreator,
cli_options: &CliOptions,
roots: &CompileModuleRoots,
) -> Result<ModuleGraph, AnyError> {
let checked_graph = module_graph_creator
.create_graph_and_maybe_check(roots.strict.clone())
.await?;
if roots.include.is_empty() && !cli_options.type_check_mode().is_true() {
Ok(Arc::try_unwrap(checked_graph).unwrap())
} else {
let mut all_roots = roots.strict.clone();
all_roots.extend(roots.include.iter().cloned());
module_graph_creator
.create_graph(GraphKind::CodeOnly, all_roots, NpmCachingStrategy::Eager)
.await
}
}
fn get_module_roots_and_include_paths(
entrypoint: &ModuleSpecifier,
include: &[String],
exclude: &[String],
cli_options: &Arc<CliOptions>,
) -> Result<CompileModuleRoots, AnyError> {
let initial_cwd = cli_options.initial_cwd();
fn is_module_graph_module(url: &ModuleSpecifier) -> bool {
if url.scheme() != "file" {
return true;
}
is_module_graph_media_type(MediaType::from_specifier(url))
}
fn is_module_graph_media_type(media_type: MediaType) -> bool {
match media_type {
MediaType::JavaScript
| MediaType::Jsx
| MediaType::Mjs
| MediaType::Cjs
| MediaType::TypeScript
| MediaType::Mts
| MediaType::Cts
| MediaType::Dts
| MediaType::Dmts
| MediaType::Dcts
| MediaType::Tsx
| MediaType::Json
| MediaType::Wasm => true,
MediaType::Css
| MediaType::Html
| MediaType::Jsonc
| MediaType::Json5
| MediaType::Markdown
| MediaType::SourceMap
| MediaType::Sql
| MediaType::Unknown => false,
}
}
fn analyze_path(
url: &ModuleSpecifier,
excluded_paths: &HashSet<PathBuf>,
searched_paths: &mut HashSet<PathBuf>,
mut add_path: impl FnMut(&Path),
) -> Result<(), AnyError> {
let Ok(path) = url_to_file_path(url) else {
return Ok(());
};
let mut pending = VecDeque::from([path]);
while let Some(path) = pending.pop_front() {
if !searched_paths.insert(path.clone()) {
continue;
}
if excluded_paths.contains(&path) {
continue;
}
if !path.is_dir() {
add_path(&path);
continue;
}
for entry in std::fs::read_dir(&path).with_context(|| {
format!("Failed reading directory '{}'", path.display())
})? {
let entry = entry.with_context(|| {
format!("Failed reading entry in directory '{}'", path.display())
})?;
pending.push_back(entry.path());
}
}
Ok(())
}
let mut searched_paths = HashSet::new();
let mut module_roots = Vec::new();
let mut include_module_roots = Vec::new();
let mut include_paths = Vec::new();
let exclude_set = exclude
.iter()
.map(|path| initial_cwd.join(path))
.collect::<HashSet<_>>();
module_roots.push(entrypoint.clone());
for side_module in include {
let url = resolve_url_or_path(side_module, initial_cwd)?;
if is_module_graph_module(&url) {
include_module_roots.push(url.clone());
} else {
analyze_path(&url, &exclude_set, &mut searched_paths, |file_path| {
let media_type = MediaType::from_path(file_path);
if is_module_graph_media_type(media_type)
&& let Ok(file_url) = url_from_file_path(file_path)
{
include_module_roots.push(file_url);
}
})?;
}
if url.scheme() == "file" {
include_paths.push(url);
}
}
for preload_module in cli_options.preload_modules()? {
module_roots.push(preload_module);
}
for require_module in cli_options.require_modules()? {
module_roots.push(require_module);
}
Ok(CompileModuleRoots {
strict: module_roots,
include: include_module_roots,
include_paths,
})
}
async fn resolve_compile_executable_output_path(
bin_name_resolver: &BinNameResolver<'_>,
compile_flags: &CompileFlags,
current_dir: &Path,
) -> Result<PathBuf, AnyError> {
let module_specifier =
resolve_url_or_path(&compile_flags.source_file, current_dir)?;
let output_flag = compile_flags.output.clone();
let mut output_path = if let Some(out) = output_flag.as_ref() {
let mut out_path = PathBuf::from(out);
if out.ends_with('/') || out.ends_with('\\') {
if let Some(infer_file_name) = bin_name_resolver
.infer_name_from_url(&module_specifier)
.await
.map(PathBuf::from)
{
out_path = out_path.join(infer_file_name);
}
} else {
out_path = out_path.to_path_buf();
}
Some(out_path)
} else {
None
};
if output_flag.is_none() {
output_path = bin_name_resolver
.infer_name_from_url(&module_specifier)
.await
.map(PathBuf::from)
}
output_path.ok_or_else(|| anyhow!(
"An executable name was not provided. One could not be inferred from the URL. Aborting.",
)).map(|output_path| {
get_os_specific_filepath(output_path, &compile_flags.target)
})
}
fn get_os_specific_filepath(
output: PathBuf,
target: &Option<String>,
) -> PathBuf {
let is_windows = match target {
Some(target) => target.contains("windows"),
None => cfg!(windows),
};
if is_windows && output.extension().unwrap_or_default() != "exe" {
if let Some(ext) = output.extension() {
output.with_extension(format!("{}.exe", ext.to_string_lossy()))
} else {
output.with_extension("exe")
}
} else {
output
}
}
#[cfg(test)]
mod test {
use deno_npm::registry::TestNpmRegistryApi;
use deno_npm::resolution::NpmVersionResolver;
pub use super::*;
use crate::http_util::HttpClientProvider;
use crate::util::env::resolve_cwd;
#[tokio::test]
async fn resolve_compile_executable_output_path_target_linux() {
let http_client = HttpClientProvider::new(None, None);
let npm_api = TestNpmRegistryApi::default();
let npm_version_resolver = NpmVersionResolver::default();
let bin_name_resolver =
BinNameResolver::new(&http_client, &npm_api, &npm_version_resolver);
let path = resolve_compile_executable_output_path(
&bin_name_resolver,
&CompileFlags {
source_file: "mod.ts".to_string(),
output: Some(String::from("./file")),
args: Vec::new(),
target: Some("x86_64-unknown-linux-gnu".to_string()),
no_terminal: false,
icon: None,
include: Default::default(),
exclude: Default::default(),
eszip: true,
self_extracting: false,
bundle: false,
minify: false,
},
&resolve_cwd(None).unwrap(),
)
.await
.unwrap();
assert_eq!(path.file_name().unwrap(), "file");
}
#[tokio::test]
async fn resolve_compile_executable_output_path_target_windows() {
let http_client = HttpClientProvider::new(None, None);
let npm_api = TestNpmRegistryApi::default();
let npm_version_resolver = NpmVersionResolver::default();
let bin_name_resolver =
BinNameResolver::new(&http_client, &npm_api, &npm_version_resolver);
let path = resolve_compile_executable_output_path(
&bin_name_resolver,
&CompileFlags {
source_file: "mod.ts".to_string(),
output: Some(String::from("./file")),
args: Vec::new(),
target: Some("x86_64-pc-windows-msvc".to_string()),
include: Default::default(),
exclude: Default::default(),
icon: None,
no_terminal: false,
eszip: true,
self_extracting: false,
bundle: false,
minify: false,
},
&resolve_cwd(None).unwrap(),
)
.await
.unwrap();
assert_eq!(path.file_name().unwrap(), "file.exe");
}
#[test]
fn test_os_specific_file_path() {
fn run_test(path: &str, target: Option<&str>, expected: &str) {
assert_eq!(
get_os_specific_filepath(
PathBuf::from(path),
&target.map(|s| s.to_string())
),
PathBuf::from(expected)
);
}
if cfg!(windows) {
run_test("C:\\my-exe", None, "C:\\my-exe.exe");
run_test("C:\\my-exe.exe", None, "C:\\my-exe.exe");
run_test("C:\\my-exe-0.1.2", None, "C:\\my-exe-0.1.2.exe");
} else {
run_test("my-exe", Some("linux"), "my-exe");
run_test("my-exe-0.1.2", Some("linux"), "my-exe-0.1.2");
}
run_test("C:\\my-exe", Some("windows"), "C:\\my-exe.exe");
run_test("C:\\my-exe.exe", Some("windows"), "C:\\my-exe.exe");
run_test("C:\\my-exe.0.1.2", Some("windows"), "C:\\my-exe.0.1.2.exe");
run_test("my-exe-0.1.2", Some("linux"), "my-exe-0.1.2");
}
#[test]
fn test_rewrite_absolute_bundle_paths_native() {
let bundle_dir = if cfg!(windows) {
PathBuf::from("C:\\proj\\dist")
} else {
PathBuf::from("/proj/dist")
};
let abs = bundle_dir.join("pkg").join("index.js");
let abs_in_js = abs.to_string_lossy().replace('\\', "\\\\");
let src = format!("var m = require(\"{abs_in_js}\");\n");
let result =
rewrite_absolute_bundle_paths_inner(&src, &bundle_dir, |_| true);
assert!(result.rewrote_paths);
let out = String::from_utf8(result.bytes).unwrap();
assert!(
out.contains(r#"__internalResolveBundlePath("pkg/index.js")"#),
"unexpected output: {out}"
);
}
#[test]
fn test_rewrite_absolute_bundle_paths_skips_missing() {
let bundle_dir = PathBuf::from("/proj/dist");
let src = "var m = require(\"/does/not/exist.js\");\n";
let result =
rewrite_absolute_bundle_paths_inner(src, &bundle_dir, |_| false);
assert!(!result.rewrote_paths);
assert_eq!(result.bytes.as_slice(), src.as_bytes());
}
#[test]
fn test_rewrite_absolute_bundle_paths_skips_relative_key() {
let bundle_dir = PathBuf::from("/proj/dist");
let src = "var b = { \"pkg/index.js\"(exports, module) { module.exports = 1; } };\n";
let result =
rewrite_absolute_bundle_paths_inner(src, &bundle_dir, |_| true);
assert!(!result.rewrote_paths);
assert_eq!(result.bytes.as_slice(), src.as_bytes());
}
}