Import all nix files in a directory tree. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic inputs

readme

+75 -183
+75 -183
README.md
··· 1 1 # 🌲🌴 import-tree 🎄🌳 2 2 3 - > Helper functions for import of [Nixpkgs module system](https://nix.dev/tutorials/module-system/) modules under a directory recursively 3 + > Recursively import [Nix modules](https://nix.dev/tutorials/module-system/) from a directory, with a simple, extensible API. 4 4 5 - - Flake callable; Easy to use, intuitive for the most common use case: `inputs.import-tree ./modules` 6 - - Module class agnostic; can be used for NixOS, nix-darwin, home-manager, flake-parts, NixVim. 7 - - Can be used outside flakes as a dependencies-free lib; Just import our `./default.nix`. 8 - - Can be used to list other file types, not just `.nix`. See `.initFilter`, `.files` API. 9 - - Extensible API. import-tree objects are customizable. See `.addAPI`. 10 - - Useful for implementing the [Dendritic Pattern](https://github.com/mightyiam/dendritic). 5 + ## Quick Start (flake-parts) 11 6 12 - ## Quick Usage (with flake-parts) 13 - 14 - This example shows how to load all nix files inside `./modules`, on [Dendritic](https://vic.github.io/dendrix/Dendritic.html) setups. (see also [flake-file's dendritic template](https://github.com/vic/flake-file?tab=readme-ov-file#flakemodulesdendritic)) 7 + Import all nix files inside `./modules` in your flake: 15 8 16 9 ```nix 17 10 { 18 11 inputs.import-tree.url = "github:vic/import-tree"; 19 12 inputs.flake-parts.url = "github:hercules-ci/flake-parts"; 20 13 21 - outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); 14 + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } 15 + (inputs.import-tree ./modules); 22 16 } 23 17 ``` 24 18 25 - ## Quick Usage (outside a modules evaluation) 19 + > By default, paths having `/_` are ignored. 26 20 27 - If you want to get a list of nix files programmatically outside of a modules evaluation, 28 - you can use the import-tree API (read below for more). 21 + ## Features 22 + 23 + 🌳 Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim, etc.\ 24 + 🌲 Callable as a deps-free Flake or nix lib.\ 25 + 🌴 API for listing custom file types with filters and transformations.\ 26 + 🌵 Extensible: add your own API methods to tailor import-tree objects.\ 27 + 🎄 No dependencies outside flakes: just `import ./default.nix`.\ 28 + 🌿 Useful on [Dendritic Pattern](https://github.com/mightyiam/dendritic) setups.\ 29 + 🌱 [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) 30 + 31 + ## Other Usage (outside module evaluation) 32 + 33 + Get a list of nix files programmatically: 29 34 30 35 ```nix 31 - (import-tree.withLib pkgs.lib).leafs ./modules # => list of .nix files 36 + (import-tree.withLib pkgs.lib).leafs ./modules 32 37 ``` 33 38 34 - ## Ignored files 35 - 36 - By default, paths having a component that begins with an underscore (`/_`) are ignored. 39 + <details> 40 + <summary>Advanced Usage, API, and Rationale</summary> 37 41 38 - This can be changed by using `.initFilter` API. 42 + ### Ignored files 39 43 40 - <details> 41 - <summary> 44 + By default, paths having a component that begins with an underscore (`/_`) are ignored. This can be changed by using `.initFilter` API. 42 45 43 - ## API usage 46 + ### API usage 44 47 45 48 The following goes recursively through `./modules` and imports all `.nix` files. 46 49 47 50 ```nix 48 - # Usage as part of any nix module system. 49 51 {config, ...} { 50 52 imports = [ (import-tree ./modules) ]; 51 53 } ··· 53 55 54 56 For more advanced usage, `import-tree` can be configured via its extensible API. 55 57 56 - </summary> 58 + ______________________________________________________________________ 57 59 58 - ## Obtaining the API 60 + #### Obtaining the API 59 61 60 - When used as a flake, the flake outputs attrset is the primary callable. 61 - Otherwise, importing the `default.nix` that is at the root of this repository will evaluate into the same attrset. 62 - This callable attrset is referred to as `import-tree` in this documentation. 62 + 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. 63 63 64 - ## `import-tree` 64 + #### `import-tree` 65 65 66 - Takes a single argument: path or deeply nested list of path. 67 - Returns a module that imports the discovered files. 68 - For example, given the following file tree: 66 + 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: 69 67 70 68 ``` 71 69 default.nix ··· 98 96 } 99 97 ``` 100 98 101 - If given a deeply nested list of paths the list will be flattened and results concatenated. 102 - The following is valid usage: 99 + If given a deeply nested list of paths the list will be flattened and results concatenated. The following is valid usage: 103 100 104 101 ```nix 105 102 {lib, config, ...} { ··· 109 106 110 107 Other import-tree objects can also be given as arguments (or in lists) as if they were paths. 111 108 112 - As an special case, when the single argument given to an `import-tree` object is an 113 - attribute-set containing an `options` attribute, the `import-tree` object 114 - assumes it is being evaluated as a module. This way, a pre-configured `import-tree` can 115 - also be used directly in a list of module imports. 116 - 117 - This is useful for authors exposing pre-configured `import-tree`s that users can directly 118 - add to their import list or continue configuring themselves using its API. 119 - 120 - ```nix 121 - let 122 - # imagine this configured tree comes from some author's flake or library. 123 - # library author can extend an import-tree with custom API methods 124 - # according to the library's directory and file naming conventions. 125 - configured-tree = import-tree.addAPI { 126 - # the knowledge of where modules are located inside the library structure 127 - # or which filters/regexes/transformations to apply are abstracted 128 - # from the user by the author providing a meaningful API. 129 - maximal = self: self.addPath ./modules; 130 - minimal = self: self.maximal.filter (lib.hasInfix "minimal"); 131 - }; 132 - in { 133 - # the library user can directly import or further configure an import-tree. 134 - imports = [ configured-tree.minimal ]; 135 - } 136 - ``` 109 + 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. 137 110 138 - ## Configurable behavior 111 + #### Configurable behavior 139 112 140 - `import-tree` objects with custom behavior can be obtained using a builder pattern. 141 - For example: 113 + `import-tree` objects with custom behavior can be obtained using a builder pattern. For example: 142 114 143 115 ```nix 144 116 lib.pipe import-tree [ 145 - (i: i.map lib.traceVal) # trace all paths. useful for debugging what is being imported. 146 - (i: i.filter (lib.hasInfix ".mod.")) # filter nix files by some predicate 147 - (i: i ./modules) # finally, call the configured import-tree with a path 117 + (i: i.map lib.traceVal) 118 + (i: i.filter (lib.hasInfix ".mod.")) 119 + (i: i ./modules) 148 120 ] 149 121 ``` 150 122 151 - Here is a simpler but less readable equivalent: 123 + Or, in a simpler but less readable way: 152 124 153 125 ```nix 154 126 ((import-tree.map lib.traceVal).filter (lib.hasInfix ".mod.")) ./modules 155 127 ``` 156 128 157 - ### `import-tree.filter` and `import-tree.filterNot` 158 - 159 - `filter` takes a predicate function `path -> bool`. Only paths for which the filter returns `true` are selected: 129 + ##### `import-tree.filter` and `import-tree.filterNot` 160 130 161 - > \[!NOTE\] 162 - > Only files with suffix `.nix` are candidates. 131 + `filter` takes a predicate function `path -> bool`. Only files with suffix `.nix` are candidates. 163 132 164 133 ```nix 165 - # import-tree.filter : (path -> bool) -> import-tree 166 - 167 134 import-tree.filter (lib.hasInfix ".mod.") ./some-dir 168 135 ``` 169 136 170 - `filter` can be applied multiple times, in which case only the files matching _all_ filters will be selected: 171 - 172 - ```nix 173 - lib.pipe import-tree [ 174 - (i: i.filter (lib.hasInfix ".mod.")) 175 - (i: i.filter (lib.hasSuffix "default.nix")) 176 - (i: i ./some-dir) 177 - ] 178 - ``` 179 - 180 - Or, in a simpler but less readable way: 181 - 182 - ```nix 183 - (import-tree.filter (lib.hasInfix ".mod.")).filter (lib.hasSuffix "default.nix") ./some-dir 184 - ``` 185 - 186 - See also `import-tree.initFilter`. 137 + Multiple filters can be combined, results must match all of them. 187 138 188 - ### `import-tree.match` and `import-tree.matchNot` 139 + ##### `import-tree.match` and `import-tree.matchNot` 189 140 190 141 `match` takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with `builtins.match`. 191 142 192 143 ```nix 193 - # import-tree.match : regex -> import-tree 194 - 195 144 import-tree.match ".*/[a-z]+@(foo|bar)\.nix" ./some-dir 196 145 ``` 197 146 198 - `match` can be applied multiple times, in which case only the paths matching _all_ regex patterns will be selected, and can be combined with any number of `filter`, in any order. 147 + Multiple match filters can be added, results must match all of them. 199 148 200 - ### `import-tree.map` 149 + ##### `import-tree.map` 201 150 202 151 `map` can be used to transform each path by providing a function. 203 152 204 - e.g. to convert the path into a module explicitly: 205 - 206 153 ```nix 207 - # import-tree.map : (path -> any) -> import-tree 208 - 209 - import-tree.map (path: { 210 - imports = [ path ]; 211 - # assuming such an option is declared 212 - automaticallyImportedPaths = [ path ]; 213 - }) 154 + # generate a custom module from path 155 + import-tree.map (path: { imports = [ path ]; }) 214 156 ``` 215 157 216 - `map` can be applied multiple times, composing the transformations: 158 + Outside modules evaluation, you can transform paths into something else: 217 159 218 160 ```nix 219 161 lib.pipe import-tree [ 220 - (i: i.map (lib.removeSuffix ".nix")) 221 - (i: i.map builtins.stringLength) 222 - ] ./some-dir 162 + (i: i.map builtins.readFile) 163 + (i: i.withLib lib) 164 + (i: i.leafs ./dir) 165 + ] 166 + # => list of contents of all files. 223 167 ``` 224 168 225 - The above example first removes the `.nix` suffix from all selected paths, then takes their lengths. 169 + ##### `import-tree.addPath` 226 170 227 - Or, in a simpler but less readable way: 228 - 229 - ```nix 230 - ((import-tree.map (lib.removeSuffix ".nix")).map builtins.stringLength) ./some-dir 231 - ``` 232 - 233 - `map` can be combined with any number of `filter` and `match` calls, in any order, but the (composed) transformation is applied _after_ the filters, and only to the paths that match all of them. 234 - 235 - ### `import-tree.addPath` 236 - 237 - `addPath` can be used to prepend paths to be filter as a setup for import-tree. 238 - This function can be applied multiple times. 171 + `addPath` can be used to prepend paths to be filtered as a setup for import-tree. 239 172 240 173 ```nix 241 - # import-tree.addPath : (path_or_list_of_paths) -> import-tree 242 - 243 - # Both of these result in the same imported files. 244 - # however, the first adds ./vendor as a *pre-configured* path. 245 - # and the final user can supply ./modules or [] empty. 246 174 (import-tree.addPath ./vendor) ./modules 247 175 import-tree [./vendor ./modules] 248 176 ``` 249 177 250 - ### `import-tree.addAPI` 178 + ##### `import-tree.addAPI` 251 179 252 180 `addAPI` extends the current import-tree object with new methods. 253 - The API is cumulative, meaning that this function can be called multiple times. 254 - 255 - `addAPI` takes an attribute set of functions taking a single argument: 256 - `self` which is the current import-tree object. 257 181 258 182 ```nix 259 - # import-tree.addAPI : api-attr-set -> import-tree 260 - 261 183 import-tree.addAPI { 262 184 maximal = self: self.addPath ./modules; 263 - feature = self: featureName: self.maximal.filter (lib.hasInfix feature); 185 + feature = self: infix: self.maximal.filter (lib.hasInfix infix); 264 186 minimal = self: self.feature "minimal"; 265 187 } 266 188 ``` 267 189 268 - on the previous API, users can call `import-tree.feature "+vim"` or `import-tree.minimal`, etc. 190 + ##### `import-tree.withLib` 269 191 270 - ### `import-tree.withLib` 271 - 272 - > \[!NOTE\] 273 - > `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo`. 274 - > Because with the use of those functions the implementation does not have access to a `lib` that is provided as a module argument. 192 + `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo` when not used as part of a nix modules evaluation. 275 193 276 194 ```nix 277 - # import-tree.withLib : lib -> import-tree 278 - 279 195 import-tree.withLib pkgs.lib 280 196 ``` 281 197 282 - ### `import-tree.pipeTo` 198 + ##### `import-tree.pipeTo` 283 199 284 200 `pipeTo` takes a function that will receive the list of paths. 285 - When configured with this, `import-tree` will not return a nix module but the result of the function being piped to. 286 201 287 202 ```nix 288 - # import-tree.pipeTo : ([paths] -> any) -> import-tree 289 - 290 203 import-tree.pipeTo lib.id # equivalent to `.leafs` 291 204 ``` 292 205 293 - ### `import-tree.leafs` 206 + ##### `import-tree.leafs` 294 207 295 - `leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`. That is, instead of producing a nix module, just return the list of results. 208 + `leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`. 296 209 297 210 ```nix 298 - # import-tree.leafs : import-tree 299 - 300 211 import-tree.leafs 301 212 ``` 302 213 303 - ### `import-tree.new` 214 + ##### `import-tree.new` 304 215 305 - Returns a fresh import-tree with empty state. If you previously had any path, lib, filter, etc, 306 - you might need to set them on the new empty tree. 216 + Returns a fresh import-tree with empty state. 307 217 308 - ### `import-tree.initFilter` 218 + ##### `import-tree.initFilter` 309 219 310 220 *Replaces* the initial filter which defaults to: Include files with `.nix` suffix and not having `/_` infix. 311 221 312 - _NOTE_: initFilter is non-accumulating and is the *first* filter to run before those accumulated via `filter`/`match`. 313 - 314 - You can use this to make import-tree scan for other file types or change the ignore convention. 315 - 316 222 ```nix 317 - # import-tree.initFilter : (path -> bool) -> import-tree 318 - 319 - import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/") # nix files not inside /ignored/ 320 - import-tree.initFilter (lib.hasSuffix ".md") # scan for .md files everywhere, nothing ignored. 223 + import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/" p) 224 + import-tree.initFilter (lib.hasSuffix ".md") 321 225 ``` 322 226 323 - ### `import-tree.files` 227 + ##### `import-tree.files` 324 228 325 229 A shorthand for `import-tree.leafs.result`. Returns a list of matching files. 326 230 327 - This can be used when you don't want to import the tree, but just get a list of files from it. 328 - 329 - Useful for listing files other than `.nix`, for example, for passing all `.js` files to a minifier: 330 - 331 - _TIP_: remember to use `withLib` when *not* using import-tree as a module import. 332 - 333 231 ```nix 334 - # import-tree.files : [ <list-of-files> ] 335 - 336 - # paths to give to uglify-js 337 232 lib.pipe import-tree [ 338 - (i: i.initFilter (lib.hasSuffix ".js")) # look for .js files. ignore nothing. 339 - (i: i.addPath ./out) # under the typescript compiler outDir 340 - (i: i.withLib lib) # set lib since we are not importing modules. 233 + (i: i.initFilter (lib.hasSuffix ".js")) 234 + (i: i.addPath ./out) 235 + (i: i.withLib lib) 341 236 (i: i.files) 342 237 ] 343 - # => list of all .js files 344 238 ``` 345 239 346 - ### `import-tree.result` 240 + ##### `import-tree.result` 347 241 348 242 Exactly the same as calling the import-tree object with an empty list `[ ]`. 349 - This is useful for import-tree objects that already have paths configured via `.addPath`. 350 243 351 244 ```nix 352 - # import-tree.result : <module-or-piped-result> 353 - 354 - # these two are exactly the same: 355 245 (import-tree.addPath ./modules).result 356 246 (import-tree.addPath ./modules) [ ] 357 247 ``` 358 248 359 - </details> 249 + ______________________________________________________________________ 360 250 361 251 ## Why 362 252 ··· 427 317 } 428 318 ``` 429 319 430 - </details> 320 + ______________________________________________________________________ 431 321 432 322 ## Testing 433 323 ··· 444 334 ```sh 445 335 nix run github:vic/checkmate#fmt 446 336 ``` 337 + 338 + </details>