use crate::ast::Location;
use crate::deno_dir::DenoDir;
use crate::disk_cache::DiskCache;
use crate::file_fetcher::SourceFileFetcher;
use crate::media_type::MediaType;
use crate::permissions::Permissions;
use crate::program_state::ProgramState;
use deno_core::error::AnyError;
use deno_core::futures::Future;
use deno_core::futures::FutureExt;
use deno_core::serde_json;
use deno_core::ModuleSpecifier;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::env;
use std::fmt;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
pub type DependencyMap = HashMap<String, Dependency>;
pub type FetchFuture =
Pin<Box<(dyn Future<Output = Result<CachedModule, AnyError>> + 'static)>>;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum HandlerError {
FetchErrorWithLocation(String, Location),
}
impl fmt::Display for HandlerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
HandlerError::FetchErrorWithLocation(ref err, ref location) => {
write!(f, "{}\n at {}", err, location)
}
}
}
}
impl std::error::Error for HandlerError {}
#[derive(Debug, Clone)]
pub struct CachedModule {
pub is_remote: bool,
pub maybe_dependencies: Option<DependencyMap>,
pub maybe_emit: Option<Emit>,
pub maybe_emit_path: Option<(PathBuf, Option<PathBuf>)>,
pub maybe_types: Option<String>,
pub maybe_version: Option<String>,
pub media_type: MediaType,
pub requested_specifier: ModuleSpecifier,
pub source: String,
pub source_path: PathBuf,
pub specifier: ModuleSpecifier,
}
#[cfg(test)]
impl Default for CachedModule {
fn default() -> Self {
let specifier = ModuleSpecifier::resolve_url("file:///example.js").unwrap();
CachedModule {
is_remote: false,
maybe_dependencies: None,
maybe_emit: None,
maybe_emit_path: None,
maybe_types: None,
maybe_version: None,
media_type: MediaType::Unknown,
requested_specifier: specifier.clone(),
source: "".to_string(),
source_path: PathBuf::new(),
specifier,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Emit {
Cli((String, Option<String>)),
}
impl Default for Emit {
fn default() -> Self {
Emit::Cli(("".to_string(), None))
}
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub is_dynamic: bool,
pub location: Location,
pub maybe_code: Option<ModuleSpecifier>,
pub maybe_type: Option<ModuleSpecifier>,
}
impl Dependency {
pub fn new(location: Location) -> Self {
Dependency {
is_dynamic: false,
location,
maybe_code: None,
maybe_type: None,
}
}
}
pub trait SpecifierHandler {
fn fetch(
&mut self,
specifier: ModuleSpecifier,
maybe_location: Option<Location>,
is_dynamic: bool,
) -> FetchFuture;
fn get_tsbuildinfo(
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<String>, AnyError>;
fn set_cache(
&mut self,
specifier: &ModuleSpecifier,
emit: &Emit,
) -> Result<(), AnyError>;
fn set_types(
&mut self,
specifier: &ModuleSpecifier,
types: String,
) -> Result<(), AnyError>;
fn set_tsbuildinfo(
&mut self,
specifier: &ModuleSpecifier,
tsbuildinfo: String,
) -> Result<(), AnyError>;
fn set_deps(
&mut self,
specifier: &ModuleSpecifier,
dependencies: DependencyMap,
) -> Result<(), AnyError>;
fn set_version(
&mut self,
specifier: &ModuleSpecifier,
version: String,
) -> Result<(), AnyError>;
}
impl fmt::Debug for dyn SpecifierHandler {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SpecifierHandler {{ }}")
}
}
#[derive(Deserialize, Serialize)]
pub struct CompiledFileMetadata {
pub version_hash: String,
}
impl CompiledFileMetadata {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, AnyError> {
let metadata_string = std::str::from_utf8(bytes)?;
serde_json::from_str::<Self>(metadata_string).map_err(|e| e.into())
}
pub fn to_json_string(&self) -> Result<String, AnyError> {
serde_json::to_string(self).map_err(|e| e.into())
}
}
pub struct FetchHandler {
disk_cache: DiskCache,
runtime_permissions: Permissions,
file_fetcher: SourceFileFetcher,
}
impl FetchHandler {
pub fn new(
program_state: &Arc<ProgramState>,
runtime_permissions: Permissions,
) -> Result<Self, AnyError> {
let custom_root = env::var("DENO_DIR").map(String::into).ok();
let deno_dir = DenoDir::new(custom_root)?;
let disk_cache = deno_dir.gen_cache;
let file_fetcher = program_state.file_fetcher.clone();
Ok(FetchHandler {
disk_cache,
runtime_permissions,
file_fetcher,
})
}
}
impl SpecifierHandler for FetchHandler {
fn fetch(
&mut self,
requested_specifier: ModuleSpecifier,
maybe_location: Option<Location>,
is_dynamic: bool,
) -> FetchFuture {
let permissions = if is_dynamic {
self.runtime_permissions.clone()
} else {
Permissions::allow_all()
};
let file_fetcher = self.file_fetcher.clone();
let disk_cache = self.disk_cache.clone();
let maybe_referrer: Option<ModuleSpecifier> =
if let Some(location) = &maybe_location {
Some(location.clone().into())
} else {
None
};
async move {
let source_file = file_fetcher
.fetch_source_file(&requested_specifier, maybe_referrer, permissions)
.await
.map_err(|err| {
if let Some(location) = maybe_location {
if !is_dynamic {
HandlerError::FetchErrorWithLocation(err.to_string(), location)
.into()
} else {
err
}
} else {
err
}
})?;
let url = source_file.url.clone();
let is_remote = url.scheme() != "file";
let filename = disk_cache.get_cache_filename_with_extension(&url, "meta");
let maybe_version = if let Ok(bytes) = disk_cache.get(&filename) {
if let Ok(compiled_file_metadata) =
CompiledFileMetadata::from_bytes(&bytes)
{
Some(compiled_file_metadata.version_hash)
} else {
None
}
} else {
None
};
let mut maybe_map_path = None;
let map_path =
disk_cache.get_cache_filename_with_extension(&url, "js.map");
let maybe_map = if let Ok(map) = disk_cache.get(&map_path) {
maybe_map_path = Some(disk_cache.location.join(map_path));
Some(String::from_utf8(map)?)
} else {
None
};
let mut maybe_emit = None;
let mut maybe_emit_path = None;
let emit_path = disk_cache.get_cache_filename_with_extension(&url, "js");
if let Ok(code) = disk_cache.get(&emit_path) {
maybe_emit = Some(Emit::Cli((String::from_utf8(code)?, maybe_map)));
maybe_emit_path =
Some((disk_cache.location.join(emit_path), maybe_map_path));
};
let specifier = ModuleSpecifier::from(url);
Ok(CachedModule {
is_remote,
maybe_dependencies: None,
maybe_emit,
maybe_emit_path,
maybe_types: source_file.types_header,
maybe_version,
media_type: source_file.media_type,
requested_specifier,
source: source_file.source_code,
source_path: source_file.filename,
specifier,
})
}
.boxed_local()
}
fn get_tsbuildinfo(
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<String>, AnyError> {
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_url(), "buildinfo");
if let Ok(tsbuildinfo) = self.disk_cache.get(&filename) {
Ok(Some(String::from_utf8(tsbuildinfo)?))
} else {
Ok(None)
}
}
fn set_tsbuildinfo(
&mut self,
specifier: &ModuleSpecifier,
tsbuildinfo: String,
) -> Result<(), AnyError> {
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_url(), "buildinfo");
debug!("set_tsbuildinfo - filename {:?}", filename);
self
.disk_cache
.set(&filename, tsbuildinfo.as_bytes())
.map_err(|e| e.into())
}
fn set_cache(
&mut self,
specifier: &ModuleSpecifier,
emit: &Emit,
) -> Result<(), AnyError> {
match emit {
Emit::Cli((code, maybe_map)) => {
let url = specifier.as_url();
let filename =
self.disk_cache.get_cache_filename_with_extension(url, "js");
self.disk_cache.set(&filename, code.as_bytes())?;
if let Some(map) = maybe_map {
let filename = self
.disk_cache
.get_cache_filename_with_extension(url, "js.map");
self.disk_cache.set(&filename, map.as_bytes())?;
}
}
};
Ok(())
}
fn set_deps(
&mut self,
_specifier: &ModuleSpecifier,
_dependencies: DependencyMap,
) -> Result<(), AnyError> {
Ok(())
}
fn set_types(
&mut self,
_specifier: &ModuleSpecifier,
_types: String,
) -> Result<(), AnyError> {
Ok(())
}
fn set_version(
&mut self,
specifier: &ModuleSpecifier,
version_hash: String,
) -> Result<(), AnyError> {
let compiled_file_metadata = CompiledFileMetadata { version_hash };
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_url(), "meta");
self
.disk_cache
.set(
&filename,
compiled_file_metadata.to_json_string()?.as_bytes(),
)
.map_err(|e| e.into())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::http_cache::HttpCache;
use tempfile::TempDir;
fn setup() -> (TempDir, FetchHandler) {
let temp_dir = TempDir::new().expect("could not setup");
let deno_dir = DenoDir::new(Some(temp_dir.path().to_path_buf()))
.expect("could not setup");
let file_fetcher = SourceFileFetcher::new(
HttpCache::new(&temp_dir.path().to_path_buf().join("deps")),
true,
Vec::new(),
false,
false,
None,
)
.expect("could not setup");
let disk_cache = deno_dir.gen_cache;
let fetch_handler = FetchHandler {
disk_cache,
runtime_permissions: Permissions::default(),
file_fetcher,
};
(temp_dir, fetch_handler)
}
#[tokio::test]
async fn test_fetch_handler_fetch() {
let _http_server_guard = test_util::http_server();
let (_, mut file_fetcher) = setup();
let specifier = ModuleSpecifier::resolve_url_or_path(
"http://localhost:4545/cli/tests/subdir/mod2.ts",
)
.unwrap();
let cached_module: CachedModule = file_fetcher
.fetch(specifier.clone(), None, false)
.await
.unwrap();
assert!(cached_module.maybe_emit.is_none());
assert!(cached_module.maybe_dependencies.is_none());
assert_eq!(cached_module.media_type, MediaType::TypeScript);
assert_eq!(
cached_module.source,
"export { printHello } from \"./print_hello.ts\";\n"
);
assert_eq!(cached_module.specifier, specifier);
}
#[tokio::test]
async fn test_fetch_handler_set_cache() {
let _http_server_guard = test_util::http_server();
let (_, mut file_fetcher) = setup();
let specifier = ModuleSpecifier::resolve_url_or_path(
"http://localhost:4545/cli/tests/subdir/mod2.ts",
)
.unwrap();
let cached_module: CachedModule = file_fetcher
.fetch(specifier.clone(), None, false)
.await
.unwrap();
assert!(cached_module.maybe_emit.is_none());
let code = String::from("some code");
file_fetcher
.set_cache(&specifier, &Emit::Cli((code, None)))
.expect("could not set cache");
let cached_module: CachedModule = file_fetcher
.fetch(specifier.clone(), None, false)
.await
.unwrap();
assert_eq!(
cached_module.maybe_emit,
Some(Emit::Cli(("some code".to_string(), None)))
);
}
#[tokio::test]
async fn test_fetch_handler_is_remote() {
let _http_server_guard = test_util::http_server();
let (_, mut file_fetcher) = setup();
let specifier = ModuleSpecifier::resolve_url_or_path(
"http://localhost:4545/cli/tests/subdir/mod2.ts",
)
.unwrap();
let cached_module: CachedModule =
file_fetcher.fetch(specifier, None, false).await.unwrap();
assert_eq!(cached_module.is_remote, true);
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let specifier = ModuleSpecifier::resolve_url_or_path(
c.join("tests/subdir/mod1.ts").as_os_str().to_str().unwrap(),
)
.unwrap();
let cached_module: CachedModule =
file_fetcher.fetch(specifier, None, false).await.unwrap();
assert_eq!(cached_module.is_remote, false);
}
}