Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 97 additions & 49 deletions default.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
let
perform =
{
lib ? null,
pipef ? null,
initf ? null,
filterf,
Expand All @@ -12,23 +11,19 @@ let
}:
path:
let
result =
if pipef == null then
{ imports = [ module ]; }
else if lib == null then
throw "You need to call withLib before trying to read the tree."
else
pipef (leafs lib path);
result = if pipef == null then { imports = [ module ]; } else pipef (leafs path);

# module exists so we delay access to lib til we are part of the module system.
# module stays a function: callers may apply it as a module function, and
# keeping it a lambda defers the tree read (`leafs path`) until module-eval
# rather than at `it ./dir` construction time. It no longer needs `lib` —
# the reader is pure `builtins` — so the argument is ignored.
module =
{ lib, ... }:
_:
let
files = leafs lib path;
imports = if scoped == { } then files else map scoped-import files;
files = leafs path;
in
{
inherit imports;
imports = if scoped == { } then files else map scoped-import files;
};

scoped-import = builtins.scopedImport (
Expand All @@ -40,22 +35,19 @@ let
);

leafs =
lib:
let
treeFiles = t: (t.withLib lib).files;

listFilesRecursive =
x:
if isImportTree x then
treeFiles x
x.files
else if hasOutPath x then
listFilesRecursive x.outPath
else if isDirectory x then
lib.filesystem.listFilesRecursive x
listDirFilesRecursive x
else
[ x ];

nixFilter = andNot (lib.hasInfix "/_") (lib.hasSuffix ".nix");
nixFilter = andNot (hasInfix "/_") (hasSuffix ".nix");

initialFilter = if initf != null then initf else nixFilter;

Expand All @@ -68,31 +60,31 @@ let
isFileRelative =
root:
{ file, rel }:
if file != null && lib.hasPrefix root file then
if file != null && hasPrefix root file then
{
file = null;
rel = lib.removePrefix root file;
rel = removePrefix root file;
}
else
{ inherit file rel; };
getFileRelative = { file, rel }: if rel == null then file else rel;

# Strip the first matching root-directory prefix from a file, yielding a
# path relative to that root (or the file itself if none match). Folds the
# `{ file, rel }` state directly over the roots — once a root matches,
# `file` becomes null and later roots are no-ops.
makeRelative =
roots:
lib.pipe roots [
(lib.lists.flatten)
(builtins.filter isDirectory)
(builtins.map builtins.toString)
(builtins.map isFileRelative)
(fx: fx ++ [ getFileRelative ])
(
fx: file:
lib.pipe {
file = builtins.toString file;
rel = null;
} fx
)
];
let
dirs = builtins.map builtins.toString (builtins.filter isDirectory (flatten roots));
in
file:
getFileRelative (
builtins.foldl' (acc: root: isFileRelative root acc) {
file = builtins.toString file;
rel = null;
} dirs
);

