···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: Aspects & Resolution
33+description: How aspect submodules are defined and resolved into Nix modules.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Aspect structure
99+1010+Each aspect is a Nix submodule ([`nix/types.nix`](https://github.com/vic/flake-aspects/blob/main/nix/types.nix)) with:
1111+1212+| Attribute | Type | Purpose |
1313+|---|---|---|
1414+| `name` | `str` | Auto-set to the attribute name |
1515+| `description` | `str` | Human-readable description |
1616+| `<class>` | `deferredModule` | Freeform: any key not in this table is a class-specific config |
1717+| `includes` | `listOf providerType` | Dependencies on other aspects |
1818+| `provides` / `_` | `submodule` | Nested sub-aspects (see [Providers](/concepts/providers/)) |
1919+| `__functor` | `function` | Override resolution behavior (see [Functor guide](/guides/functor/)) |
2020+| `resolve` | internal | `{ class, aspect-chain? } → module` |
2121+| `modules` | internal | `<class> → resolved-module` (lazy attrset) |
2222+2323+An aspect is simultaneously an attribute set (with class configs) and callable (via `__functor`).
2424+2525+## Defining aspects
2626+2727+```nix
2828+flake.aspects = {
2929+ my-desktop = {
3030+ nixos = { services.xserver.enable = true; };
3131+ darwin = { services.yabai.enable = true; };
3232+ };
3333+};
3434+```
3535+3636+Each key under the aspect that is not a reserved attribute (`name`, `description`, `includes`, `provides`, `_`, `__functor`) is treated as a **class name** with its value being a deferred Nix module.
3737+3838+## Resolution
3939+4040+Source: [`nix/resolve.nix`](https://github.com/vic/flake-aspects/blob/main/nix/resolve.nix)
4141+4242+```
4343+resolve : class → aspect-chain → provided → { imports }
4444+```
4545+4646+Given a `class` (e.g. `"nixos"`) and the aspect config:
4747+4848+1. Extract `provided.${class}` (the class-specific config) — may be absent.
4949+2. Extract `provided.includes` — the list of dependency providers.
5050+3. For each include, invoke it with `{ class, aspect-chain }` and recurse.
5151+4. Return `{ imports = [ class-config ] ++ [ recursive-include-results ] }`.
5252+5353+The result is a single Nix module whose `imports` list contains all transitively collected class-specific configs.
5454+5555+```mermaid
5656+graph TD
5757+ A["aspect.resolve { class = 'nixos' }"]
5858+ A --> C["aspect.nixos config"]
5959+ A --> I["aspect.includes"]
6060+ I --> D1["dep1.resolve { class = 'nixos' }"]
6161+ I --> D2["dep2.resolve { class = 'nixos' }"]
6262+ D1 --> M["merged { imports = [...] }"]
6363+ D2 --> M
6464+ C --> M
6565+```
6666+6767+## The `aspect-chain`
6868+6969+The `aspect-chain` is the call stack during resolution — the list of aspect configs that led to the current point (most recent last). It grows by one entry on each recursive call.
7070+7171+Providers receive `{ class, aspect-chain }` and can inspect who is including them:
7272+7373+```nix
7474+provides.logging = { aspect-chain, class }:
7575+ let caller = (lib.last aspect-chain).name;
7676+ in { ${class}.tag = "from-${caller}"; };
7777+```
7878+7979+<Aside>
8080+ Tests: [`aspect_chain.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_chain.nix) ·
8181+ [`aspect_modules_resolved.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_modules_resolved.nix)
8282+</Aside>
8383+8484+## Accessing resolved modules
8585+8686+Two equivalent ways:
8787+8888+```nix
8989+# Via .resolve
9090+aspect.resolve { class = "nixos"; }
9191+9292+# Via .modules
9393+aspect.modules.nixos
9494+```
9595+9696+Both return the same fully-resolved Nix module.
+81
docs/src/content/docs/concepts/providers.mdx
···11+---
22+title: Providers & Fixpoint
33+description: Nested sub-aspects, fixpoint semantics, and the provides/_ shorthand.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## `provides` and `_`
99+1010+Aspects can expose sub-aspects through the `provides` attribute. The `_` attribute is a shorthand alias.
1111+1212+```nix
1313+flake.aspects = { aspects, ... }: {
1414+ gaming = {
1515+ nixos = { };
1616+ provides.emulation = {
1717+ nixos = { };
1818+ _.nes.nixos = { }; # _ is alias for provides
1919+ };
2020+ };
2121+ my-host.includes = [ aspects.gaming._.emulation._.nes ];
2222+};
2323+```
2424+2525+Each entry in `provides` is itself an aspect — it has its own `name`, `includes`, class configs, and can nest further.
2626+2727+Providers that are functions receive `{ class, aspect-chain }`:
2828+2929+```nix
3030+provides.logging = { class, aspect-chain }: {
3131+ ${class}.enableLogging = true;
3232+};
3333+```
3434+3535+<Aside>
3636+ Test: [`aspect_provides.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_provides.nix)
3737+</Aside>
3838+3939+## Fixpoint semantics
4040+4141+Both the top-level `flake.aspects` and each `provides` submodule receive an `aspects` argument — a fixpoint of their sibling scope. This means:
4242+4343+- **Top-level aspects** can reference each other: `aspects.foo`, `aspects.bar`.
4444+- **Providers** can reference siblings: `aspects.sibling` within the same `provides` block.
4545+- **Providers** can reference top-level aspects via closure.
4646+4747+```nix
4848+flake.aspects = { aspects, ... }: {
4949+ two.provides = { aspects, ... }: {
5050+ sub = {
5151+ classOne = { };
5252+ includes = [ aspects.sibling ];
5353+ };
5454+ sibling.classOne = { };
5555+ };
5656+ five.classOne = { };
5757+ one.includes = [
5858+ aspects.two._.sub # reaches into two's providers
5959+ ];
6060+};
6161+```
6262+6363+The fixpoint is implemented via `freeformType` with `_module.args.aspects = config` in [types.nix](https://github.com/vic/flake-aspects/blob/main/nix/types.nix), so `aspects` always refers to the evaluated sibling scope.
6464+6565+<Aside>
6666+ Test: [`aspect_fixpoint.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_fixpoint.nix) — resolves aspects from five levels: top-level `aspects.five`, provider-local `aspects.four`, and cross-scope `aspects.two._.sub`.
6767+</Aside>
6868+6969+## Provider types
7070+7171+The type system ([`nix/types.nix`](https://github.com/vic/flake-aspects/blob/main/nix/types.nix)) accepts several provider shapes:
7272+7373+| Shape | Description |
7474+|---|---|
7575+| Aspect attrset | Direct `{ classX = ...; includes = [...]; }` |
7676+| `{ class, aspect-chain } → aspect` | Context-aware provider |
7777+| `{ class } → aspect` | Provider needing only class |
7878+| `{ aspect-chain } → aspect` | Provider needing only chain |
7979+| `args → provider` | Curried — parametric (see [guide](/guides/parametric/)) |
8080+8181+All of these can appear in `includes` or as `provides` entries. The type system distinguishes them via `isProviderFn` and `isSubmoduleFn` checks.
+65
docs/src/content/docs/concepts/transpose.mdx
···11+---
22+title: Transpose
33+description: The generic 2-level attribute set transposition primitive.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## What it does
99+1010+`transpose` swaps the outer two levels of a nested attribute set:
1111+1212+```nix
1313+transpose { a.b.c = 1; }
1414+# ⇒ { b.a.c = 1; }
1515+```
1616+1717+Values below the second level are preserved as-is. Common children merge under one parent:
1818+1919+```nix
2020+transpose { a.x = 1; b.x = 2; }
2121+# ⇒ { x = { a = 1; b = 2; }; }
2222+```
2323+2424+<Aside>
2525+ Tests: [`transpose_swap.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/transpose_swap.nix) ·
2626+ [`transpose_common.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/transpose_common.nix)
2727+</Aside>
2828+2929+## Source: [`nix/default.nix`](https://github.com/vic/flake-aspects/blob/main/nix/default.nix)
3030+3131+The implementation is a three-phase pipeline:
3232+3333+1. **Deconstruct** — `mapAttrsToList` over all parents, producing `{ child, parent, value }` items via `emit`.
3434+2. **Flatten** — collapse the nested lists into a flat list.
3535+3. **Reconstruct** — `foldl` items back into the transposed structure.
3636+3737+## The `emit` hook
3838+3939+`emit` is a function `{ child, parent, value } → [{ parent, child, value }]`.
4040+4141+Default is `lib.singleton` — identity transformation, one-to-one mapping.
4242+4343+You can use `emit` to:
4444+- **Filter**: return `[]` to drop items during transposition.
4545+- **Modify**: change the value, rename parent/child.
4646+- **Multiply**: return multiple items from a single input.
4747+4848+## How aspects exploit `emit`
4949+5050+[`nix/aspects.nix`](https://github.com/vic/flake-aspects/blob/main/nix/aspects.nix) supplies a custom `emit` that calls [`resolve`](/concepts/aspects/#resolution) on each item:
5151+5252+```nix
5353+emit = transposed: [{
5454+ inherit (transposed) parent child;
5555+ value = aspects.${transposed.child}.resolve {
5656+ class = transposed.parent;
5757+ };
5858+}];
5959+```
6060+6161+This intercepts every `<aspect>.<class>` → `<class>.<aspect>` transposition and replaces the raw value with a fully-resolved module that includes all transitive dependencies.
6262+6363+<Aside>
6464+ Test: [`tranpose_flake_modules.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/tranpose_flake_modules.nix)
6565+</Aside>
+37
docs/src/content/docs/contributing.md
···11+---
22+title: Contributing
33+description: How to contribute to flake-aspects.
44+---
55+66+All contributions welcome. PRs are checked by CI.
77+88+## Run tests
99+1010+```shell
1111+nix flake check github:vic/checkmate --override-input target . -L
1212+```
1313+1414+## Format code
1515+1616+```shell
1717+nix run github:vic/checkmate#fmt --override-input target .
1818+```
1919+2020+## Bug reports
2121+2222+Create a minimal reproduction as a test case in `checkmate/modules/tests/` and send a PR.
2323+2424+Failing tests are the best way to report bugs — they become the regression test once fixed.
2525+2626+## Documentation
2727+2828+The docs site lives under `./docs/`. Run locally:
2929+3030+```shell
3131+cd docs && pnpm install && pnpm run dev
3232+```
3333+3434+## Community
3535+3636+- [GitHub Issues](https://github.com/vic/flake-aspects/issues) — bugs and features
3737+- [GitHub Discussions](https://github.com/vic/flake-aspects/discussions) — questions and ideas
+86
docs/src/content/docs/guides/dependencies.mdx
···11+---
22+title: Cross-Aspect Dependencies
33+description: Using includes to form a dependency graph between aspects.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## `includes`
99+1010+Every aspect can declare an `includes` list — references to other aspects (or providers) that should be resolved together:
1111+1212+```nix
1313+flake.aspects = { aspects, ... }: {
1414+ server = {
1515+ includes = with aspects; [ networking monitoring ];
1616+ nixos = { services.nginx.enable = true; };
1717+ };
1818+ networking.nixos = { networking.firewall.enable = true; };
1919+ monitoring.nixos = { services.prometheus.enable = true; };
2020+};
2121+```
2222+2323+When `server` is resolved for class `"nixos"`, the result is:
2424+2525+```nix
2626+{
2727+ imports = [
2828+ { services.nginx.enable = true; } # server.nixos
2929+ { networking.firewall.enable = true; } # networking.nixos
3030+ { services.prometheus.enable = true; } # monitoring.nixos
3131+ ];
3232+}
3333+```
3434+3535+Only classes **present on the included aspect** propagate. If `networking` has no `darwin` key, nothing from `networking` appears when resolving for `"darwin"`.
3636+3737+<Aside>
3838+ Test: [`aspect_dependencies.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_dependencies.nix)
3939+</Aside>
4040+4141+## Transitive resolution
4242+4343+Dependencies resolve transitively. If `A` includes `B` and `B` includes `C`, resolving `A` collects configs from `A`, `B`, and `C`:
4444+4545+```nix
4646+flake.aspects = { aspects, ... }: {
4747+ one = { includes = [ aspects.two ]; classOne.bar = [ "1" ]; };
4848+ two = {
4949+ includes = [ aspects.two._.three-and-four ];
5050+ classOne.bar = [ "2" ];
5151+ provides = { aspects, ... }: {
5252+ three-and-four = {
5353+ classOne.bar = [ "3" ];
5454+ includes = [ aspects.four ];
5555+ };
5656+ four.classOne.bar = [ "4" ];
5757+ };
5858+ };
5959+};
6060+```
6161+6262+Resolving `one` for `classOne` collects `["1", "2", "3", "4"]`.
6363+6464+<Aside>
6565+ Test: [`aspect_fixpoint.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_fixpoint.nix) — tests five-level transitive resolution.
6666+</Aside>
6767+6868+## Inline aspects as includes
6969+7070+`includes` entries don't have to be references to named aspects. They can be inline aspect attrsets:
7171+7272+```nix
7373+server.includes = [
7474+ { nixos = { services.sshd.enable = true; }; }
7575+];
7676+```
7777+7878+Or context-aware providers:
7979+8080+```nix
8181+server.includes = [
8282+ ({ class, aspect-chain }: {
8383+ ${class}.tag = "included-by-${(lib.last aspect-chain).name}";
8484+ })
8585+];
8686+```
+82
docs/src/content/docs/guides/flake-parts.mdx
···11+---
22+title: With flake-parts
33+description: Using flake-aspects as a flake-parts module.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Setup
99+1010+Add `flake-aspects` to your flake inputs and import its `flakeModule`:
1111+1212+```nix
1313+{
1414+ inputs.flake-aspects.url = "github:vic/flake-aspects";
1515+1616+ outputs = { flake-parts, flake-aspects, nixpkgs, ... }@inputs:
1717+ flake-parts.lib.mkFlake { inherit inputs; } {
1818+ imports = [ flake-aspects.flakeModule ];
1919+2020+ flake.aspects = {
2121+ my-desktop = {
2222+ nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.firefox ]; };
2323+ darwin = { pkgs, ... }: { environment.systemPackages = [ pkgs.firefox ]; };
2424+ };
2525+ my-server = {
2626+ nixos = { services.nginx.enable = true; };
2727+ };
2828+ };
2929+3030+ flake.nixosConfigurations.workstation = nixpkgs.lib.nixosSystem {
3131+ modules = [
3232+ inputs.self.modules.nixos.my-desktop
3333+ ];
3434+ };
3535+ };
3636+}
3737+```
3838+3939+## How it works
4040+4141+The [`flakeModule`](https://github.com/vic/flake-aspects/blob/main/nix/flakeModule.nix) calls [`new`](/reference/api/#new) with a callback that:
4242+4343+1. Creates `flake.aspects` as a user-facing option (type: `aspectsType`).
4444+2. Sets `flake.modules` to the transposed + resolved output.
4545+4646+After evaluation, `flake.modules.<class>.<aspect>` contains fully-resolved Nix modules ready for use in `nixosSystem`, `darwinSystem`, `evalModules`, etc.
4747+4848+## With flake-parts `modules` output
4949+5050+If you also import `flake-parts.flakeModules.modules`, the resolved modules are exposed as `inputs.self.modules`:
5151+5252+```nix
5353+imports = [
5454+ flake-aspects.flakeModule
5555+ flake-parts.flakeModules.modules # exposes flake.modules as output
5656+];
5757+```
5858+5959+Then in your system configs: `inputs.self.modules.nixos.my-desktop`.
6060+6161+<Aside>
6262+ Test: [`tranpose_flake_modules.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/tranpose_flake_modules.nix) ·
6363+ [`default_empty.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/default_empty.nix)
6464+</Aside>
6565+6666+## Using dependencies
6767+6868+Aspects can reference each other via the `aspects` fixpoint argument:
6969+7070+```nix
7171+flake.aspects = { aspects, ... }: {
7272+ base-tools = {
7373+ nixos = { pkgs, ... }: { environment.systemPackages = with pkgs; [ git curl ]; };
7474+ };
7575+ workstation = {
7676+ includes = [ aspects.base-tools ];
7777+ nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.vscode ]; };
7878+ };
7979+};
8080+```
8181+8282+Resolving `workstation` for `"nixos"` produces `{ imports = [ workstation-nixos, base-tools-nixos ] }`.
+85
docs/src/content/docs/guides/forward.mdx
···11+---
22+title: Forward Across Classes
33+description: Route resolved modules from one class into a submodule of another.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## What `forward` does
99+1010+[`forward`](https://github.com/vic/flake-aspects/blob/main/nix/forward.nix) resolves an aspect for one class and injects the result into a submodule path of another class. This enables cross-class configuration routing.
1111+1212+The canonical use case: forward `homeManager` modules into `nixos.home-manager.users.<name>`.
1313+1414+## Arguments
1515+1616+```nix
1717+forward {
1818+ each = [ items ]; # list of items to iterate over
1919+ fromClass = item: "sourceClass"; # class to resolve from
2020+ intoClass = item: "targetClass"; # class to inject into
2121+ intoPath = item: [ "path" ]; # submodule path in targetClass
2222+ fromAspect = item: aspect; # aspect to resolve
2323+}
2424+```
2525+2626+| Parameter | Signature | Purpose |
2727+|---|---|---|
2828+| `each` | `listOf any` | Items to iterate. One forward per item. |
2929+| `fromClass` | `item → string` | Source class name to resolve |
3030+| `intoClass` | `item → string` | Target class name to inject into |
3131+| `intoPath` | `item → listOf string` | Attribute path for the submodule in target |
3232+| `fromAspect` | `item → aspect` | The aspect to resolve for `fromClass` |
3333+3434+## Return value
3535+3636+`forward` returns `{ includes = [ ... ]; }` — an aspect with one include per item. Each include:
3737+3838+1. Resolves `fromAspect item` for `fromClass item`
3939+2. Wraps the resolved module as `{ imports = [ module ]; }` at `intoPath item`
4040+3. Places the result under `intoClass item`
4141+4242+## Example
4343+4444+```nix
4545+flake.aspects = { aspects, ... }: {
4646+ my-host = {
4747+ targetClass = {
4848+ imports = [ targetSubmodule ];
4949+ targetMod.names = [ "from-target" ];
5050+ };
5151+ sourceClass.names = [ "from-source" ];
5252+ includes = [
5353+ ({ class, aspect-chain }: forward {
5454+ each = [ "source" ];
5555+ fromClass = item: "${item}Class";
5656+ intoClass = _: "targetClass";
5757+ intoPath = _: [ "targetMod" ];
5858+ fromAspect = _: lib.head aspect-chain;
5959+ })
6060+ ];
6161+ };
6262+};
6363+```
6464+6565+Resolving for `"targetClass"` merges `["from-target", "from-source"]`.
6666+6767+<Aside>
6868+ Test: [`forward.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/forward.nix)
6969+</Aside>
7070+7171+## Real-world: Home-Manager into NixOS
7272+7373+This is how [Den](https://github.com/vic/den) forwards user home-manager configs into host NixOS:
7474+7575+```nix
7676+hmSupport = { host }: forward {
7777+ each = host.users;
7878+ fromClass = _user: "homeManager";
7979+ intoClass = _user: "nixos";
8080+ intoPath = user: [ "home-manager" "users" user.userName ];
8181+ fromAspect = user: den.aspects.${user.userName};
8282+};
8383+```
8484+8585+Each user's `homeManager` aspect gets resolved and placed at `nixos.home-manager.users.<name>`.
+67
docs/src/content/docs/guides/functor.mdx
···11+---
22+title: __functor Override
33+description: Override how an aspect behaves when included by others.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Default behavior
99+1010+Every aspect has a default `__functor` that, when the aspect is called as a function, returns itself ignoring the context:
1111+1212+```nix
1313+__functor = self: _context: self;
1414+```
1515+1616+This means when aspect `A` appears in another's `includes`, resolution calls `A { class, aspect-chain }` which just returns `A` unchanged — so all of `A`'s class configs and nested includes get resolved normally.
1717+1818+## Overriding `__functor`
1919+2020+Replace `__functor` to intercept inclusion. The functor receives `self` (the attrset) and must return a function `{ class, aspect-chain } → aspect`:
2121+2222+```nix
2323+flake.aspects = { aspects, ... }: {
2424+ adaptable = {
2525+ nixos.base = true;
2626+ __functor = self: { class, aspect-chain }:
2727+ if class == "nixos" then self
2828+ else { darwin.fallback = true; };
2929+ };
3030+};
3131+```
3232+3333+When resolved for `"nixos"`, it returns the full aspect (with `nixos.base = true`).
3434+When resolved for `"darwin"`, it returns a different aspect with `darwin.fallback = true`.
3535+3636+<Aside>
3737+ Test: [`aspect_default_provider_functor.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_default_provider_functor.nix)
3838+</Aside>
3939+4040+## Functor with parametric includes
4141+4242+A common pattern: the functor intercepts `includes` to inject arguments before delegating:
4343+4444+```nix
4545+flake.aspects = { aspects, ... }: {
4646+ wrapper = {
4747+ includes = [ aspects.tool ];
4848+ __functor = self: {
4949+ includes = [
5050+ { classOne.bar = [ "from-functor" ]; }
5151+ ] ++ map (f: f { message = "hello"; }) self.includes;
5252+ };
5353+ };
5454+ tool = { message }: {
5555+ classOne.bar = [ message ];
5656+ };
5757+};
5858+```
5959+6060+Here `wrapper.__functor` transforms each include by calling it with `{ message = "hello"; }` before resolution.
6161+6262+Note: when `__functor` is overridden, the aspect's own class configs (e.g., `classOne.bar = [ "should-not-be-present" ]`)
6363+are **not** automatically included — the functor's return value replaces the aspect entirely during resolution.
6464+6565+<Aside>
6666+ Test: [`aspect_default_provider_override.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_default_provider_override.nix) — demonstrates that functor return replaces the original aspect config.
6767+</Aside>
+66
docs/src/content/docs/guides/parametric.mdx
···11+---
22+title: Parametric Aspects
33+description: Curried functions as configurable providers.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Provider-level parametric
99+1010+Providers inside `provides` can be curried functions. The outer function takes user arguments; the inner function (if needed) takes `{ class, aspect-chain }`:
1111+1212+```nix
1313+flake.aspects = { aspects, ... }: {
1414+ base.provides.user = userName: {
1515+ nixos.users.users.${userName}.isNormalUser = true;
1616+ };
1717+ server = {
1818+ includes = [ (aspects.base._.user "bob") ];
1919+ nixos = { };
2020+ };
2121+};
2222+```
2323+2424+Resolving `server` for `"nixos"` produces imports containing `{ users.users.bob.isNormalUser = true; }`.
2525+2626+<Aside>
2727+ Test: [`aspect_parametric.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_parametric.nix)
2828+</Aside>
2929+3030+## Top-level parametric
3131+3232+Top-level aspects can also be curried. The function takes named arguments and returns an aspect:
3333+3434+```nix
3535+flake.aspects = { aspects, ... }: {
3636+ greeter = { message }: {
3737+ nixos.greeting = message;
3838+ };
3939+ host = {
4040+ includes = [ (aspects.greeter { message = "hello"; }) ];
4141+ nixos = { };
4242+ };
4343+};
4444+```
4545+4646+The type system ([`nix/types.nix`](https://github.com/vic/flake-aspects/blob/main/nix/types.nix)) distinguishes between:
4747+- **Direct providers**: `{ class, aspect-chain } → aspect` (1-2 named args: `class` and/or `aspect-chain`)
4848+- **Curried providers**: `anything → provider` (other function signatures)
4949+5050+A curried provider is invoked by the user at inclusion time. Its return value must be either another provider function or a direct aspect attrset.
5151+5252+<Aside>
5353+ Test: [`aspect_toplevel_parametric.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_toplevel_parametric.nix)
5454+</Aside>
5555+5656+## Combining with context
5757+5858+A parametric provider can return a context-aware function:
5959+6060+```nix
6161+provides.themed = theme:
6262+ { class, aspect-chain }:
6363+ { ${class}.theme = "${theme}-for-${(lib.last aspect-chain).name}"; };
6464+```
6565+6666+This is a three-level chain: user args → context → aspect config.
+73
docs/src/content/docs/guides/standalone.mdx
···11+---
22+title: Without Flakes
33+description: Using flake-aspects with lib.evalModules and new-scope.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## `new-scope` — named aspect namespaces
99+1010+[`new-scope`](https://github.com/vic/flake-aspects/blob/main/nix/new-scope.nix) creates an isolated aspect namespace within any `lib.evalModules` evaluation:
1111+1212+```nix
1313+let
1414+ result = lib.evalModules {
1515+ modules = [
1616+ (new-scope "my")
1717+ {
1818+ my.aspects = { aspects, ... }: {
1919+ laptop = {
2020+ nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.vim ]; };
2121+ includes = [ aspects.base ];
2222+ };
2323+ base.nixos = { lib, ... }: { networking.hostName = "laptop"; };
2424+ };
2525+ }
2626+ ];
2727+ };
2828+in
2929+ lib.nixosSystem { modules = [ result.config.my.modules.nixos.laptop ]; }
3030+```
3131+3232+`new-scope "my"` creates:
3333+- `my.aspects` — input: aspect definitions (type: `aspectsType`)
3434+- `my.modules` — output: transposed + resolved modules (read-only)
3535+3636+<Aside>
3737+ Test: [`without_flakes.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/without_flakes.nix)
3838+</Aside>
3939+4040+## Multiple scopes
4141+4242+Independent scopes can coexist and merge. Each creates its own `<name>.aspects` / `<name>.modules` pair:
4343+4444+```nix
4545+modules = [
4646+ (new-scope "foo")
4747+ (new-scope "bar")
4848+ { foo.aspects.a.nixos.x = [ "from-foo" ]; }
4949+ { bar.aspects.a.nixos.x = [ "from-bar" ]; }
5050+ ({ config, ... }: { bar = config.foo; }) # merge foo into bar
5151+];
5252+```
5353+5454+After merging, `bar.modules.nixos.a` contains both `"from-foo"` and `"from-bar"`.
5555+5656+<Aside>
5757+ Test: [`aspect_assignment.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_assignment.nix)
5858+</Aside>
5959+6060+## `new` — low-level factory
6161+6262+[`new`](https://github.com/vic/flake-aspects/blob/main/nix/new.nix) is the callback-based primitive underneath `new-scope`:
6363+6464+```nix
6565+new (option: transposed: {
6666+ options.myLib.aspects = option;
6767+ config.myLib.modules = transposed;
6868+}) config.myLib.aspects
6969+```
7070+7171+`new-scope` is sugar for the common `${name}.aspects` / `${name}.modules` pattern. Use `new` directly when you need custom option paths or additional logic in the callback.
7272+7373+Useful for libraries that want isolated aspect scopes or flake-parts independence (see [Den's scope](https://github.com/vic/den/blob/main/nix/scope.nix)).
+73
docs/src/content/docs/index.mdx
···11+---
22+title: flake-aspects
33+description: Aspect-oriented transposition for Nix module systems.
44+template: splash
55+hero:
66+ tagline: "<code><aspect>.<class></code> transposition for Nix — dependency-free, composable, cross-class."
77+ image:
88+ html: |
99+ <img width="400" height="400" src="https://github.com/user-attachments/assets/dd28ce8d-f727-4e31-a192-d3002ee8984e" />
1010+ actions:
1111+ - text: Get Started
1212+ link: /guides/flake-parts/
1313+ icon: right-arrow
1414+ - text: Overview
1515+ link: /overview/
1616+ variant: minimal
1717+ - text: Source Code
1818+ link: https://github.com/vic/flake-aspects
1919+ icon: github
2020+ variant: secondary
2121+---
2222+2323+import { Card, CardGrid } from '@astrojs/starlight/components';
2424+2525+**flake-aspects** is a small, dependency-free Nix library that transposes
2626+`<aspect>.<class>` into `<class>.<aspect>` — the standard `flake.modules` layout.
2727+2828+On top of transposition, it provides a composable dependency graph via `includes`,
2929+nestable sub-aspects via `provides` (alias `_`), parametric curried providers,
3030+context-aware `__functor` override, and cross-class `forward`.
3131+3232+<CardGrid>
3333+ <Card title="Zero dependencies" icon="approve-check">
3434+ Pure Nix. Works with flakes, without flakes, with flake-parts, or standalone via `lib.evalModules`.
3535+ </Card>
3636+ <Card title="Any Nix class" icon="puzzle">
3737+ NixOS, Darwin, Home-Manager, NixVim, Terranix — any configuration class expressible as a Nix module.
3838+ </Card>
3939+ <Card title="Composable graph" icon="random">
4040+ Aspects declare `includes` forming a dependency DAG. Resolution collects all class-specific configs transitively.
4141+ </Card>
4242+ <Card title="Parametric" icon="setting">
4343+ Curried functions as providers. Pass arguments at inclusion time. Override behavior with `__functor`.
4444+ </Card>
4545+</CardGrid>
4646+4747+4848+```nix
4949+# input: flake.aspects
5050+{
5151+ vim-btw = {
5252+ nixos = { ... };
5353+ darwin = { ... };
5454+ homeManager = { ... };
5555+ };
5656+ tiling-desktop = {
5757+ includes = [ aspects.vim-btw ];
5858+ nixos = { ... };
5959+ };
6060+}
6161+```
6262+6363+```nix
6464+# output: flake.modules (transposed + resolved)
6565+{
6666+ nixos.vim-btw = { imports = [ ... ]; };
6767+ nixos.tiling-desktop = { imports = [ nixos-config, vim-btw-nixos ]; };
6868+ darwin.vim-btw = { imports = [ ... ]; };
6969+ homeManager.vim-btw = { imports = [ ... ]; };
7070+}
7171+```
7272+7373+Used by [Den](https://github.com/vic/den) and others for dendritic Nix setups.
+50
docs/src/content/docs/motivation.mdx
···11+---
22+title: Motivation
33+description: Why flake-aspects exists.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## The problem
99+1010+In [Dendritic](https://github.com/mightyiam/dendritic) Nix setups, modules are organized as
1111+`flake.modules.<class>.<name>` — a flat, two-level structure where class comes first.
1212+1313+This works well for consumers, but for *authors* it fragments a logical concern across
1414+multiple class namespaces. A "desktop" aspect touching `nixos`, `darwin`, and `homeManager`
1515+ends up as three separate entries under three different class keys.
1616+1717+## The transposition idea
1818+1919+[Unify](https://codeberg.org/quasigod/unify)'s author invented the `<aspect>.<class>` layout:
2020+group all class-specific configs under the aspect name, then transpose into the standard
2121+`<class>.<aspect>` output.
2222+2323+**flake-aspects** implements this transposition as a small, dependency-free primitive.
2424+2525+## Beyond transposition
2626+2727+A flat transposition is not enough for real-world reusability. flake-aspects adds:
2828+2929+- **`includes`** — declare dependencies between aspects, forming a DAG resolved recursively per class.
3030+- **`provides` / `_`** — nest sub-aspects in a tree, with fixpoint semantics so siblings can reference each other.
3131+- **Parametric providers** — curried functions that accept arguments at inclusion time.
3232+- **`__functor` override** — intercept inclusion and dispatch based on `{ class, aspect-chain }` context.
3333+- **`forward`** — route resolved modules from one class into a submodule of another (e.g., homeManager → nixos).
3434+- **`aspect-chain`** — the call stack of aspects during resolution, enabling context-aware decisions.
3535+3636+All of this with zero dependencies beyond `lib`.
3737+3838+## What flake-aspects is not
3939+4040+flake-aspects is a **library**, not a framework. It does not:
4141+- Define hosts, users, or homes
4242+- Build `nixosConfigurations` or `darwinConfigurations`
4343+- Provide a context pipeline or entity schemas
4444+- Ship batteries or opinionated defaults
4545+4646+For a framework built on flake-aspects, see [Den](https://github.com/vic/den).
4747+4848+<Aside type="tip" icon="heart" title="Support development">
4949+[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++ and AI--. If you find them useful, consider [sponsoring](https://github.com/sponsors/vic).
5050+</Aside>
+69
docs/src/content/docs/overview.mdx
···11+---
22+title: Overview
33+description: Everything in flake-aspects at a glance.
44+---
55+66+import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components';
77+88+## Core Concepts
99+1010+<CardGrid>
1111+ <Card title="Transpose" icon="random">
1212+ Generic 2-level attribute set transposition: `{ a.b = v }` → `{ b.a = v }`. Parameterized by an `emit` hook.
1313+ <LinkButton href="/concepts/transpose" variant="minimal" icon="right-arrow">Learn More</LinkButton>
1414+ </Card>
1515+ <Card title="Aspects & Resolution" icon="puzzle">
1616+ Aspect submodules with `includes` (dependency graph) and per-class configs. Recursive resolution collects all transitive class-specific modules.
1717+ <LinkButton href="/concepts/aspects" variant="minimal" icon="right-arrow">Learn More</LinkButton>
1818+ </Card>
1919+ <Card title="Providers & Fixpoint" icon="setting">
2020+ Nested sub-aspects via `provides` / `_`. Fixpoint semantics let providers reference siblings and top-level aspects.
2121+ <LinkButton href="/concepts/providers" variant="minimal" icon="right-arrow">Learn More</LinkButton>
2222+ </Card>
2323+</CardGrid>
2424+2525+## Guides
2626+2727+<CardGrid>
2828+ <Card title="With flake-parts" icon="rocket">
2929+ The `flakeModule` wires `flake.aspects` → `flake.modules` automatically. Minimal setup.
3030+ <LinkButton href="/guides/flake-parts" variant="minimal" icon="right-arrow">Learn More</LinkButton>
3131+ </Card>
3232+ <Card title="Without Flakes" icon="open-book">
3333+ Use `new-scope` with `lib.evalModules` for isolated namespaces. No flake-parts dependency needed.
3434+ <LinkButton href="/guides/standalone" variant="minimal" icon="right-arrow">Learn More</LinkButton>
3535+ </Card>
3636+ <Card title="Cross-Aspect Dependencies" icon="list-format">
3737+ `includes` forms a DAG. Only classes present on the included aspect propagate during resolution.
3838+ <LinkButton href="/guides/dependencies" variant="minimal" icon="right-arrow">Learn More</LinkButton>
3939+ </Card>
4040+ <Card title="Parametric Aspects" icon="seti:config">
4141+ Curried providers with arguments at inclusion time. Works at both `provides` and top levels.
4242+ <LinkButton href="/guides/parametric" variant="minimal" icon="right-arrow">Learn More</LinkButton>
4343+ </Card>
4444+ <Card title="__functor Override" icon="pencil">
4545+ Intercept inclusion. Dispatch different configs based on `class` and `aspect-chain` context.
4646+ <LinkButton href="/guides/functor" variant="minimal" icon="right-arrow">Learn More</LinkButton>
4747+ </Card>
4848+ <Card title="Forward Across Classes" icon="right-arrow">
4949+ Route resolved modules from one class into a submodule path of another class.
5050+ <LinkButton href="/guides/forward" variant="minimal" icon="right-arrow">Learn More</LinkButton>
5151+ </Card>
5252+</CardGrid>
5353+5454+## Reference
5555+5656+<CardGrid>
5757+ <Card title="API" icon="seti:code-search">
5858+ Full export table: `transpose`, `types`, `aspects`, `new`, `new-scope`, `forward`.
5959+ <LinkButton href="/reference/api" variant="minimal" icon="right-arrow">Learn More</LinkButton>
6060+ </Card>
6161+ <Card title="Type System" icon="document">
6262+ `aspectsType`, `aspectSubmodule`, `providerType` — the Nix option types that validate aspect definitions.
6363+ <LinkButton href="/reference/types" variant="minimal" icon="right-arrow">Learn More</LinkButton>
6464+ </Card>
6565+ <Card title="Test Suite" icon="seti:test">
6666+ 16 test files covering every feature. How to run and read them.
6767+ <LinkButton href="/reference/tests" variant="minimal" icon="right-arrow">Learn More</LinkButton>
6868+ </Card>
6969+</CardGrid>
+89
docs/src/content/docs/reference/api.mdx
···11+---
22+title: API Reference
33+description: All exports from the flake-aspects library.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+## Entry points
99+1010+The flake exposes three entry points:
1111+1212+```nix
1313+{
1414+ # Default functor: (lib) → { transpose, types, aspects, new, new-scope, forward }
1515+ __functor = _: import ./nix;
1616+1717+ # flake-parts module: creates flake.aspects → flake.modules
1818+ flakeModule = ./nix/flakeModule.nix;
1919+2020+ # Library API: (lib) → { transpose, types, aspects, new, new-scope, forward }
2121+ lib = import ./nix/lib.nix;
2222+}
2323+```
2424+2525+Usage: `flake-aspects.lib nixpkgs.lib` or `import ./nix/lib.nix lib`.
2626+2727+## Exports
2828+2929+Source: [`nix/lib.nix`](https://github.com/vic/flake-aspects/blob/main/nix/lib.nix)
3030+3131+### `transpose { emit? }`
3232+3333+Source: [`nix/default.nix`](https://github.com/vic/flake-aspects/blob/main/nix/default.nix)
3434+3535+Generic 2-level transposition. Returns a function `attrs → transposed-attrs`.
3636+3737+| Parameter | Type | Default | Purpose |
3838+|---|---|---|---|
3939+| `emit` | `{ child, parent, value } → [{ parent, child, value }]` | `lib.singleton` | Hook for filtering, modifying, or multiplying items |
4040+4141+See [Transpose concept](/concepts/transpose/).
4242+4343+### `types`
4444+4545+Source: [`nix/types.nix`](https://github.com/vic/flake-aspects/blob/main/nix/types.nix)
4646+4747+Returns `{ aspectsType, aspectSubmodule, providerType }`.
4848+4949+See [Type System reference](/reference/types/).
5050+5151+### `aspects`
5252+5353+Source: [`nix/aspects.nix`](https://github.com/vic/flake-aspects/blob/main/nix/aspects.nix)
5454+5555+`aspects : lib → aspectsConfig → { transposed }`.
5656+5757+Aspect-aware transposition. Supplies a custom `emit` to `transpose` that calls `resolve` on each item. Returns `{ transposed = { <class>.<aspect> = resolved-module; }; }`.
5858+5959+### `new`
6060+6161+Source: [`nix/new.nix`](https://github.com/vic/flake-aspects/blob/main/nix/new.nix)
6262+6363+`new : lib → (option → transposed → moduleDefinition) → aspectsConfig → moduleDefinition`.
6464+6565+Low-level scope factory. The callback receives:
6666+- `option` — an `mkOption` for `aspectsType` (the user-facing input)
6767+- `transposed` — the resolved `{ <class>.<aspect> }` output
6868+6969+### `new-scope`
7070+7171+Source: [`nix/new-scope.nix`](https://github.com/vic/flake-aspects/blob/main/nix/new-scope.nix)
7272+7373+`new-scope : name → nixos-module`.
7474+7575+Creates `${name}.aspects` (input) and `${name}.modules` (read-only output). Sugar over `new`. See [Standalone guide](/guides/standalone/).
7676+7777+### `forward`
7878+7979+Source: [`nix/forward.nix`](https://github.com/vic/flake-aspects/blob/main/nix/forward.nix)
8080+8181+`forward : lib → { each, fromClass, intoClass, intoPath, fromAspect } → aspect`.
8282+8383+Cross-class module forwarding. See [Forward guide](/guides/forward/).
8484+8585+## `flakeModule`
8686+8787+Source: [`nix/flakeModule.nix`](https://github.com/vic/flake-aspects/blob/main/nix/flakeModule.nix)
8888+8989+A flake-parts module. Import it to get `flake.aspects` (input) → `flake.modules` (output). Uses `new` internally. See [flake-parts guide](/guides/flake-parts/).
+77
docs/src/content/docs/reference/tests.mdx
···11+---
22+title: Test Suite
33+description: All test files and how to run them.
44+---
55+66+## Running tests
77+88+```shell
99+# Format code
1010+nix run github:vic/checkmate#fmt --override-input target .
1111+1212+# Run all tests
1313+nix flake check github:vic/checkmate --override-input target . -L
1414+```
1515+1616+Tests use [checkmate](https://github.com/vic/checkmate) — each test defines `flake.tests.<name> = { expr, expected }`.
1717+1818+## Test index
1919+2020+### Transpose
2121+2222+| Test | What it verifies |
2323+|---|---|
2424+| [`transpose_swap.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/transpose_swap.nix) | `{ a.b.c = 1 }` → `{ b.a.c = 1 }` |
2525+| [`transpose_common.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/transpose_common.nix) | Common children merge: `{ a.b, c.b }` → `{ b.{a,c} }` |
2626+| [`tranpose_flake_modules.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/tranpose_flake_modules.nix) | `flake.aspects` → `flake.modules` end-to-end |
2727+2828+### Resolution & Dependencies
2929+3030+| Test | What it verifies |
3131+|---|---|
3232+| [`aspect_dependencies.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_dependencies.nix) | `includes` resolves transitive deps per class |
3333+| [`aspect_chain.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_chain.nix) | `aspect-chain` grows correctly through resolution |
3434+| [`aspect_modules_resolved.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_modules_resolved.nix) | `.modules.<class>` matches `.resolve { class }` |
3535+| [`aspect_fixpoint.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_fixpoint.nix) | Fixpoint: providers reference siblings and top-level |
3636+3737+### Providers
3838+3939+| Test | What it verifies |
4040+|---|---|
4141+| [`aspect_provides.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_provides.nix) | `provides` / `_` with context-aware providers |
4242+| [`aspect_parametric.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_parametric.nix) | Curried provider in `provides` |
4343+| [`aspect_toplevel_parametric.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_toplevel_parametric.nix) | Curried top-level aspect |
4444+4545+### Functor
4646+4747+| Test | What it verifies |
4848+|---|---|
4949+| [`aspect_default_provider_functor.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_default_provider_functor.nix) | `__functor` override with parametric includes |
5050+| [`aspect_default_provider_override.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_default_provider_override.nix) | Functor replaces original config entirely |
5151+5252+### Scopes & Standalone
5353+5454+| Test | What it verifies |
5555+|---|---|
5656+| [`without_flakes.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/without_flakes.nix) | `new-scope` + `lib.evalModules` without flakes |
5757+| [`aspect_assignment.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/aspect_assignment.nix) | Multiple scopes merge correctly |
5858+| [`default_empty.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/default_empty.nix) | Empty `flake.aspects` produces empty `flake.modules` |
5959+6060+### Forward
6161+6262+| Test | What it verifies |
6363+|---|---|
6464+| [`forward.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests/forward.nix) | Cross-class module forwarding |
6565+6666+## Test harness
6767+6868+The test infrastructure ([`checkmate/modules/tests.nix`](https://github.com/vic/flake-aspects/blob/main/checkmate/modules/tests.nix)) provides:
6969+7070+| Helper | Purpose |
7171+|---|---|
7272+| `mkFlake` | Creates a flake evaluation with aspects + test options pre-wired |
7373+| `evalMod` | Evaluates a module with `lib.evalModules` and returns `.config` |
7474+| `fooOpt` | Standard test options: `foo` (str), `bar` (listOf str), `baz` (attrsOf str) |
7575+| `transpose` | Direct access to the transpose function |
7676+| `new-scope` | Direct access for standalone tests |
7777+| `forward` | Direct access to the forward function |
+76
docs/src/content/docs/reference/types.mdx
···11+---
22+title: Type System
33+description: The Nix option types that validate aspect definitions.
44+---
55+66+import { Aside } from '@astrojs/starlight/components';
77+88+Source: [`nix/types.nix`](https://github.com/vic/flake-aspects/blob/main/nix/types.nix)
99+1010+## Exported types
1111+1212+| Type | Purpose |
1313+|---|---|
1414+| `aspectsType` | Top-level container for all aspects. Used as the type for `flake.aspects`. |
1515+| `aspectSubmodule` | Individual aspect definition with class configs, includes, provides. |
1616+| `providerType` | Union of all valid provider shapes (functions and aspect attrsets). |
1717+1818+## `aspectsType`
1919+2020+A `submodule` with `freeformType = lazyAttrsOf (either aspectSubmodule providerType)`.
2121+2222+Provides a fixpoint: `_module.args.aspects = config` — so aspect definitions can reference siblings via the `aspects` argument.
2323+2424+```nix
2525+flake.aspects = { aspects, ... }: {
2626+ a.includes = [ aspects.b ];
2727+ b.nixos = { };
2828+};
2929+```
3030+3131+Top-level entries can be either `aspectSubmodule` values (with submodule-style function args like `{ lib, config, options, aspect }`) or `providerType` values (curried or direct provider functions).
3232+3333+## `aspectSubmodule`
3434+3535+A `submodule` defining a single aspect. Key options:
3636+3737+| Option | Type | Default | Description |
3838+|---|---|---|---|
3939+| `name` | `str` | attribute name | Aspect name, auto-set |
4040+| `description` | `str` | `"Aspect ${name}"` | Human description |
4141+| `includes` | `listOf providerType` | `[]` | Dependency providers |
4242+| `provides` / `_` | `submodule` (freeform) | `{}` | Nested sub-aspects |
4343+| `__functor` | `self → context → provider` | identity | Override resolution behavior |
4444+| `resolve` | internal | — | `{ class, aspect-chain? } → module` |
4545+| `modules` | internal | — | `{ <class> = module }` lazy attrset |
4646+4747+The `freeformType = lazyAttrsOf deferredModule` means any key not listed above is treated as a class name with a deferred module value.
4848+4949+`_` is an alias for `provides` (via `mkAliasOptionModule`).
5050+5151+The `provides` submodule also receives a fixpoint: `_module.args.aspects = config` — providers can reference siblings.
5252+5353+## `providerType`
5454+5555+A union type covering all valid provider shapes:
5656+5757+```
5858+providerType = either providerFn aspectSubmodule
5959+```
6060+6161+Where `providerFn = either directProviderFn curriedProviderFn`:
6262+6363+- **`directProviderFn`**: a function whose args are exactly `{ class }`, `{ aspect-chain }`, or `{ class, aspect-chain }`.
6464+- **`curriedProviderFn`**: any other function that returns a `providerType`.
6565+6666+The distinction is made by `isProviderFn` which inspects `functionArgs`:
6767+- If the function takes exactly `class` and/or `aspect-chain` (and nothing else), it's a direct provider.
6868+- Otherwise, it's treated as curried — the user must call it with arguments first.
6969+7070+## Internal types
7171+7272+| Type | Purpose |
7373+|---|---|
7474+| `ignoredType` | For computed values that only exist during evaluation. Merges to `null`. |
7575+| `mkInternal` | Helper to create internal, invisible, read-only options with an `apply` function. |
7676+| `isSubmoduleFn` | Checks if a function has submodule-style args (`lib`, `config`, `options`, `aspect`). |
+8
docs/src/content/docs/sponsor.md
···11+---
22+title: Sponsor
33+description: Support flake-aspects development.
44+---
55+66+flake-aspects 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++ and AI--.
77+88+If you find them useful, consider [sponsoring](https://github.com/sponsors/vic).