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