1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-26 16:09:27 -05:00
denoland-deno/cli/specifier_handler.rs

585 lines
17 KiB
Rust

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::deno_dir::DenoDir;
use crate::disk_cache::DiskCache;
use crate::file_fetcher::SourceFileFetcher;
use crate::file_fetcher::TextDocument;
use crate::flags::Flags;
use crate::http_cache::HttpCache;
use crate::media_type::MediaType;
use crate::permissions::Permissions;
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::error::Error;
use std::fmt;
use std::pin::Pin;
use std::result;
type Result<V> = result::Result<V, AnyError>;
pub type DependencyMap = HashMap<String, Dependency>;
pub type EmitMap = HashMap<EmitType, (TextDocument, Option<TextDocument>)>;
pub type FetchFuture =
Pin<Box<(dyn Future<Output = Result<CachedModule>> + 'static)>>;
#[derive(Debug, Clone)]
pub struct CachedModule {
pub emits: EmitMap,
pub maybe_dependencies: Option<DependencyMap>,
pub maybe_types: Option<String>,
pub maybe_version: Option<String>,
pub media_type: MediaType,
pub source: TextDocument,
pub specifier: ModuleSpecifier,
}
#[cfg(test)]
impl Default for CachedModule {
fn default() -> Self {
CachedModule {
emits: HashMap::new(),
maybe_dependencies: None,
maybe_types: None,
maybe_version: None,
media_type: MediaType::Unknown,
source: TextDocument::new(Vec::new(), Option::<&str>::None),
specifier: ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts")
.unwrap(),
}
}
}
/// An enum that represents the different types of emitted code that can be
/// cached. Different types can utilise different configurations which can
/// change the validity of the emitted code.
#[allow(unused)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum EmitType {
/// Code that was emitted for use by the CLI
Cli,
/// Code that was emitted for bundling purposes
Bundle,
/// Code that was emitted based on a request to the runtime APIs
Runtime,
}
impl Default for EmitType {
fn default() -> Self {
EmitType::Cli
}
}
#[derive(Debug, Clone, Default)]
pub struct Dependency {
/// The module specifier that resolves to the runtime code dependency for the
/// module.
pub maybe_code: Option<ModuleSpecifier>,
/// The module specifier that resolves to the type only dependency for the
/// module.
pub maybe_type: Option<ModuleSpecifier>,
}
pub trait SpecifierHandler {
/// Instructs the handler to fetch a specifier or retrieve its value from the
/// cache if there is a valid cached version.
fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture;
/// Get the optional build info from the cache for a given module specifier.
/// Because build infos are only associated with the "root" modules, they are
/// not expected to be cached for each module, but are "lazily" checked when
/// a root module is identified. The `emit_type` also indicates what form
/// of the module the build info is valid for.
fn get_build_info(
&self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
) -> Result<Option<TextDocument>>;
/// Set the emitted code (and maybe map) for a given module specifier. The
/// cache type indicates what form the emit is related to.
fn set_cache(
&mut self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
code: TextDocument,
maybe_map: Option<TextDocument>,
) -> Result<()>;
/// When parsed out of a JavaScript module source, the triple slash reference
/// to the types should be stored in the cache.
fn set_types(
&mut self,
specifier: &ModuleSpecifier,
types: String,
) -> Result<()>;
/// Set the build info for a module specifier, also providing the cache type.
fn set_build_info(
&mut self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
build_info: TextDocument,
) -> Result<()>;
/// Set the graph dependencies for a given module specifier.
fn set_deps(
&mut self,
specifier: &ModuleSpecifier,
dependencies: DependencyMap,
) -> Result<()>;
/// Set the version of the source for a given module, which is used to help
/// determine if a module needs to be re-type-checked.
fn set_version(
&mut self,
specifier: &ModuleSpecifier,
version: String,
) -> Result<()>;
}
impl fmt::Debug for dyn SpecifierHandler {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SpecifierHandler {{ }}")
}
}
/// Errors that could be raised by a `SpecifierHandler` implementation.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum SpecifierHandlerError {
/// An error representing an error the `EmitType` that was supplied to a
/// method of an implementor of the `SpecifierHandler` trait.
UnsupportedEmitType(EmitType),
}
use SpecifierHandlerError::*;
impl fmt::Display for SpecifierHandlerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
UnsupportedEmitType(ref emit_type) => write!(
f,
"The emit type of \"{:?}\" is unsupported for this operation.",
emit_type
),
}
}
}
impl Error for SpecifierHandlerError {}
/// A representation of meta data for a compiled file.
///
/// *Note* this is currently just a copy of what is located in `tsc.rs` but will
/// be refactored to be able to store dependencies and type information in the
/// future.
#[derive(Deserialize, Serialize)]
pub struct CompiledFileMetadata {
pub version_hash: String,
}
impl CompiledFileMetadata {
pub fn from_json_string(metadata_string: &str) -> Result<Self> {
serde_json::from_str::<Self>(metadata_string).map_err(|e| e.into())
}
pub fn to_json_string(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| e.into())
}
}
/// An implementation of the `SpecifierHandler` trait that integrates with the
/// existing `file_fetcher` interface, which will eventually be refactored to
/// align it more to the `SpecifierHandler` trait.
pub struct FetchHandler {
disk_cache: DiskCache,
file_fetcher: SourceFileFetcher,
permissions: Permissions,
}
impl FetchHandler {
pub fn new(flags: &Flags, permissions: &Permissions) -> Result<Self> {
let custom_root = env::var("DENO_DIR").map(String::into).ok();
let deno_dir = DenoDir::new(custom_root)?;
let deps_cache_location = deno_dir.root.join("deps");
let http_cache = HttpCache::new(&deps_cache_location);
let ca_file = flags.ca_file.clone().or_else(|| env::var("DENO_CERT").ok());
let file_fetcher = SourceFileFetcher::new(
http_cache,
!flags.reload,
flags.cache_blocklist.clone(),
flags.no_remote,
flags.cached_only,
ca_file.as_deref(),
)?;
let disk_cache = deno_dir.gen_cache;
Ok(FetchHandler {
disk_cache,
file_fetcher,
permissions: permissions.clone(),
})
}
}
impl SpecifierHandler for FetchHandler {
fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture {
let permissions = self.permissions.clone();
let file_fetcher = self.file_fetcher.clone();
let disk_cache = self.disk_cache.clone();
async move {
let source_file = file_fetcher
.fetch_source_file(&specifier, None, permissions)
.await?;
let url = source_file.url;
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(metadata_string) = std::str::from_utf8(&bytes) {
if let Ok(compiled_file_metadata) =
CompiledFileMetadata::from_json_string(metadata_string)
{
Some(compiled_file_metadata.version_hash)
} else {
None
}
} else {
None
}
} else {
None
};
let filename =
disk_cache.get_cache_filename_with_extension(&url, "js.map");
let maybe_map: Option<TextDocument> =
if let Ok(map) = disk_cache.get(&filename) {
Some(map.into())
} else {
None
};
let mut emits = HashMap::new();
let filename = disk_cache.get_cache_filename_with_extension(&url, "js");
if let Ok(code) = disk_cache.get(&filename) {
emits.insert(EmitType::Cli, (code.into(), maybe_map));
};
Ok(CachedModule {
emits,
maybe_dependencies: None,
maybe_types: source_file.types_header,
maybe_version,
media_type: source_file.media_type,
source: source_file.source_code,
specifier,
})
}
.boxed_local()
}
fn get_build_info(
&self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
) -> Result<Option<TextDocument>> {
if emit_type != &EmitType::Cli {
return Err(UnsupportedEmitType(emit_type.clone()).into());
}
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_url(), "buildinfo");
if let Ok(build_info) = self.disk_cache.get(&filename) {
return Ok(Some(build_info.into()));
}
Ok(None)
}
fn set_build_info(
&mut self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
build_info: TextDocument,
) -> Result<()> {
if emit_type != &EmitType::Cli {
return Err(UnsupportedEmitType(emit_type.clone()).into());
}
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_url(), "buildinfo");
self
.disk_cache
.set(&filename, build_info.as_bytes())
.map_err(|e| e.into())
}
fn set_cache(
&mut self,
specifier: &ModuleSpecifier,
emit_type: &EmitType,
code: TextDocument,
maybe_map: Option<TextDocument>,
) -> Result<()> {
if emit_type != &EmitType::Cli {
return Err(UnsupportedEmitType(emit_type.clone()).into());
}
let filename = self
.disk_cache
.get_cache_filename_with_extension(specifier.as_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(specifier.as_url(), "js.map");
self.disk_cache.set(&filename, map.as_bytes())?;
}
Ok(())
}
fn set_deps(
&mut self,
_specifier: &ModuleSpecifier,
_dependencies: DependencyMap,
) -> Result<()> {
// file_fetcher doesn't have the concept of caching dependencies
Ok(())
}
fn set_types(
&mut self,
_specifier: &ModuleSpecifier,
_types: String,
) -> Result<()> {
// file_fetcher doesn't have the concept of caching of the types
Ok(())
}
fn set_version(
&mut self,
specifier: &ModuleSpecifier,
version_hash: String,
) -> Result<()> {
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 deno_core::futures::future;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
/// This is a testing mock for `SpecifierHandler` that uses a special file
/// system renaming to mock local and remote modules as well as provides
/// "spies" for the critical methods for testing purposes.
#[derive(Debug, Default)]
pub struct MockSpecifierHandler {
pub fixtures: PathBuf,
pub build_info: HashMap<ModuleSpecifier, TextDocument>,
pub build_info_calls: Vec<(ModuleSpecifier, EmitType, TextDocument)>,
pub cache_calls: Vec<(
ModuleSpecifier,
EmitType,
TextDocument,
Option<TextDocument>,
)>,
pub deps_calls: Vec<(ModuleSpecifier, DependencyMap)>,
pub types_calls: Vec<(ModuleSpecifier, String)>,
pub version_calls: Vec<(ModuleSpecifier, String)>,
}
impl MockSpecifierHandler {}
impl MockSpecifierHandler {
fn get_cache(&self, specifier: ModuleSpecifier) -> Result<CachedModule> {
let specifier_text = specifier
.to_string()
.replace(":///", "_")
.replace("://", "_")
.replace("/", "-");
let specifier_path = self.fixtures.join(specifier_text);
let media_type =
match specifier_path.extension().unwrap().to_str().unwrap() {
"ts" => {
if specifier_path.to_string_lossy().ends_with(".d.ts") {
MediaType::Dts
} else {
MediaType::TypeScript
}
}
"tsx" => MediaType::TSX,
"js" => MediaType::JavaScript,
"jsx" => MediaType::JSX,
_ => MediaType::Unknown,
};
let source =
TextDocument::new(fs::read(specifier_path)?, Option::<&str>::None);
Ok(CachedModule {
source,
specifier,
media_type,
..CachedModule::default()
})
}
}
impl SpecifierHandler for MockSpecifierHandler {
fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture {
Box::pin(future::ready(self.get_cache(specifier)))
}
fn get_build_info(
&self,
specifier: &ModuleSpecifier,
_cache_type: &EmitType,
) -> Result<Option<TextDocument>> {
Ok(self.build_info.get(specifier).cloned())
}
fn set_cache(
&mut self,
specifier: &ModuleSpecifier,
cache_type: &EmitType,
code: TextDocument,
maybe_map: Option<TextDocument>,
) -> Result<()> {
self.cache_calls.push((
specifier.clone(),
cache_type.clone(),
code,
maybe_map,
));
Ok(())
}
fn set_types(
&mut self,
specifier: &ModuleSpecifier,
types: String,
) -> Result<()> {
self.types_calls.push((specifier.clone(), types));
Ok(())
}
fn set_build_info(
&mut self,
specifier: &ModuleSpecifier,
cache_type: &EmitType,
build_info: TextDocument,
) -> Result<()> {
self
.build_info
.insert(specifier.clone(), build_info.clone());
self.build_info_calls.push((
specifier.clone(),
cache_type.clone(),
build_info,
));
Ok(())
}
fn set_deps(
&mut self,
specifier: &ModuleSpecifier,
dependencies: DependencyMap,
) -> Result<()> {
self.deps_calls.push((specifier.clone(), dependencies));
Ok(())
}
fn set_version(
&mut self,
specifier: &ModuleSpecifier,
version: String,
) -> Result<()> {
self.version_calls.push((specifier.clone(), version));
Ok(())
}
}
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,
file_fetcher,
permissions: Permissions::allow_all(),
};
(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()).await.unwrap();
assert_eq!(cached_module.emits.len(), 0);
assert!(cached_module.maybe_dependencies.is_none());
assert_eq!(cached_module.media_type, MediaType::TypeScript);
assert_eq!(
cached_module.source.to_str().unwrap(),
"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()).await.unwrap();
assert_eq!(cached_module.emits.len(), 0);
let code = TextDocument::from("some code");
file_fetcher
.set_cache(&specifier, &EmitType::Cli, code, None)
.expect("could not set cache");
let cached_module: CachedModule =
file_fetcher.fetch(specifier.clone()).await.unwrap();
assert_eq!(cached_module.emits.len(), 1);
let actual_emit = cached_module.emits.get(&EmitType::Cli).unwrap();
assert_eq!(actual_emit.0.to_str().unwrap(), "some code");
assert_eq!(actual_emit.1, None);
}
}