Repository: fzakaria/nix-auto-follow

Issue:
Null inputs
The tool adds null inputs to the flake.lock, which I don't think is necessary:
![image](https://github.com/user-attachments/assets/640de12b-c793-4a4d-8f92-c87ca2da09ad)


Discussion:

Patch:
diff --git a/src/nix_auto_follow/cli.py b/src/nix_auto_follow/cli.py
index f18f7d3..b15d422 100644
--- a/src/nix_auto_follow/cli.py
+++ b/src/nix_auto_follow/cli.py
@@ -24,7 +24,15 @@ class Node:
         return cls(inputs, remaining)
 
     def to_dict(self) -> dict[str, Any]:
-        return {"inputs": self.inputs, **self.remaining}
+        """Convert the Node to a dictionary.
+
+        In order to minimize diff with the original lockfile,
+        we only include the `inputs` key if it is not None.
+        """
+        if self.inputs is None:
+            return self.remaining
+        else:
+            return {"inputs": self.inputs, **self.remaining}
 
     def get_url(self) -> str:
         """
@@ -71,9 +79,9 @@ class LockFile:
 
     def to_dict(self) -> dict[str, Any]:
         return {
+            "nodes": {key: value.to_dict() for key, value in self.nodes.items()},
             "root": self.root,
             "version": self.version,
-            "nodes": {key: value.to_dict() for key, value in self.nodes.items()},
         }
 
 
@@ -209,7 +217,7 @@ def start(
     if program_args.in_place:
         # Write the modified JSON back to the file
         with open(program_args.filename, "w") as f:
-            f.write(modified_data)
+            f.write(modified_data + "\n")
     else:
         # Write the modified JSON to stdout
         print(modified_data, file=stdout)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a2a40ef..bbe1318 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -130,3 +130,48 @@ def test_check_lock_file_fail() -> None:
         modified_lock = update_flake_lock(flake_lock)
         # still fails
         assert not check_lock_file(modified_lock)
+
+
+def test_do_not_include_empty_inputs() -> None:
+    with open("tests/fixtures/simple.json") as f:
+        flake_json = json.load(f)
+        flake_lock = LockFile.from_dict(flake_json)
+        # precondition: inputs does not exist in original lock file
+        assert "inputs" not in flake_json["nodes"]["nixpkgs"]
+        assert flake_lock.nodes["nixpkgs"].inputs is None
+
+        modified_lock = update_flake_lock(flake_lock)
+        modified_lock_json = modified_lock.to_dict()
+        # postcondition: inputs does not exist in modified lock file
+        assert "inputs" not in modified_lock_json["nodes"]["nixpkgs"]
+        assert modified_lock.nodes["nixpkgs"].inputs is None
+
+
+def test_top_level_keys_sorted() -> None:
+    with open("tests/fixtures/simple.json") as f:
+        flake_json = json.load(f)
+        # precondition: keys are sorted in original file
+        assert list(flake_json.keys()) == sorted(flake_json.keys())
+
+        flake_lock = LockFile.from_dict(flake_json)
+        modified_lock = update_flake_lock(flake_lock)
+        modified_lock_json = modified_lock.to_dict()
+
+        # postcondition: keys are sorted in modified file
+        assert list(modified_lock_json.keys()) == sorted(modified_lock_json.keys())
+
+
+def test_node_keys_sorted() -> None:
+    with open("tests/fixtures/root_has_follow.json") as f:
+        flake_json = json.load(f)
+        # precondition: keys are sorted in original file
+        assert list(flake_json["nodes"].keys()) == sorted(flake_json["nodes"].keys())
+
+        flake_lock = LockFile.from_dict(flake_json)
+        modified_lock = update_flake_lock(flake_lock)
+        modified_lock_json = modified_lock.to_dict()
+
+        # postcondition: keys are sorted in modified file
+        assert list(modified_lock_json["nodes"].keys()) == sorted(
+            modified_lock_json["nodes"].keys()
+        )