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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# dag

## 0.2.1

## 0.3.0

### Minor Changes

- add getAffectedPkg function which return the dependents including direct, transitive and circular dependency
Comment on lines 1 to +7
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Changelog is in wrong order — newest version should appear first.

Version 0.3.0 (lines 3–7) is placed below 0.2.1 (line 1). Changelogs conventionally list the most recent release at the top. Also, minor grammar nit: "return" → "returns".

Proposed fix
-## 0.2.1
-
 ## 0.3.0
 
 ### Minor Changes
 
-- add getAffectedPkg function which return the dependents including direct, transitive and circular dependency
+- add getAffectedPkg function which returns the dependents including direct, transitive and circular dependency
+
+## 0.2.1
 
 ### Patch Changes
🤖 Prompt for AI Agents
In `@CHANGELOG.md` around lines 1 - 7, Swap the two release sections so the newest
release heading "## 0.3.0" appears above "## 0.2.1", and update the bullet under
0.3.0 to fix the grammar by changing "add getAffectedPkg function which return
the dependents including direct, transitive and circular dependency" to use
"returns" (e.g., "add getAffectedPkg function which returns the dependents
including direct, transitive and circular dependency").


### Patch Changes

- docs: fix import dag
Expand Down
149 changes: 128 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,148 @@ yarn add dag-rs
```

## Usage

### CommonJS
```
import dag from 'dag-rs';
const { dag, getAffectedPkg } = require('dag-rs');

for (const obj of dag()) {
console.log(obj);
}

for (const obj of getAffectedPkg("@rocket.chat/fuselage")) {
console.log(obj);
}
```

## Example Output `(dependent → dependency)`:
### ESM
```
import { dag, getAffectedPkg } from 'dag-rs';

for (const obj of dag()) {
console.log(obj);
}

for (const obj of getAffectedPkg("@rocket.chat/fuselage")) {
console.log(obj);
}
```

## Example Output :
### getAffectedPkg("@rocket.chat/fuselage") → returns all the direct , transitive , circular dependency
```
// dependents of @rocket.chat/fuselage package

@rocket.chat/layout
@rocket.chat/fuselage-toastbar
@rocket.chat/fuselage-forms
@rocket.chat/onboarding-ui

```
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/icons' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/layout' }

## Example Output:
### dag() → returns all packages in the project

```
// dependent : dependency

{ '@rocket.chat/fuselage-monorepo': '@rocket.chat/prettier-config' }
{ '@rocket.chat/fuselage-monorepo': 'update-readme' }
{ '@rocket.chat/css-in-js': '@rocket.chat/css-supports' }
{ '@rocket.chat/css-in-js': '@rocket.chat/memo' }
{
'@rocket.chat/css-in-js': '@rocket.chat/stylis-logical-props-middleware'
}
{ '@rocket.chat/css-in-js': 'lint-all' }
{ '@rocket.chat/css-supports': '@rocket.chat/memo' }
{ '@rocket.chat/css-supports': 'lint-all' }
{ '@rocket.chat/emitter': 'lint-all' }
{ '@rocket.chat/fuselage': '@rocket.chat/css-in-js' }
{ '@rocket.chat/fuselage': '@rocket.chat/css-supports' }
{ '@rocket.chat/fuselage': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/fuselage': '@rocket.chat/memo' }
{ '@rocket.chat/fuselage': '@rocket.chat/styled' }
{ '@rocket.chat/fuselage': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/fuselage': '@rocket.chat/icons' }
{ '@rocket.chat/fuselage': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/fuselage': '@rocket.chat/icons' }
{ '@rocket.chat/fuselage': '@rocket.chat/storybook-dark-mode' }
{ '@rocket.chat/fuselage': 'lint-all' }
{ '@rocket.chat/fuselage': 'testing-utils' }

{ '@rocket.chat/fuselage-forms': '@rocket.chat/emitter' }
{ '@rocket.chat/fuselage-forms': '@rocket.chat/fuselage' }
{ '@rocket.chat/fuselage-forms': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/fuselage-forms': '@rocket.chat/fuselage' }
{ '@rocket.chat/fuselage-forms': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/fuselage-forms': '@rocket.chat/storybook-dark-mode' }
{ '@rocket.chat/fuselage-forms': 'lint-all' }
{ '@rocket.chat/fuselage-hooks': '@rocket.chat/emitter' }
{ '@rocket.chat/fuselage-hooks': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/fuselage-hooks': '@rocket.chat/emitter' }
{ '@rocket.chat/fuselage-hooks': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/fuselage-hooks': 'lint-all' }
{ '@rocket.chat/fuselage-hooks': 'testing-utils' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/fuselage' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/styled' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/fuselage' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/layout' }
{
'@rocket.chat/fuselage-toastbar': '@rocket.chat/storybook-dark-mode'
}
{ '@rocket.chat/fuselage-toastbar': '@rocket.chat/styled' }
{ '@rocket.chat/fuselage-toastbar': 'lint-all' }
{ '@rocket.chat/fuselage-tokens': 'build-design-tokens' }
{ '@rocket.chat/fuselage-tokens': 'lint-all' }
{ '@rocket.chat/icons': 'build-icons' }
{ '@rocket.chat/icons': 'lint-all' }
{ '@rocket.chat/layout': '@rocket.chat/fuselage' }
{ '@rocket.chat/layout': '@rocket.chat/fuselage' }
{ '@rocket.chat/layout': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/layout': '@rocket.chat/storybook-dark-mode' }
{ '@rocket.chat/layout': 'lint-all' }
{ '@rocket.chat/logo': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/logo': '@rocket.chat/styled' }
{ '@rocket.chat/logo': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/logo': 'build-logo' }
{ '@rocket.chat/logo': 'lint-all' }
{ '@rocket.chat/memo': 'lint-all' }
{ '@rocket.chat/mp3-encoder': 'lint-all' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/icons' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/layout' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/logo' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/styled' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage-hooks' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/fuselage-tokens' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/icons' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/layout' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/logo' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/storybook-dark-mode' }
{ '@rocket.chat/onboarding-ui': '@rocket.chat/styled' }
{ '@rocket.chat/onboarding-ui': 'lint-all' }
{ '@rocket.chat/prettier-config': 'lint-all' }
{ '@rocket.chat/storybook-dark-mode': 'lint-all' }
{ '@rocket.chat/string-helpers': 'lint-all' }
{ '@rocket.chat/styled': '@rocket.chat/css-in-js' }
{ '@rocket.chat/styled': 'lint-all' }
{
'@rocket.chat/stylis-logical-props-middleware': '@rocket.chat/css-supports'
}
{ '@rocket.chat/stylis-logical-props-middleware': 'lint-all' }
{ 'build-design-tokens': 'tools-utils' }
{ 'build-design-tokens': 'lint-all' }
{ 'build-icons': 'tools-utils' }
{ 'build-icons': 'lint-all' }
{ 'build-logo': 'lint-all' }
{ 'build-logo': 'tools-utils' }
{ 'testing-utils': 'lint-all' }
{ 'tools-utils': 'lint-all' }
{ 'update-readme': 'lint-all' }
```

## Building dag-rs
Expand All @@ -50,19 +169,6 @@ $ yarn build

## Project Layout

The directory structure of this project is:

```
dag/
├── Cargo.toml
├── README.md
├── src/
| └── lib.rs
├── index.node
├── package.json
└── target/
```

| Entry | Purpose |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
Expand All @@ -73,4 +179,5 @@ dag/
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
| `target/` | Binary artifacts generated by the Rust build. |


## Contribution
All forms of contribution are welcome!
Binary file modified index.node
Binary file not shown.
6 changes: 6 additions & 0 deletions lib/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const native = require('../index.node');

module.exports = {
dag: native.dag,
getAffectedPkg: native.getAffectedPkg
};
3 changes: 0 additions & 3 deletions lib/index.js

This file was deleted.

6 changes: 6 additions & 0 deletions lib/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import cjs from './index.cjs';

export const dag = cjs.dag;
export const getAffectedPkg = cjs.getAffectedPkg;

export default cjs;
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dag-rs",
"version": "0.2.1",
"version": "0.3.0",
"description": "",
"main": "lib/index.js",
"publishConfig": {
Expand All @@ -10,6 +10,12 @@
"lib",
"*.node"
],
"exports": {
".": {
"require": "./lib/index.cjs",
"import": "./lib/index.mjs"
}
},
"scripts": {
"test": "cargo test",
"cargo-build": "cargo build --message-format=json-render-diagnostics > cargo.log",
Expand Down
48 changes: 47 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod find_pkg_json;
mod utils;
use neon::prelude::*;

use std::collections::HashSet;
use serde_json::Value;

fn get_pkg_name(path: String) -> String {
Expand Down Expand Up @@ -48,6 +48,52 @@ fn get_dependents(path: &str, pkg_names: &[String]) -> Vec<(String, String)>{
graph
}

fn get_dependents_recursively(pkg_name: String, res: Vec<(String, String)>, duplicate_dependents:&mut Vec<String>) {
for (_i, (dependent, dependency)) in res.clone().into_iter().enumerate() {
if dependency == pkg_name {
duplicate_dependents.push(dependent.clone());
get_dependents_recursively(dependent.clone(), res.clone(),duplicate_dependents);
}
}
return;
}
Comment on lines +51 to +59
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: infinite recursion on circular dependencies — will stack overflow.

The changelog explicitly mentions "circular dependency" support, yet there is no visited-set guard. If package A depends on B and B depends on A, this function recurses infinitely until the stack overflows.

Additionally, res.clone() on every recursive call copies the entire graph each time, which is unnecessarily expensive. Pass res by reference instead.

Proposed fix using a visited set
-fn get_dependents_recursively(pkg_name: String, res: Vec<(String, String)>, duplicate_dependents:&mut Vec<String>) {
-    for (_i, (dependent, dependency)) in res.clone().into_iter().enumerate() {
-        if dependency == pkg_name {
-            duplicate_dependents.push(dependent.clone());
-            get_dependents_recursively(dependent.clone(), res.clone(),duplicate_dependents);
-        }
-    }
-    return;
-}
+fn get_dependents_recursively(
+    pkg_name: &str,
+    res: &[(String, String)],
+    visited: &mut HashSet<String>,
+    result: &mut Vec<String>,
+) {
+    for (dependent, dependency) in res.iter() {
+        if dependency == pkg_name && !visited.contains(dependent) {
+            visited.insert(dependent.clone());
+            result.push(dependent.clone());
+            get_dependents_recursively(dependent, res, visited, result);
+        }
+    }
+}

Update the call site accordingly:

let mut visited = HashSet::new();
let mut dependents = Vec::new();
get_dependents_recursively(&pkg_name, &res, &mut visited, &mut dependents);
🤖 Prompt for AI Agents
In `@src/lib.rs` around lines 51 - 59, The function get_dependents_recursively
currently takes ownership of pkg_name and res and re-clones res on each call and
lacks a visited guard, causing infinite recursion for circular dependency
graphs; change its signature to take &str or &String for pkg_name, take res by
reference (&Vec<(String,String)> or a slice) and add a visited HashSet<&str> (or
HashSet<String>) parameter plus a mutable dependents Vec parameter, check/infer
visited before recursing (mark pkg_name visited, skip if already present) and
remove all res.clone() uses—update all call sites to create and pass a mutable
visited set and the mutable dependents vector when invoking
get_dependents_recursively.


#[neon::export]
fn get_affected_pkg<'a>(cx: &mut FunctionContext<'a>, pkg_name: String) -> JsResult<'a, JsArray>{
let filter = vec![".yarn", "node_modules"];
let paths = find_pkg_json::find_pkg_json(filter);
let mut res: Vec<(String, String)> = Vec::new();
let mut pkg_names = Vec::new();
let mut unique_dependents = HashSet::<String>::new();
let mut duplicate_dependents = Vec::new();
let mut dependents = Vec::new();
let dependents_js = JsArray::new(cx, 0);

for path in paths.clone() {
pkg_names.push(get_pkg_name(path.clone()));
}

for path in paths.clone() {
let mut r = get_dependents(&path, &pkg_names);
res.append(&mut r);
Comment on lines +72 to +78
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The paths.clone() operation is performed inside both for loops. Cloning a Vec can be an expensive operation, especially if paths contains many elements. You can optimize this by iterating over references (&paths) or cloning paths once if a mutable copy is truly needed, but in this case, iterating over references should suffice.

    for path in &paths {
        pkg_names.push(get_pkg_name(path.clone()));
    }

    for path in &paths {
        let mut r = get_dependents(path, &pkg_names);
        res.append(&mut r);
    }

}

get_dependents_recursively(pkg_name, res,&mut duplicate_dependents);

for pkg in duplicate_dependents {
unique_dependents.insert(pkg);
}
for pkg in unique_dependents.clone() {
dependents.push(pkg);
}

for (i, s) in unique_dependents.iter().enumerate() {
let v = cx.string(s);
dependents_js.set(cx, i as u32, v)?;
}
Ok(dependents_js)
}
Comment on lines 61 to 95
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Function does not return any value — the JS caller receives undefined.

The function signature returns () (unit), so calling getAffectedPkg(name) from JavaScript will always yield undefined. Per issue #4, it should return a mapping in the shape { packageName: [affected packages] }.

Currently the function only println!s matches instead of collecting and returning them. Compare with the dag function (lines 74–101), which builds and returns a JsArray.

You need to:

  1. Change the return type to produce a JS-compatible value (e.g., JsResult<'a, JsObject>).
  2. Collect the matching dependent names into a list.
  3. Build and return a JS object with the pkg_name key mapped to that list.
🐛 Sketch of a possible fix
-#[neon::export]
-fn get_affected_pkg(pkg_name: String) {
+#[neon::export]
+fn get_affected_pkg<'a>(cx: &mut FunctionContext<'a>, pkg_name: String) -> JsResult<'a, JsObject> {
     let filter = vec![".yarn", "node_modules"];
     let paths = find_pkg_json::find_pkg_json(filter);
     let mut res: Vec<(String, String)> = Vec::new();
     let mut pkg_names = Vec::new();
 
     for path in paths.clone() {
         pkg_names.push(get_pkg_name(path.clone()));
     }
 
     for path in paths.clone() {
         let mut r = get_dependents(&path, &pkg_names);
         res.append(&mut r);
     }
-    // res stores the dependents and dependency
-    for (_i, (_dependent, dependency)) in res.into_iter().enumerate() {
-        if dependency == pkg_name {
-            println!("pkg found");
-        }
-    }
-    println!("{}", pkg_name);
+
+    let affected: Vec<String> = res
+        .into_iter()
+        .filter(|(_dependent, dependency)| dependency == &pkg_name)
+        .map(|(dependent, _)| dependent)
+        .collect();
+
+    let result = cx.empty_object();
+    let js_array = JsArray::new(cx, affected.len() as u32);
+    for (i, name) in affected.iter().enumerate() {
+        let js_str = cx.string(name);
+        js_array.set(cx, i as u32, js_str)?;
+    }
+    result.set(cx, pkg_name.as_str(), js_array)?;
+    Ok(result)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[neon::export]
fn get_affected_pkg(pkg_name: String) {
let filter = vec![".yarn", "node_modules"];
let paths = find_pkg_json::find_pkg_json(filter);
let mut res: Vec<(String, String)> = Vec::new();
let mut pkg_names = Vec::new();
for path in paths.clone() {
pkg_names.push(get_pkg_name(path.clone()));
}
for path in paths.clone() {
let mut r = get_dependents(&path, &pkg_names);
res.append(&mut r);
}
// res stores the dependents and dependency
for (_i, (_dependent, dependency)) in res.into_iter().enumerate() {
if dependency == pkg_name {
println!("pkg found");
}
}
println!("{}", pkg_name);
}
#[neon::export]
fn get_affected_pkg<'a>(cx: &mut FunctionContext<'a>, pkg_name: String) -> JsResult<'a, JsObject> {
let filter = vec![".yarn", "node_modules"];
let paths = find_pkg_json::find_pkg_json(filter);
let mut res: Vec<(String, String)> = Vec::new();
let mut pkg_names = Vec::new();
for path in paths.clone() {
pkg_names.push(get_pkg_name(path.clone()));
}
for path in paths.clone() {
let mut r = get_dependents(&path, &pkg_names);
res.append(&mut r);
}
let affected: Vec<String> = res
.into_iter()
.filter(|(_dependent, dependency)| dependency == &pkg_name)
.map(|(dependent, _)| dependent)
.collect();
let result = cx.empty_object();
let js_array = JsArray::new(cx, affected.len() as u32);
for (i, name) in affected.iter().enumerate() {
let js_str = cx.string(name);
js_array.set(cx, i as u32, js_str)?;
}
result.set(cx, pkg_name.as_str(), js_array)?;
Ok(result)
}
🤖 Prompt for AI Agents
In `@src/lib.rs` around lines 51 - 73, The get_affected_pkg function currently
returns unit and only prints matches; change get_affected_pkg to a Neon
JS-returning signature (e.g., fn get_affected_pkg<'a>(cx: FunctionContext<'a>)
-> JsResult<'a, JsObject>), parse the pkg_name argument from the context,
collect dependent names by iterating res (built via get_dependents and
get_pkg_name) into a Vec<String>, then construct a JsArray of those names and a
JsObject mapping pkg_name -> that JsArray (similar to how dag builds and returns
its JsArray/JsObject) and return it via cx.null() or Ok(object) accordingly
using Neon APIs. Ensure you remove println! usage and return the created
JsObject so JS receives { packageName: [affected packages] }.


#[neon::export]
fn dag<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> {
let js_array = JsArray::new(cx, 0);
Expand Down