Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
288 changes: 2 additions & 286 deletions src/core/repo_graph.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use serde_json::Value as JsonValue;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
Expand All @@ -7,6 +6,7 @@ use toml::Value as TomlValue;
mod generic;
mod go;
mod impact;
mod node;
mod types;

pub use impact::analyze_impact;
Expand All @@ -18,7 +18,7 @@ pub fn inspect_repo(repo_path: impl AsRef<Path>) -> RepoInspection {
let mut builder = RepoGraphBuilder::new(display_path(&root));

detect_rust(&root, &mut builder);
detect_node(&root, &mut builder);
node::detect_node(&root, &mut builder);
detect_python(&root, &mut builder);
go::detect_go(&root, &mut builder);
generic::detect_generic(&root, &mut builder);
Expand Down Expand Up @@ -730,168 +730,6 @@ fn detect_cargo_targets(
}
}

fn detect_node(root: &Path, builder: &mut RepoGraphBuilder) {
let package_json = root.join("package.json");
if package_json.exists() {
let manifest_evidence = builder.add_detected_file(
Path::new("package.json"),
DetectedFileKind::Manifest,
"manifest",
None,
"Node package manifest detected.",
);
builder.add_package_manager(PackageManagerKind::Npm, "npm", manifest_evidence.clone());
if !root.join("package-lock.json").exists()
&& !root.join("pnpm-lock.yaml").exists()
&& !root.join("yarn.lock").exists()
{
builder.add_warning(
DetectionSeverity::Info,
DetectionCategory::AmbiguousDetection,
"package.json was found without a lockfile; npm is treated as the default package manager hint.",
Some(Path::new("package.json")),
Some(manifest_evidence.clone()),
);
}

match read_json(&package_json) {
Ok(manifest) => {
if let Some(name) = manifest.get("name").and_then(JsonValue::as_str) {
let evidence_id = builder.add_evidence(
Path::new("package.json"),
"manifest",
Some("name"),
"Node package name.",
);
builder.add_component(
"component-node-package",
name,
"node_package",
".",
vec![
"package.json".to_string(),
"src/**".to_string(),
"test/**".to_string(),
"tests/**".to_string(),
],
evidence_id,
);
builder.add_relationship(
RelationshipKind::UsesPackageManager,
"component-node-package",
"package-manager-npm",
manifest_evidence.clone(),
);
}

if manifest.get("workspaces").is_some() {
if let Some(workspaces) = extract_package_json_workspaces(&manifest) {
let evidence_id = builder.add_evidence(
Path::new("package.json"),
"manifest",
Some("workspaces"),
"Node workspace members.",
);
builder.add_workspace(
"workspace-node",
"node-workspace",
workspaces,
evidence_id,
);
} else {
builder.add_warning(
DetectionSeverity::Warning,
DetectionCategory::UnsupportedPattern,
"package.json workspaces field is present but not in a supported array/packages shape.",
Some(Path::new("package.json")),
Some(manifest_evidence.clone()),
);
}
}

if let Some(scripts) = manifest.get("scripts").and_then(JsonValue::as_object) {
let has_test_script =
add_node_script(builder, scripts, "test", RepoCommandKind::Test, 0.9);
add_node_script(builder, scripts, "build", RepoCommandKind::Build, 0.85);
add_node_script(builder, scripts, "lint", RepoCommandKind::Lint, 0.85);
add_node_script(builder, scripts, "check", RepoCommandKind::Check, 0.8);
add_node_script(
builder,
scripts,
"typecheck",
RepoCommandKind::Typecheck,
0.8,
);
if !has_test_script {
builder.add_warning(
DetectionSeverity::Info,
DetectionCategory::MissingCommand,
"package.json does not define scripts.test; no Node test target was inferred.",
Some(Path::new("package.json")),
Some(manifest_evidence.clone()),
);
}
} else {
builder.add_warning(
DetectionSeverity::Info,
DetectionCategory::MissingCommand,
"package.json does not define scripts; no Node commands were inferred.",
Some(Path::new("package.json")),
Some(manifest_evidence.clone()),
);
}
}
Err(message) => builder.add_warning(
DetectionSeverity::Error,
manifest_warning_category(&message),
&message,
Some(Path::new("package.json")),
Some(manifest_evidence),
),
}
}

detect_lockfile(
builder,
root,
"package-lock.json",
PackageManagerKind::Npm,
"npm",
);
detect_lockfile(
builder,
root,
"pnpm-lock.yaml",
PackageManagerKind::Pnpm,
"pnpm",
);
detect_lockfile(builder, root, "yarn.lock", PackageManagerKind::Yarn, "yarn");

let pnpm_workspace = root.join("pnpm-workspace.yaml");
if pnpm_workspace.exists() {
let evidence_id = builder.add_detected_file(
Path::new("pnpm-workspace.yaml"),
DetectedFileKind::WorkspaceConfig,
"workspace_config",
None,
"pnpm workspace config detected.",
);
builder.add_package_manager(PackageManagerKind::Pnpm, "pnpm", evidence_id.clone());
let members = parse_simple_yaml_packages(&pnpm_workspace);
if !members.is_empty() {
builder.add_workspace("workspace-pnpm", "pnpm-workspace", members, evidence_id);
} else {
builder.add_warning(
DetectionSeverity::Warning,
DetectionCategory::UnsupportedPattern,
"pnpm-workspace.yaml was detected but no supported packages list was parsed.",
Some(Path::new("pnpm-workspace.yaml")),
Some(evidence_id),
);
}
}
}

