diff options
Diffstat (limited to 'cli/tools/vendor/build.rs')
-rw-r--r-- | cli/tools/vendor/build.rs | 349 |
1 files changed, 335 insertions, 14 deletions
diff --git a/cli/tools/vendor/build.rs b/cli/tools/vendor/build.rs index dd362ebfb..ecb7db717 100644 --- a/cli/tools/vendor/build.rs +++ b/cli/tools/vendor/build.rs @@ -1,11 +1,17 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use std::path::Path; +use std::path::PathBuf; +use deno_ast::ModuleSpecifier; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_graph::Module; use deno_graph::ModuleGraph; use deno_graph::ModuleKind; +use import_map::ImportMap; +use import_map::SpecifierMap; use super::analyze::has_default_export; use super::import_map::build_import_map; @@ -15,29 +21,53 @@ use super::specifiers::is_remote_specifier; /// Allows substituting the environment for testing purposes. pub trait VendorEnvironment { + fn cwd(&self) -> Result<PathBuf, AnyError>; fn create_dir_all(&self, dir_path: &Path) -> Result<(), AnyError>; fn write_file(&self, file_path: &Path, text: &str) -> Result<(), AnyError>; + fn path_exists(&self, path: &Path) -> bool; } pub struct RealVendorEnvironment; impl VendorEnvironment for RealVendorEnvironment { + fn cwd(&self) -> Result<PathBuf, AnyError> { + Ok(std::env::current_dir()?) + } + fn create_dir_all(&self, dir_path: &Path) -> Result<(), AnyError> { Ok(std::fs::create_dir_all(dir_path)?) } fn write_file(&self, file_path: &Path, text: &str) -> Result<(), AnyError> { - Ok(std::fs::write(file_path, text)?) + std::fs::write(file_path, text) + .with_context(|| format!("Failed writing {}", file_path.display())) + } + + fn path_exists(&self, path: &Path) -> bool { + path.exists() } } /// Vendors remote modules and returns how many were vendored. pub fn build( - graph: &ModuleGraph, + graph: ModuleGraph, output_dir: &Path, + original_import_map: Option<&ImportMap>, environment: &impl VendorEnvironment, ) -> Result<usize, AnyError> { assert!(output_dir.is_absolute()); + let output_dir_specifier = + ModuleSpecifier::from_directory_path(output_dir).unwrap(); + + if let Some(original_im) = &original_import_map { + validate_original_import_map(original_im, &output_dir_specifier)?; + } + + // build the graph + graph.lock()?; + graph.valid()?; + + // figure out how to map remote modules to local let all_modules = graph.modules(); let remote_modules = all_modules .iter() @@ -45,7 +75,7 @@ pub fn build( .copied() .collect::<Vec<_>>(); let mappings = - Mappings::from_remote_modules(graph, &remote_modules, output_dir)?; + Mappings::from_remote_modules(&graph, &remote_modules, output_dir)?; // write out all the files for module in &remote_modules { @@ -77,16 +107,59 @@ pub fn build( environment.write_file(&proxy_path, &text)?; } - // create the import map - if !mappings.base_specifiers().is_empty() { - let import_map_text = build_import_map(graph, &all_modules, &mappings); - environment - .write_file(&output_dir.join("import_map.json"), &import_map_text)?; + // create the import map if necessary + if !remote_modules.is_empty() { + let import_map_path = output_dir.join("import_map.json"); + let import_map_text = build_import_map( + &output_dir_specifier, + &graph, + &all_modules, + &mappings, + original_import_map, + ); + environment.write_file(&import_map_path, &import_map_text)?; } Ok(remote_modules.len()) } +fn validate_original_import_map( + import_map: &ImportMap, + output_dir: &ModuleSpecifier, +) -> Result<(), AnyError> { + fn validate_imports( + imports: &SpecifierMap, + output_dir: &ModuleSpecifier, + ) -> Result<(), AnyError> { + for entry in imports.entries() { + if let Some(value) = entry.value { + if value.as_str().starts_with(output_dir.as_str()) { + bail!( + "Providing an existing import map with entries for the output directory is not supported (\"{}\": \"{}\").", + entry.raw_key, + entry.raw_value.unwrap_or("<INVALID>"), + ); + } + } + } + Ok(()) + } + + validate_imports(import_map.imports(), output_dir)?; + + for scope in import_map.scopes() { + if scope.key.starts_with(output_dir.as_str()) { + bail!( + "Providing an existing import map with a scope for the output directory is not supported (\"{}\").", + scope.raw_key, + ); + } + validate_imports(scope.imports, output_dir)?; + } + + Ok(()) +} + fn build_proxy_module_source( module: &Module, proxied_module: &ProxiedModule, @@ -171,9 +244,9 @@ mod test { output.import_map, Some(json!({ "imports": { - "https://localhost/": "./localhost/", "https://localhost/other.ts?test": "./localhost/other.ts", "https://localhost/redirect.ts": "./localhost/mod.ts", + "https://localhost/": "./localhost/", } })) ); @@ -241,7 +314,7 @@ mod test { "imports": { "https://localhost/": "./localhost/", "https://localhost/redirect.ts": "./localhost/other.ts", - "https://other/": "./other/" + "https://other/": "./other/", }, "scopes": { "./localhost/": { @@ -313,10 +386,10 @@ mod test { output.import_map, Some(json!({ "imports": { - "https://localhost/": "./localhost/", "https://localhost/mod.TS": "./localhost/mod_2.TS", "https://localhost/mod.ts": "./localhost/mod_3.ts", "https://localhost/mod.ts?test": "./localhost/mod_4.ts", + "https://localhost/": "./localhost/", } })) ); @@ -543,8 +616,8 @@ mod test { output.import_map, Some(json!({ "imports": { - "http://localhost:4545/": "./localhost_4545/", "http://localhost:4545/sub/logger/mod.ts?testing": "./localhost_4545/sub/logger/mod.ts", + "http://localhost:4545/": "./localhost_4545/", }, "scopes": { "./localhost_4545/": { @@ -599,9 +672,9 @@ mod test { output.import_map, Some(json!({ "imports": { + "https://localhost/std/hash/mod.ts": "./localhost/std@0.1.0/hash/mod.ts", "https://localhost/": "./localhost/", - "https://localhost/std/hash/mod.ts": "./localhost/std@0.1.0/hash/mod.ts" - } + }, })) ); assert_eq!( @@ -675,6 +748,254 @@ mod test { ); } + #[tokio::test] + async fn existing_import_map_basic() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map2.json"); + original_import_map + .imports_mut() + .append( + "https://localhost/mod.ts".to_string(), + "./local_vendor/mod.ts".to_string(), + ) + .unwrap(); + let local_vendor_scope = original_import_map + .get_or_append_scope_mut("./local_vendor/") + .unwrap(); + local_vendor_scope + .append( + "https://localhost/logger.ts".to_string(), + "./local_vendor/logger.ts".to_string(), + ) + .unwrap(); + local_vendor_scope + .append( + "/console_logger.ts".to_string(), + "./local_vendor/console_logger.ts".to_string(), + ) + .unwrap(); + + let output = builder + .with_loader(|loader| { + loader.add("/mod.ts", "import 'https://localhost/mod.ts'; import 'https://localhost/other.ts';"); + loader.add("/local_vendor/mod.ts", "import 'https://localhost/logger.ts'; import '/console_logger.ts'; console.log(5);"); + loader.add("/local_vendor/logger.ts", "export class Logger {}"); + loader.add("/local_vendor/console_logger.ts", "export class ConsoleLogger {}"); + loader.add("https://localhost/mod.ts", "console.log(6);"); + loader.add("https://localhost/other.ts", "import './mod.ts';"); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .unwrap(); + + assert_eq!( + output.import_map, + Some(json!({ + "imports": { + "https://localhost/mod.ts": "../local_vendor/mod.ts", + "https://localhost/": "./localhost/" + }, + "scopes": { + "../local_vendor/": { + "https://localhost/logger.ts": "../local_vendor/logger.ts", + "/console_logger.ts": "../local_vendor/console_logger.ts", + }, + "./localhost/": { + "./localhost/mod.ts": "../local_vendor/mod.ts", + }, + } + })) + ); + assert_eq!( + output.files, + to_file_vec(&[("/vendor/localhost/other.ts", "import './mod.ts';")]), + ); + } + + #[tokio::test] + async fn existing_import_map_mapped_bare_specifier() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map.json"); + let imports = original_import_map.imports_mut(); + imports + .append("$fresh".to_string(), "https://localhost/fresh".to_string()) + .unwrap(); + imports + .append("std/".to_string(), "https://deno.land/std/".to_string()) + .unwrap(); + let output = builder + .with_loader(|loader| { + loader.add("/mod.ts", "import 'std/mod.ts'; import '$fresh';"); + loader.add("https://deno.land/std/mod.ts", "export function test() {}"); + loader.add_with_headers( + "https://localhost/fresh", + "export function fresh() {}", + &[("content-type", "application/typescript")], + ); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .unwrap(); + + assert_eq!( + output.import_map, + Some(json!({ + "imports": { + "https://deno.land/": "./deno.land/", + "https://localhost/": "./localhost/", + "$fresh": "./localhost/fresh.ts", + "std/mod.ts": "./deno.land/std/mod.ts", + }, + })) + ); + assert_eq!( + output.files, + to_file_vec(&[ + ("/vendor/deno.land/std/mod.ts", "export function test() {}"), + ("/vendor/localhost/fresh.ts", "export function fresh() {}") + ]), + ); + } + + #[tokio::test] + async fn existing_import_map_remote_absolute_specifier_local() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map.json"); + original_import_map + .imports_mut() + .append( + "https://localhost/logger.ts?test".to_string(), + "./local/logger.ts".to_string(), + ) + .unwrap(); + + let output = builder + .with_loader(|loader| { + loader.add("/mod.ts", "import 'https://localhost/mod.ts'; import 'https://localhost/logger.ts?test';"); + loader.add("/local/logger.ts", "export class Logger {}"); + // absolute specifier in a remote module that will point at ./local/logger.ts + loader.add("https://localhost/mod.ts", "import '/logger.ts?test';"); + loader.add("https://localhost/logger.ts?test", "export class Logger {}"); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .unwrap(); + + assert_eq!( + output.import_map, + Some(json!({ + "imports": { + "https://localhost/logger.ts?test": "../local/logger.ts", + "https://localhost/": "./localhost/", + }, + "scopes": { + "./localhost/": { + "/logger.ts?test": "../local/logger.ts", + }, + } + })) + ); + assert_eq!( + output.files, + to_file_vec(&[("/vendor/localhost/mod.ts", "import '/logger.ts?test';")]), + ); + } + + #[tokio::test] + async fn existing_import_map_imports_output_dir() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map.json"); + original_import_map + .imports_mut() + .append( + "std/mod.ts".to_string(), + "./vendor/deno.land/std/mod.ts".to_string(), + ) + .unwrap(); + let err = builder + .with_loader(|loader| { + loader.add("/mod.ts", "import 'std/mod.ts';"); + loader.add("/vendor/deno.land/std/mod.ts", "export function f() {}"); + loader.add("https://deno.land/std/mod.ts", "export function f() {}"); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + concat!( + "Providing an existing import map with entries for the output ", + "directory is not supported ", + "(\"std/mod.ts\": \"./vendor/deno.land/std/mod.ts\").", + ) + ); + } + + #[tokio::test] + async fn existing_import_map_scopes_entry_output_dir() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map.json"); + let scopes = original_import_map + .get_or_append_scope_mut("./other/") + .unwrap(); + scopes + .append("/mod.ts".to_string(), "./vendor/mod.ts".to_string()) + .unwrap(); + let err = builder + .with_loader(|loader| { + loader.add("/mod.ts", "console.log(5);"); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + concat!( + "Providing an existing import map with entries for the output ", + "directory is not supported ", + "(\"/mod.ts\": \"./vendor/mod.ts\").", + ) + ); + } + + #[tokio::test] + async fn existing_import_map_scopes_key_output_dir() { + let mut builder = VendorTestBuilder::with_default_setup(); + let mut original_import_map = builder.new_import_map("/import_map.json"); + let scopes = original_import_map + .get_or_append_scope_mut("./vendor/") + .unwrap(); + scopes + .append("/mod.ts".to_string(), "./vendor/mod.ts".to_string()) + .unwrap(); + let err = builder + .with_loader(|loader| { + loader.add("/mod.ts", "console.log(5);"); + }) + .set_original_import_map(original_import_map.clone()) + .build() + .await + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + concat!( + "Providing an existing import map with a scope for the output ", + "directory is not supported (\"./vendor/\").", + ) + ); + } + fn to_file_vec(items: &[(&str, &str)]) -> Vec<(String, String)> { items .iter() |