7.6 KiB
AGENTS.md - Coding Agent Instructions
This document provides essential information for AI coding agents working in this repository.
Project Overview
Quartz is a static site generator for publishing digital gardens and notes as websites. Built with TypeScript, Preact, and unified/remark/rehype for markdown processing.
| Stack | Technology |
|---|---|
| Language | TypeScript 5.x (strict mode) |
| Runtime | Node.js >=22 (v22.16.0 pinned) |
| Package Mgr | npm >=10.9.2 |
| Module System | ES Modules ("type": "module") |
| UI Framework | Preact 10.x (JSX with react-jsx pragma) |
| Build Tool | esbuild |
| Styling | SCSS via esbuild-sass-plugin |
Environment
This is a Nix project. Use the provided flake.nix to enter a dev shell with Node.js 22 and npm:
nix develop
All npm commands below must be run inside the dev shell.
Build, Lint, and Test Commands
# Type check and format check (CI validation)
npm run check
# Auto-format code with Prettier
npm run format
# Run all tests
npm run test
# Run a single test file
npx tsx --test quartz/util/path.test.ts
# Run tests matching a pattern (use --test-name-pattern)
npx tsx --test --test-name-pattern="typeguards" quartz/util/path.test.ts
# Build the static site
npx quartz build
# Build and serve with hot reload
npx quartz build --serve
# Profile build performance
npm run profile
Test Files Location
Tests use Node.js native test runner via tsx. Test files follow the *.test.ts pattern:
quartz/util/path.test.tsquartz/util/fileTrie.test.tsquartz/components/scripts/search.test.ts
Code Style Guidelines
Prettier Configuration (.prettierrc)
{
"printWidth": 100,
"tabWidth": 2,
"semi": false,
"trailingComma": "all",
"quoteProps": "as-needed"
}
No ESLint - only Prettier for formatting. Run npm run format before committing.
TypeScript Configuration
- Strict mode enabled (
strict: true) noUnusedLocals: true- no unused variablesnoUnusedParameters: true- no unused function parameters- JSX configured for Preact (
jsxImportSource: "preact")
Import Conventions
// 1. External packages first
import { PluggableList } from "unified"
import { visit } from "unist-util-visit"
// 2. Internal utilities/types (relative paths)
import { QuartzTransformerPlugin } from "../types"
import { FilePath, slugifyFilePath } from "../../util/path"
import { i18n } from "../../i18n"
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Files (utils) | camelCase | path.ts, fileTrie.ts |
| Files (comps) | PascalCase | TableOfContents.tsx, Search.tsx |
| Types/Interfaces | PascalCase | QuartzComponent, FullSlug |
| Type Guards | is* prefix |
isFilePath(), isFullSlug() |
| Constants | UPPER_CASE | QUARTZ, UPSTREAM_NAME |
| Options types | Options |
interface Options { ... } |
Branded Types Pattern
This codebase uses branded types for type-safe path handling:
type SlugLike<T> = string & { __brand: T }
export type FilePath = SlugLike<"filepath">
export type FullSlug = SlugLike<"full">
export type SimpleSlug = SlugLike<"simple">
// Always validate with type guards before using
export function isFilePath(s: string): s is FilePath { ... }
Component Pattern (Preact)
Components use a factory function pattern with attached static properties:
export default ((userOpts?: Partial<Options>) => {
const opts: Options = { ...defaultOptions, ...userOpts }
const ComponentName: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
return <div class={classNames(displayClass, "component-name")}>...</div>
}
ComponentName.css = style // SCSS styles
ComponentName.afterDOMLoaded = script // Client-side JS
return ComponentName
}) satisfies QuartzComponentConstructor
Plugin Pattern
Three plugin types: transformers, filters, and emitters.
export const PluginName: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "PluginName",
markdownPlugins(ctx) { return [...] },
htmlPlugins(ctx) { return [...] },
externalResources(ctx) { return { js: [], css: [] } },
}
}
Testing Pattern
Use Node.js native test runner with assert:
import test, { describe, beforeEach } from "node:test"
import assert from "node:assert"
describe("FeatureName", () => {
test("should do something", () => {
assert.strictEqual(actual, expected)
assert.deepStrictEqual(actualObj, expectedObj)
assert(condition) // truthy assertion
assert(!condition) // falsy assertion
})
})
Error Handling
- Use
try/catchfor critical operations (file I/O, parsing) - Custom
traceutility for error reporting with stack traces process.exit(1)for fatal errorsconsole.warn()for non-fatal issues
Async Patterns
- Prefer
async/awaitover raw promises - Use async generators (
async *emit()) for streaming file output - Use
async-mutexfor concurrent build protection
Project Structure
quartz/
├── bootstrap-cli.mjs # CLI entry point
├── build.ts # Build orchestration
├── cfg.ts # Configuration types
├── components/ # Preact UI components
│ ├── *.tsx # Components
│ ├── scripts/ # Client-side scripts (*.inline.ts)
│ └── styles/ # Component SCSS
├── plugins/
│ ├── transformers/ # Markdown AST transformers
│ ├── filters/ # Content filters
│ ├── emitters/ # Output generators
│ └── types.ts # Plugin type definitions
├── processors/ # Build pipeline (parse/filter/emit)
├── util/ # Utility functions
└── i18n/ # Internationalization (30+ locales)
Branch Workflow
This is a fork of jackyzha0/quartz with org-roam customizations.
| Branch | Purpose |
|---|---|
main |
Clean mirror of upstream quartz — no custom code |
org-roam |
Default branch — all customizations live here |
feature/* |
Short-lived branches off org-roam |
Pulling Upstream Updates
git checkout main
git fetch upstream
git merge upstream/main
git checkout org-roam
git merge main
# Resolve conflicts if any, then commit
Working on Features
git checkout org-roam
git checkout -b feature/my-feature
# ... work ...
git checkout org-roam
git merge feature/my-feature
git branch -d feature/my-feature
Merge direction: upstream → main → org-roam → feature/*
Important Notes
- Client-side scripts: Use
.inline.tssuffix, bundled via esbuild - Isomorphic code:
quartz/util/path.tsmust not use Node.js APIs - Incremental builds: Plugins can implement
partialEmitfor efficiency - Markdown flavors: Supports Obsidian (
ofm.ts) and Roam (roam.ts) syntax - Pipeline build artifacts:
scripts/pipeline/_build/andscripts/pipeline/deps/are gitignored — runmix deps.getinsidescripts/pipeline/after a fresh clone