Thank you for your interest in contributing to Polly! This guide will help you get started.
Polly (πολύς - "many") is a multi-execution-context framework for building Chrome extensions, PWAs, and worker-based applications with reactive state and cross-context messaging.
- Bun 1.3.0 or later
- Chrome or Edge browser for testing Chrome extensions
- Git
-
Clone the repository:
git clone <repository-url> cd polly
-
Install dependencies and set up CLI:
bun run setup
This command will:
- Install all dependencies
- Build the library with TypeScript declarations
- Link the
pollyCLI globally
-
Add CLI to your PATH (add to
~/.zshrcor~/.bashrc):export PATH="$HOME/.bun/bin:$PATH"
-
Verify setup:
polly help
polly/
├── src/ # Main framework source
├── tools/
│ ├── verify/ # Formal verification tool
│ ├── visualize/ # Architecture visualization tool
│ └── analysis/ # Static analysis
├── examples/ # Example applications (Chrome extensions, web apps, etc.)
│ ├── minimal/ # Simplest starting point
│ ├── todo-list/ # CRUD with formal verification
│ ├── full-featured/ # Complete Chrome extension showcase
│ ├── elysia-todo-app/ # Full-stack web app (Elysia + Bun)
│ ├── webrtc-p2p-chat/ # P2P chat with WebRTC
│ └── team-task-manager/ # Collaborative task management
├── docs/ # Documentation
└── specs/ # TLA+ specifications
-
Create a feature branch:
git checkout -b feature/your-feature-name
-
Make your changes
-
Run quality checks:
bun run check
This runs:
- TypeScript type checking
- Biome linter
- Unit tests
- Build verification
-
Test your changes:
# Run all tests bun run test # Run specific test suites bun run test:framework # E2E tests bun run test:watch # Watch mode
-
Format and lint:
bun run format # Format code bun run lint # Check linting bun run lint:fix # Auto-fix lint issues
bun run testUnit tests are located alongside source files with .test.ts suffix.
bun run test:framework # Run all E2E tests
bun run test:framework:ui # Run with Playwright UI
bun run test:framework:headed # Run in headed mode
bun run test:framework:debug # Run with debuggingE2E tests use Playwright to test real Chrome extension behavior.
// Unit test example
import { describe, test, expect } from "bun:test";
describe("MessageBus", () => {
test("sends messages", async () => {
const bus = getMessageBus("popup");
const result = await bus.send({ type: "PING" });
expect(result).toBeDefined();
});
});
// E2E test example
import { test, expect } from "@playwright/test";
test("state syncs between contexts", async ({ page, context }) => {
// Test implementation
});If the project ships a factory or composite entry point —
createMeshClient, createPeerRepoServer, any createX or
configureX helper — there must be a test that exercises that exact
entry point end-to-end. Hand-wired tests that bypass the factory are
useful as unit scaffolds; they are not a substitute.
0.27.0 of this package shipped a bug where createMeshClient omitted
the peerId option to new Repo(...). Automerge auto-generated a
random id, MeshNetworkAdapter stamped every outgoing envelope with
that auto-id as its senderId, and the remote receiver — whose
knownPeers was keyed by the mesh peer id the application had paired
against — silently dropped every message at the signature step. The
full test suite passed, including the browser e2e harness, because
tests/browser/mesh-webrtc.browser.ts wired the stack by hand and
passed an explicit peerId to new Repo(...). The test silently
compensated for the factory's gap. Every downstream consumer of the
factory — fairfox, any external application — saw real-world sync
break, with no signal from any tier of the suite that anything was
wrong.
tests/browser/mesh-client-roundtrip.browser.ts is the pattern to
mirror when adding a new factory: two clients built through the
public API with mutually-paired keyrings, a real WebRTC data channel,
a document round-trip. Run the test in two states before committing
it — green against your fix, red against the unfixed code — so you
know the test actually exercises the path it claims to.
A hand-wired test tells you the pieces work. A factory-path test tells you the assembly ships correctly. You need both.
# Build framework
bun run build
# Build library for publishing
bun run build:lib
# Production build
bun run build:prodTest your changes with the example extensions:
cd examples/full-featured
bun install
bun run buildThen load dist/ in Chrome.
- Use strict TypeScript settings
- Prefer type inference over explicit types
- Use
interfacefor public APIs,typefor internal - Never use type casting (
as) - use type guards instead
// ❌ Bad - using type casting
const value = stored.settings as Settings;
// ✅ Good - using type guard
function isSettings(value: unknown): value is Settings {
return value !== null && typeof value === "object" && "theme" in value;
}
if (isSettings(stored.settings)) {
settings.value = stored.settings;
}- Use Biome for formatting (configured in
biome.json) - 2-space indentation
- No semicolons (except where required)
- Single quotes for strings
- Files: kebab-case (
message-bus.ts) - Classes: PascalCase (
MessageRouter) - Functions: camelCase (
getMessageBus) - Constants: UPPER_SNAKE_CASE (
DEFAULT_TIMEOUT) - Types/Interfaces: PascalCase (
ExtensionMessage)
- Use JSDoc for public APIs
- Explain why, not what
- Keep comments up to date
/**
* Creates a message bus for the specified context.
*
* @param context - The extension context (popup, background, etc.)
* @returns A typed message bus instance
*/
export function getMessageBus<T>(context: string): MessageBus<T> {
// Implementation
}Follow the Conventional Commits specification:
type(scope): description
[optional body]
[optional footer]
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Code style changes (formatting, etc.)
- refactor: Code refactoring
- test: Adding or updating tests
- chore: Maintenance tasks
feat(state): add $persistedState primitive
Add a new state primitive that persists to storage without syncing.
Useful for local-only state that should survive reloads.
Closes #123
fix(message-bus): prevent duplicate message handlers
MessageRouter now tracks registered handlers to prevent duplicates
when the background script is executed multiple times.
-
Run all checks:
bun run check
-
Test your changes:
bun run test bun run test:framework -
Update documentation if needed
-
Add tests for new features
- Title: Use conventional commit format
- Description: Explain what, why, and how
- Link issues: Reference related issues
- Screenshots: Add for UI changes
- Breaking changes: Clearly document
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests pass
- [ ] E2E tests pass
- [ ] Manual testing completed
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] No breaking changes (or documented)- Use Preact signals for reactivity
- Lamport clocks for distributed consistency
- Chrome storage for persistence
- Ports for cross-context communication
- No
anytypes - No type casting (
as) - use type guards - Strict TypeScript configuration
- End-to-end type safety
- Zero Magic: Explicit, predictable behavior
- Type Safety First: Compile-time guarantees
- Developer Experience: Fast builds, great errors
- Framework Over Library: Opinionated patterns
- Formal Correctness: TLA+ for critical components
Critical components have TLA+ specifications in specs/:
# Start TLA+ environment
bun run tla:up
# Run verification
bun run tla:check
# Stop environment
bun run tla:downcd examples/full-featured
bun run verify # Run verification
bun run verify --setup # Generate config- README.md: High-level overview, quick start
- Code comments: Complex logic, non-obvious behavior
- JSDoc: Public APIs
- Examples: Working code samples
- CONTRIBUTING.md: Development guidelines
- Clear and concise
- Include code examples
- Keep up to date
- Test examples (ensure they work)
(For maintainers)
The project uses a single-package publishing model:
- Only
@fairfox/pollyis published to npm - Internal packages (verify, visualize, analysis) are marked
private: true - All tools are bundled into the main CLI
- Users get the complete framework and toolchain with one install
- Update version in
package.json - Update CHANGELOG.md
- Run full test suite:
bun run check bun run test:all
- Build for production:
bun run build:lib
- Publish to npm:
npm publish --access public
This publishes the framework with:
- Core runtime (state, message-bus, adapters)
- CLI with all commands (polly init, build, verify, visualize, etc.)
- Bundled verification and visualization tools
- TypeScript definitions
- Multi-platform support (Chrome extensions, PWAs, workers)
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [maintainer email]
- Be respectful and inclusive
- Welcome newcomers
- Focus on what's best for the community
- Show empathy towards others
Thank you for contributing! 🎉