fn detect_python(root: &Path, builder: &mut RepoGraphBuilder) {
let mut python_project_detected = false;
let mut python_project_evidence = None;
Expand Down Expand Up @@ -1063,68 +901,6 @@ fn detect_python(root: &Path, builder: &mut RepoGraphBuilder) {
}
}

fn add_node_script(
builder: &mut RepoGraphBuilder,
scripts: &serde_json::Map<String, JsonValue>,
script_name: &str,
kind: RepoCommandKind,
confidence: f32,
) -> bool {
if scripts
.get(script_name)
.and_then(JsonValue::as_str)
.is_some()
{
let evidence_id = builder.add_evidence(
Path::new("package.json"),
"manifest",
Some(&format!("scripts.{script_name}")),
"Node package script detected.",
);
let command = format!("npm run {script_name}");
builder.add_command(
&format!("cmd-npm-{script_name}"),
kind.clone(),
&command,
Some("component-node-package"),
confidence,
evidence_id.clone(),
);
if matches!(kind, RepoCommandKind::Test) {
builder.add_test(
&format!("test-npm-{script_name}"),
script_name,
&command,
Some("component-node-package"),
confidence,
evidence_id,
);
}
true
} else {
false
}
}

fn detect_lockfile(
builder: &mut RepoGraphBuilder,
root: &Path,
file_name: &str,
kind: PackageManagerKind,
name: &str,
) {
if root.join(file_name).exists() {
let evidence_id = builder.add_detected_file(
Path::new(file_name),
DetectedFileKind::Lockfile,
"lockfile",
None,
&format!("{name} lockfile detected."),
);
builder.add_package_manager(kind, name, evidence_id);
}
}

fn detect_python_lockfile(
builder: &mut RepoGraphBuilder,
root: &Path,
Expand Down Expand Up @@ -1255,59 +1031,6 @@ fn dependency_name_matches(dependency: &str, package_name: &str) -> bool {
.is_some_and(|character| matches!(character, '=' | '<' | '>' | '~' | '[' | '!' | ' '))
}

fn extract_package_json_workspaces(manifest: &JsonValue) -> Option<Vec<String>> {
let workspaces = manifest.get("workspaces")?;

if let Some(items) = workspaces.as_array() {
let members = items
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_string)
.collect::<Vec<_>>();
return (!members.is_empty()).then_some(members);
}

let packages = workspaces.get("packages")?.as_array()?;
let members = packages
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_string)
.collect::<Vec<_>>();
(!members.is_empty()).then_some(members)
}

fn parse_simple_yaml_packages(path: &Path) -> Vec<String> {
let Ok(contents) = fs::read_to_string(path) else {
return Vec::new();
};

let mut in_packages = false;
let mut members = Vec::new();

for line in contents.lines() {
let trimmed = line.trim();
if trimmed == "packages:" {
in_packages = true;
continue;
}

if in_packages && trimmed.starts_with('-') {
let member = trimmed
.trim_start_matches('-')
.trim()
.trim_matches('"')
.trim_matches('\'');
if !member.is_empty() {
members.push(member.to_string());
}
} else if in_packages && !trimmed.is_empty() && !line.starts_with(' ') {
break;
}
}

members
}

fn detect_ignored_paths(root: &Path, builder: &mut RepoGraphBuilder) {
for (ignored_path, emit_warning) in [
(".git", false),
Expand Down Expand Up @@ -1427,13 +1150,6 @@ fn read_toml(path: &Path) -> Result<TomlValue, String> {
.map_err(|error| format!("Failed to parse {}: {error}", normalize_path(path)))
}

fn read_json(path: &Path) -> Result<JsonValue, String> {
let contents = fs::read_to_string(path)
.map_err(|error| format!("Failed to read {}: {error}", normalize_path(path)))?;
serde_json::from_str(&contents)
.map_err(|error| format!("Failed to parse {}: {error}", normalize_path(path)))
}

fn normalize_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
Expand Down
Loading