summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin (Kun) "Kassimo" Qian <kevinkassimo@gmail.com>2019-04-01 18:46:40 -0700
committerRyan Dahl <ry@tinyclouds.org>2019-04-01 21:46:40 -0400
commit534b8d30219a605c82bac706bc5429d44ae53c32 (patch)
tree2e653b1590fc237030d56f8535fa40d2dd923262
parente44084c90dec8cafbc66d4e485ff24479c56ec2e (diff)
Follow redirect location as new referrers for nested module imports (#2031)
Fixes #1742 Fixes #2021
-rw-r--r--cli/compiler.rs5
-rw-r--r--cli/deno_dir.rs761
-rw-r--r--cli/http_util.rs80
-rw-r--r--cli/isolate.rs29
-rw-r--r--cli/modules.rs68
-rw-r--r--tests/023_no_ext_with_headers (renamed from tests/023_no_ext_with_mime)0
-rw-r--r--tests/023_no_ext_with_headers.headers.json1
-rw-r--r--tests/023_no_ext_with_headers.out (renamed from tests/023_no_ext_with_mime.out)0
-rw-r--r--tests/023_no_ext_with_headers.test2
-rw-r--r--tests/023_no_ext_with_mime.mime1
-rw-r--r--tests/023_no_ext_with_mime.test2
-rw-r--r--tests/024_import_no_ext_with_headers.test2
-rw-r--r--tests/024_import_no_ext_with_headers.ts1
-rw-r--r--tests/024_import_no_ext_with_headers.ts.out (renamed from tests/024_import_no_ext_with_mime.ts.out)0
-rw-r--r--tests/024_import_no_ext_with_mime.test2
-rw-r--r--tests/024_import_no_ext_with_mime.ts1
-rw-r--r--tests/026_redirect_javascript.js2
-rw-r--r--tests/026_redirect_javascript.js.out1
-rw-r--r--tests/026_redirect_javascript.js.test2
-rw-r--r--tests/027_redirect_typescript.ts2
-rw-r--r--tests/027_redirect_typescript.ts.out1
-rw-r--r--tests/027_redirect_typescript.ts.test2
-rw-r--r--tests/subdir/redirects/redirect1.js1
-rw-r--r--tests/subdir/redirects/redirect1.ts1
-rw-r--r--tests/subdir/redirects/redirect2.js1
-rw-r--r--tests/subdir/redirects/redirect3.js2
-rw-r--r--tests/subdir/redirects/redirect4.ts2
-rwxr-xr-xtools/http_server.py40
28 files changed, 813 insertions, 199 deletions
diff --git a/cli/compiler.rs b/cli/compiler.rs
index bd0a763c1..6816df2ee 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<String>, // source of redirect
pub filename: String,
pub media_type: msg::MediaType,
pub source_code: Vec<u8>,
@@ -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 703df43ad..1d69046d3 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"),
- );
-
- 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());
+ // 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 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<ModuleMetaData> {
- 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<u8>) -> Vec<u8> {
/// 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<Item = Option<ModuleMetaData>, 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::<http::uri::Uri>().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<Option<ModuleMetaData>> {
- 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<String>,
) -> DenoResult<Option<ModuleMetaData>> {
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<String> -> 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<String>,
+ /// Where should we actually look for source code.
+ /// This should be an absolute path!
+ pub redirect_to: Option<String>,
+}
+
+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::Value> =
+ 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<String>,
+ redirect_to: Option<String>,
+) {
+ 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,
@@ -746,6 +966,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();
// http_util::fetch_sync_string requires tokio
@@ -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,52 +1084,174 @@ 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;
// http_util::fetch_sync_string requires tokio
@@ -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 9c92c08eb..d04f18b1e 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<String>),
+ 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<Item = FetchOnceResult, Error = DenoError> {
+ type FetchAttempt = (Option<String>, Option<String>, Option<FetchOnceResult>);
+ let client = get_client();
+ client
+ .get(url.clone())
+ .map_err(DenoError::from)
+ .and_then(move |response| -> Box<dyn Future<Item = FetchAttempt, Error = DenoError> + 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::<Uri>().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 ced1cb792..9dcf6e8f0 100644
--- a/cli/isolate.rs
+++ b/cli/isolate.rs
@@ -84,6 +84,15 @@ impl<B: DenoBehavior> Isolate<B> {
&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<B: DenoBehavior> Isolate<B> {
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<B: DenoBehavior> Isolate<B> {
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 e2ded67b2..f40f5ca08 100644
--- a/cli/modules.rs
+++ b/cli/modules.rs
@@ -12,23 +12,78 @@ pub struct ModuleInfo {
children: Vec<deno_mod>,
}
+/// 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<String, SymbolicModule>,
+}
+
+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<deno_mod> {
+ 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<deno_mod, ModuleInfo>,
- pub by_name: HashMap<String, deno_mod>,
+ 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<deno_mod> {
- self.by_name.get(name).cloned()
+ self.by_name.get(name)
}
pub fn get_children(&self, id: deno_mod) -> Option<&Vec<deno_mod>> {
@@ -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
index 87951d835..87951d835 100644
--- a/tests/023_no_ext_with_mime
+++ b/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 000000000..5b6f09aeb
--- /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
index e427984d4..e427984d4 100644
--- a/tests/023_no_ext_with_mime.out
+++ b/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 000000000..5be189af0
--- /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 d7482e1bc..000000000
--- 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 5943f6526..000000000
--- 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 000000000..572158f12
--- /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 000000000..c8621d0e6
--- /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
index e427984d4..e427984d4 100644
--- a/tests/024_import_no_ext_with_mime.ts.out
+++ b/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 2ffe4810f..000000000
--- 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 d5140de66..000000000
--- 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 000000000..226a6b622
--- /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 000000000..290864299
--- /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 000000000..a66cb0ea0
--- /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 000000000..584341975
--- /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 000000000..480d4e8ca
--- /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 000000000..8abfbc616
--- /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 000000000..d674be88c
--- /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 000000000..d674be88c
--- /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 000000000..e4244f638
--- /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 000000000..e24f2af32
--- /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 000000000..f31ad886e
--- /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 4df0e7690..e44a86fc7 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