diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b81a564d..6164147cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ #### :boom: Breaking Change +- `rescript format` no longer accepts `--all`. All (non-dev) files of the current rescript.json are now formatted by default. https://github.com/rescript-lang/rescript/pull/7752 + #### :eyeglasses: Spec Compliance #### :rocket: New Feature @@ -21,12 +23,16 @@ - Add new Stdlib helpers: `String.capitalize`, `String.isEmpty`, `Dict.size`, `Dict.isEmpty`, `Array.isEmpty`, `Map.isEmpty`, `Set.isEmpty`. https://github.com/rescript-lang/rescript/pull/7516 #### :bug: Bug fix + - Fix issue with ast conversion (for ppx use) on functions with attributes on first argument. https://github.com/rescript-lang/rescript/pull/7761 #### :memo: Documentation #### :nail_care: Polish +- `rescript format` now has a `--dev` flag that works similar to `rescript clean`. https://github.com/rescript-lang/rescript/pull/7752 +- `rescript clean` now will clean an individual project (see [#7707](https://github.com/rescript-lang/rescript/issues/7707)). https://github.com/rescript-lang/rescript/pull/7752 + #### :house: Internal - AST: Use jsx_tag_name instead of Longindent.t to store jsx tag name. https://github.com/rescript-lang/rescript/pull/7760 diff --git a/rescript.json b/rescript.json index 1f566dcf3d..2aa506b7b6 100644 --- a/rescript.json +++ b/rescript.json @@ -1,4 +1,4 @@ { "name": "rescript", - "dependencies": ["@tests/gentype-react-example"] + "dependencies": ["@tests/gentype-react-example", "playground"] } diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 990333ca05..c7c4cead3f 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -8,11 +8,11 @@ pub mod packages; pub mod parse; pub mod read_compile_state; -use self::compile::compiler_args; use self::parse::parser_args; use crate::build::compile::{mark_modules_with_deleted_deps_dirty, mark_modules_with_expired_deps_dirty}; use crate::helpers::emojis::*; -use crate::helpers::{self, get_workspace_root}; +use crate::helpers::{self}; +use crate::project_context::ProjectContext; use crate::{config, sourcedirs}; use anyhow::{Result, anyhow}; use build_types::*; @@ -55,31 +55,30 @@ pub struct CompilerArgs { pub parser_args: Vec, } -pub fn get_compiler_args(path: &Path) -> Result { - let filename = &helpers::get_abs_path(path); - let package_root = - helpers::get_abs_path(&helpers::get_nearest_config(path).expect("Couldn't find package root")); - let workspace_root = get_workspace_root(&package_root).map(|p| helpers::get_abs_path(&p)); - let root_rescript_config = - packages::read_config(&workspace_root.to_owned().unwrap_or(package_root.to_owned()))?; - let rescript_config = packages::read_config(&package_root)?; - let is_type_dev = match filename.strip_prefix(&package_root) { +pub fn get_compiler_args(rescript_file_path: &Path) -> Result { + let filename = &helpers::get_abs_path(rescript_file_path); + let current_package = helpers::get_abs_path( + &helpers::get_nearest_config(rescript_file_path).expect("Couldn't find package root"), + ); + let project_context = ProjectContext::new(¤t_package)?; + + let is_type_dev = match filename.strip_prefix(¤t_package) { Err(_) => false, - Ok(relative_path) => root_rescript_config.find_is_type_dev_for_path(relative_path), + Ok(relative_path) => project_context + .current_config + .find_is_type_dev_for_path(relative_path), }; // make PathBuf from package root and get the relative path for filename - let relative_filename = filename.strip_prefix(PathBuf::from(&package_root)).unwrap(); + let relative_filename = filename.strip_prefix(PathBuf::from(¤t_package)).unwrap(); - let file_path = PathBuf::from(&package_root).join(filename); + let file_path = PathBuf::from(¤t_package).join(filename); let contents = helpers::read_file(&file_path).expect("Error reading file"); let (ast_path, parser_args) = parser_args( - &rescript_config, - &root_rescript_config, + &project_context, + &project_context.current_config, relative_filename, - &workspace_root, - workspace_root.as_ref().unwrap_or(&package_root), &contents, ); let is_interface = filename.to_string_lossy().ends_with('i'); @@ -90,15 +89,13 @@ pub fn get_compiler_args(path: &Path) -> Result { interface_filename.push('i'); PathBuf::from(&interface_filename).exists() }; - let compiler_args = compiler_args( - &rescript_config, - &root_rescript_config, + let compiler_args = compile::compiler_args( + &project_context.current_config, &ast_path, relative_filename, is_interface, has_interface, - &package_root, - &workspace_root, + &project_context, &None, is_type_dev, true, @@ -120,10 +117,8 @@ pub fn initialize_build( build_dev_deps: bool, snapshot_output: bool, ) -> Result { - let project_root = helpers::get_abs_path(path); - let workspace_root = helpers::get_workspace_root(&project_root); let bsc_path = helpers::get_bsc(); - let root_config_name = packages::read_package_name(&project_root)?; + let project_context = ProjectContext::new(path)?; if !snapshot_output && show_progress { print!("{} {}Building package tree...", style("[1/7]").bold().dim(), TREE); @@ -131,13 +126,7 @@ pub fn initialize_build( } let timing_package_tree = Instant::now(); - let packages = packages::make( - filter, - &project_root, - &workspace_root, - show_progress, - build_dev_deps, - )?; + let packages = packages::make(filter, &project_context, show_progress, build_dev_deps)?; let timing_package_tree_elapsed = timing_package_tree.elapsed(); if !snapshot_output && show_progress { @@ -167,7 +156,7 @@ pub fn initialize_build( let _ = stdout().flush(); } - let mut build_state = BuildState::new(project_root, root_config_name, packages, workspace_root, bsc_path); + let mut build_state = BuildState::new(project_context, packages, bsc_path); packages::parse_packages(&mut build_state); let timing_source_files_elapsed = timing_source_files.elapsed(); @@ -190,7 +179,7 @@ pub fn initialize_build( let _ = stdout().flush(); } let timing_compile_state = Instant::now(); - let compile_assets_state = read_compile_state::read(&mut build_state); + let compile_assets_state = read_compile_state::read(&mut build_state)?; let timing_compile_state_elapsed = timing_compile_state.elapsed(); if !snapshot_output && show_progress { @@ -563,9 +552,8 @@ pub fn build( pub fn pass_through_legacy(mut args: Vec) -> i32 { let project_root = helpers::get_abs_path(Path::new(".")); - let workspace_root = helpers::get_workspace_root(&project_root); - - let rescript_legacy_path = helpers::get_rescript_legacy(&project_root, workspace_root); + let project_context = ProjectContext::new(&project_root).unwrap(); + let rescript_legacy_path = helpers::get_rescript_legacy(&project_context); args.insert(0, rescript_legacy_path.into()); let status = std::process::Command::new("node") diff --git a/rewatch/src/build/build_types.rs b/rewatch/src/build/build_types.rs index e8c1a28ee4..76035ab27f 100644 --- a/rewatch/src/build/build_types.rs +++ b/rewatch/src/build/build_types.rs @@ -1,4 +1,6 @@ use crate::build::packages::{Namespace, Package}; +use crate::config::Config; +use crate::project_context::ProjectContext; use ahash::{AHashMap, AHashSet}; use std::{fmt::Display, path::PathBuf, time::SystemTime}; @@ -89,14 +91,12 @@ impl Module { #[derive(Debug)] pub struct BuildState { + pub project_context: ProjectContext, pub modules: AHashMap, pub packages: AHashMap, pub module_names: AHashSet, - pub project_root: PathBuf, - pub root_config_name: String, pub deleted_modules: AHashSet, pub bsc_path: PathBuf, - pub workspace_root: Option, pub deps_initialized: bool, } @@ -109,20 +109,16 @@ impl BuildState { self.modules.get(module_name) } pub fn new( - project_root: PathBuf, - root_config_name: String, + project_context: ProjectContext, packages: AHashMap, - workspace_root: Option, bsc_path: PathBuf, ) -> Self { Self { + project_context, module_names: AHashSet::new(), modules: AHashMap::new(), packages, - project_root, - root_config_name, deleted_modules: AHashSet::new(), - workspace_root, bsc_path, deps_initialized: false, } @@ -132,6 +128,10 @@ impl BuildState { self.modules.insert(module_name.to_owned(), module); self.module_names.insert(module_name.to_owned()); } + + pub fn get_root_config(&self) -> &Config { + self.project_context.get_root_config() + } } #[derive(Debug)] diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index cb7a7db73f..e4a4786a8c 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -1,7 +1,10 @@ use super::build_types::*; use super::packages; +use crate::build::packages::Package; +use crate::config::Config; use crate::helpers; use crate::helpers::emojis::*; +use crate::project_context::ProjectContext; use ahash::AHashSet; use anyhow::Result; use console::style; @@ -28,10 +31,10 @@ fn remove_iast(package: &packages::Package, source_file: &Path) { )); } -fn remove_mjs_file(source_file: &Path, suffix: &String) { +fn remove_mjs_file(source_file: &Path, suffix: &str) { let _ = std::fs::remove_file(source_file.with_extension( // suffix.to_string includes the ., so we need to remove it - &suffix.to_string()[1..], + &suffix[1..], )); } @@ -58,31 +61,32 @@ pub fn remove_compile_assets(package: &packages::Package, source_file: &Path) { } } -fn clean_source_files(build_state: &BuildState, root_package: &packages::Package) { +fn clean_source_files(build_state: &BuildState, root_config: &Config, suffix: &str) { // get all rescript file locations let rescript_file_locations = build_state .modules .values() .filter_map(|module| match &module.source_type { SourceType::SourceFile(source_file) => { - let package = build_state.packages.get(&module.package_name).unwrap(); - Some( - root_package - .config + build_state.packages.get(&module.package_name).map(|package| { + root_config .get_package_specs() - .iter() + .into_iter() .filter_map(|spec| { if spec.in_source { Some(( package.path.join(&source_file.implementation.path), - root_package.config.get_suffix(spec), + match spec.suffix { + None => suffix.to_owned(), + Some(suffix) => suffix, + }, )) } else { None } }) - .collect::>(), - ) + .collect::>() + }) } _ => None, }) @@ -326,17 +330,10 @@ pub fn cleanup_after_build(build_state: &BuildState) { }); } -pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, build_dev_deps: bool) -> Result<()> { - let project_root = helpers::get_abs_path(path); - let workspace_root = helpers::get_workspace_root(&project_root); - let packages = packages::make( - &None, - &project_root, - &workspace_root, - show_progress, - build_dev_deps, - )?; - let root_config_name = packages::read_package_name(&project_root)?; +pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, clean_dev_deps: bool) -> Result<()> { + let project_context = ProjectContext::new(path)?; + + let packages = packages::make(&None, &project_context, show_progress, clean_dev_deps)?; let bsc_path = helpers::get_bsc(); let timing_clean_compiler_assets = Instant::now(); @@ -348,30 +345,11 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, build_dev_ ); let _ = std::io::stdout().flush(); }; - packages.iter().for_each(|(_, package)| { - if show_progress { - if snapshot_output { - println!("Cleaning {}", package.name) - } else { - print!( - "{}{} {}Cleaning {}...", - LINE_CLEAR, - style("[1/2]").bold().dim(), - SWEEP, - package.name - ); - } - let _ = std::io::stdout().flush(); - } - let path_str = package.get_build_path(); - let path = std::path::Path::new(&path_str); - let _ = std::fs::remove_dir_all(path); + for (_, package) in &packages { + clean_package(show_progress, snapshot_output, package) + } - let path_str = package.get_ocaml_build_path(); - let path = std::path::Path::new(&path_str); - let _ = std::fs::remove_dir_all(path); - }); let timing_clean_compiler_assets_elapsed = timing_clean_compiler_assets.elapsed(); if !snapshot_output && show_progress { @@ -386,20 +364,10 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, build_dev_ } let timing_clean_mjs = Instant::now(); - let mut build_state = BuildState::new( - project_root.to_owned(), - root_config_name, - packages, - workspace_root, - bsc_path, - ); + let mut build_state = BuildState::new(project_context, packages, bsc_path); packages::parse_packages(&mut build_state); - let root_package = build_state - .packages - .get(&build_state.root_config_name) - .expect("Could not find root package"); - - let suffix = root_package.config.suffix.as_deref().unwrap_or(".res.mjs"); + let root_config = build_state.get_root_config(); + let suffix = build_state.project_context.get_suffix(); if !snapshot_output && show_progress { println!( @@ -411,7 +379,7 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, build_dev_ let _ = std::io::stdout().flush(); } - clean_source_files(&build_state, root_package); + clean_source_files(&build_state, root_config, &suffix); let timing_clean_mjs_elapsed = timing_clean_mjs.elapsed(); if !snapshot_output && show_progress { @@ -428,3 +396,28 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, build_dev_ Ok(()) } + +fn clean_package(show_progress: bool, snapshot_output: bool, package: &Package) { + if show_progress { + if snapshot_output { + println!("Cleaning {}", package.name) + } else { + print!( + "{}{} {}Cleaning {}...", + LINE_CLEAR, + style("[1/2]").bold().dim(), + SWEEP, + package.name + ); + } + let _ = std::io::stdout().flush(); + } + + let path_str = package.get_build_path(); + let path = std::path::Path::new(&path_str); + let _ = std::fs::remove_dir_all(path); + + let path_str = package.get_ocaml_build_path(); + let path = std::path::Path::new(&path_str); + let _ = std::fs::remove_dir_all(path); +} diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index b8b4873c48..2e13b92563 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -8,12 +8,13 @@ use super::packages; use crate::config; use crate::helpers; use crate::helpers::StrippedVerbatimPath; +use crate::project_context::ProjectContext; use ahash::{AHashMap, AHashSet}; -use anyhow::anyhow; +use anyhow::{Result, anyhow}; use console::style; use log::{debug, trace}; use rayon::prelude::*; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use std::time::SystemTime; @@ -154,21 +155,14 @@ pub fn compile( .get_package(&module.package_name) .expect("Package not found"); - let root_package = - build_state.get_package(&build_state.root_config_name).unwrap(); - let interface_result = match source_file.interface.to_owned() { Some(Interface { path, .. }) => { let result = compile_file( package, - root_package, &helpers::get_ast_path(&path), module, true, - &build_state.bsc_path, - &build_state.packages, - &build_state.project_root, - &build_state.workspace_root, + build_state, ); Some(result) } @@ -176,14 +170,10 @@ pub fn compile( }; let result = compile_file( package, - root_package, &helpers::get_ast_path(&source_file.implementation.path), module, false, - &build_state.bsc_path, - &build_state.packages, - &build_state.project_root, - &build_state.workspace_root, + build_state, ); let cmi_digest_after = helpers::compute_file_hash(Path::new(&cmi_path)); @@ -347,24 +337,21 @@ pub fn compile( pub fn compiler_args( config: &config::Config, - root_config: &config::Config, ast_path: &Path, file_path: &Path, is_interface: bool, has_interface: bool, - project_root: &Path, - workspace_root: &Option, + project_context: &ProjectContext, // if packages are known, we pass a reference here - // this saves us a scan to find their paths + // this saves us a scan to find their paths. + // This is None when called by build::get_compiler_args packages: &Option<&AHashMap>, // Is the file listed as "type":"dev"? is_type_dev: bool, is_local_dep: bool, ) -> Vec { let bsc_flags = config::flatten_flags(&config.compiler_flags); - - let dependency_paths = get_dependency_paths(config, project_root, workspace_root, packages, is_type_dev); - + let dependency_paths = get_dependency_paths(config, project_context, packages, is_type_dev); let module_name = helpers::file_path_to_module_name(file_path, &config.get_namespace()); let namespace_args = match &config.get_namespace() { @@ -388,6 +375,7 @@ pub fn compiler_args( packages::Namespace::NoNamespace => vec![], }; + let root_config = project_context.get_root_config(); let jsx_args = root_config.get_jsx_args(); let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); @@ -449,7 +437,7 @@ pub fn compiler_args( "-I".to_string(), Path::new("..").join("ocaml").to_string_lossy().to_string(), ], - dependency_paths.concat(), + dependency_paths, jsx_args, jsx_module_args, jsx_mode_args, @@ -497,11 +485,10 @@ impl DependentPackage { fn get_dependency_paths( config: &config::Config, - project_root: &Path, - workspace_root: &Option, + project_context: &ProjectContext, packages: &Option<&AHashMap>, is_file_type_dev: bool, -) -> Vec> { +) -> Vec { let normal_deps = config .dependencies .clone() @@ -534,7 +521,7 @@ fn get_dependency_paths( .as_ref() .map(|package| package.path.clone()) } else { - packages::read_dependency(package_name, project_root, project_root, workspace_root).ok() + packages::read_dependency(package_name, project_context).ok() } .map(|canonicalized_path| { vec![ @@ -555,19 +542,23 @@ fn get_dependency_paths( dependency_path }) .collect::>>() + .concat() } fn compile_file( package: &packages::Package, - root_package: &packages::Package, ast_path: &Path, module: &Module, is_interface: bool, - bsc_path: &Path, - packages: &AHashMap, - project_root: &Path, - workspace_root: &Option, -) -> Result, String> { + build_state: &BuildState, +) -> Result> { + let BuildState { + packages, + project_context, + bsc_path, + .. + } = build_state; + let root_config = build_state.get_root_config(); let ocaml_build_path_abs = package.get_ocaml_build_path(); let build_path_abs = package.get_build_path(); let implementation_file_path = match &module.source_type { @@ -577,20 +568,19 @@ fn compile_file( sourcetype, ast_path.to_string_lossy() )), - }?; + } + .map_err(|e| anyhow!(e))?; let basename = helpers::file_path_to_compiler_asset_basename(implementation_file_path, &package.namespace); let has_interface = module.get_interface().is_some(); let is_type_dev = module.is_type_dev; let to_mjs_args = compiler_args( &package.config, - &root_package.config, ast_path, implementation_file_path, is_interface, has_interface, - project_root, - workspace_root, + project_context, &Some(packages), is_type_dev, package.is_local_dep, @@ -611,9 +601,9 @@ fn compile_file( Ok(x) if !x.status.success() => { let stderr = String::from_utf8_lossy(&x.stderr); let stdout = String::from_utf8_lossy(&x.stdout); - Err(stderr.to_string() + &stdout) + Err(anyhow!(stderr.to_string() + &stdout)) } - Err(e) => Err(format!( + Err(e) => Err(anyhow!( "Could not compile file. Error: {e}. Path to AST: {ast_path:?}" )), Ok(x) => { @@ -621,7 +611,7 @@ fn compile_file( .expect("stdout should be non-null") .to_string(); - let dir = std::path::Path::new(implementation_file_path).parent().unwrap(); + let dir = Path::new(implementation_file_path).parent().unwrap(); // perhaps we can do this copying somewhere else if !is_interface { @@ -709,7 +699,7 @@ fn compile_file( } // copy js file - root_package.config.get_package_specs().iter().for_each(|spec| { + root_config.get_package_specs().iter().for_each(|spec| { if spec.in_source { if let SourceType::SourceFile(SourceFile { implementation: Implementation { path, .. }, @@ -718,11 +708,11 @@ fn compile_file( { let source = helpers::get_source_file_from_rescript_file( &Path::new(&package.path).join(path), - &root_package.config.get_suffix(spec), + &root_config.get_suffix(spec), ); let destination = helpers::get_source_file_from_rescript_file( &package.get_build_path().join(path), - &root_package.config.get_suffix(spec), + &root_config.get_suffix(spec), ); if source.exists() { diff --git a/rewatch/src/build/deps.rs b/rewatch/src/build/deps.rs index 8159cdc5f3..c89f5898a0 100644 --- a/rewatch/src/build/deps.rs +++ b/rewatch/src/build/deps.rs @@ -18,11 +18,11 @@ fn get_dep_modules( Ok(lines) => { // we skip the first line with is some null characters // the following lines in the AST are the dependency modules - // we stop when we hit a line that starts with a "/", this is the path of the file. + // we stop when we hit a line that is an absolute path, this is the path of the file. // this is the point where the dependencies end and the actual AST starts for line in lines.skip(1).flatten() { let line = line.trim().to_string(); - if line.starts_with('/') { + if std::path::Path::new(&line).is_absolute() { break; } else if !line.is_empty() { deps.insert(line); diff --git a/rewatch/src/build/packages.rs b/rewatch/src/build/packages.rs index 623e4b2757..c4d1b6e9b6 100644 --- a/rewatch/src/build/packages.rs +++ b/rewatch/src/build/packages.rs @@ -2,9 +2,11 @@ use super::build_types::*; use super::namespaces; use super::packages; use crate::config; +use crate::config::Config; use crate::helpers; use crate::helpers::StrippedVerbatimPath; use crate::helpers::emojis::*; +use crate::project_context::{MonoRepoContext, ProjectContext}; use ahash::{AHashMap, AHashSet}; use anyhow::{Result, anyhow}; use console::style; @@ -237,43 +239,27 @@ fn get_source_dirs(source: config::Source, sub_path: Option) -> AHashSe source_folders } -pub fn read_config(package_dir: &Path) -> Result { +pub fn read_config(package_dir: &Path) -> Result { let rescript_json_path = package_dir.join("rescript.json"); let bsconfig_json_path = package_dir.join("bsconfig.json"); if Path::new(&rescript_json_path).exists() { - config::Config::new(&rescript_json_path) + Config::new(&rescript_json_path) } else { - config::Config::new(&bsconfig_json_path) + Config::new(&bsconfig_json_path) } } -pub fn read_dependency( - package_name: &str, - parent_path: &Path, - project_root: &Path, - workspace_root: &Option, -) -> Result { - let path_from_parent = helpers::package_path(parent_path, package_name); - let path_from_project_root = helpers::package_path(project_root, package_name); - let maybe_path_from_workspace_root = workspace_root - .as_ref() - .map(|workspace_root| helpers::package_path(workspace_root, package_name)); - - let path = match ( - path_from_parent, - path_from_project_root, - maybe_path_from_workspace_root, - ) { - (path_from_parent, _, _) if path_from_parent.exists() => Ok(path_from_parent), - (_, path_from_project_root, _) if path_from_project_root.exists() => Ok(path_from_project_root), - (_, _, Some(path_from_workspace_root)) if path_from_workspace_root.exists() => { - Ok(path_from_workspace_root) - } - _ => Err(format!( +pub fn read_dependency(package_name: &str, project_context: &ProjectContext) -> Result { + // root folder + node_modules + package_name + let path_from_root = helpers::package_path(project_context.get_root_path(), package_name); + let path = (if path_from_root.exists() { + Ok(path_from_root) + } else { + Err(format!( "The package \"{package_name}\" is not found (are node_modules up-to-date?)..." - )), - }?; + )) + })?; let canonical_path = match path .canonicalize() @@ -299,17 +285,15 @@ pub fn read_dependency( /// recursively continues operation for their dependencies as well. fn read_dependencies( registered_dependencies_set: &mut AHashSet, - parent_config: &config::Config, - parent_path: &Path, - project_root: &Path, - workspace_root: &Option, + project_context: &ProjectContext, + package_config: &Config, show_progress: bool, build_dev_deps: bool, ) -> Vec { - let mut dependencies = parent_config.dependencies.to_owned().unwrap_or_default(); + let mut dependencies = package_config.dependencies.to_owned().unwrap_or_default(); // Concatenate dev dependencies if build_dev_deps is true - if build_dev_deps && let Some(dev_deps) = parent_config.dev_dependencies.to_owned() { + if build_dev_deps && let Some(dev_deps) = package_config.dev_dependencies.to_owned() { dependencies.extend(dev_deps); } @@ -328,7 +312,7 @@ fn read_dependencies( .par_iter() .map(|package_name| { let (config, canonical_path) = - match read_dependency(package_name, parent_path, project_root, workspace_root) { + match read_dependency(package_name, project_context) { Err(error) => { if show_progress { println!( @@ -339,7 +323,7 @@ fn read_dependencies( ); } - let parent_path_str = parent_path.to_string_lossy(); + let parent_path_str = project_context.get_root_path().to_string_lossy(); log::error!( "We could not build package tree reading dependency '{package_name}', at path '{parent_path_str}'. Error: {error}", ); @@ -350,7 +334,7 @@ fn read_dependencies( match read_config(&canonical_path) { Ok(config) => (config, canonical_path), Err(error) => { - let parent_path_str = parent_path.to_string_lossy(); + let parent_path_str = project_context.get_root_path().to_string_lossy(); log::error!( "We could not build package tree '{package_name}', at path '{parent_path_str}'. Error: {error}", ); @@ -361,16 +345,27 @@ fn read_dependencies( }; let is_local_dep = { - canonical_path.starts_with(project_root) - && !canonical_path.components().any(|c| c.as_os_str() == "node_modules") + match &project_context.monorepo_context { + None => project_context.current_config.name.as_str() == package_name + , + Some(MonoRepoContext::MonorepoRoot { + local_dependencies, + local_dev_dependencies, + }) => { + local_dependencies.contains(package_name) || local_dev_dependencies.contains(package_name) + }, + Some(MonoRepoContext::MonorepoPackage { + parent_config, + }) => { + helpers::is_local_package(&parent_config.path, &canonical_path) + } + } }; let dependencies = read_dependencies( &mut registered_dependencies_set.to_owned(), + project_context, &config, - &canonical_path, - project_root, - workspace_root, show_progress, is_local_dep && build_dev_deps, ); @@ -380,7 +375,7 @@ fn read_dependencies( config, path: canonical_path, dependencies, - is_local_dep + is_local_dep, } }) .collect() @@ -483,25 +478,28 @@ This inconsistency will cause issues with package resolution.\n", } fn read_packages( - project_root: &Path, - workspace_root: &Option, + project_context: &ProjectContext, show_progress: bool, build_dev_deps: bool, ) -> Result> { - let root_config = read_config(project_root)?; - // Store all packages and completely deduplicate them let mut map: AHashMap = AHashMap::new(); - let root_package = make_package(root_config.to_owned(), project_root, true, true); - map.insert(root_package.name.to_string(), root_package); + let current_package = { + let config = &project_context.current_config; + let folder = config + .path + .parent() + .ok_or_else(|| anyhow!("Could not the read parent folder or a rescript.json file"))?; + make_package(config.to_owned(), folder, true, true) + }; + + map.insert(current_package.name.to_string(), current_package); let mut registered_dependencies_set: AHashSet = AHashSet::new(); let dependencies = flatten_dependencies(read_dependencies( &mut registered_dependencies_set, - &root_config, - project_root, - project_root, - workspace_root, + project_context, + &project_context.current_config, show_progress, build_dev_deps, )); @@ -617,15 +615,14 @@ fn extend_with_children( /// 2. Take the (by then deduplicated) packages, and find all the '.res' and /// interface files. /// -/// The two step process is there to reduce IO overhead +/// The two step process is there to reduce IO overhead. pub fn make( filter: &Option, - root_folder: &Path, - workspace_root: &Option, + project_context: &ProjectContext, show_progress: bool, build_dev_deps: bool, ) -> Result> { - let map = read_packages(root_folder, workspace_root, show_progress, build_dev_deps)?; + let map = read_packages(project_context, show_progress, build_dev_deps)?; /* Once we have the deduplicated packages, we can add the source files for each - to minimize * the IO */ @@ -648,11 +645,9 @@ pub fn parse_packages(build_state: &mut BuildState) { let bs_build_path = package.get_ocaml_build_path(); helpers::create_path(&build_path_abs); helpers::create_path(&bs_build_path); - let root_config = build_state - .get_package(&build_state.root_config_name) - .expect("cannot find root config"); + let root_config = build_state.get_root_config(); - root_config.config.get_package_specs().iter().for_each(|spec| { + root_config.get_package_specs().iter().for_each(|spec| { if !spec.in_source { // we don't want to calculate this if we don't have out of source specs // we do this twice, but we almost never have multiple package specs @@ -996,6 +991,7 @@ mod test { bs_deps: args.bs_deps, build_dev_deps: args.build_dev_deps, allowed_dependents: args.allowed_dependents, + path: PathBuf::from("./something/rescript.json"), }), source_folders: AHashSet::new(), source_files: None, diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index 67908e772e..375c5e9abe 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -1,10 +1,11 @@ use super::build_types::*; use super::logs; use super::namespaces; -use super::packages; +use crate::build::packages::Package; use crate::config; -use crate::config::OneOrMore; +use crate::config::{Config, OneOrMore}; use crate::helpers; +use crate::project_context::ProjectContext; use ahash::AHashSet; use log::debug; use rayon::prelude::*; @@ -38,8 +39,6 @@ pub fn generate_asts( } SourceType::SourceFile(source_file) => { - let root_package = build_state.get_package(&build_state.root_config_name).unwrap(); - let (ast_result, iast_result, dirty) = if source_file.implementation.parse_dirty || source_file .interface @@ -50,21 +49,15 @@ pub fn generate_asts( inc(); let ast_result = generate_ast( package.to_owned(), - root_package.to_owned(), &source_file.implementation.path.to_owned(), - &build_state.bsc_path, - &build_state.workspace_root, + build_state, ); let iast_result = match source_file.interface.as_ref().map(|i| i.path.to_owned()) { - Some(interface_file_path) => generate_ast( - package.to_owned(), - root_package.to_owned(), - &interface_file_path.to_owned(), - &build_state.bsc_path, - &build_state.workspace_root, - ) - .map(Some), + Some(interface_file_path) => { + generate_ast(package.to_owned(), &interface_file_path.to_owned(), build_state) + .map(Some) + } _ => Ok(None), }; @@ -240,29 +233,25 @@ pub fn generate_asts( } pub fn parser_args( - config: &config::Config, - root_config: &config::Config, + project_context: &ProjectContext, + package_config: &Config, filename: &Path, - workspace_root: &Option, - root_path: &Path, contents: &str, ) -> (PathBuf, Vec) { + let root_config = project_context.get_root_config(); let file = &filename; let ast_path = helpers::get_ast_path(file); + let node_modules_path = project_context.get_root_path().join("node_modules"); let ppx_flags = config::flatten_ppx_flags( - &if let Some(workspace_root) = workspace_root { - workspace_root.join("node_modules") - } else { - root_path.join("node_modules") - }, - &filter_ppx_flags(&config.ppx_flags, contents), - &config.name, + node_modules_path.as_path(), + &filter_ppx_flags(&package_config.ppx_flags, contents), + &package_config.name, ); let jsx_args = root_config.get_jsx_args(); let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); - let bsc_flags = config::flatten_flags(&config.compiler_flags); + let bsc_flags = config::flatten_flags(&package_config.compiler_flags); let file = PathBuf::from("..").join("..").join(file); @@ -288,24 +277,16 @@ pub fn parser_args( } fn generate_ast( - package: packages::Package, - root_package: packages::Package, + package: Package, filename: &Path, - bsc_path: &PathBuf, - workspace_root: &Option, + build_state: &BuildState, ) -> Result<(PathBuf, Option), String> { let file_path = PathBuf::from(&package.path).join(filename); let contents = helpers::read_file(&file_path).expect("Error reading file"); let build_path_abs = package.get_build_path(); - let (ast_path, parser_args) = parser_args( - &package.config, - &root_package.config, - filename, - workspace_root, - &root_package.path, - &contents, - ); + let (ast_path, parser_args) = + parser_args(&build_state.project_context, &package.config, filename, &contents); // generate the dir of the ast_path (it mirrors the source file dir) let ast_parent_path = package.get_build_path().join(ast_path.parent().unwrap()); @@ -313,7 +294,7 @@ fn generate_ast( /* Create .ast */ let result = match Some( - Command::new(bsc_path) + Command::new(&build_state.bsc_path) .current_dir(&build_path_abs) .args(parser_args) .output() diff --git a/rewatch/src/build/read_compile_state.rs b/rewatch/src/build/read_compile_state.rs index 0f516fc6d0..3a3b8adc59 100644 --- a/rewatch/src/build/read_compile_state.rs +++ b/rewatch/src/build/read_compile_state.rs @@ -7,7 +7,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::SystemTime; -pub fn read(build_state: &mut BuildState) -> CompileAssetsState { +pub fn read(build_state: &mut BuildState) -> anyhow::Result { let mut ast_modules: AHashMap = AHashMap::new(); let mut cmi_modules: AHashMap = AHashMap::new(); let mut cmt_modules: AHashMap = AHashMap::new(); @@ -73,16 +73,14 @@ pub fn read(build_state: &mut BuildState) -> CompileAssetsState { .flatten() .collect::>(); + let root_config = build_state.get_root_config(); + compile_assets.iter().for_each( |(path, last_modified, extension, package_name, package_namespace, package_is_root)| { match extension.as_str() { "iast" | "ast" => { let module_name = helpers::file_path_to_module_name(path, package_namespace); - let root_package = build_state - .packages - .get(&build_state.root_config_name) - .expect("Could not find root package"); if let Some(res_file_path_buf) = get_res_path_from_ast(path) { let _ = ast_modules.insert( res_file_path_buf.clone(), @@ -93,9 +91,8 @@ pub fn read(build_state: &mut BuildState) -> CompileAssetsState { last_modified: last_modified.to_owned(), ast_file_path: path.to_path_buf(), is_root: *package_is_root, - suffix: root_package - .config - .get_suffix(root_package.config.get_package_specs().first().unwrap()), + suffix: root_config + .get_suffix(root_config.get_package_specs().first().unwrap()), }, ); let _ = ast_rescript_file_locations.insert(res_file_path_buf); @@ -126,13 +123,13 @@ pub fn read(build_state: &mut BuildState) -> CompileAssetsState { }, ); - CompileAssetsState { + Ok(CompileAssetsState { ast_modules, cmi_modules, cmt_modules, ast_rescript_file_locations, rescript_file_locations, - } + }) } fn get_res_path_from_ast(ast_file: &Path) -> Option { diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index 0870f603ea..5a66fe0df1 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -181,10 +181,6 @@ pub enum Command { }, /// Formats ReScript files. Format { - /// Format the whole project. - #[arg(short, long, group = "format_input_mode")] - all: bool, - /// Check formatting status without applying changes. #[arg(short, long)] check: bool, @@ -199,9 +195,12 @@ pub enum Command { )] stdin: Option, - /// Files to format. - #[arg(group = "format_input_mode", required_unless_present_any = ["format_input_mode"])] + /// Files to format. If no files are provided, all files are formatted. + #[arg(group = "format_input_mode")] files: Vec, + + #[command(flatten)] + dev: DevArg, }, /// This prints the compiler arguments. It expects the path to a rescript file (.res or .resi). CompilerArgs { diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 6d2878c790..3435859e4b 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -266,6 +266,13 @@ pub struct Config { // Holds all deprecation warnings for the config struct #[serde(skip)] deprecation_warnings: Vec, + + #[serde(default = "default_path")] + pub path: PathBuf, +} + +fn default_path() -> PathBuf { + PathBuf::from("./rescript.json") } /// This flattens string flags @@ -375,7 +382,9 @@ impl Config { /// Try to convert a bsconfig from a certain path to a bsconfig struct pub fn new(path: &Path) -> Result { let read = fs::read_to_string(path)?; - Config::new_from_json_string(&read) + let mut config = Config::new_from_json_string(&read)?; + config.set_path(path.to_path_buf())?; + Ok(config) } /// Try to convert a bsconfig from a string to a bsconfig struct @@ -387,6 +396,11 @@ impl Config { Ok(config) } + fn set_path(&mut self, path: PathBuf) -> Result<()> { + self.path = path; + Ok(()) + } + pub fn get_namespace(&self) -> packages::Namespace { let namespace_from_package = namespace_from_package_name(&self.name); match (self.namespace.as_ref(), self.namespace_entry.as_ref()) { @@ -533,8 +547,6 @@ impl Config { .unwrap_or(".js".to_string()) } - // TODO: needs improving! - pub fn find_is_type_dev_for_path(&self, relative_path: &Path) -> bool { let relative_parent = match relative_path.parent() { None => return false, @@ -620,6 +632,7 @@ pub mod tests { pub bs_deps: Vec, pub build_dev_deps: Vec, pub allowed_dependents: Option>, + pub path: PathBuf, } pub fn create_config(args: CreateConfigArgs) -> Config { @@ -644,6 +657,7 @@ pub mod tests { namespace_entry: None, deprecation_warnings: vec![], allowed_dependents: args.allowed_dependents, + path: args.path, } } diff --git a/rewatch/src/format.rs b/rewatch/src/format.rs index 742a7a1511..2fa918c930 100644 --- a/rewatch/src/format.rs +++ b/rewatch/src/format.rs @@ -1,4 +1,4 @@ -use crate::helpers; +use crate::{helpers, project_context}; use anyhow::{Result, bail}; use num_cpus; use rayon::prelude::*; @@ -14,9 +14,9 @@ use clap::ValueEnum; pub fn format( stdin_extension: Option, - all: bool, check: bool, files: Vec, + format_dev_deps: bool, ) -> Result<()> { let bsc_path = helpers::get_bsc(); @@ -25,7 +25,11 @@ pub fn format( format_stdin(&bsc_path, extension)?; } None => { - let files = if all { get_all_files()? } else { files }; + let files = if files.is_empty() { + get_files_in_scope(format_dev_deps)? + } else { + files + }; format_files(&bsc_path, files, check)?; } } @@ -33,17 +37,17 @@ pub fn format( Ok(()) } -fn get_all_files() -> Result> { +fn get_files_in_scope(format_dev_deps: bool) -> Result> { let current_dir = std::env::current_dir()?; - let project_root = helpers::get_abs_path(¤t_dir); - let workspace_root_option = helpers::get_workspace_root(&project_root); + let project_context = project_context::ProjectContext::new(¤t_dir)?; - let build_state = packages::make(&None, &project_root, &workspace_root_option, false, false)?; + let packages = packages::make(&None, &project_context, false, format_dev_deps)?; let mut files: Vec = Vec::new(); + let packages_to_format = project_context.get_scoped_local_packages(format_dev_deps); - for (_package_name, package) in build_state { - if package.is_local_dep - && let Some(source_files) = package.source_files + for (_package_name, package) in packages { + if packages_to_format.contains(&package.name) + && let Some(source_files) = &package.source_files { for (path, _metadata) in source_files { if let Some(extension) = path.extension() { diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 456a43cdae..b3115f528d 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -1,4 +1,5 @@ use crate::build::packages; +use crate::project_context::ProjectContext; use std::ffi::OsString; use std::fs; use std::fs::File; @@ -191,27 +192,26 @@ pub fn get_bsc() -> PathBuf { .to_stripped_verbatim_path() } -pub fn get_rescript_legacy(root_path: &Path, workspace_root: Option) -> PathBuf { - let bin_dir = Path::new("node_modules").join("rescript").join("cli"); - - match ( +pub fn get_rescript_legacy(project_context: &ProjectContext) -> PathBuf { + let root_path = project_context.get_root_path(); + let node_modules_rescript = root_path.join("node_modules").join("rescript"); + let rescript_legacy_path = if node_modules_rescript.exists() { + node_modules_rescript + .join("cli") + .join("rescript-legacy.js") + .canonicalize() + .map(StrippedVerbatimPath::to_stripped_verbatim_path) + } else { + // If the root folder / node_modules doesn't exist, something is wrong. + // The only way this can happen is if we are inside the rescript repository. root_path - .join(&bin_dir) + .join("cli") .join("rescript-legacy.js") .canonicalize() - .map(StrippedVerbatimPath::to_stripped_verbatim_path), - workspace_root.map(|workspace_root| { - workspace_root - .join(&bin_dir) - .join("rescript-legacy.js") - .canonicalize() - .map(StrippedVerbatimPath::to_stripped_verbatim_path) - }), - ) { - (Ok(path), _) => path, - (_, Some(Ok(path))) => path, - _ => panic!("Could not find rescript-legacy.exe"), - } + .map(StrippedVerbatimPath::to_stripped_verbatim_path) + }; + + rescript_legacy_path.unwrap_or_else(|_| panic!("Could not find rescript-legacy.exe")) } pub fn string_ends_with_any(s: &Path, suffixes: &[&str]) -> bool { @@ -357,12 +357,6 @@ fn has_rescript_config(path: &Path) -> bool { path.join("bsconfig.json").exists() || path.join("rescript.json").exists() } -pub fn get_workspace_root(package_root: &Path) -> Option { - std::path::PathBuf::from(&package_root) - .parent() - .and_then(get_nearest_config) -} - // traverse up the directory tree until we find a config.json, if not return None pub fn get_nearest_config(path_buf: &Path) -> Option { let mut current_dir = path_buf.to_owned(); @@ -390,3 +384,10 @@ pub fn get_source_file_from_rescript_file(path: &Path, suffix: &str) -> PathBuf &suffix.to_string()[1..], ) } + +pub fn is_local_package(workspace_path: &Path, canonical_package_path: &Path) -> bool { + canonical_package_path.starts_with(workspace_path) + && !canonical_package_path + .components() + .any(|c| c.as_os_str() == "node_modules") +} diff --git a/rewatch/src/lib.rs b/rewatch/src/lib.rs index 5b24c38886..a389e8172e 100644 --- a/rewatch/src/lib.rs +++ b/rewatch/src/lib.rs @@ -5,6 +5,7 @@ pub mod config; pub mod format; pub mod helpers; pub mod lock; +pub mod project_context; pub mod queue; pub mod sourcedirs; pub mod watcher; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 7ae9ae0884..5fc08d4250 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -93,10 +93,10 @@ fn main() -> Result<()> { } cli::Command::Format { stdin, - all, check, files, - } => format::format(stdin, all, check, files), + dev, + } => format::format(stdin, check, files, dev.dev), } } diff --git a/rewatch/src/project_context.rs b/rewatch/src/project_context.rs new file mode 100644 index 0000000000..0293b7c809 --- /dev/null +++ b/rewatch/src/project_context.rs @@ -0,0 +1,249 @@ +use crate::build::packages; +use crate::config::Config; +use crate::helpers; +use ahash::AHashSet; +use anyhow::Result; +use anyhow::anyhow; +use log::debug; +use std::fmt; +use std::path::Path; + +pub enum MonoRepoContext { + /// Monorepo root - contains local dependencies (symlinked in node_modules) + MonorepoRoot { + local_dependencies: AHashSet, // names of local deps + local_dev_dependencies: AHashSet, + }, + /// Package within a monorepo - has a parent workspace + MonorepoPackage { parent_config: Box }, +} + +pub struct ProjectContext { + pub current_config: Config, + pub monorepo_context: Option, +} + +fn format_dependencies(dependencies: &AHashSet) -> String { + if dependencies.is_empty() { + "[]".to_string() + } else { + let mut out = String::from("[\n"); + let mut first = true; + + for dep in dependencies { + if !first { + out.push_str(",\n"); + } else { + first = false; + } + out.push_str(" \""); + out.push_str(dep); + out.push('"'); + } + + out.push_str("\n]"); + out + } +} + +impl fmt::Debug for ProjectContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.monorepo_context { + None => { + write!( + f, + "Single project: \"{}\" at \"{}\"", + &self.current_config.name, + &self.current_config.path.to_string_lossy() + ) + } + Some(MonoRepoContext::MonorepoRoot { + local_dependencies, + local_dev_dependencies, + }) => { + let deps = format_dependencies(local_dependencies); + let dev_deps = format_dependencies(local_dev_dependencies); + + write!( + f, + "Monorepo root: \"{}\" at \"{}\" with dependencies:\n {}\n and devDependencies:\n {}", + &self.current_config.name, + &self.current_config.path.to_string_lossy(), + deps, + dev_deps + ) + } + Some(MonoRepoContext::MonorepoPackage { parent_config }) => { + write!( + f, + "MonorepoPackage:\n \"{}\" at \"{}\"\n with parent \"{}\" at \"{}\"", + &self.current_config.name, + &self.current_config.path.to_string_lossy(), + parent_config.name, + parent_config.path.to_string_lossy() + ) + } + } + } +} +fn read_local_packages( + folder_path: &Path, + dependencies_from_config: &Vec, +) -> Result> { + let mut local_dependencies = AHashSet::::new(); + + for dep in dependencies_from_config { + // Monorepo packages are expected to be symlinked in node_modules. + if let Ok(dep_path) = folder_path + .join("node_modules") + .join(dep) + .canonicalize() + .map(helpers::StrippedVerbatimPath::to_stripped_verbatim_path) + { + let is_local = helpers::is_local_package(folder_path, &dep_path); + if is_local { + local_dependencies.insert(dep.to_string()); + } + + debug!( + "Dependency \"{}\" is a {}local package at \"{}\"", + dep, + if is_local { "" } else { "non-" }, + dep_path.display() + ); + } + } + + Ok(local_dependencies) +} + +fn monorepo_or_single_project(path: &Path, current_config: Config) -> Result { + let local_dependencies = match ¤t_config.dependencies { + None => AHashSet::::new(), + Some(deps) => read_local_packages(path, deps)?, + }; + let local_dev_dependencies = match ¤t_config.dev_dependencies { + None => AHashSet::::new(), + Some(deps) => read_local_packages(path, deps)?, + }; + if local_dependencies.is_empty() && local_dev_dependencies.is_empty() { + Ok(ProjectContext { + current_config, + monorepo_context: None, + }) + } else { + Ok(ProjectContext { + current_config, + monorepo_context: Some(MonoRepoContext::MonorepoRoot { + local_dependencies, + local_dev_dependencies, + }), + }) + } +} + +fn is_config_listed_in_workspace(current_config: &Config, workspace_config: &Config) -> bool { + workspace_config + .dependencies + .to_owned() + .unwrap_or_default() + .iter() + .any(|dep| dep == ¤t_config.name) + || workspace_config + .dev_dependencies + .to_owned() + .unwrap_or_default() + .iter() + .any(|dep| dep == ¤t_config.name) +} + +impl ProjectContext { + pub fn new(path: &Path) -> Result { + let path = helpers::get_abs_path(path); + let current_config = packages::read_config(&path) + .map_err(|_| anyhow!("Could not read rescript.json at {}", path.to_string_lossy()))?; + let nearest_parent_config_path = match path.parent() { + None => Err(anyhow!( + "The current path \"{}\" does not have a parent folder", + path.to_string_lossy() + )), + Some(parent) => Ok(helpers::get_nearest_config(parent)), + }?; + let context = match nearest_parent_config_path { + None => monorepo_or_single_project(&path, current_config), + Some(parent_config_path) => { + match packages::read_config(parent_config_path.as_path()) { + Err(e) => Err(anyhow!( + "Could not read the parent config at {}: {}", + parent_config_path.to_string_lossy(), + e + )), + Ok(workspace_config) + if is_config_listed_in_workspace(¤t_config, &workspace_config) => + { + // There is a parent rescript.json, and it has a reference to the current package. + Ok(ProjectContext { + current_config, + monorepo_context: Some(MonoRepoContext::MonorepoPackage { + parent_config: Box::new(workspace_config), + }), + }) + } + Ok(_) => { + // There is a parent rescript.json, but it has no reference to the current package. + // However, the current package could still be a monorepo root! + monorepo_or_single_project(&path, current_config) + } + } + } + }; + context.iter().for_each(|pc| { + debug!("Created project context {:#?} for \"{}\"", pc, path.display()); + }); + context + } + + pub fn get_root_config(&self) -> &Config { + match &self.monorepo_context { + None => &self.current_config, + Some(MonoRepoContext::MonorepoRoot { .. }) => &self.current_config, + Some(MonoRepoContext::MonorepoPackage { parent_config }) => parent_config, + } + } + + pub fn get_root_path(&self) -> &Path { + self.get_root_config().path.parent().unwrap() + } + + pub fn get_suffix(&self) -> String { + self.get_root_config() + .suffix + .clone() + .unwrap_or(String::from(".res.mjs")) + } + + /// Returns the local packages relevant for the current context. + /// Either a single project, all projects from a monorepo or a single package inside a monorepo. + pub fn get_scoped_local_packages(&self, include_dev_deps: bool) -> AHashSet { + let mut local_packages = AHashSet::::new(); + match &self.monorepo_context { + None => { + local_packages.insert(self.current_config.name.clone()); + } + Some(MonoRepoContext::MonorepoRoot { + local_dependencies, + local_dev_dependencies, + }) => { + local_packages.insert(self.current_config.name.clone()); + local_packages.extend(local_dependencies.iter().cloned()); + if include_dev_deps { + local_packages.extend(local_dev_dependencies.iter().cloned()); + } + } + Some(MonoRepoContext::MonorepoPackage { .. }) => { + local_packages.insert(self.current_config.name.clone()); + } + }; + local_packages + } +} diff --git a/rewatch/testrepo/package.json b/rewatch/testrepo/package.json index f42be392d8..3bb995984a 100644 --- a/rewatch/testrepo/package.json +++ b/rewatch/testrepo/package.json @@ -13,7 +13,9 @@ "packages/nonexisting-dev-files", "packages/deprecated-config", "packages/file-casing", - "packages/file-casing-no-namespace" + "packages/file-casing-no-namespace", + "packages/pure-dev", + "packages/with-ppx" ] }, "dependencies": { diff --git a/rewatch/testrepo/packages/pure-dev/dev/RealDev.res b/rewatch/testrepo/packages/pure-dev/dev/RealDev.res new file mode 100644 index 0000000000..cd25d227f7 --- /dev/null +++ b/rewatch/testrepo/packages/pure-dev/dev/RealDev.res @@ -0,0 +1 @@ +let dev = true \ No newline at end of file diff --git a/rewatch/testrepo/packages/pure-dev/package.json b/rewatch/testrepo/packages/pure-dev/package.json new file mode 100644 index 0000000000..0276294558 --- /dev/null +++ b/rewatch/testrepo/packages/pure-dev/package.json @@ -0,0 +1,9 @@ +{ + "name": "@testrepo/pure-dev", + "version": "0.0.1", + "keywords": [ + "rescript" + ], + "author": "", + "license": "MIT" +} diff --git a/rewatch/testrepo/packages/pure-dev/rescript.json b/rewatch/testrepo/packages/pure-dev/rescript.json new file mode 100644 index 0000000000..7eb6700ad8 --- /dev/null +++ b/rewatch/testrepo/packages/pure-dev/rescript.json @@ -0,0 +1,8 @@ +{ + "name": "@testrepo/nonexisting-dev-files", + "sources": { + "dir": "dev", + "subdirs": true, + "type": "dev" + } +} diff --git a/rewatch/testrepo/packages/with-ppx/package.json b/rewatch/testrepo/packages/with-ppx/package.json new file mode 100644 index 0000000000..8843d691cd --- /dev/null +++ b/rewatch/testrepo/packages/with-ppx/package.json @@ -0,0 +1,14 @@ +{ + "name": "@testrepo/with-ppx", + "version": "0.0.1", + "keywords": [ + "rescript" + ], + "author": "", + "license": "MIT", + "dependencies": { + "rescript-nodejs": "16.1.0", + "sury": "^11.0.0-alpha.2", + "sury-ppx": "^11.0.0-alpha.2" + } +} diff --git a/rewatch/testrepo/packages/with-ppx/rescript.json b/rewatch/testrepo/packages/with-ppx/rescript.json new file mode 100644 index 0000000000..e8b4facce3 --- /dev/null +++ b/rewatch/testrepo/packages/with-ppx/rescript.json @@ -0,0 +1,20 @@ +{ + "name": "@testrepo/with-ppx", + "sources": [ + { + "dir": "src" + } + ], + "dependencies": [ + "rescript-nodejs", + "sury" + ], + "package-specs": { + "module": "es6", + "in-source": true + }, + "suffix": ".res.js", + "ppx-flags": [ + "sury-ppx/bin" + ] +} \ No newline at end of file diff --git a/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.mjs b/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.mjs new file mode 100644 index 0000000000..23929387dd --- /dev/null +++ b/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.mjs @@ -0,0 +1,17 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as S from "sury/src/S.mjs"; + +let schema = S.schema(s => ({ + foo: s.m(S.string) +})); + +let foo = S.parseOrThrow("{ \"foo\": \"bar\" }", schema); + +console.log(foo); + +export { + schema, + foo, +} +/* schema Not a pure module */ diff --git a/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.res b/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.res new file mode 100644 index 0000000000..1d80e5b98a --- /dev/null +++ b/rewatch/testrepo/packages/with-ppx/src/FileWithPpx.res @@ -0,0 +1,6 @@ +@schema +type t = {foo: string} + +let foo = S.parseOrThrow(`{ "foo": "bar" }`, schema) + +Console.log(foo) \ No newline at end of file diff --git a/rewatch/testrepo/rescript.json b/rewatch/testrepo/rescript.json index ae437e38dd..6085fed3a3 100644 --- a/rewatch/testrepo/rescript.json +++ b/rewatch/testrepo/rescript.json @@ -25,6 +25,10 @@ "@testrepo/nonexisting-dev-files", "@testrepo/deprecated-config", "@testrepo/file-casing", - "@testrepo/file-casing-no-namespace" + "@testrepo/file-casing-no-namespace", + "@testrepo/with-ppx" + ], + "dev-dependencies": [ + "@testrepo/pure-dev" ] } diff --git a/rewatch/testrepo/yarn.lock b/rewatch/testrepo/yarn.lock index 808197041f..7712ac2c52 100644 --- a/rewatch/testrepo/yarn.lock +++ b/rewatch/testrepo/yarn.lock @@ -114,6 +114,12 @@ __metadata: languageName: unknown linkType: soft +"@testrepo/pure-dev@workspace:packages/pure-dev": + version: 0.0.0-use.local + resolution: "@testrepo/pure-dev@workspace:packages/pure-dev" + languageName: unknown + linkType: soft + "@testrepo/with-dev-deps@workspace:packages/with-dev-deps": version: 0.0.0-use.local resolution: "@testrepo/with-dev-deps@workspace:packages/with-dev-deps" @@ -123,6 +129,16 @@ __metadata: languageName: unknown linkType: soft +"@testrepo/with-ppx@workspace:packages/with-ppx": + version: 0.0.0-use.local + resolution: "@testrepo/with-ppx@workspace:packages/with-ppx" + dependencies: + rescript-nodejs: "npm:16.1.0" + sury: "npm:^11.0.0-alpha.2" + sury-ppx: "npm:^11.0.0-alpha.2" + languageName: unknown + linkType: soft + "rescript-nodejs@npm:16.1.0": version: 16.1.0 resolution: "rescript-nodejs@npm:16.1.0" @@ -160,6 +176,27 @@ __metadata: languageName: node linkType: hard +"sury-ppx@npm:^11.0.0-alpha.2": + version: 11.0.0-alpha.2 + resolution: "sury-ppx@npm:11.0.0-alpha.2" + peerDependencies: + sury: ^11.0.0-alpha.2 + checksum: 10c0/ae9190fa4e406de46e88b67db233e757db36f5377301227cf5b084b5b81d360725d6fc4781e24c56cb87476cd3a42af5acc0cfc49f0c7ea17435caf065ba22ab + languageName: node + linkType: hard + +"sury@npm:^11.0.0-alpha.2": + version: 11.0.0-alpha.2 + resolution: "sury@npm:11.0.0-alpha.2" + peerDependencies: + rescript: 11.x + peerDependenciesMeta: + rescript: + optional: true + checksum: 10c0/254dd708608b125defc6b4be0f038df0f6704290df60504b70b7cd613f1e840d150ff65a3f23dbc7213f2b18b86fdc60400b4361ca46f5c86bbe7360eff9c84a + languageName: node + linkType: hard + "testrepo@workspace:.": version: 0.0.0-use.local resolution: "testrepo@workspace:." diff --git a/rewatch/tests/clean.sh b/rewatch/tests/clean.sh new file mode 100755 index 0000000000..f04aa14387 --- /dev/null +++ b/rewatch/tests/clean.sh @@ -0,0 +1,119 @@ +#!/bin/bash +cd $(dirname $0) +source "./utils.sh" +cd ../testrepo + +bold "Test: It should clean a single project" + +# First we build the entire monorepo +error_output=$(rewatch build 2>&1) +if [ $? -eq 0 ]; +then + success "Built monorepo" +else + error "Error building monorepo" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Then we clean a single project +error_output=$(cd packages/file-casing && "../../$REWATCH_EXECUTABLE" clean 2>&1) +clean_status=$? +if [ $clean_status -ne 0 ]; +then + error "Error cleaning current project file-casing" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Count compiled files in the cleaned project +project_compiled_files=$(find packages/file-casing -type f -name '*.mjs' | wc -l | tr -d '[:space:]') +if [ "$project_compiled_files" -eq 0 ]; +then + success "file-casing cleaned" +else + error "Expected 0 .mjs files in file-casing after clean, got $project_compiled_files" + printf "%s\n" "$error_output" + exit 1 +fi + +# Ensure other project files were not cleaned +other_project_compiled_files=$(find packages/new-namespace -type f -name '*.mjs' | wc -l | tr -d '[:space:]') +if [ "$other_project_compiled_files" -gt 0 ]; +then + success "Didn't clean other project files" + git restore . +else + error "Expected files from new-namespace not to be cleaned" + exit 1 +fi + +bold "--dev should clean dev-dependencies of monorepo" + +# First we build the entire monorepo (including --dev) +error_output=$(rewatch build --dev 2>&1) +if [ $? -eq 0 ]; +then + success "Built monorepo" +else + error "Error building monorepo" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Clean entire monorepo (including --dev) +error_output=$(rewatch clean --dev 2>&1) +clean_status=$? +if [ $clean_status -ne 0 ]; +then + error "Error cleaning current project file-casing" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Count compiled files in dev-dependency project "pure-dev" +project_compiled_files=$(find packages/pure-dev -type f -name '*.mjs' | wc -l | tr -d '[:space:]') +if [ "$project_compiled_files" -eq 0 ]; +then + success "pure-dev cleaned" + git restore . +else + error "Expected 0 .mjs files in pure-dev after clean, got $project_compiled_files" + printf "%s\n" "$error_output" + exit 1 +fi + +bold "Test: It should clean dependencies from node_modules" + +# Build a package with external dependencies +error_output=$(cd packages/with-dev-deps && "../../$REWATCH_EXECUTABLE" build 2>&1) +if [ $? -eq 0 ]; +then + success "Built with-dev-deps" +else + error "Error building with-dev-deps" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Then we clean a single project +error_output=$(cd packages/with-dev-deps && "../../$REWATCH_EXECUTABLE" clean 2>&1) +clean_status=$? +if [ $clean_status -ne 0 ]; +then + error "Error cleaning current project file-casing" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Count compiled files in the cleaned project +compiler_assets=$(find node_modules/rescript-nodejs/lib/ocaml -type f -name '*.*' | wc -l | tr -d '[:space:]') +if [ $compiler_assets -eq 0 ]; +then + success "compiler assets from node_modules cleaned" + git restore . +else + error "Expected 0 files in node_modules/rescript-nodejs/lib/ocaml after clean, got $compiler_assets" + printf "%s\n" "$error_output" + exit 1 +fi \ No newline at end of file diff --git a/rewatch/tests/compiler-args.sh b/rewatch/tests/compiler-args.sh new file mode 100755 index 0000000000..bbe455bfc0 --- /dev/null +++ b/rewatch/tests/compiler-args.sh @@ -0,0 +1,36 @@ +#!/bin/bash +cd $(dirname $0) +source "./utils.sh" +cd ../testrepo + +bold "Test: It should not matter where we grab the compiler-args for a file" +# Capture stdout for both invocations +stdout_root=$(rewatch compiler-args packages/file-casing/src/Consume.res 2>/dev/null) +stdout_pkg=$(cd packages/file-casing && "../../$REWATCH_EXECUTABLE" compiler-args src/Consume.res 2>/dev/null) + +error_output=$(rewatch compiler-args packages/file-casing/src/Consume.res 2>&1) +if [ $? -ne 0 ]; then + error "Error grabbing compiler args for packages/file-casing/src/Consume.res" + printf "%s\n" "$error_output" >&2 + exit 1 +fi +error_output=$(cd packages/file-casing && "../../$REWATCH_EXECUTABLE" compiler-args src/Consume.res 2>&1) +if [ $? -ne 0 ]; then + error "Error grabbing compiler args for src/Consume.res in packages/file-casing" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +# Compare the stdout of both runs; must be exactly identical +tmp1=$(mktemp); tmp2=$(mktemp) +trap 'rm -f "$tmp1" "$tmp2"' EXIT +printf "%s" "$stdout_root" > "$tmp1" +printf "%s" "$stdout_pkg" > "$tmp2" +if git diff --no-index --exit-code "$tmp1" "$tmp2" > /dev/null; then + success "compiler-args stdout is identical regardless of cwd" +else + error "compiler-args stdout differs depending on cwd" + echo "---- diff ----" + git diff --no-index "$tmp1" "$tmp2" || true + exit 1 +fi diff --git a/rewatch/tests/format.sh b/rewatch/tests/format.sh index ccfef0f4e4..2dd2df093a 100755 --- a/rewatch/tests/format.sh +++ b/rewatch/tests/format.sh @@ -4,15 +4,15 @@ cd ../testrepo bold "Test: It should format all files" git diff --name-only ./ -error_output=$("$REWATCH_EXECUTABLE" format --all) +error_output=$("$REWATCH_EXECUTABLE" format) git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) -if [ $? -eq 0 ] && [ $git_diff_file_count -eq 8 ]; +if [ $? -eq 0 ] && [ $git_diff_file_count -eq 9 ]; then success "Test package formatted. Got $git_diff_file_count changed files." git restore . else error "Error formatting test package" - echo "Expected 8 files to be changed, got $git_diff_file_count" + echo "Expected 9 files to be changed, got $git_diff_file_count" echo $error_output exit 1 fi @@ -42,3 +42,33 @@ else echo $error_output exit 1 fi + +bold "Test: It should format only the current project" + +error_output=$(cd packages/file-casing && "../../$REWATCH_EXECUTABLE" format) +git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) +if [ $? -eq 0 ] && [ $git_diff_file_count -eq 2 ]; +then + success "file-casing formatted" + git restore . +else + error "Error formatting current project file-casing" + echo "Expected 2 files to be changed, got $git_diff_file_count" + echo $error_output + exit 1 +fi + +bold "Test: it should format dev package as well" + +error_output=$("$REWATCH_EXECUTABLE" format --dev) +git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) +if [ $? -eq 0 ] && [ $git_diff_file_count -eq 10 ]; +then + success "All packages (including dev) were formatted. Got $git_diff_file_count changed files." + git restore . +else + error "Error formatting test package" + echo "Expected 9 files to be changed, got $git_diff_file_count" + echo $error_output + exit 1 +fi \ No newline at end of file diff --git a/rewatch/tests/snapshots/bs-dev-dependency-used-by-non-dev-source.txt b/rewatch/tests/snapshots/bs-dev-dependency-used-by-non-dev-source.txt index 04222b84a0..dcb7d2a84c 100644 --- a/rewatch/tests/snapshots/bs-dev-dependency-used-by-non-dev-source.txt +++ b/rewatch/tests/snapshots/bs-dev-dependency-used-by-non-dev-source.txt @@ -1,4 +1,4 @@ -Cleaned 0/60 +Cleaned 0/67 Parsed 2 source files Compiled 2 modules diff --git a/rewatch/tests/snapshots/dependency-cycle.txt b/rewatch/tests/snapshots/dependency-cycle.txt index cc25fb9c92..d188bae03b 100644 --- a/rewatch/tests/snapshots/dependency-cycle.txt +++ b/rewatch/tests/snapshots/dependency-cycle.txt @@ -1,4 +1,4 @@ -Cleaned 0/60 +Cleaned 0/67 Parsed 1 source files Compiled 0 modules diff --git a/rewatch/tests/snapshots/remove-file.txt b/rewatch/tests/snapshots/remove-file.txt index a19c44e596..f29d684156 100644 --- a/rewatch/tests/snapshots/remove-file.txt +++ b/rewatch/tests/snapshots/remove-file.txt @@ -1,4 +1,4 @@ -Cleaned 1/60 +Cleaned 1/67 Parsed 0 source files Compiled 1 modules diff --git a/rewatch/tests/snapshots/rename-file-internal-dep-namespace.txt b/rewatch/tests/snapshots/rename-file-internal-dep-namespace.txt index a0e20f68f0..dbfd47cfa9 100644 --- a/rewatch/tests/snapshots/rename-file-internal-dep-namespace.txt +++ b/rewatch/tests/snapshots/rename-file-internal-dep-namespace.txt @@ -1,4 +1,4 @@ -Cleaned 2/60 +Cleaned 2/67 Parsed 2 source files Compiled 3 modules diff --git a/rewatch/tests/snapshots/rename-file-internal-dep.txt b/rewatch/tests/snapshots/rename-file-internal-dep.txt index 34d9fc9c90..e2e6058d9d 100644 --- a/rewatch/tests/snapshots/rename-file-internal-dep.txt +++ b/rewatch/tests/snapshots/rename-file-internal-dep.txt @@ -1,4 +1,4 @@ -Cleaned 2/60 +Cleaned 2/67 Parsed 2 source files Compiled 2 modules diff --git a/rewatch/tests/snapshots/rename-file-with-interface.txt b/rewatch/tests/snapshots/rename-file-with-interface.txt index fa2b3dcd73..a5a1eba38b 100644 --- a/rewatch/tests/snapshots/rename-file-with-interface.txt +++ b/rewatch/tests/snapshots/rename-file-with-interface.txt @@ -1,5 +1,5 @@  No implementation file found for interface file (skipping): src/ModuleWithInterface.resi -Cleaned 2/60 +Cleaned 2/67 Parsed 1 source files Compiled 2 modules diff --git a/rewatch/tests/snapshots/rename-file.txt b/rewatch/tests/snapshots/rename-file.txt index 388a7fa123..12816226ad 100644 --- a/rewatch/tests/snapshots/rename-file.txt +++ b/rewatch/tests/snapshots/rename-file.txt @@ -1,4 +1,4 @@ -Cleaned 1/60 +Cleaned 1/67 Parsed 1 source files Compiled 1 modules diff --git a/rewatch/tests/snapshots/rename-interface-file.txt b/rewatch/tests/snapshots/rename-interface-file.txt index 7ab0b0c690..c422225d07 100644 --- a/rewatch/tests/snapshots/rename-interface-file.txt +++ b/rewatch/tests/snapshots/rename-interface-file.txt @@ -1,5 +1,5 @@  No implementation file found for interface file (skipping): src/ModuleWithInterface2.resi -Cleaned 1/60 +Cleaned 1/67 Parsed 1 source files Compiled 2 modules diff --git a/rewatch/tests/suffix.sh b/rewatch/tests/suffix.sh index aa67fcfde9..32450befea 100755 --- a/rewatch/tests/suffix.sh +++ b/rewatch/tests/suffix.sh @@ -31,7 +31,7 @@ fi # Count files with new extension file_count=$(find ./packages -name *.res.js | wc -l) -if [ "$file_count" -eq 36 ]; +if [ "$file_count" -eq 38 ]; then success "Found files with correct suffix" else diff --git a/rewatch/tests/suite-ci.sh b/rewatch/tests/suite-ci.sh index 76cc50b21c..fc1d615643 100755 --- a/rewatch/tests/suite-ci.sh +++ b/rewatch/tests/suite-ci.sh @@ -18,8 +18,11 @@ export RESCRIPT_BSC_EXE source ./utils.sh +bold "Yarn install" +(cd ../testrepo && yarn) + bold "Rescript version" -(cd ../testrepo && ./node_modules/.bin/rescript -v) +(cd ../testrepo && ./node_modules/.bin/rescript --version) # we need to reset the yarn.lock and package.json to the original state # so there is not diff in git. The CI will install new ReScript package @@ -40,4 +43,4 @@ else exit 1 fi -./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh && ./format.sh +./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh && ./format.sh && ./clean.sh && ./compiler-args.sh \ No newline at end of file diff --git a/rewatch/tests/watch.sh b/rewatch/tests/watch.sh index 314d74dc83..229cd01f97 100755 --- a/rewatch/tests/watch.sh +++ b/rewatch/tests/watch.sh @@ -18,12 +18,33 @@ exit_watcher() { rm lib/rescript.lock } -rewatch_bg watch > /dev/null 2>&1 & +# Wait until a file exists (with timeout in seconds, default 30) +wait_for_file() { + local file="$1"; local timeout="${2:-30}" + while [ "$timeout" -gt 0 ]; do + [ -f "$file" ] && return 0 + sleep 1 + timeout=$((timeout - 1)) + done + return 1 +} + +# Start watcher and capture logs for debugging +rewatch_bg watch > rewatch.log 2>&1 & success "Watcher Started" +# Trigger a recompilation echo 'Js.log("added-by-test")' >> ./packages/main/src/Main.res -sleep 2 +# Wait for the compiled JS to show up (Windows CI can be slower) +target=./packages/main/src/Main.mjs +if ! wait_for_file "$target" 10; then + error "Expected output not found: $target" + ls -la ./packages/main/src || true + tail -n 200 rewatch.log || true + exit_watcher + exit 1 +fi if node ./packages/main/src/Main.mjs | grep 'added-by-test' &> /dev/null; then