Contributing to Stati
Stati is an open-source project and we welcome contributions from the community! Whether you’re fixing bugs, adding features, improving documentation, or helping with testing, there are many ways to get involved.
Quick Start
- Fork the repository on GitHub
- Clone your fork locally
- Install dependencies with
npm install - Run the test suite with
npm test - Make your changes and add tests
- Submit a pull request
Development Setup
Prerequisites
- Node.js 22.0.0 or higher
- npm 11.5.1 or higher
- Git for version control
Local Development
# Clone the repository
git clone https://github.com/your-username/stati.git
cd stati
# Install dependencies
npm install
# Build all packages (core → cli → create-stati)
npm run build
# Run the test suite
npm test
# Lint and type-check
npm run lint
npm run typecheck
Project Structure
stati/
├── packages/
│ ├── core/ # Core Stati engine
│ │ ├── src/
│ │ ├── tests/
│ │ └── package.json
│ ├── cli/ # Command-line interface
│ │ ├── src/
│ │ ├── tests/
│ │ └── package.json
│ └── create-stati/ # Project scaffolder
│ ├── src/
│ ├── tests/
│ └── package.json
├── examples/ # Example projects (bundled with the scaffolder)
│ ├── blog/
│ ├── docs/
│ └── blank/
├── docs-site/ # Project documentation site
├── scripts/ # Build and development scripts
└── package.json # Root package.json
Monorepo Workflow
Stati uses a monorepo structure with multiple packages:
# Install dependencies for all packages
npm install
# Build all packages (required order: core → cli → create-stati)
npm run build
# Test all packages
npm run test
# Test specific package
npm run test --workspace=@stati/core
# Develop with a specific example
cd examples/blog
npm install
npm run dev
Making Changes
Code Style
Stati uses strict TypeScript and follows these conventions:
- ESLint for code linting
- Prettier for code formatting
- TypeScript strict mode
- Conventional Commits for commit messages
# Run linting
npm run lint
# Type checking
npm run typecheck
Testing
We maintain comprehensive test coverage:
# Run all tests
npm test
# Run tests with coverage
npm test -- --coverage
# Run specific test file
npm test -- packages/core/src/__tests__/build.test.ts
# Run tests in watch mode
npm test -- --watch
CI Pipeline
Every push and pull request triggers the CI workflow (.github/workflows/ci.yml):
- Dependency Install -
npm ciwith npm 11.5.1+ - Build Packages -
npm run build(core → cli → create-stati) - Run Tests - Vitest with coverage (perf tests excluded from CI)
- Upload Coverage - Reports sent to Codecov
The pipeline runs on Ubuntu with Node.js 22. Version commits from the publish workflow are automatically skipped.
Performance Benchmarks
Performance tests live in packages/core/test/perf/ and measure build speed across scenarios:
| Scenario | Description | Baseline |
|---|---|---|
| Cold Build | Clean slate, no cache | ~220ms median |
| Warm Build | No changes, high cache hit | ~50ms median |
| Incremental | Single file change | ~55ms median |
| Complex | Nested components, 111 pages | ~280ms median |
Run benchmarks locally:
# Run perf tests (excluded from regular test runs)
npx vitest run packages/core/test/perf
# Benchmarks use 100 generated pages with warmup runs
Baselines are defined in perf/baselines/benchmark.json with a 30% tolerance. Tests validate median duration and cache hit rates.
Build Metrics System
Stati includes a metrics system for debugging performance issues. Enable it via CLI:
stati build --metrics # Write JSON to .stati/metrics/
stati build --metrics --metrics-html # Also generate HTML report
stati build --metrics --metrics-detailed # Include per-page timings
Metrics include:
- Totals: Duration, peak RSS, heap usage
- Phases: Config load, content discovery, rendering, asset copy, etc.
- Counts: Pages rendered, cache hits/misses, assets copied
- ISG: Cache hit rate, skipped pages, rebuild reasons
- Per-page timing (when
--metrics-detailedis used)
Metrics are written to .stati/metrics/ as JSON files with timestamps. Use these to diagnose slow builds or cache inefficiencies.
Contributing Guidelines
Pull Request Process
- Create an Issue first for significant changes
- Fork and Branch from
main - Make Changes with tests
- Update Documentation if needed
- Test Thoroughly across packages
- Submit PR with clear description
PR Template
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Refactoring
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests passing
- [ ] Manual testing completed
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No breaking changes (or properly documented)
Commit Messages
We use Conventional Commits:
# Format: <type>(<scope>): <description>
# Examples:
feat(core): add ISG aging algorithm
fix(cli): resolve build output path issue
docs(readme): update installation instructions
test(core): add markdown processing tests
refactor(templates): improve Eta helper organization
Types:
feat- New featuresfix- Bug fixesdocs- Documentation changestest- Test additions/changesrefactor- Code refactoringperf- Performance improvementschore- Maintenance tasks
Development Guidelines
TypeScript Standards
// Use strict TypeScript
interface StatiConfig {
site: SiteConfig;
markdown?: MarkdownConfig;
// ... other properties
}
// Prefer interfaces over types
interface BuildResult {
pageCount: number;
buildTime: number;
cacheHitRate: number;
}
// Use JSDoc for public APIs
/**
* Build a Stati site
* @param config - Site configuration
* @param options - Build options
* @returns Build result with statistics
*/
export async function build(config: StatiConfig, options: BuildOptions = {}): Promise<BuildResult> {
// Implementation
}
Error Handling
Stati uses domain-specific error classes rather than a generic error class. Each error type captures relevant context for debugging:
// Domain-specific errors with context
export class ISGConfigurationError extends Error {
constructor(
public readonly code: ISGValidationError,
public readonly field: string,
public readonly value: unknown,
message: string,
) {
super(message);
this.name = 'ISGConfigurationError';
}
}
// Error with dependency chain for debugging
export class CircularDependencyError extends Error {
constructor(
public readonly dependencyChain: string[],
message: string,
) {
super(message);
this.name = 'CircularDependencyError';
}
}
// Self-documenting error messages
export class DuplicateBundleNameError extends Error {
constructor(duplicates: string[]) {
super(
`Duplicate bundleName(s) found in configuration: ${duplicates.join(', ')}. ` +
'Each bundle must have a unique bundleName.',
);
this.name = 'DuplicateBundleNameError';
}
}
Guidelines:
- Create domain-specific error classes (e.g.,
TemplateError,ISGConfigurationError) - Include relevant context as public readonly properties
- Provide actionable error messages that explain what went wrong and how to fix it
- Use error codes (enums) for structured error identification where appropriate
Performance Considerations
Stati uses several patterns to optimize build performance:
Module-level caches with clear functions:
// Cache expensive operations at module level
const templatePathCache = new Map<string, string | null>();
const templateContentCache = new Map<string, string | null>();
// Clear at the start of each build
export function clearTemplatePathCache(): void {
templatePathCache.clear();
templateContentCache.clear();
}
// Use cache in hot paths
export async function computeFileHash(filePath: string): Promise<string | null> {
const normalizedPath = filePath.replace(/\\/g, '/');
if (fileHashCache.has(normalizedPath)) {
return fileHashCache.get(normalizedPath) ?? null;
}
const hash = createSha256Hash(await readFile(filePath, 'utf-8'));
fileHashCache.set(normalizedPath, hash);
return hash;
}
Bounded caches with LRU eviction:
const escapeHtmlCache = new Map<string, string>();
const ESCAPE_CACHE_MAX_SIZE = 1000;
export function escapeHtml(text: string): string {
const cached = escapeHtmlCache.get(text);
if (cached !== undefined) return cached;
const result = text.replace(/[&<>"']/g, (char) => htmlEscapes[char] || char);
// Clear when full to prevent unbounded growth
if (escapeHtmlCache.size >= ESCAPE_CACHE_MAX_SIZE) {
escapeHtmlCache.clear();
}
escapeHtmlCache.set(text, result);
return result;
}
Parallel I/O operations:
// Batch file writes in dev mode for Windows filesystem performance
const uniqueDirs = [...new Set(pendingWrites.map((w) => dirname(w.outputPath)))];
await Promise.all(uniqueDirs.map((dir) => ensureDir(dir)));
await Promise.all(pendingWrites.map((w) => writeFile(w.outputPath, w.content, 'utf-8')));
Guidelines:
- Use module-level
Mapcaches with explicitclear()functions - Implement size limits on caches to prevent memory leaks
- Batch I/O operations with
Promise.allfor parallel execution - Normalize paths before using as cache keys (
/not\)
Release Process
Versioning
Stati follows Semantic Versioning:
- MAJOR - Breaking changes
- MINOR - New features (backward compatible)
- PATCH - Bug fixes (backward compatible)
Changesets
We use Changesets for version management:
# Create a changeset manually
npm run changeset
# Check pending changes
npm run changeset:status
# Auto-generate changesets from commits (used by CI)
npm run changeset:generate
# Preview version changes
npm run changeset:dry-run
Changeset messages should be a brief summary of the changes made:
---
'@stati/core': minor
'@stati/cli': patch
---
Add ISG aging algorithm for cache TTL management
Documentation
Writing Style
- Clear and concise explanations
- Code examples for every concept
- Step-by-step instructions
- Real-world use cases
Documentation Structure
# Page Title
Brief description of what this page covers.
## Section 1
Explanation with code example:
```javascript
// Code example
const example = 'value';
```
Subsection
More detailed information.
Best Practices
- Tip 1
- Tip 2
- Tip 3
Next Steps
Links to related documentation.
### API Documentation
Use TypeScript interfaces for API docs:
```typescript
/**
* Configuration for Stati sites
*/
export interface StatiConfig {
/** Site metadata and settings */
site: SiteConfig;
/** Markdown processing configuration */
markdown?: MarkdownConfig;
/** Template engine settings */
templates?: TemplateConfig;
}
Community
Getting Help
- GitHub Discussions - Ask questions and share ideas
- GitHub Issues - Report bugs and request features
Code of Conduct
We follow the Contributor Covenant:
- Be respectful and inclusive
- Be patient with newcomers
- Be constructive in feedback
- Be collaborative in discussions
Recognition
Contributors are recognized in:
- Changelog for each release
- Contributors section in README
- All Contributors bot acknowledgment
Getting Started Contributing
- Browse Issues - Look for “good first issue” labels
- Join Discussions - Introduce yourself and ask questions
- Start Small - Fix typos, add tests, improve docs
- Ask for Help - Don’t hesitate to ask for guidance
Good First Issues
- Documentation improvements
- Test coverage additions
- Error message improvements
- Example project enhancements
- Performance optimizations
Thank you for considering contributing to Stati! Every contribution, no matter how small, helps make Stati better for everyone. We’re excited to work with you and see what we can build together.
For questions about contributing, feel free to open a discussion on GitHub or reach out to the maintainers. We’re here to help and want to make contributing as smooth as possible for everyone.