···11<p align="right">
22- <a href="https://github.com/sponsors/vic"><img src="https://img.shields.io/badge/sponsor-vic-white?logo=githubsponsors&logoColor=white&labelColor=%23FF0000" alt="Sponsor Vic"/>
22+ <a href="https://dendritic.oeiuwq.com/sponsor"><img src="https://img.shields.io/badge/sponsor-vic-white?logo=githubsponsors&logoColor=white&labelColor=%23FF0000" alt="Sponsor Vic"/>
33 </a>
44- <a href="https://vic.github.io/dendrix/Dendritic-Ecosystem.html#vics-dendritic-libraries"> <img src="https://img.shields.io/badge/Dendritic-Nix-informational?logo=nixos&logoColor=white" alt="Dendritic Nix"/> </a>
44+ <a href="https://dendritic.oeiuwq.com"> <img src="https://img.shields.io/badge/Dendritic-Nix-informational?logo=nixos&logoColor=white" alt="Dendritic Nix"/> </a>
55 <a href="LICENSE"> <img src="https://img.shields.io/github/license/vic/import-tree" alt="License"/> </a>
66 <a href="https://github.com/vic/import-tree/actions">
77 <img src="https://github.com/vic/import-tree/actions/workflows/test.yml/badge.svg" alt="CI Status"/> </a>
88</p>
991010-> `import-tree` and [vic](https://github.com/vic/vix)'s [dendritic libs](https://vic.github.io/dendrix/Dendritic-Ecosystem.html#vics-dendritic-libraries) made for you with Love++ and AI--. If you like my work, consider [sponsoring](https://github.com/sponsors/vic)
1010+> `import-tree` and [vic](https://bsky.app/profile/oeiuwq.bsky.social)'s [dendritic libs](https://dendritic.oeiuwq.com) made for you with Love++ and AI--. If you like my work, consider [sponsoring](https://dendritic.oeiuwq.com/sponsor)
11111212# 🌲🌴 import-tree 🎄🌳
13131414> Recursively import [Nix modules](https://nix.dev/tutorials/module-system/) from a directory, with a simple, extensible API.
15151616## Quick Start (flake-parts)
1717-1818-Import all nix files inside `./modules` in your flake:
19172018```nix
2119{
···2725}
2826```
29273030-> By default, paths having `/_` are ignored.
2828+By default, paths having `/_` are ignored.
31293230## Features
33313432🌳 Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim, etc.\
3533🌲 Callable as a deps-free Flake or nix lib.\
3634🌴 Sensible defaults and configurable behaviour.\
3737-🌵 API for listing custom file types with filters and transformations.\
3838-🎄 Extensible: add your own API methods to tailor import-tree objects.\
3939-🌿 Useful on [Dendritic Pattern](https://github.com/mightyiam/dendritic) setups.\
3535+🌵 Chain `.filter`, `.match`, `.map` for precise file selection.\
3636+🎄 Extensible: `.addAPI` to create domain-specific instances.\
3737+🌿 Built for the [Dendritic Pattern](https://github.com/mightyiam/dendritic).\
4038🌱 [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)
41394242-## Other Usage (outside module evaluation)
4343-4444-Get a list of nix files programmatically:
4545-4646-```nix
4747-(import-tree.withLib pkgs.lib).leafs ./modules
4848-```
4949-5050-<details>
5151-<summary>Advanced Usage, API, and Rationale</summary>
5252-5353-### Ignored files
5454-5555-By default, paths having a component that begins with an underscore (`/_`) are ignored. This can be changed by using `.initFilter` API.
5656-5757-### API usage
5858-5959-The following goes recursively through `./modules` and imports all `.nix` files.
6060-6161-```nix
6262-{config, ...} {
6363- imports = [ (import-tree ./modules) ];
6464-}
6565-```
6666-6767-For more advanced usage, `import-tree` can be configured via its extensible API.
6868-6969----
7070-7171-#### Obtaining the API
7272-7373-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.
7474-7575-#### `import-tree`
7676-7777-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:
7878-7979-```
8080-default.nix
8181-modules/
8282- a.nix
8383- subdir/
8484- b.nix
8585-```
8686-8787-The following
8888-8989-```nix
9090-{lib, config, ...} {
9191- imports = [ (import-tree ./modules) ];
9292-}
9393-```
9494-9595-Is similar to
9696-9797-```nix
9898-{lib, config, ...} {
9999- imports = [
100100- {
101101- imports = [
102102- ./modules/a.nix
103103- ./modules/subdir/b.nix
104104- ];
105105- }
106106- ];
107107-}
108108-```
109109-110110-If given a deeply nested list of paths the list will be flattened and results concatenated. The following is valid usage:
111111-112112-```nix
113113-{lib, config, ...} {
114114- imports = [ (import-tree [./a [./b]]) ];
115115-}
116116-```
117117-118118-Other import-tree objects can also be given as arguments (or in lists) as if they were paths.
119119-120120-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.
121121-122122-#### Configurable behavior
123123-124124-`import-tree` objects with custom behavior can be obtained using a builder pattern. For example:
125125-126126-```nix
127127-lib.pipe import-tree [
128128- (i: i.map lib.traceVal)
129129- (i: i.filter (lib.hasInfix ".mod."))
130130- (i: i ./modules)
131131-]
132132-```
133133-134134-Or, in a simpler but less readable way:
135135-136136-```nix
137137-((import-tree.map lib.traceVal).filter (lib.hasInfix ".mod.")) ./modules
138138-```
139139-140140-##### 🌲 `import-tree.filter` and `import-tree.filterNot`
141141-142142-`filter` takes a predicate function `path -> bool`. Only files with suffix `.nix` are candidates.
143143-144144-```nix
145145-import-tree.filter (lib.hasInfix ".mod.") ./some-dir
146146-```
147147-148148-Multiple filters can be combined, results must match all of them.
149149-150150-##### 🌳 `import-tree.match` and `import-tree.matchNot`
151151-152152-`match` takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with `builtins.match`.
153153-154154-```nix
155155-import-tree.match ".*/[a-z]+@(foo|bar)\.nix" ./some-dir
156156-```
157157-158158-Multiple match filters can be added, results must match all of them.
159159-160160-##### 🌴 `import-tree.map`
161161-162162-`map` can be used to transform each path by providing a function.
163163-164164-```nix
165165-# generate a custom module from path
166166-import-tree.map (path: { imports = [ path ]; })
167167-```
168168-169169-Outside modules evaluation, you can transform paths into something else:
170170-171171-```nix
172172-lib.pipe import-tree [
173173- (i: i.map builtins.readFile)
174174- (i: i.withLib lib)
175175- (i: i.leafs ./dir)
176176-]
177177-# => list of contents of all files.
178178-```
179179-180180-##### 🌵 `import-tree.addPath`
181181-182182-`addPath` can be used to prepend paths to be filtered as a setup for import-tree.
183183-184184-```nix
185185-(import-tree.addPath ./vendor) ./modules
186186-import-tree [./vendor ./modules]
187187-```
188188-189189-##### 🎄 `import-tree.addAPI`
190190-191191-`addAPI` extends the current import-tree object with new methods.
192192-193193-```nix
194194-import-tree.addAPI {
195195- maximal = self: self.addPath ./modules;
196196- feature = self: infix: self.maximal.filter (lib.hasInfix infix);
197197- minimal = self: self.feature "minimal";
198198-}
199199-```
200200-201201-##### 🌿 `import-tree.withLib`
202202-203203-`withLib` is required prior to invocation of any of `.leafs` or `.pipeTo` when not used as part of a nix modules evaluation.
204204-205205-```nix
206206-import-tree.withLib pkgs.lib
207207-```
208208-209209-##### 🌱 `import-tree.pipeTo`
210210-211211-`pipeTo` takes a function that will receive the list of paths.
212212-213213-```nix
214214-import-tree.pipeTo lib.id # equivalent to `.leafs`
215215-```
216216-217217-##### 🍃 `import-tree.leafs`
4040+## Documentation
21841219219-`leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`.
220220-221221-```nix
222222-import-tree.leafs
223223-```
224224-225225-##### 🌲 `import-tree.new`
226226-227227-Returns a fresh import-tree with empty state.
228228-229229-##### 🌳 `import-tree.initFilter`
230230-231231-_Replaces_ the initial filter which defaults to: Include files with `.nix` suffix and not having `/_` infix.
232232-233233-```nix
234234-import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/" p)
235235-import-tree.initFilter (lib.hasSuffix ".md")
236236-```
237237-238238-##### 🌴 `import-tree.files`
239239-240240-A shorthand for `import-tree.leafs.result`. Returns a list of matching files.
241241-242242-```nix
243243-lib.pipe import-tree [
244244- (i: i.initFilter (lib.hasSuffix ".js"))
245245- (i: i.addPath ./out)
246246- (i: i.withLib lib)
247247- (i: i.files)
248248-]
249249-```
250250-251251-##### 🌵 `import-tree.result`
252252-253253-Exactly the same as calling the import-tree object with an empty list `[ ]`.
254254-255255-```nix
256256-(import-tree.addPath ./modules).result
257257-(import-tree.addPath ./modules) [ ]
258258-```
259259-260260----
261261-262262-## Why
263263-264264-Importing a tree of nix modules has some advantages:
265265-266266-### Dendritic Pattern: each file is a flake-parts module
267267-268268-[That pattern](https://github.com/mightyiam/dendritic) was the original inspiration for this library.
269269-See [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271),
270270-[@drupol's blog post](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) and
271271-[@vic's reply](https://discourse.nixos.org/t/how-do-you-structure-your-nixos-configs/65851/8)
272272-to learn about the Dendritic pattern advantages.
273273-274274-### Sharing pre-configured subtrees of modules
275275-276276-Since the import-tree API is _extensible_ and lets you add paths or
277277-filters at configuration time, configuration-library authors can
278278-provide custom import-tree instances with an API suited for their
279279-particular idioms.
280280-281281-@vic is using this on [Dendrix](https://github.com/vic/dendrix) for [community conventions](https://github.com/vic/dendrix/blob/main/dev/modules/community/_pipeline.nix) on tagging files.
282282-283283-This would allow us to have community-driven _sets_ of configurations,
284284-much like those popular for editors: spacemacs/lazy-vim distributions.
285285-286286-Imagine an editor distribution exposing the following flake output:
287287-288288-```nix
289289-# editor-distro's flakeModule
290290-{inputs, lib, ...}:
291291-let
292292- flake.lib.modules-tree = lib.pipe inputs.import-tree [
293293- (i: i.addPath ./modules)
294294- (i: i.addAPI { inherit on off exclusive; })
295295- (i: i.addAPI { ruby = self: self.on "ruby"; })
296296- (i: i.addAPI { python = self: self.on "python"; })
297297- (i: i.addAPI { old-school = self: self.off "copilot"; })
298298- (i: i.addAPI { vim-btw = self: self.exclusive "vim" "emacs"; })
299299- ];
300300-301301- on = self: flag: self.filter (lib.hasInfix "+${flag}");
302302- off = self: flag: self.filterNot (lib.hasInfix "+${flag}");
303303- exclusive = self: onFlag: offFlag: lib.pipe self [
304304- (self: on self onFlag)
305305- (self: off self offFlag)
306306- ];
307307-in
308308-{
309309- inherit flake;
310310-}
311311-```
312312-313313-Users of such distribution can do:
314314-315315-```nix
316316-# consumer flakeModule
317317-{inputs, lib, ...}: let
318318- ed-tree = inputs.editor-distro.lib.modules-tree;
319319-in {
320320- imports = [
321321- (ed-tree.vim-btw.old-school.on "rust")
322322- ];
323323-}
324324-```
325325-326326----
4242+📖 **[Full documentation](https://import-tree.oeiuwq.com)** — guides, API reference, and examples.
3274332844## Testing
32945330330-`import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing.
331331-332332-The test suite can be found in [`checkmate.nix`](checkmate.nix). To run it locally:
4646+`import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing:
3334733448```sh
33549nix flake check github:vic/checkmate --override-input target path:.
33650```
337337-338338-Run the following to format files:
339339-340340-```sh
341341-nix run github:vic/checkmate#fmt
342342-```
343343-344344-</details>
···11+# Starlight Starter Kit: Basics
22+33+[](https://starlight.astro.build)
44+55+```
66+pnpm create astro@latest -- --template starlight
77+```
88+99+> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
1010+1111+## 🚀 Project Structure
1212+1313+Inside of your Astro + Starlight project, you'll see the following folders and files:
1414+1515+```
1616+.
1717+├── public/
1818+├── src/
1919+│ ├── assets/
2020+│ ├── content/
2121+│ │ └── docs/
2222+│ └── content.config.ts
2323+├── astro.config.mjs
2424+├── package.json
2525+└── tsconfig.json
2626+```
2727+2828+Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
2929+3030+Images can be added to `src/assets/` and embedded in Markdown with a relative link.
3131+3232+Static assets, like favicons, can be placed in the `public/` directory.
3333+3434+## 🧞 Commands
3535+3636+All commands are run from the root of the project, from a terminal:
3737+3838+| Command | Action |
3939+| :------------------------ | :----------------------------------------------- |
4040+| `pnpm install` | Installs dependencies |
4141+| `pnpm dev` | Starts local dev server at `localhost:4321` |
4242+| `pnpm build` | Build your production site to `./dist/` |
4343+| `pnpm preview` | Preview your build locally, before deploying |
4444+| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
4545+| `pnpm astro -- --help` | Get help using the Astro CLI |
4646+4747+## 👀 Want to learn more?
4848+4949+Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
···11+---
22+import Default from '@astrojs/starlight/components/PageSidebar.astro';
33+import Ad from './Ad.astro';
44+---
55+66+<Default />
77+88+<Ad />
99+
+154
docs/src/components/Sidebar.astro
···11+---
22+import MobileMenuFooter from '@astrojs/starlight/components/MobileMenuFooter.astro';
33+import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
44+import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
55+66+const { sidebar, id } = Astro.locals.starlightRoute;
77+88+import { Icon } from '@astrojs/starlight/components';
99+1010+import TabbedContent from './tabs/TabbedContent.astro';
1111+import TabListItem from './tabs/TabListItem.astro';
1212+import TabPanel from './tabs/TabPanel.astro';
1313+1414+/** Get the icon for a group. Update the icon names in the array to change the icons associated with a group. */
1515+const getIcon = (index: number) =>
1616+ (['nix', 'open-book', 'rocket', 'puzzle', 'information', 'setting'] as const)[index];
1717+1818+/** Convert a group label to an `id` we can use to identify tab panels. */
1919+// The id is prefixed to avoid clashing with existing heading IDs on the page.
2020+const makeId = (label: string) => '__tab-' + label.toLowerCase().replaceAll(/\s+/g, '-');
2121+2222+/** Determine if an array of sidebar items contains the current page. */
2323+const isCurrent = (sidebar: SidebarEntry[]): boolean =>
2424+ sidebar
2525+ .map((entry) => (entry.type === 'link' ? entry.isCurrent : isCurrent(entry.entries)))
2626+ .some((entry) => entry === true);
2727+2828+---
2929+<SidebarPersister>
3030+ <TabbedContent class="tabbed-sidebar">
3131+ <Fragment slot="tab-list">
3232+ {
3333+ sidebar.map(({ label, entries }, index) => (
3434+ <TabListItem id={makeId(label)} initial={isCurrent(entries)} class="tab-item">
3535+ <Icon class="icon" name={getIcon(index)} /> {label}
3636+ </TabListItem>
3737+ ))
3838+ }
3939+ </Fragment>
4040+ {
4141+ sidebar.map(({ label, entries }) => (
4242+ <TabPanel id={makeId(label)} initial={isCurrent(entries)}>
4343+ <SidebarSublist sublist={entries} />
4444+ </TabPanel>
4545+ ))
4646+ }
4747+ </TabbedContent>
4848+</SidebarPersister>
4949+5050+<div class="md:sl-hidden">
5151+ <MobileMenuFooter />
5252+</div>
5353+5454+<style>
5555+ /** Add "EN" to the end of sidebar items with the `fallback` class. */
5656+ :global(.fallback)::after {
5757+ content: 'EN';
5858+ vertical-align: super;
5959+ font-size: 0.75em;
6060+ font-weight: 700;
6161+ }
6262+6363+ /** Align sponsors at sidebar bottom when there is room. */
6464+ .desktop-footer {
6565+ margin-top: auto;
6666+ }
6767+6868+ /** Always show the scrollbar gutter. */
6969+ :global(.sidebar-pane) {
7070+ overflow-y: scroll;
7171+ }
7272+7373+ /* Styles for the custom tab switcher. */
7474+ .tabbed-sidebar {
7575+ /* Layout variables */
7676+ --tab-switcher-border-width: 1px;
7777+ --tab-switcher-padding: calc(0.25rem - var(--tab-switcher-border-width));
7878+ --tab-item-border-radius: 0.5rem;
7979+ --tab-switcher-border-radius: calc(
8080+ var(--tab-item-border-radius) + var(--tab-switcher-padding) + var(--tab-switcher-border-width)
8181+ );
8282+8383+ /* Color variables */
8484+ --tab-switcher-border-color: var(--sl-color-hairline-light);
8585+ --tab-switcher-background-color: var(--sl-color-gray-7, var(--sl-color-gray-6));
8686+ --tab-switcher-text-color: var(--sl-color-gray-3);
8787+ --tab-switcher-text-color--active: var(--sl-color-white);
8888+ --tab-switcher-icon-color: var(--sl-color-gray-4);
8989+ --tab-switcher-icon-color--active: var(--sl-color-text-accent);
9090+ --tab-item-background-color--hover: var(--sl-color-gray-6);
9191+ --tab-item-background-color--active: var(--sl-color-black);
9292+ }
9393+ /* Dark theme variations */
9494+ :global([data-theme='dark']) .tabbed-sidebar {
9595+ --tab-switcher-text-color: var(--sl-color-gray-2);
9696+ --tab-switcher-icon-color: var(--sl-color-gray-3);
9797+ --tab-item-background-color--hover: var(--sl-color-gray-5);
9898+ }
9999+100100+ @media (min-width: 50rem) {
101101+ /* Dark theme variations with the desktop sidebar visible */
102102+ :global([data-theme='dark']) .tabbed-sidebar {
103103+ --tab-switcher-background-color: var(--sl-color-black);
104104+ --tab-item-background-color--hover: var(--sl-color-gray-6);
105105+ --tab-item-background-color--active: var(--sl-color-gray-6);
106106+ }
107107+ }
108108+109109+ .tabbed-sidebar.tab-list {
110110+ border: var(--tab-switcher-border-width) solid var(--tab-switcher-border-color);
111111+ border-radius: var(--tab-switcher-border-radius);
112112+ display: flex;
113113+ flex-direction: column;
114114+ gap: 0.25rem;
115115+ padding: var(--tab-switcher-padding);
116116+ background-color: var(--tab-switcher-background-color);
117117+ margin-bottom: 1.5rem;
118118+ }
119119+120120+ .tab-item :global(a) {
121121+ border: var(--tab-switcher-border-width) solid transparent;
122122+ border-radius: var(--tab-item-border-radius);
123123+ display: flex;
124124+ align-items: center;
125125+ gap: 0.5rem;
126126+ padding: calc(0.5rem - var(--tab-switcher-border-width));
127127+ background-clip: padding-box;
128128+ line-height: var(--sl-line-height-headings);
129129+ text-decoration: none;
130130+ color: var(--tab-switcher-text-color);
131131+ font-weight: 600;
132132+ }
133133+134134+ .tab-item :global(a:hover) {
135135+ color: var(--tab-switcher-text-color--active);
136136+ background-color: var(--tab-item-background-color--hover);
137137+ }
138138+ .tab-item :global(a[aria-selected='true']) {
139139+ border-color: var(--tab-switcher-border-color);
140140+ color: var(--tab-switcher-text-color--active);
141141+ background-color: var(--tab-item-background-color--active);
142142+ }
143143+144144+ .icon {
145145+ margin: 0.25rem;
146146+ color: var(--tab-switcher-icon-color);
147147+ }
148148+ .tab-item :global(a:hover) .icon {
149149+ color: inherit;
150150+ }
151151+ .tab-item :global(a[aria-selected='true']) .icon {
152152+ color: var(--tab-switcher-icon-color--active);
153153+ }
154154+</style>
···11+MIT License
22+33+Copyright (c) 2022 withastro
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+23
docs/src/components/tabs/TabListItem.astro
···11+---
22+import type { HTMLAttributes } from 'astro/types';
33+44+export interface Props {
55+ /** Unique ID for the tab panel this item links to. */
66+ id: string;
77+ /** Mark this item as visible when the page loads. */
88+ initial?: boolean;
99+ /** Additional class names to apply to the `<li>` */
1010+ class?: string;
1111+}
1212+1313+const { id, initial } = Astro.props;
1414+const linkAttributes: HTMLAttributes<'a'> = initial
1515+ ? { 'data-initial': 'true', 'aria-selected': 'true' }
1616+ : {};
1717+---
1818+1919+<li class:list={Astro.props.class}>
2020+ <a href={'#' + id} class="tab-link" {...linkAttributes}>
2121+ <slot />
2222+ </a>
2323+</li>
+44
docs/src/components/tabs/TabPanel.astro
···11+---
22+import type { HTMLAttributes } from 'astro/types';
33+44+export interface Props {
55+ id: string;
66+ initial?: boolean;
77+}
88+const { id, initial } = Astro.props;
99+const attributes: HTMLAttributes<'div'> = initial ? { 'data-initial': 'true' } : {};
1010+---
1111+1212+<div {id} {...attributes}>
1313+ <slot />
1414+</div>
1515+1616+<style>
1717+ /*
1818+ These styles avoid layout shift on page load.
1919+ We don’t want to hide all tabs forever in case JS never loads,
2020+ so instead we hide them initially with an animation that jumps
2121+ from hidden to visible after 10s. Usually JS will run before
2222+ 10s at which point we’ll rely on the `hidden` attribute and
2323+ toggle off the animation using `role='tabpanel'`. Both these
2424+ attributes are injected by the JS.
2525+ */
2626+2727+ div {
2828+ animation: tab-panel-appear 10s steps(2, jump-none) 1;
2929+ }
3030+3131+ div[data-initial],
3232+ div[role='tabpanel'] {
3333+ animation: none;
3434+ }
3535+3636+ @keyframes tab-panel-appear {
3737+ from {
3838+ /* `content-visibility` is set as well as `display` to work around a Firefox
3939+ bug where animations containing only `display: none` don’t play. */
4040+ display: none;
4141+ content-visibility: hidden;
4242+ }
4343+ }
4444+</style>
+173
docs/src/components/tabs/TabbedContent.astro
···11+---
22+import TabListItem from './TabListItem.astro';
33+44+export interface Props {
55+ /**
66+ * List of content for the tab list.
77+ *
88+ * To use more complex mark-up for the tab list, pass `<TabListItem>`s
99+ * inside a `<Fragment slot="tab-list">`.
1010+ */
1111+ tabs?: { label: string; id: string; initial?: boolean }[];
1212+ /** Enable default styles for the tab list and panels. */
1313+ styled?: boolean;
1414+ /** Additional class names to apply to `.tab-list` and `.panels`. */
1515+ class?: string;
1616+}
1717+1818+const { tabs, styled } = Astro.props as Props;
1919+---
2020+2121+<tabbed-content>
2222+ <ul class:list={['tab-list', Astro.props.class, { 'tab-list--styled': styled }]}>
2323+ <slot name="tab-list">
2424+ {
2525+ tabs?.map((tab) => (
2626+ <TabListItem id={tab.id} initial={tab.initial}>
2727+ {tab.label}
2828+ </TabListItem>
2929+ ))
3030+ }
3131+ </slot>
3232+ </ul>
3333+3434+ <div class:list={['panels', Astro.props.class, { 'panels--styled': styled }]}>
3535+ <slot />
3636+ </div>
3737+</tabbed-content>
3838+3939+<style>
4040+ .tab-list {
4141+ list-style: none;
4242+ padding: 0;
4343+ }
4444+ .tab-list--styled {
4545+ display: flex;
4646+ margin-top: -1px;
4747+ overflow-x: auto;
4848+ overflow-y: hidden;
4949+ }
5050+ @media (min-width: 72em) {
5151+ .tab-list--styled {
5252+ justify-content: space-between;
5353+ margin-top: 0;
5454+ padding: 1px;
5555+ }
5656+ }
5757+5858+ .panels--styled {
5959+ padding-left: 1px;
6060+ padding-right: 1px;
6161+ }
6262+</style>
6363+6464+<script>
6565+ class Tabs extends HTMLElement {
6666+ readonly id = Math.floor(Math.random() * 10e10).toString(32);
6767+ count = 0;
6868+ TabStore: Set<HTMLElement>[] = [];
6969+ PanelStore: Set<HTMLElement>[] = [];
7070+7171+ constructor() {
7272+ super();
7373+7474+ // Get relevant elements and collections
7575+ const panels = this.querySelectorAll<HTMLElement>('.panels > [id]');
7676+ const tablist = this.querySelector('.tab-list')!;
7777+ const tabs = tablist.querySelectorAll('a');
7878+7979+ // Add the tablist role to the first <ul> in the .tabbed container
8080+ tablist.setAttribute('role', 'tablist');
8181+8282+ let initialTab = 0;
8383+8484+ // Add semantics are remove user focusability for each tab
8585+ Array.prototype.forEach.call(tabs, (tab: HTMLElement, i: number) => {
8686+ tab.setAttribute('role', 'tab');
8787+ tab.setAttribute('id', this.id + 'tab' + this.count++);
8888+ tab.setAttribute('tabindex', '-1');
8989+ tab.parentElement?.setAttribute('role', 'presentation');
9090+ if (!this.TabStore[i]) this.TabStore.push(new Set());
9191+ this.TabStore[i].add(tab);
9292+ if ('initial' in tab.dataset && tab.dataset.initial !== 'false') initialTab = i;
9393+9494+ // Handle clicking of tabs for mouse users
9595+ const onClick = (e: MouseEvent) => {
9696+ e.preventDefault();
9797+ const currentTab = tablist.querySelector('[aria-selected]');
9898+ if (e.currentTarget !== currentTab) {
9999+ this.switchTab(e.currentTarget as HTMLElement, i);
100100+ }
101101+ };
102102+ tab.addEventListener('click', onClick);
103103+ tab.addEventListener('auxclick', onClick);
104104+105105+ // Handle keydown events for keyboard users
106106+ tab.addEventListener('keydown', (e) => {
107107+ // Get the index of the current tab in the tabs node list
108108+ const index: number = Array.prototype.indexOf.call(tabs, e.currentTarget);
109109+ // Work out which key the user is pressing and
110110+ // Calculate the new tab's index where appropriate
111111+ const dir =
112112+ e.key === 'ArrowLeft'
113113+ ? index - 1
114114+ : e.key === 'ArrowRight'
115115+ ? index + 1
116116+ : e.key === 'Home'
117117+ ? 0
118118+ : e.key === 'End'
119119+ ? tabs.length - 1
120120+ : null;
121121+ if (dir !== null) {
122122+ e.preventDefault();
123123+ if (tabs[dir]) this.switchTab(tabs[dir], dir);
124124+ }
125125+ });
126126+ });
127127+128128+ // Add tab panel semantics and hide them all
129129+ Array.prototype.forEach.call(panels, (panel: HTMLElement, i: number) => {
130130+ panel.setAttribute('role', 'tabpanel');
131131+ panel.setAttribute('tabindex', '-1');
132132+ panel.setAttribute('aria-labelledby', tabs[i].id);
133133+ panel.hidden = true;
134134+ if (!this.PanelStore[i]) this.PanelStore.push(new Set());
135135+ this.PanelStore[i].add(panel);
136136+ });
137137+138138+ // Activate and reveal the initial tab panel
139139+ tabs[initialTab].removeAttribute('tabindex');
140140+ tabs[initialTab].setAttribute('aria-selected', 'true');
141141+ panels[initialTab].hidden = false;
142142+ }
143143+144144+ // The tab switching function
145145+ switchTab(newTab: HTMLElement, index: number) {
146146+ this.TabStore.forEach((store) =>
147147+ store.forEach((oldTab) => {
148148+ oldTab.removeAttribute('aria-selected');
149149+ oldTab.setAttribute('tabindex', '-1');
150150+ })
151151+ );
152152+ this.TabStore[index].forEach((newTab) => {
153153+ // Make the active tab focusable by the user (Tab key)
154154+ newTab.removeAttribute('tabindex');
155155+ // Set the selected state
156156+ newTab.setAttribute('aria-selected', 'true');
157157+ });
158158+159159+ this.PanelStore.forEach((store) =>
160160+ store.forEach((oldPanel) => {
161161+ oldPanel.hidden = true;
162162+ })
163163+ );
164164+ this.PanelStore[index].forEach((newPanel) => {
165165+ newPanel.hidden = false;
166166+ });
167167+168168+ newTab.focus();
169169+ }
170170+ }
171171+172172+ customElements.define('tabbed-content', Tabs);
173173+</script>
···11+---
22+title: Community
33+description: Get help, share your work, and find real-world import-tree usage.
44+---
55+66+## Get Support
77+88+- [GitHub Issues](https://github.com/vic/import-tree/issues) — report bugs, request features
99+- [GitHub Discussions](https://github.com/vic/import-tree/discussions) — ask questions, share ideas
1010+1111+Everyone is welcome. Be kind and respectful.
1212+1313+## Real-World Usage
1414+1515+- [GitHub Code Search](https://github.com/search?q=language%3ANix+import-tree&type=code) — find projects using import-tree
1616+- [Dendrix Trees](https://vic.github.io/dendrix/Dendrix-Trees.html) — community index of dendritic setups
1717+1818+## Ecosystem
1919+2020+- [Den](https://github.com/vic/den) — context-aware dendritic Nix framework
2121+- [flake-aspects](https://github.com/vic/flake-aspects) — aspect composition library
2222+- [denful](https://github.com/vic/denful) — community aspect distribution
2323+- [Dendrix](https://dendrix.oeiuwq.com/) — index of dendritic aspects
2424+- [Dendritic Design](https://github.com/mightyiam/dendritic) — the pattern that inspired this ecosystem
+36
docs/src/content/docs/contributing.md
···11+---
22+title: Contributing
33+description: How to report bugs, run tests, and contribute to import-tree.
44+---
55+66+All contributions are welcome. PRs are checked by CI.
77+88+## Run Tests
99+1010+`import-tree` uses [checkmate](https://github.com/vic/checkmate) for testing:
1111+1212+```sh
1313+nix flake check github:vic/checkmate --override-input target path:.
1414+```
1515+1616+## Format Code
1717+1818+```sh
1919+nix run github:vic/checkmate#fmt
2020+```
2121+2222+## Bug Reports
2323+2424+Open an [issue](https://github.com/vic/import-tree/issues) with a minimal reproduction.
2525+2626+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/`.
2727+2828+## Documentation
2929+3030+The documentation site lives under `./docs/`. It uses [Starlight](https://starlight.astro.build/).
3131+3232+To run locally:
3333+3434+```sh
3535+cd docs && pnpm install && pnpm run dev
3636+```
···11+---
22+title: Quick Start
33+description: Get up and running with import-tree in minutes.
44+---
55+66+import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
77+88+## Installation
99+1010+<Tabs>
1111+<TabItem label="Flake">
1212+1313+Add `import-tree` to your flake inputs:
1414+1515+```nix
1616+{
1717+ inputs.import-tree.url = "github:vic/import-tree";
1818+}
1919+```
2020+2121+</TabItem>
2222+<TabItem label="No Flake">
2323+2424+Import directly from a pinned source:
2525+2626+```nix
2727+let
2828+ import-tree = import (builtins.fetchTarball {
2929+ url = "https://github.com/vic/import-tree/archive/main.tar.gz";
3030+ });
3131+in
3232+# use import-tree
3333+```
3434+3535+Or from a local checkout:
3636+3737+```nix
3838+let import-tree = import ./path-to/import-tree;
3939+in
4040+# use import-tree
4141+```
4242+4343+</TabItem>
4444+</Tabs>
4545+4646+## Basic Usage
4747+4848+### With flake-parts
4949+5050+The most common pattern — import all modules from a directory as flake-parts modules:
5151+5252+```nix
5353+{
5454+ inputs.import-tree.url = "github:vic/import-tree";
5555+ inputs.flake-parts.url = "github:hercules-ci/flake-parts";
5656+5757+ outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; }
5858+ (inputs.import-tree ./modules);
5959+}
6060+```
6161+6262+### With NixOS / nix-darwin / home-manager
6363+6464+Use `import-tree` anywhere you have an `imports` list:
6565+6666+```nix
6767+{ config, ... }: {
6868+ imports = [ (import-tree ./modules) ];
6969+}
7070+```
7171+7272+This recursively discovers all `.nix` files under `./modules` and imports them.
7373+7474+### What Gets Imported?
7575+7676+Given this tree:
7777+7878+```
7979+modules/
8080+ networking.nix
8181+ desktop/
8282+ sway.nix
8383+ _private/
8484+ helper.nix
8585+```
8686+8787+`import-tree ./modules` imports `networking.nix` and `desktop/sway.nix`. The `_private/` directory is skipped because paths with `/_` are ignored by default.
8888+8989+<Aside type="tip">
9090+Use `/_` prefixed directories for helper files, library code, or anything you don't want auto-imported.
9191+</Aside>
9292+9393+## Multiple Directories
9494+9595+Pass a list to import from several directories:
9696+9797+```nix
9898+{ imports = [ (import-tree [ ./modules ./extra-modules ]) ]; }
9999+```
100100+101101+Lists can be arbitrarily nested — they are flattened automatically.
102102+103103+## Next Steps
104104+105105+- [Filtering](/guides/filtering/) — select specific files by predicate or regex
106106+- [API Reference](/reference/api/) — complete method documentation
107107+- [Dendritic Pattern](/guides/dendritic/) — learn the file-per-module approach
+90
docs/src/content/docs/guides/custom-api.mdx
···11+---
22+title: Custom API
33+description: Extend import-tree with your own methods using .addAPI.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## addAPI
99+1010+`.addAPI` extends the import-tree object with new named methods. Each method receives the current import-tree instance (`self`) and can call any existing method on it.
1111+1212+```nix
1313+import-tree.addAPI {
1414+ helloOption = self: self.addPath ./modules/hello-option;
1515+ feature = self: infix: self.filter (lib.hasInfix infix);
1616+ minimal = self: self.feature "minimal";
1717+}
1818+```
1919+2020+After calling `.addAPI`, the new methods are available directly on the import-tree object:
2121+2222+```nix
2323+extended.helloOption.files
2424+extended.feature "networking" ./modules
2525+extended.minimal ./src
2626+```
2727+2828+## Preserving Previous Extensions
2929+3030+Calling `.addAPI` multiple times is cumulative — previous extensions are preserved:
3131+3232+```nix
3333+let
3434+ first = import-tree.addAPI { foo = self: self.addPath ./foo; };
3535+ second = first.addAPI { bar = self: self.addPath ./bar; };
3636+in
3737+second.foo.files # still works
3838+```
3939+4040+## Late Binding
4141+4242+API methods are late-bound. You can reference methods that don't exist yet — they resolve when actually called:
4343+4444+```nix
4545+let
4646+ first = import-tree.addAPI { result = self: self.late; };
4747+ extended = first.addAPI { late = _self: "hello"; };
4848+in
4949+extended.result # => "hello"
5050+```
5151+5252+This enables building APIs incrementally across multiple `.addAPI` calls.
5353+5454+## Real-World Example: Module Distribution
5555+5656+<Aside type="tip">
5757+This pattern enables community-driven module distributions — think editor plugin sets, server presets, or infrastructure templates.
5858+</Aside>
5959+6060+A library author can ship a pre-configured import-tree with domain-specific methods:
6161+6262+```nix
6363+# editor-distro flake module
6464+{ inputs, lib, ... }:
6565+let
6666+ on = self: flag: self.filter (lib.hasInfix "+${flag}");
6767+ off = self: flag: self.filterNot (lib.hasInfix "+${flag}");
6868+ exclusive = self: onFlag: offFlag: (on self onFlag) |> (s: off s offFlag);
6969+in {
7070+ flake.lib.modules-tree = lib.pipe inputs.import-tree [
7171+ (i: i.addPath ./modules)
7272+ (i: i.addAPI { inherit on off exclusive; })
7373+ (i: i.addAPI { ruby = self: self.on "ruby"; })
7474+ (i: i.addAPI { python = self: self.on "python"; })
7575+ (i: i.addAPI { old-school = self: self.off "copilot"; })
7676+ (i: i.addAPI { vim-btw = self: self.exclusive "vim" "emacs"; })
7777+ ];
7878+}
7979+```
8080+8181+Consumers pick exactly the features they want:
8282+8383+```nix
8484+# consumer flake module
8585+{ inputs, ... }:
8686+let ed = inputs.editor-distro.lib.modules-tree;
8787+in {
8888+ imports = [ (ed.vim-btw.old-school.on "rust") ];
8989+}
9090+```
+94
docs/src/content/docs/guides/dendritic.mdx
···11+---
22+title: Dendritic Pattern
33+description: How import-tree enables the Dendritic pattern for Nix configurations.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## What is the Dendritic Pattern?
99+1010+The [Dendritic pattern](https://github.com/mightyiam/dendritic) is a convention where **each file is a self-contained Nix module**. Rather than large monolithic files, your configuration becomes a directory tree where each concern lives in its own file.
1111+1212+```
1313+modules/
1414+ networking.nix # network config
1515+ openssh.nix # SSH server
1616+ desktop/
1717+ sway.nix # window manager
1818+ waybar.nix # status bar
1919+ notifications.nix # notification daemon
2020+ users/
2121+ alice.nix # user account
2222+```
2323+2424+Each file is a standard Nix module:
2525+2626+```nix
2727+# modules/openssh.nix
2828+{ ... }: {
2929+ services.openssh.enable = true;
3030+ services.openssh.settings.PasswordAuthentication = false;
3131+}
3232+```
3333+3434+## Why Dendritic?
3535+3636+- **Locality** — each concern in its own file, easy to find and modify
3737+- **Composability** — add or remove features by adding or removing files
3838+- **No boilerplate** — `import-tree` handles the wiring
3939+- **Git-friendly** — file additions don't cause merge conflicts in import lists
4040+- **Discoverable** — directory structure documents the system
4141+4242+## Using with flake-parts
4343+4444+With flake-parts, each file is a flake-parts module:
4545+4646+```nix
4747+# flake.nix
4848+{
4949+ inputs.import-tree.url = "github:vic/import-tree";
5050+ inputs.flake-parts.url = "github:hercules-ci/flake-parts";
5151+5252+ outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; }
5353+ (inputs.import-tree ./modules);
5454+}
5555+```
5656+5757+```nix
5858+# modules/dev-shell.nix
5959+{ inputs, ... }: {
6060+ perSystem = { pkgs, ... }: {
6161+ devShells.default = pkgs.mkShell {
6262+ packages = [ pkgs.nil pkgs.nixfmt-rfc-style ];
6363+ };
6464+ };
6565+}
6666+```
6767+6868+## The `/_` Convention
6969+7070+<Aside type="tip">
7171+Use underscore-prefixed directories for helper code that shouldn't be auto-imported.
7272+</Aside>
7373+7474+Files under `/_` prefixed paths are ignored by default. This gives you a place for shared utilities:
7575+7676+```
7777+modules/
7878+ feature.nix
7979+ _lib/
8080+ helpers.nix # not auto-imported
8181+```
8282+8383+```nix
8484+# modules/feature.nix
8585+let helpers = import ./_lib/helpers.nix;
8686+in { ... }: { /* use helpers */ }
8787+```
8888+8989+## Further Reading
9090+9191+- [Dendritic Design](https://github.com/mightyiam/dendritic) — the pattern specification
9292+- [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271) — introducing the pattern
9393+- [@drupol's blog](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) — real-world adoption
9494+- [Dendrix](https://dendrix.oeiuwq.com/) — index of dendritic aspects
+92
docs/src/content/docs/guides/filtering.mdx
···11+---
22+title: Filtering Files
33+description: Control which files import-tree discovers using predicates and regular expressions.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Default Behavior
99+1010+By default, `import-tree` includes files that:
1111+1212+1. Have the `.nix` suffix
1313+2. Do **not** have `/_` in their path (underscore-prefixed directories are ignored)
1414+1515+You can narrow this down with filters, or replace the defaults entirely.
1616+1717+## filter / filterNot
1818+1919+`filter` takes a predicate function `string -> bool` applied to each file's path:
2020+2121+```nix
2222+# Only import files containing ".mod." in their name
2323+import-tree.filter (lib.hasInfix ".mod.") ./modules
2424+```
2525+2626+`filterNot` is the inverse — exclude files matching the predicate:
2727+2828+```nix
2929+# Skip any file with "experimental" in the path
3030+import-tree.filterNot (lib.hasInfix "experimental") ./modules
3131+```
3232+3333+### Composing Filters
3434+3535+Multiple filters combine with logical AND — a file must pass **all** of them:
3636+3737+```nix
3838+lib.pipe import-tree [
3939+ (i: i.filter (lib.hasInfix "/desktop/"))
4040+ (i: i.filter (lib.hasSuffix "bar.nix"))
4141+ (i: i ./modules)
4242+]
4343+```
4444+4545+This selects only files under a `desktop/` directory whose name ends in `bar.nix`.
4646+4747+## match / matchNot
4848+4949+`match` takes a regular expression. The regex is tested against the **full path** using `builtins.match`:
5050+5151+```nix
5252+# Only files named like "word_word.nix"
5353+import-tree.match ".*/[a-z]+_[a-z]+\.nix" ./modules
5454+```
5555+5656+`matchNot` excludes files matching the regex:
5757+5858+```nix
5959+# Skip files with numeric names
6060+import-tree.matchNot ".*/[0-9]+\.nix" ./modules
6161+```
6262+6363+<Aside type="caution">
6464+`builtins.match` tests the **entire** string. Your regex must match the full path, not just a substring. Use `.*` to match surrounding parts.
6565+</Aside>
6666+6767+### Mixing filter and match
6868+6969+All filter types compose together:
7070+7171+```nix
7272+(import-tree.match ".*_.*\.nix").filter (lib.hasInfix "/src/") ./tree
7373+```
7474+7575+This finds files matching the regex **and** containing `/src/` in their path.
7676+7777+## initFilter — Replacing Defaults
7878+7979+`initFilter` **replaces** the built-in `.nix` + no-`/_` filter entirely. Use it to discover non-Nix files or change the ignore convention:
8080+8181+```nix
8282+# Find .txt files instead of .nix files
8383+import-tree.initFilter (lib.hasSuffix ".txt") ./dir
8484+8585+# Find .nix files but use /ignored/ instead of /_
8686+import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/ignored/" p)
8787+```
8888+8989+<Aside>
9090+`initFilter` also applies to non-path items (like attrsets) passed directly in import lists.
9191+This lets you filter out module attrsets programmatically.
9292+</Aside>
+50
docs/src/content/docs/guides/mapping.mdx
···11+---
22+title: Transforming Paths
33+description: Use .map to transform discovered file paths before they are imported.
44+---
55+66+## map
77+88+`.map` takes a function that transforms each discovered path. Transformations are applied **after** filtering.
99+1010+### Wrapping in Custom Modules
1111+1212+```nix
1313+import-tree.map (path: { imports = [ path ]; }) ./modules
1414+```
1515+1616+### Tracing Discovered Files
1717+1818+```nix
1919+import-tree.map lib.traceVal ./modules
2020+```
2121+2222+This prints each discovered path during evaluation — useful for debugging.
2323+2424+### Composing Maps
2525+2626+Multiple `.map` calls compose left-to-right (the first map runs first):
2727+2828+```nix
2929+lib.pipe import-tree [
3030+ (i: i.map import) # import each .nix file
3131+ (i: i.map builtins.stringLength) # get the length of each result
3232+ (i: i.withLib lib)
3333+ (i: i.leafs ./dir)
3434+]
3535+```
3636+3737+### Using map Outside Module Evaluation
3838+3939+When used with `.leafs` or `.pipeTo`, `.map` transforms paths into arbitrary values — not just modules:
4040+4141+```nix
4242+# Read all .md files under a directory
4343+lib.pipe import-tree [
4444+ (i: i.initFilter (lib.hasSuffix ".md"))
4545+ (i: i.map builtins.readFile)
4646+ (i: i.withLib lib)
4747+ (i: i.leafs ./docs)
4848+]
4949+# => [ "# Title\n..." "# Other\n..." ]
5050+```
+75
docs/src/content/docs/guides/outside-modules.mdx
···11+---
22+title: Outside Module Evaluation
33+description: Use import-tree to list files programmatically, without importing them as modules.
44+---
55+66+## Using import-tree as a File Lister
77+88+`import-tree` doesn't have to produce module imports. You can use it to get a plain list of files:
99+1010+```nix
1111+(import-tree.withLib pkgs.lib).leafs ./modules
1212+# => [ /path/to/modules/a.nix /path/to/modules/b.nix ]
1313+```
1414+1515+### withLib
1616+1717+Outside module evaluation, `import-tree` needs access to `lib` (specifically `lib.filesystem.listFilesRecursive`). Call `.withLib` before `.leafs` or `.pipeTo`:
1818+1919+```nix
2020+import-tree.withLib lib
2121+```
2222+2323+Omitting `.withLib` when calling `.leafs` produces an error:
2424+`"You need to call withLib before trying to read the tree."`
2525+2626+### leafs
2727+2828+`.leafs` returns a configured import-tree that, when given a path, produces a flat list of discovered files:
2929+3030+```nix
3131+(import-tree.withLib lib).leafs ./src
3232+# => [ ./src/main.nix ./src/utils.nix ]
3333+```
3434+3535+### files
3636+3737+`.files` is a shortcut for `.leafs.result` — returns the list directly when paths have already been added via `.addPath`:
3838+3939+```nix
4040+lib.pipe import-tree [
4141+ (i: i.addPath ./modules)
4242+ (i: i.withLib lib)
4343+ (i: i.files)
4444+]
4545+```
4646+4747+### pipeTo
4848+4949+`.pipeTo` takes a function that receives the list of discovered paths, letting you process the results:
5050+5151+```nix
5252+(import-tree.withLib lib).pipeTo builtins.length ./modules
5353+# => 5 (number of .nix files)
5454+```
5555+5656+Combine with `.map` for powerful pipelines:
5757+5858+```nix
5959+lib.pipe import-tree [
6060+ (i: i.map import)
6161+ (i: i.pipeTo lib.length)
6262+ (i: i.withLib lib)
6363+ (i: i ./modules)
6464+]
6565+```
6666+6767+### result
6868+6969+`.result` evaluates the import-tree with an empty path list — useful when paths are already configured via `.addPath`:
7070+7171+```nix
7272+(import-tree.addPath ./modules).result
7373+# equivalent to:
7474+(import-tree.addPath ./modules) []
7575+```
+60
docs/src/content/docs/index.mdx
···11+---
22+title: import-tree
33+description: Recursively import Nix modules from a directory tree, with a simple, extensible API.
44+template: splash
55+hero:
66+ tagline: Recursively import Nix modules from a directory tree
77+ image:
88+ html: |
99+ <span style="font-size: 8rem">🌲</span>
1010+ actions:
1111+ - text: Getting Started
1212+ link: /getting-started/quick-start/
1313+ icon: right-arrow
1414+ - text: API Reference
1515+ link: /reference/api/
1616+ variant: minimal
1717+ - text: Why import-tree?
1818+ link: /motivation/
1919+ variant: minimal
2020+ - text: Source Code
2121+ link: https://github.com/vic/import-tree
2222+ icon: github
2323+ variant: secondary
2424+---
2525+2626+import { Card, CardGrid } from '@astrojs/starlight/components';
2727+2828+`import-tree` recursively discovers and imports Nix files from a directory tree. It works with **NixOS**, **nix-darwin**, **home-manager**, **flake-parts**, **NixVim**, and any Nix module system.
2929+3030+## At a Glance
3131+3232+```nix
3333+# In your flake.nix — import every .nix file under ./modules
3434+{
3535+ inputs.import-tree.url = "github:vic/import-tree";
3636+ inputs.flake-parts.url = "github:hercules-ci/flake-parts";
3737+3838+ outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; }
3939+ (inputs.import-tree ./modules);
4040+}
4141+```
4242+4343+Paths containing `/_` are ignored by default — use underscored directories for private helpers.
4444+4545+## Highlights
4646+4747+<CardGrid>
4848+ <Card title="Zero Dependencies" icon="rocket">
4949+ A single `default.nix`. Usable as a flake or plain import. No extra inputs needed.
5050+ </Card>
5151+ <Card title="Sensible Defaults" icon="approve-check-circle">
5252+ Recursively finds `.nix` files, skips `/_` prefixed paths. Works out of the box.
5353+ </Card>
5454+ <Card title="Builder API" icon="setting">
5555+ Chain `.filter`, `.match`, `.map`, `.addPath`, and more to customize discovery.
5656+ </Card>
5757+ <Card title="Extensible" icon="puzzle">
5858+ Add your own API methods with `.addAPI` to create domain-specific import-tree instances.
5959+ </Card>
6060+</CardGrid>
+79
docs/src/content/docs/motivation.mdx
···11+---
22+title: Why import-tree?
33+description: The motivation behind import-tree and the problems it solves.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## The Problem
99+1010+As Nix configurations grow, the `imports` list becomes a maintenance burden:
1111+1212+```nix
1313+# This doesn't scale.
1414+{
1515+ imports = [
1616+ ./modules/networking.nix
1717+ ./modules/desktop/sway.nix
1818+ ./modules/desktop/waybar.nix
1919+ ./modules/services/docker.nix
2020+ ./modules/services/ssh.nix
2121+ ./modules/users/alice.nix
2222+ # ... dozens more
2323+ ];
2424+}
2525+```
2626+2727+Every new file requires updating the import list. Forget one and your config silently ignores it. Reorganize your directory and you must update every path by hand.
2828+2929+## The Solution
3030+3131+```nix
3232+{
3333+ imports = [ (import-tree ./modules) ];
3434+}
3535+```
3636+3737+Add a file to `./modules/` and it is automatically discovered. Reorganize freely. Use `/_` prefixed directories for helpers that should not be imported.
3838+3939+## Dendritic Pattern
4040+4141+The [Dendritic pattern](https://github.com/mightyiam/dendritic) — where each file is a self-contained module — was the original inspiration for `import-tree`.
4242+4343+<Aside type="tip">
4444+See [@mightyiam's post](https://discourse.nixos.org/t/pattern-each-file-is-a-flake-parts-module/61271),
4545+[@drupol's blog](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/), and
4646+[@vic's reply](https://discourse.nixos.org/t/how-do-you-structure-your-nixos-configs/65851/8)
4747+for more on Dendritic setups.
4848+</Aside>
4949+5050+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.
5151+5252+## Beyond Loading Files
5353+5454+`import-tree` is not just a file loader. Its builder API lets you:
5555+5656+- **Filter** which files are selected — by predicate, regex, or both.
5757+- **Transform** discovered paths — wrap them in custom modules, read their contents, or anything else.
5858+- **Compose** multiple directory trees with shared filters.
5959+- **Extend** with domain-specific APIs — let library authors ship curated import-tree instances.
6060+6161+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:
6262+6363+```nix
6464+# A library could expose:
6565+lib.modules-tree = import-tree.addAPI {
6666+ gaming = self: self.filter (lib.hasInfix "+gaming");
6767+ minimal = self: self.filterNot (lib.hasInfix "+heavy");
6868+};
6969+7070+# Consumers use it like:
7171+{ imports = [ lib.modules-tree.gaming.minimal ]; }
7272+```
7373+7474+## Design Goals
7575+7676+- **Zero dependencies** — a single `default.nix`, no extra flake inputs
7777+- **Works everywhere** — flakes, non-flakes, any module system
7878+- **Composable** — builder pattern with filter/map/extend chains
7979+- **Predictable** — sensible defaults, clear ignore rules, no magic
+63
docs/src/content/docs/overview.mdx
···11+---
22+title: Overview
33+description: What import-tree does and how it fits into your Nix setup.
44+---
55+66+import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components';
77+88+## What is import-tree?
99+1010+`import-tree` is a Nix library that recursively discovers files in a directory tree and produces a list of Nix module imports. Instead of manually listing every module in your `imports = [ ... ]`, you point `import-tree` at a directory and it does the rest.
1111+1212+It is a **dependency-free**, **single-file** library that works as a flake or a plain `import`.
1313+1414+## How It Works
1515+1616+Given a directory tree:
1717+1818+```
1919+modules/
2020+ networking.nix
2121+ desktop/
2222+ sway.nix
2323+ waybar.nix
2424+ _helpers/
2525+ utils.nix
2626+```
2727+2828+Calling `import-tree ./modules` produces a module whose `imports` contain:
2929+3030+- `./modules/networking.nix`
3131+- `./modules/desktop/sway.nix`
3232+- `./modules/desktop/waybar.nix`
3333+3434+The `_helpers/` directory is skipped because paths containing `/_` are ignored by default.
3535+3636+## Key Features
3737+3838+<CardGrid>
3939+ <Card title="Universal" icon="open-book">
4040+ Works with NixOS, nix-darwin, home-manager, flake-parts, NixVim — any Nix module system.
4141+ <LinkButton href="/getting-started/quick-start" variant="minimal" icon="right-arrow">Quick Start</LinkButton>
4242+ </Card>
4343+ <Card title="Composable Filters" icon="list-format">
4444+ Chain `.filter`, `.filterNot`, `.match`, `.matchNot` to select exactly the files you need.
4545+ <LinkButton href="/guides/filtering" variant="minimal" icon="right-arrow">Filtering Guide</LinkButton>
4646+ </Card>
4747+ <Card title="Transformations" icon="random">
4848+ Use `.map` to transform discovered paths — wrap in modules, read contents, or anything else.
4949+ <LinkButton href="/guides/mapping" variant="minimal" icon="right-arrow">Mapping Guide</LinkButton>
5050+ </Card>
5151+ <Card title="Extensible API" icon="puzzle">
5252+ Build domain-specific APIs with `.addAPI` — create named presets, feature flags, module sets.
5353+ <LinkButton href="/guides/custom-api" variant="minimal" icon="right-arrow">Custom API Guide</LinkButton>
5454+ </Card>
5555+ <Card title="Non-Module Usage" icon="seti:folder">
5656+ Use `.leafs` and `.files` outside module evaluation to get raw file lists for any purpose.
5757+ <LinkButton href="/guides/outside-modules" variant="minimal" icon="right-arrow">Outside Modules</LinkButton>
5858+ </Card>
5959+ <Card title="Dendritic Pattern" icon="star">
6060+ Purpose-built for the Dendritic pattern where every file is a self-contained module.
6161+ <LinkButton href="/guides/dendritic" variant="minimal" icon="right-arrow">Dendritic Pattern</LinkButton>
6262+ </Card>
6363+</CardGrid>
+200
docs/src/content/docs/reference/api.mdx
···11+---
22+title: API Reference
33+description: Complete reference for every import-tree method.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Obtaining import-tree
99+1010+**As a flake input:**
1111+```nix
1212+inputs.import-tree.url = "github:vic/import-tree";
1313+# Then use: inputs.import-tree
1414+```
1515+1616+**As a plain import:**
1717+```nix
1818+let import-tree = import ./path-to/import-tree;
1919+```
2020+2121+The resulting value is a callable attrset — the primary `import-tree` object.
2222+2323+---
2424+2525+## Core: Calling import-tree
2626+2727+### `import-tree <path | [paths]>`
2828+2929+Takes a path or a (nested) list of paths. Returns a Nix module with `imports` set to all discovered files.
3030+3131+```nix
3232+import-tree ./modules
3333+import-tree [ ./modules ./extra ]
3434+import-tree [ ./a [ ./b ] ] # nested lists are flattened
3535+```
3636+3737+Other import-tree objects can appear in the list as if they were paths.
3838+3939+<Aside>
4040+When the argument is an attrset with an `options` attribute, import-tree assumes it is being evaluated as a module. This lets a pre-configured import-tree object appear directly in `imports`.
4141+</Aside>
4242+4343+Anything with an `outPath` attribute (like flake inputs) is treated as a path:
4444+4545+```nix
4646+import-tree [ { outPath = ./modules; } ]
4747+```
4848+4949+Non-path values (like attrsets) are passed through the filter and included if they pass.
5050+5151+---
5252+5353+## Filtering
5454+5555+### `.filter <fn>`
5656+5757+`fn : string -> bool` — only include paths where `fn` returns `true`.
5858+5959+```nix
6060+import-tree.filter (lib.hasInfix ".mod.") ./modules
6161+```
6262+6363+Multiple `.filter` calls compose with AND.
6464+6565+### `.filterNot <fn>`
6666+6767+Inverse of `.filter` — exclude paths where `fn` returns `true`.
6868+6969+```nix
7070+import-tree.filterNot (lib.hasInfix "experimental") ./modules
7171+```
7272+7373+### `.match <regex>`
7474+7575+Include only paths matching the regex. Uses `builtins.match` (tests full string).
7676+7777+```nix
7878+import-tree.match ".*/[a-z]+_[a-z]+\.nix" ./modules
7979+```
8080+8181+Multiple `.match` calls compose with AND.
8282+8383+### `.matchNot <regex>`
8484+8585+Exclude paths matching the regex.
8686+8787+```nix
8888+import-tree.matchNot ".*/test_.*\.nix" ./modules
8989+```
9090+9191+### `.initFilter <fn>`
9292+9393+**Replaces** the default filter (`.nix` suffix, no `/_` infix). Use for non-Nix files or custom ignore conventions.
9494+9595+```nix
9696+import-tree.initFilter (lib.hasSuffix ".md") ./docs
9797+import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/skip/" p)
9898+```
9999+100100+Also applies to non-path items in import lists.
101101+102102+---
103103+104104+## Transformation
105105+106106+### `.map <fn>`
107107+108108+`fn : path -> a` — transform each discovered path.
109109+110110+```nix
111111+import-tree.map lib.traceVal ./modules # trace each path
112112+import-tree.map (p: { imports = [ p ]; }) # wrap in module
113113+import-tree.map import # actually import
114114+```
115115+116116+Multiple `.map` calls compose (first map runs first).
117117+118118+---
119119+120120+## Path Accumulation
121121+122122+### `.addPath <path>`
123123+124124+Prepend a path to the internal path list. Can be called multiple times:
125125+126126+```nix
127127+(import-tree.addPath ./vendor).addPath ./modules
128128+# discovers files in both directories
129129+```
130130+131131+---
132132+133133+## Extension
134134+135135+### `.addAPI <attrset>`
136136+137137+Extend the import-tree object with new methods. Each value is a function receiving `self` (the current import-tree):
138138+139139+```nix
140140+import-tree.addAPI {
141141+ maximal = self: self.addPath ./all-modules;
142142+ feature = self: name: self.filter (lib.hasInfix name);
143143+}
144144+```
145145+146146+Methods are late-bound — you can reference methods added in later `.addAPI` calls.
147147+148148+---
149149+150150+## Output
151151+152152+### `.withLib <lib>`
153153+154154+Required before `.leafs` or `.pipeTo` when used outside module evaluation. Provides `lib.filesystem.listFilesRecursive`.
155155+156156+```nix
157157+import-tree.withLib pkgs.lib
158158+```
159159+160160+### `.leafs`
161161+162162+Returns a configured import-tree that produces file lists instead of modules:
163163+164164+```nix
165165+(import-tree.withLib lib).leafs ./modules
166166+# => [ ./modules/a.nix ./modules/b.nix ]
167167+```
168168+169169+### `.files`
170170+171171+Shorthand for `.leafs.result`:
172172+173173+```nix
174174+(import-tree.addPath ./modules).withLib lib |>.files
175175+```
176176+177177+### `.pipeTo <fn>`
178178+179179+Like `.leafs` but pipes the result list through `fn`:
180180+181181+```nix
182182+(import-tree.withLib lib).pipeTo builtins.length ./modules
183183+# => 3
184184+```
185185+186186+### `.result`
187187+188188+Evaluate with an empty path list. Equivalent to calling with `[]`:
189189+190190+```nix
191191+(import-tree.addPath ./modules).result
192192+```
193193+194194+### `.new`
195195+196196+Returns a fresh import-tree with empty state — no paths, filters, maps, or API extensions.
197197+198198+```nix
199199+configured-tree.new # back to a clean slate
200200+```
···11+---
22+title: Sponsor
33+description: Support import-tree development.
44+---
55+66+`import-tree` and [vic](https://bsky.app/profile/oeiuwq.bsky.social)'s [dendritic libs](https://vic.github.io/dendrix/Dendritic-Ecosystem.html#vics-dendritic-libraries) are made with love.
77+88+If you find import-tree useful, consider [sponsoring](https://github.com/sponsors/vic).