diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/disk_cache.rs | 2 | ||||
-rw-r--r-- | cli/file_fetcher.rs | 182 | ||||
-rw-r--r-- | cli/http_cache.rs | 9 | ||||
-rw-r--r-- | cli/program_state.rs | 2 | ||||
-rw-r--r-- | cli/specifier_handler.rs | 2 | ||||
-rw-r--r-- | cli/tests/import_data_url.ts | 12 | ||||
-rw-r--r-- | cli/tests/import_data_url.ts.out | 3 | ||||
-rw-r--r-- | cli/tests/import_data_url_error_stack.ts | 3 | ||||
-rw-r--r-- | cli/tests/import_data_url_error_stack.ts.out | 5 | ||||
-rw-r--r-- | cli/tests/import_data_url_import_relative.ts | 4 | ||||
-rw-r--r-- | cli/tests/import_data_url_import_relative.ts.out | 1 | ||||
-rw-r--r-- | cli/tests/import_data_url_imports.ts | 4 | ||||
-rw-r--r-- | cli/tests/import_data_url_imports.ts.out | 1 | ||||
-rw-r--r-- | cli/tests/import_data_url_jsx.ts | 10 | ||||
-rw-r--r-- | cli/tests/import_data_url_jsx.ts.out | 1 | ||||
-rw-r--r-- | cli/tests/import_dynamic_data_url.ts | 14 | ||||
-rw-r--r-- | cli/tests/import_dynamic_data_url.ts.out | 3 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 33 | ||||
-rw-r--r-- | cli/tests/unsupported_dynamic_import_scheme.out | 3 | ||||
-rw-r--r-- | cli/tests/workers_test.ts | 21 | ||||
-rw-r--r-- | cli/tsc.rs | 95 |
21 files changed, 367 insertions, 43 deletions
diff --git a/cli/disk_cache.rs b/cli/disk_cache.rs index 3cc3b16f3..51844d24e 100644 --- a/cli/disk_cache.rs +++ b/cli/disk_cache.rs @@ -67,7 +67,7 @@ impl DiskCache { out.push(path_seg); } } - "http" | "https" => out = url_to_filename(url), + "http" | "https" | "data" => out = url_to_filename(url), "file" => { let path = match url.to_file_path() { Ok(path) => path, diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index f10574c2d..5a31ee6cc 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -27,7 +27,7 @@ use std::pin::Pin; use std::sync::Arc; use std::sync::Mutex; -pub const SUPPORTED_SCHEMES: [&str; 3] = ["http", "https", "file"]; +pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"]; /// A structure representing a source file. #[derive(Debug, Clone, Eq, PartialEq)] @@ -145,6 +145,41 @@ pub fn get_source_from_bytes( Ok(source) } +fn get_source_from_data_url( + specifier: &ModuleSpecifier, +) -> Result<(String, MediaType, String), AnyError> { + let url = specifier.as_url(); + if url.scheme() != "data" { + return Err(custom_error( + "BadScheme", + format!("Unexpected scheme of \"{}\"", url.scheme()), + )); + } + let path = url.path(); + let mut parts = path.splitn(2, ','); + let media_type_part = + percent_encoding::percent_decode_str(parts.next().unwrap()) + .decode_utf8()?; + let data_part = if let Some(data) = parts.next() { + data + } else { + return Err(custom_error( + "BadUrl", + "The data URL is badly formed, missing a comma.", + )); + }; + let (media_type, maybe_charset) = + map_content_type(specifier, Some(media_type_part.to_string())); + let is_base64 = media_type_part.rsplit(';').any(|p| p == "base64"); + let bytes = if is_base64 { + base64::decode(data_part)? + } else { + percent_encoding::percent_decode_str(data_part).collect() + }; + let source = strip_shebang(get_source_from_bytes(bytes, maybe_charset)?); + Ok((source, media_type, media_type_part.to_string())) +} + /// Return a validated scheme for a given module specifier. fn get_validated_scheme( specifier: &ModuleSpecifier, @@ -185,6 +220,8 @@ pub fn map_content_type( | "application/node" => { map_js_like_extension(specifier, MediaType::JavaScript) } + "text/jsx" => MediaType::JSX, + "text/tsx" => MediaType::TSX, "application/json" | "text/json" => MediaType::Json, "application/wasm" => MediaType::Wasm, // Handle plain and possibly webassembly @@ -354,6 +391,47 @@ impl FileFetcher { Ok(Some(file)) } + /// Convert a data URL into a file, resulting in an error if the URL is + /// invalid. + fn fetch_data_url( + &self, + specifier: &ModuleSpecifier, + ) -> Result<File, AnyError> { + debug!("FileFetcher::fetch_data_url() - specifier: {}", specifier); + match self.fetch_cached(specifier, 0) { + Ok(Some(file)) => return Ok(file), + Ok(None) => {} + Err(err) => return Err(err), + } + + if self.cache_setting == CacheSetting::Only { + return Err(custom_error( + "NotFound", + format!( + "Specifier not found in cache: \"{}\", --cached-only is specified.", + specifier + ), + )); + } + + let (source, media_type, content_type) = + get_source_from_data_url(specifier)?; + let local = self.http_cache.get_cache_filename(specifier.as_url()); + let mut headers = HashMap::new(); + headers.insert("content-type".to_string(), content_type); + self + .http_cache + .set(specifier.as_url(), headers, source.as_bytes())?; + + Ok(File { + local, + maybe_types: None, + media_type, + source, + specifier: specifier.clone(), + }) + } + /// Asynchronously fetch remote source file specified by the URL following /// redirects. /// @@ -450,26 +528,27 @@ impl FileFetcher { permissions.check_specifier(specifier)?; if let Some(file) = self.cache.get(specifier) { Ok(file) + } else if scheme == "file" { + // we do not in memory cache files, as this would prevent files on the + // disk changing effecting things like workers and dynamic imports. + fetch_local(specifier) + } else if scheme == "data" { + let result = self.fetch_data_url(specifier); + if let Ok(file) = &result { + self.cache.insert(specifier.clone(), file.clone()); + } + result + } else if !self.allow_remote { + Err(custom_error( + "NoRemote", + format!("A remote specifier was requested: \"{}\", but --no-remote is specified.", specifier), + )) } else { - let is_local = scheme == "file"; - if is_local { - fetch_local(specifier) - } else if !self.allow_remote { - Err(custom_error( - "NoRemote", - format!("A remote specifier was requested: \"{}\", but --no-remote is specified.", specifier), - )) - } else { - let result = self.fetch_remote(specifier, permissions, 10).await; - // only cache remote resources, as they are the only things that would - // be "expensive" to fetch multiple times during an invocation, and it - // also allows local file sources to be changed, enabling things like - // dynamic import and workers to be updated while Deno is running. - if let Ok(file) = &result { - self.cache.insert(specifier.clone(), file.clone()); - } - result + let result = self.fetch_remote(specifier, permissions, 10).await; + if let Ok(file) = &result { + self.cache.insert(specifier.clone(), file.clone()); } + result } } @@ -582,12 +661,46 @@ mod tests { } #[test] + fn test_get_source_from_data_url() { + let fixtures = vec![ + ("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", true, MediaType::TypeScript, "application/typescript;base64", "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"), + ("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=?a=b&b=c", true, MediaType::TypeScript, "application/typescript;base64", "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"), + ("data:text/plain,Hello%2C%20Deno!", true, MediaType::Unknown, "text/plain", "Hello, Deno!"), + ("data:,Hello%2C%20Deno!", true, MediaType::Unknown, "", "Hello, Deno!"), + ("data:application/javascript,console.log(\"Hello, Deno!\");%0A", true, MediaType::JavaScript, "application/javascript", "console.log(\"Hello, Deno!\");\n"), + ("data:text/jsx;base64,ZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24oKSB7CiAgcmV0dXJuIDxkaXY+SGVsbG8gRGVubyE8L2Rpdj4KfQo=", true, MediaType::JSX, "text/jsx;base64", "export default function() {\n return <div>Hello Deno!</div>\n}\n"), + ("data:text/tsx;base64,ZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24oKSB7CiAgcmV0dXJuIDxkaXY+SGVsbG8gRGVubyE8L2Rpdj4KfQo=", true, MediaType::TSX, "text/tsx;base64", "export default function() {\n return <div>Hello Deno!</div>\n}\n"), + ]; + + for ( + url_str, + expected_ok, + expected_media_type, + expected_media_type_str, + expected, + ) in fixtures + { + let specifier = ModuleSpecifier::resolve_url(url_str).unwrap(); + let actual = get_source_from_data_url(&specifier); + assert_eq!(actual.is_ok(), expected_ok); + if expected_ok { + let (actual, actual_media_type, actual_media_type_str) = + actual.unwrap(); + assert_eq!(actual, expected); + assert_eq!(actual_media_type, expected_media_type); + assert_eq!(actual_media_type_str, expected_media_type_str); + } + } + } + + #[test] fn test_get_validated_scheme() { let fixtures = vec![ ("https://deno.land/x/mod.ts", true, "https"), ("http://deno.land/x/mod.ts", true, "http"), ("file:///a/b/c.ts", true, "file"), ("file:///C:/a/b/c.ts", true, "file"), + ("data:,some%20text", true, "data"), ("ftp://a/b/c.ts", false, ""), ("mailto:dino@deno.land", false, ""), ]; @@ -692,6 +805,18 @@ mod tests { ), ( "https://deno.land/x/mod", + Some("text/jsx".to_string()), + MediaType::JSX, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/tsx".to_string()), + MediaType::TSX, + None, + ), + ( + "https://deno.land/x/mod", Some("text/json".to_string()), MediaType::Json, None, @@ -828,6 +953,25 @@ mod tests { } #[tokio::test] + async fn test_fetch_data_url() { + let (file_fetcher, _) = setup(CacheSetting::Use, None); + let specifier = ModuleSpecifier::resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); + + let result = file_fetcher + .fetch(&specifier, &Permissions::allow_all()) + .await; + assert!(result.is_ok()); + let file = result.unwrap(); + assert_eq!( + file.source, + "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n" + ); + assert_eq!(file.media_type, MediaType::TypeScript); + assert_eq!(file.maybe_types, None); + assert_eq!(file.specifier, specifier); + } + + #[tokio::test] async fn test_fetch_complex() { let _http_server_guard = test_util::http_server(); let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None); diff --git a/cli/http_cache.rs b/cli/http_cache.rs index 4677b44c9..7a27cf3f8 100644 --- a/cli/http_cache.rs +++ b/cli/http_cache.rs @@ -38,6 +38,7 @@ fn base_url_to_filename(url: &Url) -> PathBuf { }; out.push(host_port); } + "data" => (), scheme => { unimplemented!( "Don't know how to create cache name for scheme: {}", @@ -253,6 +254,14 @@ mod tests { "https://deno.land/?asdf=qwer#qwer", "https/deno.land/e4edd1f433165141015db6a823094e6bd8f24dd16fe33f2abd99d34a0a21a3c0", ), + ( + "data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", + "data/c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37", + ), + ( + "data:text/plain,Hello%2C%20Deno!", + "data/967374e3561d6741234131e342bf5c6848b70b13758adfe23ee1a813a8131818", + ) ]; for (url, expected) in test_cases.iter() { diff --git a/cli/program_state.rs b/cli/program_state.rs index dfe8aa6c3..896f4d7b4 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -252,7 +252,7 @@ impl ProgramState { match url.scheme() { // we should only be looking for emits for schemes that denote external // modules, which the disk_cache supports - "wasm" | "file" | "http" | "https" => (), + "wasm" | "file" | "http" | "https" | "data" => (), _ => { return None; } diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 25ee915f9..74aa21391 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -305,7 +305,7 @@ impl SpecifierHandler for FetchHandler { } })?; let url = source_file.specifier.as_url(); - let is_remote = url.scheme() != "file"; + let is_remote = !(url.scheme() == "file" || url.scheme() == "data"); let filename = disk_cache.get_cache_filename_with_extension(url, "meta"); let maybe_version = if let Some(filename) = filename { if let Ok(bytes) = disk_cache.get(&filename) { diff --git a/cli/tests/import_data_url.ts b/cli/tests/import_data_url.ts new file mode 100644 index 000000000..258514a5e --- /dev/null +++ b/cli/tests/import_data_url.ts @@ -0,0 +1,12 @@ +// export const a = "a"; + +// export enum A { +// A, +// B, +// C, +// } +import * as a from "data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo="; + +console.log(a.a); +console.log(a.A); +console.log(a.A.A); diff --git a/cli/tests/import_data_url.ts.out b/cli/tests/import_data_url.ts.out new file mode 100644 index 000000000..bfa0b9d94 --- /dev/null +++ b/cli/tests/import_data_url.ts.out @@ -0,0 +1,3 @@ +a +{ "0": "A", "1": "B", "2": "C", A: 0, B: 1, C: 2 } +0 diff --git a/cli/tests/import_data_url_error_stack.ts b/cli/tests/import_data_url_error_stack.ts new file mode 100644 index 000000000..022e49fe1 --- /dev/null +++ b/cli/tests/import_data_url_error_stack.ts @@ -0,0 +1,3 @@ +import { a } from "data:application/typescript;base64,ZW51bSBBIHsKICBBLAogIEIsCiAgQywKIH0KIAogZXhwb3J0IGZ1bmN0aW9uIGEoKSB7CiAgIHRocm93IG5ldyBFcnJvcihgSGVsbG8gJHtBLkN9YCk7CiB9CiA="; + +a(); diff --git a/cli/tests/import_data_url_error_stack.ts.out b/cli/tests/import_data_url_error_stack.ts.out new file mode 100644 index 000000000..ca8b906da --- /dev/null +++ b/cli/tests/import_data_url_error_stack.ts.out @@ -0,0 +1,5 @@ +error: Uncaught Error: Hello 2 + throw new Error(`Hello ${A.C}`); + ^ + at a (72554b3efdc211ba4aa0b62629589f048e7d4afe7b0576f35ff340ce0ea8f9b8.ts:8:10) + at import_data_url_error_stack.ts:3:1 diff --git a/cli/tests/import_data_url_import_relative.ts b/cli/tests/import_data_url_import_relative.ts new file mode 100644 index 000000000..23947fe60 --- /dev/null +++ b/cli/tests/import_data_url_import_relative.ts @@ -0,0 +1,4 @@ +// export { a } from "./a.ts"; +import * as a from "data:application/javascript;base64,ZXhwb3J0IHsgYSB9IGZyb20gIi4vYS50cyI7Cg=="; + +console.log(a); diff --git a/cli/tests/import_data_url_import_relative.ts.out b/cli/tests/import_data_url_import_relative.ts.out new file mode 100644 index 000000000..5123f2fe4 --- /dev/null +++ b/cli/tests/import_data_url_import_relative.ts.out @@ -0,0 +1 @@ +error: invalid URL: relative URL with a cannot-be-a-base base diff --git a/cli/tests/import_data_url_imports.ts b/cli/tests/import_data_url_imports.ts new file mode 100644 index 000000000..f25c2c389 --- /dev/null +++ b/cli/tests/import_data_url_imports.ts @@ -0,0 +1,4 @@ +// export { printHello } from "http://localhost:4545/cli/tests/subdir/mod2.ts"; +import { printHello } from "data:application/typescript;base64,ZXhwb3J0IHsgcHJpbnRIZWxsbyB9IGZyb20gImh0dHA6Ly9sb2NhbGhvc3Q6NDU0NS9jbGkvdGVzdHMvc3ViZGlyL21vZDIudHMiOwo="; + +printHello(); diff --git a/cli/tests/import_data_url_imports.ts.out b/cli/tests/import_data_url_imports.ts.out new file mode 100644 index 000000000..e965047ad --- /dev/null +++ b/cli/tests/import_data_url_imports.ts.out @@ -0,0 +1 @@ +Hello diff --git a/cli/tests/import_data_url_jsx.ts b/cli/tests/import_data_url_jsx.ts new file mode 100644 index 000000000..1881211f9 --- /dev/null +++ b/cli/tests/import_data_url_jsx.ts @@ -0,0 +1,10 @@ +import render from "data:text/jsx;base64,ZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24oKSB7CiAgcmV0dXJuIDxkaXY+SGVsbG8gRGVubyE8L2Rpdj4KfQo="; + +// deno-lint-ignore no-explicit-any +(globalThis as any).React = { + createElement(...args: unknown[]) { + console.log(...args); + }, +}; + +render(); diff --git a/cli/tests/import_data_url_jsx.ts.out b/cli/tests/import_data_url_jsx.ts.out new file mode 100644 index 000000000..c1c85f250 --- /dev/null +++ b/cli/tests/import_data_url_jsx.ts.out @@ -0,0 +1 @@ +div null Hello Deno! diff --git a/cli/tests/import_dynamic_data_url.ts b/cli/tests/import_dynamic_data_url.ts new file mode 100644 index 000000000..53a0fbcd3 --- /dev/null +++ b/cli/tests/import_dynamic_data_url.ts @@ -0,0 +1,14 @@ +// export const a = "a"; + +// export enum A { +// A, +// B, +// C, +// } +const a = await import( + "data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=" +); + +console.log(a.a); +console.log(a.A); +console.log(a.A.A); diff --git a/cli/tests/import_dynamic_data_url.ts.out b/cli/tests/import_dynamic_data_url.ts.out new file mode 100644 index 000000000..bfa0b9d94 --- /dev/null +++ b/cli/tests/import_dynamic_data_url.ts.out @@ -0,0 +1,3 @@ +a +{ "0": "A", "1": "B", "2": "C", A: 0, B: 1, C: 2 } +0 diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index f4081a7ea..624f17df1 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -3412,6 +3412,39 @@ itest!(deno_doc_import_map { output: "doc/use_import_map.out", }); +itest!(import_data_url_error_stack { + args: "run --quiet --reload import_data_url_error_stack.ts", + output: "import_data_url_error_stack.ts.out", + exit_code: 1, +}); + +itest!(import_data_url_import_relative { + args: "run --quiet --reload import_data_url_import_relative.ts", + output: "import_data_url_import_relative.ts.out", + exit_code: 1, +}); + +itest!(import_data_url_imports { + args: "run --quiet --reload import_data_url_imports.ts", + output: "import_data_url_imports.ts.out", + http_server: true, +}); + +itest!(import_data_url_jsx { + args: "run --quiet --reload import_data_url_jsx.ts", + output: "import_data_url_jsx.ts.out", +}); + +itest!(import_data_url { + args: "run --quiet --reload import_data_url.ts", + output: "import_data_url.ts.out", +}); + +itest!(import_dynamic_data_url { + args: "run --quiet --reload import_dynamic_data_url.ts", + output: "import_dynamic_data_url.ts.out", +}); + itest!(import_file_with_colon { args: "run --quiet --reload import_file_with_colon.ts", output: "import_file_with_colon.ts.out", diff --git a/cli/tests/unsupported_dynamic_import_scheme.out b/cli/tests/unsupported_dynamic_import_scheme.out index 434f14c4c..9160f935f 100644 --- a/cli/tests/unsupported_dynamic_import_scheme.out +++ b/cli/tests/unsupported_dynamic_import_scheme.out @@ -1,5 +1,6 @@ error: Uncaught (in promise) TypeError: Unsupported scheme "xxx" for module "xxx:". Supported schemes: [ + "data", + "file", "http", "https", - "file", ] diff --git a/cli/tests/workers_test.ts b/cli/tests/workers_test.ts index d907c97a9..3bfe0181a 100644 --- a/cli/tests/workers_test.ts +++ b/cli/tests/workers_test.ts @@ -47,6 +47,27 @@ Deno.test({ }); Deno.test({ + name: "worker from data url", + async fn() { + const promise = deferred(); + const tsWorker = new Worker( + "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", + { type: "module", name: "tsWorker" }, + ); + + tsWorker.onmessage = (e): void => { + assertEquals(e.data, "Hello World"); + promise.resolve(); + }; + + tsWorker.postMessage("Hello World"); + + await promise; + tsWorker.terminate(); + }, +}); + +Deno.test({ name: "worker nested", fn: async function (): Promise<void> { const promise = deferred(); diff --git a/cli/tsc.rs b/cli/tsc.rs index d63667876..29ea45140 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -82,6 +82,19 @@ fn get_maybe_hash( } } +fn hash_data_url( + specifier: &ModuleSpecifier, + media_type: &MediaType, +) -> String { + assert_eq!( + specifier.as_url().scheme(), + "data", + "Specifier must be a data: specifier." + ); + let hash = crate::checksum::gen(&[specifier.as_url().path().as_bytes()]); + format!("data:///{}{}", hash, media_type.as_ts_extension()) +} + /// tsc only supports `.ts`, `.tsx`, `.d.ts`, `.js`, or `.jsx` as root modules /// and so we have to detect the apparent media type based on extensions it /// supports. @@ -152,7 +165,9 @@ pub struct Response { pub stats: Stats, } +#[derive(Debug)] struct State { + data_url_map: HashMap<String, ModuleSpecifier>, hash_data: Vec<Vec<u8>>, emitted_files: Vec<EmittedFile>, graph: Arc<Mutex<Graph>>, @@ -167,10 +182,12 @@ impl State { hash_data: Vec<Vec<u8>>, maybe_tsbuildinfo: Option<String>, root_map: HashMap<String, ModuleSpecifier>, + data_url_map: HashMap<String, ModuleSpecifier>, ) -> Self { State { + data_url_map, hash_data, - emitted_files: Vec::new(), + emitted_files: Default::default(), graph, maybe_tsbuildinfo, maybe_response: None, @@ -231,7 +248,9 @@ fn emit(state: &mut State, args: Value) -> Result<Value, AnyError> { let specifiers = specifiers .iter() .map(|s| { - if let Some(remapped_specifier) = state.root_map.get(s) { + if let Some(data_specifier) = state.data_url_map.get(s) { + data_specifier.clone() + } else if let Some(remapped_specifier) = state.root_map.get(s) { remapped_specifier.clone() } else { ModuleSpecifier::resolve_url_or_path(s).unwrap() @@ -278,12 +297,15 @@ fn load(state: &mut State, args: Value) -> Result<Value, AnyError> { maybe_source } else { let graph = state.graph.lock().unwrap(); - let specifier = - if let Some(remapped_specifier) = state.root_map.get(&v.specifier) { - remapped_specifier.clone() - } else { - specifier - }; + let specifier = if let Some(data_specifier) = + state.data_url_map.get(&v.specifier) + { + data_specifier.clone() + } else if let Some(remapped_specifier) = state.root_map.get(&v.specifier) { + remapped_specifier.clone() + } else { + specifier + }; let maybe_source = graph.get_source(&specifier); media_type = if let Some(media_type) = graph.get_media_type(&specifier) { media_type @@ -313,7 +335,9 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { let v: ResolveArgs = serde_json::from_value(args) .context("Invalid request from JavaScript for \"op_resolve\".")?; let mut resolved: Vec<(String, String)> = Vec::new(); - let referrer = if let Some(remapped_base) = state.root_map.get(&v.base) { + let referrer = if let Some(data_specifier) = state.data_url_map.get(&v.base) { + data_specifier.clone() + } else if let Some(remapped_base) = state.root_map.get(&v.base) { remapped_base.clone() } else { ModuleSpecifier::resolve_url_or_path(&v.base).context( @@ -340,10 +364,18 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { resolved_specifier ) }; - resolved.push(( - resolved_specifier.to_string(), - media_type.as_ts_extension(), - )); + let resolved_specifier_str = if resolved_specifier.as_url().scheme() + == "data" + { + let specifier_str = hash_data_url(&resolved_specifier, &media_type); + state + .data_url_map + .insert(specifier_str.clone(), resolved_specifier); + specifier_str + } else { + resolved_specifier.to_string() + }; + resolved.push((resolved_specifier_str, media_type.as_ts_extension())); } // in certain situations, like certain dynamic imports, we won't have // the source file in the graph, so we will return a fake module to @@ -384,17 +416,24 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { // extensions and remap any that are unacceptable to tsc and add them to the // op state so when requested, we can remap to the original specifier. let mut root_map = HashMap::new(); + let mut data_url_map = HashMap::new(); let root_names: Vec<String> = request .root_names .iter() .map(|(s, mt)| { - let ext_media_type = get_tsc_media_type(s); - if mt != &ext_media_type { - let new_specifier = format!("{}{}", s, mt.as_ts_extension()); - root_map.insert(new_specifier.clone(), s.clone()); - new_specifier + if s.as_url().scheme() == "data" { + let specifier_str = hash_data_url(s, mt); + data_url_map.insert(specifier_str.clone(), s.clone()); + specifier_str } else { - s.as_str().to_owned() + let ext_media_type = get_tsc_media_type(s); + if mt != &ext_media_type { + let new_specifier = format!("{}{}", s, mt.as_ts_extension()); + root_map.insert(new_specifier.clone(), s.clone()); + new_specifier + } else { + s.as_str().to_owned() + } } }) .collect(); @@ -407,6 +446,7 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { request.hash_data.clone(), request.maybe_tsbuildinfo.clone(), root_map, + data_url_map, )); } @@ -484,7 +524,13 @@ mod tests { .await .expect("module not inserted"); let graph = Arc::new(Mutex::new(builder.get_graph())); - State::new(graph, hash_data, maybe_tsbuildinfo, HashMap::new()) + State::new( + graph, + hash_data, + maybe_tsbuildinfo, + HashMap::new(), + HashMap::new(), + ) } async fn test_exec( @@ -560,6 +606,15 @@ mod tests { } #[test] + fn test_hash_data_url() { + let specifier = ModuleSpecifier::resolve_url( + "data:application/javascript,console.log(\"Hello%20Deno\");", + ) + .unwrap(); + assert_eq!(hash_data_url(&specifier, &MediaType::JavaScript), "data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"); + } + + #[test] fn test_get_tsc_media_type() { let fixtures = vec![ ("file:///a.ts", MediaType::TypeScript), |