Contributing Guide
How to contribute to Spry and help improve the project
TypeScript Style
General Principles
- Use TypeScript's type system fully
- Prefer immutability
- Write self-documenting code
- Keep functions small and focused
Naming Conventions
// Files: kebab-case
// contained-in-section.ts
// code-frontmatter.ts// Types/Interfaces: PascalCase
interface GraphEdge { ... }
type ExecutableTask = ...// Functions/Variables: camelCase
function buildGraph() { ... }
const taskRunner = ...// Constants: UPPER_SNAKE_CASE or camelCase
const DEFAULT_TIMEOUT = 120;
const typicalRules = [...];Type Annotations
// Prefer explicit return types for public functions
function parseCell(code: Code): ParsedCell {
// ...
}
// Use type inference for local variables
const result = parseCell(node); // Type is inferred
// Avoid `any` - use `unknown` if type is truly unknown
function processUnknown(value: unknown): void {
if (typeof value === "string") {
// Now TypeScript knows it's a string
}
}Avoid using any type. Use unknown when the type is truly unknown and perform type guards.
Import Organization
Standard library imports
import { join } from "std/path/mod.ts";External dependencies
import { unified } from "unified";
import { visit } from "unist-util-visit";Internal absolute imports
import { graph } from "@spry/axiom/graph.ts";Relative imports
import { helper } from "./helper.ts";
import type { MyType } from "./types.ts";Code Organization
Module Structure
// 1. Imports
import { ... } from "...";
// 2. Types/Interfaces
export interface MyInterface { ... }
export type MyType = ...;
// 3. Constants
const DEFAULT_VALUE = 42;
// 4. Helper functions (private)
function helperFunction() { ... }
// 5. Main exports (public)
export function mainFunction() { ... }
export class MainClass { ... }File Size
- Keep files under 300 lines when possible
- Split large files into focused modules
- One primary export per file
Function Design
// Good: Small, focused functions
function validateTask(task: Task): ValidationResult {
const errors = [];
if (!task.taskId()) errors.push("Missing task ID");
if (task.taskDeps()?.some(d => !isValidId(d))) {
errors.push("Invalid dependency");
}
return { valid: errors.length === 0, errors };
}// Avoid: Large functions doing multiple things
// Split into smaller, testable functionsError Handling
Use Result Types
// Prefer Result types over exceptions for expected failures
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function parseConfig(content: string): Result<Config, ParseError> {
try {
const parsed = JSON.parse(content);
return { ok: true, value: parsed as Config };
} catch (e) {
return { ok: false, error: new ParseError(e.message) };
}
}Prefer Result types over exceptions for expected failures to make error handling explicit.
Exceptions for Unexpected Errors
// Use exceptions for programming errors
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// Validate inputs at boundaries
function processFile(path: string): void {
if (!path) throw new Error("Path is required");
// ...
}Testing Patterns
Test File Naming
module.ts → module_test.ts
graph.ts → graph_test.tsTest Structure
import { assertEquals, assertThrows } from "std/assert/mod.ts";
import { describe, it } from "std/testing/bdd.ts";
describe("MyModule", () => {
describe("myFunction", () => {
it("should handle normal input", () => {
const result = myFunction("input");
assertEquals(result, "expected");
});
it("should throw on invalid input", () => {
assertThrows(
() => myFunction(""),
Error,
"Input required"
);
});
});
});Documentation
JSDoc Comments
/**
* Builds a semantic graph from an MDAST root.
*
* @param root - The MDAST root node
* @param rules - Edge rules to apply
* @returns A graph with discovered relationships
*
* @example
* ```ts
* const g = graph(root, typicalRules());
* console.log(g.edges.length);
* ```
*/
export function graph(
root: Root,
rules: EdgeRule[]
): Graph {
// ...
}When to Document
- All public exports
- Complex algorithms
- Non-obvious behavior
- API boundaries
When NOT to Document
- Self-explanatory code
- Internal implementation details
- Obvious getters/setters
Performance
Prefer Generators for Large Collections
// Good: Lazy evaluation
function* findMatches(nodes: Node[]): Generator<Node> {
for (const node of nodes) {
if (matches(node)) yield node;
}
}// Use when needed
for (const match of findMatches(largeArray)) {
// Process one at a time
}Avoid Unnecessary Allocations
// Avoid: Creates new array each call
function getIds(tasks: Task[]): string[] {
return tasks.map(t => t.taskId());
}// Better: Reuse or lazy evaluate if appropriate
function* getIds(tasks: Task[]): Generator<string> {
for (const task of tasks) {
yield task.taskId();
}
}Formatting
Use Deno's built-in formatter:
deno fmtKey settings (in deno.jsonc):
- Line width: 80
- Indent: 2 spaces
- Single quotes for strings
- Trailing commas
Linting
Use Deno's linter:
deno lintAddress all warnings before committing.
Commit Messages
Follow conventional commits:
type(scope): subject
body (optional)
footer (optional)Types
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation |
refactor | Code change (no new feature or fix) |
test | Adding tests |
chore | Maintenance |
Examples
feat(axiom): add support for nested dependencies
fix(cli): handle missing file gracefully
docs: update installation instructions
refactor(edge): simplify rule pipelineGetting Help
GitHub Issues
Check existing issues or create new ones.
Discussions
Ask questions in GitHub Discussions.
Documentation
Browse the documentation for guides and API reference.
Code of Conduct
All contributors are expected to:
- Be respectful and considerate
- Welcome newcomers
- Give and receive constructive feedback gracefully
- Focus on what's best for the community
- Show empathy towards others
License
By contributing to Spry, you agree that your contributions will be licensed under the project's MIT License.
Thank You
Every contribution, no matter how small, helps make Spry better for everyone. We appreciate your time and effort!
How is this guide?
Last updated on