Skip to content
vic

vic/import-tree

Import all nix files in a directory tree.

vic/import-tree.json
{
"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"
}

Sponsor Vic Dendritic Nix License CI Status

import-tree and vic’s dendritic libs made for you with Love++ and AI—. If you like my work, consider sponsoring

Recursively import Nix modules from a directory, with a simple, extensible API.

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.

🌳 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

Get a list of nix files programmatically:

(import-tree.withLib pkgs.lib).leafs ./modules
Advanced Usage, API, and Rationale

By default, paths having a component that begins with an underscore (/_) are ignored. This can be changed by using .initFilter API.

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.


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.

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.nix
modules/
a.nix
subdir/
b.nix

The 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.

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-dir

Multiple 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-dir

Multiple match filters can be added, results must match all of them.

map can be used to transform each path by providing a function.

# generate a custom module from path
import-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.

addPath can be used to prepend paths to be filtered as a setup for import-tree.

(import-tree.addPath ./vendor) ./modules
import-tree [./vendor ./modules]

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";
}

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

pipeTo takes a function that will receive the list of paths.

import-tree.pipeTo lib.id # equivalent to `.leafs`

leafs takes no arguments, it is equivalent to calling import-tree.pipeTo lib.id.

import-tree.leafs

Returns a fresh import-tree with empty state.

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")

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)
]

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")
];
}

import-tree uses checkmate for testing.

The test suite can be found in [checkmate.nix]!(checkmate.nix). To run it locally:

Terminal window
nix flake check github:vic/checkmate --override-input target path:.

Run the following to format files:

Terminal window
nix run github:vic/checkmate#fmt