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
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.
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:
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.
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
. To run it locally:
nix flake check path:checkmate --override-input target path:.
Run the following to format files:
nix run github:vic/checkmate#fmt