diff --git a/cli/compiler.rs b/cli/compiler.rs index bd0a763c1b..6816df2ee0 100644 --- a/cli/compiler.rs +++ b/cli/compiler.rs @@ -104,6 +104,7 @@ impl WorkerBehavior for CompilerBehavior { #[derive(Debug, Clone)] pub struct ModuleMetaData { pub module_name: String, + pub module_redirect_source_name: Option, // source of redirect pub filename: String, pub media_type: msg::MediaType, pub source_code: Vec, @@ -250,6 +251,9 @@ pub fn compile_sync( ) { Ok(serde_json::Value::Object(map)) => ModuleMetaData { module_name: module_meta_data_.module_name.clone(), + module_redirect_source_name: module_meta_data_ + .module_redirect_source_name + .clone(), filename: module_meta_data_.filename.clone(), media_type: module_meta_data_.media_type, source_code: module_meta_data_.source_code.clone(), @@ -342,6 +346,7 @@ mod tests { let mut out = ModuleMetaData { module_name: "xxx".to_owned(), + module_redirect_source_name: None, filename: "/tests/002_hello.ts".to_owned(), media_type: msg::MediaType::TypeScript, source_code: include_bytes!("../tests/002_hello.ts").to_vec(), diff --git a/cli/deno_dir.rs b/cli/deno_dir.rs index 703df43ad3..1d69046d31 100644 --- a/cli/deno_dir.rs +++ b/cli/deno_dir.rs @@ -11,9 +11,11 @@ use crate::msg; use crate::tokio_util; use crate::version; use dirs; -use futures::future::Either; +use futures::future::{loop_fn, Either, Loop}; use futures::Future; +use http; use ring; +use serde_json; use std; use std::fmt::Write; use std::fs; @@ -34,6 +36,7 @@ fn extmap(ext: &str) -> msg::MediaType { } } +#[derive(Clone)] pub struct DenoDir { // Example: /Users/rld/.deno/ pub root: PathBuf, @@ -154,66 +157,70 @@ impl DenoDir { let gen = self.gen.clone(); Either::B( - get_source_code_async(module_name.as_str(), filename.as_str(), use_cache) - .then(move |result| { - let mut out = match result { - Ok(out) => out, - Err(err) => { - if err.kind() == ErrorKind::NotFound { - // For NotFound, change the message to something better. - return Err(errors::new( - ErrorKind::NotFound, - format!( - "Cannot resolve module \"{}\" from \"{}\"", - specifier, referrer - ), - )); - } else { - return Err(err); - } + get_source_code_async( + self, + module_name.as_str(), + filename.as_str(), + use_cache, + ).then(move |result| { + let mut out = match result { + Ok(out) => out, + Err(err) => { + if err.kind() == ErrorKind::NotFound { + // For NotFound, change the message to something better. + return Err(errors::new( + ErrorKind::NotFound, + format!( + "Cannot resolve module \"{}\" from \"{}\"", + specifier, referrer + ), + )); + } else { + return Err(err); } - }; - - if out.source_code.starts_with(b"#!") { - out.source_code = filter_shebang(out.source_code); } + }; - // If TypeScript we have to also load corresponding compile js and - // source maps (called output_code and output_source_map) - if out.media_type != msg::MediaType::TypeScript || !use_cache { - return Ok(out); - } + if out.source_code.starts_with(b"#!") { + out.source_code = filter_shebang(out.source_code); + } - let cache_key = - source_code_hash(&out.filename, &out.source_code, version::DENO); - let (output_code_filename, output_source_map_filename) = ( - gen.join(cache_key.to_string() + ".js"), - gen.join(cache_key.to_string() + ".js.map"), - ); + // If TypeScript we have to also load corresponding compile js and + // source maps (called output_code and output_source_map) + if out.media_type != msg::MediaType::TypeScript || !use_cache { + return Ok(out); + } - let result = - load_cache2(&output_code_filename, &output_source_map_filename); - match result { - Err(err) => { - if err.kind() == std::io::ErrorKind::NotFound { - // If there's no compiled JS or source map, that's ok, just - // return what we have. - Ok(out) - } else { - Err(err.into()) - } - } - Ok((output_code, source_map)) => { - out.maybe_output_code = Some(output_code); - out.maybe_source_map = Some(source_map); - out.maybe_output_code_filename = - Some(output_code_filename.to_str().unwrap().to_string()); - out.maybe_source_map_filename = - Some(output_source_map_filename.to_str().unwrap().to_string()); + let cache_key = + source_code_hash(&out.filename, &out.source_code, version::DENO); + let (output_code_filename, output_source_map_filename) = ( + gen.join(cache_key.to_string() + ".js"), + gen.join(cache_key.to_string() + ".js.map"), + ); + + let result = + load_cache2(&output_code_filename, &output_source_map_filename); + match result { + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + // If there's no compiled JS or source map, that's ok, just + // return what we have. Ok(out) + } else { + Err(err.into()) } } - }), + Ok((output_code, source_map)) => { + out.maybe_output_code = Some(output_code); + out.maybe_source_map = Some(source_map); + out.maybe_output_code_filename = + Some(output_code_filename.to_str().unwrap().to_string()); + out.maybe_source_map_filename = + Some(output_source_map_filename.to_str().unwrap().to_string()); + Ok(out) + } + } + }), ) } @@ -318,7 +325,7 @@ impl DenoDir { get_cache_filename(self.deps_http.as_path(), &j).as_ref(), ) } - // TODO(kevinkassimo): change this to support other protocols than http + // TODO(kevinkassimo): change this to support other protocols than http. _ => unimplemented!(), } @@ -353,6 +360,7 @@ impl SourceMapGetter for DenoDir { /// download will be written to "filename". This happens no matter the value of /// use_cache. fn get_source_code_async( + deno_dir: &DenoDir, module_name: &str, filename: &str, use_cache: bool, @@ -361,15 +369,15 @@ fn get_source_code_async( let module_name = module_name.to_string(); let is_module_remote = is_remote(&module_name); // We try fetch local. Two cases: - // 1. This is a remote module and we're allowed to use cached downloads - // 2. This is a local module + // 1. This is a remote module and we're allowed to use cached downloads. + // 2. This is a local module. if !is_module_remote || use_cache { debug!( "fetch local or reload {} is_module_remote {}", module_name, is_module_remote ); // Note that local fetch is done synchronously. - match fetch_local_source(&module_name, &filename) { + match fetch_local_source(deno_dir, &module_name, &filename, None) { Ok(Some(output)) => { debug!("found local source "); return Either::A(futures::future::ok(output)); @@ -396,27 +404,35 @@ fn get_source_code_async( debug!("is remote but didn't find module"); - // not cached/local, try remote - Either::B(fetch_remote_source_async(&module_name, &filename).and_then( - move |maybe_remote_source| match maybe_remote_source { - Some(output) => Ok(output), - None => Err(DenoError::from(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("cannot find remote file '{}'", &filename), - ))), - }, - )) + // not cached/local, try remote. + Either::B( + fetch_remote_source_async(deno_dir, &module_name, &filename).and_then( + move |maybe_remote_source| match maybe_remote_source { + Some(output) => Ok(output), + None => Err(DenoError::from(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("cannot find remote file '{}'", &filename), + ))), + }, + ), + ) } #[cfg(test)] /// Synchronous version of get_source_code_async /// This function is deprecated. fn get_source_code( + deno_dir: &DenoDir, module_name: &str, filename: &str, use_cache: bool, ) -> DenoResult { - tokio_util::block_on(get_source_code_async(module_name, filename, use_cache)) + tokio_util::block_on(get_source_code_async( + deno_dir, + module_name, + filename, + use_cache, + )) } fn get_cache_filename(basedir: &Path, url: &Url) -> PathBuf { @@ -537,71 +553,176 @@ fn filter_shebang(bytes: Vec) -> Vec { /// Asynchronously fetch remote source file specified by the URL `module_name` /// and write it to disk at `filename`. fn fetch_remote_source_async( + deno_dir: &DenoDir, module_name: &str, filename: &str, ) -> impl Future, Error = DenoError> { - let filename = filename.to_string(); - let module_name = module_name.to_string(); - let p = PathBuf::from(filename.clone()); - // We write a special ".mime" file into the `.deno/deps` directory along side the - // cached file, containing just the media type. - let media_type_filename = [&filename, ".mime"].concat(); - let mt = PathBuf::from(&media_type_filename); - eprintln!("Downloading {}", &module_name); - http_util::fetch_string(&module_name).and_then( - move |(source, content_type)| { - match p.parent() { - Some(ref parent) => fs::create_dir_all(parent), - None => Ok(()), - }?; - deno_fs::write_file(&p, &source, 0o666)?; - // Remove possibly existing stale .mime file - // may not exist. DON'T unwrap - let _ = std::fs::remove_file(&media_type_filename); - // Create .mime file only when content type different from extension - let resolved_content_type = map_content_type(&p, Some(&content_type)); - let ext = p - .extension() - .map(|x| x.to_str().unwrap_or("")) - .unwrap_or(""); - let media_type = extmap(&ext); - if media_type == msg::MediaType::Unknown - || media_type != resolved_content_type - { - deno_fs::write_file(&mt, content_type.as_bytes(), 0o666)? - } - Ok(Some(ModuleMetaData { - module_name: module_name.to_string(), - filename: filename.to_string(), - media_type: map_content_type(&p, Some(&content_type)), - source_code: source.as_bytes().to_owned(), - maybe_output_code_filename: None, - maybe_output_code: None, - maybe_source_map_filename: None, - maybe_source_map: None, - })) + use crate::http_util::FetchOnceResult; + { + eprintln!("Downloading {}", module_name); + } + + let filename = filename.to_owned(); + let module_name = module_name.to_owned(); + + // We write a special ".headers.json" file into the `.deno/deps` directory along side the + // cached file, containing just the media type and possible redirect target (both are http headers). + // If redirect target is present, the file itself if not cached. + // In future resolutions, we would instead follow this redirect target ("redirect_to"). + loop_fn( + ( + deno_dir.clone(), + None, + None, + module_name.clone(), + filename.clone(), + ), + |( + dir, + maybe_initial_module_name, + maybe_initial_filename, + module_name, + filename, + )| { + let url = module_name.parse::().unwrap(); + // Single pass fetch, either yields code or yields redirect. + http_util::fetch_string_once(url).and_then(move |fetch_once_result| { + match fetch_once_result { + FetchOnceResult::Redirect(url) => { + // If redirects, update module_name and filename for next looped call. + let resolve_result = dir + .resolve_module(&(url.to_string()), ".") + .map_err(DenoError::from); + match resolve_result { + Ok((new_module_name, new_filename)) => { + let mut maybe_initial_module_name = maybe_initial_module_name; + let mut maybe_initial_filename = maybe_initial_filename; + if maybe_initial_module_name.is_none() { + maybe_initial_module_name = Some(module_name.clone()); + maybe_initial_filename = Some(filename.clone()); + } + // Not yet completed. Follow the redirect and loop. + Ok(Loop::Continue(( + dir, + maybe_initial_module_name, + maybe_initial_filename, + new_module_name, + new_filename, + ))) + } + Err(e) => Err(e), + } + } + FetchOnceResult::Code(source, maybe_content_type) => { + // We land on the code. + let p = PathBuf::from(filename.clone()); + match p.parent() { + Some(ref parent) => fs::create_dir_all(parent), + None => Ok(()), + }?; + // Write file and create .headers.json for the file. + deno_fs::write_file(&p, &source, 0o666)?; + { + save_source_code_headers(&filename, maybe_content_type.clone(), None); + } + // Check if this file is downloaded due to some old redirect request. + if maybe_initial_filename.is_some() { + // If yes, record down the headers for redirect. + // Also create its containing folder. + let pp = PathBuf::from(filename.clone()); + match pp.parent() { + Some(ref parent) => fs::create_dir_all(parent), + None => Ok(()), + }?; + { + save_source_code_headers( + &maybe_initial_filename.clone().unwrap(), + maybe_content_type.clone(), + Some(module_name.clone()), + ); + } + } + Ok(Loop::Break(Some(ModuleMetaData { + module_name: module_name.to_string(), + module_redirect_source_name: maybe_initial_module_name, + filename: filename.to_string(), + media_type: map_content_type( + &p, + maybe_content_type.as_ref().map(|s| s.as_str()), + ), + source_code: source.as_bytes().to_owned(), + maybe_output_code_filename: None, + maybe_output_code: None, + maybe_source_map_filename: None, + maybe_source_map: None, + }))) + } + } + }) }, ) } -// Prototype https://github.com/denoland/deno/blob/golang/deno_dir.go#L37-L73 /// Fetch remote source code. #[cfg(test)] fn fetch_remote_source( + deno_dir: &DenoDir, module_name: &str, filename: &str, ) -> DenoResult> { - tokio_util::block_on(fetch_remote_source_async(module_name, filename)) + tokio_util::block_on(fetch_remote_source_async( + deno_dir, + module_name, + filename, + )) } /// Fetch local or cached source code. +/// This is a recursive operation if source file has redirection. +/// It will keep reading filename.headers.json for information about redirection. +/// module_initial_source_name would be None on first call, +/// and becomes the name of the very first module that initiates the call +/// in subsequent recursions. +/// AKA if redirection occurs, module_initial_source_name is the source path +/// that user provides, and the final module_name is the resolved path +/// after following all redirections. fn fetch_local_source( + deno_dir: &DenoDir, module_name: &str, filename: &str, + module_initial_source_name: Option, ) -> DenoResult> { let p = Path::new(&filename); - let media_type_filename = [&filename, ".mime"].concat(); - let mt = Path::new(&media_type_filename); + let source_code_headers = get_source_code_headers(&filename); + // If source code headers says that it would redirect elsewhere, + // (meaning that the source file might not exist; only .headers.json is present) + // Abort reading attempts to the cached source file and and follow the redirect. + if let Some(redirect_to) = source_code_headers.redirect_to { + // E.g. + // module_name https://import-meta.now.sh/redirect.js + // filename /Users/kun/Library/Caches/deno/deps/https/import-meta.now.sh/redirect.js + // redirect_to https://import-meta.now.sh/sub/final1.js + // real_filename /Users/kun/Library/Caches/deno/deps/https/import-meta.now.sh/sub/final1.js + // real_module_name = https://import-meta.now.sh/sub/final1.js + let (real_module_name, real_filename) = + deno_dir.resolve_module(&redirect_to, ".")?; + let mut module_initial_source_name = module_initial_source_name; + // If this is the first redirect attempt, + // then module_initial_source_name should be None. + // In that case, use current module name as module_initial_source_name. + if module_initial_source_name.is_none() { + module_initial_source_name = Some(module_name.to_owned()); + } + // Recurse. + return fetch_local_source( + deno_dir, + &real_module_name, + &real_filename, + module_initial_source_name, + ); + } + // No redirect needed or end of redirects. + // We can try read the file let source_code = match fs::read(p) { Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { @@ -612,16 +733,14 @@ fn fetch_local_source( } Ok(c) => c, }; - // .mime file might not exists - // this is okay for local source: maybe_content_type_str will be None - let maybe_content_type_string = fs::read_to_string(&mt).ok(); - // Option -> Option<&str> - let maybe_content_type_str = - maybe_content_type_string.as_ref().map(String::as_str); Ok(Some(ModuleMetaData { module_name: module_name.to_string(), + module_redirect_source_name: module_initial_source_name, filename: filename.to_string(), - media_type: map_content_type(&p, maybe_content_type_str), + media_type: map_content_type( + &p, + source_code_headers.mime_type.as_ref().map(String::as_str), + ), source_code, maybe_output_code_filename: None, maybe_output_code: None, @@ -630,6 +749,106 @@ fn fetch_local_source( })) } +#[derive(Debug)] +/// Header metadata associated with a particular "symbolic" source code file. +/// (the associated source code file might not be cached, while remaining +/// a user accessible entity through imports (due to redirects)). +pub struct SourceCodeHeaders { + /// MIME type of the source code. + pub mime_type: Option, + /// Where should we actually look for source code. + /// This should be an absolute path! + pub redirect_to: Option, +} + +static MIME_TYPE: &'static str = "mime_type"; +static REDIRECT_TO: &'static str = "redirect_to"; + +fn source_code_headers_filename(filename: &str) -> String { + [&filename, ".headers.json"].concat() +} + +/// Get header metadata associated with a single source code file. +/// NOTICE: chances are that the source code itself is not downloaded due to redirects. +/// In this case, the headers file provides info about where we should go and get +/// the source code that redirect eventually points to (which should be cached). +fn get_source_code_headers(filename: &str) -> SourceCodeHeaders { + let headers_filename = source_code_headers_filename(filename); + let hd = Path::new(&headers_filename); + // .headers.json file might not exists. + // This is okay for local source. + let maybe_headers_string = fs::read_to_string(&hd).ok(); + if let Some(headers_string) = maybe_headers_string { + // TODO(kevinkassimo): consider introduce serde::Deserialize to make things simpler. + let maybe_headers: serde_json::Result = + serde_json::from_str(&headers_string); + if let Ok(headers) = maybe_headers { + return SourceCodeHeaders { + mime_type: headers[MIME_TYPE].as_str().map(String::from), + redirect_to: headers[REDIRECT_TO].as_str().map(String::from), + }; + } + } + SourceCodeHeaders { + mime_type: None, + redirect_to: None, + } +} + +/// Save headers related to source filename to {filename}.headers.json file, +/// only when there is actually something necessary to save. +/// For example, if the extension ".js" already mean JS file and we have +/// content type of "text/javascript", then we would not save the mime type. +/// If nothing needs to be saved, the headers file is not created. +fn save_source_code_headers( + filename: &str, + mime_type: Option, + redirect_to: Option, +) { + let headers_filename = source_code_headers_filename(filename); + // Remove possibly existing stale .headers.json file. + // May not exist. DON'T unwrap. + let _ = std::fs::remove_file(&headers_filename); + let p = PathBuf::from(filename); + // TODO(kevinkassimo): consider introduce serde::Deserialize to make things simpler. + // This is super ugly at this moment... + // Had trouble to make serde_derive work: I'm unable to build proc-macro2. + let mut value_map = serde_json::map::Map::new(); + if mime_type.is_some() { + let mime_type_string = mime_type.clone().unwrap(); + let resolved_mime_type = + { map_content_type(Path::new(""), Some(mime_type_string.as_str())) }; + let ext = p + .extension() + .map(|x| x.to_str().unwrap_or("")) + .unwrap_or(""); + let ext_based_mime_type = extmap(&ext); + // Add mime to headers only when content type is different from extension. + if ext_based_mime_type == msg::MediaType::Unknown + || resolved_mime_type != ext_based_mime_type + { + value_map.insert(MIME_TYPE.to_string(), json!(mime_type_string)); + } + } + if redirect_to.is_some() { + value_map.insert(REDIRECT_TO.to_string(), json!(redirect_to.unwrap())); + } + // Only save to file when there is actually data. + if value_map.len() > 0 { + let _ = serde_json::to_string(&value_map).map(|s| { + // It is possible that we need to create file + // with parent folders not yet created. + // (Due to .headers.json feature for redirection) + let hd = PathBuf::from(&headers_filename); + let _ = match hd.parent() { + Some(ref parent) => fs::create_dir_all(parent), + None => Ok(()), + }; + let _ = deno_fs::write_file(&(hd.as_path()), s, 0o666); + }); + } +} + #[cfg(test)] mod tests { use super::*; @@ -709,6 +928,7 @@ mod tests { filename: filename.to_owned(), source_code: source_code[..].to_owned(), module_name: "hello.js".to_owned(), + module_redirect_source_name: None, media_type: msg::MediaType::TypeScript, maybe_output_code: Some(output_code[..].to_owned()), maybe_output_code_filename: None, @@ -745,6 +965,35 @@ mod tests { ); } + #[test] + fn test_source_code_headers_get_and_save() { + let (temp_dir, _deno_dir) = test_setup(); + let filename = + deno_fs::normalize_path(temp_dir.into_path().join("f.js").as_ref()); + let headers_file_name = source_code_headers_filename(&filename); + assert_eq!(headers_file_name, [&filename, ".headers.json"].concat()); + let _ = deno_fs::write_file(&PathBuf::from(&headers_file_name), + "{\"mime_type\":\"text/javascript\",\"redirect_to\":\"http://example.com/a.js\"}", 0o666); + let headers = get_source_code_headers(&filename); + assert_eq!(headers.mime_type.clone().unwrap(), "text/javascript"); + assert_eq!( + headers.redirect_to.clone().unwrap(), + "http://example.com/a.js" + ); + + save_source_code_headers( + &filename, + Some("text/typescript".to_owned()), + Some("http://deno.land/a.js".to_owned()), + ); + let headers2 = get_source_code_headers(&filename); + assert_eq!(headers2.mime_type.clone().unwrap(), "text/typescript"); + assert_eq!( + headers2.redirect_to.clone().unwrap(), + "http://deno.land/a.js" + ); + } + #[test] fn test_get_source_code_1() { let (_temp_dir, deno_dir) = test_setup(); @@ -757,9 +1006,9 @@ mod tests { .join("localhost_PORT4545/tests/subdir/mod2.ts") .as_ref(), ); - let mime_file_name = format!("{}.mime", &filename); + let headers_file_name = source_code_headers_filename(&filename); - let result = get_source_code(module_name, &filename, true); + let result = get_source_code(&deno_dir, module_name, &filename, true); assert!(result.is_ok()); let r = result.unwrap(); assert_eq!( @@ -767,12 +1016,13 @@ mod tests { "export { printHello } from \"./print_hello.ts\";\n".as_bytes() ); assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); - // Should not create .mime file due to matching ext - assert!(fs::read_to_string(&mime_file_name).is_err()); + // Should not create .headers.json file due to matching ext + assert!(fs::read_to_string(&headers_file_name).is_err()); - // Modify .mime - let _ = fs::write(&mime_file_name, "text/javascript"); - let result2 = get_source_code(module_name, &filename, true); + // Modify .headers.json, write using fs write and read using save_source_code_headers + let _ = + fs::write(&headers_file_name, "{ \"mime_type\": \"text/javascript\" }"); + let result2 = get_source_code(&deno_dir, module_name, &filename, true); assert!(result2.is_ok()); let r2 = result2.unwrap(); assert_eq!( @@ -780,23 +1030,45 @@ mod tests { "export { printHello } from \"./print_hello.ts\";\n".as_bytes() ); // If get_source_code does not call remote, this should be JavaScript - // as we modified before! (we do not overwrite .mime due to no http fetch) + // as we modified before! (we do not overwrite .headers.json due to no http fetch) assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); assert_eq!( - fs::read_to_string(&mime_file_name).unwrap(), + get_source_code_headers(&filename).mime_type.unwrap(), "text/javascript" ); - // Don't use_cache - let result3 = get_source_code(module_name, &filename, false); + // Modify .headers.json again, but the other way around + save_source_code_headers( + &filename, + Some("application/json".to_owned()), + None, + ); + let result3 = get_source_code(&deno_dir, module_name, &filename, true); assert!(result3.is_ok()); let r3 = result3.unwrap(); - let expected3 = + assert_eq!( + r3.source_code, + "export { printHello } from \"./print_hello.ts\";\n".as_bytes() + ); + // If get_source_code does not call remote, this should be JavaScript + // as we modified before! (we do not overwrite .headers.json due to no http fetch) + assert_eq!(&(r3.media_type), &msg::MediaType::Json); + assert!( + fs::read_to_string(&headers_file_name) + .unwrap() + .contains("application/json") + ); + + // Don't use_cache + let result4 = get_source_code(&deno_dir, module_name, &filename, false); + assert!(result4.is_ok()); + let r4 = result4.unwrap(); + let expected4 = "export { printHello } from \"./print_hello.ts\";\n".as_bytes(); - assert_eq!(r3.source_code, expected3); - // Now the old .mime file should have gone! Resolved back to TypeScript - assert_eq!(&(r3.media_type), &msg::MediaType::TypeScript); - assert!(fs::read_to_string(&mime_file_name).is_err()); + assert_eq!(r4.source_code, expected4); + // Now the old .headers.json file should have gone! Resolved back to TypeScript + assert_eq!(&(r4.media_type), &msg::MediaType::TypeScript); + assert!(fs::read_to_string(&headers_file_name).is_err()); }); } @@ -812,51 +1084,173 @@ mod tests { .join("localhost_PORT4545/tests/subdir/mismatch_ext.ts") .as_ref(), ); - let mime_file_name = format!("{}.mime", &filename); + let headers_file_name = source_code_headers_filename(&filename); - let result = get_source_code(module_name, &filename, true); + let result = get_source_code(&deno_dir, module_name, &filename, true); assert!(result.is_ok()); let r = result.unwrap(); let expected = "export const loaded = true;\n".as_bytes(); assert_eq!(r.source_code, expected); - // Mismatch ext with content type, create .mime + // Mismatch ext with content type, create .headers.json assert_eq!(&(r.media_type), &msg::MediaType::JavaScript); assert_eq!( - fs::read_to_string(&mime_file_name).unwrap(), + get_source_code_headers(&filename).mime_type.unwrap(), "text/javascript" ); - // Modify .mime - let _ = fs::write(&mime_file_name, "text/typescript"); - let result2 = get_source_code(module_name, &filename, true); + // Modify .headers.json + save_source_code_headers( + &filename, + Some("text/typescript".to_owned()), + None, + ); + let result2 = get_source_code(&deno_dir, module_name, &filename, true); assert!(result2.is_ok()); let r2 = result2.unwrap(); let expected2 = "export const loaded = true;\n".as_bytes(); assert_eq!(r2.source_code, expected2); // If get_source_code does not call remote, this should be TypeScript - // as we modified before! (we do not overwrite .mime due to no http fetch) + // as we modified before! (we do not overwrite .headers.json due to no http fetch) assert_eq!(&(r2.media_type), &msg::MediaType::TypeScript); - assert_eq!( - fs::read_to_string(&mime_file_name).unwrap(), - "text/typescript" - ); + assert!(fs::read_to_string(&headers_file_name).is_err()); // Don't use_cache - let result3 = get_source_code(module_name, &filename, false); + let result3 = get_source_code(&deno_dir, module_name, &filename, false); assert!(result3.is_ok()); let r3 = result3.unwrap(); let expected3 = "export const loaded = true;\n".as_bytes(); assert_eq!(r3.source_code, expected3); - // Now the old .mime file should be overwritten back to JavaScript! + // Now the old .headers.json file should be overwritten back to JavaScript! // (due to http fetch) assert_eq!(&(r3.media_type), &msg::MediaType::JavaScript); assert_eq!( - fs::read_to_string(&mime_file_name).unwrap(), + get_source_code_headers(&filename).mime_type.unwrap(), "text/javascript" ); }); } + #[test] + fn test_get_source_code_3() { + let (_temp_dir, deno_dir) = test_setup(); + // Test basic follow and headers recording + tokio_util::init(|| { + let redirect_module_name = + "http://localhost:4546/tests/subdir/redirects/redirect1.js"; + let redirect_source_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4546/tests/subdir/redirects/redirect1.js") + .as_ref(), + ); + let target_module_name = + "http://localhost:4545/tests/subdir/redirects/redirect1.js"; + let redirect_target_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/redirects/redirect1.js") + .as_ref(), + ); + let mod_meta = get_source_code( + &deno_dir, + redirect_module_name, + &redirect_source_filename, + true, + ).unwrap(); + // File that requires redirection is not downloaded. + assert!(fs::read_to_string(&redirect_source_filename).is_err()); + // ... but its .headers.json is created. + let redirect_source_headers = + get_source_code_headers(&redirect_source_filename); + assert_eq!( + redirect_source_headers.redirect_to.unwrap(), + "http://localhost:4545/tests/subdir/redirects/redirect1.js" + ); + // The target of redirection is downloaded instead. + assert_eq!( + fs::read_to_string(&redirect_target_filename).unwrap(), + "export const redirect = 1;\n" + ); + let redirect_target_headers = + get_source_code_headers(&redirect_target_filename); + assert!(redirect_target_headers.redirect_to.is_none()); + + // Examine the meta result. + assert_eq!(&mod_meta.module_name, target_module_name); + assert_eq!( + &mod_meta.module_redirect_source_name.clone().unwrap(), + redirect_module_name + ); + }); + } + + #[test] + fn test_get_source_code_4() { + let (_temp_dir, deno_dir) = test_setup(); + // Test double redirects and headers recording + tokio_util::init(|| { + let redirect_module_name = + "http://localhost:4548/tests/subdir/redirects/redirect1.js"; + let redirect_source_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4548/tests/subdir/redirects/redirect1.js") + .as_ref(), + ); + let redirect_source_filename_intermediate = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4546/tests/subdir/redirects/redirect1.js") + .as_ref(), + ); + let target_module_name = + "http://localhost:4545/tests/subdir/redirects/redirect1.js"; + let redirect_target_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/redirects/redirect1.js") + .as_ref(), + ); + let mod_meta = get_source_code( + &deno_dir, + redirect_module_name, + &redirect_source_filename, + true, + ).unwrap(); + + // File that requires redirection is not downloaded. + assert!(fs::read_to_string(&redirect_source_filename).is_err()); + // ... but its .headers.json is created. + let redirect_source_headers = + get_source_code_headers(&redirect_source_filename); + assert_eq!( + redirect_source_headers.redirect_to.unwrap(), + target_module_name + ); + + // In the intermediate redirection step, file is also not downloaded. + assert!( + fs::read_to_string(&redirect_source_filename_intermediate).is_err() + ); + + // The target of redirection is downloaded instead. + assert_eq!( + fs::read_to_string(&redirect_target_filename).unwrap(), + "export const redirect = 1;\n" + ); + let redirect_target_headers = + get_source_code_headers(&redirect_target_filename); + assert!(redirect_target_headers.redirect_to.is_none()); + + // Examine the meta result. + assert_eq!(&mod_meta.module_name, target_module_name); + assert_eq!( + &mod_meta.module_redirect_source_name.clone().unwrap(), + redirect_module_name + ); + }); + } + #[test] fn test_fetch_source_async_1() { use crate::tokio_util; @@ -871,9 +1265,10 @@ mod tests { .join("127.0.0.1_PORT4545/tests/subdir/mt_video_mp2t.t3.ts") .as_ref(), ); - let mime_file_name = format!("{}.mime", &filename); + let headers_file_name = source_code_headers_filename(&filename); let result = tokio_util::block_on(fetch_remote_source_async( + &deno_dir, &module_name, &filename, )); @@ -881,16 +1276,21 @@ mod tests { let r = result.unwrap().unwrap(); assert_eq!(r.source_code, b"export const loaded = true;\n"); assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); - // matching ext, no .mime file created - assert!(fs::read_to_string(&mime_file_name).is_err()); + // matching ext, no .headers.json file created + assert!(fs::read_to_string(&headers_file_name).is_err()); - // Modify .mime, make sure read from local - let _ = fs::write(&mime_file_name, "text/javascript"); - let result2 = fetch_local_source(&module_name, &filename); + // Modify .headers.json, make sure read from local + save_source_code_headers( + &filename, + Some("text/javascript".to_owned()), + None, + ); + let result2 = + fetch_local_source(&deno_dir, &module_name, &filename, None); assert!(result2.is_ok()); let r2 = result2.unwrap().unwrap(); assert_eq!(r2.source_code, b"export const loaded = true;\n"); - // Not MediaType::TypeScript due to .mime modification + // Not MediaType::TypeScript due to .headers.json modification assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); }); } @@ -909,23 +1309,27 @@ mod tests { .join("localhost_PORT4545/tests/subdir/mt_video_mp2t.t3.ts") .as_ref(), ); - let mime_file_name = format!("{}.mime", &filename); + let headers_file_name = source_code_headers_filename(&filename); - let result = fetch_remote_source(module_name, &filename); + let result = fetch_remote_source(&deno_dir, module_name, &filename); assert!(result.is_ok()); let r = result.unwrap().unwrap(); assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); - // matching ext, no .mime file created - assert!(fs::read_to_string(&mime_file_name).is_err()); + // matching ext, no .headers.json file created + assert!(fs::read_to_string(&headers_file_name).is_err()); - // Modify .mime, make sure read from local - let _ = fs::write(&mime_file_name, "text/javascript"); - let result2 = fetch_local_source(module_name, &filename); + // Modify .headers.json, make sure read from local + save_source_code_headers( + &filename, + Some("text/javascript".to_owned()), + None, + ); + let result2 = fetch_local_source(&deno_dir, module_name, &filename, None); assert!(result2.is_ok()); let r2 = result2.unwrap().unwrap(); assert_eq!(r2.source_code, "export const loaded = true;\n".as_bytes()); - // Not MediaType::TypeScript due to .mime modification + // Not MediaType::TypeScript due to .headers.json modification assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); }); } @@ -943,15 +1347,14 @@ mod tests { .join("localhost_PORT4545/tests/subdir/no_ext") .as_ref(), ); - let mime_file_name = format!("{}.mime", &filename); - let result = fetch_remote_source(module_name, &filename); + let result = fetch_remote_source(&deno_dir, module_name, &filename); assert!(result.is_ok()); let r = result.unwrap().unwrap(); assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); - // no ext, should create .mime file + // no ext, should create .headers.json file assert_eq!( - fs::read_to_string(&mime_file_name).unwrap(), + get_source_code_headers(&filename).mime_type.unwrap(), "text/typescript" ); @@ -962,15 +1365,14 @@ mod tests { .join("localhost_PORT4545/tests/subdir/mismatch_ext.ts") .as_ref(), ); - let mime_file_name_2 = format!("{}.mime", &filename_2); - let result_2 = fetch_remote_source(module_name_2, &filename_2); + let result_2 = fetch_remote_source(&deno_dir, module_name_2, &filename_2); assert!(result_2.is_ok()); let r2 = result_2.unwrap().unwrap(); assert_eq!(r2.source_code, "export const loaded = true;\n".as_bytes()); assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); - // mismatch ext, should create .mime file + // mismatch ext, should create .headers.json file assert_eq!( - fs::read_to_string(&mime_file_name_2).unwrap(), + get_source_code_headers(&filename_2).mime_type.unwrap(), "text/javascript" ); @@ -982,15 +1384,14 @@ mod tests { .join("localhost_PORT4545/tests/subdir/unknown_ext.deno") .as_ref(), ); - let mime_file_name_3 = format!("{}.mime", &filename_3); - let result_3 = fetch_remote_source(module_name_3, &filename_3); + let result_3 = fetch_remote_source(&deno_dir, module_name_3, &filename_3); assert!(result_3.is_ok()); let r3 = result_3.unwrap().unwrap(); assert_eq!(r3.source_code, "export const loaded = true;\n".as_bytes()); assert_eq!(&(r3.media_type), &msg::MediaType::TypeScript); - // unknown ext, should create .mime file + // unknown ext, should create .headers.json file assert_eq!( - fs::read_to_string(&mime_file_name_3).unwrap(), + get_source_code_headers(&filename_3).mime_type.unwrap(), "text/typescript" ); }); @@ -999,14 +1400,14 @@ mod tests { #[test] fn test_fetch_source_3() { // only local, no http_util::fetch_sync_string called - let (_temp_dir, _deno_dir) = test_setup(); + let (_temp_dir, deno_dir) = test_setup(); let cwd = std::env::current_dir().unwrap(); let cwd_string = cwd.to_str().unwrap(); let module_name = "http://example.com/mt_text_typescript.t1.ts"; // not used let filename = format!("{}/tests/subdir/mt_text_typescript.t1.ts", &cwd_string); - let result = fetch_local_source(module_name, &filename); + let result = fetch_local_source(&deno_dir, module_name, &filename, None); assert!(result.is_ok()); let r = result.unwrap().unwrap(); assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); diff --git a/cli/http_util.rs b/cli/http_util.rs index 9c92c08eb1..d04f18b1e8 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,6 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. use crate::errors; use crate::errors::DenoError; +#[cfg(test)] use futures::future::{loop_fn, Loop}; use futures::{future, Future, Stream}; use hyper; @@ -45,8 +46,13 @@ fn resolve_uri_from_location(base_uri: &Uri, location: &str) -> Uri { } else { // assuming path-noscheme | path-empty let mut new_uri_parts = base_uri.clone().into_parts(); - new_uri_parts.path_and_query = - Some(format!("{}/{}", base_uri.path(), location).parse().unwrap()); + let base_uri_path_str = base_uri.path().to_owned(); + let segs: Vec<&str> = base_uri_path_str.rsplitn(2, "/").collect(); + new_uri_parts.path_and_query = Some( + format!("{}/{}", segs.last().unwrap_or(&""), location) + .parse() + .unwrap(), + ); Uri::from_parts(new_uri_parts).unwrap() } } @@ -61,6 +67,74 @@ pub fn fetch_sync_string(module_name: &str) -> DenoResult<(String, String)> { tokio_util::block_on(fetch_string(module_name)) } +pub enum FetchOnceResult { + // (code, maybe_content_type) + Code(String, Option), + Redirect(http::uri::Uri), +} + +/// Asynchronously fetchs the given HTTP URL one pass only. +/// If no redirect is present and no error occurs, +/// yields Code(code, maybe_content_type). +/// If redirect occurs, does not follow and +/// yields Redirect(url). +pub fn fetch_string_once( + url: http::uri::Uri, +) -> impl Future { + type FetchAttempt = (Option, Option, Option); + let client = get_client(); + client + .get(url.clone()) + .map_err(DenoError::from) + .and_then(move |response| -> Box + Send> { + if response.status().is_redirection() { + let location_string = response + .headers() + .get("location") + .expect("url redirection should provide 'location' header") + .to_str() + .unwrap() + .to_string(); + debug!("Redirecting to {}...", &location_string); + let new_url = resolve_uri_from_location(&url, &location_string); + // Boxed trait object turns out to be the savior for 2+ types yielding same results. + return Box::new( + future::ok(None).join3( + future::ok(None), + future::ok(Some(FetchOnceResult::Redirect(new_url)) + )) + ); + } else if response.status().is_client_error() || response.status().is_server_error() { + return Box::new(future::err( + errors::new(errors::ErrorKind::Other, + format!("Import '{}' failed: {}", &url, response.status())) + )); + } + let content_type = response + .headers() + .get(CONTENT_TYPE) + .map(|content_type| content_type.to_str().unwrap().to_owned()); + let body = response + .into_body() + .concat2() + .map(|body| String::from_utf8(body.to_vec()).ok()) + .map_err(DenoError::from); + Box::new(body.join3( + future::ok(content_type), + future::ok(None) + )) + }) + .and_then(move |(maybe_code, maybe_content_type, maybe_redirect)| { + if let Some(redirect) = maybe_redirect { + future::ok(redirect) + } else { + // maybe_code should always contain code here! + future::ok(FetchOnceResult::Code(maybe_code.unwrap(), maybe_content_type)) + } + }) +} + +#[cfg(test)] /// Asynchronously fetchs the given HTTP URL. Returns (content, media_type). pub fn fetch_string( module_name: &str, @@ -182,5 +256,5 @@ fn test_resolve_uri_from_location_relative_3() { let url = "http://deno.land/x".parse::().unwrap(); let new_uri = resolve_uri_from_location(&url, "z"); assert_eq!(new_uri.host().unwrap(), "deno.land"); - assert_eq!(new_uri.path(), "/x/z"); + assert_eq!(new_uri.path(), "/z"); } diff --git a/cli/isolate.rs b/cli/isolate.rs index ced1cb7926..9dcf6e8f06 100644 --- a/cli/isolate.rs +++ b/cli/isolate.rs @@ -84,6 +84,15 @@ impl Isolate { &out.js_source(), )?; + // The resolved module is an alias to another module (due to redirects). + // Save such alias to the module map. + if out.module_redirect_source_name.is_some() { + self.mod_alias( + &out.module_redirect_source_name.clone().unwrap(), + &out.module_name, + ); + } + self.mod_load_deps(child_id)?; } } @@ -117,10 +126,23 @@ impl Isolate { let out = fetch_module_meta_data_and_maybe_compile(&self.state, url, ".") .map_err(RustOrJsError::from)?; + // Be careful. + // url might not match the actual out.module_name + // due to the mechanism of redirection. + let id = self .mod_new_and_register(true, &out.module_name.clone(), &out.js_source()) .map_err(RustOrJsError::from)?; + // The resolved module is an alias to another module (due to redirects). + // Save such alias to the module map. + if out.module_redirect_source_name.is_some() { + self.mod_alias( + &out.module_redirect_source_name.clone().unwrap(), + &out.module_name, + ); + } + self.mod_load_deps(id)?; let state = self.state.clone(); @@ -153,6 +175,13 @@ impl Isolate { Ok(id) } + /// Create an alias for another module. + /// The alias could later be used to grab the module + /// which `target` points to. + fn mod_alias(&self, name: &str, target: &str) { + self.state.modules.lock().unwrap().alias(name, target); + } + pub fn print_file_info(&self, module: &str) { let m = self.state.modules.lock().unwrap(); m.print_file_info(&self.state.dir, module.to_string()); diff --git a/cli/modules.rs b/cli/modules.rs index e2ded67b20..f40f5ca089 100644 --- a/cli/modules.rs +++ b/cli/modules.rs @@ -12,23 +12,78 @@ pub struct ModuleInfo { children: Vec, } +/// A symbolic module entity. +pub enum SymbolicModule { + /// This module is an alias to another module. + /// This is useful such that multiple names could point to + /// the same underlying module (particularly due to redirects). + Alias(String), + /// This module associates with a V8 module by id. + Mod(deno_mod), +} + +#[derive(Default)] +/// Alias-able module name map +pub struct ModuleNameMap { + inner: HashMap, +} + +impl ModuleNameMap { + pub fn new() -> Self { + ModuleNameMap { + inner: HashMap::new(), + } + } + + /// Get the id of a module. + /// If this module is internally represented as an alias, + /// follow the alias chain to get the final module id. + pub fn get(&self, name: &str) -> Option { + let mut mod_name = name; + loop { + let cond = self.inner.get(mod_name); + match cond { + Some(SymbolicModule::Alias(target)) => { + mod_name = target; + } + Some(SymbolicModule::Mod(mod_id)) => { + return Some(*mod_id); + } + _ => { + return None; + } + } + } + } + + /// Insert a name assocated module id. + pub fn insert(&mut self, name: String, id: deno_mod) { + self.inner.insert(name, SymbolicModule::Mod(id)); + } + + /// Create an alias to another module. + pub fn alias(&mut self, name: String, target: String) { + self.inner.insert(name, SymbolicModule::Alias(target)); + } +} + /// A collection of JS modules. #[derive(Default)] pub struct Modules { pub info: HashMap, - pub by_name: HashMap, + pub by_name: ModuleNameMap, } impl Modules { pub fn new() -> Modules { Self { info: HashMap::new(), - by_name: HashMap::new(), + by_name: ModuleNameMap::new(), } } pub fn get_id(&self, name: &str) -> Option { - self.by_name.get(name).cloned() + self.by_name.get(name) } pub fn get_children(&self, id: deno_mod) -> Option<&Vec> { @@ -56,6 +111,10 @@ impl Modules { ); } + pub fn alias(&mut self, name: &str, target: &str) { + self.by_name.alias(name.to_owned(), target.to_owned()); + } + pub fn resolve_cb( &mut self, deno_dir: &DenoDir, @@ -78,8 +137,7 @@ impl Modules { } let (name, _local_filename) = r.unwrap(); - if let Some(id) = self.by_name.get(&name) { - let child_id = *id; + if let Some(child_id) = self.by_name.get(&name) { info.children.push(child_id); return child_id; } else { diff --git a/tests/023_no_ext_with_mime b/tests/023_no_ext_with_headers similarity index 100% rename from tests/023_no_ext_with_mime rename to tests/023_no_ext_with_headers diff --git a/tests/023_no_ext_with_headers.headers.json b/tests/023_no_ext_with_headers.headers.json new file mode 100644 index 0000000000..5b6f09aeb1 --- /dev/null +++ b/tests/023_no_ext_with_headers.headers.json @@ -0,0 +1 @@ +{ "mime_type": "application/javascript" } diff --git a/tests/023_no_ext_with_mime.out b/tests/023_no_ext_with_headers.out similarity index 100% rename from tests/023_no_ext_with_mime.out rename to tests/023_no_ext_with_headers.out diff --git a/tests/023_no_ext_with_headers.test b/tests/023_no_ext_with_headers.test new file mode 100644 index 0000000000..5be189af0b --- /dev/null +++ b/tests/023_no_ext_with_headers.test @@ -0,0 +1,2 @@ +args: tests/023_no_ext_with_headers --reload +output: tests/023_no_ext_with_headers.out diff --git a/tests/023_no_ext_with_mime.mime b/tests/023_no_ext_with_mime.mime deleted file mode 100644 index d7482e1bc1..0000000000 --- a/tests/023_no_ext_with_mime.mime +++ /dev/null @@ -1 +0,0 @@ -application/javascript \ No newline at end of file diff --git a/tests/023_no_ext_with_mime.test b/tests/023_no_ext_with_mime.test deleted file mode 100644 index 5943f65262..0000000000 --- a/tests/023_no_ext_with_mime.test +++ /dev/null @@ -1,2 +0,0 @@ -args: tests/023_no_ext_with_mime --reload -output: tests/023_no_ext_with_mime.out diff --git a/tests/024_import_no_ext_with_headers.test b/tests/024_import_no_ext_with_headers.test new file mode 100644 index 0000000000..572158f12c --- /dev/null +++ b/tests/024_import_no_ext_with_headers.test @@ -0,0 +1,2 @@ +args: tests/024_import_no_ext_with_headers.ts --reload +output: tests/024_import_no_ext_with_headers.ts.out diff --git a/tests/024_import_no_ext_with_headers.ts b/tests/024_import_no_ext_with_headers.ts new file mode 100644 index 0000000000..c8621d0e66 --- /dev/null +++ b/tests/024_import_no_ext_with_headers.ts @@ -0,0 +1 @@ +import "./023_no_ext_with_headers"; diff --git a/tests/024_import_no_ext_with_mime.ts.out b/tests/024_import_no_ext_with_headers.ts.out similarity index 100% rename from tests/024_import_no_ext_with_mime.ts.out rename to tests/024_import_no_ext_with_headers.ts.out diff --git a/tests/024_import_no_ext_with_mime.test b/tests/024_import_no_ext_with_mime.test deleted file mode 100644 index 2ffe4810f0..0000000000 --- a/tests/024_import_no_ext_with_mime.test +++ /dev/null @@ -1,2 +0,0 @@ -args: tests/024_import_no_ext_with_mime.ts --reload -output: tests/024_import_no_ext_with_mime.ts.out diff --git a/tests/024_import_no_ext_with_mime.ts b/tests/024_import_no_ext_with_mime.ts deleted file mode 100644 index d5140de668..0000000000 --- a/tests/024_import_no_ext_with_mime.ts +++ /dev/null @@ -1 +0,0 @@ -import "./023_no_ext_with_mime"; diff --git a/tests/026_redirect_javascript.js b/tests/026_redirect_javascript.js new file mode 100644 index 0000000000..226a6b622c --- /dev/null +++ b/tests/026_redirect_javascript.js @@ -0,0 +1,2 @@ +import { value } from "http://localhost:4547/redirects/redirect3.js"; +console.log(value); diff --git a/tests/026_redirect_javascript.js.out b/tests/026_redirect_javascript.js.out new file mode 100644 index 0000000000..2908642993 --- /dev/null +++ b/tests/026_redirect_javascript.js.out @@ -0,0 +1 @@ +3 imports 1 diff --git a/tests/026_redirect_javascript.js.test b/tests/026_redirect_javascript.js.test new file mode 100644 index 0000000000..a66cb0ea07 --- /dev/null +++ b/tests/026_redirect_javascript.js.test @@ -0,0 +1,2 @@ +args: tests/026_redirect_javascript.js --reload +output: tests/026_redirect_javascript.js.out diff --git a/tests/027_redirect_typescript.ts b/tests/027_redirect_typescript.ts new file mode 100644 index 0000000000..584341975f --- /dev/null +++ b/tests/027_redirect_typescript.ts @@ -0,0 +1,2 @@ +import { value } from "http://localhost:4547/redirects/redirect4.ts"; +console.log(value); diff --git a/tests/027_redirect_typescript.ts.out b/tests/027_redirect_typescript.ts.out new file mode 100644 index 0000000000..480d4e8ca0 --- /dev/null +++ b/tests/027_redirect_typescript.ts.out @@ -0,0 +1 @@ +4 imports 1 diff --git a/tests/027_redirect_typescript.ts.test b/tests/027_redirect_typescript.ts.test new file mode 100644 index 0000000000..8abfbc6161 --- /dev/null +++ b/tests/027_redirect_typescript.ts.test @@ -0,0 +1,2 @@ +args: tests/027_redirect_typescript.ts --reload +output: tests/027_redirect_typescript.ts.out \ No newline at end of file diff --git a/tests/subdir/redirects/redirect1.js b/tests/subdir/redirects/redirect1.js new file mode 100644 index 0000000000..d674be88c3 --- /dev/null +++ b/tests/subdir/redirects/redirect1.js @@ -0,0 +1 @@ +export const redirect = 1; diff --git a/tests/subdir/redirects/redirect1.ts b/tests/subdir/redirects/redirect1.ts new file mode 100644 index 0000000000..d674be88c3 --- /dev/null +++ b/tests/subdir/redirects/redirect1.ts @@ -0,0 +1 @@ +export const redirect = 1; diff --git a/tests/subdir/redirects/redirect2.js b/tests/subdir/redirects/redirect2.js new file mode 100644 index 0000000000..e4244f6386 --- /dev/null +++ b/tests/subdir/redirects/redirect2.js @@ -0,0 +1 @@ +import "./redirect1.js"; diff --git a/tests/subdir/redirects/redirect3.js b/tests/subdir/redirects/redirect3.js new file mode 100644 index 0000000000..e24f2af32f --- /dev/null +++ b/tests/subdir/redirects/redirect3.js @@ -0,0 +1,2 @@ +import { redirect } from "./redirect1.js"; +export const value = `3 imports ${redirect}`; diff --git a/tests/subdir/redirects/redirect4.ts b/tests/subdir/redirects/redirect4.ts new file mode 100644 index 0000000000..f31ad886ea --- /dev/null +++ b/tests/subdir/redirects/redirect4.ts @@ -0,0 +1,2 @@ +import { redirect } from "./redirect1.ts"; +export const value: string = `4 imports ${redirect}`; diff --git a/tools/http_server.py b/tools/http_server.py index 4df0e7690d..e44a86fc7b 100755 --- a/tools/http_server.py +++ b/tools/http_server.py @@ -12,6 +12,8 @@ from time import sleep PORT = 4545 REDIRECT_PORT = 4546 +ANOTHER_REDIRECT_PORT = 4547 +DOUBLE_REDIRECTS_PORT = 4548 class ContentTypeHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): @@ -99,24 +101,42 @@ def server(): return s -def redirect_server(): +def base_redirect_server(host_port, target_port, extra_path_segment=""): os.chdir(root_path) - target_host = "http://localhost:%d" % PORT + target_host = "http://localhost:%d" % target_port class RedirectHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(301) - self.send_header('Location', target_host + self.path) + self.send_header('Location', + target_host + extra_path_segment + self.path) self.end_headers() Handler = RedirectHandler SocketServer.TCPServer.allow_reuse_address = True - s = SocketServer.TCPServer(("", REDIRECT_PORT), Handler) + s = SocketServer.TCPServer(("", host_port), Handler) print "redirect server http://localhost:%d/ -> http://localhost:%d/" % ( - REDIRECT_PORT, PORT) + host_port, target_port) return s +# redirect server +def redirect_server(): + return base_redirect_server(REDIRECT_PORT, PORT) + + +# another redirect server pointing to the same port as the one above +# BUT with an extra subdir path +def another_redirect_server(): + return base_redirect_server( + ANOTHER_REDIRECT_PORT, PORT, extra_path_segment="/tests/subdir") + + +# redirect server that points to another redirect server +def double_redirects_server(): + return base_redirect_server(DOUBLE_REDIRECTS_PORT, REDIRECT_PORT) + + def spawn(): # Main http server s = server() @@ -128,6 +148,16 @@ def spawn(): r_thread = Thread(target=rs.serve_forever) r_thread.daemon = True r_thread.start() + # Another redirect server + ars = another_redirect_server() + ar_thread = Thread(target=ars.serve_forever) + ar_thread.daemon = True + ar_thread.start() + # Double redirects server + drs = double_redirects_server() + dr_thread = Thread(target=drs.serve_forever) + dr_thread.daemon = True + dr_thread.start() sleep(1) # TODO I'm too lazy to figure out how to do this properly. return thread