Skip to content

vic/import-tree

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

77 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌲🌴 import-tree πŸŽ„πŸŒ³

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

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

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

Get a list of nix files programmatically:

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

{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

{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

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

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

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.

🌴 import-tree.map

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.
🌡 import-tree.addPath

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]
πŸŽ„ 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

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

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

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.

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.

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.

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 [ ].

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

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

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

About

Import all nix files in a directory tree.

Topics

Resources

License

Stars

Watchers

Forks

Languages