Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#### :bug: Bug fix

- Fix ppx resolution with package inside monorepo. https://github.com/rescript-lang/rescript/pull/7776

#### :memo: Documentation

#### :nail_care: Polish
Expand Down
2 changes: 1 addition & 1 deletion rewatch/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub fn get_compiler_args(rescript_file_path: &Path) -> Result<String> {
&project_context.current_config,
relative_filename,
&contents,
);
)?;
let is_interface = filename.to_string_lossy().ends_with('i');
let has_interface = if is_interface {
true
Expand Down
43 changes: 1 addition & 42 deletions rewatch/src/build/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,48 +255,7 @@ pub fn read_dependency(
package_config: &Config,
project_context: &ProjectContext,
) -> Result<PathBuf> {
// package folder + node_modules + package_name
// This can happen in the following scenario:
// The ProjectContext has a MonoRepoContext::MonorepoRoot.
// We are reading a dependency from the root package.
// And that local dependency has a hoisted dependency.
let path_from_current_package = package_config
.path
.parent()
.ok_or_else(|| {
anyhow!(
"Expected {} to have a parent folder",
package_config.path.to_string_lossy()
)
})
.map(|parent_path| helpers::package_path(parent_path, package_name))?;

// current folder + node_modules + package_name
let path_from_current_config = project_context
.current_config
.path
.parent()
.ok_or_else(|| {
anyhow!(
"Expected {} to have a parent folder",
project_context.current_config.path.to_string_lossy()
)
})
.map(|parent_path| helpers::package_path(parent_path, package_name))?;

// 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_current_package.exists() {
Ok(path_from_current_package)
} else if path_from_current_config.exists() {
Ok(path_from_current_config)
} else if path_from_root.exists() {
Ok(path_from_root)
} else {
Err(anyhow!(
"The package \"{package_name}\" is not found (are node_modules up-to-date?)..."
))
})?;
let path = helpers::try_package_path(package_config, project_context, package_name)?;

let canonical_path = match path
.canonicalize()
Expand Down
34 changes: 21 additions & 13 deletions rewatch/src/build/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::config::{Config, OneOrMore};
use crate::helpers;
use crate::project_context::ProjectContext;
use ahash::AHashSet;
use anyhow::anyhow;
use log::debug;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -51,11 +52,13 @@ pub fn generate_asts(
package.to_owned(),
&source_file.implementation.path.to_owned(),
build_state,
);
)
.map_err(|e| e.to_string());

