Contributing Guide — GeoLeaf.js
Thank you for your interest in contributing to GeoLeaf.js. This guide covers what is needed to get started.
Applies to: @geoleaf/core v3.x
Table of contents
- Code of Conduct
- How to contribute
- Development setup
- Code standards
- Pull request process
- Architecture and modules
- Tests
- Documentation
- Versioning
Code of Conduct
Our commitments
GeoLeaf.js is committed to an open, welcoming and inclusive community. All contributors are expected to:
- Respect every participant, whatever their level of experience
- Accept constructive criticism gracefully
- Focus on what is best for the community
- Communicate professionally and courteously
Unacceptable behaviour
- Sexualised language or imagery, unwanted attention
- Trolling, insulting or derogatory comments
- Public or private harassment
- Publishing private information without permission
How to contribute
Types of contribution
Several kinds of contribution are welcome.
Bug reports
- Use the GitHub issue template
- Include a clear description of the problem
- Provide steps to reproduce
- Add screenshots where relevant
- State the GeoLeaf.js version
Feature suggestions
- Open an issue with the "enhancement" label
- Describe the use case and the problem being solved
- Propose a solution or alternatives
- Stay open to discussion
Documentation
- Typo corrections
- Clarifications
- New guides or tutorials
Code
- Bug fixes
- New features
- Performance optimisations
- Refactoring
Development setup
Prerequisites
# Node.js >= 22.13.0 (see `engines` in the root package.json)
node --version # v22.13.0 or higher
# npm >= 11.x (workspaces support required)
npm --versionInstallation
# 1. Fork the repository on GitHub
# 2. Clone your fork
git clone https://github.com/VOTRE-USERNAME/geoleaf-js.git
cd geoleaf-js
# 3. Add the upstream remote
git remote add upstream https://github.com/geoleaf/geoleaf-js.git
# 4. Install dependencies (npm workspaces — installs every package)
npm install
# 5. Check the installation
npm testBranch structure
main # Stable production
├── feature/* # New features
├── bugfix/* # Bug fixes
├── hotfix/* # Urgent production fixes
└── release/* # Release preparationActive development happens on main; pull requests are squash-merged directly into main.
Git workflow
# 1. Create a branch from main
git checkout main
git pull upstream main
git checkout -b feature/my-new-feature
# 2. Make your changes
# ... code, test, commit ...
# 3. Update from upstream
git fetch upstream
git rebase upstream/main
# 4. Push to your fork
git push origin feature/my-new-feature
# 5. Open a pull request on GitHubCode standards
TypeScript style
GeoLeaf.js uses strict TypeScript (ES2022), ESLint and Prettier for a uniform codebase.
# Lint the code
npm run lint
# Fix automatically
npm run lint:fix
# Format with Prettier
npm run format
# Type checking
cd packages/core && npm run typecheckNaming conventions
// Variables and functions: camelCase
const mapInstance = map;
function createMarker() {}
// Classes and interfaces: PascalCase
class LayerManager {}
interface POIConfig {}
// Constants: UPPER_SNAKE_CASE
const MAX_ZOOM_LEVEL = 18;
const DEFAULT_CONFIG = {};
// Modules: kebab-case
// file: layer-manager.ts
// directory: feature-info/Code organisation
/**
* Structure of a GeoLeaf v2 module.
*/
// 1. Imports
import { dependency } from "./module.js";
// 2. Constants
const CONSTANT_VALUE = "value";
// 3. Internal state
let _initialized = false;
// 4. Private helper functions
function _helperFunction() {}
// 5. Public functions (exports)
/**
* Public function description.
* @param config - Module configuration
* @returns true on success
*/
export function publicFunction(config: Config): boolean {
// Implementation
return true;
}TSDoc and inline documentation
Every public function must carry complete TSDoc.
/**
* Creates a POI marker on the map with custom options.
*
* @param mapId - Target map container ID
* @param poi - POI object with coordinates and metadata
* @param poi.lnglat - [longitude, latitude] pair (MapLibre GL JS convention)
* @param poi.title - Display title
* @param options - Additional options
* @param options.draggable - Whether the marker is draggable
*
* @returns The created marker instance
*
* @throws {TypeError} If mapId is not a valid string
* @throws {ValidationError} If coordinates are out of valid range
*
* @example
* const marker = createPoiMarker('my-map', {
* lnglat: [2.3522, 48.8566],
* title: 'Paris'
* }, { draggable: true });
*
* @since 2.0.0
*/
export function createPoiMarker(mapId: string, poi: POIConfig, options: MarkerOptions = {}) {
// Implementation
}Modular architecture v2.0
Modularisation principles
- Single Responsibility Principle — one module, one responsibility
- Modules under 500 lines — soft limit; split beyond it (hard limit: 700)
- Explicit exports — always name exports
- Minimal dependencies — avoid tight coupling
- Pure ESM — no
require(), no CommonJS syntax
Directory structure (packages/core/src/)
src/
├── bundle-esm-entry.ts ← ESM entry point (27 named exports)
├── kernel-exports.ts ← Kernel surface, re-exported as a block by the entry
├── app/ ← boot, module registry, helpers
├── adapters/maplibre/ ← The ONLY place that imports maplibre-gl
├── contracts/ ← Shared cross-module interfaces
├── capabilities/ ← In-core capabilities, each with its own install.ts
│ (taxonomy, filter, cluster, legend, permalink, offline…)
├── api/ ← Public `geoleaf.*.ts` facades (GeoLeaf.* namespace), no logic
├── globals/ ← `globals.*.ts` — mounts kernel facades on window.GeoLeaf
├── kernel/ ← The kernel: api/, config/, geojson/, ui/, security/, shared/…
├── utils/ ← log, errors, constants, performance, general
├── css/ · lang/ · presets/ ← Source styles, translations, preset manifests
└── global.d.ts ← Global namespace typingThe exhaustive, generated tree lives in
docs/reference/ARBORESCENCE_QUALIFIEE.md(npm run docs:tree); the listing above is an orientation summary.
Example: create a new capability
// packages/core/src/capabilities/mon-module/mon-module.ts
/**
* @description Module description
*/
import { Log } from "../../utils/log/index.js";
import { Validators } from "../../api/geoleaf.validators.js";
// Constants
const MODULE_NAME = "MonModule";
// Internal state
let _initialized = false;
/**
* Initializes the module with the provided configuration.
*
* @param config - Module configuration object
* @returns true on success
*/
export function init(config: ModuleConfig): boolean {
try {
Validators.validateConfig(config);
_initialized = true;
return true;
} catch (error) {
Log.error(`${MODULE_NAME} init failed`, error);
return false;
}
}
/**
* Main feature entry point.
*
* @param data - Data to process
* @returns Processed result
*/
export function mainFeature(data: unknown): unknown {
if (!_initialized) {
throw new Error(`${MODULE_NAME} not initialized`);
}
// Implementation
}Pull request process
Pre-submission checklist
Code Quality & Style
- [ ] Code style — lint and format pass (
npm run lint,npm run format) - [ ] TypeScript —
npm run typecheckreports no error - [ ] Complexity — no function over 80 LOC (100 LOC maximum, with justification)
- [ ] Duplication — no duplicated code (use shared modules)
- [ ] Naming — conventions respected (camelCase, PascalCase, UPPER_SNAKE_CASE)
Security Checklist
- [ ] XSS prevention — no use of
innerHTMLwithout sanitization- Use
GeoLeaf.DOMSecurity.setSafeHTML()ortextContent - Verify with
node scripts/audit-innerhtml.cjs
- Use
- [ ] Input validation — every user input validated
JSON.parse()wrapped in try/catch- No
Object.assign()with untrusted data (prototype pollution)
- [ ] CSRF protection — CSRF tokens for forms and mutations
- [ ] No-plugin-in-core — no reference to
@geoleaf-plugins/*insidepackages/core/src/
Tests & Coverage
- [ ] Tests — all tests pass (
npm test) - [ ] Coverage — at least 75% on new code (
npm run test:coverage) - [ ] Edge cases — tests for null, undefined and empty values
- [ ] New tests — added for new features
Documentation
- [ ] TSDoc — complete on every public function and export
@paramwith types,@returns,@throwswhere applicable
- [ ] CHANGELOG — entry added with a description
- [ ] Examples — code examples provided for a new feature
Architecture & Performance
- [ ] Memory leaks — none introduced
- Check event listener cleanup
- Check that setTimeout/setInterval are cleared
- [ ] Bundle size — no significant increase (over 5%)
npm run size(hard budget: build fails above 300 KB gz, warning above 270 KB gz)
- [ ] Boot order —
globals.*.tsnot modified without checking the B1→B11 sequence
Git & CI/CD
- [ ] Commits — clear messages (Conventional Commits)
- [ ] Branch — up to date with
upstream/main - [ ] CI/CD — pipeline passes (lint, typecheck, tests, build)
- [ ] No warnings — no ESLint or TypeScript warning
Pull request template
## Description
Short description of the changes.
## Type of change
- [ ] Bug fix (non-breaking change that fixes a problem)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that breaks compatibility)
- [ ] Documentation (documentation changes only)
- [ ] Refactoring (change that neither adds a feature nor fixes a bug)
- [ ] Performance (performance improvement)
- [ ] Tests (test additions or corrections)
## Motivation and context
Why is this change needed? Which problem does it solve?
## How to test
1.
2.
3.
## Checklist
- [ ] My code follows the project style
- [ ] I have self-reviewed my code
- [ ] I have updated the documentation
- [ ] My changes generate no warnings
- [ ] I have added tests proving that my fix works
- [ ] All tests pass locally
## Related issues
Fixes #(issue_number)Code review
Pull requests are reviewed against these criteria:
- Code quality — conventions, readability, performance
- Tests — adequate coverage, no regression
- Documentation — complete TSDoc, examples where needed
- Architecture — established patterns, well-decoupled modules, no-plugin-in-core
Merge process
- Review approved by at least one maintainer
- CI/CD green (typecheck, lint, tests, build)
- Conflicts resolved against main
- Squash merge into main (cleaned-up commits)
Tests
Test frameworks
- Vitest 3 — unit and integration tests (ESM, Istanbul provider)
- Playwright — end-to-end tests (Chromium)
Test commands
# All unit tests (through Turborepo)
npm test
# Unit tests (Vitest directly, inside packages/core)
cd packages/core && npm run test:vitest
# Tests with coverage
npm run test:coverage
# E2E tests (requires a built deploy/)
npm run test:e2e
# Specific tests (filter on the file path)
cd packages/core && npx vitest run __tests__/legendWriting tests
Unit tests (Vitest)
// packages/core/__tests__/capabilities/mon-module.test.ts
import { init, mainFeature } from "../../src/capabilities/mon-module/mon-module.js";
describe("MonModule", () => {
describe("init()", () => {
it("should initialize with valid config", () => {
const config = { key: "value" };
const result = init(config);
expect(result).toBe(true);
});
it("should reject invalid config", () => {
const result = init(null as any);
expect(result).toBe(false);
});
});
describe("mainFeature()", () => {
beforeEach(() => {
init({ key: "value" });
});
it("should process data correctly", () => {
const data = { test: "data" };
const result = mainFeature(data);
expect(result).toBeDefined();
});
it("should throw if not initialized", () => {
expect(() => mainFeature({})).toThrow();
});
});
});E2E tests (Playwright)
// e2e/mon-feature.spec.ts
import { test, expect } from "@playwright/test";
test.describe("My Feature", () => {
test.beforeEach(async ({ page }) => {
// Each spec targets its own deploy variant (ports 8766-8768)
await page.goto("http://localhost:8766");
await page.waitForSelector("#geoleaf-map");
});
test("should render the map", async ({ page }) => {
const canvas = await page.locator("canvas.maplibregl-canvas");
await expect(canvas).toBeVisible();
});
});Test coverage
Target — at least 75% overall coverage (reached in v2.0.0)
- Critical functions — 100% coverage recommended
- Utilities — above 90% coverage
- UI components — above 70% coverage
Documentation
Documentation types
- Inline TSDoc — in the source code (mandatory for public APIs)
- Guides — documentation under
packages/core/docs/ - API reference — generated from TSDoc by TypeDoc (
npm run docs:api) - Examples — JSON profiles under
profiles/and demos underpackages/core/demo/
Updating the documentation
# 1. Update the TSDoc in the source code
# 2. Regenerate the API reference
cd packages/core && npm run docs:api
# 3. Update the relevant guide under docs/
# 4. Update packages/core/docs/CHANGELOG.md (the [Unreleased] section)Versioning
GeoLeaf.js follows Semantic Versioning 2.0.0.
Format: MAJOR.MINOR.PATCH
- MAJOR — incompatible changes (breaking changes)
- MINOR — backwards-compatible new features
- PATCH — backwards-compatible bug fixes
Examples
2.0.0 → 2.0.1 # Bug fix
2.0.0 → 2.1.0 # New feature
2.x.x → 3.0.0 # Breaking changeCommit messages
Format: Conventional Commits
# Format
<type>(<scope>): <short description>
[optional body]
[optional footer]Commit types
feat— new featurefix— bug fixdocs— documentation onlystyle— formatting, missing semicolons, and similarrefactor— refactoring without behaviour changeperf— performance improvementtest— test additions or correctionschore— maintenance, build, dependencies
Examples
# Feature
git commit -m "feat(legend): add template caching system"
# Bug fix
git commit -m "fix(layers): correct point positioning on zoom"
# Breaking change
git commit -m "feat(api)!: change init signature to accept options object
BREAKING CHANGE: init() now requires options object instead of individual parameters"Best practices
Performance
- Payload — never import a capability (
src/capabilities/*) from the kernel; that is what makes it undetachable from the bundle.npm run size:examplechecks this against the sourcemaps - Debounce/throttle — on frequent events (scroll, resize, input)
- Cache — cache expensive results
- Memory management
- Clean up event listeners in destroy()
- Clear setTimeout/setInterval
- Avoid circular references (use WeakMap where needed)
Security
XSS Prevention
// FORBIDDEN
element.innerHTML = userInput;
// CORRECT
GeoLeaf.DOMSecurity.setSafeHTML(element, userInput);
element.textContent = userText;Input Validation
// JSON.parse with error handling
try {
const config = JSON.parse(jsonString);
if (!validateSchema(config)) {
throw new Error("Invalid schema");
}
} catch (e) {
Log.error("Parse error:", (e as Error).message);
config = DEFAULT_CONFIG;
}Prototype Pollution Prevention
// FORBIDDEN
const merged = Object.assign({}, baseConfig, userConfig);
// CORRECT
const ALLOWED_KEYS = ["id", "label", "data"];
const safe: Record<string, unknown> = {};
Object.keys(userConfig).forEach((key) => {
if (ALLOWED_KEYS.includes(key) && key !== "__proto__" && key !== "constructor") {
safe[key] = (userConfig as any)[key];
}
});
const merged = { ...baseConfig, ...safe };Security Tools
# Audit XSS vulnerabilities
node scripts/audit-innerhtml.cjs
# Check dependencies
npm audit
# Validate code
npm run lintMaintainability
- DRY — Don't Repeat Yourself
- KISS — Keep It Simple, Stupid
- YAGNI — You Aren't Gonna Need It
- Documentation — explain the "why", not the "how"
Need help?
Resources
- Documentation —
packages/core/docs/ - GitHub issues — Issues
- Contact — Mattieu Pottier, contact@geoleaf.dev
Licensing
License Header
All TypeScript/JavaScript files contributed to GeoLeaf Core must include the MIT license header:
/*!
* GeoLeaf Core
* © 2026 Mattieu Pottier
* Released under the MIT License
* https://geoleaf.dev
*/Important: this header must be placed before all other code and comments in the file.
License Agreement
By contributing to GeoLeaf Core, you agree that:
- Your contributions are licensed under the MIT License
- You have the right to license your contributions
- Your contributions do not infringe on any third-party rights
- You understand that the core and every plugin are MIT-licensed and independently versioned
See the LICENSE file for the complete license text.
Thank you for contributing to GeoLeaf.js.
Every contribution makes GeoLeaf.js a better tool for everyone.
