diff options
Diffstat (limited to 'cli/tools/vendor/mod.rs')
-rw-r--r-- | cli/tools/vendor/mod.rs | 336 |
1 files changed, 260 insertions, 76 deletions
diff --git a/cli/tools/vendor/mod.rs b/cli/tools/vendor/mod.rs index d478c2b57..5690f5b22 100644 --- a/cli/tools/vendor/mod.rs +++ b/cli/tools/vendor/mod.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use std::sync::Arc; use deno_ast::ModuleSpecifier; +use deno_ast::TextChange; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; @@ -12,6 +13,7 @@ use deno_core::resolve_url_or_path; use log::warn; use crate::args::CliOptions; +use crate::args::ConfigFile; use crate::args::Flags; use crate::args::FmtOptionsConfig; use crate::args::VendorFlags; @@ -51,6 +53,9 @@ pub async fn vendor( cli_options.initial_cwd(), ) .await?; + let npm_package_count = graph.npm_packages.len(); + let try_add_node_modules_dir = npm_package_count > 0 + && cli_options.node_modules_dir_enablement().unwrap_or(true); let vendored_count = build::build( graph, factory.parsed_source_cache()?, @@ -70,9 +75,48 @@ pub async fn vendor( }, raw_output_dir.display(), ); + + let try_add_import_map = vendored_count > 0; + let modified_result = maybe_update_config_file( + &output_dir, + cli_options, + try_add_import_map, + try_add_node_modules_dir, + ); + + // cache the node_modules folder when it's been added to the config file + if modified_result.added_node_modules_dir { + let node_modules_path = cli_options.node_modules_dir_path().or_else(|| { + cli_options + .maybe_config_file_specifier() + .filter(|c| c.scheme() == "file") + .and_then(|c| c.to_file_path().ok()) + .map(|config_path| config_path.parent().unwrap().join("node_modules")) + }); + if let Some(node_modules_path) = node_modules_path { + factory + .create_node_modules_npm_fs_resolver(node_modules_path) + .await? + .cache_packages() + .await?; + } + log::info!( + concat!( + "Vendored {} npm {} into node_modules directory. Set `nodeModulesDir: false` ", + "in the Deno configuration file to disable vendoring npm packages in the future.", + ), + npm_package_count, + if npm_package_count == 1 { + "package" + } else { + "packages" + }, + ); + } + if vendored_count > 0 { let import_map_path = raw_output_dir.join("import_map.json"); - if maybe_update_config_file(&output_dir, cli_options) { + if modified_result.updated_import_map { log::info!( concat!( "\nUpdated your local Deno configuration file with a reference to the ", @@ -154,107 +198,156 @@ fn validate_options( Ok(()) } -fn maybe_update_config_file(output_dir: &Path, options: &CliOptions) -> bool { +fn maybe_update_config_file( + output_dir: &Path, + options: &CliOptions, + try_add_import_map: bool, + try_add_node_modules_dir: bool, +) -> ModifiedResult { assert!(output_dir.is_absolute()); - let config_file_specifier = match options.maybe_config_file_specifier() { - Some(f) => f, - None => return false, + let config_file = match options.maybe_config_file() { + Some(config_file) => config_file, + None => return ModifiedResult::default(), }; + if config_file.specifier.scheme() != "file" { + return ModifiedResult::default(); + } - let fmt_config = options - .maybe_config_file() - .as_ref() - .and_then(|config| config.to_fmt_config().ok()) + let fmt_config = config_file + .to_fmt_config() + .ok() .unwrap_or_default() .unwrap_or_default(); let result = update_config_file( - &config_file_specifier, - &ModuleSpecifier::from_file_path(output_dir.join("import_map.json")) - .unwrap(), + config_file, &fmt_config.options, + if try_add_import_map { + Some( + ModuleSpecifier::from_file_path(output_dir.join("import_map.json")) + .unwrap(), + ) + } else { + None + }, + try_add_node_modules_dir, ); match result { - Ok(()) => true, + Ok(modified_result) => modified_result, Err(err) => { warn!("Error updating config file. {:#}", err); - false + ModifiedResult::default() } } } fn update_config_file( - config_specifier: &ModuleSpecifier, - import_map_specifier: &ModuleSpecifier, + config_file: &ConfigFile, fmt_options: &FmtOptionsConfig, -) -> Result<(), AnyError> { - if config_specifier.scheme() != "file" { - return Ok(()); - } - - let config_path = specifier_to_file_path(config_specifier)?; + import_map_specifier: Option<ModuleSpecifier>, + try_add_node_modules_dir: bool, +) -> Result<ModifiedResult, AnyError> { + let config_path = specifier_to_file_path(&config_file.specifier)?; let config_text = std::fs::read_to_string(&config_path)?; - let relative_text = - match relative_specifier(config_specifier, import_map_specifier) { - Some(text) => text, - None => return Ok(()), // ignore - }; - if let Some(new_text) = - update_config_text(&config_text, &relative_text, fmt_options) - { + let import_map_specifier = + import_map_specifier.and_then(|import_map_specifier| { + relative_specifier(&config_file.specifier, &import_map_specifier) + }); + let modified_result = update_config_text( + &config_text, + fmt_options, + import_map_specifier.as_deref(), + try_add_node_modules_dir, + )?; + if let Some(new_text) = &modified_result.new_text { std::fs::write(config_path, new_text)?; } + Ok(modified_result) +} - Ok(()) +#[derive(Default)] +struct ModifiedResult { + updated_import_map: bool, + added_node_modules_dir: bool, + new_text: Option<String>, } fn update_config_text( text: &str, - import_map_specifier: &str, fmt_options: &FmtOptionsConfig, -) -> Option<String> { + import_map_specifier: Option<&str>, + try_add_node_modules_dir: bool, +) -> Result<ModifiedResult, AnyError> { use jsonc_parser::ast::ObjectProp; use jsonc_parser::ast::Value; let ast = - jsonc_parser::parse_to_ast(text, &Default::default(), &Default::default()) - .ok()?; + jsonc_parser::parse_to_ast(text, &Default::default(), &Default::default())?; let obj = match ast.value { Some(Value::Object(obj)) => obj, - _ => return None, // shouldn't happen, so ignore + _ => bail!("Failed updating config file due to no object."), }; - let import_map_specifier = import_map_specifier.replace('\"', "\\\""); - - match obj.get("importMap") { - Some(ObjectProp { - value: Value::StringLit(lit), - .. - }) => Some(format!( - "{}{}{}", - &text[..lit.range.start + 1], - import_map_specifier, - &text[lit.range.end - 1..], - )), - None => { - // insert it crudely at a position that won't cause any issues - // with comments and format after to make it look nice + let mut modified_result = ModifiedResult::default(); + let mut text_changes = Vec::new(); + let mut should_format = false; + + if try_add_node_modules_dir { + // Only modify the nodeModulesDir property if it's not set + // as this allows people to opt-out of this when vendoring + // by specifying `nodeModulesDir: false` + if obj.get("nodeModulesDir").is_none() { let insert_position = obj.range.end - 1; - let insert_text = format!( - r#"{}"importMap": "{}""#, - if obj.properties.is_empty() { "" } else { "," }, - import_map_specifier - ); - let new_text = format!( - "{}{}{}", - &text[..insert_position], - insert_text, - &text[insert_position..], - ); - format_json(&new_text, fmt_options) - .ok() - .map(|formatted_text| formatted_text.unwrap_or(new_text)) + text_changes.push(TextChange { + range: insert_position..insert_position, + new_text: r#""nodeModulesDir": true"#.to_string(), + }); + should_format = true; + modified_result.added_node_modules_dir = true; } - // shouldn't happen, so ignore - Some(_) => None, } + + if let Some(import_map_specifier) = import_map_specifier { + let import_map_specifier = import_map_specifier.replace('\"', "\\\""); + match obj.get("importMap") { + Some(ObjectProp { + value: Value::StringLit(lit), + .. + }) => { + text_changes.push(TextChange { + range: lit.range.start..lit.range.end, + new_text: format!("\"{}\"", import_map_specifier), + }); + modified_result.updated_import_map = true; + } + None => { + // insert it crudely at a position that won't cause any issues + // with comments and format after to make it look nice + let insert_position = obj.range.end - 1; + text_changes.push(TextChange { + range: insert_position..insert_position, + new_text: format!(r#""importMap": "{}""#, import_map_specifier), + }); + should_format = true; + modified_result.updated_import_map = true; + } + // shouldn't happen + Some(_) => { + bail!("Failed updating importMap in config file due to invalid type.") + } + } + } + + if text_changes.is_empty() { + return Ok(modified_result); + } + + let new_text = deno_ast::apply_text_changes(text, text_changes); + modified_result.new_text = if should_format { + format_json(&new_text, fmt_options) + .ok() + .map(|formatted_text| formatted_text.unwrap_or(new_text)) + } else { + Some(new_text) + }; + Ok(modified_result) } fn is_dir_empty(dir_path: &Path) -> Result<bool, AnyError> { @@ -288,36 +381,94 @@ mod internal_test { #[test] fn update_config_text_no_existing_props_add_prop() { - let text = update_config_text( + let result = update_config_text( + "{\n}", + &Default::default(), + Some("./vendor/import_map.json"), + false, + ) + .unwrap(); + assert!(result.updated_import_map); + assert!(!result.added_node_modules_dir); + assert_eq!( + result.new_text.unwrap(), + r#"{ + "importMap": "./vendor/import_map.json" +} +"# + ); + + let result = update_config_text( "{\n}", - "./vendor/import_map.json", &Default::default(), + Some("./vendor/import_map.json"), + true, ) .unwrap(); + assert!(result.updated_import_map); + assert!(result.added_node_modules_dir); assert_eq!( - text, + result.new_text.unwrap(), r#"{ + "nodeModulesDir": true, "importMap": "./vendor/import_map.json" } "# ); + + let result = + update_config_text("{\n}", &Default::default(), None, true).unwrap(); + assert!(!result.updated_import_map); + assert!(result.added_node_modules_dir); + assert_eq!( + result.new_text.unwrap(), + r#"{ + "nodeModulesDir": true +} +"# + ); } #[test] fn update_config_text_existing_props_add_prop() { - let text = update_config_text( + let result = update_config_text( r#"{ "tasks": { "task1": "other" } } "#, - "./vendor/import_map.json", &Default::default(), + Some("./vendor/import_map.json"), + false, + ) + .unwrap(); + assert_eq!( + result.new_text.unwrap(), + r#"{ + "tasks": { + "task1": "other" + }, + "importMap": "./vendor/import_map.json" +} +"# + ); + + // trailing comma + let result = update_config_text( + r#"{ + "tasks": { + "task1": "other" + }, +} +"#, + &Default::default(), + Some("./vendor/import_map.json"), + false, ) .unwrap(); assert_eq!( - text, + result.new_text.unwrap(), r#"{ "tasks": { "task1": "other" @@ -330,21 +481,54 @@ mod internal_test { #[test] fn update_config_text_update_prop() { - let text = update_config_text( + let result = update_config_text( r#"{ "importMap": "./local.json" } "#, - "./vendor/import_map.json", &Default::default(), + Some("./vendor/import_map.json"), + false, ) .unwrap(); assert_eq!( - text, + result.new_text.unwrap(), r#"{ "importMap": "./vendor/import_map.json" } "# ); } + + #[test] + fn no_update_node_modules_dir() { + // will not update if this is already set (even if it's false) + let result = update_config_text( + r#"{ + "nodeModulesDir": false +} +"#, + &Default::default(), + None, + true, + ) + .unwrap(); + assert!(!result.added_node_modules_dir); + assert!(!result.updated_import_map); + assert_eq!(result.new_text, None); + + let result = update_config_text( + r#"{ + "nodeModulesDir": true +} +"#, + &Default::default(), + None, + true, + ) + .unwrap(); + assert!(!result.added_node_modules_dir); + assert!(!result.updated_import_map); + assert_eq!(result.new_text, None); + } } |