Sponsor Vic Dendritic Nix License CI Status

> `import-tree` and [vic](https://bsky.app/profile/oeiuwq.bsky.social)'s [dendritic libs](https://vic.github.io/dendrix/Dendritic-Ecosystem.html#vics-dendritic-libraries) made for you with Love++ and AI--. If you like my work, consider [sponsoring](https://github.com/sponsors/vic) # 🌲🌴 import-tree 🎄🌳 > Recursively import [Nix modules](https://nix.dev/tutorials/module-system/) from a directory, with a simple, extensible API. ## Quick Start (flake-parts) Import all nix files inside `./modules` in your flake: ```nix { 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 🌳 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](https://github.com/mightyiam/dendritic) setups.\ 🌱 [Growing](https://github.com/search?q=language%3ANix+import-tree&type=code) [community](https://vic.github.io/dendrix/Dendrix-Trees.html) [adoption](https://github.com/vic/flake-file) ## Other Usage (outside module evaluation) Get a list of nix files programmatically: ```nix (import-tree.withLib pkgs.lib).leafs ./modules ```
Advanced Usage, API, and Rationale ### 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 The following goes recursively through `./modules` and imports all `.nix` files. ```nix {config, ...} { imports = [ (import-tree ./modules) ]; } ``` For more advanced usage, `import-tree` can be configured via its extensible API. --- #### 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` 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 ```nix {lib, config, ...} { imports = [ (import-tree ./modules) ]; } ``` Is similar to ```nix {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: ```nix {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 `import-tree` objects with custom behavior can be obtained using a builder pattern. For example: ```nix 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: ```nix ((import-tree.map lib.traceVal).filter (lib.hasInfix ".mod.")) ./modules ``` ##### 🌲 `import-tree.filter` and `import-tree.filterNot` `filter` takes a predicate function `path -> bool`. Only files with suffix `.nix` are candidates. ```nix 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` `match` takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with `builtins.match`. ```nix import-tree.match ".*/[a-z]+@(foo|bar)\.nix" ./some-dir ``` Multiple match filters can be added, results must match all of them. ##### 🌴 `import-tree.map` `map` can be used to transform each path by providing a function. ```nix # generate a custom module from path import-tree.map (path: { imports = [ path ]; }) ``` Outside modules evaluation, you can transform paths into something else: ```nix 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` `addPath` can be used to prepend paths to be filtered as a setup for import-tree. ```nix (import-tree.addPath ./vendor) ./modules import-tree [./vendor ./modules] ``` ##### 🎄 `import-tree.addAPI` `addAPI` extends the current import-tree object with new methods. ```nix 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` `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo` when not used as part of a nix modules evaluation. ```nix import-tree.withLib pkgs.lib ``` ##### 🌱 `import-tree.pipeTo` `pipeTo` takes a function that will receive the list of paths. ```nix import-tree.pipeTo lib.id # equivalent to `.leafs` ``` ##### 🍃 `import-tree.leafs` `leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`. ```nix import-tree.leafs ``` ##### 🌲 `import-tree.new` Returns a fresh import-tree with empty state. ##### 🌳 `import-tree.initFilter` _Replaces_ the initial filter which defaults to: Include files with `.nix` suffix and not having `/_` infix. ```nix import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/" p) import-tree.initFilter (lib.hasSuffix ".md") ``` ##### 🌴 `import-tree.files` A shorthand for `import-tree.leafs.result`. Returns a list of matching files. ```nix lib.pipe import-tree [ (i: i.initFilter (lib.hasSuffix ".js")) (i: i.addPath ./out) (i: i.withLib lib) (i: i.files) ] ``` ##### 🌵 `import-tree.result` Exactly the same as calling the import-tree object with an empty list `[ ]`. ```nix (import-tree.addPath ./modules).result (import-tree.addPath ./modules) [ ] ``` --- ## Why Importing a tree of nix modules has some advantages: ### Dendritic Pattern: each file is a flake-parts module [That pattern](https://github.com/mightyiam/dendritic) was the original inspiration for this library. See [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271), [@drupol's blog post](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) and [@vic's reply](https://discourse.nixos.org/t/how-do-you-structure-your-nixos-configs/65851/8) to learn about the Dendritic pattern advantages. ### 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](https://github.com/vic/dendrix) for [community conventions](https://github.com/vic/dendrix/blob/main/dev/modules/community/_pipeline.nix) 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: ```nix # 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: ```nix # consumer flakeModule {inputs, lib, ...}: let ed-tree = inputs.editor-distro.lib.modules-tree; in { imports = [ (ed-tree.vim-btw.old-school.on "rust") ]; } ``` --- ## Testing `import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing. The test suite can be found in [`checkmate.nix`](checkmate.nix). To run it locally: ```sh nix flake check github:vic/checkmate --override-input target path:. ``` Run the following to format files: ```sh nix run github:vic/checkmate#fmt ```