summaryrefslogtreecommitdiff
path: root/cli/tools/vendor/build.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/vendor/build.rs')
-rw-r--r--cli/tools/vendor/build.rs349
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()