rootRelative =
roots:
Expand All @@ -102,24 +94,78 @@ let
x: if isPathLike x then mkRel x else x;
in
root:
lib.pipe
[ paths root ]
[
(lib.lists.flatten)
(map listFilesRecursive)
(lib.lists.flatten)
(builtins.filter (
compose filter (rootRelative [
paths
root
])
))
(map mapf)
let
roots = flatten [
paths
root
];
files = flatten (map listFilesRecursive roots);
relativize = rootRelative [
paths
root
];
in
map mapf (builtins.filter (compose filter relativize) files);

in
result;

# ── utilities vendored from nixpkgs lib (behavior-identical, pure builtins) ──

# lib.lists.flatten
flatten = x: if builtins.isList x then builtins.concatMap flatten x else [ x ];

# lib.strings.hasPrefix
hasPrefix = pre: s: builtins.substring 0 (builtins.stringLength pre) s == pre;

# lib.strings.removePrefix
removePrefix =
pre: s:
let
n = builtins.stringLength pre;
in
if builtins.substring 0 n s == pre then builtins.substring n (builtins.stringLength s - n) s else s;

# lib.strings.hasSuffix
hasSuffix =
suffix: content:
let
lenSuffix = builtins.stringLength suffix;
lenContent = builtins.stringLength content;
in
lenContent >= lenSuffix && builtins.substring (lenContent - lenSuffix) lenContent content == suffix;

# lib.strings.hasInfix — literal substring scan (no regex, so no escaping hazard)
hasInfix =
infix: content:
let
lenInfix = builtins.stringLength infix;
lenContent = builtins.stringLength content;
go =
i:
if i + lenInfix > lenContent then
false
else if builtins.substring i lenInfix content == infix then
true
else
go (i + 1);
in
go 0;

# lib.filesystem.listFilesRecursive
listDirFilesRecursive =
dir:
let
entries = builtins.readDir dir;
in
builtins.concatMap (
name:
if entries.${name} == "directory" then
listDirFilesRecursive (dir + "/${name}")
else
[ (dir + "/${name}") ]
) (builtins.attrNames entries);

compose =
g: f: x:
g (f x);
Expand Down Expand Up @@ -199,7 +245,9 @@ let
addAPI = api: accAttr "api" (a: a // api);

# Configuration updates (non-accumulating)
withLib = lib: mergeAttrs { inherit lib; };
# withLib is retained as a no-op for backward compatibility: the tree
# reader no longer needs nixpkgs lib, so any passed lib is ignored.
withLib = _lib: mergeAttrs { };
initFilter = initf: mergeAttrs { inherit initf; };
pipeTo = pipef: mergeAttrs { inherit pipef; };
leafs = mergeAttrs { pipef = (i: i); };
Expand Down
14 changes: 6 additions & 8 deletions docs/src/content/docs/guides/combinator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,30 @@ Combinator syntax allows you to pass functions to `import-tree` to configure and
```nix
# Without combinators (verbose)
let
configured = import-tree.withLib lib;
configured = import-tree.map import;
leafs = configured.leafs;
in
leafs ./modules

# With combinators (terse)
import-tree (it: it.withLib lib) (it: it.leafs) ./modules
import-tree (it: it.map import) (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)
import-tree (self: self.addPath ./modules)
```

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
(it: it.addPath ./modules) # first: add a path
(it: it.filter (lib.hasSuffix "mod.nix")) # then: filter
./src # finally: evaluate
```

This is purely syntactic sugar - it's equivalent to piping through the operations in order.
Expand All @@ -50,7 +49,6 @@ 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
Expand Down
2 changes: 0 additions & 2 deletions docs/src/content/docs/guides/mapping.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Multiple `.map` calls compose left-to-right (the first map runs first):
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)
```

Expand All @@ -42,7 +41,6 @@ When used with `.leafs` or `.pipeTo`, `.map` transforms paths into arbitrary val
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..." ]
Expand Down
21 changes: 5 additions & 16 deletions docs/src/content/docs/guides/outside-modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,18 @@ description: Use import-tree to list files programmatically, without importing t
`import-tree` doesn't have to produce module imports. You can use it to get a plain list of files:

```nix
(import-tree.withLib pkgs.lib).leafs ./modules
import-tree.leafs ./modules
# => [ /path/to/modules/a.nix /path/to/modules/b.nix ]
```

### withLib

Outside module evaluation, `import-tree` needs access to `lib` (specifically `lib.filesystem.listFilesRecursive`). Call `.withLib` before `.leafs` or `.pipeTo`:

```nix
import-tree.withLib lib
```

Omitting `.withLib` when calling `.leafs` produces an error:
`"You need to call withLib before trying to read the tree."`
The tree reader is implemented with pure `builtins`, so `.leafs`, `.files` and `.pipeTo` work everywhere — including outside module evaluation — with no setup.

### leafs

`.leafs` returns a configured import-tree that, when given a path, produces a flat list of discovered files:

```nix
(import-tree.withLib lib).leafs ./src
import-tree.leafs ./src
# => [ ./src/main.nix ./src/utils.nix ]
```

Expand All @@ -39,7 +30,6 @@ Omitting `.withLib` when calling `.leafs` produces an error:
```nix
import-tree
(i: i.addPath ./modules)
(i: i.withLib lib)
(i: i.files)
```

Expand All @@ -48,7 +38,7 @@ import-tree
`.pipeTo` takes a function that receives the list of discovered paths, letting you process the results:

```nix
import-tree (it: it.withLib lib) (it: it.pipeTo builtins.length) ./modules
import-tree.pipeTo builtins.length ./modules
# => 5 (number of .nix files)
```

Expand All @@ -57,8 +47,7 @@ Combine with `.map` for powerful pipelines:
```nix
import-tree
(i: i.map import)
(i: i.pipeTo lib.length)
(i: i.withLib lib)
(i: i.pipeTo builtins.length)
(i: i ./modules)
```

Expand Down
14 changes: 3 additions & 11 deletions docs/src/content/docs/reference/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,12 @@ import-tree (it: it.addScoped { foo = 42 }) (it: it.addScoped { bar = 99 })

## Output

### `.withLib <lib>`

Required before `.leafs` or `.pipeTo` when used outside module evaluation. Provides `lib.filesystem.listFilesRecursive`.

```nix
import-tree.withLib pkgs.lib
```

### `.leafs`

Returns a configured import-tree that produces file lists instead of modules:

```nix
(import-tree.withLib lib).leafs ./modules
import-tree.leafs ./modules
# => [ ./modules/a.nix ./modules/b.nix ]
```

Expand All @@ -199,15 +191,15 @@ Returns a configured import-tree that produces file lists instead of modules:
Shorthand for `.leafs.result`:

```nix
(import-tree.addPath ./modules).withLib lib |>.files
(import-tree.addPath ./modules).files
```

### `.pipeTo <fn>`

Like `.leafs` but pipes the result list through `fn`:

```nix
(import-tree.withLib lib).pipeTo builtins.length ./modules
import-tree.pipeTo builtins.length ./modules
# => 3
```

Expand Down
3 changes: 1 addition & 2 deletions docs/src/content/docs/reference/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ 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
```
Expand Down Expand Up @@ -91,7 +90,7 @@ in {
## Counting Files

```nix
(import-tree.withLib lib).pipeTo builtins.length ./modules
import-tree.pipeTo builtins.length ./modules
```

## Mixing with Manual Imports
Expand Down
Loading
Loading