vic/import-tree
{ "defaultBranch": "main", "description": "Import all nix files in a directory tree.", "fullName": "vic/import-tree", "homepage": "", "language": "Nix", "name": "import-tree", "pushedAt": "2025-11-21T22:07:24Z", "stargazersCount": 100, "updatedAt": "2025-11-25T11:17:24Z", "url": "https://github.com/vic/import-tree"}
import-treeand vic’s dendritic libs made for you with Love++ and AI—. If you like my work, consider sponsoring
🌲🌴 import-tree 🎄🌳
Section titled “🌲🌴 import-tree 🎄🌳”Recursively import Nix modules from a directory, with a simple, extensible API.
Quick Start (flake-parts)
Section titled “Quick Start (flake-parts)”Import all nix files inside ./modules in your flake:
{ inputs.import-tree.url = "github:vic/import-tree"; inputs.flake-parts.url = "github:hercules-ci/flake-parts";
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);}By default, paths having
/_are ignored.
Features
Section titled “Features”🌳 Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim, etc.
🌲 Callable as a deps-free Flake or nix lib.
🌴 Sensible defaults and configurable behaviour.
🌵 API for listing custom file types with filters and transformations.
🎄 Extensible: add your own API methods to tailor import-tree objects.
🌿 Useful on Dendritic Pattern setups.
🌱 Growing community adoption
Other Usage (outside module evaluation)
Section titled “Other Usage (outside module evaluation)”Get a list of nix files programmatically:
(import-tree.withLib pkgs.lib).leafs ./modulesAdvanced Usage, API, and Rationale
Ignored files
Section titled “Ignored files”By default, paths having a component that begins with an underscore (/_) are ignored. This can be changed by using .initFilter API.
API usage
Section titled “API usage”The following goes recursively through ./modules and imports all .nix files.
{config, ...} { imports = [ (import-tree ./modules) ];}For more advanced usage, import-tree can be configured via its extensible API.
Obtaining the API
Section titled “Obtaining the API”When used as a flake, the flake outputs attrset is the primary callable. Otherwise, importing the default.nix at the root of this repository will evaluate into the same attrset. This callable attrset is referred to as import-tree in this documentation.
import-tree
Section titled “import-tree”Takes a single argument: path or deeply nested list of path. Returns a module that imports the discovered files. For example, given the following file tree:
default.nixmodules/ a.nix subdir/ b.nixThe following
{lib, config, ...} { imports = [ (import-tree ./modules) ];}Is similar to
{lib, config, ...} { imports = [ { imports = [ ./modules/a.nix ./modules/subdir/b.nix ]; } ];}If given a deeply nested list of paths the list will be flattened and results concatenated. The following is valid usage:
{lib, config, ...} { imports = [ (import-tree [./a [./b]]) ];}Other import-tree objects can also be given as arguments (or in lists) as if they were paths.
As a special case, when the single argument given to an import-tree object is an attribute-set containing an options attribute, the import-tree object assumes it is being evaluated as a module. This way, a pre-configured import-tree object can also be used directly in a list of module imports.
Configurable behavior
Section titled “Configurable behavior”import-tree objects with custom behavior can be obtained using a builder pattern. For example:
lib.pipe import-tree [ (i: i.map lib.traceVal) (i: i.filter (lib.hasInfix ".mod.")) (i: i ./modules)]Or, in a simpler but less readable way:
((import-tree.map lib.traceVal).filter (lib.hasInfix ".mod.")) ./modules🌲 import-tree.filter and import-tree.filterNot
Section titled “🌲 import-tree.filter and import-tree.filterNot”filter takes a predicate function path -> bool. Only files with suffix .nix are candidates.
import-tree.filter (lib.hasInfix ".mod.") ./some-dirMultiple filters can be combined, results must match all of them.
🌳 import-tree.match and import-tree.matchNot
Section titled “🌳 import-tree.match and import-tree.matchNot”match takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with builtins.match.
import-tree.match ".*/[a-z]+@(foo|bar)\.nix" ./some-dirMultiple match filters can be added, results must match all of them.
🌴 import-tree.map
Section titled “🌴 import-tree.map”map can be used to transform each path by providing a function.
# generate a custom module from pathimport-tree.map (path: { imports = [ path ]; })Outside modules evaluation, you can transform paths into something else:
lib.pipe import-tree [ (i: i.map builtins.readFile) (i: i.withLib lib) (i: i.leafs ./dir)]# => list of contents of all files.🌵 import-tree.addPath
Section titled “🌵 import-tree.addPath”addPath can be used to prepend paths to be filtered as a setup for import-tree.
(import-tree.addPath ./vendor) ./modulesimport-tree [./vendor ./modules]🎄 import-tree.addAPI
Section titled “🎄 import-tree.addAPI”addAPI extends the current import-tree object with new methods.
import-tree.addAPI { maximal = self: self.addPath ./modules; feature = self: infix: self.maximal.filter (lib.hasInfix infix); minimal = self: self.feature "minimal";}🌿 import-tree.withLib
Section titled “🌿 import-tree.withLib”withLib is required prior to invocation of any of .leafs or .pipeTo when not used as part of a nix modules evaluation.
import-tree.withLib pkgs.lib🌱 import-tree.pipeTo
Section titled “🌱 import-tree.pipeTo”pipeTo takes a function that will receive the list of paths.
import-tree.pipeTo lib.id # equivalent to `.leafs`🍃 import-tree.leafs
Section titled “🍃 import-tree.leafs”leafs takes no arguments, it is equivalent to calling import-tree.pipeTo lib.id.
import-tree.leafs🌲 import-tree.new
Section titled “🌲 import-tree.new”Returns a fresh import-tree with empty state.
🌳 import-tree.initFilter
Section titled “🌳 import-tree.initFilter”Replaces the initial filter which defaults to: Include files with .nix suffix and not having /_ infix.
import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/" p)import-tree.initFilter (lib.hasSuffix ".md")🌴 import-tree.files
Section titled “🌴 import-tree.files”A shorthand for import-tree.leafs.result. Returns a list of matching files.
lib.pipe import-tree [ (i: i.initFilter (lib.hasSuffix ".js")) (i: i.addPath ./out) (i: i.withLib lib) (i: i.files)]🌵 import-tree.result
Section titled “🌵 import-tree.result”Exactly the same as calling the import-tree object with an empty list [ ].
(import-tree.addPath ./modules).result(import-tree.addPath ./modules) [ ]Importing a tree of nix modules has some advantages:
Dendritic Pattern: each file is a flake-parts module
Section titled “Dendritic Pattern: each file is a flake-parts module”That pattern was the original inspiration for this library. See @mightyiam’s post, @drupol’s blog post and @vic’s reply to learn about the Dendritic pattern advantages.
Sharing pre-configured subtrees of modules
Section titled “Sharing pre-configured subtrees of modules”Since the import-tree API is extensible and lets you add paths or filters at configuration time, configuration-library authors can provide custom import-tree instances with an API suited for their particular idioms.
@vic is using this on Dendrix for community conventions on tagging files.
This would allow us to have community-driven sets of configurations, much like those popular for editors: spacemacs/lazy-vim distributions.
Imagine an editor distribution exposing the following flake output:
# editor-distro's flakeModule{inputs, lib, ...}:let flake.lib.modules-tree = lib.pipe inputs.import-tree [ (i: i.addPath ./modules) (i: i.addAPI { inherit on off exclusive; }) (i: i.addAPI { ruby = self: self.on "ruby"; }) (i: i.addAPI { python = self: self.on "python"; }) (i: i.addAPI { old-school = self: self.off "copilot"; }) (i: i.addAPI { vim-btw = self: self.exclusive "vim" "emacs"; }) ];
on = self: flag: self.filter (lib.hasInfix "+${flag}"); off = self: flag: self.filterNot (lib.hasInfix "+${flag}"); exclusive = self: onFlag: offFlag: lib.pipe self [ (self: on self onFlag) (self: off self offFlag) ];in{ inherit flake;}Users of such distribution can do:
# consumer flakeModule{inputs, lib, ...}: let ed-tree = inputs.editor-distro.lib.modules-tree;in { imports = [ (ed-tree.vim-btw.old-school.on "rust") ];}Testing
Section titled “Testing”import-tree uses checkmate for testing.
The test suite can be found in [checkmate.nix]!(checkmate.nix). To run it locally:
nix flake check github:vic/checkmate --override-input target path:.Run the following to format files:
nix run github:vic/checkmate#fmt