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 ./modulesAdvanced 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.")) ./modulesfilter 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.
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.
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.libpipeTo 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.leafsReturns 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