Skip to content
LexBuild
On this page

Contributing

LexBuild is open source and welcomes contributions. This guide covers setting up a development environment, understanding the codebase, and submitting changes.

GitHub repository: github.com/chris-c-thomas/LexBuild

Prerequisites

  • Node.js >= 22 LTS — check with node --version
  • pnpm >= 10 — check with pnpm --version

Setup

Clone the repository, install dependencies, and build all packages:

git clone https://github.com/chris-c-thomas/LexBuild.git
cd LexBuild
pnpm install
pnpm turbo build

Turborepo builds packages in dependency order: core first, then usc, ecfr, and fr in parallel, then cli last.

Verify everything is working:

pnpm turbo test && pnpm turbo lint && pnpm turbo typecheck

Monorepo Structure

lexbuild/
├── packages/
│   ├── core/     # @lexbuild/core -- XML parsing, AST, Markdown rendering
│   ├── usc/      # @lexbuild/usc -- U.S. Code converter and downloader
│   ├── ecfr/     # @lexbuild/ecfr -- eCFR converter and downloader
│   ├── fr/       # @lexbuild/fr -- Federal Register converter and downloader
│   └── cli/      # @lexbuild/cli -- CLI binary
├── apps/
│   ├── astro/    # Web app (Astro 6, SSR)
│   └── api/      # Data API (Hono, SQLite)
├── fixtures/     # Test fixtures
├── downloads/    # Downloaded XML (gitignored)
└── scripts/      # Deploy and ops scripts

Source packages (usc, ecfr, fr) depend only on core, never on each other. The CLI depends on all four packages.

Common Commands

CommandDescription
pnpm turbo buildBuild all packages
pnpm turbo testRun all tests
pnpm turbo lintLint all packages
pnpm turbo typecheckType-check all packages
pnpm turbo devWatch mode (rebuild on change)
pnpm formatAuto-format with Prettier
pnpm format:checkCheck formatting without modifying files

Use --filter to target a specific package:

pnpm turbo build --filter=@lexbuild/core
pnpm turbo test --filter=@lexbuild/usc
pnpm turbo lint --filter=@lexbuild/cli

Code Standards

LexBuild enforces strict coding standards across all packages:

  • TypeScript strict modestrict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true
  • ESM only — all imports use ESM syntax with .js extensions on relative imports
  • import type for type-only imports
  • unknown over any — if any is truly needed, add an ESLint disable comment explaining why
  • Prettier formatting — double quotes, trailing commas, 100 character print width

Testing

LexBuild uses Vitest with co-located test files. A module and its tests share the same directory:

packages/core/src/xml/
├── parser.ts
├── parser.test.ts
├── uslm-elements.ts
└── uslm-elements.test.ts

Write descriptive test names that state the specific behavior under test:

describe("renderSection", () => {
  it("converts <subsection> with chapeau to indented bold-lettered paragraph", () => {
    // ...
  });
});

Run all tests or target a specific package:

pnpm turbo test
pnpm turbo test --filter=@lexbuild/core

To run a single test file in watch mode:

cd packages/usc
pnpm exec vitest src/converter.test.ts

Snapshot tests verify Markdown output stability. Update snapshots intentionally when you change rendering behavior:

cd packages/usc
pnpm exec vitest run --update

Making Changes

  1. Create a branch from main:

    git checkout -b feat/your-feature
  2. Make your changes. Follow the code standards above.

  3. Run the full check suite before submitting:

    pnpm turbo test && pnpm turbo lint && pnpm turbo typecheck
  4. Create a changeset for version bumping. Changesets track which packages changed and what kind of change it was (patch, minor, major):

    pnpm changeset

    Follow the prompts to select affected packages and describe the change.

  5. Commit using Conventional Commits format:

    feat(core): add support for new element type
    fix(cli): handle empty title list gracefully
    chore(ecfr): update test fixtures

    Types: feat, fix, docs, chore, refactor, test, style. Scopes: core, usc, ecfr, fr, cli, astro, api.

  6. Submit a pull request to main.

Package Boundaries

Source packages are independent by design. Each source package (usc, ecfr, fr) depends only on @lexbuild/core and never imports from another source package. This boundary is enforced by ESLint no-restricted-imports rules.

When adding a new source package, add it to the restriction lists of all existing source packages in eslint.config.js.