Thanks for taking the time to contribute. This doc covers the local dev setup, the conventions the project follows, and the workflow for shipping a change.
File name: GitHub picks up
CONTRIBUTING.md(with the-ING) to surface it on the repo sidebar and link it from issue/PR templates. If you see docs referring toCONTRIBUTE.md, they mean this file.
- Node 20+ (the package's
enginesfield; CI runs Node 20 exclusively) - npm (the lockfile is
package-lock.json— stick with npm)
git clone https://github.com/iannbing/react-simple-tree-menu.git
cd react-simple-tree-menu
npm ci # library deps
npm --prefix docs ci # docs-site deps (separate package.json)All run from the repo root.
| Script | What it does |
|---|---|
npm test |
Run the full Vitest suite (characterization + forward + unit + API contract). |
npm run test:watch |
Vitest in watch mode. |
npm run test:bench |
The walk/render-cost benchmarks. |
npm run typecheck |
tsc --noEmit — strict, exactOptionalPropertyTypes: true. |
npm run lint |
ESLint 9 flat config. |
npm run format / format:check |
Prettier. |
npm run build |
tsup (ESM + CJS + .d.ts) + PostCSS for the stylesheet. |
npm run check:api |
Diffs dist/index.d.ts against the v1 fixture; fails on any unlisted export/prop change. Runs after npm run build. |
npm run check:size |
size-limit; budget is 3.5 KB minified + brotli. |
npm run storybook |
Local-only Storybook 8 on port 9001 (dev/QA; not deployed). |
npm run docs:dev |
Starlight docs site at http://localhost:4321/react-simple-tree-menu/. Hot-reloads on library source via a Vite alias — no npm run build needed first. |
npm run docs:build / docs:preview |
Production build + preview of the docs site. |
- Long-lived branches:
master(stable, what npm ships from) anddevelopment(integration). - Feature branches:
feat/<topic>,fix/<topic>,ci/<topic>, etc. Branch offdevelopment. - PRs target
development. Maintainers promotedevelopment→masterwhen ready for a release. - Keep PRs focused. One topic, one reviewable diff.
- For non-trivial changes, open an issue first so we can agree on scope before you write code.
The log follows Conventional Commits with these types:
feat— user-facing additionfix— bug fixperf— performance work without behavior changerefactor— internal restructuring, no behavior changetest— tests onlydocs— README / CHANGELOG / docs site / code commentschore— tooling, dependenciesci— GitHub Actions / workflow changesstyle— cosmetic (formatting, whitespace)
Optional scope in parens: feat(api): …, fix(styles): …, docs(site): …, ci(docs): ….
First line ≤ 72 chars. Body wraps at 72. Explain the why, not the what — the diff shows what.
Don't append a Co-Authored-By trailer unless the commit genuinely has two humans on it.
The project was rewritten under strict TDD (see SPEC.md for the behavioral contract):
- Characterization tests (
src/__tests__/characterization.test.tsx) encode the public behavior. They must stay green across every commit. - Forward tests cover additive v2 behavior (ARIA, SSR, API contract).
- Unit tests sit next to each pure module (
src/tree/*.test.ts).
If you're changing behavior, a characterization test should have to move or be added. If nothing in the test suite reacts to your change, that's a signal the change isn't covered — add a test before the implementation.
The rendered DOM follows the WAI-ARIA tree pattern with roving tabindex. When touching rendering:
- Preserve
role="tree"on the container androle="treeitem"on each item. - Keep
aria-level,aria-setsize,aria-posinset,aria-expanded,aria-selectedin sync with state. - Every interactive element needs a visible
:focus-visibleindicator. - Respect
prefers-reduced-motion— new transitions/animations must inherit the global kill-switch indocs/src/styles/docs.cssor declare their own@mediaguard.
- Runtime
dependenciesmust stay empty ("dependencies": {}). React is the only peer. - The brotli-minified ESM bundle must stay under 3.5 KB (
npm run check:size). - If a feature needs a new runtime dep, the PR has to make the case in the description.
v2.0.0 committed to a public API contract. scripts/check-api-contract.mjs runs after each build and diffs dist/index.d.ts against test-fixtures/api-v1.d.ts.
- Any new export or prop needs an entry in
test-fixtures/api-v2-removals.jsonunderaddedExportsoraddedProps. CI fails without it. - Any removal needs an entry under
removedExportsorremovedProps, with a reason. - No renames without discussion — breaks consumer upgrade paths.
CI runs the suite on Node 20 × React 16.14.0, 17.0.2, 18.3.1, 19.0.0. Locally you can only hit the React version in devDependencies (currently 18.3). If a PR touches React-API surfaces (hooks, rendering), let CI catch the matrix and react to the failures.
Two known caveats:
- React 16/17 use
@testing-library/react@^12which doesn't exportrenderHook. The hook test files (src/tree/use-*.test.ts) include a localrenderHookshim built onrender()— portable across RTL versions. Preserve that pattern when adding new hook tests. - React 19 + RTL v16 + fake timers +
userEvent.typecan deadlock. For debounce/search tests, usefireEvent.changeinstead ofuser.type(seesrc/__tests__/render-props-contract.test.tsxfor the pattern).
Releases are branch-driven — the long-lived branch that receives the merge dictates what gets published.
- RC (release candidate): every push to
developmenttriggers.github/workflows/release.yml'srelease-rcjob. It runs all gates, bumps the version vianpm-version-suffix(2.0.0→2.0.0-rc.N),npm publish --tag next --provenance, then pushes the bump commit + tag back todevelopment. Consumersnpm install react-simple-tree-menu@nextto try it. - Stable: every push to
mastertriggers therelease-stablejob. It strips any-rc.Nsuffix that RC runs left on package.json, runs gates, andnpm publish --provenance(no dist-tag → becomeslatest). Idempotent: if the clean version is already on npm (e.g. master got a doc-only commit), the publish step skips cleanly. A manualworkflow_dispatchon the same workflow takes a version input as an escape hatch for off-cycle / hot-fix publishes. - Docs site: a separate
docs.ymlworkflow publishes the Starlight site to GitHub Pages only on push tomaster(path-filtered todocs/**,src/**, or the workflow file). Merging todevelopmentdoes not publish docs. - Publishing auth is npm Trusted Publishing (OIDC) — no
NPM_TOKENsecret is set. Provenance attestation is on for every publish. npm's "Publishing access" is locked to "Require 2FA and disallow tokens," so the only path to publish is through this workflow's trusted OIDC exchange.
Starting a new minor/major cycle: bump the base version manually in a PR to development (npm version 2.1.0 --no-git-tag-version) — the next merge triggers an RC on the new base (2.1.0-rc.1). Stable ships when that RC lands on master.
- Source lives under
docs/— Astro Starlight + React islands. - The docs site imports
react-simple-tree-menuvia a Vite alias that points at the local library source (not the published package). That's the hot-reload shortcut; the alias is indocs/astro.config.mjsif you need to see where the resolution happens. docs.ymlpublishes the built site to GitHub Pages on every push tomaster(path-filtered todocs/**,src/**, and the workflow file itself). The workflow also idempotently flips the Pages source to "workflow build" if it's still on the legacy branch source.
- Don't introduce runtime dependencies.
- Don't commit
PLAN.mdorSPEC.md— they're author working documents, local-only. - Don't skip hooks (
--no-verify) or bypass signing without a reason. - Don't force-push to
masterordevelopment. - Don't add
console.logor leftover debug statements — ESLint catches some, not all.
Open an issue with the question label, or comment on the PR you're working against. If you're proposing a larger refactor, sketch the intent in an issue before investing the implementation time — it's a lot cheaper to disagree about scope at the issue stage than at the PR stage.