diff --git a/default.nix b/default.nix index b5eab39..a015c6c 100644 --- a/default.nix +++ b/default.nix @@ -1,7 +1,6 @@ let perform = { - lib ? null, pipef ? null, initf ? null, filterf, @@ -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 ( @@ -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; @@ -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: @@ -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); @@ -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); }; diff --git a/docs/src/content/docs/guides/combinator.mdx b/docs/src/content/docs/guides/combinator.mdx index 153e61e..6c1c6f7 100644 --- a/docs/src/content/docs/guides/combinator.mdx +++ b/docs/src/content/docs/guides/combinator.mdx @@ -12,13 +12,13 @@ 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 @@ -26,17 +26,16 @@ import-tree (it: it.withLib lib) (it: it.leafs) ./modules 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. @@ -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 diff --git a/docs/src/content/docs/guides/mapping.mdx b/docs/src/content/docs/guides/mapping.mdx index ebd0496..b1ac47b 100644 --- a/docs/src/content/docs/guides/mapping.mdx +++ b/docs/src/content/docs/guides/mapping.mdx @@ -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) ``` @@ -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..." ] diff --git a/docs/src/content/docs/guides/outside-modules.mdx b/docs/src/content/docs/guides/outside-modules.mdx index f157447..0e85366 100644 --- a/docs/src/content/docs/guides/outside-modules.mdx +++ b/docs/src/content/docs/guides/outside-modules.mdx @@ -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 ] ``` @@ -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) ``` @@ -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) ``` @@ -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) ``` diff --git a/docs/src/content/docs/reference/api.mdx b/docs/src/content/docs/reference/api.mdx index 0222cd4..fe63575 100644 --- a/docs/src/content/docs/reference/api.mdx +++ b/docs/src/content/docs/reference/api.mdx @@ -177,20 +177,12 @@ import-tree (it: it.addScoped { foo = 42 }) (it: it.addScoped { bar = 99 }) ## Output -### `.withLib ` - -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 ] ``` @@ -199,7 +191,7 @@ 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 ` @@ -207,7 +199,7 @@ Shorthand for `.leafs.result`: 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 ``` diff --git a/docs/src/content/docs/reference/examples.mdx b/docs/src/content/docs/reference/examples.mdx index 37cb50c..73e1b5d 100644 --- a/docs/src/content/docs/reference/examples.mdx +++ b/docs/src/content/docs/reference/examples.mdx @@ -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 ``` @@ -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 diff --git a/tests.nix b/tests.nix index 600402f..9e16deb 100644 --- a/tests.nix +++ b/tests.nix @@ -1,16 +1,22 @@ let lib = import ; it = import ./.; + # withLib is now a no-op (the tree reader is pure builtins), so `lit == it`. + # Kept so these tests also exercise the backward-compat shim. 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 works without withLib (lib no longer required)" = { + expr = it.leafs ./tree/a; + expected = [ + ./tree/a/a_b.nix + ./tree/a/b/b_a.nix + ./tree/a/b/m.nix + ]; }; - leafs."test succeeds when lib has been set" = { + leafs."test withLib is a no-op kept for backward compatibility" = { expr = (it.withLib lib).leafs ./tree/hello; expected = [ ]; };