Thank you for your interest in contributing to Juniper! This document provides guidelines and information for contributors.
Please be respectful and inclusive in all interactions. We are committed to providing a welcoming and harassment-free experience for everyone. Be kind, constructive, and professional in your communications.
Before contributing, ensure you have:
- Deno v2.x or later installed
- Git for version control
- A code editor (VS Code with the Deno extension is recommended)
- Docker (optional, for OpenTelemetry development)
-
Fork the repository on GitHub
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/juniper.git cd juniper -
Install dependencies:
deno install
-
Run the example application:
deno task dev
-
Run tests to verify your setup:
deno task test
juniper/
├── src/ # Core framework source code
│ ├── mod.ts # Main module exports
│ ├── build.ts # Build system
│ ├── server.tsx # Server-side rendering
│ ├── client.tsx # Client-side hydration
│ ├── dev.ts # Development server
│ └── utils/ # Utility modules
├── example/ # Example application
│ ├── routes/ # Route files
│ ├── services/ # Data services
│ └── components/ # Reusable components
├── templates/ # Project templates
│ ├── minimal/ # Minimal starter template
│ ├── tailwindcss/ # TailwindCSS template
│ └── tanstack/ # TanStack Query template
├── docs/ # Documentation
├── deno.json # Workspace configuration
└── CONTRIBUTING.md # This file
The repository uses a Deno workspace. Run the example application:
# Run the example in development mode
deno task dev
# Run a specific template in development mode
deno task dev:minimal
deno task dev:tailwindcss
deno task dev:tanstack# Run all Juniper core tests
deno task test
# Run example tests
deno task test:example
# Run template tests
deno task test:minimal
deno task test:tailwindcss
deno task test:tanstack
# Run tests with coverage
deno task test --coverage# Run all checks (type check, lint, format check)
deno task check
# Individual commands
deno check # Type check
deno lint # Lint
deno fmt --check # Check formatting
deno fmt # Auto-format- Use TypeScript for all source files
- Prefer explicit types for function parameters and return types
- Use
interfacefor object types,typefor unions and intersections - Avoid
any- useunknownand narrow types when needed - Use descriptive names for variables and functions
// Good
interface User {
id: string;
name: string;
email: string;
}
function getUser(id: string): Promise<User | null> {
// implementation
}
// Avoid
function getUser(id: any): any {
// implementation
}Organize imports in this order:
- Standard library imports (
@std/*) - Third-party imports (npm packages, JSR packages)
- Internal framework imports (
@udibo/juniper/*) - Relative imports (local files)
Use blank lines to separate groups:
import { assertEquals } from "@std/assert";
import { describe, it } from "@std/testing/bdd";
import { Hono } from "hono";
import React from "react";
import { HttpError } from "@udibo/juniper";
import { userService } from "./services/user.ts";- Use Deno's built-in formatter:
deno fmt - Maximum line length: 80 characters (enforced by formatter)
- Use 2 spaces for indentation
- Always include trailing commas in multi-line arrays/objects
- Write tests for all new functionality
- Place test files next to the code they test with a
.test.tsextension - Use descriptive test names that explain what's being tested
- Test both success and error cases
import { assertEquals, assertThrows } from "@std/assert";
import { describe, it } from "@std/testing/bdd";
import { myFunction } from "./my-function.ts";
describe("myFunction", () => {
it("should return expected value for valid input", () => {
const result = myFunction("valid");
assertEquals(result, "expected");
});
it("should throw error for invalid input", () => {
assertThrows(
() => myFunction(""),
Error,
"Input cannot be empty",
);
});
});- Use
describeblocks to group related tests - Use
itortestfor individual test cases - Use
beforeEachandafterEachfor setup/cleanup - Keep tests focused and independent
Use Deno's standard testing utilities:
import { spy, stub } from "@std/testing/mock";
import { FakeTime } from "@std/testing/time";
describe("time-dependent tests", () => {
it("should handle mocked time", () => {
using time = new FakeTime();
// Test with controlled time
time.tick(1000);
});
});Use descriptive branch names:
feature/add-user-authenticationfix/loader-error-handlingdocs/update-routing-guiderefactor/simplify-build-process
We use Conventional Commits for PR titles. When your PR is merged, the squash commit message will use your PR title, so it must follow this format:
<type>(<scope>): <description>
Types:
| Type | Description | Version Bump |
|---|---|---|
feat |
A new feature | Minor |
fix |
A bug fix | Patch |
docs |
Documentation only changes | None |
style |
Code style changes (formatting, semicolons) | None |
refactor |
Code changes that neither fix bugs nor add features | None |
perf |
Performance improvements | Patch |
test |
Adding or updating tests | None |
build |
Build system or external dependency changes | None |
ci |
CI configuration changes | None |
chore |
Other changes that don't modify src or test files | None |
Scope (optional): The area of the codebase affected (e.g., router,
build, server).
Examples:
feat: add server-side caching for loaders
fix(router): handle undefined params gracefully
docs: update installation instructions
feat!: change loader API signature
Breaking Changes:
For breaking changes, add ! after the type/scope or include BREAKING CHANGE:
in the PR description body:
feat!: change loader return type to Response
BREAKING CHANGE: Loaders now return Response objects instead of plain data.
Individual commits within a PR don't need to follow the convention (they will be squashed). Write clear, descriptive messages:
- Use present tense: "Add feature" not "Added feature"
- Start with a verb: "Fix", "Add", "Update", "Remove", "Refactor"
- Keep the subject line under 50 characters
Include in your pull request:
- Summary - What does this PR do?
- Changes - List of specific changes made
- Testing - How was this tested?
- Related Issues - Link to any related issues
- Ensure all CI checks pass
- Request review from maintainers
- Address all feedback
- Maintainer will merge when approved
- mod.ts - Main exports for the framework
- build.ts - esbuild configuration and bundle generation
- server.tsx - Server-side rendering and route handling
- client.tsx - Client-side hydration and routing
- dev.ts - Development server with hot reload
The build system uses esbuild with the Deno plugin:
- Scans the
routes/directory for route files - Generates
main.ts(server) andmain.tsx(client) entry points - Bundles client code with code splitting
- Outputs to
public/build/
Juniper uses a clear server/client separation:
-
Server routes (
.tsfiles) - Run only on the server- Loaders, actions, middleware
- Direct database access
- Secret/environment variable access
-
Client routes (
.tsxfiles) - Run on both server (SSR) and client- Loaders, actions, middleware
- React components
- Client-side navigation
- Serializable data only
Data flows from server loaders to client components through serialization.
Releases are fully automated using semantic-release:
- PRs are merged to
mainusing squash merge - semantic-release analyzes commit messages (from PR titles)
- Version is automatically determined based on commit types
- CHANGELOG.md is updated automatically
- Package is published to JSR
- GitHub release is created with release notes
Version bumps are determined by PR titles:
fix:→ Patch release (0.1.x)feat:→ Minor release (0.x.0)feat!:orBREAKING CHANGE:→ Minor release during v0.x, Major after v1.0
- Issues - Report bugs or request features on GitHub Issues
- Discussions - Ask questions on GitHub Discussions
- Documentation - Check the docs folder
Thank you for contributing to Juniper!