diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 70602ce..f90d2f9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,11 +1,23 @@
on:
- pull_request:
push:
branches: [main]
+ pull_request:
+ types: [labeled, opened, synchronize, reopened, review_requested, ready_for_review]
+ pull_request_review:
+ types: [submitted]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+env:
+ NIX_PATH: "nixpkgs=https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
jobs:
- flake-check:
- name: nix flake check
+ test:
+ name: test
runs-on: ubuntu-latest
steps:
- - uses: cachix/install-nix-action@v30
- - run: nix flake check -L github:vic/checkmate --override-input target github:$GITHUB_REPOSITORY/$GITHUB_SHA
+ - uses: wimpysworld/nothing-but-nix@main
+ - uses: cachix/install-nix-action@v31
+ - uses: DeterminateSystems/magic-nix-cache-action@v13
+ - uses: actions/checkout@v6
+ - run: nix-shell ./shell.nix --run 'just ci'
+
diff --git a/Justfile b/Justfile
index 7598c53..e9847b0 100644
--- a/Justfile
+++ b/Justfile
@@ -1,15 +1,18 @@
-system := `nix-instantiate --eval --raw -E builtins.currentSystem`
-
help:
just -l
-ci:
- just fmt -- --ci --no-cache
- just test
+docs:
+ cd docs && pnpm run dev
+
+zerover:
+ echo "obase=2; $(date +%s)" | bc
fmt *args:
- nix run github:denful/checkmate#fmt --override-input target path:. -L {{args}}
+ treefmt {{args}}
-test *args:
- nix flake check github:denful/checkmate --override-input target . -L {{args}}
+ci:
+ just fmt --ci --no-cache
+ just test
+test suite="all" *args:
+ nix-unit --expr 'let x = import ./tests.nix; in if "{{suite}}" == "all" then x else x.{{suite}}' {{args}}
diff --git a/README.md b/README.md
index 773ebc9..af1bc64 100644
--- a/README.md
+++ b/README.md
@@ -83,8 +83,7 @@ with-inputs outputs
## Testing
-`import-tree` uses [`checkmate`](https://github.com/denful/checkmate) for testing:
```sh
-nix flake check github:denful/checkmate --override-input target path:.
+nix-shell ./shell.nix --run 'just ci'
```
diff --git a/checkmate/modules/formatter.nix b/checkmate/modules/formatter.nix
deleted file mode 100644
index 41301a2..0000000
--- a/checkmate/modules/formatter.nix
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- perSystem.treefmt.settings.global.excludes = [
- "checkmate/tree/*"
- "docs/*"
- "Justfile"
- ];
-}
diff --git a/checkmate/modules/tests.nix b/checkmate/modules/tests.nix
deleted file mode 100644
index a9738b2..0000000
--- a/checkmate/modules/tests.nix
+++ /dev/null
@@ -1,282 +0,0 @@
-# If formatting fails, run
-# nix run github:denful/checkmate#checkmate-treefmt
-#
-{ inputs, lib, ... }:
-let
- # since we are tested by github:denful/checkmate
- it = inputs.target;
- lit = it.withLib lib;
-in
-{
- perSystem = (
- { ... }:
- {
- nix-unit.tests = {
- leafs."test fails if no lib has been set" = {
- expr = it.leafs ../tree;
- expectedError.type = "ThrownError";
- };
-
- leafs."test succeeds when lib has been set" = {
- expr = (it.withLib lib).leafs ../tree/hello;
- expected = [ ];
- };
-
- leafs."test only returns nix non-ignored files" = {
- expr = lit.leafs ../tree/a;
- expected = [
- ../tree/a/a_b.nix
- ../tree/a/b/b_a.nix
- ../tree/a/b/m.nix
- ];
- };
-
- filter."test returns empty if no nix files with true predicate" = {
- expr = (lit.filter (_: false)).leafs ../tree;
- expected = [ ];
- };
-
- filter."test only returns nix files with true predicate" = {
- expr = (lit.filter (lib.hasSuffix "m.nix")).leafs ../tree;
- expected = [ ../tree/a/b/m.nix ];
- };
-
- filter."test multiple `filter`s compose" = {
- expr = ((lit.filter (lib.hasInfix "b/")).filter (lib.hasInfix "_")).leafs ../tree;
- expected = [ ../tree/a/b/b_a.nix ];
- };
-
- match."test returns empty if no files match regex" = {
- expr = (lit.match "badregex").leafs ../tree;
- expected = [ ];
- };
-
- match."test returns files matching regex" = {
- expr = (lit.match ".*/[^/]+_[^/]+\.nix").leafs ../tree;
- expected = [
- ../tree/a/a_b.nix
- ../tree/a/b/b_a.nix
- ];
- };
-
- matchNot."test returns files not matching regex" = {
- expr = (lit.matchNot ".*/[^/]+_[^/]+\.nix").leafs ../tree/a/b;
- expected = [
- ../tree/a/b/m.nix
- ];
- };
-
- match."test `match` composes with `filter`" = {
- expr = ((lit.match ".*a_b.nix").filter (lib.hasInfix "/a/")).leafs ../tree;
- expected = [ ../tree/a/a_b.nix ];
- };
-
- match."test multiple `match`s compose" = {
- expr = ((lit.match ".*/[^/]+_[^/]+\.nix").match ".*b\.nix").leafs ../tree;
- expected = [ ../tree/a/a_b.nix ];
- };
-
- map."test transforms each matching file with function" = {
- expr = (lit.map import).leafs ../tree/x;
- expected = [ "z" ];
- };
-
- map."test `map` composes with `filter`" = {
- expr = ((lit.filter (lib.hasInfix "/x")).map import).leafs ../tree;
- expected = [ "z" ];
- };
-
- map."test multiple `map`s compose" = {
- expr = ((lit.map import).map builtins.stringLength).leafs ../tree/x;
- expected = [ 1 ];
- };
-
- addPath."test `addPath` prepends a path to filter" = {
- expr = (lit.addPath ../tree/x).files;
- expected = [ ../tree/x/y.nix ];
- };
-
- addPath."test `addPath` can be called multiple times" = {
- expr = ((lit.addPath ../tree/x).addPath ../tree/a/b).files;
- expected = [
- ../tree/x/y.nix
- ../tree/a/b/b_a.nix
- ../tree/a/b/m.nix
- ];
- };
-
- addPath."test `addPath` identity" = {
- expr = ((lit.addPath ../tree/x).addPath ../tree/a/b).files;
- expected = lit.leafs [
- ../tree/x
- ../tree/a/b
- ];
- };
-
- new."test `new` returns a clear state" = {
- expr = lib.pipe lit [
- (i: i.addPath ../tree/x)
- (i: i.addPath ../tree/a/b)
- (i: i.new)
- (i: i.addPath ../tree/modules/hello-world)
- (i: i.withLib lib)
- (i: i.files)
- ];
- expected = [ ../tree/modules/hello-world/mod.nix ];
- };
-
- initFilter."test can change the initial filter to look for other file types" = {
- expr = (lit.initFilter (p: lib.hasSuffix ".txt" p)).leafs [ ../tree/a ];
- expected = [ ../tree/a/a.txt ];
- };
-
- initFilter."test initf does filter non-paths" = {
- expr =
- let
- mod = (it.initFilter (x: !(x ? config.boom))) [
- {
- options.hello = lib.mkOption {
- default = "world";
- type = lib.types.str;
- };
- }
- {
- config.boom = "boom";
- }
- ];
- res = lib.modules.evalModules { modules = [ mod ]; };
- in
- res.config.hello;
- expected = "world";
- };
-
- addAPI."test extends the API available on an import-tree object" = {
- expr =
- let
- extended = lit.addAPI { helloOption = self: self.addPath ../tree/modules/hello-option; };
- in
- extended.helloOption.files;
- expected = [ ../tree/modules/hello-option/mod.nix ];
- };
-
- addAPI."test preserves previous API extensions on an import-tree object" = {
- expr =
- let
- first = lit.addAPI { helloOption = self: self.addPath ../tree/modules/hello-option; };
- second = first.addAPI { helloWorld = self: self.addPath ../tree/modules/hello-world; };
- extended = second.addAPI { res = self: self.helloOption.files; };
- in
- extended.res;
- expected = [ ../tree/modules/hello-option/mod.nix ];
- };
-
- addAPI."test API extensions are late bound" = {
- expr =
- let
- first = lit.addAPI { res = self: self.late; };
- extended = first.addAPI { late = _self: "hello"; };
- in
- extended.res;
- expected = "hello";
- };
-
- pipeTo."test pipes list into a function" = {
- expr = (lit.map lib.pathType).pipeTo (lib.length) ../tree/x;
- expected = 1;
- };
-
- import-tree."test does not break if given a path to a file instead of a directory." = {
- expr = lit.leafs ../tree/x/y.nix;
- expected = [ ../tree/x/y.nix ];
- };
-
- import-tree."test returns a module with a single imported nested module having leafs" = {
- expr =
- let
- oneElement = arr: if lib.length arr == 1 then lib.elemAt arr 0 else throw "Expected one element";
- module = it ../tree/x;
- inner = (oneElement module.imports) { inherit lib; };
- in
- oneElement inner.imports;
- expected = ../tree/x/y.nix;
- };
-
- import-tree."test evaluates returned module as part of module-eval" = {
- expr =
- let
- res = lib.modules.evalModules { modules = [ (it ../tree/modules) ]; };
- in
- res.config.hello;
- expected = "world";
- };
-
- import-tree."test can itself be used as a module" = {
- expr =
- let
- res = lib.modules.evalModules { modules = [ (it.addPath ../tree/modules) ]; };
- in
- res.config.hello;
- expected = "world";
- };
-
- import-tree."test take as arg anything path convertible" = {
- expr = lit.leafs [
- {
- outPath = ../tree/modules/hello-world;
- }
- ];
- expected = [ ../tree/modules/hello-world/mod.nix ];
- };
-
- import-tree."test passes non-paths without string conversion" = {
- expr =
- let
- mod = it [
- {
- options.hello = lib.mkOption {
- default = "world";
- type = lib.types.str;
- };
- }
- ];
- res = lib.modules.evalModules { modules = [ mod ]; };
- in
- res.config.hello;
- expected = "world";
- };
-
- import-tree."test can take other import-trees as if they were paths" = {
- expr = (lit.filter (lib.hasInfix "mod")).leafs [
- (it.addPath ../tree/modules/hello-option)
- ../tree/modules/hello-world
- ];
- expected = [
- ../tree/modules/hello-option/mod.nix
- ../tree/modules/hello-world/mod.nix
- ];
- };
-
- leafs."test loads from hidden directory but excludes sub-hidden" = {
- expr = lit.leafs ../tree/a/b/_c;
- expected = [ ../tree/a/b/_c/d/e.nix ];
- };
-
- scoped."test adds attrs via scopedImport" = {
- expr =
- (lib.evalModules {
- modules = [
- ((lit.addScoped { foo = 22; }) ../tree/_scoped)
- ];
- }).config.foo;
- expected = 22;
- };
-
- combinator."test combinator syntax to compose import-tree" = {
- expr = it (it: it.withLib lib) (it: it.leafs) ../tree/_scoped;
- expected = [ ../tree/_scoped/foo.nix ];
- };
- };
-
- }
- );
-}
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 533d0e1..753063b 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -46,6 +46,7 @@ export default defineConfig({
{ label: 'Filtering Files', slug: 'guides/filtering' },
{ label: 'Transforming Paths', slug: 'guides/mapping' },
{ label: 'Custom API', slug: 'guides/custom-api' },
+ { label: 'Combinator Syntax', slug: 'guides/combinator' },
{ label: 'Outside Modules', slug: 'guides/outside-modules' },
{ label: 'Dendritic Pattern', slug: 'guides/dendritic' },
],
diff --git a/docs/src/content/docs/community.md b/docs/src/content/docs/community.md
index 52c3acf..589e0aa 100644
--- a/docs/src/content/docs/community.md
+++ b/docs/src/content/docs/community.md
@@ -5,20 +5,20 @@ description: Get help, share your work, and find real-world import-tree usage.
## Get Support
-- [GitHub Issues](https://github.com/denful/import-tree/issues) — report bugs, request features
-- [GitHub Discussions](https://github.com/denful/import-tree/discussions) — ask questions, share ideas
+- [GitHub Issues](https://github.com/denful/import-tree/issues) - report bugs, request features
+- [GitHub Discussions](https://github.com/denful/import-tree/discussions) - ask questions, share ideas
Everyone is welcome. Be kind and respectful.
## Real-World Usage
-- [GitHub Code Search](https://github.com/search?q=language%3ANix+import-tree&type=code) — find projects using import-tree
-- [Dendrix Trees](https://denful.github.io/dendrix/Dendrix-Trees.html) — community index of dendritic setups
+- [GitHub Code Search](https://github.com/search?q=language%3ANix+import-tree&type=code) - find projects using import-tree
+- [Dendrix Trees](https://denful.github.io/dendrix/Dendrix-Trees.html) - community index of dendritic setups
## Ecosystem
-- [Den](https://github.com/denful/den) — context-aware dendritic Nix framework
-- [flake-aspects](https://github.com/denful/flake-aspects) — aspect composition library
-- [denful](https://github.com/denful/denful) — community aspect distribution
-- [Dendrix](https://dendrix.oeiuwq.com/) — index of dendritic aspects
-- [Dendritic Design](https://github.com/mightyiam/dendritic) — the pattern that inspired this ecosystem
+- [Den](https://github.com/denful/den) - context-aware dendritic Nix framework
+- [flake-aspects](https://github.com/denful/flake-aspects) - aspect composition library
+- [denful](https://github.com/denful/denful) - community aspect distribution
+- [Dendrix](https://dendrix.oeiuwq.com/) - index of dendritic aspects
+- [Dendritic Design](https://github.com/mightyiam/dendritic) - the pattern that inspired this ecosystem
diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md
index e67760f..31b5da6 100644
--- a/docs/src/content/docs/contributing.md
+++ b/docs/src/content/docs/contributing.md
@@ -7,23 +7,23 @@ All contributions are welcome. PRs are checked by CI.
## Run Tests
-`import-tree` uses [checkmate](https://github.com/denful/checkmate) for testing:
+Load development environment from `./shell.nix`
```sh
-nix flake check github:denful/checkmate --override-input target path:.
+just ci
```
## Format Code
```sh
-nix run github:denful/checkmate#fmt
+just fmt
```
## Bug Reports
Open an [issue](https://github.com/denful/import-tree/issues) with a minimal reproduction.
-If possible, include a failing test case — the test suite is in `checkmate/modules/tests.nix` and the test tree fixtures are in `checkmate/tree/`.
+If possible, include a failing test case: the test suite is in `tests.nix` and the test tree fixtures are in `tree/`.
## Documentation
@@ -32,5 +32,5 @@ The documentation site lives under `./docs/`. It uses [Starlight](https://starli
To run locally:
```sh
-cd docs && pnpm install && pnpm run dev
+just docs
```
diff --git a/docs/src/content/docs/getting-started/quick-start.mdx b/docs/src/content/docs/getting-started/quick-start.mdx
index 3a1c1a5..5dcb224 100644
--- a/docs/src/content/docs/getting-started/quick-start.mdx
+++ b/docs/src/content/docs/getting-started/quick-start.mdx
@@ -47,7 +47,7 @@ in
### With flake-parts
-The most common pattern — import all modules from a directory as flake-parts modules:
+The most common pattern - import all modules from a directory as flake-parts modules:
```nix
{
@@ -98,10 +98,10 @@ Pass a list to import from several directories:
{ imports = [ (import-tree [ ./modules ./extra-modules ]) ]; }
```
-Lists can be arbitrarily nested — they are flattened automatically.
+Lists can be arbitrarily nested - they are flattened automatically.
## Next Steps
-- [Filtering](/guides/filtering/) — select specific files by predicate or regex
-- [API Reference](/reference/api/) — complete method documentation
-- [Dendritic Pattern](/guides/dendritic/) — learn the file-per-module approach
+- [Filtering](/guides/filtering/) - select specific files by predicate or regex
+- [API Reference](/reference/api/) - complete method documentation
+- [Dendritic Pattern](/guides/dendritic/) - learn the file-per-module approach
diff --git a/docs/src/content/docs/guides/combinator.mdx b/docs/src/content/docs/guides/combinator.mdx
new file mode 100644
index 0000000..153e61e
--- /dev/null
+++ b/docs/src/content/docs/guides/combinator.mdx
@@ -0,0 +1,94 @@
+---
+title: Combinator Syntax
+description: Compose import-tree configurations using function combinators for clean, terse syntax.
+---
+
+import { Aside } from '@astrojs/starlight/components';
+
+## What is Combinator Syntax?
+
+Combinator syntax allows you to pass functions to `import-tree` to configure and immediately use it, reducing boilerplate and enabling fluent composition:
+
+```nix
+# Without combinators (verbose)
+let
+ configured = import-tree.withLib lib;
+ leafs = configured.leafs;
+in
+leafs ./modules
+
+# With combinators (terse)
+import-tree (it: it.withLib lib) (it: it.leafs) ./modules
+```
+
+## How It Works
+
+When you call `import-tree` with a function (a zero-argument function), it receives the current import-tree object and can call any methods on it:
+
+```nix
+import-tree (self: self.withLib lib)
+```
+
+The function is applied, and the result replaces the state. You can chain multiple functions:
+
+```nix
+import-tree
+ (it: it.withLib lib) # first: add lib
+ (it: it.addPath ./modules) # second: add path
+ (it: it.filter (lib.hasSuffix "mod.nix")) # third: filter
+ ./src # finally: evaluate
+```
+
+This is purely syntactic sugar - it's equivalent to piping through the operations in order.
+
+## Real-World Examples
+
+### Filtered List with Combinator
+
+Get a filtered file list in one expression:
+
+
+```nix
+import-tree
+ (it: it.withLib lib)
+ (it: it.filter (lib.hasSuffix "mod.nix"))
+ (it: it.leafs)
+ ./modules
+```
+
+### With Scoped Imports
+
+```nix
+import-tree
+ (it: it.addScoped { helpers = my-utils; })
+ ./modules
+```
+
+### Using Custom API
+
+Combinators work with custom API methods:
+
+```nix
+let
+ custom = import-tree.addAPI {
+ nixFilesOnly = self: self.match ".*\\.nix$";
+ };
+in
+custom
+ (it: it.nixFilesOnly)
+ (it: it.leafs)
+ ./src
+```
+
+## When to Use
+
+
+
+Combinators shine for:
+- One-off file discovery in scripts
+- Building configurations inline without intermediate variables
+- Chaining multiple small transformations
+
+For complex, reusable patterns, consider instead building domain-specific methods with `.addAPI`.
diff --git a/docs/src/content/docs/guides/custom-api.mdx b/docs/src/content/docs/guides/custom-api.mdx
index 3cad3a4..5ba9c46 100644
--- a/docs/src/content/docs/guides/custom-api.mdx
+++ b/docs/src/content/docs/guides/custom-api.mdx
@@ -27,7 +27,7 @@ extended.minimal ./src
## Preserving Previous Extensions
-Calling `.addAPI` multiple times is cumulative — previous extensions are preserved:
+Calling `.addAPI` multiple times is cumulative - previous extensions are preserved:
```nix
let
@@ -39,7 +39,7 @@ second.foo.files # still works
## Late Binding
-API methods are late-bound. You can reference methods that don't exist yet — they resolve when actually called:
+API methods are late-bound. You can reference methods that don't exist yet - they resolve when actually called:
```nix
let
@@ -54,7 +54,7 @@ This enables building APIs incrementally across multiple `.addAPI` calls.
## Real-World Example: Module Distribution
A library author can ship a pre-configured import-tree with domain-specific methods:
@@ -67,14 +67,14 @@ let
off = self: flag: self.filterNot (lib.hasInfix "+${flag}");
exclusive = self: onFlag: offFlag: (on self onFlag) |> (s: off s offFlag);
in {
- flake.lib.modules-tree = lib.pipe inputs.import-tree [
+ flake.lib.modules-tree = 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"; })
- ];
+ ;
}
```
diff --git a/docs/src/content/docs/guides/dendritic.mdx b/docs/src/content/docs/guides/dendritic.mdx
index eb11299..0ce6c1d 100644
--- a/docs/src/content/docs/guides/dendritic.mdx
+++ b/docs/src/content/docs/guides/dendritic.mdx
@@ -33,11 +33,11 @@ Each file is a standard Nix module:
## Why Dendritic?
-- **Locality** — each concern in its own file, easy to find and modify
-- **Composability** — add or remove features by adding or removing files
-- **No boilerplate** — `import-tree` handles the wiring
-- **Git-friendly** — file additions don't cause merge conflicts in import lists
-- **Discoverable** — directory structure documents the system
+- **Locality** - each concern in its own file, easy to find and modify
+- **Composability** - add or remove features by adding or removing files
+- **No boilerplate** - `import-tree` handles the wiring
+- **Git-friendly** - file additions don't cause merge conflicts in import lists
+- **Discoverable** - directory structure documents the system
## Using with flake-parts
@@ -88,7 +88,7 @@ in { ... }: { /* use helpers */ }
## Further Reading
-- [Dendritic Design](https://github.com/mightyiam/dendritic) — the pattern specification
-- [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271) — introducing the pattern
-- [@drupol's blog](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) — real-world adoption
-- [Dendrix](https://dendrix.oeiuwq.com/) — index of dendritic aspects
+- [Dendritic Design](https://github.com/mightyiam/dendritic) - the pattern specification
+- [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271) - introducing the pattern
+- [@drupol's blog](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) - real-world adoption
+- [Dendrix](https://dendrix.oeiuwq.com/) - index of dendritic aspects
diff --git a/docs/src/content/docs/guides/filtering.mdx b/docs/src/content/docs/guides/filtering.mdx
index 70980dd..a4e5457 100644
--- a/docs/src/content/docs/guides/filtering.mdx
+++ b/docs/src/content/docs/guides/filtering.mdx
@@ -23,7 +23,7 @@ You can narrow this down with filters, or replace the defaults entirely.
import-tree.filter (lib.hasInfix ".mod.") ./modules
```
-`filterNot` is the inverse — exclude files matching the predicate:
+`filterNot` is the inverse - exclude files matching the predicate:
```nix
# Skip any file with "experimental" in the path
@@ -32,14 +32,13 @@ import-tree.filterNot (lib.hasInfix "experimental") ./modules
### Composing Filters
-Multiple filters combine with logical AND — a file must pass **all** of them:
+Multiple filters combine with logical AND - a file must pass **all** of them:
```nix
-lib.pipe import-tree [
+import-tree
(i: i.filter (lib.hasInfix "/desktop/"))
(i: i.filter (lib.hasSuffix "bar.nix"))
(i: i ./modules)
-]
```
This selects only files under a `desktop/` directory whose name ends in `bar.nix`.
@@ -74,7 +73,7 @@ All filter types compose together:
This finds files matching the regex **and** containing `/src/` in their path.
-## initFilter — Replacing Defaults
+## initFilter - Replacing Defaults
`initFilter` **replaces** the built-in `.nix` + no-`/_` filter entirely. Use it to discover non-Nix files or change the ignore convention:
diff --git a/docs/src/content/docs/guides/mapping.mdx b/docs/src/content/docs/guides/mapping.mdx
index 4bfb0dd..ebd0496 100644
--- a/docs/src/content/docs/guides/mapping.mdx
+++ b/docs/src/content/docs/guides/mapping.mdx
@@ -19,32 +19,31 @@ import-tree.map (path: { imports = [ path ]; }) ./modules
import-tree.map lib.traceVal ./modules
```
-This prints each discovered path during evaluation — useful for debugging.
+This prints each discovered path during evaluation - useful for debugging.
### Composing Maps
Multiple `.map` calls compose left-to-right (the first map runs first):
```nix
-lib.pipe import-tree [
+import-tree
(i: i.map import) # import each .nix file
(i: i.map builtins.stringLength) # get the length of each result
(i: i.withLib lib)
(i: i.leafs ./dir)
-]
```
### Using map Outside Module Evaluation
-When used with `.leafs` or `.pipeTo`, `.map` transforms paths into arbitrary values — not just modules:
+When used with `.leafs` or `.pipeTo`, `.map` transforms paths into arbitrary values - not just modules:
```nix
# Read all .md files under a directory
-lib.pipe import-tree [
+import-tree
(i: i.initFilter (lib.hasSuffix ".md"))
(i: i.map builtins.readFile)
(i: i.withLib lib)
(i: i.leafs ./docs)
-]
+
# => [ "# Title\n..." "# Other\n..." ]
```
diff --git a/docs/src/content/docs/guides/outside-modules.mdx b/docs/src/content/docs/guides/outside-modules.mdx
index 0479474..f157447 100644
--- a/docs/src/content/docs/guides/outside-modules.mdx
+++ b/docs/src/content/docs/guides/outside-modules.mdx
@@ -34,14 +34,13 @@ Omitting `.withLib` when calling `.leafs` produces an error:
### files
-`.files` is a shortcut for `.leafs.result` — returns the list directly when paths have already been added via `.addPath`:
+`.files` is a shortcut for `.leafs.result`: returns the list directly when paths have already been added via `.addPath`:
```nix
-lib.pipe import-tree [
+import-tree
(i: i.addPath ./modules)
(i: i.withLib lib)
(i: i.files)
-]
```
### pipeTo
@@ -49,24 +48,23 @@ lib.pipe import-tree [
`.pipeTo` takes a function that receives the list of discovered paths, letting you process the results:
```nix
-(import-tree.withLib lib).pipeTo builtins.length ./modules
+import-tree (it: it.withLib lib) (it: it.pipeTo builtins.length) ./modules
# => 5 (number of .nix files)
```
Combine with `.map` for powerful pipelines:
```nix
-lib.pipe import-tree [
+import-tree
(i: i.map import)
(i: i.pipeTo lib.length)
(i: i.withLib lib)
(i: i ./modules)
-]
```
### result
-`.result` evaluates the import-tree with an empty path list — useful when paths are already configured via `.addPath`:
+`.result` evaluates the import-tree with an empty path list, useful when paths are already configured via `.addPath`:
```nix
(import-tree.addPath ./modules).result
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index 9b4d3b3..178affc 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -30,7 +30,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
## At a Glance
```nix
-# In your flake.nix — import every .nix file under ./modules
+# In your flake.nix - import every .nix file under ./modules
{
inputs.import-tree.url = "github:denful/import-tree";
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
@@ -40,7 +40,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
}
```
-Paths containing `/_` are ignored by default — use underscored directories for private helpers.
+Paths containing `/_` are ignored by default - use underscored directories for private helpers.
## Highlights
diff --git a/docs/src/content/docs/motivation.mdx b/docs/src/content/docs/motivation.mdx
index 2d06679..ffbbace 100644
--- a/docs/src/content/docs/motivation.mdx
+++ b/docs/src/content/docs/motivation.mdx
@@ -38,7 +38,7 @@ Add a file to `./modules/` and it is automatically discovered. Reorganize freely
## Dendritic Pattern
-The [Dendritic pattern](https://github.com/mightyiam/dendritic) — where each file is a self-contained module — was the original inspiration for `import-tree`.
+The [Dendritic pattern](https://github.com/mightyiam/dendritic) - where each file is a self-contained module - was the original inspiration for `import-tree`.
-With Dendritic, your configuration becomes a file tree — each concern in its own file, each file a module. `import-tree` removes the glue code that would otherwise connect them.
+With Dendritic, your configuration becomes a file tree - each concern in its own file, each file a module. `import-tree` removes the glue code that would otherwise connect them.
## Beyond Loading Files
`import-tree` is not just a file loader. Its builder API lets you:
-- **Filter** which files are selected — by predicate, regex, or both.
-- **Transform** discovered paths — wrap them in custom modules, read their contents, or anything else.
+- **Filter** which files are selected - by predicate, regex, or both.
+- **Transform** discovered paths - wrap them in custom modules, read their contents, or anything else.
- **Compose** multiple directory trees with shared filters.
-- **Extend** with domain-specific APIs — let library authors ship curated import-tree instances.
+- **Extend** with domain-specific APIs - let library authors ship curated import-tree instances.
This makes `import-tree` useful for sharing pre-configured sets of modules across projects. Library authors can ship an `import-tree` instance with custom filters and API methods, and consumers pick what they need:
@@ -73,7 +73,7 @@ lib.modules-tree = import-tree.addAPI {
## Design Goals
-- **Zero dependencies** — a single `default.nix`, no extra flake inputs
-- **Works everywhere** — flakes, non-flakes, any module system
-- **Composable** — builder pattern with filter/map/extend chains
-- **Predictable** — sensible defaults, clear ignore rules, no magic
+- **Zero dependencies** - a single `default.nix`, no extra flake inputs
+- **Works everywhere** - flakes, non-flakes, any module system
+- **Composable** - builder pattern with filter/map/extend chains
+- **Predictable** - sensible defaults, clear ignore rules, no magic
diff --git a/docs/src/content/docs/overview.mdx b/docs/src/content/docs/overview.mdx
index 24c33f4..ba663ec 100644
--- a/docs/src/content/docs/overview.mdx
+++ b/docs/src/content/docs/overview.mdx
@@ -37,7 +37,7 @@ The `_helpers/` directory is skipped because paths containing `/_` are ignored b
- Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim — any Nix module system.
+ Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim - any Nix module system.
Quick Start
@@ -45,11 +45,11 @@ The `_helpers/` directory is skipped because paths containing `/_` are ignored b
Filtering Guide
- Use `.map` to transform discovered paths — wrap in modules, read contents, or anything else.
+ Use `.map` to transform discovered paths - wrap in modules, read contents, or anything else.
Mapping Guide
- Build domain-specific APIs with `.addAPI` — create named presets, feature flags, module sets.
+ Build domain-specific APIs with `.addAPI` - create named presets, feature flags, module sets.
Custom API Guide
diff --git a/docs/src/content/docs/reference/api.mdx b/docs/src/content/docs/reference/api.mdx
index 5eb8589..0222cd4 100644
--- a/docs/src/content/docs/reference/api.mdx
+++ b/docs/src/content/docs/reference/api.mdx
@@ -18,7 +18,7 @@ inputs.import-tree.url = "github:denful/import-tree";
let import-tree = import ./path-to/import-tree;
```
-The resulting value is a callable attrset — the primary `import-tree` object.
+The resulting value is a callable attrset - the primary `import-tree` object.
---
@@ -54,7 +54,7 @@ Non-path values (like attrsets) are passed through the filter and included if th
### `.filter `
-`fn : string -> bool` — only include paths where `fn` returns `true`.
+`fn : string -> bool` - only include paths where `fn` returns `true`.
```nix
import-tree.filter (lib.hasInfix ".mod.") ./modules
@@ -64,7 +64,7 @@ Multiple `.filter` calls compose with AND.
### `.filterNot `
-Inverse of `.filter` — exclude paths where `fn` returns `true`.
+Inverse of `.filter` - exclude paths where `fn` returns `true`.
```nix
import-tree.filterNot (lib.hasInfix "experimental") ./modules
@@ -105,7 +105,7 @@ Also applies to non-path items in import lists.
### `.map `
-`fn : path -> a` — transform each discovered path.
+`fn : path -> a` - transform each discovered path.
```nix
import-tree.map lib.traceVal ./modules # trace each path
@@ -143,7 +143,35 @@ import-tree.addAPI {
}
```
-Methods are late-bound — you can reference methods added in later `.addAPI` calls.
+Methods are late-bound - you can reference methods added in later `.addAPI` calls.
+
+## Scoped Imports
+
+### `.addScoped `
+
+Add attributes to the scope passed to imported modules. Enables direct access to custom variables inside imports:
+
+```nix
+import-tree.addScoped { foo = 42; mylib = lib; } ./modules
+# modules can now use `foo` and `mylib` directly
+```
+
+The provided attributes are merged into the scope used by `builtins.scopedImport`:
+
+```nix
+# module.nix - can reference scoped values directly
+{
+ config.myValue = foo;
+ config.libResult = mylib.version;
+}
+```
+
+Multiple `.addScoped` calls accumulate:
+
+```nix
+import-tree (it: it.addScoped { foo = 42 }) (it: it.addScoped { bar = 99 })
+# modules can access both foo and bar
+```
---
@@ -193,7 +221,7 @@ Evaluate with an empty path list. Equivalent to calling with `[]`:
### `.new`
-Returns a fresh import-tree with empty state — no paths, filters, maps, or API extensions.
+Returns a fresh import-tree with empty state - no paths, filters, maps, or API extensions.
```nix
configured-tree.new # back to a clean slate
diff --git a/docs/src/content/docs/reference/examples.mdx b/docs/src/content/docs/reference/examples.mdx
index 3587a09..37cb50c 100644
--- a/docs/src/content/docs/reference/examples.mdx
+++ b/docs/src/content/docs/reference/examples.mdx
@@ -50,23 +50,21 @@ import-tree.match ".*/[a-z]+_[a-z]+\.nix" ./modules
## Pipeline Style
```nix
-lib.pipe import-tree [
+import-tree
(i: i.filter (lib.hasInfix "/desktop/"))
(i: i.map lib.traceVal)
(i: i ./modules)
-]
```
## Non-Nix File Discovery
```nix
-lib.pipe import-tree [
+import-tree
(i: i.initFilter (lib.hasSuffix ".json"))
(i: i.map builtins.readFile)
(i: i.map builtins.fromJSON)
(i: i.withLib lib)
(i: i.leafs ./config)
-]
# => list of parsed JSON objects
```
@@ -74,14 +72,13 @@ lib.pipe import-tree [
```nix
let
- my-tree = lib.pipe import-tree [
+ my-tree = import-tree
(i: i.addPath ./modules)
(i: i.addAPI {
desktop = self: self.filter (lib.hasInfix "/desktop/");
server = self: self.filter (lib.hasInfix "/server/");
all = self: self;
- })
- ];
+ });
in {
# Use the desktop subset
imports = [ my-tree.desktop ];
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..278b982
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,15 @@
+{
+ pkgs ? import { },
+ ...
+}:
+pkgs.mkShell {
+ buildInputs = [
+ pkgs.nix-unit
+ pkgs.treefmt
+ pkgs.nixfmt
+ pkgs.just
+ pkgs.nodejs
+ pkgs.pnpm
+ pkgs.bc
+ ];
+}
diff --git a/tests.nix b/tests.nix
new file mode 100644
index 0000000..600402f
--- /dev/null
+++ b/tests.nix
@@ -0,0 +1,273 @@
+let
+ lib = import ;
+ it = import ./.;
+ lit = it.withLib lib;
+in
+{
+ import-tree = {
+ leafs."test fails if no lib has been set" = {
+ expr = it.leafs ./tree;
+ expectedError.type = "ThrownError";
+ };
+
+ leafs."test succeeds when lib has been set" = {
+ expr = (it.withLib lib).leafs ./tree/hello;
+ expected = [ ];
+ };
+
+ leafs."test only returns nix non-ignored files" = {
+ expr = lit.leafs ./tree/a;
+ expected = [
+ ./tree/a/a_b.nix
+ ./tree/a/b/b_a.nix
+ ./tree/a/b/m.nix
+ ];
+ };
+
+ filter."test returns empty if no nix files with true predicate" = {
+ expr = (lit.filter (_: false)).leafs ./tree;
+ expected = [ ];
+ };
+
+ filter."test only returns nix files with true predicate" = {
+ expr = (lit.filter (lib.hasSuffix "m.nix")).leafs ./tree;
+ expected = [ ./tree/a/b/m.nix ];
+ };
+
+ filter."test multiple `filter`s compose" = {
+ expr = ((lit.filter (lib.hasInfix "b/")).filter (lib.hasInfix "_")).leafs ./tree;
+ expected = [ ./tree/a/b/b_a.nix ];
+ };
+
+ match."test returns empty if no files match regex" = {
+ expr = (lit.match "badregex").leafs ./tree;
+ expected = [ ];
+ };
+
+ match."test returns files matching regex" = {
+ expr = (lit.match ".*/[^/]+_[^/]+\.nix").leafs ./tree;
+ expected = [
+ ./tree/a/a_b.nix
+ ./tree/a/b/b_a.nix
+ ];
+ };
+
+ matchNot."test returns files not matching regex" = {
+ expr = (lit.matchNot ".*/[^/]+_[^/]+\.nix").leafs ./tree/a/b;
+ expected = [
+ ./tree/a/b/m.nix
+ ];
+ };
+
+ match."test `match` composes with `filter`" = {
+ expr = ((lit.match ".*a_b.nix").filter (lib.hasInfix "/a/")).leafs ./tree;
+ expected = [ ./tree/a/a_b.nix ];
+ };
+
+ match."test multiple `match`s compose" = {
+ expr = ((lit.match ".*/[^/]+_[^/]+\.nix").match ".*b\.nix").leafs ./tree;
+ expected = [ ./tree/a/a_b.nix ];
+ };
+
+ map."test transforms each matching file with function" = {
+ expr = (lit.map import).leafs ./tree/x;
+ expected = [ "z" ];
+ };
+
+ map."test `map` composes with `filter`" = {
+ expr = ((lit.filter (lib.hasInfix "/x")).map import).leafs ./tree;
+ expected = [ "z" ];
+ };
+
+ map."test multiple `map`s compose" = {
+ expr = ((lit.map import).map builtins.stringLength).leafs ./tree/x;
+ expected = [ 1 ];
+ };
+
+ addPath."test `addPath` prepends a path to filter" = {
+ expr = (lit.addPath ./tree/x).files;
+ expected = [ ./tree/x/y.nix ];
+ };
+
+ addPath."test `addPath` can be called multiple times" = {
+ expr = ((lit.addPath ./tree/x).addPath ./tree/a/b).files;
+ expected = [
+ ./tree/x/y.nix
+ ./tree/a/b/b_a.nix
+ ./tree/a/b/m.nix
+ ];
+ };
+
+ addPath."test `addPath` identity" = {
+ expr = ((lit.addPath ./tree/x).addPath ./tree/a/b).files;
+ expected = lit.leafs [
+ ./tree/x
+ ./tree/a/b
+ ];
+ };
+
+ new."test `new` returns a clear state" = {
+ expr = lib.pipe lit [
+ (i: i.addPath ./tree/x)
+ (i: i.addPath ./tree/a/b)
+ (i: i.new)
+ (i: i.addPath ./tree/modules/hello-world)
+ (i: i.withLib lib)
+ (i: i.files)
+ ];
+ expected = [ ./tree/modules/hello-world/mod.nix ];
+ };
+
+ initFilter."test can change the initial filter to look for other file types" = {
+ expr = (lit.initFilter (p: lib.hasSuffix ".txt" p)).leafs [ ./tree/a ];
+ expected = [ ./tree/a/a.txt ];
+ };
+
+ initFilter."test initf does filter non-paths" = {
+ expr =
+ let
+ mod = (it.initFilter (x: !(x ? config.boom))) [
+ {
+ options.hello = lib.mkOption {
+ default = "world";
+ type = lib.types.str;
+ };
+ }
+ {
+ config.boom = "boom";
+ }
+ ];
+ res = lib.modules.evalModules { modules = [ mod ]; };
+ in
+ res.config.hello;
+ expected = "world";
+ };
+
+ addAPI."test extends the API available on an import-tree object" = {
+ expr =
+ let
+ extended = lit.addAPI { helloOption = self: self.addPath ./tree/modules/hello-option; };
+ in
+ extended.helloOption.files;
+ expected = [ ./tree/modules/hello-option/mod.nix ];
+ };
+
+ addAPI."test preserves previous API extensions on an import-tree object" = {
+ expr =
+ let
+ first = lit.addAPI { helloOption = self: self.addPath ./tree/modules/hello-option; };
+ second = first.addAPI { helloWorld = self: self.addPath ./tree/modules/hello-world; };
+ extended = second.addAPI { res = self: self.helloOption.files; };
+ in
+ extended.res;
+ expected = [ ./tree/modules/hello-option/mod.nix ];
+ };
+
+ addAPI."test API extensions are late bound" = {
+ expr =
+ let
+ first = lit.addAPI { res = self: self.late; };
+ extended = first.addAPI { late = _self: "hello"; };
+ in
+ extended.res;
+ expected = "hello";
+ };
+
+ pipeTo."test pipes list into a function" = {
+ expr = (lit.map lib.pathType).pipeTo (lib.length) ./tree/x;
+ expected = 1;
+ };
+
+ import-tree."test does not break if given a path to a file instead of a directory." = {
+ expr = lit.leafs ./tree/x/y.nix;
+ expected = [ ./tree/x/y.nix ];
+ };
+
+ import-tree."test returns a module with a single imported nested module having leafs" = {
+ expr =
+ let
+ oneElement = arr: if lib.length arr == 1 then lib.elemAt arr 0 else throw "Expected one element";
+ module = it ./tree/x;
+ inner = (oneElement module.imports) { inherit lib; };
+ in
+ oneElement inner.imports;
+ expected = ./tree/x/y.nix;
+ };
+
+ import-tree."test evaluates returned module as part of module-eval" = {
+ expr =
+ let
+ res = lib.modules.evalModules { modules = [ (it ./tree/modules) ]; };
+ in
+ res.config.hello;
+ expected = "world";
+ };
+
+ import-tree."test can itself be used as a module" = {
+ expr =
+ let
+ res = lib.modules.evalModules { modules = [ (it.addPath ./tree/modules) ]; };
+ in
+ res.config.hello;
+ expected = "world";
+ };
+
+ import-tree."test take as arg anything path convertible" = {
+ expr = lit.leafs [
+ {
+ outPath = ./tree/modules/hello-world;
+ }
+ ];
+ expected = [ ./tree/modules/hello-world/mod.nix ];
+ };
+
+ import-tree."test passes non-paths without string conversion" = {
+ expr =
+ let
+ mod = it [
+ {
+ options.hello = lib.mkOption {
+ default = "world";
+ type = lib.types.str;
+ };
+ }
+ ];
+ res = lib.modules.evalModules { modules = [ mod ]; };
+ in
+ res.config.hello;
+ expected = "world";
+ };
+
+ import-tree."test can take other import-trees as if they were paths" = {
+ expr = (lit.filter (lib.hasInfix "mod")).leafs [
+ (it.addPath ./tree/modules/hello-option)
+ ./tree/modules/hello-world
+ ];
+ expected = [
+ ./tree/modules/hello-option/mod.nix
+ ./tree/modules/hello-world/mod.nix
+ ];
+ };
+
+ leafs."test loads from hidden directory but excludes sub-hidden" = {
+ expr = lit.leafs ./tree/a/b/_c;
+ expected = [ ./tree/a/b/_c/d/e.nix ];
+ };
+
+ scoped."test adds attrs via scopedImport" = {
+ expr =
+ (lib.evalModules {
+ modules = [
+ ((lit.addScoped { foo = 22; }) ./tree/_scoped)
+ ];
+ }).config.foo;
+ expected = 22;
+ };
+
+ combinator."test combinator syntax to compose import-tree" = {
+ expr = it (it: it.withLib lib) (it: it.leafs) ./tree/_scoped;
+ expected = [ ./tree/_scoped/foo.nix ];
+ };
+ };
+
+}
diff --git a/checkmate/tree/_scoped/foo.nix b/tree/_scoped/foo.nix
similarity index 100%
rename from checkmate/tree/_scoped/foo.nix
rename to tree/_scoped/foo.nix
diff --git a/checkmate/tree/a/a.txt b/tree/a/a.txt
similarity index 100%
rename from checkmate/tree/a/a.txt
rename to tree/a/a.txt
diff --git a/checkmate/tree/a/a_b.nix b/tree/a/a_b.nix
similarity index 100%
rename from checkmate/tree/a/a_b.nix
rename to tree/a/a_b.nix
diff --git a/checkmate/tree/a/b/_c/d/_f.nix b/tree/a/b/_c/d/_f.nix
similarity index 100%
rename from checkmate/tree/a/b/_c/d/_f.nix
rename to tree/a/b/_c/d/_f.nix
diff --git a/checkmate/tree/a/b/_c/d/e.nix b/tree/a/b/_c/d/e.nix
similarity index 100%
rename from checkmate/tree/a/b/_c/d/e.nix
rename to tree/a/b/_c/d/e.nix
diff --git a/checkmate/tree/a/b/_n.nix b/tree/a/b/_n.nix
similarity index 100%
rename from checkmate/tree/a/b/_n.nix
rename to tree/a/b/_n.nix
diff --git a/checkmate/tree/a/b/b_a.nix b/tree/a/b/b_a.nix
similarity index 100%
rename from checkmate/tree/a/b/b_a.nix
rename to tree/a/b/b_a.nix
diff --git a/checkmate/tree/a/b/m.nix b/tree/a/b/m.nix
similarity index 100%
rename from checkmate/tree/a/b/m.nix
rename to tree/a/b/m.nix
diff --git a/checkmate/tree/hello/world b/tree/hello/world
similarity index 100%
rename from checkmate/tree/hello/world
rename to tree/hello/world
diff --git a/checkmate/tree/modules/hello-option/mod.nix b/tree/modules/hello-option/mod.nix
similarity index 100%
rename from checkmate/tree/modules/hello-option/mod.nix
rename to tree/modules/hello-option/mod.nix
diff --git a/checkmate/tree/modules/hello-world/mod.nix b/tree/modules/hello-world/mod.nix
similarity index 100%
rename from checkmate/tree/modules/hello-world/mod.nix
rename to tree/modules/hello-world/mod.nix
diff --git a/checkmate/tree/x/y.nix b/tree/x/y.nix
similarity index 100%
rename from checkmate/tree/x/y.nix
rename to tree/x/y.nix
diff --git a/treefmt.toml b/treefmt.toml
new file mode 100644
index 0000000..d50e8eb
--- /dev/null
+++ b/treefmt.toml
@@ -0,0 +1,3 @@
+[formatter.nixfmt]
+command = "nixfmt"
+includes = [ "*.nix" ]
\ No newline at end of file