let iast_result = match source_file.interface.as_ref().map(|i| i.path.to_owned()) {
Some(interface_file_path) => {
generate_ast(package.to_owned(), &interface_file_path.to_owned(), build_state)
.map_err(|e| e.to_string())
.map(Some)
}
_ => Ok(None),
Expand Down Expand Up @@ -237,16 +240,15 @@ pub fn parser_args(
package_config: &Config,
filename: &Path,
contents: &str,
) -> (PathBuf, Vec<String>) {
) -> anyhow::Result<(PathBuf, Vec<String>)> {
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(
node_modules_path.as_path(),
project_context,
package_config,
&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();
Expand All @@ -255,7 +257,7 @@ pub fn parser_args(

let file = PathBuf::from("..").join("..").join(file);

(
Ok((
ast_path.to_owned(),
[
ppx_flags,
Expand All @@ -273,20 +275,20 @@ pub fn parser_args(
],
]
.concat(),
)
))
}

fn generate_ast(
package: Package,
filename: &Path,
build_state: &BuildState,
) -> Result<(PathBuf, Option<helpers::StdErr>), String> {
) -> anyhow::Result<(PathBuf, Option<helpers::StdErr>)> {
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(&build_state.project_context, &package.config, filename, &contents);
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());
Expand All @@ -298,7 +300,13 @@ fn generate_ast(
.current_dir(&build_path_abs)
.args(parser_args)
.output()
.expect("Error converting .res to .ast"),
.map_err(|e| {
anyhow!(
"Error running bsc for parsing {}: {}",
filename.to_string_lossy(),
e
)
})?,
) {
Some(res_to_ast) => {
let stderr = String::from_utf8_lossy(&res_to_ast.stderr).to_string();
Expand All @@ -307,7 +315,7 @@ fn generate_ast(
if res_to_ast.status.success() {
Ok((ast_path, Some(stderr.to_string())))
} else {
Err(format!("Error in {}:\n{}", package.name, stderr))
Err(anyhow!("Error in {}:\n{}", package.name, stderr))
}
} else {
Ok((ast_path, None))
Expand All @@ -316,7 +324,7 @@ fn generate_ast(
_ => {
log::info!("Parsing file {}...", filename.display());

Err(format!(
Err(anyhow!(
"Could not find canonicalize_string_path for file {} in package {}",
filename.display(),
package.name
Expand Down
71 changes: 39 additions & 32 deletions rewatch/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::build::packages;
use crate::helpers;
use crate::helpers::deserialize::*;
use crate::project_context::ProjectContext;
use anyhow::{Result, bail};
use convert_case::{Case, Casing};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use std::path::{MAIN_SEPARATOR, Path, PathBuf};

#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
Expand Down Expand Up @@ -296,56 +298,61 @@ pub fn flatten_flags(flags: &Option<Vec<OneOrMore<String>>>) -> Vec<String> {
/// Since ppx-flags could be one or more, and could potentially be nested, this function takes the
/// flags and flattens them.
pub fn flatten_ppx_flags(
node_modules_dir: &Path,
project_context: &ProjectContext,
package_config: &Config,
flags: &Option<Vec<OneOrMore<String>>>,
package_name: &String,
) -> Vec<String> {
) -> Result<Vec<String>> {
match flags {
None => vec![],
Some(flags) => flags
.iter()
.flat_map(|x| match x {
None => Ok(vec![]),
Some(flags) => flags.iter().try_fold(Vec::new(), |mut acc, x| {
match x {
OneOrMore::Single(y) => {
let first_character = y.chars().next();
match first_character {
Some('.') => {
vec![
"-ppx".to_string(),
node_modules_dir
.join(package_name)
.join(y)
.to_string_lossy()
.to_string(),
]
let path = helpers::try_package_path(
package_config,
project_context,
format!("{}{}{}", &package_config.name, MAIN_SEPARATOR, y).as_str(),
Copy link
Preview

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string formatting with MAIN_SEPARATOR could be simplified using Path::join() which handles path separators correctly across platforms.

Suggested change
format!("{}{}{}", &package_config.name, MAIN_SEPARATOR, y).as_str(),
Path::new(&package_config.name).join(y).to_str().unwrap(),

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this really, making a Path and calling unwrap later seems worse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use path concat, I think this is the way to constructs paths, and it works on every platform. Generally in the codebase I use to_string_lossy and then you don't need unwrap.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw the function try_package_path should recieve a Path

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it receives a package name or a path relative to the current package I think? Perhaps we can pass in a variant of Name(String) | Path({package_name, path}) or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw the function try_package_path should recieve a Path

@jfrolich you mean pass package_name as Path?

)
.map(|p| p.to_string_lossy().to_string())?;

acc.push(String::from("-ppx"));
acc.push(path);
}
_ => {
acc.push(String::from("-ppx"));
let path = helpers::try_package_path(package_config, project_context, y)
.map(|p| p.to_string_lossy().to_string())?;
acc.push(path);
}
_ => vec![
"-ppx".to_string(),
node_modules_dir.join(y).to_string_lossy().to_string(),
],
}
}
OneOrMore::Multiple(ys) if ys.is_empty() => vec![],
OneOrMore::Multiple(ys) if ys.is_empty() => (),
OneOrMore::Multiple(ys) => {
let first_character = ys[0].chars().next();
let ppx = match first_character {
Some('.') => node_modules_dir
.join(package_name)
.join(&ys[0])
.to_string_lossy()
.to_string(),
_ => node_modules_dir.join(&ys[0]).to_string_lossy().to_string(),
Some('.') => helpers::try_package_path(
package_config,
project_context,
format!("{}{}{}", package_config.name, MAIN_SEPARATOR, &ys[0]).as_str(),
Copy link
Preview

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous occurrence, using Path::join() would be more idiomatic and handle cross-platform path separators automatically.

Suggested change
format!("{}{}{}", package_config.name, MAIN_SEPARATOR, &ys[0]).as_str(),
Path::new(&package_config.name).join(&ys[0]).to_string_lossy().as_ref(),

Copilot uses AI. Check for mistakes.

)
.map(|p| p.to_string_lossy().to_string())?,
_ => helpers::try_package_path(package_config, project_context, &ys[0])
.map(|p| p.to_string_lossy().to_string())?,
};
vec![
"-ppx".to_string(),
acc.push(String::from("-ppx"));
acc.push(
vec![ppx]
.into_iter()
.chain(ys[1..].to_owned())
.collect::<Vec<String>>()
.join(" "),
]
);
}
})
.collect::<Vec<String>>(),
};
Ok(acc)
}),
}
}

Expand Down
57 changes: 57 additions & 0 deletions rewatch/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::build::packages;
use crate::config::Config;
use crate::helpers;
use crate::project_context::ProjectContext;
use anyhow::anyhow;
use std::ffi::OsString;
use std::fs;
use std::fs::File;
Expand Down Expand Up @@ -103,6 +106,60 @@ pub fn package_path(root: &Path, package_name: &str) -> PathBuf {
root.join("node_modules").join(package_name)
}

/// Tries to find a path for input package_name.
/// The node_modules folder may be found at different levels in the case of a monorepo.
/// This helper tries a variety of paths.
pub fn try_package_path(
package_config: &Config,
project_context: &ProjectContext,
package_name: &str,
) -> anyhow::Result<PathBuf> {
// package folder + node_modules + package_name
// This can happen in the following scenario:
// The ProjectContext has a MonoRepoContext::MonorepoRoot.
// We are reading a dependency from the root package.
// And that local dependency has a hoisted dependency.
// Example, we need to find package_name `foo` in the following scenario:
// root/packages/a/node_modules/foo
let path_from_current_package = package_config
.path
.parent()
.ok_or_else(|| {
anyhow!(
"Expected {} to have a parent folder",
package_config.path.to_string_lossy()
)
})
.map(|parent_path| helpers::package_path(parent_path, package_name))?;

// current folder + node_modules + package_name
let path_from_current_config = project_context
.current_config
.path
.parent()
.ok_or_else(|| {
anyhow!(
"Expected {} to have a parent folder",
project_context.current_config.path.to_string_lossy()
)
})
.map(|parent_path| package_path(parent_path, package_name))?;

// root folder + node_modules + package_name
let path_from_root = package_path(project_context.get_root_path(), package_name);
if path_from_current_package.exists() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the exists are quite expensive for every ppx run (== every file), can we not have one resolution per situation?
Like ./my_ppx/bla always resolves to the the current package, @my_package/ppx always resolves to the location in the package (which we scanned so we know).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This of course specifically for the ppx resolution, and I know this is also used for the package scanning.
For package scanning, I think we can make path_from_current_config and path_from_current_package more efficient because they are the same in 99% of the cases, if they are the same we don't have to check both.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I am saying that for ppx resolution we can probably create a new function that can look up the paths for the package based on the scanning that was done earlier instead of scanning again (and special casing the relative path).

Ok(path_from_current_package)
} else if path_from_current_config.exists() {
Ok(path_from_current_config)
} else if path_from_root.exists() {
Ok(path_from_root)
} else {
Err(anyhow!(
"The package \"{package_name}\" is not found (are node_modules up-to-date?)..."
))
}
}

pub fn get_abs_path(path: &Path) -> PathBuf {
let abs_path_buf = PathBuf::from(path);

Expand Down
Loading